Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
michael@0 | 1 | /* Any copyright is dedicated to the Public Domain. |
michael@0 | 2 | * http://creativecommons.org/publicdomain/zero/1.0/ |
michael@0 | 3 | */ |
michael@0 | 4 | |
michael@0 | 5 | const AM_Cc = Components.classes; |
michael@0 | 6 | const AM_Ci = Components.interfaces; |
michael@0 | 7 | |
michael@0 | 8 | const XULAPPINFO_CONTRACTID = "@mozilla.org/xre/app-info;1"; |
michael@0 | 9 | const XULAPPINFO_CID = Components.ID("{c763b610-9d49-455a-bbd2-ede71682a1ac}"); |
michael@0 | 10 | |
michael@0 | 11 | const PREF_EM_CHECK_UPDATE_SECURITY = "extensions.checkUpdateSecurity"; |
michael@0 | 12 | const PREF_EM_STRICT_COMPATIBILITY = "extensions.strictCompatibility"; |
michael@0 | 13 | const PREF_EM_MIN_COMPAT_APP_VERSION = "extensions.minCompatibleAppVersion"; |
michael@0 | 14 | const PREF_EM_MIN_COMPAT_PLATFORM_VERSION = "extensions.minCompatiblePlatformVersion"; |
michael@0 | 15 | const PREF_GETADDONS_BYIDS = "extensions.getAddons.get.url"; |
michael@0 | 16 | const PREF_GETADDONS_BYIDS_PERFORMANCE = "extensions.getAddons.getWithPerformance.url"; |
michael@0 | 17 | |
michael@0 | 18 | // Forcibly end the test if it runs longer than 15 minutes |
michael@0 | 19 | const TIMEOUT_MS = 900000; |
michael@0 | 20 | |
michael@0 | 21 | Components.utils.import("resource://gre/modules/addons/AddonRepository.jsm"); |
michael@0 | 22 | Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); |
michael@0 | 23 | Components.utils.import("resource://gre/modules/FileUtils.jsm"); |
michael@0 | 24 | Components.utils.import("resource://gre/modules/Services.jsm"); |
michael@0 | 25 | Components.utils.import("resource://gre/modules/NetUtil.jsm"); |
michael@0 | 26 | Components.utils.import("resource://gre/modules/Promise.jsm"); |
michael@0 | 27 | Components.utils.import("resource://gre/modules/Task.jsm"); |
michael@0 | 28 | Components.utils.import("resource://gre/modules/osfile.jsm"); |
michael@0 | 29 | |
michael@0 | 30 | Services.prefs.setBoolPref("toolkit.osfile.log", true); |
michael@0 | 31 | |
michael@0 | 32 | // We need some internal bits of AddonManager |
michael@0 | 33 | let AMscope = Components.utils.import("resource://gre/modules/AddonManager.jsm"); |
michael@0 | 34 | let AddonManager = AMscope.AddonManager; |
michael@0 | 35 | let AddonManagerInternal = AMscope.AddonManagerInternal; |
michael@0 | 36 | // Mock out AddonManager's reference to the AsyncShutdown module so we can shut |
michael@0 | 37 | // down AddonManager from the test |
michael@0 | 38 | let MockAsyncShutdown = { |
michael@0 | 39 | hook: null, |
michael@0 | 40 | profileBeforeChange: { |
michael@0 | 41 | addBlocker: function(aName, aBlocker) { |
michael@0 | 42 | do_print("Mock profileBeforeChange blocker for '" + aName + "'"); |
michael@0 | 43 | MockAsyncShutdown.hook = aBlocker; |
michael@0 | 44 | } |
michael@0 | 45 | } |
michael@0 | 46 | }; |
michael@0 | 47 | AMscope.AsyncShutdown = MockAsyncShutdown; |
michael@0 | 48 | |
michael@0 | 49 | var gInternalManager = null; |
michael@0 | 50 | var gAppInfo = null; |
michael@0 | 51 | var gAddonsList; |
michael@0 | 52 | |
michael@0 | 53 | var gPort = null; |
michael@0 | 54 | var gUrlToFileMap = {}; |
michael@0 | 55 | |
michael@0 | 56 | var TEST_UNPACKED = false; |
michael@0 | 57 | |
michael@0 | 58 | function isNightlyChannel() { |
michael@0 | 59 | var channel = "default"; |
michael@0 | 60 | try { |
michael@0 | 61 | channel = Services.prefs.getCharPref("app.update.channel"); |
michael@0 | 62 | } |
michael@0 | 63 | catch (e) { } |
michael@0 | 64 | |
michael@0 | 65 | return channel != "aurora" && channel != "beta" && channel != "release" && channel != "esr"; |
michael@0 | 66 | } |
michael@0 | 67 | |
michael@0 | 68 | function createAppInfo(id, name, version, platformVersion) { |
michael@0 | 69 | gAppInfo = { |
michael@0 | 70 | // nsIXULAppInfo |
michael@0 | 71 | vendor: "Mozilla", |
michael@0 | 72 | name: name, |
michael@0 | 73 | ID: id, |
michael@0 | 74 | version: version, |
michael@0 | 75 | appBuildID: "2007010101", |
michael@0 | 76 | platformVersion: platformVersion ? platformVersion : "1.0", |
michael@0 | 77 | platformBuildID: "2007010101", |
michael@0 | 78 | |
michael@0 | 79 | // nsIXULRuntime |
michael@0 | 80 | inSafeMode: false, |
michael@0 | 81 | logConsoleErrors: true, |
michael@0 | 82 | OS: "XPCShell", |
michael@0 | 83 | XPCOMABI: "noarch-spidermonkey", |
michael@0 | 84 | invalidateCachesOnRestart: function invalidateCachesOnRestart() { |
michael@0 | 85 | // Do nothing |
michael@0 | 86 | }, |
michael@0 | 87 | |
michael@0 | 88 | // nsICrashReporter |
michael@0 | 89 | annotations: {}, |
michael@0 | 90 | |
michael@0 | 91 | annotateCrashReport: function(key, data) { |
michael@0 | 92 | this.annotations[key] = data; |
michael@0 | 93 | }, |
michael@0 | 94 | |
michael@0 | 95 | QueryInterface: XPCOMUtils.generateQI([AM_Ci.nsIXULAppInfo, |
michael@0 | 96 | AM_Ci.nsIXULRuntime, |
michael@0 | 97 | AM_Ci.nsICrashReporter, |
michael@0 | 98 | AM_Ci.nsISupports]) |
michael@0 | 99 | }; |
michael@0 | 100 | |
michael@0 | 101 | var XULAppInfoFactory = { |
michael@0 | 102 | createInstance: function (outer, iid) { |
michael@0 | 103 | if (outer != null) |
michael@0 | 104 | throw Components.results.NS_ERROR_NO_AGGREGATION; |
michael@0 | 105 | return gAppInfo.QueryInterface(iid); |
michael@0 | 106 | } |
michael@0 | 107 | }; |
michael@0 | 108 | var registrar = Components.manager.QueryInterface(AM_Ci.nsIComponentRegistrar); |
michael@0 | 109 | registrar.registerFactory(XULAPPINFO_CID, "XULAppInfo", |
michael@0 | 110 | XULAPPINFO_CONTRACTID, XULAppInfoFactory); |
michael@0 | 111 | } |
michael@0 | 112 | |
michael@0 | 113 | /** |
michael@0 | 114 | * Tests that an add-on does appear in the crash report annotations, if |
michael@0 | 115 | * crash reporting is enabled. The test will fail if the add-on is not in the |
michael@0 | 116 | * annotation. |
michael@0 | 117 | * @param aId |
michael@0 | 118 | * The ID of the add-on |
michael@0 | 119 | * @param aVersion |
michael@0 | 120 | * The version of the add-on |
michael@0 | 121 | */ |
michael@0 | 122 | function do_check_in_crash_annotation(aId, aVersion) { |
michael@0 | 123 | if (!("nsICrashReporter" in AM_Ci)) |
michael@0 | 124 | return; |
michael@0 | 125 | |
michael@0 | 126 | if (!("Add-ons" in gAppInfo.annotations)) { |
michael@0 | 127 | do_check_false(true); |
michael@0 | 128 | return; |
michael@0 | 129 | } |
michael@0 | 130 | |
michael@0 | 131 | let addons = gAppInfo.annotations["Add-ons"].split(","); |
michael@0 | 132 | do_check_false(addons.indexOf(encodeURIComponent(aId) + ":" + |
michael@0 | 133 | encodeURIComponent(aVersion)) < 0); |
michael@0 | 134 | } |
michael@0 | 135 | |
michael@0 | 136 | /** |
michael@0 | 137 | * Tests that an add-on does not appear in the crash report annotations, if |
michael@0 | 138 | * crash reporting is enabled. The test will fail if the add-on is in the |
michael@0 | 139 | * annotation. |
michael@0 | 140 | * @param aId |
michael@0 | 141 | * The ID of the add-on |
michael@0 | 142 | * @param aVersion |
michael@0 | 143 | * The version of the add-on |
michael@0 | 144 | */ |
michael@0 | 145 | function do_check_not_in_crash_annotation(aId, aVersion) { |
michael@0 | 146 | if (!("nsICrashReporter" in AM_Ci)) |
michael@0 | 147 | return; |
michael@0 | 148 | |
michael@0 | 149 | if (!("Add-ons" in gAppInfo.annotations)) { |
michael@0 | 150 | do_check_true(true); |
michael@0 | 151 | return; |
michael@0 | 152 | } |
michael@0 | 153 | |
michael@0 | 154 | let addons = gAppInfo.annotations["Add-ons"].split(","); |
michael@0 | 155 | do_check_true(addons.indexOf(encodeURIComponent(aId) + ":" + |
michael@0 | 156 | encodeURIComponent(aVersion)) < 0); |
michael@0 | 157 | } |
michael@0 | 158 | |
michael@0 | 159 | /** |
michael@0 | 160 | * Returns a testcase xpi |
michael@0 | 161 | * |
michael@0 | 162 | * @param aName |
michael@0 | 163 | * The name of the testcase (without extension) |
michael@0 | 164 | * @return an nsIFile pointing to the testcase xpi |
michael@0 | 165 | */ |
michael@0 | 166 | function do_get_addon(aName) { |
michael@0 | 167 | return do_get_file("addons/" + aName + ".xpi"); |
michael@0 | 168 | } |
michael@0 | 169 | |
michael@0 | 170 | function do_get_addon_hash(aName, aAlgorithm) { |
michael@0 | 171 | if (!aAlgorithm) |
michael@0 | 172 | aAlgorithm = "sha1"; |
michael@0 | 173 | |
michael@0 | 174 | let file = do_get_addon(aName); |
michael@0 | 175 | |
michael@0 | 176 | let crypto = AM_Cc["@mozilla.org/security/hash;1"]. |
michael@0 | 177 | createInstance(AM_Ci.nsICryptoHash); |
michael@0 | 178 | crypto.initWithString(aAlgorithm); |
michael@0 | 179 | let fis = AM_Cc["@mozilla.org/network/file-input-stream;1"]. |
michael@0 | 180 | createInstance(AM_Ci.nsIFileInputStream); |
michael@0 | 181 | fis.init(file, -1, -1, false); |
michael@0 | 182 | crypto.updateFromStream(fis, file.fileSize); |
michael@0 | 183 | |
michael@0 | 184 | // return the two-digit hexadecimal code for a byte |
michael@0 | 185 | function toHexString(charCode) |
michael@0 | 186 | ("0" + charCode.toString(16)).slice(-2); |
michael@0 | 187 | |
michael@0 | 188 | let binary = crypto.finish(false); |
michael@0 | 189 | return aAlgorithm + ":" + [toHexString(binary.charCodeAt(i)) for (i in binary)].join("") |
michael@0 | 190 | } |
michael@0 | 191 | |
michael@0 | 192 | /** |
michael@0 | 193 | * Returns an extension uri spec |
michael@0 | 194 | * |
michael@0 | 195 | * @param aProfileDir |
michael@0 | 196 | * The extension install directory |
michael@0 | 197 | * @return a uri spec pointing to the root of the extension |
michael@0 | 198 | */ |
michael@0 | 199 | function do_get_addon_root_uri(aProfileDir, aId) { |
michael@0 | 200 | let path = aProfileDir.clone(); |
michael@0 | 201 | path.append(aId); |
michael@0 | 202 | if (!path.exists()) { |
michael@0 | 203 | path.leafName += ".xpi"; |
michael@0 | 204 | return "jar:" + Services.io.newFileURI(path).spec + "!/"; |
michael@0 | 205 | } |
michael@0 | 206 | else { |
michael@0 | 207 | return Services.io.newFileURI(path).spec; |
michael@0 | 208 | } |
michael@0 | 209 | } |
michael@0 | 210 | |
michael@0 | 211 | function do_get_expected_addon_name(aId) { |
michael@0 | 212 | if (TEST_UNPACKED) |
michael@0 | 213 | return aId; |
michael@0 | 214 | return aId + ".xpi"; |
michael@0 | 215 | } |
michael@0 | 216 | |
michael@0 | 217 | /** |
michael@0 | 218 | * Check that an array of actual add-ons is the same as an array of |
michael@0 | 219 | * expected add-ons. |
michael@0 | 220 | * |
michael@0 | 221 | * @param aActualAddons |
michael@0 | 222 | * The array of actual add-ons to check. |
michael@0 | 223 | * @param aExpectedAddons |
michael@0 | 224 | * The array of expected add-ons to check against. |
michael@0 | 225 | * @param aProperties |
michael@0 | 226 | * An array of properties to check. |
michael@0 | 227 | */ |
michael@0 | 228 | function do_check_addons(aActualAddons, aExpectedAddons, aProperties) { |
michael@0 | 229 | do_check_neq(aActualAddons, null); |
michael@0 | 230 | do_check_eq(aActualAddons.length, aExpectedAddons.length); |
michael@0 | 231 | for (let i = 0; i < aActualAddons.length; i++) |
michael@0 | 232 | do_check_addon(aActualAddons[i], aExpectedAddons[i], aProperties); |
michael@0 | 233 | } |
michael@0 | 234 | |
michael@0 | 235 | /** |
michael@0 | 236 | * Check that the actual add-on is the same as the expected add-on. |
michael@0 | 237 | * |
michael@0 | 238 | * @param aActualAddon |
michael@0 | 239 | * The actual add-on to check. |
michael@0 | 240 | * @param aExpectedAddon |
michael@0 | 241 | * The expected add-on to check against. |
michael@0 | 242 | * @param aProperties |
michael@0 | 243 | * An array of properties to check. |
michael@0 | 244 | */ |
michael@0 | 245 | function do_check_addon(aActualAddon, aExpectedAddon, aProperties) { |
michael@0 | 246 | do_check_neq(aActualAddon, null); |
michael@0 | 247 | |
michael@0 | 248 | aProperties.forEach(function(aProperty) { |
michael@0 | 249 | let actualValue = aActualAddon[aProperty]; |
michael@0 | 250 | let expectedValue = aExpectedAddon[aProperty]; |
michael@0 | 251 | |
michael@0 | 252 | // Check that all undefined expected properties are null on actual add-on |
michael@0 | 253 | if (!(aProperty in aExpectedAddon)) { |
michael@0 | 254 | if (actualValue !== undefined && actualValue !== null) { |
michael@0 | 255 | do_throw("Unexpected defined/non-null property for add-on " + |
michael@0 | 256 | aExpectedAddon.id + " (addon[" + aProperty + "] = " + |
michael@0 | 257 | actualValue.toSource() + ")"); |
michael@0 | 258 | } |
michael@0 | 259 | |
michael@0 | 260 | return; |
michael@0 | 261 | } |
michael@0 | 262 | else if (expectedValue && !actualValue) { |
michael@0 | 263 | do_throw("Missing property for add-on " + aExpectedAddon.id + |
michael@0 | 264 | ": expected addon[" + aProperty + "] = " + expectedValue); |
michael@0 | 265 | return; |
michael@0 | 266 | } |
michael@0 | 267 | |
michael@0 | 268 | switch (aProperty) { |
michael@0 | 269 | case "creator": |
michael@0 | 270 | do_check_author(actualValue, expectedValue); |
michael@0 | 271 | break; |
michael@0 | 272 | |
michael@0 | 273 | case "developers": |
michael@0 | 274 | case "translators": |
michael@0 | 275 | case "contributors": |
michael@0 | 276 | do_check_eq(actualValue.length, expectedValue.length); |
michael@0 | 277 | for (let i = 0; i < actualValue.length; i++) |
michael@0 | 278 | do_check_author(actualValue[i], expectedValue[i]); |
michael@0 | 279 | break; |
michael@0 | 280 | |
michael@0 | 281 | case "screenshots": |
michael@0 | 282 | do_check_eq(actualValue.length, expectedValue.length); |
michael@0 | 283 | for (let i = 0; i < actualValue.length; i++) |
michael@0 | 284 | do_check_screenshot(actualValue[i], expectedValue[i]); |
michael@0 | 285 | break; |
michael@0 | 286 | |
michael@0 | 287 | case "sourceURI": |
michael@0 | 288 | do_check_eq(actualValue.spec, expectedValue); |
michael@0 | 289 | break; |
michael@0 | 290 | |
michael@0 | 291 | case "updateDate": |
michael@0 | 292 | do_check_eq(actualValue.getTime(), expectedValue.getTime()); |
michael@0 | 293 | break; |
michael@0 | 294 | |
michael@0 | 295 | case "compatibilityOverrides": |
michael@0 | 296 | do_check_eq(actualValue.length, expectedValue.length); |
michael@0 | 297 | for (let i = 0; i < actualValue.length; i++) |
michael@0 | 298 | do_check_compatibilityoverride(actualValue[i], expectedValue[i]); |
michael@0 | 299 | break; |
michael@0 | 300 | |
michael@0 | 301 | case "icons": |
michael@0 | 302 | do_check_icons(actualValue, expectedValue); |
michael@0 | 303 | break; |
michael@0 | 304 | |
michael@0 | 305 | default: |
michael@0 | 306 | if (remove_port(actualValue) !== remove_port(expectedValue)) |
michael@0 | 307 | do_throw("Failed for " + aProperty + " for add-on " + aExpectedAddon.id + |
michael@0 | 308 | " (" + actualValue + " === " + expectedValue + ")"); |
michael@0 | 309 | } |
michael@0 | 310 | }); |
michael@0 | 311 | } |
michael@0 | 312 | |
michael@0 | 313 | /** |
michael@0 | 314 | * Check that the actual author is the same as the expected author. |
michael@0 | 315 | * |
michael@0 | 316 | * @param aActual |
michael@0 | 317 | * The actual author to check. |
michael@0 | 318 | * @param aExpected |
michael@0 | 319 | * The expected author to check against. |
michael@0 | 320 | */ |
michael@0 | 321 | function do_check_author(aActual, aExpected) { |
michael@0 | 322 | do_check_eq(aActual.toString(), aExpected.name); |
michael@0 | 323 | do_check_eq(aActual.name, aExpected.name); |
michael@0 | 324 | do_check_eq(aActual.url, aExpected.url); |
michael@0 | 325 | } |
michael@0 | 326 | |
michael@0 | 327 | /** |
michael@0 | 328 | * Check that the actual screenshot is the same as the expected screenshot. |
michael@0 | 329 | * |
michael@0 | 330 | * @param aActual |
michael@0 | 331 | * The actual screenshot to check. |
michael@0 | 332 | * @param aExpected |
michael@0 | 333 | * The expected screenshot to check against. |
michael@0 | 334 | */ |
michael@0 | 335 | function do_check_screenshot(aActual, aExpected) { |
michael@0 | 336 | do_check_eq(aActual.toString(), aExpected.url); |
michael@0 | 337 | do_check_eq(aActual.url, aExpected.url); |
michael@0 | 338 | do_check_eq(aActual.width, aExpected.width); |
michael@0 | 339 | do_check_eq(aActual.height, aExpected.height); |
michael@0 | 340 | do_check_eq(aActual.thumbnailURL, aExpected.thumbnailURL); |
michael@0 | 341 | do_check_eq(aActual.thumbnailWidth, aExpected.thumbnailWidth); |
michael@0 | 342 | do_check_eq(aActual.thumbnailHeight, aExpected.thumbnailHeight); |
michael@0 | 343 | do_check_eq(aActual.caption, aExpected.caption); |
michael@0 | 344 | } |
michael@0 | 345 | |
michael@0 | 346 | /** |
michael@0 | 347 | * Check that the actual compatibility override is the same as the expected |
michael@0 | 348 | * compatibility override. |
michael@0 | 349 | * |
michael@0 | 350 | * @param aAction |
michael@0 | 351 | * The actual compatibility override to check. |
michael@0 | 352 | * @param aExpected |
michael@0 | 353 | * The expected compatibility override to check against. |
michael@0 | 354 | */ |
michael@0 | 355 | function do_check_compatibilityoverride(aActual, aExpected) { |
michael@0 | 356 | do_check_eq(aActual.type, aExpected.type); |
michael@0 | 357 | do_check_eq(aActual.minVersion, aExpected.minVersion); |
michael@0 | 358 | do_check_eq(aActual.maxVersion, aExpected.maxVersion); |
michael@0 | 359 | do_check_eq(aActual.appID, aExpected.appID); |
michael@0 | 360 | do_check_eq(aActual.appMinVersion, aExpected.appMinVersion); |
michael@0 | 361 | do_check_eq(aActual.appMaxVersion, aExpected.appMaxVersion); |
michael@0 | 362 | } |
michael@0 | 363 | |
michael@0 | 364 | function do_check_icons(aActual, aExpected) { |
michael@0 | 365 | for (var size in aExpected) { |
michael@0 | 366 | do_check_eq(remove_port(aActual[size]), remove_port(aExpected[size])); |
michael@0 | 367 | } |
michael@0 | 368 | } |
michael@0 | 369 | |
michael@0 | 370 | // Record the error (if any) from trying to save the XPI |
michael@0 | 371 | // database at shutdown time |
michael@0 | 372 | let gXPISaveError = null; |
michael@0 | 373 | |
michael@0 | 374 | /** |
michael@0 | 375 | * Starts up the add-on manager as if it was started by the application. |
michael@0 | 376 | * |
michael@0 | 377 | * @param aAppChanged |
michael@0 | 378 | * An optional boolean parameter to simulate the case where the |
michael@0 | 379 | * application has changed version since the last run. If not passed it |
michael@0 | 380 | * defaults to true |
michael@0 | 381 | */ |
michael@0 | 382 | function startupManager(aAppChanged) { |
michael@0 | 383 | if (gInternalManager) |
michael@0 | 384 | do_throw("Test attempt to startup manager that was already started."); |
michael@0 | 385 | |
michael@0 | 386 | if (aAppChanged || aAppChanged === undefined) { |
michael@0 | 387 | if (gExtensionsINI.exists()) |
michael@0 | 388 | gExtensionsINI.remove(true); |
michael@0 | 389 | } |
michael@0 | 390 | |
michael@0 | 391 | gInternalManager = AM_Cc["@mozilla.org/addons/integration;1"]. |
michael@0 | 392 | getService(AM_Ci.nsIObserver). |
michael@0 | 393 | QueryInterface(AM_Ci.nsITimerCallback); |
michael@0 | 394 | |
michael@0 | 395 | gInternalManager.observe(null, "addons-startup", null); |
michael@0 | 396 | |
michael@0 | 397 | // Load the add-ons list as it was after extension registration |
michael@0 | 398 | loadAddonsList(); |
michael@0 | 399 | } |
michael@0 | 400 | |
michael@0 | 401 | /** |
michael@0 | 402 | * Helper to spin the event loop until a promise resolves or rejects |
michael@0 | 403 | */ |
michael@0 | 404 | function loopUntilPromise(aPromise) { |
michael@0 | 405 | let done = false; |
michael@0 | 406 | aPromise.then( |
michael@0 | 407 | () => done = true, |
michael@0 | 408 | err => { |
michael@0 | 409 | do_report_unexpected_exception(err); |
michael@0 | 410 | done = true; |
michael@0 | 411 | }); |
michael@0 | 412 | |
michael@0 | 413 | let thr = Services.tm.mainThread; |
michael@0 | 414 | |
michael@0 | 415 | while (!done) { |
michael@0 | 416 | thr.processNextEvent(true); |
michael@0 | 417 | } |
michael@0 | 418 | } |
michael@0 | 419 | |
michael@0 | 420 | /** |
michael@0 | 421 | * Restarts the add-on manager as if the host application was restarted. |
michael@0 | 422 | * |
michael@0 | 423 | * @param aNewVersion |
michael@0 | 424 | * An optional new version to use for the application. Passing this |
michael@0 | 425 | * will change nsIXULAppInfo.version and make the startup appear as if |
michael@0 | 426 | * the application version has changed. |
michael@0 | 427 | */ |
michael@0 | 428 | function restartManager(aNewVersion) { |
michael@0 | 429 | loopUntilPromise(promiseRestartManager(aNewVersion)); |
michael@0 | 430 | } |
michael@0 | 431 | |
michael@0 | 432 | function promiseRestartManager(aNewVersion) { |
michael@0 | 433 | return promiseShutdownManager() |
michael@0 | 434 | .then(null, err => do_report_unexpected_exception(err)) |
michael@0 | 435 | .then(() => { |
michael@0 | 436 | if (aNewVersion) { |
michael@0 | 437 | gAppInfo.version = aNewVersion; |
michael@0 | 438 | startupManager(true); |
michael@0 | 439 | } |
michael@0 | 440 | else { |
michael@0 | 441 | startupManager(false); |
michael@0 | 442 | } |
michael@0 | 443 | }); |
michael@0 | 444 | } |
michael@0 | 445 | |
michael@0 | 446 | function shutdownManager() { |
michael@0 | 447 | loopUntilPromise(promiseShutdownManager()); |
michael@0 | 448 | } |
michael@0 | 449 | |
michael@0 | 450 | function promiseShutdownManager() { |
michael@0 | 451 | if (!gInternalManager) { |
michael@0 | 452 | return Promise.resolve(false); |
michael@0 | 453 | } |
michael@0 | 454 | |
michael@0 | 455 | let hookErr = null; |
michael@0 | 456 | Services.obs.notifyObservers(null, "quit-application-granted", null); |
michael@0 | 457 | return MockAsyncShutdown.hook() |
michael@0 | 458 | .then(null, err => hookErr = err) |
michael@0 | 459 | .then( () => { |
michael@0 | 460 | gInternalManager = null; |
michael@0 | 461 | |
michael@0 | 462 | // Load the add-ons list as it was after application shutdown |
michael@0 | 463 | loadAddonsList(); |
michael@0 | 464 | |
michael@0 | 465 | // Clear any crash report annotations |
michael@0 | 466 | gAppInfo.annotations = {}; |
michael@0 | 467 | |
michael@0 | 468 | // Force the XPIProvider provider to reload to better |
michael@0 | 469 | // simulate real-world usage. |
michael@0 | 470 | let XPIscope = Components.utils.import("resource://gre/modules/addons/XPIProvider.jsm"); |
michael@0 | 471 | // This would be cleaner if I could get it as the rejection reason from |
michael@0 | 472 | // the AddonManagerInternal.shutdown() promise |
michael@0 | 473 | gXPISaveError = XPIscope.XPIProvider._shutdownError; |
michael@0 | 474 | do_print("gXPISaveError set to: " + gXPISaveError); |
michael@0 | 475 | AddonManagerPrivate.unregisterProvider(XPIscope.XPIProvider); |
michael@0 | 476 | Components.utils.unload("resource://gre/modules/addons/XPIProvider.jsm"); |
michael@0 | 477 | if (hookErr) { |
michael@0 | 478 | throw hookErr; |
michael@0 | 479 | } |
michael@0 | 480 | }); |
michael@0 | 481 | } |
michael@0 | 482 | |
michael@0 | 483 | function loadAddonsList() { |
michael@0 | 484 | function readDirectories(aSection) { |
michael@0 | 485 | var dirs = []; |
michael@0 | 486 | var keys = parser.getKeys(aSection); |
michael@0 | 487 | while (keys.hasMore()) { |
michael@0 | 488 | let descriptor = parser.getString(aSection, keys.getNext()); |
michael@0 | 489 | try { |
michael@0 | 490 | let file = AM_Cc["@mozilla.org/file/local;1"]. |
michael@0 | 491 | createInstance(AM_Ci.nsIFile); |
michael@0 | 492 | file.persistentDescriptor = descriptor; |
michael@0 | 493 | dirs.push(file); |
michael@0 | 494 | } |
michael@0 | 495 | catch (e) { |
michael@0 | 496 | // Throws if the directory doesn't exist, we can ignore this since the |
michael@0 | 497 | // platform will too. |
michael@0 | 498 | } |
michael@0 | 499 | } |
michael@0 | 500 | return dirs; |
michael@0 | 501 | } |
michael@0 | 502 | |
michael@0 | 503 | gAddonsList = { |
michael@0 | 504 | extensions: [], |
michael@0 | 505 | themes: [] |
michael@0 | 506 | }; |
michael@0 | 507 | |
michael@0 | 508 | if (!gExtensionsINI.exists()) |
michael@0 | 509 | return; |
michael@0 | 510 | |
michael@0 | 511 | var factory = AM_Cc["@mozilla.org/xpcom/ini-parser-factory;1"]. |
michael@0 | 512 | getService(AM_Ci.nsIINIParserFactory); |
michael@0 | 513 | var parser = factory.createINIParser(gExtensionsINI); |
michael@0 | 514 | gAddonsList.extensions = readDirectories("ExtensionDirs"); |
michael@0 | 515 | gAddonsList.themes = readDirectories("ThemeDirs"); |
michael@0 | 516 | } |
michael@0 | 517 | |
michael@0 | 518 | function isItemInAddonsList(aType, aDir, aId) { |
michael@0 | 519 | var path = aDir.clone(); |
michael@0 | 520 | path.append(aId); |
michael@0 | 521 | var xpiPath = aDir.clone(); |
michael@0 | 522 | xpiPath.append(aId + ".xpi"); |
michael@0 | 523 | for (var i = 0; i < gAddonsList[aType].length; i++) { |
michael@0 | 524 | let file = gAddonsList[aType][i]; |
michael@0 | 525 | if (!file.exists()) |
michael@0 | 526 | do_throw("Non-existant path found in extensions.ini: " + file.path) |
michael@0 | 527 | if (file.isDirectory() && file.equals(path)) |
michael@0 | 528 | return true; |
michael@0 | 529 | if (file.isFile() && file.equals(xpiPath)) |
michael@0 | 530 | return true; |
michael@0 | 531 | } |
michael@0 | 532 | return false; |
michael@0 | 533 | } |
michael@0 | 534 | |
michael@0 | 535 | function isThemeInAddonsList(aDir, aId) { |
michael@0 | 536 | return isItemInAddonsList("themes", aDir, aId); |
michael@0 | 537 | } |
michael@0 | 538 | |
michael@0 | 539 | function isExtensionInAddonsList(aDir, aId) { |
michael@0 | 540 | return isItemInAddonsList("extensions", aDir, aId); |
michael@0 | 541 | } |
michael@0 | 542 | |
michael@0 | 543 | function check_startup_changes(aType, aIds) { |
michael@0 | 544 | var ids = aIds.slice(0); |
michael@0 | 545 | ids.sort(); |
michael@0 | 546 | var changes = AddonManager.getStartupChanges(aType); |
michael@0 | 547 | changes = changes.filter(function(aEl) /@tests.mozilla.org$/.test(aEl)); |
michael@0 | 548 | changes.sort(); |
michael@0 | 549 | |
michael@0 | 550 | do_check_eq(JSON.stringify(ids), JSON.stringify(changes)); |
michael@0 | 551 | } |
michael@0 | 552 | |
michael@0 | 553 | /** |
michael@0 | 554 | * Escapes any occurances of &, ", < or > with XML entities. |
michael@0 | 555 | * |
michael@0 | 556 | * @param str |
michael@0 | 557 | * The string to escape |
michael@0 | 558 | * @return The escaped string |
michael@0 | 559 | */ |
michael@0 | 560 | function escapeXML(aStr) { |
michael@0 | 561 | return aStr.toString() |
michael@0 | 562 | .replace(/&/g, "&") |
michael@0 | 563 | .replace(/"/g, """) |
michael@0 | 564 | .replace(/</g, "<") |
michael@0 | 565 | .replace(/>/g, ">"); |
michael@0 | 566 | } |
michael@0 | 567 | |
michael@0 | 568 | function writeLocaleStrings(aData) { |
michael@0 | 569 | let rdf = ""; |
michael@0 | 570 | ["name", "description", "creator", "homepageURL"].forEach(function(aProp) { |
michael@0 | 571 | if (aProp in aData) |
michael@0 | 572 | rdf += "<em:" + aProp + ">" + escapeXML(aData[aProp]) + "</em:" + aProp + ">\n"; |
michael@0 | 573 | }); |
michael@0 | 574 | |
michael@0 | 575 | ["developer", "translator", "contributor"].forEach(function(aProp) { |
michael@0 | 576 | if (aProp in aData) { |
michael@0 | 577 | aData[aProp].forEach(function(aValue) { |
michael@0 | 578 | rdf += "<em:" + aProp + ">" + escapeXML(aValue) + "</em:" + aProp + ">\n"; |
michael@0 | 579 | }); |
michael@0 | 580 | } |
michael@0 | 581 | }); |
michael@0 | 582 | return rdf; |
michael@0 | 583 | } |
michael@0 | 584 | |
michael@0 | 585 | function createInstallRDF(aData) { |
michael@0 | 586 | var rdf = '<?xml version="1.0"?>\n'; |
michael@0 | 587 | rdf += '<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"\n' + |
michael@0 | 588 | ' xmlns:em="http://www.mozilla.org/2004/em-rdf#">\n'; |
michael@0 | 589 | rdf += '<Description about="urn:mozilla:install-manifest">\n'; |
michael@0 | 590 | |
michael@0 | 591 | ["id", "version", "type", "internalName", "updateURL", "updateKey", |
michael@0 | 592 | "optionsURL", "optionsType", "aboutURL", "iconURL", "icon64URL", |
michael@0 | 593 | "skinnable", "bootstrap", "strictCompatibility"].forEach(function(aProp) { |
michael@0 | 594 | if (aProp in aData) |
michael@0 | 595 | rdf += "<em:" + aProp + ">" + escapeXML(aData[aProp]) + "</em:" + aProp + ">\n"; |
michael@0 | 596 | }); |
michael@0 | 597 | |
michael@0 | 598 | rdf += writeLocaleStrings(aData); |
michael@0 | 599 | |
michael@0 | 600 | if ("targetPlatforms" in aData) { |
michael@0 | 601 | aData.targetPlatforms.forEach(function(aPlatform) { |
michael@0 | 602 | rdf += "<em:targetPlatform>" + escapeXML(aPlatform) + "</em:targetPlatform>\n"; |
michael@0 | 603 | }); |
michael@0 | 604 | } |
michael@0 | 605 | |
michael@0 | 606 | if ("targetApplications" in aData) { |
michael@0 | 607 | aData.targetApplications.forEach(function(aApp) { |
michael@0 | 608 | rdf += "<em:targetApplication><Description>\n"; |
michael@0 | 609 | ["id", "minVersion", "maxVersion"].forEach(function(aProp) { |
michael@0 | 610 | if (aProp in aApp) |
michael@0 | 611 | rdf += "<em:" + aProp + ">" + escapeXML(aApp[aProp]) + "</em:" + aProp + ">\n"; |
michael@0 | 612 | }); |
michael@0 | 613 | rdf += "</Description></em:targetApplication>\n"; |
michael@0 | 614 | }); |
michael@0 | 615 | } |
michael@0 | 616 | |
michael@0 | 617 | if ("localized" in aData) { |
michael@0 | 618 | aData.localized.forEach(function(aLocalized) { |
michael@0 | 619 | rdf += "<em:localized><Description>\n"; |
michael@0 | 620 | if ("locale" in aLocalized) { |
michael@0 | 621 | aLocalized.locale.forEach(function(aLocaleName) { |
michael@0 | 622 | rdf += "<em:locale>" + escapeXML(aLocaleName) + "</em:locale>\n"; |
michael@0 | 623 | }); |
michael@0 | 624 | } |
michael@0 | 625 | rdf += writeLocaleStrings(aLocalized); |
michael@0 | 626 | rdf += "</Description></em:localized>\n"; |
michael@0 | 627 | }); |
michael@0 | 628 | } |
michael@0 | 629 | |
michael@0 | 630 | rdf += "</Description>\n</RDF>\n"; |
michael@0 | 631 | return rdf; |
michael@0 | 632 | } |
michael@0 | 633 | |
michael@0 | 634 | /** |
michael@0 | 635 | * Writes an install.rdf manifest into a directory using the properties passed |
michael@0 | 636 | * in a JS object. The objects should contain a property for each property to |
michael@0 | 637 | * appear in the RDFThe object may contain an array of objects with id, |
michael@0 | 638 | * minVersion and maxVersion in the targetApplications property to give target |
michael@0 | 639 | * application compatibility. |
michael@0 | 640 | * |
michael@0 | 641 | * @param aData |
michael@0 | 642 | * The object holding data about the add-on |
michael@0 | 643 | * @param aDir |
michael@0 | 644 | * The directory to add the install.rdf to |
michael@0 | 645 | * @param aExtraFile |
michael@0 | 646 | * An optional dummy file to create in the directory |
michael@0 | 647 | */ |
michael@0 | 648 | function writeInstallRDFToDir(aData, aDir, aExtraFile) { |
michael@0 | 649 | var rdf = createInstallRDF(aData); |
michael@0 | 650 | if (!aDir.exists()) |
michael@0 | 651 | aDir.create(AM_Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); |
michael@0 | 652 | var file = aDir.clone(); |
michael@0 | 653 | file.append("install.rdf"); |
michael@0 | 654 | if (file.exists()) |
michael@0 | 655 | file.remove(true); |
michael@0 | 656 | var fos = AM_Cc["@mozilla.org/network/file-output-stream;1"]. |
michael@0 | 657 | createInstance(AM_Ci.nsIFileOutputStream); |
michael@0 | 658 | fos.init(file, |
michael@0 | 659 | FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | FileUtils.MODE_TRUNCATE, |
michael@0 | 660 | FileUtils.PERMS_FILE, 0); |
michael@0 | 661 | fos.write(rdf, rdf.length); |
michael@0 | 662 | fos.close(); |
michael@0 | 663 | |
michael@0 | 664 | if (!aExtraFile) |
michael@0 | 665 | return; |
michael@0 | 666 | |
michael@0 | 667 | file = aDir.clone(); |
michael@0 | 668 | file.append(aExtraFile); |
michael@0 | 669 | file.create(AM_Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE); |
michael@0 | 670 | } |
michael@0 | 671 | |
michael@0 | 672 | /** |
michael@0 | 673 | * Writes an install.rdf manifest into an extension using the properties passed |
michael@0 | 674 | * in a JS object. The objects should contain a property for each property to |
michael@0 | 675 | * appear in the RDFThe object may contain an array of objects with id, |
michael@0 | 676 | * minVersion and maxVersion in the targetApplications property to give target |
michael@0 | 677 | * application compatibility. |
michael@0 | 678 | * |
michael@0 | 679 | * @param aData |
michael@0 | 680 | * The object holding data about the add-on |
michael@0 | 681 | * @param aDir |
michael@0 | 682 | * The install directory to add the extension to |
michael@0 | 683 | * @param aId |
michael@0 | 684 | * An optional string to override the default installation aId |
michael@0 | 685 | * @param aExtraFile |
michael@0 | 686 | * An optional dummy file to create in the extension |
michael@0 | 687 | * @return A file pointing to where the extension was installed |
michael@0 | 688 | */ |
michael@0 | 689 | function writeInstallRDFForExtension(aData, aDir, aId, aExtraFile) { |
michael@0 | 690 | var id = aId ? aId : aData.id |
michael@0 | 691 | |
michael@0 | 692 | var dir = aDir.clone(); |
michael@0 | 693 | |
michael@0 | 694 | if (TEST_UNPACKED) { |
michael@0 | 695 | dir.append(id); |
michael@0 | 696 | writeInstallRDFToDir(aData, dir, aExtraFile); |
michael@0 | 697 | return dir; |
michael@0 | 698 | } |
michael@0 | 699 | |
michael@0 | 700 | if (!dir.exists()) |
michael@0 | 701 | dir.create(AM_Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); |
michael@0 | 702 | dir.append(id + ".xpi"); |
michael@0 | 703 | var rdf = createInstallRDF(aData); |
michael@0 | 704 | var stream = AM_Cc["@mozilla.org/io/string-input-stream;1"]. |
michael@0 | 705 | createInstance(AM_Ci.nsIStringInputStream); |
michael@0 | 706 | stream.setData(rdf, -1); |
michael@0 | 707 | var zipW = AM_Cc["@mozilla.org/zipwriter;1"]. |
michael@0 | 708 | createInstance(AM_Ci.nsIZipWriter); |
michael@0 | 709 | zipW.open(dir, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | FileUtils.MODE_TRUNCATE); |
michael@0 | 710 | zipW.addEntryStream("install.rdf", 0, AM_Ci.nsIZipWriter.COMPRESSION_NONE, |
michael@0 | 711 | stream, false); |
michael@0 | 712 | if (aExtraFile) |
michael@0 | 713 | zipW.addEntryStream(aExtraFile, 0, AM_Ci.nsIZipWriter.COMPRESSION_NONE, |
michael@0 | 714 | stream, false); |
michael@0 | 715 | zipW.close(); |
michael@0 | 716 | return dir; |
michael@0 | 717 | } |
michael@0 | 718 | |
michael@0 | 719 | /** |
michael@0 | 720 | * Sets the last modified time of the extension, usually to trigger an update |
michael@0 | 721 | * of its metadata. If the extension is unpacked, this function assumes that |
michael@0 | 722 | * the extension contains only the install.rdf file. |
michael@0 | 723 | * |
michael@0 | 724 | * @param aExt a file pointing to either the packed extension or its unpacked directory. |
michael@0 | 725 | * @param aTime the time to which we set the lastModifiedTime of the extension |
michael@0 | 726 | * |
michael@0 | 727 | * @deprecated Please use promiseSetExtensionModifiedTime instead |
michael@0 | 728 | */ |
michael@0 | 729 | function setExtensionModifiedTime(aExt, aTime) { |
michael@0 | 730 | aExt.lastModifiedTime = aTime; |
michael@0 | 731 | if (aExt.isDirectory()) { |
michael@0 | 732 | let entries = aExt.directoryEntries |
michael@0 | 733 | .QueryInterface(AM_Ci.nsIDirectoryEnumerator); |
michael@0 | 734 | while (entries.hasMoreElements()) |
michael@0 | 735 | setExtensionModifiedTime(entries.nextFile, aTime); |
michael@0 | 736 | entries.close(); |
michael@0 | 737 | } |
michael@0 | 738 | } |
michael@0 | 739 | function promiseSetExtensionModifiedTime(aPath, aTime) { |
michael@0 | 740 | return Task.spawn(function* () { |
michael@0 | 741 | yield OS.File.setDates(aPath, aTime, aTime); |
michael@0 | 742 | let entries, iterator; |
michael@0 | 743 | try { |
michael@0 | 744 | let iterator = new OS.File.DirectoryIterator(aPath); |
michael@0 | 745 | entries = yield iterator.nextBatch(); |
michael@0 | 746 | } catch (ex if ex instanceof OS.File.Error) { |
michael@0 | 747 | return; |
michael@0 | 748 | } finally { |
michael@0 | 749 | if (iterator) { |
michael@0 | 750 | iterator.close(); |
michael@0 | 751 | } |
michael@0 | 752 | } |
michael@0 | 753 | for (let entry of entries) { |
michael@0 | 754 | yield promiseSetExtensionModifiedTime(entry.path, aTime); |
michael@0 | 755 | } |
michael@0 | 756 | }); |
michael@0 | 757 | } |
michael@0 | 758 | |
michael@0 | 759 | /** |
michael@0 | 760 | * Manually installs an XPI file into an install location by either copying the |
michael@0 | 761 | * XPI there or extracting it depending on whether unpacking is being tested |
michael@0 | 762 | * or not. |
michael@0 | 763 | * |
michael@0 | 764 | * @param aXPIFile |
michael@0 | 765 | * The XPI file to install. |
michael@0 | 766 | * @param aInstallLocation |
michael@0 | 767 | * The install location (an nsIFile) to install into. |
michael@0 | 768 | * @param aID |
michael@0 | 769 | * The ID to install as. |
michael@0 | 770 | */ |
michael@0 | 771 | function manuallyInstall(aXPIFile, aInstallLocation, aID) { |
michael@0 | 772 | if (TEST_UNPACKED) { |
michael@0 | 773 | let dir = aInstallLocation.clone(); |
michael@0 | 774 | dir.append(aID); |
michael@0 | 775 | dir.create(AM_Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); |
michael@0 | 776 | let zip = AM_Cc["@mozilla.org/libjar/zip-reader;1"]. |
michael@0 | 777 | createInstance(AM_Ci.nsIZipReader); |
michael@0 | 778 | zip.open(aXPIFile); |
michael@0 | 779 | let entries = zip.findEntries(null); |
michael@0 | 780 | while (entries.hasMore()) { |
michael@0 | 781 | let entry = entries.getNext(); |
michael@0 | 782 | let target = dir.clone(); |
michael@0 | 783 | entry.split("/").forEach(function(aPart) { |
michael@0 | 784 | target.append(aPart); |
michael@0 | 785 | }); |
michael@0 | 786 | zip.extract(entry, target); |
michael@0 | 787 | } |
michael@0 | 788 | zip.close(); |
michael@0 | 789 | |
michael@0 | 790 | return dir; |
michael@0 | 791 | } |
michael@0 | 792 | else { |
michael@0 | 793 | let target = aInstallLocation.clone(); |
michael@0 | 794 | target.append(aID + ".xpi"); |
michael@0 | 795 | aXPIFile.copyTo(target.parent, target.leafName); |
michael@0 | 796 | return target; |
michael@0 | 797 | } |
michael@0 | 798 | } |
michael@0 | 799 | |
michael@0 | 800 | /** |
michael@0 | 801 | * Manually uninstalls an add-on by removing its files from the install |
michael@0 | 802 | * location. |
michael@0 | 803 | * |
michael@0 | 804 | * @param aInstallLocation |
michael@0 | 805 | * The nsIFile of the install location to remove from. |
michael@0 | 806 | * @param aID |
michael@0 | 807 | * The ID of the add-on to remove. |
michael@0 | 808 | */ |
michael@0 | 809 | function manuallyUninstall(aInstallLocation, aID) { |
michael@0 | 810 | let file = getFileForAddon(aInstallLocation, aID); |
michael@0 | 811 | |
michael@0 | 812 | // In reality because the app is restarted a flush isn't necessary for XPIs |
michael@0 | 813 | // removed outside the app, but for testing we must flush manually. |
michael@0 | 814 | if (file.isFile()) |
michael@0 | 815 | Services.obs.notifyObservers(file, "flush-cache-entry", null); |
michael@0 | 816 | |
michael@0 | 817 | file.remove(true); |
michael@0 | 818 | } |
michael@0 | 819 | |
michael@0 | 820 | /** |
michael@0 | 821 | * Gets the nsIFile for where an add-on is installed. It may point to a file or |
michael@0 | 822 | * a directory depending on whether add-ons are being installed unpacked or not. |
michael@0 | 823 | * |
michael@0 | 824 | * @param aDir |
michael@0 | 825 | * The nsIFile for the install location |
michael@0 | 826 | * @param aId |
michael@0 | 827 | * The ID of the add-on |
michael@0 | 828 | * @return an nsIFile |
michael@0 | 829 | */ |
michael@0 | 830 | function getFileForAddon(aDir, aId) { |
michael@0 | 831 | var dir = aDir.clone(); |
michael@0 | 832 | dir.append(do_get_expected_addon_name(aId)); |
michael@0 | 833 | return dir; |
michael@0 | 834 | } |
michael@0 | 835 | |
michael@0 | 836 | function registerDirectory(aKey, aDir) { |
michael@0 | 837 | var dirProvider = { |
michael@0 | 838 | getFile: function(aProp, aPersistent) { |
michael@0 | 839 | aPersistent.value = true; |
michael@0 | 840 | if (aProp == aKey) |
michael@0 | 841 | return aDir.clone(); |
michael@0 | 842 | return null; |
michael@0 | 843 | }, |
michael@0 | 844 | |
michael@0 | 845 | QueryInterface: XPCOMUtils.generateQI([AM_Ci.nsIDirectoryServiceProvider, |
michael@0 | 846 | AM_Ci.nsISupports]) |
michael@0 | 847 | }; |
michael@0 | 848 | Services.dirsvc.registerProvider(dirProvider); |
michael@0 | 849 | } |
michael@0 | 850 | |
michael@0 | 851 | var gExpectedEvents = {}; |
michael@0 | 852 | var gExpectedInstalls = []; |
michael@0 | 853 | var gNext = null; |
michael@0 | 854 | |
michael@0 | 855 | function getExpectedEvent(aId) { |
michael@0 | 856 | if (!(aId in gExpectedEvents)) |
michael@0 | 857 | do_throw("Wasn't expecting events for " + aId); |
michael@0 | 858 | if (gExpectedEvents[aId].length == 0) |
michael@0 | 859 | do_throw("Too many events for " + aId); |
michael@0 | 860 | let event = gExpectedEvents[aId].shift(); |
michael@0 | 861 | if (event instanceof Array) |
michael@0 | 862 | return event; |
michael@0 | 863 | return [event, true]; |
michael@0 | 864 | } |
michael@0 | 865 | |
michael@0 | 866 | function getExpectedInstall(aAddon) { |
michael@0 | 867 | if (gExpectedInstalls instanceof Array) |
michael@0 | 868 | return gExpectedInstalls.shift(); |
michael@0 | 869 | if (!aAddon || !aAddon.id) |
michael@0 | 870 | return gExpectedInstalls["NO_ID"].shift(); |
michael@0 | 871 | let id = aAddon.id; |
michael@0 | 872 | if (!(id in gExpectedInstalls) || !(gExpectedInstalls[id] instanceof Array)) |
michael@0 | 873 | do_throw("Wasn't expecting events for " + id); |
michael@0 | 874 | if (gExpectedInstalls[id].length == 0) |
michael@0 | 875 | do_throw("Too many events for " + id); |
michael@0 | 876 | return gExpectedInstalls[id].shift(); |
michael@0 | 877 | } |
michael@0 | 878 | |
michael@0 | 879 | const AddonListener = { |
michael@0 | 880 | onPropertyChanged: function(aAddon, aProperties) { |
michael@0 | 881 | let [event, properties] = getExpectedEvent(aAddon.id); |
michael@0 | 882 | do_check_eq("onPropertyChanged", event); |
michael@0 | 883 | do_check_eq(aProperties.length, properties.length); |
michael@0 | 884 | properties.forEach(function(aProperty) { |
michael@0 | 885 | // Only test that the expected properties are listed, having additional |
michael@0 | 886 | // properties listed is not necessary a problem |
michael@0 | 887 | if (aProperties.indexOf(aProperty) == -1) |
michael@0 | 888 | do_throw("Did not see property change for " + aProperty); |
michael@0 | 889 | }); |
michael@0 | 890 | return check_test_completed(arguments); |
michael@0 | 891 | }, |
michael@0 | 892 | |
michael@0 | 893 | onEnabling: function(aAddon, aRequiresRestart) { |
michael@0 | 894 | let [event, expectedRestart] = getExpectedEvent(aAddon.id); |
michael@0 | 895 | do_check_eq("onEnabling", event); |
michael@0 | 896 | do_check_eq(aRequiresRestart, expectedRestart); |
michael@0 | 897 | if (expectedRestart) |
michael@0 | 898 | do_check_true(hasFlag(aAddon.pendingOperations, AddonManager.PENDING_ENABLE)); |
michael@0 | 899 | do_check_false(hasFlag(aAddon.permissions, AddonManager.PERM_CAN_ENABLE)); |
michael@0 | 900 | return check_test_completed(arguments); |
michael@0 | 901 | }, |
michael@0 | 902 | |
michael@0 | 903 | onEnabled: function(aAddon) { |
michael@0 | 904 | let [event, expectedRestart] = getExpectedEvent(aAddon.id); |
michael@0 | 905 | do_check_eq("onEnabled", event); |
michael@0 | 906 | do_check_false(hasFlag(aAddon.permissions, AddonManager.PERM_CAN_ENABLE)); |
michael@0 | 907 | return check_test_completed(arguments); |
michael@0 | 908 | }, |
michael@0 | 909 | |
michael@0 | 910 | onDisabling: function(aAddon, aRequiresRestart) { |
michael@0 | 911 | let [event, expectedRestart] = getExpectedEvent(aAddon.id); |
michael@0 | 912 | do_check_eq("onDisabling", event); |
michael@0 | 913 | do_check_eq(aRequiresRestart, expectedRestart); |
michael@0 | 914 | if (expectedRestart) |
michael@0 | 915 | do_check_true(hasFlag(aAddon.pendingOperations, AddonManager.PENDING_DISABLE)); |
michael@0 | 916 | do_check_false(hasFlag(aAddon.permissions, AddonManager.PERM_CAN_DISABLE)); |
michael@0 | 917 | return check_test_completed(arguments); |
michael@0 | 918 | }, |
michael@0 | 919 | |
michael@0 | 920 | onDisabled: function(aAddon) { |
michael@0 | 921 | let [event, expectedRestart] = getExpectedEvent(aAddon.id); |
michael@0 | 922 | do_check_eq("onDisabled", event); |
michael@0 | 923 | do_check_false(hasFlag(aAddon.permissions, AddonManager.PERM_CAN_DISABLE)); |
michael@0 | 924 | return check_test_completed(arguments); |
michael@0 | 925 | }, |
michael@0 | 926 | |
michael@0 | 927 | onInstalling: function(aAddon, aRequiresRestart) { |
michael@0 | 928 | let [event, expectedRestart] = getExpectedEvent(aAddon.id); |
michael@0 | 929 | do_check_eq("onInstalling", event); |
michael@0 | 930 | do_check_eq(aRequiresRestart, expectedRestart); |
michael@0 | 931 | if (expectedRestart) |
michael@0 | 932 | do_check_true(hasFlag(aAddon.pendingOperations, AddonManager.PENDING_INSTALL)); |
michael@0 | 933 | return check_test_completed(arguments); |
michael@0 | 934 | }, |
michael@0 | 935 | |
michael@0 | 936 | onInstalled: function(aAddon) { |
michael@0 | 937 | let [event, expectedRestart] = getExpectedEvent(aAddon.id); |
michael@0 | 938 | do_check_eq("onInstalled", event); |
michael@0 | 939 | return check_test_completed(arguments); |
michael@0 | 940 | }, |
michael@0 | 941 | |
michael@0 | 942 | onUninstalling: function(aAddon, aRequiresRestart) { |
michael@0 | 943 | let [event, expectedRestart] = getExpectedEvent(aAddon.id); |
michael@0 | 944 | do_check_eq("onUninstalling", event); |
michael@0 | 945 | do_check_eq(aRequiresRestart, expectedRestart); |
michael@0 | 946 | if (expectedRestart) |
michael@0 | 947 | do_check_true(hasFlag(aAddon.pendingOperations, AddonManager.PENDING_UNINSTALL)); |
michael@0 | 948 | return check_test_completed(arguments); |
michael@0 | 949 | }, |
michael@0 | 950 | |
michael@0 | 951 | onUninstalled: function(aAddon) { |
michael@0 | 952 | let [event, expectedRestart] = getExpectedEvent(aAddon.id); |
michael@0 | 953 | do_check_eq("onUninstalled", event); |
michael@0 | 954 | return check_test_completed(arguments); |
michael@0 | 955 | }, |
michael@0 | 956 | |
michael@0 | 957 | onOperationCancelled: function(aAddon) { |
michael@0 | 958 | let [event, expectedRestart] = getExpectedEvent(aAddon.id); |
michael@0 | 959 | do_check_eq("onOperationCancelled", event); |
michael@0 | 960 | return check_test_completed(arguments); |
michael@0 | 961 | } |
michael@0 | 962 | }; |
michael@0 | 963 | |
michael@0 | 964 | const InstallListener = { |
michael@0 | 965 | onNewInstall: function(install) { |
michael@0 | 966 | if (install.state != AddonManager.STATE_DOWNLOADED && |
michael@0 | 967 | install.state != AddonManager.STATE_AVAILABLE) |
michael@0 | 968 | do_throw("Bad install state " + install.state); |
michael@0 | 969 | do_check_eq(install.error, 0); |
michael@0 | 970 | do_check_eq("onNewInstall", getExpectedInstall()); |
michael@0 | 971 | return check_test_completed(arguments); |
michael@0 | 972 | }, |
michael@0 | 973 | |
michael@0 | 974 | onDownloadStarted: function(install) { |
michael@0 | 975 | do_check_eq(install.state, AddonManager.STATE_DOWNLOADING); |
michael@0 | 976 | do_check_eq(install.error, 0); |
michael@0 | 977 | do_check_eq("onDownloadStarted", getExpectedInstall()); |
michael@0 | 978 | return check_test_completed(arguments); |
michael@0 | 979 | }, |
michael@0 | 980 | |
michael@0 | 981 | onDownloadEnded: function(install) { |
michael@0 | 982 | do_check_eq(install.state, AddonManager.STATE_DOWNLOADED); |
michael@0 | 983 | do_check_eq(install.error, 0); |
michael@0 | 984 | do_check_eq("onDownloadEnded", getExpectedInstall()); |
michael@0 | 985 | return check_test_completed(arguments); |
michael@0 | 986 | }, |
michael@0 | 987 | |
michael@0 | 988 | onDownloadFailed: function(install) { |
michael@0 | 989 | do_check_eq(install.state, AddonManager.STATE_DOWNLOAD_FAILED); |
michael@0 | 990 | do_check_eq("onDownloadFailed", getExpectedInstall()); |
michael@0 | 991 | return check_test_completed(arguments); |
michael@0 | 992 | }, |
michael@0 | 993 | |
michael@0 | 994 | onDownloadCancelled: function(install) { |
michael@0 | 995 | do_check_eq(install.state, AddonManager.STATE_CANCELLED); |
michael@0 | 996 | do_check_eq(install.error, 0); |
michael@0 | 997 | do_check_eq("onDownloadCancelled", getExpectedInstall()); |
michael@0 | 998 | return check_test_completed(arguments); |
michael@0 | 999 | }, |
michael@0 | 1000 | |
michael@0 | 1001 | onInstallStarted: function(install) { |
michael@0 | 1002 | do_check_eq(install.state, AddonManager.STATE_INSTALLING); |
michael@0 | 1003 | do_check_eq(install.error, 0); |
michael@0 | 1004 | do_check_eq("onInstallStarted", getExpectedInstall(install.addon)); |
michael@0 | 1005 | return check_test_completed(arguments); |
michael@0 | 1006 | }, |
michael@0 | 1007 | |
michael@0 | 1008 | onInstallEnded: function(install, newAddon) { |
michael@0 | 1009 | do_check_eq(install.state, AddonManager.STATE_INSTALLED); |
michael@0 | 1010 | do_check_eq(install.error, 0); |
michael@0 | 1011 | do_check_eq("onInstallEnded", getExpectedInstall(install.addon)); |
michael@0 | 1012 | return check_test_completed(arguments); |
michael@0 | 1013 | }, |
michael@0 | 1014 | |
michael@0 | 1015 | onInstallFailed: function(install) { |
michael@0 | 1016 | do_check_eq(install.state, AddonManager.STATE_INSTALL_FAILED); |
michael@0 | 1017 | do_check_eq("onInstallFailed", getExpectedInstall(install.addon)); |
michael@0 | 1018 | return check_test_completed(arguments); |
michael@0 | 1019 | }, |
michael@0 | 1020 | |
michael@0 | 1021 | onInstallCancelled: function(install) { |
michael@0 | 1022 | // If the install was cancelled by a listener returning false from |
michael@0 | 1023 | // onInstallStarted, then the state will revert to STATE_DOWNLOADED. |
michael@0 | 1024 | let possibleStates = [AddonManager.STATE_CANCELLED, |
michael@0 | 1025 | AddonManager.STATE_DOWNLOADED]; |
michael@0 | 1026 | do_check_true(possibleStates.indexOf(install.state) != -1); |
michael@0 | 1027 | do_check_eq(install.error, 0); |
michael@0 | 1028 | do_check_eq("onInstallCancelled", getExpectedInstall(install.addon)); |
michael@0 | 1029 | return check_test_completed(arguments); |
michael@0 | 1030 | }, |
michael@0 | 1031 | |
michael@0 | 1032 | onExternalInstall: function(aAddon, existingAddon, aRequiresRestart) { |
michael@0 | 1033 | do_check_eq("onExternalInstall", getExpectedInstall(aAddon)); |
michael@0 | 1034 | do_check_false(aRequiresRestart); |
michael@0 | 1035 | return check_test_completed(arguments); |
michael@0 | 1036 | } |
michael@0 | 1037 | }; |
michael@0 | 1038 | |
michael@0 | 1039 | function hasFlag(aBits, aFlag) { |
michael@0 | 1040 | return (aBits & aFlag) != 0; |
michael@0 | 1041 | } |
michael@0 | 1042 | |
michael@0 | 1043 | // Just a wrapper around setting the expected events |
michael@0 | 1044 | function prepare_test(aExpectedEvents, aExpectedInstalls, aNext) { |
michael@0 | 1045 | AddonManager.addAddonListener(AddonListener); |
michael@0 | 1046 | AddonManager.addInstallListener(InstallListener); |
michael@0 | 1047 | |
michael@0 | 1048 | gExpectedInstalls = aExpectedInstalls; |
michael@0 | 1049 | gExpectedEvents = aExpectedEvents; |
michael@0 | 1050 | gNext = aNext; |
michael@0 | 1051 | } |
michael@0 | 1052 | |
michael@0 | 1053 | // Checks if all expected events have been seen and if so calls the callback |
michael@0 | 1054 | function check_test_completed(aArgs) { |
michael@0 | 1055 | if (!gNext) |
michael@0 | 1056 | return undefined; |
michael@0 | 1057 | |
michael@0 | 1058 | if (gExpectedInstalls instanceof Array && |
michael@0 | 1059 | gExpectedInstalls.length > 0) |
michael@0 | 1060 | return undefined; |
michael@0 | 1061 | else for each (let installList in gExpectedInstalls) { |
michael@0 | 1062 | if (installList.length > 0) |
michael@0 | 1063 | return undefined; |
michael@0 | 1064 | } |
michael@0 | 1065 | |
michael@0 | 1066 | for (let id in gExpectedEvents) { |
michael@0 | 1067 | if (gExpectedEvents[id].length > 0) |
michael@0 | 1068 | return undefined; |
michael@0 | 1069 | } |
michael@0 | 1070 | |
michael@0 | 1071 | return gNext.apply(null, aArgs); |
michael@0 | 1072 | } |
michael@0 | 1073 | |
michael@0 | 1074 | // Verifies that all the expected events for all add-ons were seen |
michael@0 | 1075 | function ensure_test_completed() { |
michael@0 | 1076 | for (let i in gExpectedEvents) { |
michael@0 | 1077 | if (gExpectedEvents[i].length > 0) |
michael@0 | 1078 | do_throw("Didn't see all the expected events for " + i); |
michael@0 | 1079 | } |
michael@0 | 1080 | gExpectedEvents = {}; |
michael@0 | 1081 | if (gExpectedInstalls) |
michael@0 | 1082 | do_check_eq(gExpectedInstalls.length, 0); |
michael@0 | 1083 | } |
michael@0 | 1084 | |
michael@0 | 1085 | /** |
michael@0 | 1086 | * A helper method to install an array of AddonInstall to completion and then |
michael@0 | 1087 | * call a provided callback. |
michael@0 | 1088 | * |
michael@0 | 1089 | * @param aInstalls |
michael@0 | 1090 | * The array of AddonInstalls to install |
michael@0 | 1091 | * @param aCallback |
michael@0 | 1092 | * The callback to call when all installs have finished |
michael@0 | 1093 | */ |
michael@0 | 1094 | function completeAllInstalls(aInstalls, aCallback) { |
michael@0 | 1095 | let count = aInstalls.length; |
michael@0 | 1096 | |
michael@0 | 1097 | if (count == 0) { |
michael@0 | 1098 | aCallback(); |
michael@0 | 1099 | return; |
michael@0 | 1100 | } |
michael@0 | 1101 | |
michael@0 | 1102 | function installCompleted(aInstall) { |
michael@0 | 1103 | aInstall.removeListener(listener); |
michael@0 | 1104 | |
michael@0 | 1105 | if (--count == 0) |
michael@0 | 1106 | do_execute_soon(aCallback); |
michael@0 | 1107 | } |
michael@0 | 1108 | |
michael@0 | 1109 | let listener = { |
michael@0 | 1110 | onDownloadFailed: installCompleted, |
michael@0 | 1111 | onDownloadCancelled: installCompleted, |
michael@0 | 1112 | onInstallFailed: installCompleted, |
michael@0 | 1113 | onInstallCancelled: installCompleted, |
michael@0 | 1114 | onInstallEnded: installCompleted |
michael@0 | 1115 | }; |
michael@0 | 1116 | |
michael@0 | 1117 | aInstalls.forEach(function(aInstall) { |
michael@0 | 1118 | aInstall.addListener(listener); |
michael@0 | 1119 | aInstall.install(); |
michael@0 | 1120 | }); |
michael@0 | 1121 | } |
michael@0 | 1122 | |
michael@0 | 1123 | /** |
michael@0 | 1124 | * A helper method to install an array of files and call a callback after the |
michael@0 | 1125 | * installs are completed. |
michael@0 | 1126 | * |
michael@0 | 1127 | * @param aFiles |
michael@0 | 1128 | * The array of files to install |
michael@0 | 1129 | * @param aCallback |
michael@0 | 1130 | * The callback to call when all installs have finished |
michael@0 | 1131 | * @param aIgnoreIncompatible |
michael@0 | 1132 | * Optional parameter to ignore add-ons that are incompatible in |
michael@0 | 1133 | * aome way with the application |
michael@0 | 1134 | */ |
michael@0 | 1135 | function installAllFiles(aFiles, aCallback, aIgnoreIncompatible) { |
michael@0 | 1136 | let count = aFiles.length; |
michael@0 | 1137 | let installs = []; |
michael@0 | 1138 | function callback() { |
michael@0 | 1139 | if (aCallback) { |
michael@0 | 1140 | aCallback(); |
michael@0 | 1141 | } |
michael@0 | 1142 | } |
michael@0 | 1143 | aFiles.forEach(function(aFile) { |
michael@0 | 1144 | AddonManager.getInstallForFile(aFile, function(aInstall) { |
michael@0 | 1145 | if (!aInstall) |
michael@0 | 1146 | do_throw("No AddonInstall created for " + aFile.path); |
michael@0 | 1147 | do_check_eq(aInstall.state, AddonManager.STATE_DOWNLOADED); |
michael@0 | 1148 | |
michael@0 | 1149 | if (!aIgnoreIncompatible || !aInstall.addon.appDisabled) |
michael@0 | 1150 | installs.push(aInstall); |
michael@0 | 1151 | |
michael@0 | 1152 | if (--count == 0) |
michael@0 | 1153 | completeAllInstalls(installs, callback); |
michael@0 | 1154 | }); |
michael@0 | 1155 | }); |
michael@0 | 1156 | } |
michael@0 | 1157 | |
michael@0 | 1158 | function promiseInstallAllFiles(aFiles, aIgnoreIncompatible) { |
michael@0 | 1159 | let deferred = Promise.defer(); |
michael@0 | 1160 | installAllFiles(aFiles, deferred.resolve, aIgnoreIncompatible); |
michael@0 | 1161 | return deferred.promise; |
michael@0 | 1162 | |
michael@0 | 1163 | } |
michael@0 | 1164 | |
michael@0 | 1165 | if ("nsIWindowsRegKey" in AM_Ci) { |
michael@0 | 1166 | var MockRegistry = { |
michael@0 | 1167 | LOCAL_MACHINE: {}, |
michael@0 | 1168 | CURRENT_USER: {}, |
michael@0 | 1169 | CLASSES_ROOT: {}, |
michael@0 | 1170 | |
michael@0 | 1171 | getRoot: function(aRoot) { |
michael@0 | 1172 | switch (aRoot) { |
michael@0 | 1173 | case AM_Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE: |
michael@0 | 1174 | return MockRegistry.LOCAL_MACHINE; |
michael@0 | 1175 | case AM_Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER: |
michael@0 | 1176 | return MockRegistry.CURRENT_USER; |
michael@0 | 1177 | case AM_Ci.nsIWindowsRegKey.ROOT_KEY_CLASSES_ROOT: |
michael@0 | 1178 | return MockRegistry.CLASSES_ROOT; |
michael@0 | 1179 | default: |
michael@0 | 1180 | do_throw("Unknown root " + aRootKey); |
michael@0 | 1181 | return null; |
michael@0 | 1182 | } |
michael@0 | 1183 | }, |
michael@0 | 1184 | |
michael@0 | 1185 | setValue: function(aRoot, aPath, aName, aValue) { |
michael@0 | 1186 | let rootKey = MockRegistry.getRoot(aRoot); |
michael@0 | 1187 | |
michael@0 | 1188 | if (!(aPath in rootKey)) { |
michael@0 | 1189 | rootKey[aPath] = []; |
michael@0 | 1190 | } |
michael@0 | 1191 | else { |
michael@0 | 1192 | for (let i = 0; i < rootKey[aPath].length; i++) { |
michael@0 | 1193 | if (rootKey[aPath][i].name == aName) { |
michael@0 | 1194 | if (aValue === null) |
michael@0 | 1195 | rootKey[aPath].splice(i, 1); |
michael@0 | 1196 | else |
michael@0 | 1197 | rootKey[aPath][i].value = aValue; |
michael@0 | 1198 | return; |
michael@0 | 1199 | } |
michael@0 | 1200 | } |
michael@0 | 1201 | } |
michael@0 | 1202 | |
michael@0 | 1203 | if (aValue === null) |
michael@0 | 1204 | return; |
michael@0 | 1205 | |
michael@0 | 1206 | rootKey[aPath].push({ |
michael@0 | 1207 | name: aName, |
michael@0 | 1208 | value: aValue |
michael@0 | 1209 | }); |
michael@0 | 1210 | } |
michael@0 | 1211 | }; |
michael@0 | 1212 | |
michael@0 | 1213 | /** |
michael@0 | 1214 | * This is a mock nsIWindowsRegistry implementation. It only implements the |
michael@0 | 1215 | * methods that the extension manager requires. |
michael@0 | 1216 | */ |
michael@0 | 1217 | function MockWindowsRegKey() { |
michael@0 | 1218 | } |
michael@0 | 1219 | |
michael@0 | 1220 | MockWindowsRegKey.prototype = { |
michael@0 | 1221 | values: null, |
michael@0 | 1222 | |
michael@0 | 1223 | // --- Overridden nsISupports interface functions --- |
michael@0 | 1224 | QueryInterface: XPCOMUtils.generateQI([AM_Ci.nsIWindowsRegKey]), |
michael@0 | 1225 | |
michael@0 | 1226 | // --- Overridden nsIWindowsRegKey interface functions --- |
michael@0 | 1227 | open: function(aRootKey, aRelPath, aMode) { |
michael@0 | 1228 | let rootKey = MockRegistry.getRoot(aRootKey); |
michael@0 | 1229 | |
michael@0 | 1230 | if (!(aRelPath in rootKey)) |
michael@0 | 1231 | rootKey[aRelPath] = []; |
michael@0 | 1232 | this.values = rootKey[aRelPath]; |
michael@0 | 1233 | }, |
michael@0 | 1234 | |
michael@0 | 1235 | close: function() { |
michael@0 | 1236 | this.values = null; |
michael@0 | 1237 | }, |
michael@0 | 1238 | |
michael@0 | 1239 | get valueCount() { |
michael@0 | 1240 | if (!this.values) |
michael@0 | 1241 | throw Components.results.NS_ERROR_FAILURE; |
michael@0 | 1242 | return this.values.length; |
michael@0 | 1243 | }, |
michael@0 | 1244 | |
michael@0 | 1245 | getValueName: function(aIndex) { |
michael@0 | 1246 | if (!this.values || aIndex >= this.values.length) |
michael@0 | 1247 | throw Components.results.NS_ERROR_FAILURE; |
michael@0 | 1248 | return this.values[aIndex].name; |
michael@0 | 1249 | }, |
michael@0 | 1250 | |
michael@0 | 1251 | readStringValue: function(aName) { |
michael@0 | 1252 | for (let value of this.values) { |
michael@0 | 1253 | if (value.name == aName) |
michael@0 | 1254 | return value.value; |
michael@0 | 1255 | } |
michael@0 | 1256 | return null; |
michael@0 | 1257 | } |
michael@0 | 1258 | }; |
michael@0 | 1259 | |
michael@0 | 1260 | var WinRegFactory = { |
michael@0 | 1261 | createInstance: function(aOuter, aIid) { |
michael@0 | 1262 | if (aOuter != null) |
michael@0 | 1263 | throw Components.results.NS_ERROR_NO_AGGREGATION; |
michael@0 | 1264 | |
michael@0 | 1265 | var key = new MockWindowsRegKey(); |
michael@0 | 1266 | return key.QueryInterface(aIid); |
michael@0 | 1267 | } |
michael@0 | 1268 | }; |
michael@0 | 1269 | |
michael@0 | 1270 | var registrar = Components.manager.QueryInterface(AM_Ci.nsIComponentRegistrar); |
michael@0 | 1271 | registrar.registerFactory(Components.ID("{0478de5b-0f38-4edb-851d-4c99f1ed8eba}"), |
michael@0 | 1272 | "Mock Windows Registry Implementation", |
michael@0 | 1273 | "@mozilla.org/windows-registry-key;1", WinRegFactory); |
michael@0 | 1274 | } |
michael@0 | 1275 | |
michael@0 | 1276 | // Get the profile directory for tests to use. |
michael@0 | 1277 | const gProfD = do_get_profile(); |
michael@0 | 1278 | |
michael@0 | 1279 | const EXTENSIONS_DB = "extensions.json"; |
michael@0 | 1280 | let gExtensionsJSON = gProfD.clone(); |
michael@0 | 1281 | gExtensionsJSON.append(EXTENSIONS_DB); |
michael@0 | 1282 | |
michael@0 | 1283 | const EXTENSIONS_INI = "extensions.ini"; |
michael@0 | 1284 | let gExtensionsINI = gProfD.clone(); |
michael@0 | 1285 | gExtensionsINI.append(EXTENSIONS_INI); |
michael@0 | 1286 | |
michael@0 | 1287 | // Enable more extensive EM logging |
michael@0 | 1288 | Services.prefs.setBoolPref("extensions.logging.enabled", true); |
michael@0 | 1289 | |
michael@0 | 1290 | // By default only load extensions from the profile install location |
michael@0 | 1291 | Services.prefs.setIntPref("extensions.enabledScopes", AddonManager.SCOPE_PROFILE); |
michael@0 | 1292 | |
michael@0 | 1293 | // By default don't disable add-ons from any scope |
michael@0 | 1294 | Services.prefs.setIntPref("extensions.autoDisableScopes", 0); |
michael@0 | 1295 | |
michael@0 | 1296 | // By default, don't cache add-ons in AddonRepository.jsm |
michael@0 | 1297 | Services.prefs.setBoolPref("extensions.getAddons.cache.enabled", false); |
michael@0 | 1298 | |
michael@0 | 1299 | // Disable the compatibility updates window by default |
michael@0 | 1300 | Services.prefs.setBoolPref("extensions.showMismatchUI", false); |
michael@0 | 1301 | |
michael@0 | 1302 | // Point update checks to the local machine for fast failures |
michael@0 | 1303 | Services.prefs.setCharPref("extensions.update.url", "http://127.0.0.1/updateURL"); |
michael@0 | 1304 | Services.prefs.setCharPref("extensions.update.background.url", "http://127.0.0.1/updateBackgroundURL"); |
michael@0 | 1305 | Services.prefs.setCharPref("extensions.blocklist.url", "http://127.0.0.1/blocklistURL"); |
michael@0 | 1306 | |
michael@0 | 1307 | // By default ignore bundled add-ons |
michael@0 | 1308 | Services.prefs.setBoolPref("extensions.installDistroAddons", false); |
michael@0 | 1309 | |
michael@0 | 1310 | // By default use strict compatibility |
michael@0 | 1311 | Services.prefs.setBoolPref("extensions.strictCompatibility", true); |
michael@0 | 1312 | |
michael@0 | 1313 | // By default don't check for hotfixes |
michael@0 | 1314 | Services.prefs.setCharPref("extensions.hotfix.id", ""); |
michael@0 | 1315 | |
michael@0 | 1316 | // By default, set min compatible versions to 0 |
michael@0 | 1317 | Services.prefs.setCharPref(PREF_EM_MIN_COMPAT_APP_VERSION, "0"); |
michael@0 | 1318 | Services.prefs.setCharPref(PREF_EM_MIN_COMPAT_PLATFORM_VERSION, "0"); |
michael@0 | 1319 | |
michael@0 | 1320 | // Register a temporary directory for the tests. |
michael@0 | 1321 | const gTmpD = gProfD.clone(); |
michael@0 | 1322 | gTmpD.append("temp"); |
michael@0 | 1323 | gTmpD.create(AM_Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); |
michael@0 | 1324 | registerDirectory("TmpD", gTmpD); |
michael@0 | 1325 | |
michael@0 | 1326 | // Write out an empty blocklist.xml file to the profile to ensure nothing |
michael@0 | 1327 | // is blocklisted by default |
michael@0 | 1328 | var blockFile = gProfD.clone(); |
michael@0 | 1329 | blockFile.append("blocklist.xml"); |
michael@0 | 1330 | var stream = AM_Cc["@mozilla.org/network/file-output-stream;1"]. |
michael@0 | 1331 | createInstance(AM_Ci.nsIFileOutputStream); |
michael@0 | 1332 | stream.init(blockFile, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | FileUtils.MODE_TRUNCATE, |
michael@0 | 1333 | FileUtils.PERMS_FILE, 0); |
michael@0 | 1334 | |
michael@0 | 1335 | var data = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + |
michael@0 | 1336 | "<blocklist xmlns=\"http://www.mozilla.org/2006/addons-blocklist\">\n" + |
michael@0 | 1337 | "</blocklist>\n"; |
michael@0 | 1338 | stream.write(data, data.length); |
michael@0 | 1339 | stream.close(); |
michael@0 | 1340 | |
michael@0 | 1341 | // Copies blocklistFile (an nsIFile) to gProfD/blocklist.xml. |
michael@0 | 1342 | function copyBlocklistToProfile(blocklistFile) { |
michael@0 | 1343 | var dest = gProfD.clone(); |
michael@0 | 1344 | dest.append("blocklist.xml"); |
michael@0 | 1345 | if (dest.exists()) |
michael@0 | 1346 | dest.remove(false); |
michael@0 | 1347 | blocklistFile.copyTo(gProfD, "blocklist.xml"); |
michael@0 | 1348 | dest.lastModifiedTime = Date.now(); |
michael@0 | 1349 | } |
michael@0 | 1350 | |
michael@0 | 1351 | // Throw a failure and attempt to abandon the test if it looks like it is going |
michael@0 | 1352 | // to timeout |
michael@0 | 1353 | function timeout() { |
michael@0 | 1354 | timer = null; |
michael@0 | 1355 | do_throw("Test ran longer than " + TIMEOUT_MS + "ms"); |
michael@0 | 1356 | |
michael@0 | 1357 | // Attempt to bail out of the test |
michael@0 | 1358 | do_test_finished(); |
michael@0 | 1359 | } |
michael@0 | 1360 | |
michael@0 | 1361 | var timer = AM_Cc["@mozilla.org/timer;1"].createInstance(AM_Ci.nsITimer); |
michael@0 | 1362 | timer.init(timeout, TIMEOUT_MS, AM_Ci.nsITimer.TYPE_ONE_SHOT); |
michael@0 | 1363 | |
michael@0 | 1364 | // Make sure that a given path does not exist |
michael@0 | 1365 | function pathShouldntExist(aPath) { |
michael@0 | 1366 | if (aPath.exists()) { |
michael@0 | 1367 | do_throw("Test cleanup: path " + aPath.path + " exists when it should not"); |
michael@0 | 1368 | } |
michael@0 | 1369 | } |
michael@0 | 1370 | |
michael@0 | 1371 | do_register_cleanup(function addon_cleanup() { |
michael@0 | 1372 | if (timer) |
michael@0 | 1373 | timer.cancel(); |
michael@0 | 1374 | |
michael@0 | 1375 | // Check that the temporary directory is empty |
michael@0 | 1376 | var dirEntries = gTmpD.directoryEntries |
michael@0 | 1377 | .QueryInterface(AM_Ci.nsIDirectoryEnumerator); |
michael@0 | 1378 | var entry; |
michael@0 | 1379 | while ((entry = dirEntries.nextFile)) { |
michael@0 | 1380 | do_throw("Found unexpected file in temporary directory: " + entry.leafName); |
michael@0 | 1381 | } |
michael@0 | 1382 | dirEntries.close(); |
michael@0 | 1383 | |
michael@0 | 1384 | var testDir = gProfD.clone(); |
michael@0 | 1385 | testDir.append("extensions"); |
michael@0 | 1386 | testDir.append("trash"); |
michael@0 | 1387 | pathShouldntExist(testDir); |
michael@0 | 1388 | |
michael@0 | 1389 | testDir.leafName = "staged"; |
michael@0 | 1390 | pathShouldntExist(testDir); |
michael@0 | 1391 | |
michael@0 | 1392 | testDir.leafName = "staged-xpis"; |
michael@0 | 1393 | pathShouldntExist(testDir); |
michael@0 | 1394 | |
michael@0 | 1395 | shutdownManager(); |
michael@0 | 1396 | |
michael@0 | 1397 | // Clear commonly set prefs. |
michael@0 | 1398 | try { |
michael@0 | 1399 | Services.prefs.clearUserPref(PREF_EM_CHECK_UPDATE_SECURITY); |
michael@0 | 1400 | } catch (e) {} |
michael@0 | 1401 | try { |
michael@0 | 1402 | Services.prefs.clearUserPref(PREF_EM_STRICT_COMPATIBILITY); |
michael@0 | 1403 | } catch (e) {} |
michael@0 | 1404 | }); |
michael@0 | 1405 | |
michael@0 | 1406 | /** |
michael@0 | 1407 | * Handler function that responds with the interpolated |
michael@0 | 1408 | * static file associated to the URL specified by request.path. |
michael@0 | 1409 | * This replaces the %PORT% entries in the file with the actual |
michael@0 | 1410 | * value of the running server's port (stored in gPort). |
michael@0 | 1411 | */ |
michael@0 | 1412 | function interpolateAndServeFile(request, response) { |
michael@0 | 1413 | try { |
michael@0 | 1414 | let file = gUrlToFileMap[request.path]; |
michael@0 | 1415 | var data = ""; |
michael@0 | 1416 | var fstream = Components.classes["@mozilla.org/network/file-input-stream;1"]. |
michael@0 | 1417 | createInstance(Components.interfaces.nsIFileInputStream); |
michael@0 | 1418 | var cstream = Components.classes["@mozilla.org/intl/converter-input-stream;1"]. |
michael@0 | 1419 | createInstance(Components.interfaces.nsIConverterInputStream); |
michael@0 | 1420 | fstream.init(file, -1, 0, 0); |
michael@0 | 1421 | cstream.init(fstream, "UTF-8", 0, 0); |
michael@0 | 1422 | |
michael@0 | 1423 | let (str = {}) { |
michael@0 | 1424 | let read = 0; |
michael@0 | 1425 | do { |
michael@0 | 1426 | // read as much as we can and put it in str.value |
michael@0 | 1427 | read = cstream.readString(0xffffffff, str); |
michael@0 | 1428 | data += str.value; |
michael@0 | 1429 | } while (read != 0); |
michael@0 | 1430 | } |
michael@0 | 1431 | data = data.replace(/%PORT%/g, gPort); |
michael@0 | 1432 | |
michael@0 | 1433 | response.write(data); |
michael@0 | 1434 | } catch (e) { |
michael@0 | 1435 | do_throw("Exception while serving interpolated file."); |
michael@0 | 1436 | } finally { |
michael@0 | 1437 | cstream.close(); // this closes fstream as well |
michael@0 | 1438 | } |
michael@0 | 1439 | } |
michael@0 | 1440 | |
michael@0 | 1441 | /** |
michael@0 | 1442 | * Sets up a path handler for the given URL and saves the |
michael@0 | 1443 | * corresponding file in the global url -> file map. |
michael@0 | 1444 | * |
michael@0 | 1445 | * @param url |
michael@0 | 1446 | * the actual URL |
michael@0 | 1447 | * @param file |
michael@0 | 1448 | * nsILocalFile representing a static file |
michael@0 | 1449 | */ |
michael@0 | 1450 | function mapUrlToFile(url, file, server) { |
michael@0 | 1451 | server.registerPathHandler(url, interpolateAndServeFile); |
michael@0 | 1452 | gUrlToFileMap[url] = file; |
michael@0 | 1453 | } |
michael@0 | 1454 | |
michael@0 | 1455 | function mapFile(path, server) { |
michael@0 | 1456 | mapUrlToFile(path, do_get_file(path), server); |
michael@0 | 1457 | } |
michael@0 | 1458 | |
michael@0 | 1459 | /** |
michael@0 | 1460 | * Take out the port number in an URL |
michael@0 | 1461 | * |
michael@0 | 1462 | * @param url |
michael@0 | 1463 | * String that represents an URL with a port number in it |
michael@0 | 1464 | */ |
michael@0 | 1465 | function remove_port(url) { |
michael@0 | 1466 | if (typeof url === "string") |
michael@0 | 1467 | return url.replace(/:\d+/, ""); |
michael@0 | 1468 | return url; |
michael@0 | 1469 | } |
michael@0 | 1470 | // Wrap a function (typically a callback) to catch and report exceptions |
michael@0 | 1471 | function do_exception_wrap(func) { |
michael@0 | 1472 | return function() { |
michael@0 | 1473 | try { |
michael@0 | 1474 | func.apply(null, arguments); |
michael@0 | 1475 | } |
michael@0 | 1476 | catch(e) { |
michael@0 | 1477 | do_report_unexpected_exception(e); |
michael@0 | 1478 | } |
michael@0 | 1479 | }; |
michael@0 | 1480 | } |
michael@0 | 1481 | |
michael@0 | 1482 | /** |
michael@0 | 1483 | * Change the schema version of the JSON extensions database |
michael@0 | 1484 | */ |
michael@0 | 1485 | function changeXPIDBVersion(aNewVersion) { |
michael@0 | 1486 | let jData = loadJSON(gExtensionsJSON); |
michael@0 | 1487 | jData.schemaVersion = aNewVersion; |
michael@0 | 1488 | saveJSON(jData, gExtensionsJSON); |
michael@0 | 1489 | } |
michael@0 | 1490 | |
michael@0 | 1491 | /** |
michael@0 | 1492 | * Load a file into a string |
michael@0 | 1493 | */ |
michael@0 | 1494 | function loadFile(aFile) { |
michael@0 | 1495 | let data = ""; |
michael@0 | 1496 | let fstream = Components.classes["@mozilla.org/network/file-input-stream;1"]. |
michael@0 | 1497 | createInstance(Components.interfaces.nsIFileInputStream); |
michael@0 | 1498 | let cstream = Components.classes["@mozilla.org/intl/converter-input-stream;1"]. |
michael@0 | 1499 | createInstance(Components.interfaces.nsIConverterInputStream); |
michael@0 | 1500 | fstream.init(aFile, -1, 0, 0); |
michael@0 | 1501 | cstream.init(fstream, "UTF-8", 0, 0); |
michael@0 | 1502 | let (str = {}) { |
michael@0 | 1503 | let read = 0; |
michael@0 | 1504 | do { |
michael@0 | 1505 | read = cstream.readString(0xffffffff, str); // read as much as we can and put it in str.value |
michael@0 | 1506 | data += str.value; |
michael@0 | 1507 | } while (read != 0); |
michael@0 | 1508 | } |
michael@0 | 1509 | cstream.close(); |
michael@0 | 1510 | return data; |
michael@0 | 1511 | } |
michael@0 | 1512 | |
michael@0 | 1513 | /** |
michael@0 | 1514 | * Raw load of a JSON file |
michael@0 | 1515 | */ |
michael@0 | 1516 | function loadJSON(aFile) { |
michael@0 | 1517 | let data = loadFile(aFile); |
michael@0 | 1518 | do_print("Loaded JSON file " + aFile.path); |
michael@0 | 1519 | return(JSON.parse(data)); |
michael@0 | 1520 | } |
michael@0 | 1521 | |
michael@0 | 1522 | /** |
michael@0 | 1523 | * Raw save of a JSON blob to file |
michael@0 | 1524 | */ |
michael@0 | 1525 | function saveJSON(aData, aFile) { |
michael@0 | 1526 | do_print("Starting to save JSON file " + aFile.path); |
michael@0 | 1527 | let stream = FileUtils.openSafeFileOutputStream(aFile); |
michael@0 | 1528 | let converter = AM_Cc["@mozilla.org/intl/converter-output-stream;1"]. |
michael@0 | 1529 | createInstance(AM_Ci.nsIConverterOutputStream); |
michael@0 | 1530 | converter.init(stream, "UTF-8", 0, 0x0000); |
michael@0 | 1531 | // XXX pretty print the JSON while debugging |
michael@0 | 1532 | converter.writeString(JSON.stringify(aData, null, 2)); |
michael@0 | 1533 | converter.flush(); |
michael@0 | 1534 | // nsConverterOutputStream doesn't finish() safe output streams on close() |
michael@0 | 1535 | FileUtils.closeSafeFileOutputStream(stream); |
michael@0 | 1536 | converter.close(); |
michael@0 | 1537 | do_print("Done saving JSON file " + aFile.path); |
michael@0 | 1538 | } |
michael@0 | 1539 | |
michael@0 | 1540 | /** |
michael@0 | 1541 | * Create a callback function that calls do_execute_soon on an actual callback and arguments |
michael@0 | 1542 | */ |
michael@0 | 1543 | function callback_soon(aFunction) { |
michael@0 | 1544 | return function(...args) { |
michael@0 | 1545 | do_execute_soon(function() { |
michael@0 | 1546 | aFunction.apply(null, args); |
michael@0 | 1547 | }, aFunction.name ? "delayed callback " + aFunction.name : "delayed callback"); |
michael@0 | 1548 | } |
michael@0 | 1549 | } |
michael@0 | 1550 | |
michael@0 | 1551 | /** |
michael@0 | 1552 | * A promise-based variant of AddonManager.getAddonsByIDs. |
michael@0 | 1553 | * |
michael@0 | 1554 | * @param {array} list As the first argument of AddonManager.getAddonsByIDs |
michael@0 | 1555 | * @return {promise} |
michael@0 | 1556 | * @resolve {array} The list of add-ons sent by AddonManaget.getAddonsByIDs to |
michael@0 | 1557 | * its callback. |
michael@0 | 1558 | */ |
michael@0 | 1559 | function promiseAddonsByIDs(list) { |
michael@0 | 1560 | let deferred = Promise.defer(); |
michael@0 | 1561 | AddonManager.getAddonsByIDs(list, deferred.resolve); |
michael@0 | 1562 | return deferred.promise; |
michael@0 | 1563 | } |