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