1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/mozapps/extensions/test/xpcshell/head_addons.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,1563 @@ 1.4 +/* Any copyright is dedicated to the Public Domain. 1.5 + * http://creativecommons.org/publicdomain/zero/1.0/ 1.6 + */ 1.7 + 1.8 +const AM_Cc = Components.classes; 1.9 +const AM_Ci = Components.interfaces; 1.10 + 1.11 +const XULAPPINFO_CONTRACTID = "@mozilla.org/xre/app-info;1"; 1.12 +const XULAPPINFO_CID = Components.ID("{c763b610-9d49-455a-bbd2-ede71682a1ac}"); 1.13 + 1.14 +const PREF_EM_CHECK_UPDATE_SECURITY = "extensions.checkUpdateSecurity"; 1.15 +const PREF_EM_STRICT_COMPATIBILITY = "extensions.strictCompatibility"; 1.16 +const PREF_EM_MIN_COMPAT_APP_VERSION = "extensions.minCompatibleAppVersion"; 1.17 +const PREF_EM_MIN_COMPAT_PLATFORM_VERSION = "extensions.minCompatiblePlatformVersion"; 1.18 +const PREF_GETADDONS_BYIDS = "extensions.getAddons.get.url"; 1.19 +const PREF_GETADDONS_BYIDS_PERFORMANCE = "extensions.getAddons.getWithPerformance.url"; 1.20 + 1.21 +// Forcibly end the test if it runs longer than 15 minutes 1.22 +const TIMEOUT_MS = 900000; 1.23 + 1.24 +Components.utils.import("resource://gre/modules/addons/AddonRepository.jsm"); 1.25 +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); 1.26 +Components.utils.import("resource://gre/modules/FileUtils.jsm"); 1.27 +Components.utils.import("resource://gre/modules/Services.jsm"); 1.28 +Components.utils.import("resource://gre/modules/NetUtil.jsm"); 1.29 +Components.utils.import("resource://gre/modules/Promise.jsm"); 1.30 +Components.utils.import("resource://gre/modules/Task.jsm"); 1.31 +Components.utils.import("resource://gre/modules/osfile.jsm"); 1.32 + 1.33 +Services.prefs.setBoolPref("toolkit.osfile.log", true); 1.34 + 1.35 +// We need some internal bits of AddonManager 1.36 +let AMscope = Components.utils.import("resource://gre/modules/AddonManager.jsm"); 1.37 +let AddonManager = AMscope.AddonManager; 1.38 +let AddonManagerInternal = AMscope.AddonManagerInternal; 1.39 +// Mock out AddonManager's reference to the AsyncShutdown module so we can shut 1.40 +// down AddonManager from the test 1.41 +let MockAsyncShutdown = { 1.42 + hook: null, 1.43 + profileBeforeChange: { 1.44 + addBlocker: function(aName, aBlocker) { 1.45 + do_print("Mock profileBeforeChange blocker for '" + aName + "'"); 1.46 + MockAsyncShutdown.hook = aBlocker; 1.47 + } 1.48 + } 1.49 +}; 1.50 +AMscope.AsyncShutdown = MockAsyncShutdown; 1.51 + 1.52 +var gInternalManager = null; 1.53 +var gAppInfo = null; 1.54 +var gAddonsList; 1.55 + 1.56 +var gPort = null; 1.57 +var gUrlToFileMap = {}; 1.58 + 1.59 +var TEST_UNPACKED = false; 1.60 + 1.61 +function isNightlyChannel() { 1.62 + var channel = "default"; 1.63 + try { 1.64 + channel = Services.prefs.getCharPref("app.update.channel"); 1.65 + } 1.66 + catch (e) { } 1.67 + 1.68 + return channel != "aurora" && channel != "beta" && channel != "release" && channel != "esr"; 1.69 +} 1.70 + 1.71 +function createAppInfo(id, name, version, platformVersion) { 1.72 + gAppInfo = { 1.73 + // nsIXULAppInfo 1.74 + vendor: "Mozilla", 1.75 + name: name, 1.76 + ID: id, 1.77 + version: version, 1.78 + appBuildID: "2007010101", 1.79 + platformVersion: platformVersion ? platformVersion : "1.0", 1.80 + platformBuildID: "2007010101", 1.81 + 1.82 + // nsIXULRuntime 1.83 + inSafeMode: false, 1.84 + logConsoleErrors: true, 1.85 + OS: "XPCShell", 1.86 + XPCOMABI: "noarch-spidermonkey", 1.87 + invalidateCachesOnRestart: function invalidateCachesOnRestart() { 1.88 + // Do nothing 1.89 + }, 1.90 + 1.91 + // nsICrashReporter 1.92 + annotations: {}, 1.93 + 1.94 + annotateCrashReport: function(key, data) { 1.95 + this.annotations[key] = data; 1.96 + }, 1.97 + 1.98 + QueryInterface: XPCOMUtils.generateQI([AM_Ci.nsIXULAppInfo, 1.99 + AM_Ci.nsIXULRuntime, 1.100 + AM_Ci.nsICrashReporter, 1.101 + AM_Ci.nsISupports]) 1.102 + }; 1.103 + 1.104 + var XULAppInfoFactory = { 1.105 + createInstance: function (outer, iid) { 1.106 + if (outer != null) 1.107 + throw Components.results.NS_ERROR_NO_AGGREGATION; 1.108 + return gAppInfo.QueryInterface(iid); 1.109 + } 1.110 + }; 1.111 + var registrar = Components.manager.QueryInterface(AM_Ci.nsIComponentRegistrar); 1.112 + registrar.registerFactory(XULAPPINFO_CID, "XULAppInfo", 1.113 + XULAPPINFO_CONTRACTID, XULAppInfoFactory); 1.114 +} 1.115 + 1.116 +/** 1.117 + * Tests that an add-on does appear in the crash report annotations, if 1.118 + * crash reporting is enabled. The test will fail if the add-on is not in the 1.119 + * annotation. 1.120 + * @param aId 1.121 + * The ID of the add-on 1.122 + * @param aVersion 1.123 + * The version of the add-on 1.124 + */ 1.125 +function do_check_in_crash_annotation(aId, aVersion) { 1.126 + if (!("nsICrashReporter" in AM_Ci)) 1.127 + return; 1.128 + 1.129 + if (!("Add-ons" in gAppInfo.annotations)) { 1.130 + do_check_false(true); 1.131 + return; 1.132 + } 1.133 + 1.134 + let addons = gAppInfo.annotations["Add-ons"].split(","); 1.135 + do_check_false(addons.indexOf(encodeURIComponent(aId) + ":" + 1.136 + encodeURIComponent(aVersion)) < 0); 1.137 +} 1.138 + 1.139 +/** 1.140 + * Tests that an add-on does not appear in the crash report annotations, if 1.141 + * crash reporting is enabled. The test will fail if the add-on is in the 1.142 + * annotation. 1.143 + * @param aId 1.144 + * The ID of the add-on 1.145 + * @param aVersion 1.146 + * The version of the add-on 1.147 + */ 1.148 +function do_check_not_in_crash_annotation(aId, aVersion) { 1.149 + if (!("nsICrashReporter" in AM_Ci)) 1.150 + return; 1.151 + 1.152 + if (!("Add-ons" in gAppInfo.annotations)) { 1.153 + do_check_true(true); 1.154 + return; 1.155 + } 1.156 + 1.157 + let addons = gAppInfo.annotations["Add-ons"].split(","); 1.158 + do_check_true(addons.indexOf(encodeURIComponent(aId) + ":" + 1.159 + encodeURIComponent(aVersion)) < 0); 1.160 +} 1.161 + 1.162 +/** 1.163 + * Returns a testcase xpi 1.164 + * 1.165 + * @param aName 1.166 + * The name of the testcase (without extension) 1.167 + * @return an nsIFile pointing to the testcase xpi 1.168 + */ 1.169 +function do_get_addon(aName) { 1.170 + return do_get_file("addons/" + aName + ".xpi"); 1.171 +} 1.172 + 1.173 +function do_get_addon_hash(aName, aAlgorithm) { 1.174 + if (!aAlgorithm) 1.175 + aAlgorithm = "sha1"; 1.176 + 1.177 + let file = do_get_addon(aName); 1.178 + 1.179 + let crypto = AM_Cc["@mozilla.org/security/hash;1"]. 1.180 + createInstance(AM_Ci.nsICryptoHash); 1.181 + crypto.initWithString(aAlgorithm); 1.182 + let fis = AM_Cc["@mozilla.org/network/file-input-stream;1"]. 1.183 + createInstance(AM_Ci.nsIFileInputStream); 1.184 + fis.init(file, -1, -1, false); 1.185 + crypto.updateFromStream(fis, file.fileSize); 1.186 + 1.187 + // return the two-digit hexadecimal code for a byte 1.188 + function toHexString(charCode) 1.189 + ("0" + charCode.toString(16)).slice(-2); 1.190 + 1.191 + let binary = crypto.finish(false); 1.192 + return aAlgorithm + ":" + [toHexString(binary.charCodeAt(i)) for (i in binary)].join("") 1.193 +} 1.194 + 1.195 +/** 1.196 + * Returns an extension uri spec 1.197 + * 1.198 + * @param aProfileDir 1.199 + * The extension install directory 1.200 + * @return a uri spec pointing to the root of the extension 1.201 + */ 1.202 +function do_get_addon_root_uri(aProfileDir, aId) { 1.203 + let path = aProfileDir.clone(); 1.204 + path.append(aId); 1.205 + if (!path.exists()) { 1.206 + path.leafName += ".xpi"; 1.207 + return "jar:" + Services.io.newFileURI(path).spec + "!/"; 1.208 + } 1.209 + else { 1.210 + return Services.io.newFileURI(path).spec; 1.211 + } 1.212 +} 1.213 + 1.214 +function do_get_expected_addon_name(aId) { 1.215 + if (TEST_UNPACKED) 1.216 + return aId; 1.217 + return aId + ".xpi"; 1.218 +} 1.219 + 1.220 +/** 1.221 + * Check that an array of actual add-ons is the same as an array of 1.222 + * expected add-ons. 1.223 + * 1.224 + * @param aActualAddons 1.225 + * The array of actual add-ons to check. 1.226 + * @param aExpectedAddons 1.227 + * The array of expected add-ons to check against. 1.228 + * @param aProperties 1.229 + * An array of properties to check. 1.230 + */ 1.231 +function do_check_addons(aActualAddons, aExpectedAddons, aProperties) { 1.232 + do_check_neq(aActualAddons, null); 1.233 + do_check_eq(aActualAddons.length, aExpectedAddons.length); 1.234 + for (let i = 0; i < aActualAddons.length; i++) 1.235 + do_check_addon(aActualAddons[i], aExpectedAddons[i], aProperties); 1.236 +} 1.237 + 1.238 +/** 1.239 + * Check that the actual add-on is the same as the expected add-on. 1.240 + * 1.241 + * @param aActualAddon 1.242 + * The actual add-on to check. 1.243 + * @param aExpectedAddon 1.244 + * The expected add-on to check against. 1.245 + * @param aProperties 1.246 + * An array of properties to check. 1.247 + */ 1.248 +function do_check_addon(aActualAddon, aExpectedAddon, aProperties) { 1.249 + do_check_neq(aActualAddon, null); 1.250 + 1.251 + aProperties.forEach(function(aProperty) { 1.252 + let actualValue = aActualAddon[aProperty]; 1.253 + let expectedValue = aExpectedAddon[aProperty]; 1.254 + 1.255 + // Check that all undefined expected properties are null on actual add-on 1.256 + if (!(aProperty in aExpectedAddon)) { 1.257 + if (actualValue !== undefined && actualValue !== null) { 1.258 + do_throw("Unexpected defined/non-null property for add-on " + 1.259 + aExpectedAddon.id + " (addon[" + aProperty + "] = " + 1.260 + actualValue.toSource() + ")"); 1.261 + } 1.262 + 1.263 + return; 1.264 + } 1.265 + else if (expectedValue && !actualValue) { 1.266 + do_throw("Missing property for add-on " + aExpectedAddon.id + 1.267 + ": expected addon[" + aProperty + "] = " + expectedValue); 1.268 + return; 1.269 + } 1.270 + 1.271 + switch (aProperty) { 1.272 + case "creator": 1.273 + do_check_author(actualValue, expectedValue); 1.274 + break; 1.275 + 1.276 + case "developers": 1.277 + case "translators": 1.278 + case "contributors": 1.279 + do_check_eq(actualValue.length, expectedValue.length); 1.280 + for (let i = 0; i < actualValue.length; i++) 1.281 + do_check_author(actualValue[i], expectedValue[i]); 1.282 + break; 1.283 + 1.284 + case "screenshots": 1.285 + do_check_eq(actualValue.length, expectedValue.length); 1.286 + for (let i = 0; i < actualValue.length; i++) 1.287 + do_check_screenshot(actualValue[i], expectedValue[i]); 1.288 + break; 1.289 + 1.290 + case "sourceURI": 1.291 + do_check_eq(actualValue.spec, expectedValue); 1.292 + break; 1.293 + 1.294 + case "updateDate": 1.295 + do_check_eq(actualValue.getTime(), expectedValue.getTime()); 1.296 + break; 1.297 + 1.298 + case "compatibilityOverrides": 1.299 + do_check_eq(actualValue.length, expectedValue.length); 1.300 + for (let i = 0; i < actualValue.length; i++) 1.301 + do_check_compatibilityoverride(actualValue[i], expectedValue[i]); 1.302 + break; 1.303 + 1.304 + case "icons": 1.305 + do_check_icons(actualValue, expectedValue); 1.306 + break; 1.307 + 1.308 + default: 1.309 + if (remove_port(actualValue) !== remove_port(expectedValue)) 1.310 + do_throw("Failed for " + aProperty + " for add-on " + aExpectedAddon.id + 1.311 + " (" + actualValue + " === " + expectedValue + ")"); 1.312 + } 1.313 + }); 1.314 +} 1.315 + 1.316 +/** 1.317 + * Check that the actual author is the same as the expected author. 1.318 + * 1.319 + * @param aActual 1.320 + * The actual author to check. 1.321 + * @param aExpected 1.322 + * The expected author to check against. 1.323 + */ 1.324 +function do_check_author(aActual, aExpected) { 1.325 + do_check_eq(aActual.toString(), aExpected.name); 1.326 + do_check_eq(aActual.name, aExpected.name); 1.327 + do_check_eq(aActual.url, aExpected.url); 1.328 +} 1.329 + 1.330 +/** 1.331 + * Check that the actual screenshot is the same as the expected screenshot. 1.332 + * 1.333 + * @param aActual 1.334 + * The actual screenshot to check. 1.335 + * @param aExpected 1.336 + * The expected screenshot to check against. 1.337 + */ 1.338 +function do_check_screenshot(aActual, aExpected) { 1.339 + do_check_eq(aActual.toString(), aExpected.url); 1.340 + do_check_eq(aActual.url, aExpected.url); 1.341 + do_check_eq(aActual.width, aExpected.width); 1.342 + do_check_eq(aActual.height, aExpected.height); 1.343 + do_check_eq(aActual.thumbnailURL, aExpected.thumbnailURL); 1.344 + do_check_eq(aActual.thumbnailWidth, aExpected.thumbnailWidth); 1.345 + do_check_eq(aActual.thumbnailHeight, aExpected.thumbnailHeight); 1.346 + do_check_eq(aActual.caption, aExpected.caption); 1.347 +} 1.348 + 1.349 +/** 1.350 + * Check that the actual compatibility override is the same as the expected 1.351 + * compatibility override. 1.352 + * 1.353 + * @param aAction 1.354 + * The actual compatibility override to check. 1.355 + * @param aExpected 1.356 + * The expected compatibility override to check against. 1.357 + */ 1.358 +function do_check_compatibilityoverride(aActual, aExpected) { 1.359 + do_check_eq(aActual.type, aExpected.type); 1.360 + do_check_eq(aActual.minVersion, aExpected.minVersion); 1.361 + do_check_eq(aActual.maxVersion, aExpected.maxVersion); 1.362 + do_check_eq(aActual.appID, aExpected.appID); 1.363 + do_check_eq(aActual.appMinVersion, aExpected.appMinVersion); 1.364 + do_check_eq(aActual.appMaxVersion, aExpected.appMaxVersion); 1.365 +} 1.366 + 1.367 +function do_check_icons(aActual, aExpected) { 1.368 + for (var size in aExpected) { 1.369 + do_check_eq(remove_port(aActual[size]), remove_port(aExpected[size])); 1.370 + } 1.371 +} 1.372 + 1.373 +// Record the error (if any) from trying to save the XPI 1.374 +// database at shutdown time 1.375 +let gXPISaveError = null; 1.376 + 1.377 +/** 1.378 + * Starts up the add-on manager as if it was started by the application. 1.379 + * 1.380 + * @param aAppChanged 1.381 + * An optional boolean parameter to simulate the case where the 1.382 + * application has changed version since the last run. If not passed it 1.383 + * defaults to true 1.384 + */ 1.385 +function startupManager(aAppChanged) { 1.386 + if (gInternalManager) 1.387 + do_throw("Test attempt to startup manager that was already started."); 1.388 + 1.389 + if (aAppChanged || aAppChanged === undefined) { 1.390 + if (gExtensionsINI.exists()) 1.391 + gExtensionsINI.remove(true); 1.392 + } 1.393 + 1.394 + gInternalManager = AM_Cc["@mozilla.org/addons/integration;1"]. 1.395 + getService(AM_Ci.nsIObserver). 1.396 + QueryInterface(AM_Ci.nsITimerCallback); 1.397 + 1.398 + gInternalManager.observe(null, "addons-startup", null); 1.399 + 1.400 + // Load the add-ons list as it was after extension registration 1.401 + loadAddonsList(); 1.402 +} 1.403 + 1.404 +/** 1.405 + * Helper to spin the event loop until a promise resolves or rejects 1.406 + */ 1.407 +function loopUntilPromise(aPromise) { 1.408 + let done = false; 1.409 + aPromise.then( 1.410 + () => done = true, 1.411 + err => { 1.412 + do_report_unexpected_exception(err); 1.413 + done = true; 1.414 + }); 1.415 + 1.416 + let thr = Services.tm.mainThread; 1.417 + 1.418 + while (!done) { 1.419 + thr.processNextEvent(true); 1.420 + } 1.421 +} 1.422 + 1.423 +/** 1.424 + * Restarts the add-on manager as if the host application was restarted. 1.425 + * 1.426 + * @param aNewVersion 1.427 + * An optional new version to use for the application. Passing this 1.428 + * will change nsIXULAppInfo.version and make the startup appear as if 1.429 + * the application version has changed. 1.430 + */ 1.431 +function restartManager(aNewVersion) { 1.432 + loopUntilPromise(promiseRestartManager(aNewVersion)); 1.433 +} 1.434 + 1.435 +function promiseRestartManager(aNewVersion) { 1.436 + return promiseShutdownManager() 1.437 + .then(null, err => do_report_unexpected_exception(err)) 1.438 + .then(() => { 1.439 + if (aNewVersion) { 1.440 + gAppInfo.version = aNewVersion; 1.441 + startupManager(true); 1.442 + } 1.443 + else { 1.444 + startupManager(false); 1.445 + } 1.446 + }); 1.447 +} 1.448 + 1.449 +function shutdownManager() { 1.450 + loopUntilPromise(promiseShutdownManager()); 1.451 +} 1.452 + 1.453 +function promiseShutdownManager() { 1.454 + if (!gInternalManager) { 1.455 + return Promise.resolve(false); 1.456 + } 1.457 + 1.458 + let hookErr = null; 1.459 + Services.obs.notifyObservers(null, "quit-application-granted", null); 1.460 + return MockAsyncShutdown.hook() 1.461 + .then(null, err => hookErr = err) 1.462 + .then( () => { 1.463 + gInternalManager = null; 1.464 + 1.465 + // Load the add-ons list as it was after application shutdown 1.466 + loadAddonsList(); 1.467 + 1.468 + // Clear any crash report annotations 1.469 + gAppInfo.annotations = {}; 1.470 + 1.471 + // Force the XPIProvider provider to reload to better 1.472 + // simulate real-world usage. 1.473 + let XPIscope = Components.utils.import("resource://gre/modules/addons/XPIProvider.jsm"); 1.474 + // This would be cleaner if I could get it as the rejection reason from 1.475 + // the AddonManagerInternal.shutdown() promise 1.476 + gXPISaveError = XPIscope.XPIProvider._shutdownError; 1.477 + do_print("gXPISaveError set to: " + gXPISaveError); 1.478 + AddonManagerPrivate.unregisterProvider(XPIscope.XPIProvider); 1.479 + Components.utils.unload("resource://gre/modules/addons/XPIProvider.jsm"); 1.480 + if (hookErr) { 1.481 + throw hookErr; 1.482 + } 1.483 + }); 1.484 +} 1.485 + 1.486 +function loadAddonsList() { 1.487 + function readDirectories(aSection) { 1.488 + var dirs = []; 1.489 + var keys = parser.getKeys(aSection); 1.490 + while (keys.hasMore()) { 1.491 + let descriptor = parser.getString(aSection, keys.getNext()); 1.492 + try { 1.493 + let file = AM_Cc["@mozilla.org/file/local;1"]. 1.494 + createInstance(AM_Ci.nsIFile); 1.495 + file.persistentDescriptor = descriptor; 1.496 + dirs.push(file); 1.497 + } 1.498 + catch (e) { 1.499 + // Throws if the directory doesn't exist, we can ignore this since the 1.500 + // platform will too. 1.501 + } 1.502 + } 1.503 + return dirs; 1.504 + } 1.505 + 1.506 + gAddonsList = { 1.507 + extensions: [], 1.508 + themes: [] 1.509 + }; 1.510 + 1.511 + if (!gExtensionsINI.exists()) 1.512 + return; 1.513 + 1.514 + var factory = AM_Cc["@mozilla.org/xpcom/ini-parser-factory;1"]. 1.515 + getService(AM_Ci.nsIINIParserFactory); 1.516 + var parser = factory.createINIParser(gExtensionsINI); 1.517 + gAddonsList.extensions = readDirectories("ExtensionDirs"); 1.518 + gAddonsList.themes = readDirectories("ThemeDirs"); 1.519 +} 1.520 + 1.521 +function isItemInAddonsList(aType, aDir, aId) { 1.522 + var path = aDir.clone(); 1.523 + path.append(aId); 1.524 + var xpiPath = aDir.clone(); 1.525 + xpiPath.append(aId + ".xpi"); 1.526 + for (var i = 0; i < gAddonsList[aType].length; i++) { 1.527 + let file = gAddonsList[aType][i]; 1.528 + if (!file.exists()) 1.529 + do_throw("Non-existant path found in extensions.ini: " + file.path) 1.530 + if (file.isDirectory() && file.equals(path)) 1.531 + return true; 1.532 + if (file.isFile() && file.equals(xpiPath)) 1.533 + return true; 1.534 + } 1.535 + return false; 1.536 +} 1.537 + 1.538 +function isThemeInAddonsList(aDir, aId) { 1.539 + return isItemInAddonsList("themes", aDir, aId); 1.540 +} 1.541 + 1.542 +function isExtensionInAddonsList(aDir, aId) { 1.543 + return isItemInAddonsList("extensions", aDir, aId); 1.544 +} 1.545 + 1.546 +function check_startup_changes(aType, aIds) { 1.547 + var ids = aIds.slice(0); 1.548 + ids.sort(); 1.549 + var changes = AddonManager.getStartupChanges(aType); 1.550 + changes = changes.filter(function(aEl) /@tests.mozilla.org$/.test(aEl)); 1.551 + changes.sort(); 1.552 + 1.553 + do_check_eq(JSON.stringify(ids), JSON.stringify(changes)); 1.554 +} 1.555 + 1.556 +/** 1.557 + * Escapes any occurances of &, ", < or > with XML entities. 1.558 + * 1.559 + * @param str 1.560 + * The string to escape 1.561 + * @return The escaped string 1.562 + */ 1.563 +function escapeXML(aStr) { 1.564 + return aStr.toString() 1.565 + .replace(/&/g, "&") 1.566 + .replace(/"/g, """) 1.567 + .replace(/</g, "<") 1.568 + .replace(/>/g, ">"); 1.569 +} 1.570 + 1.571 +function writeLocaleStrings(aData) { 1.572 + let rdf = ""; 1.573 + ["name", "description", "creator", "homepageURL"].forEach(function(aProp) { 1.574 + if (aProp in aData) 1.575 + rdf += "<em:" + aProp + ">" + escapeXML(aData[aProp]) + "</em:" + aProp + ">\n"; 1.576 + }); 1.577 + 1.578 + ["developer", "translator", "contributor"].forEach(function(aProp) { 1.579 + if (aProp in aData) { 1.580 + aData[aProp].forEach(function(aValue) { 1.581 + rdf += "<em:" + aProp + ">" + escapeXML(aValue) + "</em:" + aProp + ">\n"; 1.582 + }); 1.583 + } 1.584 + }); 1.585 + return rdf; 1.586 +} 1.587 + 1.588 +function createInstallRDF(aData) { 1.589 + var rdf = '<?xml version="1.0"?>\n'; 1.590 + rdf += '<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"\n' + 1.591 + ' xmlns:em="http://www.mozilla.org/2004/em-rdf#">\n'; 1.592 + rdf += '<Description about="urn:mozilla:install-manifest">\n'; 1.593 + 1.594 + ["id", "version", "type", "internalName", "updateURL", "updateKey", 1.595 + "optionsURL", "optionsType", "aboutURL", "iconURL", "icon64URL", 1.596 + "skinnable", "bootstrap", "strictCompatibility"].forEach(function(aProp) { 1.597 + if (aProp in aData) 1.598 + rdf += "<em:" + aProp + ">" + escapeXML(aData[aProp]) + "</em:" + aProp + ">\n"; 1.599 + }); 1.600 + 1.601 + rdf += writeLocaleStrings(aData); 1.602 + 1.603 + if ("targetPlatforms" in aData) { 1.604 + aData.targetPlatforms.forEach(function(aPlatform) { 1.605 + rdf += "<em:targetPlatform>" + escapeXML(aPlatform) + "</em:targetPlatform>\n"; 1.606 + }); 1.607 + } 1.608 + 1.609 + if ("targetApplications" in aData) { 1.610 + aData.targetApplications.forEach(function(aApp) { 1.611 + rdf += "<em:targetApplication><Description>\n"; 1.612 + ["id", "minVersion", "maxVersion"].forEach(function(aProp) { 1.613 + if (aProp in aApp) 1.614 + rdf += "<em:" + aProp + ">" + escapeXML(aApp[aProp]) + "</em:" + aProp + ">\n"; 1.615 + }); 1.616 + rdf += "</Description></em:targetApplication>\n"; 1.617 + }); 1.618 + } 1.619 + 1.620 + if ("localized" in aData) { 1.621 + aData.localized.forEach(function(aLocalized) { 1.622 + rdf += "<em:localized><Description>\n"; 1.623 + if ("locale" in aLocalized) { 1.624 + aLocalized.locale.forEach(function(aLocaleName) { 1.625 + rdf += "<em:locale>" + escapeXML(aLocaleName) + "</em:locale>\n"; 1.626 + }); 1.627 + } 1.628 + rdf += writeLocaleStrings(aLocalized); 1.629 + rdf += "</Description></em:localized>\n"; 1.630 + }); 1.631 + } 1.632 + 1.633 + rdf += "</Description>\n</RDF>\n"; 1.634 + return rdf; 1.635 +} 1.636 + 1.637 +/** 1.638 + * Writes an install.rdf manifest into a directory using the properties passed 1.639 + * in a JS object. The objects should contain a property for each property to 1.640 + * appear in the RDFThe object may contain an array of objects with id, 1.641 + * minVersion and maxVersion in the targetApplications property to give target 1.642 + * application compatibility. 1.643 + * 1.644 + * @param aData 1.645 + * The object holding data about the add-on 1.646 + * @param aDir 1.647 + * The directory to add the install.rdf to 1.648 + * @param aExtraFile 1.649 + * An optional dummy file to create in the directory 1.650 + */ 1.651 +function writeInstallRDFToDir(aData, aDir, aExtraFile) { 1.652 + var rdf = createInstallRDF(aData); 1.653 + if (!aDir.exists()) 1.654 + aDir.create(AM_Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); 1.655 + var file = aDir.clone(); 1.656 + file.append("install.rdf"); 1.657 + if (file.exists()) 1.658 + file.remove(true); 1.659 + var fos = AM_Cc["@mozilla.org/network/file-output-stream;1"]. 1.660 + createInstance(AM_Ci.nsIFileOutputStream); 1.661 + fos.init(file, 1.662 + FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | FileUtils.MODE_TRUNCATE, 1.663 + FileUtils.PERMS_FILE, 0); 1.664 + fos.write(rdf, rdf.length); 1.665 + fos.close(); 1.666 + 1.667 + if (!aExtraFile) 1.668 + return; 1.669 + 1.670 + file = aDir.clone(); 1.671 + file.append(aExtraFile); 1.672 + file.create(AM_Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE); 1.673 +} 1.674 + 1.675 +/** 1.676 + * Writes an install.rdf manifest into an extension using the properties passed 1.677 + * in a JS object. The objects should contain a property for each property to 1.678 + * appear in the RDFThe object may contain an array of objects with id, 1.679 + * minVersion and maxVersion in the targetApplications property to give target 1.680 + * application compatibility. 1.681 + * 1.682 + * @param aData 1.683 + * The object holding data about the add-on 1.684 + * @param aDir 1.685 + * The install directory to add the extension to 1.686 + * @param aId 1.687 + * An optional string to override the default installation aId 1.688 + * @param aExtraFile 1.689 + * An optional dummy file to create in the extension 1.690 + * @return A file pointing to where the extension was installed 1.691 + */ 1.692 +function writeInstallRDFForExtension(aData, aDir, aId, aExtraFile) { 1.693 + var id = aId ? aId : aData.id 1.694 + 1.695 + var dir = aDir.clone(); 1.696 + 1.697 + if (TEST_UNPACKED) { 1.698 + dir.append(id); 1.699 + writeInstallRDFToDir(aData, dir, aExtraFile); 1.700 + return dir; 1.701 + } 1.702 + 1.703 + if (!dir.exists()) 1.704 + dir.create(AM_Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); 1.705 + dir.append(id + ".xpi"); 1.706 + var rdf = createInstallRDF(aData); 1.707 + var stream = AM_Cc["@mozilla.org/io/string-input-stream;1"]. 1.708 + createInstance(AM_Ci.nsIStringInputStream); 1.709 + stream.setData(rdf, -1); 1.710 + var zipW = AM_Cc["@mozilla.org/zipwriter;1"]. 1.711 + createInstance(AM_Ci.nsIZipWriter); 1.712 + zipW.open(dir, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | FileUtils.MODE_TRUNCATE); 1.713 + zipW.addEntryStream("install.rdf", 0, AM_Ci.nsIZipWriter.COMPRESSION_NONE, 1.714 + stream, false); 1.715 + if (aExtraFile) 1.716 + zipW.addEntryStream(aExtraFile, 0, AM_Ci.nsIZipWriter.COMPRESSION_NONE, 1.717 + stream, false); 1.718 + zipW.close(); 1.719 + return dir; 1.720 +} 1.721 + 1.722 +/** 1.723 + * Sets the last modified time of the extension, usually to trigger an update 1.724 + * of its metadata. If the extension is unpacked, this function assumes that 1.725 + * the extension contains only the install.rdf file. 1.726 + * 1.727 + * @param aExt a file pointing to either the packed extension or its unpacked directory. 1.728 + * @param aTime the time to which we set the lastModifiedTime of the extension 1.729 + * 1.730 + * @deprecated Please use promiseSetExtensionModifiedTime instead 1.731 + */ 1.732 +function setExtensionModifiedTime(aExt, aTime) { 1.733 + aExt.lastModifiedTime = aTime; 1.734 + if (aExt.isDirectory()) { 1.735 + let entries = aExt.directoryEntries 1.736 + .QueryInterface(AM_Ci.nsIDirectoryEnumerator); 1.737 + while (entries.hasMoreElements()) 1.738 + setExtensionModifiedTime(entries.nextFile, aTime); 1.739 + entries.close(); 1.740 + } 1.741 +} 1.742 +function promiseSetExtensionModifiedTime(aPath, aTime) { 1.743 + return Task.spawn(function* () { 1.744 + yield OS.File.setDates(aPath, aTime, aTime); 1.745 + let entries, iterator; 1.746 + try { 1.747 + let iterator = new OS.File.DirectoryIterator(aPath); 1.748 + entries = yield iterator.nextBatch(); 1.749 + } catch (ex if ex instanceof OS.File.Error) { 1.750 + return; 1.751 + } finally { 1.752 + if (iterator) { 1.753 + iterator.close(); 1.754 + } 1.755 + } 1.756 + for (let entry of entries) { 1.757 + yield promiseSetExtensionModifiedTime(entry.path, aTime); 1.758 + } 1.759 + }); 1.760 +} 1.761 + 1.762 +/** 1.763 + * Manually installs an XPI file into an install location by either copying the 1.764 + * XPI there or extracting it depending on whether unpacking is being tested 1.765 + * or not. 1.766 + * 1.767 + * @param aXPIFile 1.768 + * The XPI file to install. 1.769 + * @param aInstallLocation 1.770 + * The install location (an nsIFile) to install into. 1.771 + * @param aID 1.772 + * The ID to install as. 1.773 + */ 1.774 +function manuallyInstall(aXPIFile, aInstallLocation, aID) { 1.775 + if (TEST_UNPACKED) { 1.776 + let dir = aInstallLocation.clone(); 1.777 + dir.append(aID); 1.778 + dir.create(AM_Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); 1.779 + let zip = AM_Cc["@mozilla.org/libjar/zip-reader;1"]. 1.780 + createInstance(AM_Ci.nsIZipReader); 1.781 + zip.open(aXPIFile); 1.782 + let entries = zip.findEntries(null); 1.783 + while (entries.hasMore()) { 1.784 + let entry = entries.getNext(); 1.785 + let target = dir.clone(); 1.786 + entry.split("/").forEach(function(aPart) { 1.787 + target.append(aPart); 1.788 + }); 1.789 + zip.extract(entry, target); 1.790 + } 1.791 + zip.close(); 1.792 + 1.793 + return dir; 1.794 + } 1.795 + else { 1.796 + let target = aInstallLocation.clone(); 1.797 + target.append(aID + ".xpi"); 1.798 + aXPIFile.copyTo(target.parent, target.leafName); 1.799 + return target; 1.800 + } 1.801 +} 1.802 + 1.803 +/** 1.804 + * Manually uninstalls an add-on by removing its files from the install 1.805 + * location. 1.806 + * 1.807 + * @param aInstallLocation 1.808 + * The nsIFile of the install location to remove from. 1.809 + * @param aID 1.810 + * The ID of the add-on to remove. 1.811 + */ 1.812 +function manuallyUninstall(aInstallLocation, aID) { 1.813 + let file = getFileForAddon(aInstallLocation, aID); 1.814 + 1.815 + // In reality because the app is restarted a flush isn't necessary for XPIs 1.816 + // removed outside the app, but for testing we must flush manually. 1.817 + if (file.isFile()) 1.818 + Services.obs.notifyObservers(file, "flush-cache-entry", null); 1.819 + 1.820 + file.remove(true); 1.821 +} 1.822 + 1.823 +/** 1.824 + * Gets the nsIFile for where an add-on is installed. It may point to a file or 1.825 + * a directory depending on whether add-ons are being installed unpacked or not. 1.826 + * 1.827 + * @param aDir 1.828 + * The nsIFile for the install location 1.829 + * @param aId 1.830 + * The ID of the add-on 1.831 + * @return an nsIFile 1.832 + */ 1.833 +function getFileForAddon(aDir, aId) { 1.834 + var dir = aDir.clone(); 1.835 + dir.append(do_get_expected_addon_name(aId)); 1.836 + return dir; 1.837 +} 1.838 + 1.839 +function registerDirectory(aKey, aDir) { 1.840 + var dirProvider = { 1.841 + getFile: function(aProp, aPersistent) { 1.842 + aPersistent.value = true; 1.843 + if (aProp == aKey) 1.844 + return aDir.clone(); 1.845 + return null; 1.846 + }, 1.847 + 1.848 + QueryInterface: XPCOMUtils.generateQI([AM_Ci.nsIDirectoryServiceProvider, 1.849 + AM_Ci.nsISupports]) 1.850 + }; 1.851 + Services.dirsvc.registerProvider(dirProvider); 1.852 +} 1.853 + 1.854 +var gExpectedEvents = {}; 1.855 +var gExpectedInstalls = []; 1.856 +var gNext = null; 1.857 + 1.858 +function getExpectedEvent(aId) { 1.859 + if (!(aId in gExpectedEvents)) 1.860 + do_throw("Wasn't expecting events for " + aId); 1.861 + if (gExpectedEvents[aId].length == 0) 1.862 + do_throw("Too many events for " + aId); 1.863 + let event = gExpectedEvents[aId].shift(); 1.864 + if (event instanceof Array) 1.865 + return event; 1.866 + return [event, true]; 1.867 +} 1.868 + 1.869 +function getExpectedInstall(aAddon) { 1.870 + if (gExpectedInstalls instanceof Array) 1.871 + return gExpectedInstalls.shift(); 1.872 + if (!aAddon || !aAddon.id) 1.873 + return gExpectedInstalls["NO_ID"].shift(); 1.874 + let id = aAddon.id; 1.875 + if (!(id in gExpectedInstalls) || !(gExpectedInstalls[id] instanceof Array)) 1.876 + do_throw("Wasn't expecting events for " + id); 1.877 + if (gExpectedInstalls[id].length == 0) 1.878 + do_throw("Too many events for " + id); 1.879 + return gExpectedInstalls[id].shift(); 1.880 +} 1.881 + 1.882 +const AddonListener = { 1.883 + onPropertyChanged: function(aAddon, aProperties) { 1.884 + let [event, properties] = getExpectedEvent(aAddon.id); 1.885 + do_check_eq("onPropertyChanged", event); 1.886 + do_check_eq(aProperties.length, properties.length); 1.887 + properties.forEach(function(aProperty) { 1.888 + // Only test that the expected properties are listed, having additional 1.889 + // properties listed is not necessary a problem 1.890 + if (aProperties.indexOf(aProperty) == -1) 1.891 + do_throw("Did not see property change for " + aProperty); 1.892 + }); 1.893 + return check_test_completed(arguments); 1.894 + }, 1.895 + 1.896 + onEnabling: function(aAddon, aRequiresRestart) { 1.897 + let [event, expectedRestart] = getExpectedEvent(aAddon.id); 1.898 + do_check_eq("onEnabling", event); 1.899 + do_check_eq(aRequiresRestart, expectedRestart); 1.900 + if (expectedRestart) 1.901 + do_check_true(hasFlag(aAddon.pendingOperations, AddonManager.PENDING_ENABLE)); 1.902 + do_check_false(hasFlag(aAddon.permissions, AddonManager.PERM_CAN_ENABLE)); 1.903 + return check_test_completed(arguments); 1.904 + }, 1.905 + 1.906 + onEnabled: function(aAddon) { 1.907 + let [event, expectedRestart] = getExpectedEvent(aAddon.id); 1.908 + do_check_eq("onEnabled", event); 1.909 + do_check_false(hasFlag(aAddon.permissions, AddonManager.PERM_CAN_ENABLE)); 1.910 + return check_test_completed(arguments); 1.911 + }, 1.912 + 1.913 + onDisabling: function(aAddon, aRequiresRestart) { 1.914 + let [event, expectedRestart] = getExpectedEvent(aAddon.id); 1.915 + do_check_eq("onDisabling", event); 1.916 + do_check_eq(aRequiresRestart, expectedRestart); 1.917 + if (expectedRestart) 1.918 + do_check_true(hasFlag(aAddon.pendingOperations, AddonManager.PENDING_DISABLE)); 1.919 + do_check_false(hasFlag(aAddon.permissions, AddonManager.PERM_CAN_DISABLE)); 1.920 + return check_test_completed(arguments); 1.921 + }, 1.922 + 1.923 + onDisabled: function(aAddon) { 1.924 + let [event, expectedRestart] = getExpectedEvent(aAddon.id); 1.925 + do_check_eq("onDisabled", event); 1.926 + do_check_false(hasFlag(aAddon.permissions, AddonManager.PERM_CAN_DISABLE)); 1.927 + return check_test_completed(arguments); 1.928 + }, 1.929 + 1.930 + onInstalling: function(aAddon, aRequiresRestart) { 1.931 + let [event, expectedRestart] = getExpectedEvent(aAddon.id); 1.932 + do_check_eq("onInstalling", event); 1.933 + do_check_eq(aRequiresRestart, expectedRestart); 1.934 + if (expectedRestart) 1.935 + do_check_true(hasFlag(aAddon.pendingOperations, AddonManager.PENDING_INSTALL)); 1.936 + return check_test_completed(arguments); 1.937 + }, 1.938 + 1.939 + onInstalled: function(aAddon) { 1.940 + let [event, expectedRestart] = getExpectedEvent(aAddon.id); 1.941 + do_check_eq("onInstalled", event); 1.942 + return check_test_completed(arguments); 1.943 + }, 1.944 + 1.945 + onUninstalling: function(aAddon, aRequiresRestart) { 1.946 + let [event, expectedRestart] = getExpectedEvent(aAddon.id); 1.947 + do_check_eq("onUninstalling", event); 1.948 + do_check_eq(aRequiresRestart, expectedRestart); 1.949 + if (expectedRestart) 1.950 + do_check_true(hasFlag(aAddon.pendingOperations, AddonManager.PENDING_UNINSTALL)); 1.951 + return check_test_completed(arguments); 1.952 + }, 1.953 + 1.954 + onUninstalled: function(aAddon) { 1.955 + let [event, expectedRestart] = getExpectedEvent(aAddon.id); 1.956 + do_check_eq("onUninstalled", event); 1.957 + return check_test_completed(arguments); 1.958 + }, 1.959 + 1.960 + onOperationCancelled: function(aAddon) { 1.961 + let [event, expectedRestart] = getExpectedEvent(aAddon.id); 1.962 + do_check_eq("onOperationCancelled", event); 1.963 + return check_test_completed(arguments); 1.964 + } 1.965 +}; 1.966 + 1.967 +const InstallListener = { 1.968 + onNewInstall: function(install) { 1.969 + if (install.state != AddonManager.STATE_DOWNLOADED && 1.970 + install.state != AddonManager.STATE_AVAILABLE) 1.971 + do_throw("Bad install state " + install.state); 1.972 + do_check_eq(install.error, 0); 1.973 + do_check_eq("onNewInstall", getExpectedInstall()); 1.974 + return check_test_completed(arguments); 1.975 + }, 1.976 + 1.977 + onDownloadStarted: function(install) { 1.978 + do_check_eq(install.state, AddonManager.STATE_DOWNLOADING); 1.979 + do_check_eq(install.error, 0); 1.980 + do_check_eq("onDownloadStarted", getExpectedInstall()); 1.981 + return check_test_completed(arguments); 1.982 + }, 1.983 + 1.984 + onDownloadEnded: function(install) { 1.985 + do_check_eq(install.state, AddonManager.STATE_DOWNLOADED); 1.986 + do_check_eq(install.error, 0); 1.987 + do_check_eq("onDownloadEnded", getExpectedInstall()); 1.988 + return check_test_completed(arguments); 1.989 + }, 1.990 + 1.991 + onDownloadFailed: function(install) { 1.992 + do_check_eq(install.state, AddonManager.STATE_DOWNLOAD_FAILED); 1.993 + do_check_eq("onDownloadFailed", getExpectedInstall()); 1.994 + return check_test_completed(arguments); 1.995 + }, 1.996 + 1.997 + onDownloadCancelled: function(install) { 1.998 + do_check_eq(install.state, AddonManager.STATE_CANCELLED); 1.999 + do_check_eq(install.error, 0); 1.1000 + do_check_eq("onDownloadCancelled", getExpectedInstall()); 1.1001 + return check_test_completed(arguments); 1.1002 + }, 1.1003 + 1.1004 + onInstallStarted: function(install) { 1.1005 + do_check_eq(install.state, AddonManager.STATE_INSTALLING); 1.1006 + do_check_eq(install.error, 0); 1.1007 + do_check_eq("onInstallStarted", getExpectedInstall(install.addon)); 1.1008 + return check_test_completed(arguments); 1.1009 + }, 1.1010 + 1.1011 + onInstallEnded: function(install, newAddon) { 1.1012 + do_check_eq(install.state, AddonManager.STATE_INSTALLED); 1.1013 + do_check_eq(install.error, 0); 1.1014 + do_check_eq("onInstallEnded", getExpectedInstall(install.addon)); 1.1015 + return check_test_completed(arguments); 1.1016 + }, 1.1017 + 1.1018 + onInstallFailed: function(install) { 1.1019 + do_check_eq(install.state, AddonManager.STATE_INSTALL_FAILED); 1.1020 + do_check_eq("onInstallFailed", getExpectedInstall(install.addon)); 1.1021 + return check_test_completed(arguments); 1.1022 + }, 1.1023 + 1.1024 + onInstallCancelled: function(install) { 1.1025 + // If the install was cancelled by a listener returning false from 1.1026 + // onInstallStarted, then the state will revert to STATE_DOWNLOADED. 1.1027 + let possibleStates = [AddonManager.STATE_CANCELLED, 1.1028 + AddonManager.STATE_DOWNLOADED]; 1.1029 + do_check_true(possibleStates.indexOf(install.state) != -1); 1.1030 + do_check_eq(install.error, 0); 1.1031 + do_check_eq("onInstallCancelled", getExpectedInstall(install.addon)); 1.1032 + return check_test_completed(arguments); 1.1033 + }, 1.1034 + 1.1035 + onExternalInstall: function(aAddon, existingAddon, aRequiresRestart) { 1.1036 + do_check_eq("onExternalInstall", getExpectedInstall(aAddon)); 1.1037 + do_check_false(aRequiresRestart); 1.1038 + return check_test_completed(arguments); 1.1039 + } 1.1040 +}; 1.1041 + 1.1042 +function hasFlag(aBits, aFlag) { 1.1043 + return (aBits & aFlag) != 0; 1.1044 +} 1.1045 + 1.1046 +// Just a wrapper around setting the expected events 1.1047 +function prepare_test(aExpectedEvents, aExpectedInstalls, aNext) { 1.1048 + AddonManager.addAddonListener(AddonListener); 1.1049 + AddonManager.addInstallListener(InstallListener); 1.1050 + 1.1051 + gExpectedInstalls = aExpectedInstalls; 1.1052 + gExpectedEvents = aExpectedEvents; 1.1053 + gNext = aNext; 1.1054 +} 1.1055 + 1.1056 +// Checks if all expected events have been seen and if so calls the callback 1.1057 +function check_test_completed(aArgs) { 1.1058 + if (!gNext) 1.1059 + return undefined; 1.1060 + 1.1061 + if (gExpectedInstalls instanceof Array && 1.1062 + gExpectedInstalls.length > 0) 1.1063 + return undefined; 1.1064 + else for each (let installList in gExpectedInstalls) { 1.1065 + if (installList.length > 0) 1.1066 + return undefined; 1.1067 + } 1.1068 + 1.1069 + for (let id in gExpectedEvents) { 1.1070 + if (gExpectedEvents[id].length > 0) 1.1071 + return undefined; 1.1072 + } 1.1073 + 1.1074 + return gNext.apply(null, aArgs); 1.1075 +} 1.1076 + 1.1077 +// Verifies that all the expected events for all add-ons were seen 1.1078 +function ensure_test_completed() { 1.1079 + for (let i in gExpectedEvents) { 1.1080 + if (gExpectedEvents[i].length > 0) 1.1081 + do_throw("Didn't see all the expected events for " + i); 1.1082 + } 1.1083 + gExpectedEvents = {}; 1.1084 + if (gExpectedInstalls) 1.1085 + do_check_eq(gExpectedInstalls.length, 0); 1.1086 +} 1.1087 + 1.1088 +/** 1.1089 + * A helper method to install an array of AddonInstall to completion and then 1.1090 + * call a provided callback. 1.1091 + * 1.1092 + * @param aInstalls 1.1093 + * The array of AddonInstalls to install 1.1094 + * @param aCallback 1.1095 + * The callback to call when all installs have finished 1.1096 + */ 1.1097 +function completeAllInstalls(aInstalls, aCallback) { 1.1098 + let count = aInstalls.length; 1.1099 + 1.1100 + if (count == 0) { 1.1101 + aCallback(); 1.1102 + return; 1.1103 + } 1.1104 + 1.1105 + function installCompleted(aInstall) { 1.1106 + aInstall.removeListener(listener); 1.1107 + 1.1108 + if (--count == 0) 1.1109 + do_execute_soon(aCallback); 1.1110 + } 1.1111 + 1.1112 + let listener = { 1.1113 + onDownloadFailed: installCompleted, 1.1114 + onDownloadCancelled: installCompleted, 1.1115 + onInstallFailed: installCompleted, 1.1116 + onInstallCancelled: installCompleted, 1.1117 + onInstallEnded: installCompleted 1.1118 + }; 1.1119 + 1.1120 + aInstalls.forEach(function(aInstall) { 1.1121 + aInstall.addListener(listener); 1.1122 + aInstall.install(); 1.1123 + }); 1.1124 +} 1.1125 + 1.1126 +/** 1.1127 + * A helper method to install an array of files and call a callback after the 1.1128 + * installs are completed. 1.1129 + * 1.1130 + * @param aFiles 1.1131 + * The array of files to install 1.1132 + * @param aCallback 1.1133 + * The callback to call when all installs have finished 1.1134 + * @param aIgnoreIncompatible 1.1135 + * Optional parameter to ignore add-ons that are incompatible in 1.1136 + * aome way with the application 1.1137 + */ 1.1138 +function installAllFiles(aFiles, aCallback, aIgnoreIncompatible) { 1.1139 + let count = aFiles.length; 1.1140 + let installs = []; 1.1141 + function callback() { 1.1142 + if (aCallback) { 1.1143 + aCallback(); 1.1144 + } 1.1145 + } 1.1146 + aFiles.forEach(function(aFile) { 1.1147 + AddonManager.getInstallForFile(aFile, function(aInstall) { 1.1148 + if (!aInstall) 1.1149 + do_throw("No AddonInstall created for " + aFile.path); 1.1150 + do_check_eq(aInstall.state, AddonManager.STATE_DOWNLOADED); 1.1151 + 1.1152 + if (!aIgnoreIncompatible || !aInstall.addon.appDisabled) 1.1153 + installs.push(aInstall); 1.1154 + 1.1155 + if (--count == 0) 1.1156 + completeAllInstalls(installs, callback); 1.1157 + }); 1.1158 + }); 1.1159 +} 1.1160 + 1.1161 +function promiseInstallAllFiles(aFiles, aIgnoreIncompatible) { 1.1162 + let deferred = Promise.defer(); 1.1163 + installAllFiles(aFiles, deferred.resolve, aIgnoreIncompatible); 1.1164 + return deferred.promise; 1.1165 + 1.1166 +} 1.1167 + 1.1168 +if ("nsIWindowsRegKey" in AM_Ci) { 1.1169 + var MockRegistry = { 1.1170 + LOCAL_MACHINE: {}, 1.1171 + CURRENT_USER: {}, 1.1172 + CLASSES_ROOT: {}, 1.1173 + 1.1174 + getRoot: function(aRoot) { 1.1175 + switch (aRoot) { 1.1176 + case AM_Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE: 1.1177 + return MockRegistry.LOCAL_MACHINE; 1.1178 + case AM_Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER: 1.1179 + return MockRegistry.CURRENT_USER; 1.1180 + case AM_Ci.nsIWindowsRegKey.ROOT_KEY_CLASSES_ROOT: 1.1181 + return MockRegistry.CLASSES_ROOT; 1.1182 + default: 1.1183 + do_throw("Unknown root " + aRootKey); 1.1184 + return null; 1.1185 + } 1.1186 + }, 1.1187 + 1.1188 + setValue: function(aRoot, aPath, aName, aValue) { 1.1189 + let rootKey = MockRegistry.getRoot(aRoot); 1.1190 + 1.1191 + if (!(aPath in rootKey)) { 1.1192 + rootKey[aPath] = []; 1.1193 + } 1.1194 + else { 1.1195 + for (let i = 0; i < rootKey[aPath].length; i++) { 1.1196 + if (rootKey[aPath][i].name == aName) { 1.1197 + if (aValue === null) 1.1198 + rootKey[aPath].splice(i, 1); 1.1199 + else 1.1200 + rootKey[aPath][i].value = aValue; 1.1201 + return; 1.1202 + } 1.1203 + } 1.1204 + } 1.1205 + 1.1206 + if (aValue === null) 1.1207 + return; 1.1208 + 1.1209 + rootKey[aPath].push({ 1.1210 + name: aName, 1.1211 + value: aValue 1.1212 + }); 1.1213 + } 1.1214 + }; 1.1215 + 1.1216 + /** 1.1217 + * This is a mock nsIWindowsRegistry implementation. It only implements the 1.1218 + * methods that the extension manager requires. 1.1219 + */ 1.1220 + function MockWindowsRegKey() { 1.1221 + } 1.1222 + 1.1223 + MockWindowsRegKey.prototype = { 1.1224 + values: null, 1.1225 + 1.1226 + // --- Overridden nsISupports interface functions --- 1.1227 + QueryInterface: XPCOMUtils.generateQI([AM_Ci.nsIWindowsRegKey]), 1.1228 + 1.1229 + // --- Overridden nsIWindowsRegKey interface functions --- 1.1230 + open: function(aRootKey, aRelPath, aMode) { 1.1231 + let rootKey = MockRegistry.getRoot(aRootKey); 1.1232 + 1.1233 + if (!(aRelPath in rootKey)) 1.1234 + rootKey[aRelPath] = []; 1.1235 + this.values = rootKey[aRelPath]; 1.1236 + }, 1.1237 + 1.1238 + close: function() { 1.1239 + this.values = null; 1.1240 + }, 1.1241 + 1.1242 + get valueCount() { 1.1243 + if (!this.values) 1.1244 + throw Components.results.NS_ERROR_FAILURE; 1.1245 + return this.values.length; 1.1246 + }, 1.1247 + 1.1248 + getValueName: function(aIndex) { 1.1249 + if (!this.values || aIndex >= this.values.length) 1.1250 + throw Components.results.NS_ERROR_FAILURE; 1.1251 + return this.values[aIndex].name; 1.1252 + }, 1.1253 + 1.1254 + readStringValue: function(aName) { 1.1255 + for (let value of this.values) { 1.1256 + if (value.name == aName) 1.1257 + return value.value; 1.1258 + } 1.1259 + return null; 1.1260 + } 1.1261 + }; 1.1262 + 1.1263 + var WinRegFactory = { 1.1264 + createInstance: function(aOuter, aIid) { 1.1265 + if (aOuter != null) 1.1266 + throw Components.results.NS_ERROR_NO_AGGREGATION; 1.1267 + 1.1268 + var key = new MockWindowsRegKey(); 1.1269 + return key.QueryInterface(aIid); 1.1270 + } 1.1271 + }; 1.1272 + 1.1273 + var registrar = Components.manager.QueryInterface(AM_Ci.nsIComponentRegistrar); 1.1274 + registrar.registerFactory(Components.ID("{0478de5b-0f38-4edb-851d-4c99f1ed8eba}"), 1.1275 + "Mock Windows Registry Implementation", 1.1276 + "@mozilla.org/windows-registry-key;1", WinRegFactory); 1.1277 +} 1.1278 + 1.1279 +// Get the profile directory for tests to use. 1.1280 +const gProfD = do_get_profile(); 1.1281 + 1.1282 +const EXTENSIONS_DB = "extensions.json"; 1.1283 +let gExtensionsJSON = gProfD.clone(); 1.1284 +gExtensionsJSON.append(EXTENSIONS_DB); 1.1285 + 1.1286 +const EXTENSIONS_INI = "extensions.ini"; 1.1287 +let gExtensionsINI = gProfD.clone(); 1.1288 +gExtensionsINI.append(EXTENSIONS_INI); 1.1289 + 1.1290 +// Enable more extensive EM logging 1.1291 +Services.prefs.setBoolPref("extensions.logging.enabled", true); 1.1292 + 1.1293 +// By default only load extensions from the profile install location 1.1294 +Services.prefs.setIntPref("extensions.enabledScopes", AddonManager.SCOPE_PROFILE); 1.1295 + 1.1296 +// By default don't disable add-ons from any scope 1.1297 +Services.prefs.setIntPref("extensions.autoDisableScopes", 0); 1.1298 + 1.1299 +// By default, don't cache add-ons in AddonRepository.jsm 1.1300 +Services.prefs.setBoolPref("extensions.getAddons.cache.enabled", false); 1.1301 + 1.1302 +// Disable the compatibility updates window by default 1.1303 +Services.prefs.setBoolPref("extensions.showMismatchUI", false); 1.1304 + 1.1305 +// Point update checks to the local machine for fast failures 1.1306 +Services.prefs.setCharPref("extensions.update.url", "http://127.0.0.1/updateURL"); 1.1307 +Services.prefs.setCharPref("extensions.update.background.url", "http://127.0.0.1/updateBackgroundURL"); 1.1308 +Services.prefs.setCharPref("extensions.blocklist.url", "http://127.0.0.1/blocklistURL"); 1.1309 + 1.1310 +// By default ignore bundled add-ons 1.1311 +Services.prefs.setBoolPref("extensions.installDistroAddons", false); 1.1312 + 1.1313 +// By default use strict compatibility 1.1314 +Services.prefs.setBoolPref("extensions.strictCompatibility", true); 1.1315 + 1.1316 +// By default don't check for hotfixes 1.1317 +Services.prefs.setCharPref("extensions.hotfix.id", ""); 1.1318 + 1.1319 +// By default, set min compatible versions to 0 1.1320 +Services.prefs.setCharPref(PREF_EM_MIN_COMPAT_APP_VERSION, "0"); 1.1321 +Services.prefs.setCharPref(PREF_EM_MIN_COMPAT_PLATFORM_VERSION, "0"); 1.1322 + 1.1323 +// Register a temporary directory for the tests. 1.1324 +const gTmpD = gProfD.clone(); 1.1325 +gTmpD.append("temp"); 1.1326 +gTmpD.create(AM_Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); 1.1327 +registerDirectory("TmpD", gTmpD); 1.1328 + 1.1329 +// Write out an empty blocklist.xml file to the profile to ensure nothing 1.1330 +// is blocklisted by default 1.1331 +var blockFile = gProfD.clone(); 1.1332 +blockFile.append("blocklist.xml"); 1.1333 +var stream = AM_Cc["@mozilla.org/network/file-output-stream;1"]. 1.1334 + createInstance(AM_Ci.nsIFileOutputStream); 1.1335 +stream.init(blockFile, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | FileUtils.MODE_TRUNCATE, 1.1336 + FileUtils.PERMS_FILE, 0); 1.1337 + 1.1338 +var data = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + 1.1339 + "<blocklist xmlns=\"http://www.mozilla.org/2006/addons-blocklist\">\n" + 1.1340 + "</blocklist>\n"; 1.1341 +stream.write(data, data.length); 1.1342 +stream.close(); 1.1343 + 1.1344 +// Copies blocklistFile (an nsIFile) to gProfD/blocklist.xml. 1.1345 +function copyBlocklistToProfile(blocklistFile) { 1.1346 + var dest = gProfD.clone(); 1.1347 + dest.append("blocklist.xml"); 1.1348 + if (dest.exists()) 1.1349 + dest.remove(false); 1.1350 + blocklistFile.copyTo(gProfD, "blocklist.xml"); 1.1351 + dest.lastModifiedTime = Date.now(); 1.1352 +} 1.1353 + 1.1354 +// Throw a failure and attempt to abandon the test if it looks like it is going 1.1355 +// to timeout 1.1356 +function timeout() { 1.1357 + timer = null; 1.1358 + do_throw("Test ran longer than " + TIMEOUT_MS + "ms"); 1.1359 + 1.1360 + // Attempt to bail out of the test 1.1361 + do_test_finished(); 1.1362 +} 1.1363 + 1.1364 +var timer = AM_Cc["@mozilla.org/timer;1"].createInstance(AM_Ci.nsITimer); 1.1365 +timer.init(timeout, TIMEOUT_MS, AM_Ci.nsITimer.TYPE_ONE_SHOT); 1.1366 + 1.1367 +// Make sure that a given path does not exist 1.1368 +function pathShouldntExist(aPath) { 1.1369 + if (aPath.exists()) { 1.1370 + do_throw("Test cleanup: path " + aPath.path + " exists when it should not"); 1.1371 + } 1.1372 +} 1.1373 + 1.1374 +do_register_cleanup(function addon_cleanup() { 1.1375 + if (timer) 1.1376 + timer.cancel(); 1.1377 + 1.1378 + // Check that the temporary directory is empty 1.1379 + var dirEntries = gTmpD.directoryEntries 1.1380 + .QueryInterface(AM_Ci.nsIDirectoryEnumerator); 1.1381 + var entry; 1.1382 + while ((entry = dirEntries.nextFile)) { 1.1383 + do_throw("Found unexpected file in temporary directory: " + entry.leafName); 1.1384 + } 1.1385 + dirEntries.close(); 1.1386 + 1.1387 + var testDir = gProfD.clone(); 1.1388 + testDir.append("extensions"); 1.1389 + testDir.append("trash"); 1.1390 + pathShouldntExist(testDir); 1.1391 + 1.1392 + testDir.leafName = "staged"; 1.1393 + pathShouldntExist(testDir); 1.1394 + 1.1395 + testDir.leafName = "staged-xpis"; 1.1396 + pathShouldntExist(testDir); 1.1397 + 1.1398 + shutdownManager(); 1.1399 + 1.1400 + // Clear commonly set prefs. 1.1401 + try { 1.1402 + Services.prefs.clearUserPref(PREF_EM_CHECK_UPDATE_SECURITY); 1.1403 + } catch (e) {} 1.1404 + try { 1.1405 + Services.prefs.clearUserPref(PREF_EM_STRICT_COMPATIBILITY); 1.1406 + } catch (e) {} 1.1407 +}); 1.1408 + 1.1409 +/** 1.1410 + * Handler function that responds with the interpolated 1.1411 + * static file associated to the URL specified by request.path. 1.1412 + * This replaces the %PORT% entries in the file with the actual 1.1413 + * value of the running server's port (stored in gPort). 1.1414 + */ 1.1415 +function interpolateAndServeFile(request, response) { 1.1416 + try { 1.1417 + let file = gUrlToFileMap[request.path]; 1.1418 + var data = ""; 1.1419 + var fstream = Components.classes["@mozilla.org/network/file-input-stream;1"]. 1.1420 + createInstance(Components.interfaces.nsIFileInputStream); 1.1421 + var cstream = Components.classes["@mozilla.org/intl/converter-input-stream;1"]. 1.1422 + createInstance(Components.interfaces.nsIConverterInputStream); 1.1423 + fstream.init(file, -1, 0, 0); 1.1424 + cstream.init(fstream, "UTF-8", 0, 0); 1.1425 + 1.1426 + let (str = {}) { 1.1427 + let read = 0; 1.1428 + do { 1.1429 + // read as much as we can and put it in str.value 1.1430 + read = cstream.readString(0xffffffff, str); 1.1431 + data += str.value; 1.1432 + } while (read != 0); 1.1433 + } 1.1434 + data = data.replace(/%PORT%/g, gPort); 1.1435 + 1.1436 + response.write(data); 1.1437 + } catch (e) { 1.1438 + do_throw("Exception while serving interpolated file."); 1.1439 + } finally { 1.1440 + cstream.close(); // this closes fstream as well 1.1441 + } 1.1442 +} 1.1443 + 1.1444 +/** 1.1445 + * Sets up a path handler for the given URL and saves the 1.1446 + * corresponding file in the global url -> file map. 1.1447 + * 1.1448 + * @param url 1.1449 + * the actual URL 1.1450 + * @param file 1.1451 + * nsILocalFile representing a static file 1.1452 + */ 1.1453 +function mapUrlToFile(url, file, server) { 1.1454 + server.registerPathHandler(url, interpolateAndServeFile); 1.1455 + gUrlToFileMap[url] = file; 1.1456 +} 1.1457 + 1.1458 +function mapFile(path, server) { 1.1459 + mapUrlToFile(path, do_get_file(path), server); 1.1460 +} 1.1461 + 1.1462 +/** 1.1463 + * Take out the port number in an URL 1.1464 + * 1.1465 + * @param url 1.1466 + * String that represents an URL with a port number in it 1.1467 + */ 1.1468 +function remove_port(url) { 1.1469 + if (typeof url === "string") 1.1470 + return url.replace(/:\d+/, ""); 1.1471 + return url; 1.1472 +} 1.1473 +// Wrap a function (typically a callback) to catch and report exceptions 1.1474 +function do_exception_wrap(func) { 1.1475 + return function() { 1.1476 + try { 1.1477 + func.apply(null, arguments); 1.1478 + } 1.1479 + catch(e) { 1.1480 + do_report_unexpected_exception(e); 1.1481 + } 1.1482 + }; 1.1483 +} 1.1484 + 1.1485 +/** 1.1486 + * Change the schema version of the JSON extensions database 1.1487 + */ 1.1488 +function changeXPIDBVersion(aNewVersion) { 1.1489 + let jData = loadJSON(gExtensionsJSON); 1.1490 + jData.schemaVersion = aNewVersion; 1.1491 + saveJSON(jData, gExtensionsJSON); 1.1492 +} 1.1493 + 1.1494 +/** 1.1495 + * Load a file into a string 1.1496 + */ 1.1497 +function loadFile(aFile) { 1.1498 + let data = ""; 1.1499 + let fstream = Components.classes["@mozilla.org/network/file-input-stream;1"]. 1.1500 + createInstance(Components.interfaces.nsIFileInputStream); 1.1501 + let cstream = Components.classes["@mozilla.org/intl/converter-input-stream;1"]. 1.1502 + createInstance(Components.interfaces.nsIConverterInputStream); 1.1503 + fstream.init(aFile, -1, 0, 0); 1.1504 + cstream.init(fstream, "UTF-8", 0, 0); 1.1505 + let (str = {}) { 1.1506 + let read = 0; 1.1507 + do { 1.1508 + read = cstream.readString(0xffffffff, str); // read as much as we can and put it in str.value 1.1509 + data += str.value; 1.1510 + } while (read != 0); 1.1511 + } 1.1512 + cstream.close(); 1.1513 + return data; 1.1514 +} 1.1515 + 1.1516 +/** 1.1517 + * Raw load of a JSON file 1.1518 + */ 1.1519 +function loadJSON(aFile) { 1.1520 + let data = loadFile(aFile); 1.1521 + do_print("Loaded JSON file " + aFile.path); 1.1522 + return(JSON.parse(data)); 1.1523 +} 1.1524 + 1.1525 +/** 1.1526 + * Raw save of a JSON blob to file 1.1527 + */ 1.1528 +function saveJSON(aData, aFile) { 1.1529 + do_print("Starting to save JSON file " + aFile.path); 1.1530 + let stream = FileUtils.openSafeFileOutputStream(aFile); 1.1531 + let converter = AM_Cc["@mozilla.org/intl/converter-output-stream;1"]. 1.1532 + createInstance(AM_Ci.nsIConverterOutputStream); 1.1533 + converter.init(stream, "UTF-8", 0, 0x0000); 1.1534 + // XXX pretty print the JSON while debugging 1.1535 + converter.writeString(JSON.stringify(aData, null, 2)); 1.1536 + converter.flush(); 1.1537 + // nsConverterOutputStream doesn't finish() safe output streams on close() 1.1538 + FileUtils.closeSafeFileOutputStream(stream); 1.1539 + converter.close(); 1.1540 + do_print("Done saving JSON file " + aFile.path); 1.1541 +} 1.1542 + 1.1543 +/** 1.1544 + * Create a callback function that calls do_execute_soon on an actual callback and arguments 1.1545 + */ 1.1546 +function callback_soon(aFunction) { 1.1547 + return function(...args) { 1.1548 + do_execute_soon(function() { 1.1549 + aFunction.apply(null, args); 1.1550 + }, aFunction.name ? "delayed callback " + aFunction.name : "delayed callback"); 1.1551 + } 1.1552 +} 1.1553 + 1.1554 +/** 1.1555 + * A promise-based variant of AddonManager.getAddonsByIDs. 1.1556 + * 1.1557 + * @param {array} list As the first argument of AddonManager.getAddonsByIDs 1.1558 + * @return {promise} 1.1559 + * @resolve {array} The list of add-ons sent by AddonManaget.getAddonsByIDs to 1.1560 + * its callback. 1.1561 + */ 1.1562 +function promiseAddonsByIDs(list) { 1.1563 + let deferred = Promise.defer(); 1.1564 + AddonManager.getAddonsByIDs(list, deferred.resolve); 1.1565 + return deferred.promise; 1.1566 +}