toolkit/mozapps/extensions/internal/XPIProvider.jsm

changeset 0
6474c204b198
equal deleted inserted replaced
-1:000000000000 0:1c8461483528
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5 "use strict";
6
7 const Cc = Components.classes;
8 const Ci = Components.interfaces;
9 const Cr = Components.results;
10 const Cu = Components.utils;
11
12 this.EXPORTED_SYMBOLS = ["XPIProvider"];
13
14 Components.utils.import("resource://gre/modules/Services.jsm");
15 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
16 Components.utils.import("resource://gre/modules/AddonManager.jsm");
17
18 XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository",
19 "resource://gre/modules/addons/AddonRepository.jsm");
20 XPCOMUtils.defineLazyModuleGetter(this, "ChromeManifestParser",
21 "resource://gre/modules/ChromeManifestParser.jsm");
22 XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager",
23 "resource://gre/modules/LightweightThemeManager.jsm");
24 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
25 "resource://gre/modules/FileUtils.jsm");
26 XPCOMUtils.defineLazyModuleGetter(this, "ZipUtils",
27 "resource://gre/modules/ZipUtils.jsm");
28 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
29 "resource://gre/modules/NetUtil.jsm");
30 XPCOMUtils.defineLazyModuleGetter(this, "PermissionsUtils",
31 "resource://gre/modules/PermissionsUtils.jsm");
32 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
33 "resource://gre/modules/Promise.jsm");
34 XPCOMUtils.defineLazyModuleGetter(this, "Task",
35 "resource://gre/modules/Task.jsm");
36 XPCOMUtils.defineLazyModuleGetter(this, "OS",
37 "resource://gre/modules/osfile.jsm");
38 XPCOMUtils.defineLazyModuleGetter(this, "BrowserToolboxProcess",
39 "resource:///modules/devtools/ToolboxProcess.jsm");
40
41 XPCOMUtils.defineLazyServiceGetter(this,
42 "ChromeRegistry",
43 "@mozilla.org/chrome/chrome-registry;1",
44 "nsIChromeRegistry");
45 XPCOMUtils.defineLazyServiceGetter(this,
46 "ResProtocolHandler",
47 "@mozilla.org/network/protocol;1?name=resource",
48 "nsIResProtocolHandler");
49
50 const nsIFile = Components.Constructor("@mozilla.org/file/local;1", "nsIFile",
51 "initWithPath");
52
53 const PREF_DB_SCHEMA = "extensions.databaseSchema";
54 const PREF_INSTALL_CACHE = "extensions.installCache";
55 const PREF_BOOTSTRAP_ADDONS = "extensions.bootstrappedAddons";
56 const PREF_PENDING_OPERATIONS = "extensions.pendingOperations";
57 const PREF_MATCH_OS_LOCALE = "intl.locale.matchOS";
58 const PREF_SELECTED_LOCALE = "general.useragent.locale";
59 const PREF_EM_DSS_ENABLED = "extensions.dss.enabled";
60 const PREF_DSS_SWITCHPENDING = "extensions.dss.switchPending";
61 const PREF_DSS_SKIN_TO_SELECT = "extensions.lastSelectedSkin";
62 const PREF_GENERAL_SKINS_SELECTEDSKIN = "general.skins.selectedSkin";
63 const PREF_EM_UPDATE_URL = "extensions.update.url";
64 const PREF_EM_UPDATE_BACKGROUND_URL = "extensions.update.background.url";
65 const PREF_EM_ENABLED_ADDONS = "extensions.enabledAddons";
66 const PREF_EM_EXTENSION_FORMAT = "extensions.";
67 const PREF_EM_ENABLED_SCOPES = "extensions.enabledScopes";
68 const PREF_EM_AUTO_DISABLED_SCOPES = "extensions.autoDisableScopes";
69 const PREF_EM_SHOW_MISMATCH_UI = "extensions.showMismatchUI";
70 const PREF_XPI_ENABLED = "xpinstall.enabled";
71 const PREF_XPI_WHITELIST_REQUIRED = "xpinstall.whitelist.required";
72 const PREF_XPI_DIRECT_WHITELISTED = "xpinstall.whitelist.directRequest";
73 const PREF_XPI_FILE_WHITELISTED = "xpinstall.whitelist.fileRequest";
74 const PREF_XPI_PERMISSIONS_BRANCH = "xpinstall.";
75 const PREF_XPI_UNPACK = "extensions.alwaysUnpack";
76 const PREF_INSTALL_REQUIREBUILTINCERTS = "extensions.install.requireBuiltInCerts";
77 const PREF_INSTALL_DISTRO_ADDONS = "extensions.installDistroAddons";
78 const PREF_BRANCH_INSTALLED_ADDON = "extensions.installedDistroAddon.";
79 const PREF_SHOWN_SELECTION_UI = "extensions.shownSelectionUI";
80
81 const PREF_EM_MIN_COMPAT_APP_VERSION = "extensions.minCompatibleAppVersion";
82 const PREF_EM_MIN_COMPAT_PLATFORM_VERSION = "extensions.minCompatiblePlatformVersion";
83
84 const PREF_CHECKCOMAT_THEMEOVERRIDE = "extensions.checkCompatibility.temporaryThemeOverride_minAppVersion";
85
86 const URI_EXTENSION_SELECT_DIALOG = "chrome://mozapps/content/extensions/selectAddons.xul";
87 const URI_EXTENSION_UPDATE_DIALOG = "chrome://mozapps/content/extensions/update.xul";
88 const URI_EXTENSION_STRINGS = "chrome://mozapps/locale/extensions/extensions.properties";
89
90 const STRING_TYPE_NAME = "type.%ID%.name";
91
92 const DIR_EXTENSIONS = "extensions";
93 const DIR_STAGE = "staged";
94 const DIR_XPI_STAGE = "staged-xpis";
95 const DIR_TRASH = "trash";
96
97 const FILE_DATABASE = "extensions.json";
98 const FILE_OLD_CACHE = "extensions.cache";
99 const FILE_INSTALL_MANIFEST = "install.rdf";
100 const FILE_XPI_ADDONS_LIST = "extensions.ini";
101
102 const KEY_PROFILEDIR = "ProfD";
103 const KEY_APPDIR = "XCurProcD";
104 const KEY_TEMPDIR = "TmpD";
105 const KEY_APP_DISTRIBUTION = "XREAppDist";
106
107 const KEY_APP_PROFILE = "app-profile";
108 const KEY_APP_GLOBAL = "app-global";
109 const KEY_APP_SYSTEM_LOCAL = "app-system-local";
110 const KEY_APP_SYSTEM_SHARE = "app-system-share";
111 const KEY_APP_SYSTEM_USER = "app-system-user";
112
113 const NOTIFICATION_FLUSH_PERMISSIONS = "flush-pending-permissions";
114 const XPI_PERMISSION = "install";
115
116 const RDFURI_INSTALL_MANIFEST_ROOT = "urn:mozilla:install-manifest";
117 const PREFIX_NS_EM = "http://www.mozilla.org/2004/em-rdf#";
118
119 const TOOLKIT_ID = "toolkit@mozilla.org";
120
121 // The value for this is in Makefile.in
122 #expand const DB_SCHEMA = __MOZ_EXTENSIONS_DB_SCHEMA__;
123
124 // Properties that exist in the install manifest
125 const PROP_METADATA = ["id", "version", "type", "internalName", "updateURL",
126 "updateKey", "optionsURL", "optionsType", "aboutURL",
127 "iconURL", "icon64URL"];
128 const PROP_LOCALE_SINGLE = ["name", "description", "creator", "homepageURL"];
129 const PROP_LOCALE_MULTI = ["developers", "translators", "contributors"];
130 const PROP_TARGETAPP = ["id", "minVersion", "maxVersion"];
131
132 // Properties that should be migrated where possible from an old database. These
133 // shouldn't include properties that can be read directly from install.rdf files
134 // or calculated
135 const DB_MIGRATE_METADATA= ["installDate", "userDisabled", "softDisabled",
136 "sourceURI", "applyBackgroundUpdates",
137 "releaseNotesURI", "foreignInstall", "syncGUID"];
138 // Properties to cache and reload when an addon installation is pending
139 const PENDING_INSTALL_METADATA =
140 ["syncGUID", "targetApplications", "userDisabled", "softDisabled",
141 "existingAddonID", "sourceURI", "releaseNotesURI", "installDate",
142 "updateDate", "applyBackgroundUpdates", "compatibilityOverrides"];
143
144 // Note: When adding/changing/removing items here, remember to change the
145 // DB schema version to ensure changes are picked up ASAP.
146 const STATIC_BLOCKLIST_PATTERNS = [
147 { creator: "Mozilla Corp.",
148 level: Ci.nsIBlocklistService.STATE_BLOCKED,
149 blockID: "i162" },
150 { creator: "Mozilla.org",
151 level: Ci.nsIBlocklistService.STATE_BLOCKED,
152 blockID: "i162" }
153 ];
154
155
156 const BOOTSTRAP_REASONS = {
157 APP_STARTUP : 1,
158 APP_SHUTDOWN : 2,
159 ADDON_ENABLE : 3,
160 ADDON_DISABLE : 4,
161 ADDON_INSTALL : 5,
162 ADDON_UNINSTALL : 6,
163 ADDON_UPGRADE : 7,
164 ADDON_DOWNGRADE : 8
165 };
166
167 // Map new string type identifiers to old style nsIUpdateItem types
168 const TYPES = {
169 extension: 2,
170 theme: 4,
171 locale: 8,
172 multipackage: 32,
173 dictionary: 64,
174 experiment: 128,
175 };
176
177 const RESTARTLESS_TYPES = new Set([
178 "dictionary",
179 "experiment",
180 "locale",
181 ]);
182
183 // Keep track of where we are in startup for telemetry
184 // event happened during XPIDatabase.startup()
185 const XPI_STARTING = "XPIStarting";
186 // event happened after startup() but before the final-ui-startup event
187 const XPI_BEFORE_UI_STARTUP = "BeforeFinalUIStartup";
188 // event happened after final-ui-startup
189 const XPI_AFTER_UI_STARTUP = "AfterFinalUIStartup";
190
191 const COMPATIBLE_BY_DEFAULT_TYPES = {
192 extension: true,
193 dictionary: true
194 };
195
196 const MSG_JAR_FLUSH = "AddonJarFlush";
197
198 var gGlobalScope = this;
199
200 /**
201 * Valid IDs fit this pattern.
202 */
203 var gIDTest = /^(\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\}|[a-z0-9-\._]*\@[a-z0-9-\._]+)$/i;
204
205 Cu.import("resource://gre/modules/Log.jsm");
206 const LOGGER_ID = "addons.xpi";
207
208 // Create a new logger for use by all objects in this Addons XPI Provider module
209 // (Requires AddonManager.jsm)
210 let logger = Log.repository.getLogger(LOGGER_ID);
211
212 const LAZY_OBJECTS = ["XPIDatabase"];
213
214 var gLazyObjectsLoaded = false;
215
216 function loadLazyObjects() {
217 let scope = {};
218 scope.AddonInternal = AddonInternal;
219 scope.XPIProvider = XPIProvider;
220 Services.scriptloader.loadSubScript("resource://gre/modules/addons/XPIProviderUtils.js",
221 scope);
222
223 for (let name of LAZY_OBJECTS) {
224 delete gGlobalScope[name];
225 gGlobalScope[name] = scope[name];
226 }
227 gLazyObjectsLoaded = true;
228 return scope;
229 }
230
231 for (let name of LAZY_OBJECTS) {
232 Object.defineProperty(gGlobalScope, name, {
233 get: function lazyObjectGetter() {
234 let objs = loadLazyObjects();
235 return objs[name];
236 },
237 configurable: true
238 });
239 }
240
241
242 function findMatchingStaticBlocklistItem(aAddon) {
243 for (let item of STATIC_BLOCKLIST_PATTERNS) {
244 if ("creator" in item && typeof item.creator == "string") {
245 if ((aAddon.defaultLocale && aAddon.defaultLocale.creator == item.creator) ||
246 (aAddon.selectedLocale && aAddon.selectedLocale.creator == item.creator)) {
247 return item;
248 }
249 }
250 }
251 return null;
252 }
253
254
255 /**
256 * Sets permissions on a file
257 *
258 * @param aFile
259 * The file or directory to operate on.
260 * @param aPermissions
261 * The permisions to set
262 */
263 function setFilePermissions(aFile, aPermissions) {
264 try {
265 aFile.permissions = aPermissions;
266 }
267 catch (e) {
268 logger.warn("Failed to set permissions " + aPermissions.toString(8) + " on " +
269 aFile.path, e);
270 }
271 }
272
273 /**
274 * A safe way to install a file or the contents of a directory to a new
275 * directory. The file or directory is moved or copied recursively and if
276 * anything fails an attempt is made to rollback the entire operation. The
277 * operation may also be rolled back to its original state after it has
278 * completed by calling the rollback method.
279 *
280 * Operations can be chained. Calling move or copy multiple times will remember
281 * the whole set and if one fails all of the operations will be rolled back.
282 */
283 function SafeInstallOperation() {
284 this._installedFiles = [];
285 this._createdDirs = [];
286 }
287
288 SafeInstallOperation.prototype = {
289 _installedFiles: null,
290 _createdDirs: null,
291
292 _installFile: function SIO_installFile(aFile, aTargetDirectory, aCopy) {
293 let oldFile = aCopy ? null : aFile.clone();
294 let newFile = aFile.clone();
295 try {
296 if (aCopy)
297 newFile.copyTo(aTargetDirectory, null);
298 else
299 newFile.moveTo(aTargetDirectory, null);
300 }
301 catch (e) {
302 logger.error("Failed to " + (aCopy ? "copy" : "move") + " file " + aFile.path +
303 " to " + aTargetDirectory.path, e);
304 throw e;
305 }
306 this._installedFiles.push({ oldFile: oldFile, newFile: newFile });
307 },
308
309 _installDirectory: function SIO_installDirectory(aDirectory, aTargetDirectory, aCopy) {
310 let newDir = aTargetDirectory.clone();
311 newDir.append(aDirectory.leafName);
312 try {
313 newDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
314 }
315 catch (e) {
316 logger.error("Failed to create directory " + newDir.path, e);
317 throw e;
318 }
319 this._createdDirs.push(newDir);
320
321 // Use a snapshot of the directory contents to avoid possible issues with
322 // iterating over a directory while removing files from it (the YAFFS2
323 // embedded filesystem has this issue, see bug 772238), and to remove
324 // normal files before their resource forks on OSX (see bug 733436).
325 let entries = getDirectoryEntries(aDirectory, true);
326 entries.forEach(function(aEntry) {
327 try {
328 this._installDirEntry(aEntry, newDir, aCopy);
329 }
330 catch (e) {
331 logger.error("Failed to " + (aCopy ? "copy" : "move") + " entry " +
332 aEntry.path, e);
333 throw e;
334 }
335 }, this);
336
337 // If this is only a copy operation then there is nothing else to do
338 if (aCopy)
339 return;
340
341 // The directory should be empty by this point. If it isn't this will throw
342 // and all of the operations will be rolled back
343 try {
344 setFilePermissions(aDirectory, FileUtils.PERMS_DIRECTORY);
345 aDirectory.remove(false);
346 }
347 catch (e) {
348 logger.error("Failed to remove directory " + aDirectory.path, e);
349 throw e;
350 }
351
352 // Note we put the directory move in after all the file moves so the
353 // directory is recreated before all the files are moved back
354 this._installedFiles.push({ oldFile: aDirectory, newFile: newDir });
355 },
356
357 _installDirEntry: function SIO_installDirEntry(aDirEntry, aTargetDirectory, aCopy) {
358 let isDir = null;
359
360 try {
361 isDir = aDirEntry.isDirectory();
362 }
363 catch (e) {
364 // If the file has already gone away then don't worry about it, this can
365 // happen on OSX where the resource fork is automatically moved with the
366 // data fork for the file. See bug 733436.
367 if (e.result == Cr.NS_ERROR_FILE_TARGET_DOES_NOT_EXIST)
368 return;
369
370 logger.error("Failure " + (aCopy ? "copying" : "moving") + " " + aDirEntry.path +
371 " to " + aTargetDirectory.path);
372 throw e;
373 }
374
375 try {
376 if (isDir)
377 this._installDirectory(aDirEntry, aTargetDirectory, aCopy);
378 else
379 this._installFile(aDirEntry, aTargetDirectory, aCopy);
380 }
381 catch (e) {
382 logger.error("Failure " + (aCopy ? "copying" : "moving") + " " + aDirEntry.path +
383 " to " + aTargetDirectory.path);
384 throw e;
385 }
386 },
387
388 /**
389 * Moves a file or directory into a new directory. If an error occurs then all
390 * files that have been moved will be moved back to their original location.
391 *
392 * @param aFile
393 * The file or directory to be moved.
394 * @param aTargetDirectory
395 * The directory to move into, this is expected to be an empty
396 * directory.
397 */
398 move: function SIO_move(aFile, aTargetDirectory) {
399 try {
400 this._installDirEntry(aFile, aTargetDirectory, false);
401 }
402 catch (e) {
403 this.rollback();
404 throw e;
405 }
406 },
407
408 /**
409 * Copies a file or directory into a new directory. If an error occurs then
410 * all new files that have been created will be removed.
411 *
412 * @param aFile
413 * The file or directory to be copied.
414 * @param aTargetDirectory
415 * The directory to copy into, this is expected to be an empty
416 * directory.
417 */
418 copy: function SIO_copy(aFile, aTargetDirectory) {
419 try {
420 this._installDirEntry(aFile, aTargetDirectory, true);
421 }
422 catch (e) {
423 this.rollback();
424 throw e;
425 }
426 },
427
428 /**
429 * Rolls back all the moves that this operation performed. If an exception
430 * occurs here then both old and new directories are left in an indeterminate
431 * state
432 */
433 rollback: function SIO_rollback() {
434 while (this._installedFiles.length > 0) {
435 let move = this._installedFiles.pop();
436 if (move.newFile.isDirectory()) {
437 let oldDir = move.oldFile.parent.clone();
438 oldDir.append(move.oldFile.leafName);
439 oldDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
440 }
441 else if (!move.oldFile) {
442 // No old file means this was a copied file
443 move.newFile.remove(true);
444 }
445 else {
446 move.newFile.moveTo(move.oldFile.parent, null);
447 }
448 }
449
450 while (this._createdDirs.length > 0)
451 recursiveRemove(this._createdDirs.pop());
452 }
453 };
454
455 /**
456 * Gets the currently selected locale for display.
457 * @return the selected locale or "en-US" if none is selected
458 */
459 function getLocale() {
460 if (Prefs.getBoolPref(PREF_MATCH_OS_LOCALE, false))
461 return Services.locale.getLocaleComponentForUserAgent();
462 let locale = Prefs.getComplexValue(PREF_SELECTED_LOCALE, Ci.nsIPrefLocalizedString);
463 if (locale)
464 return locale;
465 return Prefs.getCharPref(PREF_SELECTED_LOCALE, "en-US");
466 }
467
468 /**
469 * Selects the closest matching locale from a list of locales.
470 *
471 * @param aLocales
472 * An array of locales
473 * @return the best match for the currently selected locale
474 */
475 function findClosestLocale(aLocales) {
476 let appLocale = getLocale();
477
478 // Holds the best matching localized resource
479 var bestmatch = null;
480 // The number of locale parts it matched with
481 var bestmatchcount = 0;
482 // The number of locale parts in the match
483 var bestpartcount = 0;
484
485 var matchLocales = [appLocale.toLowerCase()];
486 /* If the current locale is English then it will find a match if there is
487 a valid match for en-US so no point searching that locale too. */
488 if (matchLocales[0].substring(0, 3) != "en-")
489 matchLocales.push("en-us");
490
491 for each (var locale in matchLocales) {
492 var lparts = locale.split("-");
493 for each (var localized in aLocales) {
494 for each (let found in localized.locales) {
495 found = found.toLowerCase();
496 // Exact match is returned immediately
497 if (locale == found)
498 return localized;
499
500 var fparts = found.split("-");
501 /* If we have found a possible match and this one isn't any longer
502 then we dont need to check further. */
503 if (bestmatch && fparts.length < bestmatchcount)
504 continue;
505
506 // Count the number of parts that match
507 var maxmatchcount = Math.min(fparts.length, lparts.length);
508 var matchcount = 0;
509 while (matchcount < maxmatchcount &&
510 fparts[matchcount] == lparts[matchcount])
511 matchcount++;
512
513 /* If we matched more than the last best match or matched the same and
514 this locale is less specific than the last best match. */
515 if (matchcount > bestmatchcount ||
516 (matchcount == bestmatchcount && fparts.length < bestpartcount)) {
517 bestmatch = localized;
518 bestmatchcount = matchcount;
519 bestpartcount = fparts.length;
520 }
521 }
522 }
523 // If we found a valid match for this locale return it
524 if (bestmatch)
525 return bestmatch;
526 }
527 return null;
528 }
529
530 /**
531 * Sets the userDisabled and softDisabled properties of an add-on based on what
532 * values those properties had for a previous instance of the add-on. The
533 * previous instance may be a previous install or in the case of an application
534 * version change the same add-on.
535 *
536 * NOTE: this may modify aNewAddon in place; callers should save the database if
537 * necessary
538 *
539 * @param aOldAddon
540 * The previous instance of the add-on
541 * @param aNewAddon
542 * The new instance of the add-on
543 * @param aAppVersion
544 * The optional application version to use when checking the blocklist
545 * or undefined to use the current application
546 * @param aPlatformVersion
547 * The optional platform version to use when checking the blocklist or
548 * undefined to use the current platform
549 */
550 function applyBlocklistChanges(aOldAddon, aNewAddon, aOldAppVersion,
551 aOldPlatformVersion) {
552 // Copy the properties by default
553 aNewAddon.userDisabled = aOldAddon.userDisabled;
554 aNewAddon.softDisabled = aOldAddon.softDisabled;
555
556 let bs = Cc["@mozilla.org/extensions/blocklist;1"].
557 getService(Ci.nsIBlocklistService);
558
559 let oldBlocklistState = bs.getAddonBlocklistState(createWrapper(aOldAddon),
560 aOldAppVersion,
561 aOldPlatformVersion);
562 let newBlocklistState = bs.getAddonBlocklistState(createWrapper(aNewAddon));
563
564 // If the blocklist state hasn't changed then the properties don't need to
565 // change
566 if (newBlocklistState == oldBlocklistState)
567 return;
568
569 if (newBlocklistState == Ci.nsIBlocklistService.STATE_SOFTBLOCKED) {
570 if (aNewAddon.type != "theme") {
571 // The add-on has become softblocked, set softDisabled if it isn't already
572 // userDisabled
573 aNewAddon.softDisabled = !aNewAddon.userDisabled;
574 }
575 else {
576 // Themes just get userDisabled to switch back to the default theme
577 aNewAddon.userDisabled = true;
578 }
579 }
580 else {
581 // If the new add-on is not softblocked then it cannot be softDisabled
582 aNewAddon.softDisabled = false;
583 }
584 }
585
586 /**
587 * Calculates whether an add-on should be appDisabled or not.
588 *
589 * @param aAddon
590 * The add-on to check
591 * @return true if the add-on should not be appDisabled
592 */
593 function isUsableAddon(aAddon) {
594 // Hack to ensure the default theme is always usable
595 if (aAddon.type == "theme" && aAddon.internalName == XPIProvider.defaultSkin)
596 return true;
597
598 if (aAddon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED)
599 return false;
600
601 if (AddonManager.checkUpdateSecurity && !aAddon.providesUpdatesSecurely)
602 return false;
603
604 if (!aAddon.isPlatformCompatible)
605 return false;
606
607 if (AddonManager.checkCompatibility) {
608 if (!aAddon.isCompatible)
609 return false;
610 }
611 else {
612 let app = aAddon.matchingTargetApplication;
613 if (!app)
614 return false;
615
616 // XXX Temporary solution to let applications opt-in to make themes safer
617 // following significant UI changes even if checkCompatibility=false has
618 // been set, until we get bug 962001.
619 if (aAddon.type == "theme" && app.id == Services.appinfo.ID) {
620 try {
621 let minCompatVersion = Services.prefs.getCharPref(PREF_CHECKCOMAT_THEMEOVERRIDE);
622 if (minCompatVersion &&
623 Services.vc.compare(minCompatVersion, app.maxVersion) > 0) {
624 return false;
625 }
626 } catch (e) {}
627 }
628 }
629
630 return true;
631 }
632
633 function isAddonDisabled(aAddon) {
634 return aAddon.appDisabled || aAddon.softDisabled || aAddon.userDisabled;
635 }
636
637 XPCOMUtils.defineLazyServiceGetter(this, "gRDF", "@mozilla.org/rdf/rdf-service;1",
638 Ci.nsIRDFService);
639
640 function EM_R(aProperty) {
641 return gRDF.GetResource(PREFIX_NS_EM + aProperty);
642 }
643
644 /**
645 * Converts an RDF literal, resource or integer into a string.
646 *
647 * @param aLiteral
648 * The RDF object to convert
649 * @return a string if the object could be converted or null
650 */
651 function getRDFValue(aLiteral) {
652 if (aLiteral instanceof Ci.nsIRDFLiteral)
653 return aLiteral.Value;
654 if (aLiteral instanceof Ci.nsIRDFResource)
655 return aLiteral.Value;
656 if (aLiteral instanceof Ci.nsIRDFInt)
657 return aLiteral.Value;
658 return null;
659 }
660
661 /**
662 * Gets an RDF property as a string
663 *
664 * @param aDs
665 * The RDF datasource to read the property from
666 * @param aResource
667 * The RDF resource to read the property from
668 * @param aProperty
669 * The property to read
670 * @return a string if the property existed or null
671 */
672 function getRDFProperty(aDs, aResource, aProperty) {
673 return getRDFValue(aDs.GetTarget(aResource, EM_R(aProperty), true));
674 }
675
676 /**
677 * Reads an AddonInternal object from an RDF stream.
678 *
679 * @param aUri
680 * The URI that the manifest is being read from
681 * @param aStream
682 * An open stream to read the RDF from
683 * @return an AddonInternal object
684 * @throws if the install manifest in the RDF stream is corrupt or could not
685 * be read
686 */
687 function loadManifestFromRDF(aUri, aStream) {
688 function getPropertyArray(aDs, aSource, aProperty) {
689 let values = [];
690 let targets = aDs.GetTargets(aSource, EM_R(aProperty), true);
691 while (targets.hasMoreElements())
692 values.push(getRDFValue(targets.getNext()));
693
694 return values;
695 }
696
697 /**
698 * Reads locale properties from either the main install manifest root or
699 * an em:localized section in the install manifest.
700 *
701 * @param aDs
702 * The nsIRDFDatasource to read from
703 * @param aSource
704 * The nsIRDFResource to read the properties from
705 * @param isDefault
706 * True if the locale is to be read from the main install manifest
707 * root
708 * @param aSeenLocales
709 * An array of locale names already seen for this install manifest.
710 * Any locale names seen as a part of this function will be added to
711 * this array
712 * @return an object containing the locale properties
713 */
714 function readLocale(aDs, aSource, isDefault, aSeenLocales) {
715 let locale = { };
716 if (!isDefault) {
717 locale.locales = [];
718 let targets = ds.GetTargets(aSource, EM_R("locale"), true);
719 while (targets.hasMoreElements()) {
720 let localeName = getRDFValue(targets.getNext());
721 if (!localeName) {
722 logger.warn("Ignoring empty locale in localized properties");
723 continue;
724 }
725 if (aSeenLocales.indexOf(localeName) != -1) {
726 logger.warn("Ignoring duplicate locale in localized properties");
727 continue;
728 }
729 aSeenLocales.push(localeName);
730 locale.locales.push(localeName);
731 }
732
733 if (locale.locales.length == 0) {
734 logger.warn("Ignoring localized properties with no listed locales");
735 return null;
736 }
737 }
738
739 PROP_LOCALE_SINGLE.forEach(function(aProp) {
740 locale[aProp] = getRDFProperty(aDs, aSource, aProp);
741 });
742
743 PROP_LOCALE_MULTI.forEach(function(aProp) {
744 // Don't store empty arrays
745 let props = getPropertyArray(aDs, aSource,
746 aProp.substring(0, aProp.length - 1));
747 if (props.length > 0)
748 locale[aProp] = props;
749 });
750
751 return locale;
752 }
753
754 let rdfParser = Cc["@mozilla.org/rdf/xml-parser;1"].
755 createInstance(Ci.nsIRDFXMLParser)
756 let ds = Cc["@mozilla.org/rdf/datasource;1?name=in-memory-datasource"].
757 createInstance(Ci.nsIRDFDataSource);
758 let listener = rdfParser.parseAsync(ds, aUri);
759 let channel = Cc["@mozilla.org/network/input-stream-channel;1"].
760 createInstance(Ci.nsIInputStreamChannel);
761 channel.setURI(aUri);
762 channel.contentStream = aStream;
763 channel.QueryInterface(Ci.nsIChannel);
764 channel.contentType = "text/xml";
765
766 listener.onStartRequest(channel, null);
767
768 try {
769 let pos = 0;
770 let count = aStream.available();
771 while (count > 0) {
772 listener.onDataAvailable(channel, null, aStream, pos, count);
773 pos += count;
774 count = aStream.available();
775 }
776 listener.onStopRequest(channel, null, Components.results.NS_OK);
777 }
778 catch (e) {
779 listener.onStopRequest(channel, null, e.result);
780 throw e;
781 }
782
783 let root = gRDF.GetResource(RDFURI_INSTALL_MANIFEST_ROOT);
784 let addon = new AddonInternal();
785 PROP_METADATA.forEach(function(aProp) {
786 addon[aProp] = getRDFProperty(ds, root, aProp);
787 });
788 addon.unpack = getRDFProperty(ds, root, "unpack") == "true";
789
790 if (!addon.type) {
791 addon.type = addon.internalName ? "theme" : "extension";
792 }
793 else {
794 let type = addon.type;
795 addon.type = null;
796 for (let name in TYPES) {
797 if (TYPES[name] == type) {
798 addon.type = name;
799 break;
800 }
801 }
802 }
803
804 if (!(addon.type in TYPES))
805 throw new Error("Install manifest specifies unknown type: " + addon.type);
806
807 if (addon.type != "multipackage") {
808 if (!addon.id)
809 throw new Error("No ID in install manifest");
810 if (!gIDTest.test(addon.id))
811 throw new Error("Illegal add-on ID " + addon.id);
812 if (!addon.version)
813 throw new Error("No version in install manifest");
814 }
815
816 addon.strictCompatibility = !(addon.type in COMPATIBLE_BY_DEFAULT_TYPES) ||
817 getRDFProperty(ds, root, "strictCompatibility") == "true";
818
819 // Only read the bootstrap property for extensions.
820 if (addon.type == "extension") {
821 addon.bootstrap = getRDFProperty(ds, root, "bootstrap") == "true";
822 if (addon.optionsType &&
823 addon.optionsType != AddonManager.OPTIONS_TYPE_DIALOG &&
824 addon.optionsType != AddonManager.OPTIONS_TYPE_INLINE &&
825 addon.optionsType != AddonManager.OPTIONS_TYPE_TAB &&
826 addon.optionsType != AddonManager.OPTIONS_TYPE_INLINE_INFO) {
827 throw new Error("Install manifest specifies unknown type: " + addon.optionsType);
828 }
829 }
830 else {
831 // Some add-on types are always restartless.
832 if (RESTARTLESS_TYPES.has(addon.type)) {
833 addon.bootstrap = true;
834 }
835
836 // Only extensions are allowed to provide an optionsURL, optionsType or aboutURL. For
837 // all other types they are silently ignored
838 addon.optionsURL = null;
839 addon.optionsType = null;
840 addon.aboutURL = null;
841
842 if (addon.type == "theme") {
843 if (!addon.internalName)
844 throw new Error("Themes must include an internalName property");
845 addon.skinnable = getRDFProperty(ds, root, "skinnable") == "true";
846 }
847 }
848
849 addon.defaultLocale = readLocale(ds, root, true);
850
851 let seenLocales = [];
852 addon.locales = [];
853 let targets = ds.GetTargets(root, EM_R("localized"), true);
854 while (targets.hasMoreElements()) {
855 let target = targets.getNext().QueryInterface(Ci.nsIRDFResource);
856 let locale = readLocale(ds, target, false, seenLocales);
857 if (locale)
858 addon.locales.push(locale);
859 }
860
861 let seenApplications = [];
862 addon.targetApplications = [];
863 targets = ds.GetTargets(root, EM_R("targetApplication"), true);
864 while (targets.hasMoreElements()) {
865 let target = targets.getNext().QueryInterface(Ci.nsIRDFResource);
866 let targetAppInfo = {};
867 PROP_TARGETAPP.forEach(function(aProp) {
868 targetAppInfo[aProp] = getRDFProperty(ds, target, aProp);
869 });
870 if (!targetAppInfo.id || !targetAppInfo.minVersion ||
871 !targetAppInfo.maxVersion) {
872 logger.warn("Ignoring invalid targetApplication entry in install manifest");
873 continue;
874 }
875 if (seenApplications.indexOf(targetAppInfo.id) != -1) {
876 logger.warn("Ignoring duplicate targetApplication entry for " + targetAppInfo.id +
877 " in install manifest");
878 continue;
879 }
880 seenApplications.push(targetAppInfo.id);
881 addon.targetApplications.push(targetAppInfo);
882 }
883
884 // Note that we don't need to check for duplicate targetPlatform entries since
885 // the RDF service coalesces them for us.
886 let targetPlatforms = getPropertyArray(ds, root, "targetPlatform");
887 addon.targetPlatforms = [];
888 targetPlatforms.forEach(function(aPlatform) {
889 let platform = {
890 os: null,
891 abi: null
892 };
893
894 let pos = aPlatform.indexOf("_");
895 if (pos != -1) {
896 platform.os = aPlatform.substring(0, pos);
897 platform.abi = aPlatform.substring(pos + 1);
898 }
899 else {
900 platform.os = aPlatform;
901 }
902
903 addon.targetPlatforms.push(platform);
904 });
905
906 // A theme's userDisabled value is true if the theme is not the selected skin
907 // or if there is an active lightweight theme. We ignore whether softblocking
908 // is in effect since it would change the active theme.
909 if (addon.type == "theme") {
910 addon.userDisabled = !!LightweightThemeManager.currentTheme ||
911 addon.internalName != XPIProvider.selectedSkin;
912 }
913 // Experiments are disabled by default. It is up to the Experiments Manager
914 // to enable them (it drives installation).
915 else if (addon.type == "experiment") {
916 addon.userDisabled = true;
917 }
918 else {
919 addon.userDisabled = false;
920 addon.softDisabled = addon.blocklistState == Ci.nsIBlocklistService.STATE_SOFTBLOCKED;
921 }
922
923 addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DEFAULT;
924
925 // Experiments are managed and updated through an external "experiments
926 // manager." So disable some built-in mechanisms.
927 if (addon.type == "experiment") {
928 addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DISABLE;
929 addon.updateURL = null;
930 addon.updateKey = null;
931
932 addon.targetApplications = [];
933 addon.targetPlatforms = [];
934 }
935
936 // Load the storage service before NSS (nsIRandomGenerator),
937 // to avoid a SQLite initialization error (bug 717904).
938 let storage = Services.storage;
939
940 // Generate random GUID used for Sync.
941 // This was lifted from util.js:makeGUID() from services-sync.
942 let rng = Cc["@mozilla.org/security/random-generator;1"].
943 createInstance(Ci.nsIRandomGenerator);
944 let bytes = rng.generateRandomBytes(9);
945 let byte_string = [String.fromCharCode(byte) for each (byte in bytes)]
946 .join("");
947 // Base64 encode
948 addon.syncGUID = btoa(byte_string).replace(/\+/g, '-')
949 .replace(/\//g, '_');
950
951 return addon;
952 }
953
954 /**
955 * Loads an AddonInternal object from an add-on extracted in a directory.
956 *
957 * @param aDir
958 * The nsIFile directory holding the add-on
959 * @return an AddonInternal object
960 * @throws if the directory does not contain a valid install manifest
961 */
962 function loadManifestFromDir(aDir) {
963 function getFileSize(aFile) {
964 if (aFile.isSymlink())
965 return 0;
966
967 if (!aFile.isDirectory())
968 return aFile.fileSize;
969
970 let size = 0;
971 let entries = aFile.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
972 let entry;
973 while ((entry = entries.nextFile))
974 size += getFileSize(entry);
975 entries.close();
976 return size;
977 }
978
979 let file = aDir.clone();
980 file.append(FILE_INSTALL_MANIFEST);
981 if (!file.exists() || !file.isFile())
982 throw new Error("Directory " + aDir.path + " does not contain a valid " +
983 "install manifest");
984
985 let fis = Cc["@mozilla.org/network/file-input-stream;1"].
986 createInstance(Ci.nsIFileInputStream);
987 fis.init(file, -1, -1, false);
988 let bis = Cc["@mozilla.org/network/buffered-input-stream;1"].
989 createInstance(Ci.nsIBufferedInputStream);
990 bis.init(fis, 4096);
991
992 try {
993 let addon = loadManifestFromRDF(Services.io.newFileURI(file), bis);
994 addon._sourceBundle = aDir.clone();
995 addon.size = getFileSize(aDir);
996
997 file = aDir.clone();
998 file.append("chrome.manifest");
999 let chromeManifest = ChromeManifestParser.parseSync(Services.io.newFileURI(file));
1000 addon.hasBinaryComponents = ChromeManifestParser.hasType(chromeManifest,
1001 "binary-component");
1002
1003 addon.appDisabled = !isUsableAddon(addon);
1004 return addon;
1005 }
1006 finally {
1007 bis.close();
1008 fis.close();
1009 }
1010 }
1011
1012 /**
1013 * Loads an AddonInternal object from an nsIZipReader for an add-on.
1014 *
1015 * @param aZipReader
1016 * An open nsIZipReader for the add-on's files
1017 * @return an AddonInternal object
1018 * @throws if the XPI file does not contain a valid install manifest
1019 */
1020 function loadManifestFromZipReader(aZipReader) {
1021 let zis = aZipReader.getInputStream(FILE_INSTALL_MANIFEST);
1022 let bis = Cc["@mozilla.org/network/buffered-input-stream;1"].
1023 createInstance(Ci.nsIBufferedInputStream);
1024 bis.init(zis, 4096);
1025
1026 try {
1027 let uri = buildJarURI(aZipReader.file, FILE_INSTALL_MANIFEST);
1028 let addon = loadManifestFromRDF(uri, bis);
1029 addon._sourceBundle = aZipReader.file;
1030
1031 addon.size = 0;
1032 let entries = aZipReader.findEntries(null);
1033 while (entries.hasMore())
1034 addon.size += aZipReader.getEntry(entries.getNext()).realSize;
1035
1036 // Binary components can only be loaded from unpacked addons.
1037 if (addon.unpack) {
1038 uri = buildJarURI(aZipReader.file, "chrome.manifest");
1039 let chromeManifest = ChromeManifestParser.parseSync(uri);
1040 addon.hasBinaryComponents = ChromeManifestParser.hasType(chromeManifest,
1041 "binary-component");
1042 } else {
1043 addon.hasBinaryComponents = false;
1044 }
1045
1046 addon.appDisabled = !isUsableAddon(addon);
1047 return addon;
1048 }
1049 finally {
1050 bis.close();
1051 zis.close();
1052 }
1053 }
1054
1055 /**
1056 * Loads an AddonInternal object from an add-on in an XPI file.
1057 *
1058 * @param aXPIFile
1059 * An nsIFile pointing to the add-on's XPI file
1060 * @return an AddonInternal object
1061 * @throws if the XPI file does not contain a valid install manifest
1062 */
1063 function loadManifestFromZipFile(aXPIFile) {
1064 let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].
1065 createInstance(Ci.nsIZipReader);
1066 try {
1067 zipReader.open(aXPIFile);
1068
1069 return loadManifestFromZipReader(zipReader);
1070 }
1071 finally {
1072 zipReader.close();
1073 }
1074 }
1075
1076 function loadManifestFromFile(aFile) {
1077 if (aFile.isFile())
1078 return loadManifestFromZipFile(aFile);
1079 else
1080 return loadManifestFromDir(aFile);
1081 }
1082
1083 /**
1084 * Gets an nsIURI for a file within another file, either a directory or an XPI
1085 * file. If aFile is a directory then this will return a file: URI, if it is an
1086 * XPI file then it will return a jar: URI.
1087 *
1088 * @param aFile
1089 * The file containing the resources, must be either a directory or an
1090 * XPI file
1091 * @param aPath
1092 * The path to find the resource at, "/" separated. If aPath is empty
1093 * then the uri to the root of the contained files will be returned
1094 * @return an nsIURI pointing at the resource
1095 */
1096 function getURIForResourceInFile(aFile, aPath) {
1097 if (aFile.isDirectory()) {
1098 let resource = aFile.clone();
1099 if (aPath) {
1100 aPath.split("/").forEach(function(aPart) {
1101 resource.append(aPart);
1102 });
1103 }
1104 return NetUtil.newURI(resource);
1105 }
1106
1107 return buildJarURI(aFile, aPath);
1108 }
1109
1110 /**
1111 * Creates a jar: URI for a file inside a ZIP file.
1112 *
1113 * @param aJarfile
1114 * The ZIP file as an nsIFile
1115 * @param aPath
1116 * The path inside the ZIP file
1117 * @return an nsIURI for the file
1118 */
1119 function buildJarURI(aJarfile, aPath) {
1120 let uri = Services.io.newFileURI(aJarfile);
1121 uri = "jar:" + uri.spec + "!/" + aPath;
1122 return NetUtil.newURI(uri);
1123 }
1124
1125 /**
1126 * Sends local and remote notifications to flush a JAR file cache entry
1127 *
1128 * @param aJarFile
1129 * The ZIP/XPI/JAR file as a nsIFile
1130 */
1131 function flushJarCache(aJarFile) {
1132 Services.obs.notifyObservers(aJarFile, "flush-cache-entry", null);
1133 Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageBroadcaster)
1134 .broadcastAsyncMessage(MSG_JAR_FLUSH, aJarFile.path);
1135 }
1136
1137 function flushStartupCache() {
1138 // Init this, so it will get the notification.
1139 Services.obs.notifyObservers(null, "startupcache-invalidate", null);
1140 }
1141
1142 /**
1143 * Creates and returns a new unique temporary file. The caller should delete
1144 * the file when it is no longer needed.
1145 *
1146 * @return an nsIFile that points to a randomly named, initially empty file in
1147 * the OS temporary files directory
1148 */
1149 function getTemporaryFile() {
1150 let file = FileUtils.getDir(KEY_TEMPDIR, []);
1151 let random = Math.random().toString(36).replace(/0./, '').substr(-3);
1152 file.append("tmp-" + random + ".xpi");
1153 file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
1154
1155 return file;
1156 }
1157
1158 /**
1159 * Verifies that a zip file's contents are all signed by the same principal.
1160 * Directory entries and anything in the META-INF directory are not checked.
1161 *
1162 * @param aZip
1163 * A nsIZipReader to check
1164 * @param aPrincipal
1165 * The nsIPrincipal to compare against
1166 * @return true if all the contents that should be signed were signed by the
1167 * principal
1168 */
1169 function verifyZipSigning(aZip, aPrincipal) {
1170 var count = 0;
1171 var entries = aZip.findEntries(null);
1172 while (entries.hasMore()) {
1173 var entry = entries.getNext();
1174 // Nothing in META-INF is in the manifest.
1175 if (entry.substr(0, 9) == "META-INF/")
1176 continue;
1177 // Directory entries aren't in the manifest.
1178 if (entry.substr(-1) == "/")
1179 continue;
1180 count++;
1181 var entryPrincipal = aZip.getCertificatePrincipal(entry);
1182 if (!entryPrincipal || !aPrincipal.equals(entryPrincipal))
1183 return false;
1184 }
1185 return aZip.manifestEntriesCount == count;
1186 }
1187
1188 /**
1189 * Replaces %...% strings in an addon url (update and updateInfo) with
1190 * appropriate values.
1191 *
1192 * @param aAddon
1193 * The AddonInternal representing the add-on
1194 * @param aUri
1195 * The uri to escape
1196 * @param aUpdateType
1197 * An optional number representing the type of update, only applicable
1198 * when creating a url for retrieving an update manifest
1199 * @param aAppVersion
1200 * The optional application version to use for %APP_VERSION%
1201 * @return the appropriately escaped uri.
1202 */
1203 function escapeAddonURI(aAddon, aUri, aUpdateType, aAppVersion)
1204 {
1205 let uri = AddonManager.escapeAddonURI(aAddon, aUri, aAppVersion);
1206
1207 // If there is an updateType then replace the UPDATE_TYPE string
1208 if (aUpdateType)
1209 uri = uri.replace(/%UPDATE_TYPE%/g, aUpdateType);
1210
1211 // If this add-on has compatibility information for either the current
1212 // application or toolkit then replace the ITEM_MAXAPPVERSION with the
1213 // maxVersion
1214 let app = aAddon.matchingTargetApplication;
1215 if (app)
1216 var maxVersion = app.maxVersion;
1217 else
1218 maxVersion = "";
1219 uri = uri.replace(/%ITEM_MAXAPPVERSION%/g, maxVersion);
1220
1221 let compatMode = "normal";
1222 if (!AddonManager.checkCompatibility)
1223 compatMode = "ignore";
1224 else if (AddonManager.strictCompatibility)
1225 compatMode = "strict";
1226 uri = uri.replace(/%COMPATIBILITY_MODE%/g, compatMode);
1227
1228 return uri;
1229 }
1230
1231 function removeAsync(aFile) {
1232 return Task.spawn(function () {
1233 let info = null;
1234 try {
1235 info = yield OS.File.stat(aFile.path);
1236 if (info.isDir)
1237 yield OS.File.removeDir(aFile.path);
1238 else
1239 yield OS.File.remove(aFile.path);
1240 }
1241 catch (e if e instanceof OS.File.Error && e.becauseNoSuchFile) {
1242 // The file has already gone away
1243 return;
1244 }
1245 });
1246 }
1247
1248 /**
1249 * Recursively removes a directory or file fixing permissions when necessary.
1250 *
1251 * @param aFile
1252 * The nsIFile to remove
1253 */
1254 function recursiveRemove(aFile) {
1255 let isDir = null;
1256
1257 try {
1258 isDir = aFile.isDirectory();
1259 }
1260 catch (e) {
1261 // If the file has already gone away then don't worry about it, this can
1262 // happen on OSX where the resource fork is automatically moved with the
1263 // data fork for the file. See bug 733436.
1264 if (e.result == Cr.NS_ERROR_FILE_TARGET_DOES_NOT_EXIST)
1265 return;
1266 if (e.result == Cr.NS_ERROR_FILE_NOT_FOUND)
1267 return;
1268
1269 throw e;
1270 }
1271
1272 setFilePermissions(aFile, isDir ? FileUtils.PERMS_DIRECTORY
1273 : FileUtils.PERMS_FILE);
1274
1275 try {
1276 aFile.remove(true);
1277 return;
1278 }
1279 catch (e) {
1280 if (!aFile.isDirectory()) {
1281 logger.error("Failed to remove file " + aFile.path, e);
1282 throw e;
1283 }
1284 }
1285
1286 // Use a snapshot of the directory contents to avoid possible issues with
1287 // iterating over a directory while removing files from it (the YAFFS2
1288 // embedded filesystem has this issue, see bug 772238), and to remove
1289 // normal files before their resource forks on OSX (see bug 733436).
1290 let entries = getDirectoryEntries(aFile, true);
1291 entries.forEach(recursiveRemove);
1292
1293 try {
1294 aFile.remove(true);
1295 }
1296 catch (e) {
1297 logger.error("Failed to remove empty directory " + aFile.path, e);
1298 throw e;
1299 }
1300 }
1301
1302 /**
1303 * Returns the timestamp and leaf file name of the most recently modified
1304 * entry in a directory,
1305 * or simply the file's own timestamp if it is not a directory.
1306 * Also returns the total number of items (directories and files) visited in the scan
1307 *
1308 * @param aFile
1309 * A non-null nsIFile object
1310 * @return [File Name, Epoch time, items visited], as described above.
1311 */
1312 function recursiveLastModifiedTime(aFile) {
1313 try {
1314 let modTime = aFile.lastModifiedTime;
1315 let fileName = aFile.leafName;
1316 if (aFile.isFile())
1317 return [fileName, modTime, 1];
1318
1319 if (aFile.isDirectory()) {
1320 let entries = aFile.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
1321 let entry;
1322 let totalItems = 1;
1323 while ((entry = entries.nextFile)) {
1324 let [subName, subTime, items] = recursiveLastModifiedTime(entry);
1325 totalItems += items;
1326 if (subTime > modTime) {
1327 modTime = subTime;
1328 fileName = subName;
1329 }
1330 }
1331 entries.close();
1332 return [fileName, modTime, totalItems];
1333 }
1334 }
1335 catch (e) {
1336 logger.warn("Problem getting last modified time for " + aFile.path, e);
1337 }
1338
1339 // If the file is something else, just ignore it.
1340 return ["", 0, 0];
1341 }
1342
1343 /**
1344 * Gets a snapshot of directory entries.
1345 *
1346 * @param aDir
1347 * Directory to look at
1348 * @param aSortEntries
1349 * True to sort entries by filename
1350 * @return An array of nsIFile, or an empty array if aDir is not a readable directory
1351 */
1352 function getDirectoryEntries(aDir, aSortEntries) {
1353 let dirEnum;
1354 try {
1355 dirEnum = aDir.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
1356 let entries = [];
1357 while (dirEnum.hasMoreElements())
1358 entries.push(dirEnum.nextFile);
1359
1360 if (aSortEntries) {
1361 entries.sort(function sortDirEntries(a, b) {
1362 return a.path > b.path ? -1 : 1;
1363 });
1364 }
1365
1366 return entries
1367 }
1368 catch (e) {
1369 logger.warn("Can't iterate directory " + aDir.path, e);
1370 return [];
1371 }
1372 finally {
1373 if (dirEnum) {
1374 dirEnum.close();
1375 }
1376 }
1377 }
1378
1379 /**
1380 * A helpful wrapper around the prefs service that allows for default values
1381 * when requested values aren't set.
1382 */
1383 var Prefs = {
1384 /**
1385 * Gets a preference from the default branch ignoring user-set values.
1386 *
1387 * @param aName
1388 * The name of the preference
1389 * @param aDefaultValue
1390 * A value to return if the preference does not exist
1391 * @return the default value of the preference or aDefaultValue if there is
1392 * none
1393 */
1394 getDefaultCharPref: function Prefs_getDefaultCharPref(aName, aDefaultValue) {
1395 try {
1396 return Services.prefs.getDefaultBranch("").getCharPref(aName);
1397 }
1398 catch (e) {
1399 }
1400 return aDefaultValue;
1401 },
1402
1403 /**
1404 * Gets a string preference.
1405 *
1406 * @param aName
1407 * The name of the preference
1408 * @param aDefaultValue
1409 * A value to return if the preference does not exist
1410 * @return the value of the preference or aDefaultValue if there is none
1411 */
1412 getCharPref: function Prefs_getCharPref(aName, aDefaultValue) {
1413 try {
1414 return Services.prefs.getCharPref(aName);
1415 }
1416 catch (e) {
1417 }
1418 return aDefaultValue;
1419 },
1420
1421 /**
1422 * Gets a complex preference.
1423 *
1424 * @param aName
1425 * The name of the preference
1426 * @param aType
1427 * The interface type of the preference
1428 * @param aDefaultValue
1429 * A value to return if the preference does not exist
1430 * @return the value of the preference or aDefaultValue if there is none
1431 */
1432 getComplexValue: function Prefs_getComplexValue(aName, aType, aDefaultValue) {
1433 try {
1434 return Services.prefs.getComplexValue(aName, aType).data;
1435 }
1436 catch (e) {
1437 }
1438 return aDefaultValue;
1439 },
1440
1441 /**
1442 * Gets a boolean preference.
1443 *
1444 * @param aName
1445 * The name of the preference
1446 * @param aDefaultValue
1447 * A value to return if the preference does not exist
1448 * @return the value of the preference or aDefaultValue if there is none
1449 */
1450 getBoolPref: function Prefs_getBoolPref(aName, aDefaultValue) {
1451 try {
1452 return Services.prefs.getBoolPref(aName);
1453 }
1454 catch (e) {
1455 }
1456 return aDefaultValue;
1457 },
1458
1459 /**
1460 * Gets an integer preference.
1461 *
1462 * @param aName
1463 * The name of the preference
1464 * @param defaultValue
1465 * A value to return if the preference does not exist
1466 * @return the value of the preference or defaultValue if there is none
1467 */
1468 getIntPref: function Prefs_getIntPref(aName, defaultValue) {
1469 try {
1470 return Services.prefs.getIntPref(aName);
1471 }
1472 catch (e) {
1473 }
1474 return defaultValue;
1475 },
1476
1477 /**
1478 * Clears a preference if it has a user value
1479 *
1480 * @param aName
1481 * The name of the preference
1482 */
1483 clearUserPref: function Prefs_clearUserPref(aName) {
1484 if (Services.prefs.prefHasUserValue(aName))
1485 Services.prefs.clearUserPref(aName);
1486 }
1487 }
1488
1489 // Helper function to compare JSON saved version of the directory state
1490 // with the new state returned by getInstallLocationStates()
1491 // Structure is: ordered array of {'name':?, 'addons': {addonID: {'descriptor':?, 'mtime':?} ...}}
1492 function directoryStateDiffers(aState, aCache)
1493 {
1494 // check equality of an object full of addons; fortunately we can destroy the 'aOld' object
1495 function addonsMismatch(aNew, aOld) {
1496 for (let [id, val] of aNew) {
1497 if (!id in aOld)
1498 return true;
1499 if (val.descriptor != aOld[id].descriptor ||
1500 val.mtime != aOld[id].mtime)
1501 return true;
1502 delete aOld[id];
1503 }
1504 // make sure aOld doesn't have any extra entries
1505 for (let id in aOld)
1506 return true;
1507 return false;
1508 }
1509
1510 if (!aCache)
1511 return true;
1512 try {
1513 let old = JSON.parse(aCache);
1514 if (aState.length != old.length)
1515 return true;
1516 for (let i = 0; i < aState.length; i++) {
1517 // conveniently, any missing fields would require a 'true' return, which is
1518 // handled by our catch wrapper
1519 if (aState[i].name != old[i].name)
1520 return true;
1521 if (addonsMismatch(aState[i].addons, old[i].addons))
1522 return true;
1523 }
1524 }
1525 catch (e) {
1526 return true;
1527 }
1528 return false;
1529 }
1530
1531 /**
1532 * Wraps a function in an exception handler to protect against exceptions inside callbacks
1533 * @param aFunction function(args...)
1534 * @return function(args...), a function that takes the same arguments as aFunction
1535 * and returns the same result unless aFunction throws, in which case it logs
1536 * a warning and returns undefined.
1537 */
1538 function makeSafe(aFunction) {
1539 return function(...aArgs) {
1540 try {
1541 return aFunction(...aArgs);
1542 }
1543 catch(ex) {
1544 logger.warn("XPIProvider callback failed", ex);
1545 }
1546 return undefined;
1547 }
1548 }
1549
1550 this.XPIProvider = {
1551 // An array of known install locations
1552 installLocations: null,
1553 // A dictionary of known install locations by name
1554 installLocationsByName: null,
1555 // An array of currently active AddonInstalls
1556 installs: null,
1557 // The default skin for the application
1558 defaultSkin: "classic/1.0",
1559 // The current skin used by the application
1560 currentSkin: null,
1561 // The selected skin to be used by the application when it is restarted. This
1562 // will be the same as currentSkin when it is the skin to be used when the
1563 // application is restarted
1564 selectedSkin: null,
1565 // The value of the minCompatibleAppVersion preference
1566 minCompatibleAppVersion: null,
1567 // The value of the minCompatiblePlatformVersion preference
1568 minCompatiblePlatformVersion: null,
1569 // A dictionary of the file descriptors for bootstrappable add-ons by ID
1570 bootstrappedAddons: {},
1571 // A dictionary of JS scopes of loaded bootstrappable add-ons by ID
1572 bootstrapScopes: {},
1573 // True if the platform could have activated extensions
1574 extensionsActive: false,
1575 // File / directory state of installed add-ons
1576 installStates: [],
1577 // True if all of the add-ons found during startup were installed in the
1578 // application install location
1579 allAppGlobal: true,
1580 // A string listing the enabled add-ons for annotating crash reports
1581 enabledAddons: null,
1582 // An array of add-on IDs of add-ons that were inactive during startup
1583 inactiveAddonIDs: [],
1584 // Keep track of startup phases for telemetry
1585 runPhase: XPI_STARTING,
1586 // Keep track of the newest file in each add-on, in case we want to
1587 // report it to telemetry.
1588 _mostRecentlyModifiedFile: {},
1589 // Per-addon telemetry information
1590 _telemetryDetails: {},
1591 // Experiments are disabled by default. Track ones that are locally enabled.
1592 _enabledExperiments: null,
1593
1594 /*
1595 * Set a value in the telemetry hash for a given ID
1596 */
1597 setTelemetry: function XPI_setTelemetry(aId, aName, aValue) {
1598 if (!this._telemetryDetails[aId])
1599 this._telemetryDetails[aId] = {};
1600 this._telemetryDetails[aId][aName] = aValue;
1601 },
1602
1603 // Keep track of in-progress operations that support cancel()
1604 _inProgress: new Set(),
1605
1606 doing: function XPI_doing(aCancellable) {
1607 this._inProgress.add(aCancellable);
1608 },
1609
1610 done: function XPI_done(aCancellable) {
1611 return this._inProgress.delete(aCancellable);
1612 },
1613
1614 cancelAll: function XPI_cancelAll() {
1615 // Cancelling one may alter _inProgress, so restart the iterator after each
1616 while (this._inProgress.size > 0) {
1617 for (let c of this._inProgress) {
1618 try {
1619 c.cancel();
1620 }
1621 catch (e) {
1622 logger.warn("Cancel failed", e);
1623 }
1624 this._inProgress.delete(c);
1625 }
1626 }
1627 },
1628
1629 /**
1630 * Adds or updates a URI mapping for an Addon.id.
1631 *
1632 * Mappings should not be removed at any point. This is so that the mappings
1633 * will be still valid after an add-on gets disabled or uninstalled, as
1634 * consumers may still have URIs of (leaked) resources they want to map.
1635 */
1636 _addURIMapping: function XPI__addURIMapping(aID, aFile) {
1637 try {
1638 // Always use our own mechanics instead of nsIIOService.newFileURI, so
1639 // that we can be sure to map things as we want them mapped.
1640 let uri = this._resolveURIToFile(getURIForResourceInFile(aFile, "."));
1641 if (!uri) {
1642 throw new Error("Cannot resolve");
1643 }
1644 this._ensureURIMappings();
1645 this._uriMappings[aID] = uri.spec;
1646 }
1647 catch (ex) {
1648 logger.warn("Failed to add URI mapping", ex);
1649 }
1650 },
1651
1652 /**
1653 * Ensures that the URI to Addon mappings are available.
1654 *
1655 * The function will add mappings for all non-bootstrapped but enabled
1656 * add-ons.
1657 * Bootstrapped add-on mappings will be added directly when the bootstrap
1658 * scope get loaded. (See XPIProvider._addURIMapping() and callers)
1659 */
1660 _ensureURIMappings: function XPI__ensureURIMappings() {
1661 if (this._uriMappings) {
1662 return;
1663 }
1664 // XXX Convert to Map(), once it gets stable with stable iterators
1665 this._uriMappings = Object.create(null);
1666
1667 // XXX Convert to Set(), once it gets stable with stable iterators
1668 let enabled = Object.create(null);
1669 let enabledAddons = this.enabledAddons || "";
1670 for (let a of enabledAddons.split(",")) {
1671 a = decodeURIComponent(a.split(":")[0]);
1672 enabled[a] = null;
1673 }
1674
1675 let cache = JSON.parse(Prefs.getCharPref(PREF_INSTALL_CACHE, "[]"));
1676 for (let loc of cache) {
1677 for (let [id, val] in Iterator(loc.addons)) {
1678 if (!(id in enabled)) {
1679 continue;
1680 }
1681 let file = new nsIFile(val.descriptor);
1682 let spec = Services.io.newFileURI(file).spec;
1683 this._uriMappings[id] = spec;
1684 }
1685 }
1686 },
1687
1688 /**
1689 * Resolve a URI back to physical file.
1690 *
1691 * Of course, this works only for URIs pointing to local resources.
1692 *
1693 * @param aURI
1694 * URI to resolve
1695 * @return
1696 * resolved nsIFileURL
1697 */
1698 _resolveURIToFile: function XPI__resolveURIToFile(aURI) {
1699 switch (aURI.scheme) {
1700 case "jar":
1701 case "file":
1702 if (aURI instanceof Ci.nsIJARURI) {
1703 return this._resolveURIToFile(aURI.JARFile);
1704 }
1705 return aURI;
1706
1707 case "chrome":
1708 aURI = ChromeRegistry.convertChromeURL(aURI);
1709 return this._resolveURIToFile(aURI);
1710
1711 case "resource":
1712 aURI = Services.io.newURI(ResProtocolHandler.resolveURI(aURI), null,
1713 null);
1714 return this._resolveURIToFile(aURI);
1715
1716 case "view-source":
1717 aURI = Services.io.newURI(aURI.path, null, null);
1718 return this._resolveURIToFile(aURI);
1719
1720 case "about":
1721 if (aURI.spec == "about:blank") {
1722 // Do not attempt to map about:blank
1723 return null;
1724 }
1725
1726 let chan;
1727 try {
1728 chan = Services.io.newChannelFromURI(aURI);
1729 }
1730 catch (ex) {
1731 return null;
1732 }
1733 // Avoid looping
1734 if (chan.URI.equals(aURI)) {
1735 return null;
1736 }
1737 // We want to clone the channel URI to avoid accidentially keeping
1738 // unnecessary references to the channel or implementation details
1739 // around.
1740 return this._resolveURIToFile(chan.URI.clone());
1741
1742 default:
1743 return null;
1744 }
1745 },
1746
1747 /**
1748 * Starts the XPI provider initializes the install locations and prefs.
1749 *
1750 * @param aAppChanged
1751 * A tri-state value. Undefined means the current profile was created
1752 * for this session, true means the profile already existed but was
1753 * last used with an application with a different version number,
1754 * false means that the profile was last used by this version of the
1755 * application.
1756 * @param aOldAppVersion
1757 * The version of the application last run with this profile or null
1758 * if it is a new profile or the version is unknown
1759 * @param aOldPlatformVersion
1760 * The version of the platform last run with this profile or null
1761 * if it is a new profile or the version is unknown
1762 */
1763 startup: function XPI_startup(aAppChanged, aOldAppVersion, aOldPlatformVersion) {
1764 function addDirectoryInstallLocation(aName, aKey, aPaths, aScope, aLocked) {
1765 try {
1766 var dir = FileUtils.getDir(aKey, aPaths);
1767 }
1768 catch (e) {
1769 // Some directories aren't defined on some platforms, ignore them
1770 logger.debug("Skipping unavailable install location " + aName);
1771 return;
1772 }
1773
1774 try {
1775 var location = new DirectoryInstallLocation(aName, dir, aScope, aLocked);
1776 }
1777 catch (e) {
1778 logger.warn("Failed to add directory install location " + aName, e);
1779 return;
1780 }
1781
1782 XPIProvider.installLocations.push(location);
1783 XPIProvider.installLocationsByName[location.name] = location;
1784 }
1785
1786 function addRegistryInstallLocation(aName, aRootkey, aScope) {
1787 try {
1788 var location = new WinRegInstallLocation(aName, aRootkey, aScope);
1789 }
1790 catch (e) {
1791 logger.warn("Failed to add registry install location " + aName, e);
1792 return;
1793 }
1794
1795 XPIProvider.installLocations.push(location);
1796 XPIProvider.installLocationsByName[location.name] = location;
1797 }
1798
1799 try {
1800 AddonManagerPrivate.recordTimestamp("XPI_startup_begin");
1801
1802 logger.debug("startup");
1803 this.runPhase = XPI_STARTING;
1804 this.installs = [];
1805 this.installLocations = [];
1806 this.installLocationsByName = {};
1807 // Hook for tests to detect when saving database at shutdown time fails
1808 this._shutdownError = null;
1809 // Clear this at startup for xpcshell test restarts
1810 this._telemetryDetails = {};
1811 // Clear the set of enabled experiments (experiments disabled by default).
1812 this._enabledExperiments = new Set();
1813 // Register our details structure with AddonManager
1814 AddonManagerPrivate.setTelemetryDetails("XPI", this._telemetryDetails);
1815
1816 let hasRegistry = ("nsIWindowsRegKey" in Ci);
1817
1818 let enabledScopes = Prefs.getIntPref(PREF_EM_ENABLED_SCOPES,
1819 AddonManager.SCOPE_ALL);
1820
1821 // These must be in order of priority for processFileChanges etc. to work
1822 if (enabledScopes & AddonManager.SCOPE_SYSTEM) {
1823 if (hasRegistry) {
1824 addRegistryInstallLocation("winreg-app-global",
1825 Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE,
1826 AddonManager.SCOPE_SYSTEM);
1827 }
1828 addDirectoryInstallLocation(KEY_APP_SYSTEM_LOCAL, "XRESysLExtPD",
1829 [Services.appinfo.ID],
1830 AddonManager.SCOPE_SYSTEM, true);
1831 addDirectoryInstallLocation(KEY_APP_SYSTEM_SHARE, "XRESysSExtPD",
1832 [Services.appinfo.ID],
1833 AddonManager.SCOPE_SYSTEM, true);
1834 }
1835
1836 if (enabledScopes & AddonManager.SCOPE_APPLICATION) {
1837 addDirectoryInstallLocation(KEY_APP_GLOBAL, KEY_APPDIR,
1838 [DIR_EXTENSIONS],
1839 AddonManager.SCOPE_APPLICATION, true);
1840 }
1841
1842 if (enabledScopes & AddonManager.SCOPE_USER) {
1843 if (hasRegistry) {
1844 addRegistryInstallLocation("winreg-app-user",
1845 Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
1846 AddonManager.SCOPE_USER);
1847 }
1848 addDirectoryInstallLocation(KEY_APP_SYSTEM_USER, "XREUSysExt",
1849 [Services.appinfo.ID],
1850 AddonManager.SCOPE_USER, true);
1851 }
1852
1853 // The profile location is always enabled
1854 addDirectoryInstallLocation(KEY_APP_PROFILE, KEY_PROFILEDIR,
1855 [DIR_EXTENSIONS],
1856 AddonManager.SCOPE_PROFILE, false);
1857
1858 this.defaultSkin = Prefs.getDefaultCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN,
1859 "classic/1.0");
1860 this.currentSkin = Prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN,
1861 this.defaultSkin);
1862 this.selectedSkin = this.currentSkin;
1863 this.applyThemeChange();
1864
1865 this.minCompatibleAppVersion = Prefs.getCharPref(PREF_EM_MIN_COMPAT_APP_VERSION,
1866 null);
1867 this.minCompatiblePlatformVersion = Prefs.getCharPref(PREF_EM_MIN_COMPAT_PLATFORM_VERSION,
1868 null);
1869 this.enabledAddons = "";
1870
1871 Services.prefs.addObserver(PREF_EM_MIN_COMPAT_APP_VERSION, this, false);
1872 Services.prefs.addObserver(PREF_EM_MIN_COMPAT_PLATFORM_VERSION, this, false);
1873 Services.obs.addObserver(this, NOTIFICATION_FLUSH_PERMISSIONS, false);
1874
1875 try {
1876 BrowserToolboxProcess.on("connectionchange",
1877 this.onDebugConnectionChange.bind(this));
1878 }
1879 catch (e) {
1880 // BrowserToolboxProcess is not available in all applications
1881 }
1882
1883 let flushCaches = this.checkForChanges(aAppChanged, aOldAppVersion,
1884 aOldPlatformVersion);
1885
1886 // Changes to installed extensions may have changed which theme is selected
1887 this.applyThemeChange();
1888
1889 // If the application has been upgraded and there are add-ons outside the
1890 // application directory then we may need to synchronize compatibility
1891 // information but only if the mismatch UI isn't disabled
1892 if (aAppChanged && !this.allAppGlobal &&
1893 Prefs.getBoolPref(PREF_EM_SHOW_MISMATCH_UI, true)) {
1894 this.showUpgradeUI();
1895 flushCaches = true;
1896 }
1897 else if (aAppChanged === undefined) {
1898 // For new profiles we will never need to show the add-on selection UI
1899 Services.prefs.setBoolPref(PREF_SHOWN_SELECTION_UI, true);
1900 }
1901
1902 if (flushCaches) {
1903 flushStartupCache();
1904
1905 // UI displayed early in startup (like the compatibility UI) may have
1906 // caused us to cache parts of the skin or locale in memory. These must
1907 // be flushed to allow extension provided skins and locales to take full
1908 // effect
1909 Services.obs.notifyObservers(null, "chrome-flush-skin-caches", null);
1910 Services.obs.notifyObservers(null, "chrome-flush-caches", null);
1911 }
1912
1913 this.enabledAddons = Prefs.getCharPref(PREF_EM_ENABLED_ADDONS, "");
1914
1915 // Invalidate the URI mappings now that |enabledAddons| was updated.
1916 // |_ensureMappings()| will re-create the mappings when needed.
1917 delete this._uriMappings;
1918
1919 if ("nsICrashReporter" in Ci &&
1920 Services.appinfo instanceof Ci.nsICrashReporter) {
1921 // Annotate the crash report with relevant add-on information.
1922 try {
1923 Services.appinfo.annotateCrashReport("Theme", this.currentSkin);
1924 } catch (e) { }
1925 try {
1926 Services.appinfo.annotateCrashReport("EMCheckCompatibility",
1927 AddonManager.checkCompatibility);
1928 } catch (e) { }
1929 this.addAddonsToCrashReporter();
1930 }
1931
1932 try {
1933 AddonManagerPrivate.recordTimestamp("XPI_bootstrap_addons_begin");
1934 for (let id in this.bootstrappedAddons) {
1935 try {
1936 let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
1937 file.persistentDescriptor = this.bootstrappedAddons[id].descriptor;
1938 let reason = BOOTSTRAP_REASONS.APP_STARTUP;
1939 // Eventually set INSTALLED reason when a bootstrap addon
1940 // is dropped in profile folder and automatically installed
1941 if (AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_INSTALLED)
1942 .indexOf(id) !== -1)
1943 reason = BOOTSTRAP_REASONS.ADDON_INSTALL;
1944 this.callBootstrapMethod(id, this.bootstrappedAddons[id].version,
1945 this.bootstrappedAddons[id].type, file,
1946 "startup", reason);
1947 }
1948 catch (e) {
1949 logger.error("Failed to load bootstrap addon " + id + " from " +
1950 this.bootstrappedAddons[id].descriptor, e);
1951 }
1952 }
1953 AddonManagerPrivate.recordTimestamp("XPI_bootstrap_addons_end");
1954 }
1955 catch (e) {
1956 logger.error("bootstrap startup failed", e);
1957 AddonManagerPrivate.recordException("XPI-BOOTSTRAP", "startup failed", e);
1958 }
1959
1960 // Let these shutdown a little earlier when they still have access to most
1961 // of XPCOM
1962 Services.obs.addObserver({
1963 observe: function shutdownObserver(aSubject, aTopic, aData) {
1964 for (let id in XPIProvider.bootstrappedAddons) {
1965 let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
1966 file.persistentDescriptor = XPIProvider.bootstrappedAddons[id].descriptor;
1967 XPIProvider.callBootstrapMethod(id, XPIProvider.bootstrappedAddons[id].version,
1968 XPIProvider.bootstrappedAddons[id].type, file, "shutdown",
1969 BOOTSTRAP_REASONS.APP_SHUTDOWN);
1970 }
1971 Services.obs.removeObserver(this, "quit-application-granted");
1972 }
1973 }, "quit-application-granted", false);
1974
1975 // Detect final-ui-startup for telemetry reporting
1976 Services.obs.addObserver({
1977 observe: function uiStartupObserver(aSubject, aTopic, aData) {
1978 AddonManagerPrivate.recordTimestamp("XPI_finalUIStartup");
1979 XPIProvider.runPhase = XPI_AFTER_UI_STARTUP;
1980 Services.obs.removeObserver(this, "final-ui-startup");
1981 }
1982 }, "final-ui-startup", false);
1983
1984 AddonManagerPrivate.recordTimestamp("XPI_startup_end");
1985
1986 this.extensionsActive = true;
1987 this.runPhase = XPI_BEFORE_UI_STARTUP;
1988 }
1989 catch (e) {
1990 logger.error("startup failed", e);
1991 AddonManagerPrivate.recordException("XPI", "startup failed", e);
1992 }
1993 },
1994
1995 /**
1996 * Shuts down the database and releases all references.
1997 * Return: Promise{integer} resolves / rejects with the result of
1998 * flushing the XPI Database if it was loaded,
1999 * 0 otherwise.
2000 */
2001 shutdown: function XPI_shutdown() {
2002 logger.debug("shutdown");
2003
2004 // Stop anything we were doing asynchronously
2005 this.cancelAll();
2006
2007 this.bootstrappedAddons = {};
2008 this.bootstrapScopes = {};
2009 this.enabledAddons = null;
2010 this.allAppGlobal = true;
2011
2012 this.inactiveAddonIDs = [];
2013
2014 // If there are pending operations then we must update the list of active
2015 // add-ons
2016 if (Prefs.getBoolPref(PREF_PENDING_OPERATIONS, false)) {
2017 XPIDatabase.updateActiveAddons();
2018 Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS,
2019 !XPIDatabase.writeAddonsList());
2020 }
2021
2022 this.installs = null;
2023 this.installLocations = null;
2024 this.installLocationsByName = null;
2025
2026 // This is needed to allow xpcshell tests to simulate a restart
2027 this.extensionsActive = false;
2028
2029 // Remove URI mappings again
2030 delete this._uriMappings;
2031
2032 if (gLazyObjectsLoaded) {
2033 let done = XPIDatabase.shutdown();
2034 done.then(
2035 ret => {
2036 logger.debug("Notifying XPI shutdown observers");
2037 Services.obs.notifyObservers(null, "xpi-provider-shutdown", null);
2038 },
2039 err => {
2040 logger.debug("Notifying XPI shutdown observers");
2041 this._shutdownError = err;
2042 Services.obs.notifyObservers(null, "xpi-provider-shutdown", err);
2043 }
2044 );
2045 return done;
2046 }
2047 else {
2048 logger.debug("Notifying XPI shutdown observers");
2049 Services.obs.notifyObservers(null, "xpi-provider-shutdown", null);
2050 }
2051 },
2052
2053 /**
2054 * Applies any pending theme change to the preferences.
2055 */
2056 applyThemeChange: function XPI_applyThemeChange() {
2057 if (!Prefs.getBoolPref(PREF_DSS_SWITCHPENDING, false))
2058 return;
2059
2060 // Tell the Chrome Registry which Skin to select
2061 try {
2062 this.selectedSkin = Prefs.getCharPref(PREF_DSS_SKIN_TO_SELECT);
2063 Services.prefs.setCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN,
2064 this.selectedSkin);
2065 Services.prefs.clearUserPref(PREF_DSS_SKIN_TO_SELECT);
2066 logger.debug("Changed skin to " + this.selectedSkin);
2067 this.currentSkin = this.selectedSkin;
2068 }
2069 catch (e) {
2070 logger.error("Error applying theme change", e);
2071 }
2072 Services.prefs.clearUserPref(PREF_DSS_SWITCHPENDING);
2073 },
2074
2075 /**
2076 * Shows the "Compatibility Updates" UI
2077 */
2078 showUpgradeUI: function XPI_showUpgradeUI() {
2079 // Flip a flag to indicate that we interrupted startup with an interactive prompt
2080 Services.startup.interrupted = true;
2081
2082 if (!Prefs.getBoolPref(PREF_SHOWN_SELECTION_UI, false)) {
2083 // This *must* be modal as it has to block startup.
2084 var features = "chrome,centerscreen,dialog,titlebar,modal";
2085 Services.ww.openWindow(null, URI_EXTENSION_SELECT_DIALOG, "", features, null);
2086 Services.prefs.setBoolPref(PREF_SHOWN_SELECTION_UI, true);
2087 }
2088 else {
2089 var variant = Cc["@mozilla.org/variant;1"].
2090 createInstance(Ci.nsIWritableVariant);
2091 variant.setFromVariant(this.inactiveAddonIDs);
2092
2093 // This *must* be modal as it has to block startup.
2094 var features = "chrome,centerscreen,dialog,titlebar,modal";
2095 var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"].
2096 getService(Ci.nsIWindowWatcher);
2097 ww.openWindow(null, URI_EXTENSION_UPDATE_DIALOG, "", features, variant);
2098 }
2099
2100 // Ensure any changes to the add-ons list are flushed to disk
2101 Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS,
2102 !XPIDatabase.writeAddonsList());
2103 },
2104
2105 /**
2106 * Persists changes to XPIProvider.bootstrappedAddons to its store (a pref).
2107 */
2108 persistBootstrappedAddons: function XPI_persistBootstrappedAddons() {
2109 // Experiments are disabled upon app load, so don't persist references.
2110 let filtered = {};
2111 for (let id in this.bootstrappedAddons) {
2112 let entry = this.bootstrappedAddons[id];
2113 if (entry.type == "experiment") {
2114 continue;
2115 }
2116
2117 filtered[id] = entry;
2118 }
2119
2120 Services.prefs.setCharPref(PREF_BOOTSTRAP_ADDONS,
2121 JSON.stringify(filtered));
2122 },
2123
2124 /**
2125 * Adds a list of currently active add-ons to the next crash report.
2126 */
2127 addAddonsToCrashReporter: function XPI_addAddonsToCrashReporter() {
2128 if (!("nsICrashReporter" in Ci) ||
2129 !(Services.appinfo instanceof Ci.nsICrashReporter))
2130 return;
2131
2132 // In safe mode no add-ons are loaded so we should not include them in the
2133 // crash report
2134 if (Services.appinfo.inSafeMode)
2135 return;
2136
2137 let data = this.enabledAddons;
2138 for (let id in this.bootstrappedAddons) {
2139 data += (data ? "," : "") + encodeURIComponent(id) + ":" +
2140 encodeURIComponent(this.bootstrappedAddons[id].version);
2141 }
2142
2143 try {
2144 Services.appinfo.annotateCrashReport("Add-ons", data);
2145 }
2146 catch (e) { }
2147
2148 Cu.import("resource://gre/modules/TelemetryPing.jsm", {}).TelemetryPing.setAddOns(data);
2149 },
2150
2151 /**
2152 * Gets the add-on states for an install location.
2153 * This function may be expensive because of the recursiveLastModifiedTime call.
2154 *
2155 * @param location
2156 * The install location to retrieve the add-on states for
2157 * @return a dictionary mapping add-on IDs to objects with a descriptor
2158 * property which contains the add-ons dir/file descriptor and an
2159 * mtime property which contains the add-on's last modified time as
2160 * the number of milliseconds since the epoch.
2161 */
2162 getAddonStates: function XPI_getAddonStates(aLocation) {
2163 let addonStates = {};
2164 for (let file of aLocation.addonLocations) {
2165 let scanStarted = Date.now();
2166 let id = aLocation.getIDForLocation(file);
2167 let unpacked = 0;
2168 let [modFile, modTime, items] = recursiveLastModifiedTime(file);
2169 addonStates[id] = {
2170 descriptor: file.persistentDescriptor,
2171 mtime: modTime
2172 };
2173 try {
2174 // get the install.rdf update time, if any
2175 file.append(FILE_INSTALL_MANIFEST);
2176 let rdfTime = file.lastModifiedTime;
2177 addonStates[id].rdfTime = rdfTime;
2178 unpacked = 1;
2179 }
2180 catch (e) { }
2181 this._mostRecentlyModifiedFile[id] = modFile;
2182 this.setTelemetry(id, "unpacked", unpacked);
2183 this.setTelemetry(id, "location", aLocation.name);
2184 this.setTelemetry(id, "scan_MS", Date.now() - scanStarted);
2185 this.setTelemetry(id, "scan_items", items);
2186 }
2187
2188 return addonStates;
2189 },
2190
2191 /**
2192 * Gets an array of install location states which uniquely describes all
2193 * installed add-ons with the add-on's InstallLocation name and last modified
2194 * time. This function may be expensive because of the getAddonStates() call.
2195 *
2196 * @return an array of add-on states for each install location. Each state
2197 * is an object with a name property holding the location's name and
2198 * an addons property holding the add-on states for the location
2199 */
2200 getInstallLocationStates: function XPI_getInstallLocationStates() {
2201 let states = [];
2202 this.installLocations.forEach(function(aLocation) {
2203 let addons = aLocation.addonLocations;
2204 if (addons.length == 0)
2205 return;
2206
2207 let locationState = {
2208 name: aLocation.name,
2209 addons: this.getAddonStates(aLocation)
2210 };
2211
2212 states.push(locationState);
2213 }, this);
2214 return states;
2215 },
2216
2217 /**
2218 * Check the staging directories of install locations for any add-ons to be
2219 * installed or add-ons to be uninstalled.
2220 *
2221 * @param aManifests
2222 * A dictionary to add detected install manifests to for the purpose
2223 * of passing through updated compatibility information
2224 * @return true if an add-on was installed or uninstalled
2225 */
2226 processPendingFileChanges: function XPI_processPendingFileChanges(aManifests) {
2227 let changed = false;
2228 this.installLocations.forEach(function(aLocation) {
2229 aManifests[aLocation.name] = {};
2230 // We can't install or uninstall anything in locked locations
2231 if (aLocation.locked)
2232 return;
2233
2234 let stagedXPIDir = aLocation.getXPIStagingDir();
2235 let stagingDir = aLocation.getStagingDir();
2236
2237 if (stagedXPIDir.exists() && stagedXPIDir.isDirectory()) {
2238 let entries = stagedXPIDir.directoryEntries
2239 .QueryInterface(Ci.nsIDirectoryEnumerator);
2240 while (entries.hasMoreElements()) {
2241 let stageDirEntry = entries.nextFile;
2242
2243 if (!stageDirEntry.isDirectory()) {
2244 logger.warn("Ignoring file in XPI staging directory: " + stageDirEntry.path);
2245 continue;
2246 }
2247
2248 // Find the last added XPI file in the directory
2249 let stagedXPI = null;
2250 var xpiEntries = stageDirEntry.directoryEntries
2251 .QueryInterface(Ci.nsIDirectoryEnumerator);
2252 while (xpiEntries.hasMoreElements()) {
2253 let file = xpiEntries.nextFile;
2254 if (file.isDirectory())
2255 continue;
2256
2257 let extension = file.leafName;
2258 extension = extension.substring(extension.length - 4);
2259
2260 if (extension != ".xpi" && extension != ".jar")
2261 continue;
2262
2263 stagedXPI = file;
2264 }
2265 xpiEntries.close();
2266
2267 if (!stagedXPI)
2268 continue;
2269
2270 let addon = null;
2271 try {
2272 addon = loadManifestFromZipFile(stagedXPI);
2273 }
2274 catch (e) {
2275 logger.error("Unable to read add-on manifest from " + stagedXPI.path, e);
2276 continue;
2277 }
2278
2279 logger.debug("Migrating staged install of " + addon.id + " in " + aLocation.name);
2280
2281 if (addon.unpack || Prefs.getBoolPref(PREF_XPI_UNPACK, false)) {
2282 let targetDir = stagingDir.clone();
2283 targetDir.append(addon.id);
2284 try {
2285 targetDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
2286 }
2287 catch (e) {
2288 logger.error("Failed to create staging directory for add-on " + addon.id, e);
2289 continue;
2290 }
2291
2292 try {
2293 ZipUtils.extractFiles(stagedXPI, targetDir);
2294 }
2295 catch (e) {
2296 logger.error("Failed to extract staged XPI for add-on " + addon.id + " in " +
2297 aLocation.name, e);
2298 }
2299 }
2300 else {
2301 try {
2302 stagedXPI.moveTo(stagingDir, addon.id + ".xpi");
2303 }
2304 catch (e) {
2305 logger.error("Failed to move staged XPI for add-on " + addon.id + " in " +
2306 aLocation.name, e);
2307 }
2308 }
2309 }
2310 entries.close();
2311 }
2312
2313 if (stagedXPIDir.exists()) {
2314 try {
2315 recursiveRemove(stagedXPIDir);
2316 }
2317 catch (e) {
2318 // Non-critical, just saves some perf on startup if we clean this up.
2319 logger.debug("Error removing XPI staging dir " + stagedXPIDir.path, e);
2320 }
2321 }
2322
2323 try {
2324 if (!stagingDir || !stagingDir.exists() || !stagingDir.isDirectory())
2325 return;
2326 }
2327 catch (e) {
2328 logger.warn("Failed to find staging directory", e);
2329 return;
2330 }
2331
2332 let seenFiles = [];
2333 // Use a snapshot of the directory contents to avoid possible issues with
2334 // iterating over a directory while removing files from it (the YAFFS2
2335 // embedded filesystem has this issue, see bug 772238), and to remove
2336 // normal files before their resource forks on OSX (see bug 733436).
2337 let stagingDirEntries = getDirectoryEntries(stagingDir, true);
2338 for (let stageDirEntry of stagingDirEntries) {
2339 let id = stageDirEntry.leafName;
2340
2341 let isDir;
2342 try {
2343 isDir = stageDirEntry.isDirectory();
2344 }
2345 catch (e if e.result == Cr.NS_ERROR_FILE_TARGET_DOES_NOT_EXIST) {
2346 // If the file has already gone away then don't worry about it, this
2347 // can happen on OSX where the resource fork is automatically moved
2348 // with the data fork for the file. See bug 733436.
2349 continue;
2350 }
2351
2352 if (!isDir) {
2353 if (id.substring(id.length - 4).toLowerCase() == ".xpi") {
2354 id = id.substring(0, id.length - 4);
2355 }
2356 else {
2357 if (id.substring(id.length - 5).toLowerCase() != ".json") {
2358 logger.warn("Ignoring file: " + stageDirEntry.path);
2359 seenFiles.push(stageDirEntry.leafName);
2360 }
2361 continue;
2362 }
2363 }
2364
2365 // Check that the directory's name is a valid ID.
2366 if (!gIDTest.test(id)) {
2367 logger.warn("Ignoring directory whose name is not a valid add-on ID: " +
2368 stageDirEntry.path);
2369 seenFiles.push(stageDirEntry.leafName);
2370 continue;
2371 }
2372
2373 changed = true;
2374
2375 if (isDir) {
2376 // Check if the directory contains an install manifest.
2377 let manifest = stageDirEntry.clone();
2378 manifest.append(FILE_INSTALL_MANIFEST);
2379
2380 // If the install manifest doesn't exist uninstall this add-on in this
2381 // install location.
2382 if (!manifest.exists()) {
2383 logger.debug("Processing uninstall of " + id + " in " + aLocation.name);
2384 try {
2385 aLocation.uninstallAddon(id);
2386 seenFiles.push(stageDirEntry.leafName);
2387 }
2388 catch (e) {
2389 logger.error("Failed to uninstall add-on " + id + " in " + aLocation.name, e);
2390 }
2391 // The file check later will spot the removal and cleanup the database
2392 continue;
2393 }
2394 }
2395
2396 aManifests[aLocation.name][id] = null;
2397 let existingAddonID = id;
2398
2399 let jsonfile = stagingDir.clone();
2400 jsonfile.append(id + ".json");
2401
2402 try {
2403 aManifests[aLocation.name][id] = loadManifestFromFile(stageDirEntry);
2404 }
2405 catch (e) {
2406 logger.error("Unable to read add-on manifest from " + stageDirEntry.path, e);
2407 // This add-on can't be installed so just remove it now
2408 seenFiles.push(stageDirEntry.leafName);
2409 seenFiles.push(jsonfile.leafName);
2410 continue;
2411 }
2412
2413 // Check for a cached metadata for this add-on, it may contain updated
2414 // compatibility information
2415 if (jsonfile.exists()) {
2416 logger.debug("Found updated metadata for " + id + " in " + aLocation.name);
2417 let fis = Cc["@mozilla.org/network/file-input-stream;1"].
2418 createInstance(Ci.nsIFileInputStream);
2419 let json = Cc["@mozilla.org/dom/json;1"].
2420 createInstance(Ci.nsIJSON);
2421
2422 try {
2423 fis.init(jsonfile, -1, 0, 0);
2424 let metadata = json.decodeFromStream(fis, jsonfile.fileSize);
2425 aManifests[aLocation.name][id].importMetadata(metadata);
2426 }
2427 catch (e) {
2428 // If some data can't be recovered from the cached metadata then it
2429 // is unlikely to be a problem big enough to justify throwing away
2430 // the install, just log and error and continue
2431 logger.error("Unable to read metadata from " + jsonfile.path, e);
2432 }
2433 finally {
2434 fis.close();
2435 }
2436 }
2437 seenFiles.push(jsonfile.leafName);
2438
2439 existingAddonID = aManifests[aLocation.name][id].existingAddonID || id;
2440
2441 var oldBootstrap = null;
2442 logger.debug("Processing install of " + id + " in " + aLocation.name);
2443 if (existingAddonID in this.bootstrappedAddons) {
2444 try {
2445 var existingAddon = aLocation.getLocationForID(existingAddonID);
2446 if (this.bootstrappedAddons[existingAddonID].descriptor ==
2447 existingAddon.persistentDescriptor) {
2448 oldBootstrap = this.bootstrappedAddons[existingAddonID];
2449
2450 // We'll be replacing a currently active bootstrapped add-on so
2451 // call its uninstall method
2452 let newVersion = aManifests[aLocation.name][id].version;
2453 let oldVersion = oldBootstrap.version;
2454 let uninstallReason = Services.vc.compare(oldVersion, newVersion) < 0 ?
2455 BOOTSTRAP_REASONS.ADDON_UPGRADE :
2456 BOOTSTRAP_REASONS.ADDON_DOWNGRADE;
2457
2458 this.callBootstrapMethod(existingAddonID, oldBootstrap.version,
2459 oldBootstrap.type, existingAddon, "uninstall", uninstallReason,
2460 { newVersion: newVersion });
2461 this.unloadBootstrapScope(existingAddonID);
2462 flushStartupCache();
2463 }
2464 }
2465 catch (e) {
2466 }
2467 }
2468
2469 try {
2470 var addonInstallLocation = aLocation.installAddon(id, stageDirEntry,
2471 existingAddonID);
2472 if (aManifests[aLocation.name][id])
2473 aManifests[aLocation.name][id]._sourceBundle = addonInstallLocation;
2474 }
2475 catch (e) {
2476 logger.error("Failed to install staged add-on " + id + " in " + aLocation.name,
2477 e);
2478 // Re-create the staged install
2479 AddonInstall.createStagedInstall(aLocation, stageDirEntry,
2480 aManifests[aLocation.name][id]);
2481 // Make sure not to delete the cached manifest json file
2482 seenFiles.pop();
2483
2484 delete aManifests[aLocation.name][id];
2485
2486 if (oldBootstrap) {
2487 // Re-install the old add-on
2488 this.callBootstrapMethod(existingAddonID, oldBootstrap.version,
2489 oldBootstrap.type, existingAddon, "install",
2490 BOOTSTRAP_REASONS.ADDON_INSTALL);
2491 }
2492 continue;
2493 }
2494 }
2495
2496 try {
2497 aLocation.cleanStagingDir(seenFiles);
2498 }
2499 catch (e) {
2500 // Non-critical, just saves some perf on startup if we clean this up.
2501 logger.debug("Error cleaning staging dir " + stagingDir.path, e);
2502 }
2503 }, this);
2504 return changed;
2505 },
2506
2507 /**
2508 * Installs any add-ons located in the extensions directory of the
2509 * application's distribution specific directory into the profile unless a
2510 * newer version already exists or the user has previously uninstalled the
2511 * distributed add-on.
2512 *
2513 * @param aManifests
2514 * A dictionary to add new install manifests to to save having to
2515 * reload them later
2516 * @return true if any new add-ons were installed
2517 */
2518 installDistributionAddons: function XPI_installDistributionAddons(aManifests) {
2519 let distroDir;
2520 try {
2521 distroDir = FileUtils.getDir(KEY_APP_DISTRIBUTION, [DIR_EXTENSIONS]);
2522 }
2523 catch (e) {
2524 return false;
2525 }
2526
2527 if (!distroDir.exists())
2528 return false;
2529
2530 if (!distroDir.isDirectory())
2531 return false;
2532
2533 let changed = false;
2534 let profileLocation = this.installLocationsByName[KEY_APP_PROFILE];
2535
2536 let entries = distroDir.directoryEntries
2537 .QueryInterface(Ci.nsIDirectoryEnumerator);
2538 let entry;
2539 while ((entry = entries.nextFile)) {
2540
2541 let id = entry.leafName;
2542
2543 if (entry.isFile()) {
2544 if (id.substring(id.length - 4).toLowerCase() == ".xpi") {
2545 id = id.substring(0, id.length - 4);
2546 }
2547 else {
2548 logger.debug("Ignoring distribution add-on that isn't an XPI: " + entry.path);
2549 continue;
2550 }
2551 }
2552 else if (!entry.isDirectory()) {
2553 logger.debug("Ignoring distribution add-on that isn't a file or directory: " +
2554 entry.path);
2555 continue;
2556 }
2557
2558 if (!gIDTest.test(id)) {
2559 logger.debug("Ignoring distribution add-on whose name is not a valid add-on ID: " +
2560 entry.path);
2561 continue;
2562 }
2563
2564 let addon;
2565 try {
2566 addon = loadManifestFromFile(entry);
2567 }
2568 catch (e) {
2569 logger.warn("File entry " + entry.path + " contains an invalid add-on", e);
2570 continue;
2571 }
2572
2573 if (addon.id != id) {
2574 logger.warn("File entry " + entry.path + " contains an add-on with an " +
2575 "incorrect ID")
2576 continue;
2577 }
2578
2579 let existingEntry = null;
2580 try {
2581 existingEntry = profileLocation.getLocationForID(id);
2582 }
2583 catch (e) {
2584 }
2585
2586 if (existingEntry) {
2587 let existingAddon;
2588 try {
2589 existingAddon = loadManifestFromFile(existingEntry);
2590
2591 if (Services.vc.compare(addon.version, existingAddon.version) <= 0)
2592 continue;
2593 }
2594 catch (e) {
2595 // Bad add-on in the profile so just proceed and install over the top
2596 logger.warn("Profile contains an add-on with a bad or missing install " +
2597 "manifest at " + existingEntry.path + ", overwriting", e);
2598 }
2599 }
2600 else if (Prefs.getBoolPref(PREF_BRANCH_INSTALLED_ADDON + id, false)) {
2601 continue;
2602 }
2603
2604 // Install the add-on
2605 try {
2606 profileLocation.installAddon(id, entry, null, true);
2607 logger.debug("Installed distribution add-on " + id);
2608
2609 Services.prefs.setBoolPref(PREF_BRANCH_INSTALLED_ADDON + id, true)
2610
2611 // aManifests may contain a copy of a newly installed add-on's manifest
2612 // and we'll have overwritten that so instead cache our install manifest
2613 // which will later be put into the database in processFileChanges
2614 if (!(KEY_APP_PROFILE in aManifests))
2615 aManifests[KEY_APP_PROFILE] = {};
2616 aManifests[KEY_APP_PROFILE][id] = addon;
2617 changed = true;
2618 }
2619 catch (e) {
2620 logger.error("Failed to install distribution add-on " + entry.path, e);
2621 }
2622 }
2623
2624 entries.close();
2625
2626 return changed;
2627 },
2628
2629 /**
2630 * Compares the add-ons that are currently installed to those that were
2631 * known to be installed when the application last ran and applies any
2632 * changes found to the database. Also sends "startupcache-invalidate" signal to
2633 * observerservice if it detects that data may have changed.
2634 *
2635 * @param aState
2636 * The array of current install location states
2637 * @param aManifests
2638 * A dictionary of cached AddonInstalls for add-ons that have been
2639 * installed
2640 * @param aUpdateCompatibility
2641 * true to update add-ons appDisabled property when the application
2642 * version has changed
2643 * @param aOldAppVersion
2644 * The version of the application last run with this profile or null
2645 * if it is a new profile or the version is unknown
2646 * @param aOldPlatformVersion
2647 * The version of the platform last run with this profile or null
2648 * if it is a new profile or the version is unknown
2649 * @return a boolean indicating if a change requiring flushing the caches was
2650 * detected
2651 */
2652 processFileChanges: function XPI_processFileChanges(aState, aManifests,
2653 aUpdateCompatibility,
2654 aOldAppVersion,
2655 aOldPlatformVersion) {
2656 let visibleAddons = {};
2657 let oldBootstrappedAddons = this.bootstrappedAddons;
2658 this.bootstrappedAddons = {};
2659
2660 /**
2661 * Updates an add-on's metadata and determines if a restart of the
2662 * application is necessary. This is called when either the add-on's
2663 * install directory path or last modified time has changed.
2664 *
2665 * @param aInstallLocation
2666 * The install location containing the add-on
2667 * @param aOldAddon
2668 * The AddonInternal as it appeared the last time the application
2669 * ran
2670 * @param aAddonState
2671 * The new state of the add-on
2672 * @return a boolean indicating if flushing caches is required to complete
2673 * changing this add-on
2674 */
2675 function updateMetadata(aInstallLocation, aOldAddon, aAddonState) {
2676 logger.debug("Add-on " + aOldAddon.id + " modified in " + aInstallLocation.name);
2677
2678 // Check if there is an updated install manifest for this add-on
2679 let newAddon = aManifests[aInstallLocation.name][aOldAddon.id];
2680
2681 try {
2682 // If not load it
2683 if (!newAddon) {
2684 let file = aInstallLocation.getLocationForID(aOldAddon.id);
2685 newAddon = loadManifestFromFile(file);
2686 applyBlocklistChanges(aOldAddon, newAddon);
2687
2688 // Carry over any pendingUninstall state to add-ons modified directly
2689 // in the profile. This is important when the attempt to remove the
2690 // add-on in processPendingFileChanges failed and caused an mtime
2691 // change to the add-ons files.
2692 newAddon.pendingUninstall = aOldAddon.pendingUninstall;
2693 }
2694
2695 // The ID in the manifest that was loaded must match the ID of the old
2696 // add-on.
2697 if (newAddon.id != aOldAddon.id)
2698 throw new Error("Incorrect id in install manifest");
2699 }
2700 catch (e) {
2701 logger.warn("Add-on is invalid", e);
2702 XPIDatabase.removeAddonMetadata(aOldAddon);
2703 if (!aInstallLocation.locked)
2704 aInstallLocation.uninstallAddon(aOldAddon.id);
2705 else
2706 logger.warn("Could not uninstall invalid item from locked install location");
2707 // If this was an active add-on then we must force a restart
2708 if (aOldAddon.active)
2709 return true;
2710
2711 return false;
2712 }
2713
2714 // Set the additional properties on the new AddonInternal
2715 newAddon._installLocation = aInstallLocation;
2716 newAddon.updateDate = aAddonState.mtime;
2717 newAddon.visible = !(newAddon.id in visibleAddons);
2718
2719 // Update the database
2720 let newDBAddon = XPIDatabase.updateAddonMetadata(aOldAddon, newAddon,
2721 aAddonState.descriptor);
2722 if (newDBAddon.visible) {
2723 visibleAddons[newDBAddon.id] = newDBAddon;
2724 // Remember add-ons that were changed during startup
2725 AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_CHANGED,
2726 newDBAddon.id);
2727
2728 // If this was the active theme and it is now disabled then enable the
2729 // default theme
2730 if (aOldAddon.active && isAddonDisabled(newDBAddon))
2731 XPIProvider.enableDefaultTheme();
2732
2733 // If the new add-on is bootstrapped and active then call its install method
2734 if (newDBAddon.active && newDBAddon.bootstrap) {
2735 // Startup cache must be flushed before calling the bootstrap script
2736 flushStartupCache();
2737
2738 let installReason = Services.vc.compare(aOldAddon.version, newDBAddon.version) < 0 ?
2739 BOOTSTRAP_REASONS.ADDON_UPGRADE :
2740 BOOTSTRAP_REASONS.ADDON_DOWNGRADE;
2741
2742 let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
2743 file.persistentDescriptor = aAddonState.descriptor;
2744 XPIProvider.callBootstrapMethod(newDBAddon.id, newDBAddon.version,
2745 newDBAddon.type, file, "install",
2746 installReason, { oldVersion: aOldAddon.version });
2747 return false;
2748 }
2749
2750 return true;
2751 }
2752
2753 return false;
2754 }
2755
2756 /**
2757 * Updates an add-on's descriptor for when the add-on has moved in the
2758 * filesystem but hasn't changed in any other way.
2759 *
2760 * @param aInstallLocation
2761 * The install location containing the add-on
2762 * @param aOldAddon
2763 * The AddonInternal as it appeared the last time the application
2764 * ran
2765 * @param aAddonState
2766 * The new state of the add-on
2767 * @return a boolean indicating if flushing caches is required to complete
2768 * changing this add-on
2769 */
2770 function updateDescriptor(aInstallLocation, aOldAddon, aAddonState) {
2771 logger.debug("Add-on " + aOldAddon.id + " moved to " + aAddonState.descriptor);
2772
2773 aOldAddon.descriptor = aAddonState.descriptor;
2774 aOldAddon.visible = !(aOldAddon.id in visibleAddons);
2775 XPIDatabase.saveChanges();
2776
2777 if (aOldAddon.visible) {
2778 visibleAddons[aOldAddon.id] = aOldAddon;
2779
2780 if (aOldAddon.bootstrap && aOldAddon.active) {
2781 let bootstrap = oldBootstrappedAddons[aOldAddon.id];
2782 bootstrap.descriptor = aAddonState.descriptor;
2783 XPIProvider.bootstrappedAddons[aOldAddon.id] = bootstrap;
2784 }
2785
2786 return true;
2787 }
2788
2789 return false;
2790 }
2791
2792 /**
2793 * Called when no change has been detected for an add-on's metadata. The
2794 * add-on may have become visible due to other add-ons being removed or
2795 * the add-on may need to be updated when the application version has
2796 * changed.
2797 *
2798 * @param aInstallLocation
2799 * The install location containing the add-on
2800 * @param aOldAddon
2801 * The AddonInternal as it appeared the last time the application
2802 * ran
2803 * @param aAddonState
2804 * The new state of the add-on
2805 * @return a boolean indicating if flushing caches is required to complete
2806 * changing this add-on
2807 */
2808 function updateVisibilityAndCompatibility(aInstallLocation, aOldAddon,
2809 aAddonState) {
2810 let changed = false;
2811
2812 // This add-ons metadata has not changed but it may have become visible
2813 if (!(aOldAddon.id in visibleAddons)) {
2814 visibleAddons[aOldAddon.id] = aOldAddon;
2815
2816 if (!aOldAddon.visible) {
2817 // Remember add-ons that were changed during startup.
2818 AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_CHANGED,
2819 aOldAddon.id);
2820 XPIDatabase.makeAddonVisible(aOldAddon);
2821
2822 if (aOldAddon.bootstrap) {
2823 // The add-on is bootstrappable so call its install script
2824 let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
2825 file.persistentDescriptor = aAddonState.descriptor;
2826 XPIProvider.callBootstrapMethod(aOldAddon.id, aOldAddon.version, aOldAddon.type, file,
2827 "install",
2828 BOOTSTRAP_REASONS.ADDON_INSTALL);
2829
2830 // If it should be active then mark it as active otherwise unload
2831 // its scope
2832 if (!isAddonDisabled(aOldAddon)) {
2833 XPIDatabase.updateAddonActive(aOldAddon, true);
2834 }
2835 else {
2836 XPIProvider.unloadBootstrapScope(newAddon.id);
2837 }
2838 }
2839 else {
2840 // Otherwise a restart is necessary
2841 changed = true;
2842 }
2843 }
2844 }
2845
2846 // App version changed, we may need to update the appDisabled property.
2847 if (aUpdateCompatibility) {
2848 let wasDisabled = isAddonDisabled(aOldAddon);
2849 let wasAppDisabled = aOldAddon.appDisabled;
2850 let wasUserDisabled = aOldAddon.userDisabled;
2851 let wasSoftDisabled = aOldAddon.softDisabled;
2852
2853 // This updates the addon's JSON cached data in place
2854 applyBlocklistChanges(aOldAddon, aOldAddon, aOldAppVersion,
2855 aOldPlatformVersion);
2856 aOldAddon.appDisabled = !isUsableAddon(aOldAddon);
2857
2858 let isDisabled = isAddonDisabled(aOldAddon);
2859
2860 // If either property has changed update the database.
2861 if (wasAppDisabled != aOldAddon.appDisabled ||
2862 wasUserDisabled != aOldAddon.userDisabled ||
2863 wasSoftDisabled != aOldAddon.softDisabled) {
2864 logger.debug("Add-on " + aOldAddon.id + " changed appDisabled state to " +
2865 aOldAddon.appDisabled + ", userDisabled state to " +
2866 aOldAddon.userDisabled + " and softDisabled state to " +
2867 aOldAddon.softDisabled);
2868 XPIDatabase.saveChanges();
2869 }
2870
2871 // If this is a visible add-on and it has changed disabled state then we
2872 // may need a restart or to update the bootstrap list.
2873 if (aOldAddon.visible && wasDisabled != isDisabled) {
2874 // Remember add-ons that became disabled or enabled by the application
2875 // change
2876 let change = isDisabled ? AddonManager.STARTUP_CHANGE_DISABLED
2877 : AddonManager.STARTUP_CHANGE_ENABLED;
2878 AddonManagerPrivate.addStartupChange(change, aOldAddon.id);
2879 if (aOldAddon.bootstrap) {
2880 // Update the add-ons active state
2881 XPIDatabase.updateAddonActive(aOldAddon, !isDisabled);
2882 }
2883 else {
2884 changed = true;
2885 }
2886 }
2887 }
2888
2889 if (aOldAddon.visible && aOldAddon.active && aOldAddon.bootstrap) {
2890 XPIProvider.bootstrappedAddons[aOldAddon.id] = {
2891 version: aOldAddon.version,
2892 type: aOldAddon.type,
2893 descriptor: aAddonState.descriptor
2894 };
2895 }
2896
2897 return changed;
2898 }
2899
2900 /**
2901 * Called when an add-on has been removed.
2902 *
2903 * @param aOldAddon
2904 * The AddonInternal as it appeared the last time the application
2905 * ran
2906 * @return a boolean indicating if flushing caches is required to complete
2907 * changing this add-on
2908 */
2909 function removeMetadata(aOldAddon) {
2910 // This add-on has disappeared
2911 logger.debug("Add-on " + aOldAddon.id + " removed from " + aOldAddon.location);
2912 XPIDatabase.removeAddonMetadata(aOldAddon);
2913
2914 // Remember add-ons that were uninstalled during startup
2915 if (aOldAddon.visible) {
2916 AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_UNINSTALLED,
2917 aOldAddon.id);
2918 }
2919 else if (AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_INSTALLED)
2920 .indexOf(aOldAddon.id) != -1) {
2921 AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_CHANGED,
2922 aOldAddon.id);
2923 }
2924
2925 if (aOldAddon.active) {
2926 // Enable the default theme if the previously active theme has been
2927 // removed
2928 if (aOldAddon.type == "theme")
2929 XPIProvider.enableDefaultTheme();
2930
2931 return true;
2932 }
2933
2934 return false;
2935 }
2936
2937 /**
2938 * Called to add the metadata for an add-on in one of the install locations
2939 * to the database. This can be called in three different cases. Either an
2940 * add-on has been dropped into the location from outside of Firefox, or
2941 * an add-on has been installed through the application, or the database
2942 * has been upgraded or become corrupt and add-on data has to be reloaded
2943 * into it.
2944 *
2945 * @param aInstallLocation
2946 * The install location containing the add-on
2947 * @param aId
2948 * The ID of the add-on
2949 * @param aAddonState
2950 * The new state of the add-on
2951 * @param aMigrateData
2952 * If during startup the database had to be upgraded this will
2953 * contain data that used to be held about this add-on
2954 * @return a boolean indicating if flushing caches is required to complete
2955 * changing this add-on
2956 */
2957 function addMetadata(aInstallLocation, aId, aAddonState, aMigrateData) {
2958 logger.debug("New add-on " + aId + " installed in " + aInstallLocation.name);
2959
2960 let newAddon = null;
2961 let sameVersion = false;
2962 // Check the updated manifests lists for the install location, If there
2963 // is no manifest for the add-on ID then newAddon will be undefined
2964 if (aInstallLocation.name in aManifests)
2965 newAddon = aManifests[aInstallLocation.name][aId];
2966
2967 // If we had staged data for this add-on or we aren't recovering from a
2968 // corrupt database and we don't have migration data for this add-on then
2969 // this must be a new install.
2970 let isNewInstall = (!!newAddon) || (!XPIDatabase.activeBundles && !aMigrateData);
2971
2972 // If it's a new install and we haven't yet loaded the manifest then it
2973 // must be something dropped directly into the install location
2974 let isDetectedInstall = isNewInstall && !newAddon;
2975
2976 // Load the manifest if necessary and sanity check the add-on ID
2977 try {
2978 if (!newAddon) {
2979 // Load the manifest from the add-on.
2980 let file = aInstallLocation.getLocationForID(aId);
2981 newAddon = loadManifestFromFile(file);
2982 }
2983 // The add-on in the manifest should match the add-on ID.
2984 if (newAddon.id != aId) {
2985 throw new Error("Invalid addon ID: expected addon ID " + aId +
2986 ", found " + newAddon.id + " in manifest");
2987 }
2988 }
2989 catch (e) {
2990 logger.warn("Add-on is invalid", e);
2991
2992 // Remove the invalid add-on from the install location if the install
2993 // location isn't locked, no restart will be necessary
2994 if (!aInstallLocation.locked)
2995 aInstallLocation.uninstallAddon(aId);
2996 else
2997 logger.warn("Could not uninstall invalid item from locked install location");
2998 return false;
2999 }
3000
3001 // Update the AddonInternal properties.
3002 newAddon._installLocation = aInstallLocation;
3003 newAddon.visible = !(newAddon.id in visibleAddons);
3004 newAddon.installDate = aAddonState.mtime;
3005 newAddon.updateDate = aAddonState.mtime;
3006 newAddon.foreignInstall = isDetectedInstall;
3007
3008 if (aMigrateData) {
3009 // If there is migration data then apply it.
3010 logger.debug("Migrating data from old database");
3011
3012 DB_MIGRATE_METADATA.forEach(function(aProp) {
3013 // A theme's disabled state is determined by the selected theme
3014 // preference which is read in loadManifestFromRDF
3015 if (aProp == "userDisabled" && newAddon.type == "theme")
3016 return;
3017
3018 if (aProp in aMigrateData)
3019 newAddon[aProp] = aMigrateData[aProp];
3020 });
3021
3022 // Force all non-profile add-ons to be foreignInstalls since they can't
3023 // have been installed through the API
3024 newAddon.foreignInstall |= aInstallLocation.name != KEY_APP_PROFILE;
3025
3026 // Some properties should only be migrated if the add-on hasn't changed.
3027 // The version property isn't a perfect check for this but covers the
3028 // vast majority of cases.
3029 if (aMigrateData.version == newAddon.version) {
3030 logger.debug("Migrating compatibility info");
3031 sameVersion = true;
3032 if ("targetApplications" in aMigrateData)
3033 newAddon.applyCompatibilityUpdate(aMigrateData, true);
3034 }
3035
3036 // Since the DB schema has changed make sure softDisabled is correct
3037 applyBlocklistChanges(newAddon, newAddon, aOldAppVersion,
3038 aOldPlatformVersion);
3039 }
3040
3041 // The default theme is never a foreign install
3042 if (newAddon.type == "theme" && newAddon.internalName == XPIProvider.defaultSkin)
3043 newAddon.foreignInstall = false;
3044
3045 if (isDetectedInstall && newAddon.foreignInstall) {
3046 // If the add-on is a foreign install and is in a scope where add-ons
3047 // that were dropped in should default to disabled then disable it
3048 let disablingScopes = Prefs.getIntPref(PREF_EM_AUTO_DISABLED_SCOPES, 0);
3049 if (aInstallLocation.scope & disablingScopes)
3050 newAddon.userDisabled = true;
3051 }
3052
3053 // If we have a list of what add-ons should be marked as active then use
3054 // it to guess at migration data.
3055 if (!isNewInstall && XPIDatabase.activeBundles) {
3056 // For themes we know which is active by the current skin setting
3057 if (newAddon.type == "theme")
3058 newAddon.active = newAddon.internalName == XPIProvider.currentSkin;
3059 else
3060 newAddon.active = XPIDatabase.activeBundles.indexOf(aAddonState.descriptor) != -1;
3061
3062 // If the add-on wasn't active and it isn't already disabled in some way
3063 // then it was probably either softDisabled or userDisabled
3064 if (!newAddon.active && newAddon.visible && !isAddonDisabled(newAddon)) {
3065 // If the add-on is softblocked then assume it is softDisabled
3066 if (newAddon.blocklistState == Ci.nsIBlocklistService.STATE_SOFTBLOCKED)
3067 newAddon.softDisabled = true;
3068 else
3069 newAddon.userDisabled = true;
3070 }
3071 }
3072 else {
3073 newAddon.active = (newAddon.visible && !isAddonDisabled(newAddon))
3074 }
3075
3076 let newDBAddon = XPIDatabase.addAddonMetadata(newAddon, aAddonState.descriptor);
3077
3078 if (newDBAddon.visible) {
3079 // Remember add-ons that were first detected during startup.
3080 if (isDetectedInstall) {
3081 // If a copy from a higher priority location was removed then this
3082 // add-on has changed
3083 if (AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_UNINSTALLED)
3084 .indexOf(newDBAddon.id) != -1) {
3085 AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_CHANGED,
3086 newDBAddon.id);
3087 }
3088 else {
3089 AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_INSTALLED,
3090 newDBAddon.id);
3091 }
3092 }
3093
3094 // Note if any visible add-on is not in the application install location
3095 if (newDBAddon._installLocation.name != KEY_APP_GLOBAL)
3096 XPIProvider.allAppGlobal = false;
3097
3098 visibleAddons[newDBAddon.id] = newDBAddon;
3099
3100 let installReason = BOOTSTRAP_REASONS.ADDON_INSTALL;
3101 let extraParams = {};
3102
3103 // If we're hiding a bootstrapped add-on then call its uninstall method
3104 if (newDBAddon.id in oldBootstrappedAddons) {
3105 let oldBootstrap = oldBootstrappedAddons[newDBAddon.id];
3106 extraParams.oldVersion = oldBootstrap.version;
3107 XPIProvider.bootstrappedAddons[newDBAddon.id] = oldBootstrap;
3108
3109 // If the old version is the same as the new version, or we're
3110 // recovering from a corrupt DB, don't call uninstall and install
3111 // methods.
3112 if (sameVersion || !isNewInstall)
3113 return false;
3114
3115 installReason = Services.vc.compare(oldBootstrap.version, newDBAddon.version) < 0 ?
3116 BOOTSTRAP_REASONS.ADDON_UPGRADE :
3117 BOOTSTRAP_REASONS.ADDON_DOWNGRADE;
3118
3119 let oldAddonFile = Cc["@mozilla.org/file/local;1"].
3120 createInstance(Ci.nsIFile);
3121 oldAddonFile.persistentDescriptor = oldBootstrap.descriptor;
3122
3123 XPIProvider.callBootstrapMethod(newDBAddon.id, oldBootstrap.version,
3124 oldBootstrap.type, oldAddonFile, "uninstall",
3125 installReason, { newVersion: newDBAddon.version });
3126 XPIProvider.unloadBootstrapScope(newDBAddon.id);
3127
3128 // If the new add-on is bootstrapped then we must flush the caches
3129 // before calling the new bootstrap script
3130 if (newDBAddon.bootstrap)
3131 flushStartupCache();
3132 }
3133
3134 if (!newDBAddon.bootstrap)
3135 return true;
3136
3137 // Visible bootstrapped add-ons need to have their install method called
3138 let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
3139 file.persistentDescriptor = aAddonState.descriptor;
3140 XPIProvider.callBootstrapMethod(newDBAddon.id, newDBAddon.version, newDBAddon.type, file,
3141 "install", installReason, extraParams);
3142 if (!newDBAddon.active)
3143 XPIProvider.unloadBootstrapScope(newDBAddon.id);
3144 }
3145
3146 return false;
3147 }
3148
3149 let changed = false;
3150 let knownLocations = XPIDatabase.getInstallLocations();
3151
3152 // The install locations are iterated in reverse order of priority so when
3153 // there are multiple add-ons installed with the same ID the one that
3154 // should be visible is the first one encountered.
3155 for (let aSt of aState.reverse()) {
3156
3157 // We can't include the install location directly in the state as it has
3158 // to be cached as JSON.
3159 let installLocation = this.installLocationsByName[aSt.name];
3160 let addonStates = aSt.addons;
3161
3162 // Check if the database knows about any add-ons in this install location.
3163 if (knownLocations.has(installLocation.name)) {
3164 knownLocations.delete(installLocation.name);
3165 let addons = XPIDatabase.getAddonsInLocation(installLocation.name);
3166 // Iterate through the add-ons installed the last time the application
3167 // ran
3168 for (let aOldAddon of addons) {
3169 // If a version of this add-on has been installed in an higher
3170 // priority install location then count it as changed
3171 if (AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_INSTALLED)
3172 .indexOf(aOldAddon.id) != -1) {
3173 AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_CHANGED,
3174 aOldAddon.id);
3175 }
3176
3177 // Check if the add-on is still installed
3178 if (aOldAddon.id in addonStates) {
3179 let addonState = addonStates[aOldAddon.id];
3180 delete addonStates[aOldAddon.id];
3181
3182 // Remember add-ons that were inactive during startup
3183 if (aOldAddon.visible && !aOldAddon.active)
3184 XPIProvider.inactiveAddonIDs.push(aOldAddon.id);
3185
3186 // record a bit more per-addon telemetry
3187 let loc = aOldAddon.defaultLocale;
3188 if (loc) {
3189 XPIProvider.setTelemetry(aOldAddon.id, "name", loc.name);
3190 XPIProvider.setTelemetry(aOldAddon.id, "creator", loc.creator);
3191 }
3192
3193 // Check if the add-on has been changed outside the XPI provider
3194 if (aOldAddon.updateDate != addonState.mtime) {
3195 // Did time change in the wrong direction?
3196 if (addonState.mtime < aOldAddon.updateDate) {
3197 this.setTelemetry(aOldAddon.id, "olderFile", {
3198 name: this._mostRecentlyModifiedFile[aOldAddon.id],
3199 mtime: addonState.mtime,
3200 oldtime: aOldAddon.updateDate
3201 });
3202 }
3203 // Is the add-on unpacked?
3204 else if (addonState.rdfTime) {
3205 // Was the addon manifest "install.rdf" modified, or some other file?
3206 if (addonState.rdfTime > aOldAddon.updateDate) {
3207 this.setTelemetry(aOldAddon.id, "modifiedInstallRDF", 1);
3208 }
3209 else {
3210 this.setTelemetry(aOldAddon.id, "modifiedFile",
3211 this._mostRecentlyModifiedFile[aOldAddon.id]);
3212 }
3213 }
3214 else {
3215 this.setTelemetry(aOldAddon.id, "modifiedXPI", 1);
3216 }
3217 }
3218
3219 // The add-on has changed if the modification time has changed, or
3220 // we have an updated manifest for it. Also reload the metadata for
3221 // add-ons in the application directory when the application version
3222 // has changed
3223 if (aOldAddon.id in aManifests[installLocation.name] ||
3224 aOldAddon.updateDate != addonState.mtime ||
3225 (aUpdateCompatibility && installLocation.name == KEY_APP_GLOBAL)) {
3226 changed = updateMetadata(installLocation, aOldAddon, addonState) ||
3227 changed;
3228 }
3229 else if (aOldAddon.descriptor != addonState.descriptor) {
3230 changed = updateDescriptor(installLocation, aOldAddon, addonState) ||
3231 changed;
3232 }
3233 else {
3234 changed = updateVisibilityAndCompatibility(installLocation,
3235 aOldAddon, addonState) ||
3236 changed;
3237 }
3238 if (aOldAddon.visible && aOldAddon._installLocation.name != KEY_APP_GLOBAL)
3239 XPIProvider.allAppGlobal = false;
3240 }
3241 else {
3242 changed = removeMetadata(aOldAddon) || changed;
3243 }
3244 }
3245 }
3246
3247 // All the remaining add-ons in this install location must be new.
3248
3249 // Get the migration data for this install location.
3250 let locMigrateData = {};
3251 if (XPIDatabase.migrateData && installLocation.name in XPIDatabase.migrateData)
3252 locMigrateData = XPIDatabase.migrateData[installLocation.name];
3253 for (let id in addonStates) {
3254 changed = addMetadata(installLocation, id, addonStates[id],
3255 (locMigrateData[id] || null)) || changed;
3256 }
3257 }
3258
3259 // The remaining locations that had add-ons installed in them no longer
3260 // have any add-ons installed in them, or the locations no longer exist.
3261 // The metadata for the add-ons that were in them must be removed from the
3262 // database.
3263 for (let location of knownLocations) {
3264 let addons = XPIDatabase.getAddonsInLocation(location);
3265 for (let aOldAddon of addons) {
3266 changed = removeMetadata(aOldAddon) || changed;
3267 }
3268 }
3269
3270 // Cache the new install location states
3271 this.installStates = this.getInstallLocationStates();
3272 let cache = JSON.stringify(this.installStates);
3273 Services.prefs.setCharPref(PREF_INSTALL_CACHE, cache);
3274 this.persistBootstrappedAddons();
3275
3276 // Clear out any cached migration data.
3277 XPIDatabase.migrateData = null;
3278
3279 return changed;
3280 },
3281
3282 /**
3283 * Imports the xpinstall permissions from preferences into the permissions
3284 * manager for the user to change later.
3285 */
3286 importPermissions: function XPI_importPermissions() {
3287 PermissionsUtils.importFromPrefs(PREF_XPI_PERMISSIONS_BRANCH,
3288 XPI_PERMISSION);
3289 },
3290
3291 /**
3292 * Checks for any changes that have occurred since the last time the
3293 * application was launched.
3294 *
3295 * @param aAppChanged
3296 * A tri-state value. Undefined means the current profile was created
3297 * for this session, true means the profile already existed but was
3298 * last used with an application with a different version number,
3299 * false means that the profile was last used by this version of the
3300 * application.
3301 * @param aOldAppVersion
3302 * The version of the application last run with this profile or null
3303 * if it is a new profile or the version is unknown
3304 * @param aOldPlatformVersion
3305 * The version of the platform last run with this profile or null
3306 * if it is a new profile or the version is unknown
3307 * @return true if a change requiring a restart was detected
3308 */
3309 checkForChanges: function XPI_checkForChanges(aAppChanged, aOldAppVersion,
3310 aOldPlatformVersion) {
3311 logger.debug("checkForChanges");
3312
3313 // Keep track of whether and why we need to open and update the database at
3314 // startup time.
3315 let updateReasons = [];
3316 if (aAppChanged) {
3317 updateReasons.push("appChanged");
3318 }
3319
3320 // Load the list of bootstrapped add-ons first so processFileChanges can
3321 // modify it
3322 try {
3323 this.bootstrappedAddons = JSON.parse(Prefs.getCharPref(PREF_BOOTSTRAP_ADDONS,
3324 "{}"));
3325 } catch (e) {
3326 logger.warn("Error parsing enabled bootstrapped extensions cache", e);
3327 }
3328
3329 // First install any new add-ons into the locations, if there are any
3330 // changes then we must update the database with the information in the
3331 // install locations
3332 let manifests = {};
3333 let updated = this.processPendingFileChanges(manifests);
3334 if (updated) {
3335 updateReasons.push("pendingFileChanges");
3336 }
3337
3338 // This will be true if the previous session made changes that affect the
3339 // active state of add-ons but didn't commit them properly (normally due
3340 // to the application crashing)
3341 let hasPendingChanges = Prefs.getBoolPref(PREF_PENDING_OPERATIONS);
3342 if (hasPendingChanges) {
3343 updateReasons.push("hasPendingChanges");
3344 }
3345
3346 // If the application has changed then check for new distribution add-ons
3347 if (aAppChanged !== false &&
3348 Prefs.getBoolPref(PREF_INSTALL_DISTRO_ADDONS, true))
3349 {
3350 updated = this.installDistributionAddons(manifests);
3351 if (updated) {
3352 updateReasons.push("installDistributionAddons");
3353 }
3354 }
3355
3356 // Telemetry probe added around getInstallLocationStates() to check perf
3357 let telemetryCaptureTime = Date.now();
3358 this.installStates = this.getInstallLocationStates();
3359 let telemetry = Services.telemetry;
3360 telemetry.getHistogramById("CHECK_ADDONS_MODIFIED_MS").add(Date.now() - telemetryCaptureTime);
3361
3362 // If the install directory state has changed then we must update the database
3363 let cache = Prefs.getCharPref(PREF_INSTALL_CACHE, "[]");
3364 // For a little while, gather telemetry on whether the deep comparison
3365 // makes a difference
3366 let newState = JSON.stringify(this.installStates);
3367 if (cache != newState) {
3368 logger.debug("Directory state JSON differs: cache " + cache + " state " + newState);
3369 if (directoryStateDiffers(this.installStates, cache)) {
3370 updateReasons.push("directoryState");
3371 }
3372 else {
3373 AddonManagerPrivate.recordSimpleMeasure("XPIDB_startup_state_badCompare", 1);
3374 }
3375 }
3376
3377 // If the schema appears to have changed then we should update the database
3378 if (DB_SCHEMA != Prefs.getIntPref(PREF_DB_SCHEMA, 0)) {
3379 // If we don't have any add-ons, just update the pref, since we don't need to
3380 // write the database
3381 if (this.installStates.length == 0) {
3382 logger.debug("Empty XPI database, setting schema version preference to " + DB_SCHEMA);
3383 Services.prefs.setIntPref(PREF_DB_SCHEMA, DB_SCHEMA);
3384 }
3385 else {
3386 updateReasons.push("schemaChanged");
3387 }
3388 }
3389
3390 // If the database doesn't exist and there are add-ons installed then we
3391 // must update the database however if there are no add-ons then there is
3392 // no need to update the database.
3393 let dbFile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_DATABASE], true);
3394 if (!dbFile.exists() && this.installStates.length > 0) {
3395 updateReasons.push("needNewDatabase");
3396 }
3397
3398 if (updateReasons.length == 0) {
3399 let bootstrapDescriptors = [this.bootstrappedAddons[b].descriptor
3400 for (b in this.bootstrappedAddons)];
3401
3402 this.installStates.forEach(function(aInstallLocationState) {
3403 for (let id in aInstallLocationState.addons) {
3404 let pos = bootstrapDescriptors.indexOf(aInstallLocationState.addons[id].descriptor);
3405 if (pos != -1)
3406 bootstrapDescriptors.splice(pos, 1);
3407 }
3408 });
3409
3410 if (bootstrapDescriptors.length > 0) {
3411 logger.warn("Bootstrap state is invalid (missing add-ons: " + bootstrapDescriptors.toSource() + ")");
3412 updateReasons.push("missingBootstrapAddon");
3413 }
3414 }
3415
3416 // Catch and log any errors during the main startup
3417 try {
3418 let extensionListChanged = false;
3419 // If the database needs to be updated then open it and then update it
3420 // from the filesystem
3421 if (updateReasons.length > 0) {
3422 AddonManagerPrivate.recordSimpleMeasure("XPIDB_startup_load_reasons", updateReasons);
3423 XPIDatabase.syncLoadDB(false);
3424 try {
3425 extensionListChanged = this.processFileChanges(this.installStates, manifests,
3426 aAppChanged,
3427 aOldAppVersion,
3428 aOldPlatformVersion);
3429 }
3430 catch (e) {
3431 logger.error("Failed to process extension changes at startup", e);
3432 }
3433 }
3434
3435 if (aAppChanged) {
3436 // When upgrading the app and using a custom skin make sure it is still
3437 // compatible otherwise switch back the default
3438 if (this.currentSkin != this.defaultSkin) {
3439 let oldSkin = XPIDatabase.getVisibleAddonForInternalName(this.currentSkin);
3440 if (!oldSkin || isAddonDisabled(oldSkin))
3441 this.enableDefaultTheme();
3442 }
3443
3444 // When upgrading remove the old extensions cache to force older
3445 // versions to rescan the entire list of extensions
3446 try {
3447 let oldCache = FileUtils.getFile(KEY_PROFILEDIR, [FILE_OLD_CACHE], true);
3448 if (oldCache.exists())
3449 oldCache.remove(true);
3450 }
3451 catch (e) {
3452 logger.warn("Unable to remove old extension cache " + oldCache.path, e);
3453 }
3454 }
3455
3456 // If the application crashed before completing any pending operations then
3457 // we should perform them now.
3458 if (extensionListChanged || hasPendingChanges) {
3459 logger.debug("Updating database with changes to installed add-ons");
3460 XPIDatabase.updateActiveAddons();
3461 Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS,
3462 !XPIDatabase.writeAddonsList());
3463 Services.prefs.setCharPref(PREF_BOOTSTRAP_ADDONS,
3464 JSON.stringify(this.bootstrappedAddons));
3465 return true;
3466 }
3467
3468 logger.debug("No changes found");
3469 }
3470 catch (e) {
3471 logger.error("Error during startup file checks", e);
3472 }
3473
3474 // Check that the add-ons list still exists
3475 let addonsList = FileUtils.getFile(KEY_PROFILEDIR, [FILE_XPI_ADDONS_LIST],
3476 true);
3477 if (addonsList.exists() == (this.installStates.length == 0)) {
3478 logger.debug("Add-ons list is invalid, rebuilding");
3479 XPIDatabase.writeAddonsList();
3480 }
3481
3482 return false;
3483 },
3484
3485 /**
3486 * Called to test whether this provider supports installing a particular
3487 * mimetype.
3488 *
3489 * @param aMimetype
3490 * The mimetype to check for
3491 * @return true if the mimetype is application/x-xpinstall
3492 */
3493 supportsMimetype: function XPI_supportsMimetype(aMimetype) {
3494 return aMimetype == "application/x-xpinstall";
3495 },
3496
3497 /**
3498 * Called to test whether installing XPI add-ons is enabled.
3499 *
3500 * @return true if installing is enabled
3501 */
3502 isInstallEnabled: function XPI_isInstallEnabled() {
3503 // Default to enabled if the preference does not exist
3504 return Prefs.getBoolPref(PREF_XPI_ENABLED, true);
3505 },
3506
3507 /**
3508 * Called to test whether installing XPI add-ons by direct URL requests is
3509 * whitelisted.
3510 *
3511 * @return true if installing by direct requests is whitelisted
3512 */
3513 isDirectRequestWhitelisted: function XPI_isDirectRequestWhitelisted() {
3514 // Default to whitelisted if the preference does not exist.
3515 return Prefs.getBoolPref(PREF_XPI_DIRECT_WHITELISTED, true);
3516 },
3517
3518 /**
3519 * Called to test whether installing XPI add-ons from file referrers is
3520 * whitelisted.
3521 *
3522 * @return true if installing from file referrers is whitelisted
3523 */
3524 isFileRequestWhitelisted: function XPI_isFileRequestWhitelisted() {
3525 // Default to whitelisted if the preference does not exist.
3526 return Prefs.getBoolPref(PREF_XPI_FILE_WHITELISTED, true);
3527 },
3528
3529 /**
3530 * Called to test whether installing XPI add-ons from a URI is allowed.
3531 *
3532 * @param aUri
3533 * The URI being installed from
3534 * @return true if installing is allowed
3535 */
3536 isInstallAllowed: function XPI_isInstallAllowed(aUri) {
3537 if (!this.isInstallEnabled())
3538 return false;
3539
3540 // Direct requests without a referrer are either whitelisted or blocked.
3541 if (!aUri)
3542 return this.isDirectRequestWhitelisted();
3543
3544 // Local referrers can be whitelisted.
3545 if (this.isFileRequestWhitelisted() &&
3546 (aUri.schemeIs("chrome") || aUri.schemeIs("file")))
3547 return true;
3548
3549 this.importPermissions();
3550
3551 let permission = Services.perms.testPermission(aUri, XPI_PERMISSION);
3552 if (permission == Ci.nsIPermissionManager.DENY_ACTION)
3553 return false;
3554
3555 let requireWhitelist = Prefs.getBoolPref(PREF_XPI_WHITELIST_REQUIRED, true);
3556 if (requireWhitelist && (permission != Ci.nsIPermissionManager.ALLOW_ACTION))
3557 return false;
3558
3559 return true;
3560 },
3561
3562 /**
3563 * Called to get an AddonInstall to download and install an add-on from a URL.
3564 *
3565 * @param aUrl
3566 * The URL to be installed
3567 * @param aHash
3568 * A hash for the install
3569 * @param aName
3570 * A name for the install
3571 * @param aIcons
3572 * Icon URLs for the install
3573 * @param aVersion
3574 * A version for the install
3575 * @param aLoadGroup
3576 * An nsILoadGroup to associate requests with
3577 * @param aCallback
3578 * A callback to pass the AddonInstall to
3579 */
3580 getInstallForURL: function XPI_getInstallForURL(aUrl, aHash, aName, aIcons,
3581 aVersion, aLoadGroup, aCallback) {
3582 AddonInstall.createDownload(function getInstallForURL_createDownload(aInstall) {
3583 aCallback(aInstall.wrapper);
3584 }, aUrl, aHash, aName, aIcons, aVersion, aLoadGroup);
3585 },
3586
3587 /**
3588 * Called to get an AddonInstall to install an add-on from a local file.
3589 *
3590 * @param aFile
3591 * The file to be installed
3592 * @param aCallback
3593 * A callback to pass the AddonInstall to
3594 */
3595 getInstallForFile: function XPI_getInstallForFile(aFile, aCallback) {
3596 AddonInstall.createInstall(function getInstallForFile_createInstall(aInstall) {
3597 if (aInstall)
3598 aCallback(aInstall.wrapper);
3599 else
3600 aCallback(null);
3601 }, aFile);
3602 },
3603
3604 /**
3605 * Removes an AddonInstall from the list of active installs.
3606 *
3607 * @param install
3608 * The AddonInstall to remove
3609 */
3610 removeActiveInstall: function XPI_removeActiveInstall(aInstall) {
3611 this.installs = this.installs.filter(function installFilter(i) i != aInstall);
3612 },
3613
3614 /**
3615 * Called to get an Addon with a particular ID.
3616 *
3617 * @param aId
3618 * The ID of the add-on to retrieve
3619 * @param aCallback
3620 * A callback to pass the Addon to
3621 */
3622 getAddonByID: function XPI_getAddonByID(aId, aCallback) {
3623 XPIDatabase.getVisibleAddonForID (aId, function getAddonByID_getVisibleAddonForID(aAddon) {
3624 aCallback(createWrapper(aAddon));
3625 });
3626 },
3627
3628 /**
3629 * Called to get Addons of a particular type.
3630 *
3631 * @param aTypes
3632 * An array of types to fetch. Can be null to get all types.
3633 * @param aCallback
3634 * A callback to pass an array of Addons to
3635 */
3636 getAddonsByTypes: function XPI_getAddonsByTypes(aTypes, aCallback) {
3637 XPIDatabase.getVisibleAddons(aTypes, function getAddonsByTypes_getVisibleAddons(aAddons) {
3638 aCallback([createWrapper(a) for each (a in aAddons)]);
3639 });
3640 },
3641
3642 /**
3643 * Obtain an Addon having the specified Sync GUID.
3644 *
3645 * @param aGUID
3646 * String GUID of add-on to retrieve
3647 * @param aCallback
3648 * A callback to pass the Addon to. Receives null if not found.
3649 */
3650 getAddonBySyncGUID: function XPI_getAddonBySyncGUID(aGUID, aCallback) {
3651 XPIDatabase.getAddonBySyncGUID(aGUID, function getAddonBySyncGUID_getAddonBySyncGUID(aAddon) {
3652 aCallback(createWrapper(aAddon));
3653 });
3654 },
3655
3656 /**
3657 * Called to get Addons that have pending operations.
3658 *
3659 * @param aTypes
3660 * An array of types to fetch. Can be null to get all types
3661 * @param aCallback
3662 * A callback to pass an array of Addons to
3663 */
3664 getAddonsWithOperationsByTypes:
3665 function XPI_getAddonsWithOperationsByTypes(aTypes, aCallback) {
3666 XPIDatabase.getVisibleAddonsWithPendingOperations(aTypes,
3667 function getAddonsWithOpsByTypes_getVisibleAddonsWithPendingOps(aAddons) {
3668 let results = [createWrapper(a) for each (a in aAddons)];
3669 XPIProvider.installs.forEach(function(aInstall) {
3670 if (aInstall.state == AddonManager.STATE_INSTALLED &&
3671 !(aInstall.addon.inDatabase))
3672 results.push(createWrapper(aInstall.addon));
3673 });
3674 aCallback(results);
3675 });
3676 },
3677
3678 /**
3679 * Called to get the current AddonInstalls, optionally limiting to a list of
3680 * types.
3681 *
3682 * @param aTypes
3683 * An array of types or null to get all types
3684 * @param aCallback
3685 * A callback to pass the array of AddonInstalls to
3686 */
3687 getInstallsByTypes: function XPI_getInstallsByTypes(aTypes, aCallback) {
3688 let results = [];
3689 this.installs.forEach(function(aInstall) {
3690 if (!aTypes || aTypes.indexOf(aInstall.type) >= 0)
3691 results.push(aInstall.wrapper);
3692 });
3693 aCallback(results);
3694 },
3695
3696 /**
3697 * Synchronously map a URI to the corresponding Addon ID.
3698 *
3699 * Mappable URIs are limited to in-application resources belonging to the
3700 * add-on, such as Javascript compartments, XUL windows, XBL bindings, etc.
3701 * but do not include URIs from meta data, such as the add-on homepage.
3702 *
3703 * @param aURI
3704 * nsIURI to map or null
3705 * @return string containing the Addon ID
3706 * @see AddonManager.mapURIToAddonID
3707 * @see amIAddonManager.mapURIToAddonID
3708 */
3709 mapURIToAddonID: function XPI_mapURIToAddonID(aURI) {
3710 this._ensureURIMappings();
3711 let resolved = this._resolveURIToFile(aURI);
3712 if (!resolved) {
3713 return null;
3714 }
3715 resolved = resolved.spec;
3716 for (let [id, spec] in Iterator(this._uriMappings)) {
3717 if (resolved.startsWith(spec)) {
3718 return id;
3719 }
3720 }
3721 return null;
3722 },
3723
3724 /**
3725 * Called when a new add-on has been enabled when only one add-on of that type
3726 * can be enabled.
3727 *
3728 * @param aId
3729 * The ID of the newly enabled add-on
3730 * @param aType
3731 * The type of the newly enabled add-on
3732 * @param aPendingRestart
3733 * true if the newly enabled add-on will only become enabled after a
3734 * restart
3735 */
3736 addonChanged: function XPI_addonChanged(aId, aType, aPendingRestart) {
3737 // We only care about themes in this provider
3738 if (aType != "theme")
3739 return;
3740
3741 if (!aId) {
3742 // Fallback to the default theme when no theme was enabled
3743 this.enableDefaultTheme();
3744 return;
3745 }
3746
3747 // Look for the previously enabled theme and find the internalName of the
3748 // currently selected theme
3749 let previousTheme = null;
3750 let newSkin = this.defaultSkin;
3751 let addons = XPIDatabase.getAddonsByType("theme");
3752 addons.forEach(function(aTheme) {
3753 if (!aTheme.visible)
3754 return;
3755 if (aTheme.id == aId)
3756 newSkin = aTheme.internalName;
3757 else if (aTheme.userDisabled == false && !aTheme.pendingUninstall)
3758 previousTheme = aTheme;
3759 }, this);
3760
3761 if (aPendingRestart) {
3762 Services.prefs.setBoolPref(PREF_DSS_SWITCHPENDING, true);
3763 Services.prefs.setCharPref(PREF_DSS_SKIN_TO_SELECT, newSkin);
3764 }
3765 else if (newSkin == this.currentSkin) {
3766 try {
3767 Services.prefs.clearUserPref(PREF_DSS_SWITCHPENDING);
3768 }
3769 catch (e) { }
3770 try {
3771 Services.prefs.clearUserPref(PREF_DSS_SKIN_TO_SELECT);
3772 }
3773 catch (e) { }
3774 }
3775 else {
3776 Services.prefs.setCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN, newSkin);
3777 this.currentSkin = newSkin;
3778 }
3779 this.selectedSkin = newSkin;
3780
3781 // Flush the preferences to disk so they don't get out of sync with the
3782 // database
3783 Services.prefs.savePrefFile(null);
3784
3785 // Mark the previous theme as disabled. This won't cause recursion since
3786 // only enabled calls notifyAddonChanged.
3787 if (previousTheme)
3788 this.updateAddonDisabledState(previousTheme, true);
3789 },
3790
3791 /**
3792 * Update the appDisabled property for all add-ons.
3793 */
3794 updateAddonAppDisabledStates: function XPI_updateAddonAppDisabledStates() {
3795 let addons = XPIDatabase.getAddons();
3796 addons.forEach(function(aAddon) {
3797 this.updateAddonDisabledState(aAddon);
3798 }, this);
3799 },
3800
3801 /**
3802 * Update the repositoryAddon property for all add-ons.
3803 *
3804 * @param aCallback
3805 * Function to call when operation is complete.
3806 */
3807 updateAddonRepositoryData: function XPI_updateAddonRepositoryData(aCallback) {
3808 let self = this;
3809 XPIDatabase.getVisibleAddons(null, function UARD_getVisibleAddonsCallback(aAddons) {
3810 let pending = aAddons.length;
3811 logger.debug("updateAddonRepositoryData found " + pending + " visible add-ons");
3812 if (pending == 0) {
3813 aCallback();
3814 return;
3815 }
3816
3817 function notifyComplete() {
3818 if (--pending == 0)
3819 aCallback();
3820 }
3821
3822 for (let addon of aAddons) {
3823 AddonRepository.getCachedAddonByID(addon.id,
3824 function UARD_getCachedAddonCallback(aRepoAddon) {
3825 if (aRepoAddon) {
3826 logger.debug("updateAddonRepositoryData got info for " + addon.id);
3827 addon._repositoryAddon = aRepoAddon;
3828 addon.compatibilityOverrides = aRepoAddon.compatibilityOverrides;
3829 self.updateAddonDisabledState(addon);
3830 }
3831
3832 notifyComplete();
3833 });
3834 };
3835 });
3836 },
3837
3838 /**
3839 * When the previously selected theme is removed this method will be called
3840 * to enable the default theme.
3841 */
3842 enableDefaultTheme: function XPI_enableDefaultTheme() {
3843 logger.debug("Activating default theme");
3844 let addon = XPIDatabase.getVisibleAddonForInternalName(this.defaultSkin);
3845 if (addon) {
3846 if (addon.userDisabled) {
3847 this.updateAddonDisabledState(addon, false);
3848 }
3849 else if (!this.extensionsActive) {
3850 // During startup we may end up trying to enable the default theme when
3851 // the database thinks it is already enabled (see f.e. bug 638847). In
3852 // this case just force the theme preferences to be correct
3853 Services.prefs.setCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN,
3854 addon.internalName);
3855 this.currentSkin = this.selectedSkin = addon.internalName;
3856 Prefs.clearUserPref(PREF_DSS_SKIN_TO_SELECT);
3857 Prefs.clearUserPref(PREF_DSS_SWITCHPENDING);
3858 }
3859 else {
3860 logger.warn("Attempting to activate an already active default theme");
3861 }
3862 }
3863 else {
3864 logger.warn("Unable to activate the default theme");
3865 }
3866 },
3867
3868 onDebugConnectionChange: function(aEvent, aWhat, aConnection) {
3869 if (aWhat != "opened")
3870 return;
3871
3872 for (let id of Object.keys(this.bootstrapScopes)) {
3873 aConnection.setAddonOptions(id, { global: this.bootstrapScopes[id] });
3874 }
3875 },
3876
3877 /**
3878 * Notified when a preference we're interested in has changed.
3879 *
3880 * @see nsIObserver
3881 */
3882 observe: function XPI_observe(aSubject, aTopic, aData) {
3883 if (aTopic == NOTIFICATION_FLUSH_PERMISSIONS) {
3884 if (!aData || aData == XPI_PERMISSION) {
3885 this.importPermissions();
3886 }
3887 return;
3888 }
3889
3890 if (aTopic == "nsPref:changed") {
3891 switch (aData) {
3892 case PREF_EM_MIN_COMPAT_APP_VERSION:
3893 case PREF_EM_MIN_COMPAT_PLATFORM_VERSION:
3894 this.minCompatibleAppVersion = Prefs.getCharPref(PREF_EM_MIN_COMPAT_APP_VERSION,
3895 null);
3896 this.minCompatiblePlatformVersion = Prefs.getCharPref(PREF_EM_MIN_COMPAT_PLATFORM_VERSION,
3897 null);
3898 this.updateAddonAppDisabledStates();
3899 break;
3900 }
3901 }
3902 },
3903
3904 /**
3905 * Tests whether enabling an add-on will require a restart.
3906 *
3907 * @param aAddon
3908 * The add-on to test
3909 * @return true if the operation requires a restart
3910 */
3911 enableRequiresRestart: function XPI_enableRequiresRestart(aAddon) {
3912 // If the platform couldn't have activated extensions then we can make
3913 // changes without any restart.
3914 if (!this.extensionsActive)
3915 return false;
3916
3917 // If the application is in safe mode then any change can be made without
3918 // restarting
3919 if (Services.appinfo.inSafeMode)
3920 return false;
3921
3922 // Anything that is active is already enabled
3923 if (aAddon.active)
3924 return false;
3925
3926 if (aAddon.type == "theme") {
3927 // If dynamic theme switching is enabled then switching themes does not
3928 // require a restart
3929 if (Prefs.getBoolPref(PREF_EM_DSS_ENABLED))
3930 return false;
3931
3932 // If the theme is already the theme in use then no restart is necessary.
3933 // This covers the case where the default theme is in use but a
3934 // lightweight theme is considered active.
3935 return aAddon.internalName != this.currentSkin;
3936 }
3937
3938 return !aAddon.bootstrap;
3939 },
3940
3941 /**
3942 * Tests whether disabling an add-on will require a restart.
3943 *
3944 * @param aAddon
3945 * The add-on to test
3946 * @return true if the operation requires a restart
3947 */
3948 disableRequiresRestart: function XPI_disableRequiresRestart(aAddon) {
3949 // If the platform couldn't have activated up extensions then we can make
3950 // changes without any restart.
3951 if (!this.extensionsActive)
3952 return false;
3953
3954 // If the application is in safe mode then any change can be made without
3955 // restarting
3956 if (Services.appinfo.inSafeMode)
3957 return false;
3958
3959 // Anything that isn't active is already disabled
3960 if (!aAddon.active)
3961 return false;
3962
3963 if (aAddon.type == "theme") {
3964 // If dynamic theme switching is enabled then switching themes does not
3965 // require a restart
3966 if (Prefs.getBoolPref(PREF_EM_DSS_ENABLED))
3967 return false;
3968
3969 // Non-default themes always require a restart to disable since it will
3970 // be switching from one theme to another or to the default theme and a
3971 // lightweight theme.
3972 if (aAddon.internalName != this.defaultSkin)
3973 return true;
3974
3975 // The default theme requires a restart to disable if we are in the
3976 // process of switching to a different theme. Note that this makes the
3977 // disabled flag of operationsRequiringRestart incorrect for the default
3978 // theme (it will be false most of the time). Bug 520124 would be required
3979 // to fix it. For the UI this isn't a problem since we never try to
3980 // disable or uninstall the default theme.
3981 return this.selectedSkin != this.currentSkin;
3982 }
3983
3984 return !aAddon.bootstrap;
3985 },
3986
3987 /**
3988 * Tests whether installing an add-on will require a restart.
3989 *
3990 * @param aAddon
3991 * The add-on to test
3992 * @return true if the operation requires a restart
3993 */
3994 installRequiresRestart: function XPI_installRequiresRestart(aAddon) {
3995 // If the platform couldn't have activated up extensions then we can make
3996 // changes without any restart.
3997 if (!this.extensionsActive)
3998 return false;
3999
4000 // If the application is in safe mode then any change can be made without
4001 // restarting
4002 if (Services.appinfo.inSafeMode)
4003 return false;
4004
4005 // Add-ons that are already installed don't require a restart to install.
4006 // This wouldn't normally be called for an already installed add-on (except
4007 // for forming the operationsRequiringRestart flags) so is really here as
4008 // a safety measure.
4009 if (aAddon.inDatabase)
4010 return false;
4011
4012 // If we have an AddonInstall for this add-on then we can see if there is
4013 // an existing installed add-on with the same ID
4014 if ("_install" in aAddon && aAddon._install) {
4015 // If there is an existing installed add-on and uninstalling it would
4016 // require a restart then installing the update will also require a
4017 // restart
4018 let existingAddon = aAddon._install.existingAddon;
4019 if (existingAddon && this.uninstallRequiresRestart(existingAddon))
4020 return true;
4021 }
4022
4023 // If the add-on is not going to be active after installation then it
4024 // doesn't require a restart to install.
4025 if (isAddonDisabled(aAddon))
4026 return false;
4027
4028 // Themes will require a restart (even if dynamic switching is enabled due
4029 // to some caching issues) and non-bootstrapped add-ons will require a
4030 // restart
4031 return aAddon.type == "theme" || !aAddon.bootstrap;
4032 },
4033
4034 /**
4035 * Tests whether uninstalling an add-on will require a restart.
4036 *
4037 * @param aAddon
4038 * The add-on to test
4039 * @return true if the operation requires a restart
4040 */
4041 uninstallRequiresRestart: function XPI_uninstallRequiresRestart(aAddon) {
4042 // If the platform couldn't have activated up extensions then we can make
4043 // changes without any restart.
4044 if (!this.extensionsActive)
4045 return false;
4046
4047 // If the application is in safe mode then any change can be made without
4048 // restarting
4049 if (Services.appinfo.inSafeMode)
4050 return false;
4051
4052 // If the add-on can be disabled without a restart then it can also be
4053 // uninstalled without a restart
4054 return this.disableRequiresRestart(aAddon);
4055 },
4056
4057 /**
4058 * Loads a bootstrapped add-on's bootstrap.js into a sandbox and the reason
4059 * values as constants in the scope. This will also add information about the
4060 * add-on to the bootstrappedAddons dictionary and notify the crash reporter
4061 * that new add-ons have been loaded.
4062 *
4063 * @param aId
4064 * The add-on's ID
4065 * @param aFile
4066 * The nsIFile for the add-on
4067 * @param aVersion
4068 * The add-on's version
4069 * @param aType
4070 * The type for the add-on
4071 * @return a JavaScript scope
4072 */
4073 loadBootstrapScope: function XPI_loadBootstrapScope(aId, aFile, aVersion, aType) {
4074 // Mark the add-on as active for the crash reporter before loading
4075 this.bootstrappedAddons[aId] = {
4076 version: aVersion,
4077 type: aType,
4078 descriptor: aFile.persistentDescriptor
4079 };
4080 this.persistBootstrappedAddons();
4081 this.addAddonsToCrashReporter();
4082
4083 // Locales only contain chrome and can't have bootstrap scripts
4084 if (aType == "locale") {
4085 this.bootstrapScopes[aId] = null;
4086 return;
4087 }
4088
4089 logger.debug("Loading bootstrap scope from " + aFile.path);
4090
4091 let principal = Cc["@mozilla.org/systemprincipal;1"].
4092 createInstance(Ci.nsIPrincipal);
4093
4094 if (!aFile.exists()) {
4095 this.bootstrapScopes[aId] =
4096 new Cu.Sandbox(principal, { sandboxName: aFile.path,
4097 wantGlobalProperties: ["indexedDB"],
4098 metadata: { addonID: aId } });
4099 logger.error("Attempted to load bootstrap scope from missing directory " + aFile.path);
4100 return;
4101 }
4102
4103 let uri = getURIForResourceInFile(aFile, "bootstrap.js").spec;
4104 if (aType == "dictionary")
4105 uri = "resource://gre/modules/addons/SpellCheckDictionaryBootstrap.js"
4106
4107 this.bootstrapScopes[aId] =
4108 new Cu.Sandbox(principal, { sandboxName: uri,
4109 wantGlobalProperties: ["indexedDB"],
4110 metadata: { addonID: aId, URI: uri } });
4111
4112 let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"].
4113 createInstance(Ci.mozIJSSubScriptLoader);
4114
4115 // Add a mapping for XPIProvider.mapURIToAddonID
4116 this._addURIMapping(aId, aFile);
4117
4118 try {
4119 // Copy the reason values from the global object into the bootstrap scope.
4120 for (let name in BOOTSTRAP_REASONS)
4121 this.bootstrapScopes[aId][name] = BOOTSTRAP_REASONS[name];
4122
4123 // Add other stuff that extensions want.
4124 const features = [ "Worker", "ChromeWorker" ];
4125
4126 for (let feature of features)
4127 this.bootstrapScopes[aId][feature] = gGlobalScope[feature];
4128
4129 // As we don't want our caller to control the JS version used for the
4130 // bootstrap file, we run loadSubScript within the context of the
4131 // sandbox with the latest JS version set explicitly.
4132 this.bootstrapScopes[aId].__SCRIPT_URI_SPEC__ = uri;
4133 Components.utils.evalInSandbox(
4134 "Components.classes['@mozilla.org/moz/jssubscript-loader;1'] \
4135 .createInstance(Components.interfaces.mozIJSSubScriptLoader) \
4136 .loadSubScript(__SCRIPT_URI_SPEC__);", this.bootstrapScopes[aId], "ECMAv5");
4137 }
4138 catch (e) {
4139 logger.warn("Error loading bootstrap.js for " + aId, e);
4140 }
4141
4142 try {
4143 BrowserToolboxProcess.setAddonOptions(aId, { global: this.bootstrapScopes[aId] });
4144 }
4145 catch (e) {
4146 // BrowserToolboxProcess is not available in all applications
4147 }
4148 },
4149
4150 /**
4151 * Unloads a bootstrap scope by dropping all references to it and then
4152 * updating the list of active add-ons with the crash reporter.
4153 *
4154 * @param aId
4155 * The add-on's ID
4156 */
4157 unloadBootstrapScope: function XPI_unloadBootstrapScope(aId) {
4158 delete this.bootstrapScopes[aId];
4159 delete this.bootstrappedAddons[aId];
4160 this.persistBootstrappedAddons();
4161 this.addAddonsToCrashReporter();
4162
4163 try {
4164 BrowserToolboxProcess.setAddonOptions(aId, { global: null });
4165 }
4166 catch (e) {
4167 // BrowserToolboxProcess is not available in all applications
4168 }
4169 },
4170
4171 /**
4172 * Calls a bootstrap method for an add-on.
4173 *
4174 * @param aId
4175 * The ID of the add-on
4176 * @param aVersion
4177 * The version of the add-on
4178 * @param aType
4179 * The type for the add-on
4180 * @param aFile
4181 * The nsIFile for the add-on
4182 * @param aMethod
4183 * The name of the bootstrap method to call
4184 * @param aReason
4185 * The reason flag to pass to the bootstrap's startup method
4186 * @param aExtraParams
4187 * An object of additional key/value pairs to pass to the method in
4188 * the params argument
4189 */
4190 callBootstrapMethod: function XPI_callBootstrapMethod(aId, aVersion, aType, aFile,
4191 aMethod, aReason, aExtraParams) {
4192 // Never call any bootstrap methods in safe mode
4193 if (Services.appinfo.inSafeMode)
4194 return;
4195
4196 let timeStart = new Date();
4197 if (aMethod == "startup") {
4198 logger.debug("Registering manifest for " + aFile.path);
4199 Components.manager.addBootstrappedManifestLocation(aFile);
4200 }
4201
4202 try {
4203 // Load the scope if it hasn't already been loaded
4204 if (!(aId in this.bootstrapScopes))
4205 this.loadBootstrapScope(aId, aFile, aVersion, aType);
4206
4207 // Nothing to call for locales
4208 if (aType == "locale")
4209 return;
4210
4211 if (!(aMethod in this.bootstrapScopes[aId])) {
4212 logger.warn("Add-on " + aId + " is missing bootstrap method " + aMethod);
4213 return;
4214 }
4215
4216 let params = {
4217 id: aId,
4218 version: aVersion,
4219 installPath: aFile.clone(),
4220 resourceURI: getURIForResourceInFile(aFile, "")
4221 };
4222
4223 if (aExtraParams) {
4224 for (let key in aExtraParams) {
4225 params[key] = aExtraParams[key];
4226 }
4227 }
4228
4229 logger.debug("Calling bootstrap method " + aMethod + " on " + aId + " version " +
4230 aVersion);
4231 try {
4232 this.bootstrapScopes[aId][aMethod](params, aReason);
4233 }
4234 catch (e) {
4235 logger.warn("Exception running bootstrap method " + aMethod + " on " + aId, e);
4236 }
4237 }
4238 finally {
4239 if (aMethod == "shutdown" && aReason != BOOTSTRAP_REASONS.APP_SHUTDOWN) {
4240 logger.debug("Removing manifest for " + aFile.path);
4241 Components.manager.removeBootstrappedManifestLocation(aFile);
4242 }
4243 this.setTelemetry(aId, aMethod + "_MS", new Date() - timeStart);
4244 }
4245 },
4246
4247 /**
4248 * Updates the disabled state for an add-on. Its appDisabled property will be
4249 * calculated and if the add-on is changed the database will be saved and
4250 * appropriate notifications will be sent out to the registered AddonListeners.
4251 *
4252 * @param aAddon
4253 * The DBAddonInternal to update
4254 * @param aUserDisabled
4255 * Value for the userDisabled property. If undefined the value will
4256 * not change
4257 * @param aSoftDisabled
4258 * Value for the softDisabled property. If undefined the value will
4259 * not change. If true this will force userDisabled to be true
4260 * @throws if addon is not a DBAddonInternal
4261 */
4262 updateAddonDisabledState: function XPI_updateAddonDisabledState(aAddon,
4263 aUserDisabled,
4264 aSoftDisabled) {
4265 if (!(aAddon.inDatabase))
4266 throw new Error("Can only update addon states for installed addons.");
4267 if (aUserDisabled !== undefined && aSoftDisabled !== undefined) {
4268 throw new Error("Cannot change userDisabled and softDisabled at the " +
4269 "same time");
4270 }
4271
4272 if (aUserDisabled === undefined) {
4273 aUserDisabled = aAddon.userDisabled;
4274 }
4275 else if (!aUserDisabled) {
4276 // If enabling the add-on then remove softDisabled
4277 aSoftDisabled = false;
4278 }
4279
4280 // If not changing softDisabled or the add-on is already userDisabled then
4281 // use the existing value for softDisabled
4282 if (aSoftDisabled === undefined || aUserDisabled)
4283 aSoftDisabled = aAddon.softDisabled;
4284
4285 let appDisabled = !isUsableAddon(aAddon);
4286 // No change means nothing to do here
4287 if (aAddon.userDisabled == aUserDisabled &&
4288 aAddon.appDisabled == appDisabled &&
4289 aAddon.softDisabled == aSoftDisabled)
4290 return;
4291
4292 let wasDisabled = isAddonDisabled(aAddon);
4293 let isDisabled = aUserDisabled || aSoftDisabled || appDisabled;
4294
4295 // If appDisabled changes but the result of isAddonDisabled() doesn't,
4296 // no onDisabling/onEnabling is sent - so send a onPropertyChanged.
4297 let appDisabledChanged = aAddon.appDisabled != appDisabled;
4298
4299 // Update the properties in the database.
4300 // We never persist this for experiments because the disabled flags
4301 // are controlled by the Experiments Manager.
4302 if (aAddon.type != "experiment") {
4303 XPIDatabase.setAddonProperties(aAddon, {
4304 userDisabled: aUserDisabled,
4305 appDisabled: appDisabled,
4306 softDisabled: aSoftDisabled
4307 });
4308 }
4309
4310 if (appDisabledChanged) {
4311 AddonManagerPrivate.callAddonListeners("onPropertyChanged",
4312 aAddon,
4313 ["appDisabled"]);
4314 }
4315
4316 // If the add-on is not visible or the add-on is not changing state then
4317 // there is no need to do anything else
4318 if (!aAddon.visible || (wasDisabled == isDisabled))
4319 return;
4320
4321 // Flag that active states in the database need to be updated on shutdown
4322 Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true);
4323
4324 let wrapper = createWrapper(aAddon);
4325 // Have we just gone back to the current state?
4326 if (isDisabled != aAddon.active) {
4327 AddonManagerPrivate.callAddonListeners("onOperationCancelled", wrapper);
4328 }
4329 else {
4330 if (isDisabled) {
4331 var needsRestart = this.disableRequiresRestart(aAddon);
4332 AddonManagerPrivate.callAddonListeners("onDisabling", wrapper,
4333 needsRestart);
4334 }
4335 else {
4336 needsRestart = this.enableRequiresRestart(aAddon);
4337 AddonManagerPrivate.callAddonListeners("onEnabling", wrapper,
4338 needsRestart);
4339 }
4340
4341 if (!needsRestart) {
4342 XPIDatabase.updateAddonActive(aAddon, !isDisabled);
4343 if (isDisabled) {
4344 if (aAddon.bootstrap) {
4345 let file = aAddon._installLocation.getLocationForID(aAddon.id);
4346 this.callBootstrapMethod(aAddon.id, aAddon.version, aAddon.type, file, "shutdown",
4347 BOOTSTRAP_REASONS.ADDON_DISABLE);
4348 this.unloadBootstrapScope(aAddon.id);
4349 }
4350 AddonManagerPrivate.callAddonListeners("onDisabled", wrapper);
4351 }
4352 else {
4353 if (aAddon.bootstrap) {
4354 let file = aAddon._installLocation.getLocationForID(aAddon.id);
4355 this.callBootstrapMethod(aAddon.id, aAddon.version, aAddon.type, file, "startup",
4356 BOOTSTRAP_REASONS.ADDON_ENABLE);
4357 }
4358 AddonManagerPrivate.callAddonListeners("onEnabled", wrapper);
4359 }
4360 }
4361 }
4362
4363 // Notify any other providers that a new theme has been enabled
4364 if (aAddon.type == "theme" && !isDisabled)
4365 AddonManagerPrivate.notifyAddonChanged(aAddon.id, aAddon.type, needsRestart);
4366 },
4367
4368 /**
4369 * Uninstalls an add-on, immediately if possible or marks it as pending
4370 * uninstall if not.
4371 *
4372 * @param aAddon
4373 * The DBAddonInternal to uninstall
4374 * @throws if the addon cannot be uninstalled because it is in an install
4375 * location that does not allow it
4376 */
4377 uninstallAddon: function XPI_uninstallAddon(aAddon) {
4378 if (!(aAddon.inDatabase))
4379 throw new Error("Can only uninstall installed addons.");
4380
4381 if (aAddon._installLocation.locked)
4382 throw new Error("Cannot uninstall addons from locked install locations");
4383
4384 if ("_hasResourceCache" in aAddon)
4385 aAddon._hasResourceCache = new Map();
4386
4387 if (aAddon._updateCheck) {
4388 logger.debug("Cancel in-progress update check for " + aAddon.id);
4389 aAddon._updateCheck.cancel();
4390 }
4391
4392 // Inactive add-ons don't require a restart to uninstall
4393 let requiresRestart = this.uninstallRequiresRestart(aAddon);
4394
4395 if (requiresRestart) {
4396 // We create an empty directory in the staging directory to indicate that
4397 // an uninstall is necessary on next startup.
4398 let stage = aAddon._installLocation.getStagingDir();
4399 stage.append(aAddon.id);
4400 if (!stage.exists())
4401 stage.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
4402
4403 XPIDatabase.setAddonProperties(aAddon, {
4404 pendingUninstall: true
4405 });
4406 Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true);
4407 }
4408
4409 // If the add-on is not visible then there is no need to notify listeners.
4410 if (!aAddon.visible)
4411 return;
4412
4413 let wrapper = createWrapper(aAddon);
4414 AddonManagerPrivate.callAddonListeners("onUninstalling", wrapper,
4415 requiresRestart);
4416
4417 // Reveal the highest priority add-on with the same ID
4418 function revealAddon(aAddon) {
4419 XPIDatabase.makeAddonVisible(aAddon);
4420
4421 let wrappedAddon = createWrapper(aAddon);
4422 AddonManagerPrivate.callAddonListeners("onInstalling", wrappedAddon, false);
4423
4424 if (!isAddonDisabled(aAddon) && !XPIProvider.enableRequiresRestart(aAddon)) {
4425 XPIDatabase.updateAddonActive(aAddon, true);
4426 }
4427
4428 if (aAddon.bootstrap) {
4429 let file = aAddon._installLocation.getLocationForID(aAddon.id);
4430 XPIProvider.callBootstrapMethod(aAddon.id, aAddon.version, aAddon.type, file,
4431 "install", BOOTSTRAP_REASONS.ADDON_INSTALL);
4432
4433 if (aAddon.active) {
4434 XPIProvider.callBootstrapMethod(aAddon.id, aAddon.version, aAddon.type, file,
4435 "startup", BOOTSTRAP_REASONS.ADDON_INSTALL);
4436 }
4437 else {
4438 XPIProvider.unloadBootstrapScope(aAddon.id);
4439 }
4440 }
4441
4442 // We always send onInstalled even if a restart is required to enable
4443 // the revealed add-on
4444 AddonManagerPrivate.callAddonListeners("onInstalled", wrappedAddon);
4445 }
4446
4447 function checkInstallLocation(aPos) {
4448 if (aPos < 0)
4449 return;
4450
4451 let location = XPIProvider.installLocations[aPos];
4452 XPIDatabase.getAddonInLocation(aAddon.id, location.name,
4453 function checkInstallLocation_getAddonInLocation(aNewAddon) {
4454 if (aNewAddon)
4455 revealAddon(aNewAddon);
4456 else
4457 checkInstallLocation(aPos - 1);
4458 })
4459 }
4460
4461 if (!requiresRestart) {
4462 if (aAddon.bootstrap) {
4463 let file = aAddon._installLocation.getLocationForID(aAddon.id);
4464 if (aAddon.active) {
4465 this.callBootstrapMethod(aAddon.id, aAddon.version, aAddon.type, file,
4466 "shutdown",
4467 BOOTSTRAP_REASONS.ADDON_UNINSTALL);
4468 }
4469
4470 this.callBootstrapMethod(aAddon.id, aAddon.version, aAddon.type, file,
4471 "uninstall",
4472 BOOTSTRAP_REASONS.ADDON_UNINSTALL);
4473 this.unloadBootstrapScope(aAddon.id);
4474 flushStartupCache();
4475 }
4476 aAddon._installLocation.uninstallAddon(aAddon.id);
4477 XPIDatabase.removeAddonMetadata(aAddon);
4478 AddonManagerPrivate.callAddonListeners("onUninstalled", wrapper);
4479
4480 checkInstallLocation(this.installLocations.length - 1);
4481 }
4482
4483 // Notify any other providers that a new theme has been enabled
4484 if (aAddon.type == "theme" && aAddon.active)
4485 AddonManagerPrivate.notifyAddonChanged(null, aAddon.type, requiresRestart);
4486 },
4487
4488 /**
4489 * Cancels the pending uninstall of an add-on.
4490 *
4491 * @param aAddon
4492 * The DBAddonInternal to cancel uninstall for
4493 */
4494 cancelUninstallAddon: function XPI_cancelUninstallAddon(aAddon) {
4495 if (!(aAddon.inDatabase))
4496 throw new Error("Can only cancel uninstall for installed addons.");
4497
4498 aAddon._installLocation.cleanStagingDir([aAddon.id]);
4499
4500 XPIDatabase.setAddonProperties(aAddon, {
4501 pendingUninstall: false
4502 });
4503
4504 if (!aAddon.visible)
4505 return;
4506
4507 Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true);
4508
4509 // TODO hide hidden add-ons (bug 557710)
4510 let wrapper = createWrapper(aAddon);
4511 AddonManagerPrivate.callAddonListeners("onOperationCancelled", wrapper);
4512
4513 // Notify any other providers that this theme is now enabled again.
4514 if (aAddon.type == "theme" && aAddon.active)
4515 AddonManagerPrivate.notifyAddonChanged(aAddon.id, aAddon.type, false);
4516 }
4517 };
4518
4519 function getHashStringForCrypto(aCrypto) {
4520 // return the two-digit hexadecimal code for a byte
4521 function toHexString(charCode)
4522 ("0" + charCode.toString(16)).slice(-2);
4523
4524 // convert the binary hash data to a hex string.
4525 let binary = aCrypto.finish(false);
4526 return [toHexString(binary.charCodeAt(i)) for (i in binary)].join("").toLowerCase()
4527 }
4528
4529 /**
4530 * Instantiates an AddonInstall.
4531 *
4532 * @param aInstallLocation
4533 * The install location the add-on will be installed into
4534 * @param aUrl
4535 * The nsIURL to get the add-on from. If this is an nsIFileURL then
4536 * the add-on will not need to be downloaded
4537 * @param aHash
4538 * An optional hash for the add-on
4539 * @param aReleaseNotesURI
4540 * An optional nsIURI of release notes for the add-on
4541 * @param aExistingAddon
4542 * The add-on this install will update if known
4543 * @param aLoadGroup
4544 * The nsILoadGroup to associate any requests with
4545 * @throws if the url is the url of a local file and the hash does not match
4546 * or the add-on does not contain an valid install manifest
4547 */
4548 function AddonInstall(aInstallLocation, aUrl, aHash, aReleaseNotesURI,
4549 aExistingAddon, aLoadGroup) {
4550 this.wrapper = new AddonInstallWrapper(this);
4551 this.installLocation = aInstallLocation;
4552 this.sourceURI = aUrl;
4553 this.releaseNotesURI = aReleaseNotesURI;
4554 if (aHash) {
4555 let hashSplit = aHash.toLowerCase().split(":");
4556 this.originalHash = {
4557 algorithm: hashSplit[0],
4558 data: hashSplit[1]
4559 };
4560 }
4561 this.hash = this.originalHash;
4562 this.loadGroup = aLoadGroup;
4563 this.listeners = [];
4564 this.icons = {};
4565 this.existingAddon = aExistingAddon;
4566 this.error = 0;
4567 if (aLoadGroup)
4568 this.window = aLoadGroup.notificationCallbacks
4569 .getInterface(Ci.nsIDOMWindow);
4570 else
4571 this.window = null;
4572
4573 // Giving each instance of AddonInstall a reference to the logger.
4574 this.logger = logger;
4575 }
4576
4577 AddonInstall.prototype = {
4578 installLocation: null,
4579 wrapper: null,
4580 stream: null,
4581 crypto: null,
4582 originalHash: null,
4583 hash: null,
4584 loadGroup: null,
4585 badCertHandler: null,
4586 listeners: null,
4587 restartDownload: false,
4588
4589 name: null,
4590 type: null,
4591 version: null,
4592 icons: null,
4593 releaseNotesURI: null,
4594 sourceURI: null,
4595 file: null,
4596 ownsTempFile: false,
4597 certificate: null,
4598 certName: null,
4599
4600 linkedInstalls: null,
4601 existingAddon: null,
4602 addon: null,
4603
4604 state: null,
4605 error: null,
4606 progress: null,
4607 maxProgress: null,
4608
4609 /**
4610 * Initialises this install to be a staged install waiting to be applied
4611 *
4612 * @param aManifest
4613 * The cached manifest for the staged install
4614 */
4615 initStagedInstall: function AI_initStagedInstall(aManifest) {
4616 this.name = aManifest.name;
4617 this.type = aManifest.type;
4618 this.version = aManifest.version;
4619 this.icons = aManifest.icons;
4620 this.releaseNotesURI = aManifest.releaseNotesURI ?
4621 NetUtil.newURI(aManifest.releaseNotesURI) :
4622 null
4623 this.sourceURI = aManifest.sourceURI ?
4624 NetUtil.newURI(aManifest.sourceURI) :
4625 null;
4626 this.file = null;
4627 this.addon = aManifest;
4628
4629 this.state = AddonManager.STATE_INSTALLED;
4630
4631 XPIProvider.installs.push(this);
4632 },
4633
4634 /**
4635 * Initialises this install to be an install from a local file.
4636 *
4637 * @param aCallback
4638 * The callback to pass the initialised AddonInstall to
4639 */
4640 initLocalInstall: function AI_initLocalInstall(aCallback) {
4641 aCallback = makeSafe(aCallback);
4642 this.file = this.sourceURI.QueryInterface(Ci.nsIFileURL).file;
4643
4644 if (!this.file.exists()) {
4645 logger.warn("XPI file " + this.file.path + " does not exist");
4646 this.state = AddonManager.STATE_DOWNLOAD_FAILED;
4647 this.error = AddonManager.ERROR_NETWORK_FAILURE;
4648 aCallback(this);
4649 return;
4650 }
4651
4652 this.state = AddonManager.STATE_DOWNLOADED;
4653 this.progress = this.file.fileSize;
4654 this.maxProgress = this.file.fileSize;
4655
4656 if (this.hash) {
4657 let crypto = Cc["@mozilla.org/security/hash;1"].
4658 createInstance(Ci.nsICryptoHash);
4659 try {
4660 crypto.initWithString(this.hash.algorithm);
4661 }
4662 catch (e) {
4663 logger.warn("Unknown hash algorithm '" + this.hash.algorithm + "' for addon " + this.sourceURI.spec, e);
4664 this.state = AddonManager.STATE_DOWNLOAD_FAILED;
4665 this.error = AddonManager.ERROR_INCORRECT_HASH;
4666 aCallback(this);
4667 return;
4668 }
4669
4670 let fis = Cc["@mozilla.org/network/file-input-stream;1"].
4671 createInstance(Ci.nsIFileInputStream);
4672 fis.init(this.file, -1, -1, false);
4673 crypto.updateFromStream(fis, this.file.fileSize);
4674 let calculatedHash = getHashStringForCrypto(crypto);
4675 if (calculatedHash != this.hash.data) {
4676 logger.warn("File hash (" + calculatedHash + ") did not match provided hash (" +
4677 this.hash.data + ")");
4678 this.state = AddonManager.STATE_DOWNLOAD_FAILED;
4679 this.error = AddonManager.ERROR_INCORRECT_HASH;
4680 aCallback(this);
4681 return;
4682 }
4683 }
4684
4685 try {
4686 let self = this;
4687 this.loadManifest(function initLocalInstall_loadManifest() {
4688 XPIDatabase.getVisibleAddonForID(self.addon.id, function initLocalInstall_getVisibleAddon(aAddon) {
4689 self.existingAddon = aAddon;
4690 if (aAddon)
4691 applyBlocklistChanges(aAddon, self.addon);
4692 self.addon.updateDate = Date.now();
4693 self.addon.installDate = aAddon ? aAddon.installDate : self.addon.updateDate;
4694
4695 if (!self.addon.isCompatible) {
4696 // TODO Should we send some event here?
4697 self.state = AddonManager.STATE_CHECKING;
4698 new UpdateChecker(self.addon, {
4699 onUpdateFinished: function updateChecker_onUpdateFinished(aAddon) {
4700 self.state = AddonManager.STATE_DOWNLOADED;
4701 XPIProvider.installs.push(self);
4702 AddonManagerPrivate.callInstallListeners("onNewInstall",
4703 self.listeners,
4704 self.wrapper);
4705
4706 aCallback(self);
4707 }
4708 }, AddonManager.UPDATE_WHEN_ADDON_INSTALLED);
4709 }
4710 else {
4711 XPIProvider.installs.push(self);
4712 AddonManagerPrivate.callInstallListeners("onNewInstall",
4713 self.listeners,
4714 self.wrapper);
4715
4716 aCallback(self);
4717 }
4718 });
4719 });
4720 }
4721 catch (e) {
4722 logger.warn("Invalid XPI", e);
4723 this.state = AddonManager.STATE_DOWNLOAD_FAILED;
4724 this.error = AddonManager.ERROR_CORRUPT_FILE;
4725 aCallback(this);
4726 return;
4727 }
4728 },
4729
4730 /**
4731 * Initialises this install to be a download from a remote url.
4732 *
4733 * @param aCallback
4734 * The callback to pass the initialised AddonInstall to
4735 * @param aName
4736 * An optional name for the add-on
4737 * @param aType
4738 * An optional type for the add-on
4739 * @param aIcons
4740 * Optional icons for the add-on
4741 * @param aVersion
4742 * An optional version for the add-on
4743 */
4744 initAvailableDownload: function AI_initAvailableDownload(aName, aType, aIcons, aVersion, aCallback) {
4745 this.state = AddonManager.STATE_AVAILABLE;
4746 this.name = aName;
4747 this.type = aType;
4748 this.version = aVersion;
4749 this.icons = aIcons;
4750 this.progress = 0;
4751 this.maxProgress = -1;
4752
4753 XPIProvider.installs.push(this);
4754 AddonManagerPrivate.callInstallListeners("onNewInstall", this.listeners,
4755 this.wrapper);
4756
4757 makeSafe(aCallback)(this);
4758 },
4759
4760 /**
4761 * Starts installation of this add-on from whatever state it is currently at
4762 * if possible.
4763 *
4764 * @throws if installation cannot proceed from the current state
4765 */
4766 install: function AI_install() {
4767 switch (this.state) {
4768 case AddonManager.STATE_AVAILABLE:
4769 this.startDownload();
4770 break;
4771 case AddonManager.STATE_DOWNLOADED:
4772 this.startInstall();
4773 break;
4774 case AddonManager.STATE_DOWNLOAD_FAILED:
4775 case AddonManager.STATE_INSTALL_FAILED:
4776 case AddonManager.STATE_CANCELLED:
4777 this.removeTemporaryFile();
4778 this.state = AddonManager.STATE_AVAILABLE;
4779 this.error = 0;
4780 this.progress = 0;
4781 this.maxProgress = -1;
4782 this.hash = this.originalHash;
4783 XPIProvider.installs.push(this);
4784 this.startDownload();
4785 break;
4786 case AddonManager.STATE_DOWNLOADING:
4787 case AddonManager.STATE_CHECKING:
4788 case AddonManager.STATE_INSTALLING:
4789 // Installation is already running
4790 return;
4791 default:
4792 throw new Error("Cannot start installing from this state");
4793 }
4794 },
4795
4796 /**
4797 * Cancels installation of this add-on.
4798 *
4799 * @throws if installation cannot be cancelled from the current state
4800 */
4801 cancel: function AI_cancel() {
4802 switch (this.state) {
4803 case AddonManager.STATE_DOWNLOADING:
4804 if (this.channel)
4805 this.channel.cancel(Cr.NS_BINDING_ABORTED);
4806 case AddonManager.STATE_AVAILABLE:
4807 case AddonManager.STATE_DOWNLOADED:
4808 logger.debug("Cancelling download of " + this.sourceURI.spec);
4809 this.state = AddonManager.STATE_CANCELLED;
4810 XPIProvider.removeActiveInstall(this);
4811 AddonManagerPrivate.callInstallListeners("onDownloadCancelled",
4812 this.listeners, this.wrapper);
4813 this.removeTemporaryFile();
4814 break;
4815 case AddonManager.STATE_INSTALLED:
4816 logger.debug("Cancelling install of " + this.addon.id);
4817 let xpi = this.installLocation.getStagingDir();
4818 xpi.append(this.addon.id + ".xpi");
4819 flushJarCache(xpi);
4820 this.installLocation.cleanStagingDir([this.addon.id, this.addon.id + ".xpi",
4821 this.addon.id + ".json"]);
4822 this.state = AddonManager.STATE_CANCELLED;
4823 XPIProvider.removeActiveInstall(this);
4824
4825 if (this.existingAddon) {
4826 delete this.existingAddon.pendingUpgrade;
4827 this.existingAddon.pendingUpgrade = null;
4828 }
4829
4830 AddonManagerPrivate.callAddonListeners("onOperationCancelled", createWrapper(this.addon));
4831
4832 AddonManagerPrivate.callInstallListeners("onInstallCancelled",
4833 this.listeners, this.wrapper);
4834 break;
4835 default:
4836 throw new Error("Cannot cancel install of " + this.sourceURI.spec +
4837 " from this state (" + this.state + ")");
4838 }
4839 },
4840
4841 /**
4842 * Adds an InstallListener for this instance if the listener is not already
4843 * registered.
4844 *
4845 * @param aListener
4846 * The InstallListener to add
4847 */
4848 addListener: function AI_addListener(aListener) {
4849 if (!this.listeners.some(function addListener_matchListener(i) { return i == aListener; }))
4850 this.listeners.push(aListener);
4851 },
4852
4853 /**
4854 * Removes an InstallListener for this instance if it is registered.
4855 *
4856 * @param aListener
4857 * The InstallListener to remove
4858 */
4859 removeListener: function AI_removeListener(aListener) {
4860 this.listeners = this.listeners.filter(function removeListener_filterListener(i) {
4861 return i != aListener;
4862 });
4863 },
4864
4865 /**
4866 * Removes the temporary file owned by this AddonInstall if there is one.
4867 */
4868 removeTemporaryFile: function AI_removeTemporaryFile() {
4869 // Only proceed if this AddonInstall owns its XPI file
4870 if (!this.ownsTempFile) {
4871 this.logger.debug("removeTemporaryFile: " + this.sourceURI.spec + " does not own temp file");
4872 return;
4873 }
4874
4875 try {
4876 this.logger.debug("removeTemporaryFile: " + this.sourceURI.spec + " removing temp file " +
4877 this.file.path);
4878 this.file.remove(true);
4879 this.ownsTempFile = false;
4880 }
4881 catch (e) {
4882 this.logger.warn("Failed to remove temporary file " + this.file.path + " for addon " +
4883 this.sourceURI.spec,
4884 e);
4885 }
4886 },
4887
4888 /**
4889 * Updates the sourceURI and releaseNotesURI values on the Addon being
4890 * installed by this AddonInstall instance.
4891 */
4892 updateAddonURIs: function AI_updateAddonURIs() {
4893 this.addon.sourceURI = this.sourceURI.spec;
4894 if (this.releaseNotesURI)
4895 this.addon.releaseNotesURI = this.releaseNotesURI.spec;
4896 },
4897
4898 /**
4899 * Loads add-on manifests from a multi-package XPI file. Each of the
4900 * XPI and JAR files contained in the XPI will be extracted. Any that
4901 * do not contain valid add-ons will be ignored. The first valid add-on will
4902 * be installed by this AddonInstall instance, the rest will have new
4903 * AddonInstall instances created for them.
4904 *
4905 * @param aZipReader
4906 * An open nsIZipReader for the multi-package XPI's files. This will
4907 * be closed before this method returns.
4908 * @param aCallback
4909 * A function to call when all of the add-on manifests have been
4910 * loaded. Because this loadMultipackageManifests is an internal API
4911 * we don't exception-wrap this callback
4912 */
4913 _loadMultipackageManifests: function AI_loadMultipackageManifests(aZipReader,
4914 aCallback) {
4915 let files = [];
4916 let entries = aZipReader.findEntries("(*.[Xx][Pp][Ii]|*.[Jj][Aa][Rr])");
4917 while (entries.hasMore()) {
4918 let entryName = entries.getNext();
4919 var target = getTemporaryFile();
4920 try {
4921 aZipReader.extract(entryName, target);
4922 files.push(target);
4923 }
4924 catch (e) {
4925 logger.warn("Failed to extract " + entryName + " from multi-package " +
4926 "XPI", e);
4927 target.remove(false);
4928 }
4929 }
4930
4931 aZipReader.close();
4932
4933 if (files.length == 0) {
4934 throw new Error("Multi-package XPI does not contain any packages " +
4935 "to install");
4936 }
4937
4938 let addon = null;
4939
4940 // Find the first file that has a valid install manifest and use it for
4941 // the add-on that this AddonInstall instance will install.
4942 while (files.length > 0) {
4943 this.removeTemporaryFile();
4944 this.file = files.shift();
4945 this.ownsTempFile = true;
4946 try {
4947 addon = loadManifestFromZipFile(this.file);
4948 break;
4949 }
4950 catch (e) {
4951 logger.warn(this.file.leafName + " cannot be installed from multi-package " +
4952 "XPI", e);
4953 }
4954 }
4955
4956 if (!addon) {
4957 // No valid add-on was found
4958 aCallback();
4959 return;
4960 }
4961
4962 this.addon = addon;
4963
4964 this.updateAddonURIs();
4965
4966 this.addon._install = this;
4967 this.name = this.addon.selectedLocale.name;
4968 this.type = this.addon.type;
4969 this.version = this.addon.version;
4970
4971 // Setting the iconURL to something inside the XPI locks the XPI and
4972 // makes it impossible to delete on Windows.
4973 //let newIcon = createWrapper(this.addon).iconURL;
4974 //if (newIcon)
4975 // this.iconURL = newIcon;
4976
4977 // Create new AddonInstall instances for every remaining file
4978 if (files.length > 0) {
4979 this.linkedInstalls = [];
4980 let count = 0;
4981 let self = this;
4982 files.forEach(function(file) {
4983 AddonInstall.createInstall(function loadMultipackageManifests_createInstall(aInstall) {
4984 // Ignore bad add-ons (createInstall will have logged the error)
4985 if (aInstall.state == AddonManager.STATE_DOWNLOAD_FAILED) {
4986 // Manually remove the temporary file
4987 file.remove(true);
4988 }
4989 else {
4990 // Make the new install own its temporary file
4991 aInstall.ownsTempFile = true;
4992
4993 self.linkedInstalls.push(aInstall)
4994
4995 aInstall.sourceURI = self.sourceURI;
4996 aInstall.releaseNotesURI = self.releaseNotesURI;
4997 aInstall.updateAddonURIs();
4998 }
4999
5000 count++;
5001 if (count == files.length)
5002 aCallback();
5003 }, file);
5004 }, this);
5005 }
5006 else {
5007 aCallback();
5008 }
5009 },
5010
5011 /**
5012 * Called after the add-on is a local file and the signature and install
5013 * manifest can be read.
5014 *
5015 * @param aCallback
5016 * A function to call when the manifest has been loaded
5017 * @throws if the add-on does not contain a valid install manifest or the
5018 * XPI is incorrectly signed
5019 */
5020 loadManifest: function AI_loadManifest(aCallback) {
5021 aCallback = makeSafe(aCallback);
5022 let self = this;
5023 function addRepositoryData(aAddon) {
5024 // Try to load from the existing cache first
5025 AddonRepository.getCachedAddonByID(aAddon.id, function loadManifest_getCachedAddonByID(aRepoAddon) {
5026 if (aRepoAddon) {
5027 aAddon._repositoryAddon = aRepoAddon;
5028 self.name = self.name || aAddon._repositoryAddon.name;
5029 aAddon.compatibilityOverrides = aRepoAddon.compatibilityOverrides;
5030 aAddon.appDisabled = !isUsableAddon(aAddon);
5031 aCallback();
5032 return;
5033 }
5034
5035 // It wasn't there so try to re-download it
5036 AddonRepository.cacheAddons([aAddon.id], function loadManifest_cacheAddons() {
5037 AddonRepository.getCachedAddonByID(aAddon.id, function loadManifest_getCachedAddonByID(aRepoAddon) {
5038 aAddon._repositoryAddon = aRepoAddon;
5039 self.name = self.name || aAddon._repositoryAddon.name;
5040 aAddon.compatibilityOverrides = aRepoAddon ?
5041 aRepoAddon.compatibilityOverrides :
5042 null;
5043 aAddon.appDisabled = !isUsableAddon(aAddon);
5044 aCallback();
5045 });
5046 });
5047 });
5048 }
5049
5050 let zipreader = Cc["@mozilla.org/libjar/zip-reader;1"].
5051 createInstance(Ci.nsIZipReader);
5052 try {
5053 zipreader.open(this.file);
5054 }
5055 catch (e) {
5056 zipreader.close();
5057 throw e;
5058 }
5059
5060 let principal = zipreader.getCertificatePrincipal(null);
5061 if (principal && principal.hasCertificate) {
5062 logger.debug("Verifying XPI signature");
5063 if (verifyZipSigning(zipreader, principal)) {
5064 let x509 = principal.certificate;
5065 if (x509 instanceof Ci.nsIX509Cert)
5066 this.certificate = x509;
5067 if (this.certificate && this.certificate.commonName.length > 0)
5068 this.certName = this.certificate.commonName;
5069 else
5070 this.certName = principal.prettyName;
5071 }
5072 else {
5073 zipreader.close();
5074 throw new Error("XPI is incorrectly signed");
5075 }
5076 }
5077
5078 try {
5079 this.addon = loadManifestFromZipReader(zipreader);
5080 }
5081 catch (e) {
5082 zipreader.close();
5083 throw e;
5084 }
5085
5086 if (this.addon.type == "multipackage") {
5087 this._loadMultipackageManifests(zipreader, function loadManifest_loadMultipackageManifests() {
5088 addRepositoryData(self.addon);
5089 });
5090 return;
5091 }
5092
5093 zipreader.close();
5094
5095 this.updateAddonURIs();
5096
5097 this.addon._install = this;
5098 this.name = this.addon.selectedLocale.name;
5099 this.type = this.addon.type;
5100 this.version = this.addon.version;
5101
5102 // Setting the iconURL to something inside the XPI locks the XPI and
5103 // makes it impossible to delete on Windows.
5104 //let newIcon = createWrapper(this.addon).iconURL;
5105 //if (newIcon)
5106 // this.iconURL = newIcon;
5107
5108 addRepositoryData(this.addon);
5109 },
5110
5111 observe: function AI_observe(aSubject, aTopic, aData) {
5112 // Network is going offline
5113 this.cancel();
5114 },
5115
5116 /**
5117 * Starts downloading the add-on's XPI file.
5118 */
5119 startDownload: function AI_startDownload() {
5120 this.state = AddonManager.STATE_DOWNLOADING;
5121 if (!AddonManagerPrivate.callInstallListeners("onDownloadStarted",
5122 this.listeners, this.wrapper)) {
5123 logger.debug("onDownloadStarted listeners cancelled installation of addon " + this.sourceURI.spec);
5124 this.state = AddonManager.STATE_CANCELLED;
5125 XPIProvider.removeActiveInstall(this);
5126 AddonManagerPrivate.callInstallListeners("onDownloadCancelled",
5127 this.listeners, this.wrapper)
5128 return;
5129 }
5130
5131 // If a listener changed our state then do not proceed with the download
5132 if (this.state != AddonManager.STATE_DOWNLOADING)
5133 return;
5134
5135 if (this.channel) {
5136 // A previous download attempt hasn't finished cleaning up yet, signal
5137 // that it should restart when complete
5138 logger.debug("Waiting for previous download to complete");
5139 this.restartDownload = true;
5140 return;
5141 }
5142
5143 this.openChannel();
5144 },
5145
5146 openChannel: function AI_openChannel() {
5147 this.restartDownload = false;
5148
5149 try {
5150 this.file = getTemporaryFile();
5151 this.ownsTempFile = true;
5152 this.stream = Cc["@mozilla.org/network/file-output-stream;1"].
5153 createInstance(Ci.nsIFileOutputStream);
5154 this.stream.init(this.file, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE |
5155 FileUtils.MODE_TRUNCATE, FileUtils.PERMS_FILE, 0);
5156 }
5157 catch (e) {
5158 logger.warn("Failed to start download for addon " + this.sourceURI.spec, e);
5159 this.state = AddonManager.STATE_DOWNLOAD_FAILED;
5160 this.error = AddonManager.ERROR_FILE_ACCESS;
5161 XPIProvider.removeActiveInstall(this);
5162 AddonManagerPrivate.callInstallListeners("onDownloadFailed",
5163 this.listeners, this.wrapper);
5164 return;
5165 }
5166
5167 let listener = Cc["@mozilla.org/network/stream-listener-tee;1"].
5168 createInstance(Ci.nsIStreamListenerTee);
5169 listener.init(this, this.stream);
5170 try {
5171 Components.utils.import("resource://gre/modules/CertUtils.jsm");
5172 let requireBuiltIn = Prefs.getBoolPref(PREF_INSTALL_REQUIREBUILTINCERTS, true);
5173 this.badCertHandler = new BadCertHandler(!requireBuiltIn);
5174
5175 this.channel = NetUtil.newChannel(this.sourceURI);
5176 this.channel.notificationCallbacks = this;
5177 if (this.channel instanceof Ci.nsIHttpChannelInternal)
5178 this.channel.forceAllowThirdPartyCookie = true;
5179 this.channel.asyncOpen(listener, null);
5180
5181 Services.obs.addObserver(this, "network:offline-about-to-go-offline", false);
5182 }
5183 catch (e) {
5184 logger.warn("Failed to start download for addon " + this.sourceURI.spec, e);
5185 this.state = AddonManager.STATE_DOWNLOAD_FAILED;
5186 this.error = AddonManager.ERROR_NETWORK_FAILURE;
5187 XPIProvider.removeActiveInstall(this);
5188 AddonManagerPrivate.callInstallListeners("onDownloadFailed",
5189 this.listeners, this.wrapper);
5190 }
5191 },
5192
5193 /**
5194 * Update the crypto hasher with the new data and call the progress listeners.
5195 *
5196 * @see nsIStreamListener
5197 */
5198 onDataAvailable: function AI_onDataAvailable(aRequest, aContext, aInputstream,
5199 aOffset, aCount) {
5200 this.crypto.updateFromStream(aInputstream, aCount);
5201 this.progress += aCount;
5202 if (!AddonManagerPrivate.callInstallListeners("onDownloadProgress",
5203 this.listeners, this.wrapper)) {
5204 // TODO cancel the download and make it available again (bug 553024)
5205 }
5206 },
5207
5208 /**
5209 * Check the redirect response for a hash of the target XPI and verify that
5210 * we don't end up on an insecure channel.
5211 *
5212 * @see nsIChannelEventSink
5213 */
5214 asyncOnChannelRedirect: function AI_asyncOnChannelRedirect(aOldChannel, aNewChannel, aFlags, aCallback) {
5215 if (!this.hash && aOldChannel.originalURI.schemeIs("https") &&
5216 aOldChannel instanceof Ci.nsIHttpChannel) {
5217 try {
5218 let hashStr = aOldChannel.getResponseHeader("X-Target-Digest");
5219 let hashSplit = hashStr.toLowerCase().split(":");
5220 this.hash = {
5221 algorithm: hashSplit[0],
5222 data: hashSplit[1]
5223 };
5224 }
5225 catch (e) {
5226 }
5227 }
5228
5229 // Verify that we don't end up on an insecure channel if we haven't got a
5230 // hash to verify with (see bug 537761 for discussion)
5231 if (!this.hash)
5232 this.badCertHandler.asyncOnChannelRedirect(aOldChannel, aNewChannel, aFlags, aCallback);
5233 else
5234 aCallback.onRedirectVerifyCallback(Cr.NS_OK);
5235
5236 this.channel = aNewChannel;
5237 },
5238
5239 /**
5240 * This is the first chance to get at real headers on the channel.
5241 *
5242 * @see nsIStreamListener
5243 */
5244 onStartRequest: function AI_onStartRequest(aRequest, aContext) {
5245 this.crypto = Cc["@mozilla.org/security/hash;1"].
5246 createInstance(Ci.nsICryptoHash);
5247 if (this.hash) {
5248 try {
5249 this.crypto.initWithString(this.hash.algorithm);
5250 }
5251 catch (e) {
5252 logger.warn("Unknown hash algorithm '" + this.hash.algorithm + "' for addon " + this.sourceURI.spec, e);
5253 this.state = AddonManager.STATE_DOWNLOAD_FAILED;
5254 this.error = AddonManager.ERROR_INCORRECT_HASH;
5255 XPIProvider.removeActiveInstall(this);
5256 AddonManagerPrivate.callInstallListeners("onDownloadFailed",
5257 this.listeners, this.wrapper);
5258 aRequest.cancel(Cr.NS_BINDING_ABORTED);
5259 return;
5260 }
5261 }
5262 else {
5263 // We always need something to consume data from the inputstream passed
5264 // to onDataAvailable so just create a dummy cryptohasher to do that.
5265 this.crypto.initWithString("sha1");
5266 }
5267
5268 this.progress = 0;
5269 if (aRequest instanceof Ci.nsIChannel) {
5270 try {
5271 this.maxProgress = aRequest.contentLength;
5272 }
5273 catch (e) {
5274 }
5275 logger.debug("Download started for " + this.sourceURI.spec + " to file " +
5276 this.file.path);
5277 }
5278 },
5279
5280 /**
5281 * The download is complete.
5282 *
5283 * @see nsIStreamListener
5284 */
5285 onStopRequest: function AI_onStopRequest(aRequest, aContext, aStatus) {
5286 this.stream.close();
5287 this.channel = null;
5288 this.badCerthandler = null;
5289 Services.obs.removeObserver(this, "network:offline-about-to-go-offline");
5290
5291 // If the download was cancelled then all events will have already been sent
5292 if (aStatus == Cr.NS_BINDING_ABORTED) {
5293 this.removeTemporaryFile();
5294 if (this.restartDownload)
5295 this.openChannel();
5296 return;
5297 }
5298
5299 logger.debug("Download of " + this.sourceURI.spec + " completed.");
5300
5301 if (Components.isSuccessCode(aStatus)) {
5302 if (!(aRequest instanceof Ci.nsIHttpChannel) || aRequest.requestSucceeded) {
5303 if (!this.hash && (aRequest instanceof Ci.nsIChannel)) {
5304 try {
5305 checkCert(aRequest,
5306 !Prefs.getBoolPref(PREF_INSTALL_REQUIREBUILTINCERTS, true));
5307 }
5308 catch (e) {
5309 this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE, e);
5310 return;
5311 }
5312 }
5313
5314 // convert the binary hash data to a hex string.
5315 let calculatedHash = getHashStringForCrypto(this.crypto);
5316 this.crypto = null;
5317 if (this.hash && calculatedHash != this.hash.data) {
5318 this.downloadFailed(AddonManager.ERROR_INCORRECT_HASH,
5319 "Downloaded file hash (" + calculatedHash +
5320 ") did not match provided hash (" + this.hash.data + ")");
5321 return;
5322 }
5323 try {
5324 let self = this;
5325 this.loadManifest(function onStopRequest_loadManifest() {
5326 if (self.addon.isCompatible) {
5327 self.downloadCompleted();
5328 }
5329 else {
5330 // TODO Should we send some event here (bug 557716)?
5331 self.state = AddonManager.STATE_CHECKING;
5332 new UpdateChecker(self.addon, {
5333 onUpdateFinished: function onStopRequest_onUpdateFinished(aAddon) {
5334 self.downloadCompleted();
5335 }
5336 }, AddonManager.UPDATE_WHEN_ADDON_INSTALLED);
5337 }
5338 });
5339 }
5340 catch (e) {
5341 this.downloadFailed(AddonManager.ERROR_CORRUPT_FILE, e);
5342 }
5343 }
5344 else {
5345 if (aRequest instanceof Ci.nsIHttpChannel)
5346 this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE,
5347 aRequest.responseStatus + " " +
5348 aRequest.responseStatusText);
5349 else
5350 this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE, aStatus);
5351 }
5352 }
5353 else {
5354 this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE, aStatus);
5355 }
5356 },
5357
5358 /**
5359 * Notify listeners that the download failed.
5360 *
5361 * @param aReason
5362 * Something to log about the failure
5363 * @param error
5364 * The error code to pass to the listeners
5365 */
5366 downloadFailed: function AI_downloadFailed(aReason, aError) {
5367 logger.warn("Download of " + this.sourceURI.spec + " failed", aError);
5368 this.state = AddonManager.STATE_DOWNLOAD_FAILED;
5369 this.error = aReason;
5370 XPIProvider.removeActiveInstall(this);
5371 AddonManagerPrivate.callInstallListeners("onDownloadFailed", this.listeners,
5372 this.wrapper);
5373
5374 // If the listener hasn't restarted the download then remove any temporary
5375 // file
5376 if (this.state == AddonManager.STATE_DOWNLOAD_FAILED) {
5377 logger.debug("downloadFailed: removing temp file for " + this.sourceURI.spec);
5378 this.removeTemporaryFile();
5379 }
5380 else
5381 logger.debug("downloadFailed: listener changed AddonInstall state for " +
5382 this.sourceURI.spec + " to " + this.state);
5383 },
5384
5385 /**
5386 * Notify listeners that the download completed.
5387 */
5388 downloadCompleted: function AI_downloadCompleted() {
5389 let self = this;
5390 XPIDatabase.getVisibleAddonForID(this.addon.id, function downloadCompleted_getVisibleAddonForID(aAddon) {
5391 if (aAddon)
5392 self.existingAddon = aAddon;
5393
5394 self.state = AddonManager.STATE_DOWNLOADED;
5395 self.addon.updateDate = Date.now();
5396
5397 if (self.existingAddon) {
5398 self.addon.existingAddonID = self.existingAddon.id;
5399 self.addon.installDate = self.existingAddon.installDate;
5400 applyBlocklistChanges(self.existingAddon, self.addon);
5401 }
5402 else {
5403 self.addon.installDate = self.addon.updateDate;
5404 }
5405
5406 if (AddonManagerPrivate.callInstallListeners("onDownloadEnded",
5407 self.listeners,
5408 self.wrapper)) {
5409 // If a listener changed our state then do not proceed with the install
5410 if (self.state != AddonManager.STATE_DOWNLOADED)
5411 return;
5412
5413 self.install();
5414
5415 if (self.linkedInstalls) {
5416 self.linkedInstalls.forEach(function(aInstall) {
5417 aInstall.install();
5418 });
5419 }
5420 }
5421 });
5422 },
5423
5424 // TODO This relies on the assumption that we are always installing into the
5425 // highest priority install location so the resulting add-on will be visible
5426 // overriding any existing copy in another install location (bug 557710).
5427 /**
5428 * Installs the add-on into the install location.
5429 */
5430 startInstall: function AI_startInstall() {
5431 this.state = AddonManager.STATE_INSTALLING;
5432 if (!AddonManagerPrivate.callInstallListeners("onInstallStarted",
5433 this.listeners, this.wrapper)) {
5434 this.state = AddonManager.STATE_DOWNLOADED;
5435 XPIProvider.removeActiveInstall(this);
5436 AddonManagerPrivate.callInstallListeners("onInstallCancelled",
5437 this.listeners, this.wrapper)
5438 return;
5439 }
5440
5441 // Find and cancel any pending installs for the same add-on in the same
5442 // install location
5443 for (let aInstall of XPIProvider.installs) {
5444 if (aInstall.state == AddonManager.STATE_INSTALLED &&
5445 aInstall.installLocation == this.installLocation &&
5446 aInstall.addon.id == this.addon.id) {
5447 logger.debug("Cancelling previous pending install of " + aInstall.addon.id);
5448 aInstall.cancel();
5449 }
5450 }
5451
5452 let isUpgrade = this.existingAddon &&
5453 this.existingAddon._installLocation == this.installLocation;
5454 let requiresRestart = XPIProvider.installRequiresRestart(this.addon);
5455
5456 logger.debug("Starting install of " + this.addon.id + " from " + this.sourceURI.spec);
5457 AddonManagerPrivate.callAddonListeners("onInstalling",
5458 createWrapper(this.addon),
5459 requiresRestart);
5460
5461 let stagingDir = this.installLocation.getStagingDir();
5462 let stagedAddon = stagingDir.clone();
5463
5464 Task.spawn((function() {
5465 let installedUnpacked = 0;
5466 yield this.installLocation.requestStagingDir();
5467
5468 // First stage the file regardless of whether restarting is necessary
5469 if (this.addon.unpack || Prefs.getBoolPref(PREF_XPI_UNPACK, false)) {
5470 logger.debug("Addon " + this.addon.id + " will be installed as " +
5471 "an unpacked directory");
5472 stagedAddon.append(this.addon.id);
5473 yield removeAsync(stagedAddon);
5474 yield OS.File.makeDir(stagedAddon.path);
5475 yield ZipUtils.extractFilesAsync(this.file, stagedAddon);
5476 installedUnpacked = 1;
5477 }
5478 else {
5479 logger.debug("Addon " + this.addon.id + " will be installed as " +
5480 "a packed xpi");
5481 stagedAddon.append(this.addon.id + ".xpi");
5482 yield removeAsync(stagedAddon);
5483 yield OS.File.copy(this.file.path, stagedAddon.path);
5484 }
5485
5486 if (requiresRestart) {
5487 // Point the add-on to its extracted files as the xpi may get deleted
5488 this.addon._sourceBundle = stagedAddon;
5489
5490 // Cache the AddonInternal as it may have updated compatibility info
5491 let stagedJSON = stagedAddon.clone();
5492 stagedJSON.leafName = this.addon.id + ".json";
5493 if (stagedJSON.exists())
5494 stagedJSON.remove(true);
5495 let stream = Cc["@mozilla.org/network/file-output-stream;1"].
5496 createInstance(Ci.nsIFileOutputStream);
5497 let converter = Cc["@mozilla.org/intl/converter-output-stream;1"].
5498 createInstance(Ci.nsIConverterOutputStream);
5499
5500 try {
5501 stream.init(stagedJSON, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE |
5502 FileUtils.MODE_TRUNCATE, FileUtils.PERMS_FILE,
5503 0);
5504 converter.init(stream, "UTF-8", 0, 0x0000);
5505 converter.writeString(JSON.stringify(this.addon));
5506 }
5507 finally {
5508 converter.close();
5509 stream.close();
5510 }
5511
5512 logger.debug("Staged install of " + this.addon.id + " from " + this.sourceURI.spec + " ready; waiting for restart.");
5513 this.state = AddonManager.STATE_INSTALLED;
5514 if (isUpgrade) {
5515 delete this.existingAddon.pendingUpgrade;
5516 this.existingAddon.pendingUpgrade = this.addon;
5517 }
5518 AddonManagerPrivate.callInstallListeners("onInstallEnded",
5519 this.listeners, this.wrapper,
5520 createWrapper(this.addon));
5521 }
5522 else {
5523 // The install is completed so it should be removed from the active list
5524 XPIProvider.removeActiveInstall(this);
5525
5526 // TODO We can probably reduce the number of DB operations going on here
5527 // We probably also want to support rolling back failed upgrades etc.
5528 // See bug 553015.
5529
5530 // Deactivate and remove the old add-on as necessary
5531 let reason = BOOTSTRAP_REASONS.ADDON_INSTALL;
5532 if (this.existingAddon) {
5533 if (Services.vc.compare(this.existingAddon.version, this.addon.version) < 0)
5534 reason = BOOTSTRAP_REASONS.ADDON_UPGRADE;
5535 else
5536 reason = BOOTSTRAP_REASONS.ADDON_DOWNGRADE;
5537
5538 if (this.existingAddon.bootstrap) {
5539 let file = this.existingAddon._installLocation
5540 .getLocationForID(this.existingAddon.id);
5541 if (this.existingAddon.active) {
5542 XPIProvider.callBootstrapMethod(this.existingAddon.id,
5543 this.existingAddon.version,
5544 this.existingAddon.type, file,
5545 "shutdown", reason,
5546 { newVersion: this.addon.version });
5547 }
5548
5549 XPIProvider.callBootstrapMethod(this.existingAddon.id,
5550 this.existingAddon.version,
5551 this.existingAddon.type, file,
5552 "uninstall", reason,
5553 { newVersion: this.addon.version });
5554 XPIProvider.unloadBootstrapScope(this.existingAddon.id);
5555 flushStartupCache();
5556 }
5557
5558 if (!isUpgrade && this.existingAddon.active) {
5559 XPIDatabase.updateAddonActive(this.existingAddon, false);
5560 }
5561 }
5562
5563 // Install the new add-on into its final location
5564 let existingAddonID = this.existingAddon ? this.existingAddon.id : null;
5565 let file = this.installLocation.installAddon(this.addon.id, stagedAddon,
5566 existingAddonID);
5567
5568 // Update the metadata in the database
5569 this.addon._sourceBundle = file;
5570 this.addon._installLocation = this.installLocation;
5571 let scanStarted = Date.now();
5572 let [, mTime, scanItems] = recursiveLastModifiedTime(file);
5573 let scanTime = Date.now() - scanStarted;
5574 this.addon.updateDate = mTime;
5575 this.addon.visible = true;
5576 if (isUpgrade) {
5577 this.addon = XPIDatabase.updateAddonMetadata(this.existingAddon, this.addon,
5578 file.persistentDescriptor);
5579 }
5580 else {
5581 this.addon.installDate = this.addon.updateDate;
5582 this.addon.active = (this.addon.visible && !isAddonDisabled(this.addon))
5583 this.addon = XPIDatabase.addAddonMetadata(this.addon, file.persistentDescriptor);
5584 }
5585
5586 let extraParams = {};
5587 if (this.existingAddon) {
5588 extraParams.oldVersion = this.existingAddon.version;
5589 }
5590
5591 if (this.addon.bootstrap) {
5592 XPIProvider.callBootstrapMethod(this.addon.id, this.addon.version,
5593 this.addon.type, file, "install",
5594 reason, extraParams);
5595 }
5596
5597 AddonManagerPrivate.callAddonListeners("onInstalled",
5598 createWrapper(this.addon));
5599
5600 logger.debug("Install of " + this.sourceURI.spec + " completed.");
5601 this.state = AddonManager.STATE_INSTALLED;
5602 AddonManagerPrivate.callInstallListeners("onInstallEnded",
5603 this.listeners, this.wrapper,
5604 createWrapper(this.addon));
5605
5606 if (this.addon.bootstrap) {
5607 if (this.addon.active) {
5608 XPIProvider.callBootstrapMethod(this.addon.id, this.addon.version,
5609 this.addon.type, file, "startup",
5610 reason, extraParams);
5611 }
5612 else {
5613 // XXX this makes it dangerous to do some things in onInstallEnded
5614 // listeners because important cleanup hasn't been done yet
5615 XPIProvider.unloadBootstrapScope(this.addon.id);
5616 }
5617 }
5618 XPIProvider.setTelemetry(this.addon.id, "unpacked", installedUnpacked);
5619 XPIProvider.setTelemetry(this.addon.id, "location", this.installLocation.name);
5620 XPIProvider.setTelemetry(this.addon.id, "scan_MS", scanTime);
5621 XPIProvider.setTelemetry(this.addon.id, "scan_items", scanItems);
5622 let loc = this.addon.defaultLocale;
5623 if (loc) {
5624 XPIProvider.setTelemetry(this.addon.id, "name", loc.name);
5625 XPIProvider.setTelemetry(this.addon.id, "creator", loc.creator);
5626 }
5627 }
5628 }).bind(this)).then(null, (e) => {
5629 logger.warn("Failed to install " + this.file.path + " from " + this.sourceURI.spec, e);
5630 if (stagedAddon.exists())
5631 recursiveRemove(stagedAddon);
5632 this.state = AddonManager.STATE_INSTALL_FAILED;
5633 this.error = AddonManager.ERROR_FILE_ACCESS;
5634 XPIProvider.removeActiveInstall(this);
5635 AddonManagerPrivate.callAddonListeners("onOperationCancelled",
5636 createWrapper(this.addon));
5637 AddonManagerPrivate.callInstallListeners("onInstallFailed",
5638 this.listeners,
5639 this.wrapper);
5640 }).then(() => {
5641 this.removeTemporaryFile();
5642 return this.installLocation.releaseStagingDir();
5643 });
5644 },
5645
5646 getInterface: function AI_getInterface(iid) {
5647 if (iid.equals(Ci.nsIAuthPrompt2)) {
5648 var factory = Cc["@mozilla.org/prompter;1"].
5649 getService(Ci.nsIPromptFactory);
5650 return factory.getPrompt(this.window, Ci.nsIAuthPrompt);
5651 }
5652 else if (iid.equals(Ci.nsIChannelEventSink)) {
5653 return this;
5654 }
5655
5656 return this.badCertHandler.getInterface(iid);
5657 }
5658 }
5659
5660 /**
5661 * Creates a new AddonInstall for an already staged install. Used when
5662 * installing the staged install failed for some reason.
5663 *
5664 * @param aDir
5665 * The directory holding the staged install
5666 * @param aManifest
5667 * The cached manifest for the install
5668 */
5669 AddonInstall.createStagedInstall = function AI_createStagedInstall(aInstallLocation, aDir, aManifest) {
5670 let url = Services.io.newFileURI(aDir);
5671
5672 let install = new AddonInstall(aInstallLocation, aDir);
5673 install.initStagedInstall(aManifest);
5674 };
5675
5676 /**
5677 * Creates a new AddonInstall to install an add-on from a local file. Installs
5678 * always go into the profile install location.
5679 *
5680 * @param aCallback
5681 * The callback to pass the new AddonInstall to
5682 * @param aFile
5683 * The file to install
5684 */
5685 AddonInstall.createInstall = function AI_createInstall(aCallback, aFile) {
5686 let location = XPIProvider.installLocationsByName[KEY_APP_PROFILE];
5687 let url = Services.io.newFileURI(aFile);
5688
5689 try {
5690 let install = new AddonInstall(location, url);
5691 install.initLocalInstall(aCallback);
5692 }
5693 catch(e) {
5694 logger.error("Error creating install", e);
5695 makeSafe(aCallback)(null);
5696 }
5697 };
5698
5699 /**
5700 * Creates a new AddonInstall to download and install a URL.
5701 *
5702 * @param aCallback
5703 * The callback to pass the new AddonInstall to
5704 * @param aUri
5705 * The URI to download
5706 * @param aHash
5707 * A hash for the add-on
5708 * @param aName
5709 * A name for the add-on
5710 * @param aIcons
5711 * An icon URLs for the add-on
5712 * @param aVersion
5713 * A version for the add-on
5714 * @param aLoadGroup
5715 * An nsILoadGroup to associate the download with
5716 */
5717 AddonInstall.createDownload = function AI_createDownload(aCallback, aUri, aHash, aName, aIcons,
5718 aVersion, aLoadGroup) {
5719 let location = XPIProvider.installLocationsByName[KEY_APP_PROFILE];
5720 let url = NetUtil.newURI(aUri);
5721
5722 let install = new AddonInstall(location, url, aHash, null, null, aLoadGroup);
5723 if (url instanceof Ci.nsIFileURL)
5724 install.initLocalInstall(aCallback);
5725 else
5726 install.initAvailableDownload(aName, null, aIcons, aVersion, aCallback);
5727 };
5728
5729 /**
5730 * Creates a new AddonInstall for an update.
5731 *
5732 * @param aCallback
5733 * The callback to pass the new AddonInstall to
5734 * @param aAddon
5735 * The add-on being updated
5736 * @param aUpdate
5737 * The metadata about the new version from the update manifest
5738 */
5739 AddonInstall.createUpdate = function AI_createUpdate(aCallback, aAddon, aUpdate) {
5740 let url = NetUtil.newURI(aUpdate.updateURL);
5741 let releaseNotesURI = null;
5742 try {
5743 if (aUpdate.updateInfoURL)
5744 releaseNotesURI = NetUtil.newURI(escapeAddonURI(aAddon, aUpdate.updateInfoURL));
5745 }
5746 catch (e) {
5747 // If the releaseNotesURI cannot be parsed then just ignore it.
5748 }
5749
5750 let install = new AddonInstall(aAddon._installLocation, url,
5751 aUpdate.updateHash, releaseNotesURI, aAddon);
5752 if (url instanceof Ci.nsIFileURL) {
5753 install.initLocalInstall(aCallback);
5754 }
5755 else {
5756 install.initAvailableDownload(aAddon.selectedLocale.name, aAddon.type,
5757 aAddon.icons, aUpdate.version, aCallback);
5758 }
5759 };
5760
5761 /**
5762 * Creates a wrapper for an AddonInstall that only exposes the public API
5763 *
5764 * @param install
5765 * The AddonInstall to create a wrapper for
5766 */
5767 function AddonInstallWrapper(aInstall) {
5768 #ifdef MOZ_EM_DEBUG
5769 this.__defineGetter__("__AddonInstallInternal__", function AIW_debugGetter() {
5770 return aInstall;
5771 });
5772 #endif
5773
5774 ["name", "type", "version", "icons", "releaseNotesURI", "file", "state", "error",
5775 "progress", "maxProgress", "certificate", "certName"].forEach(function(aProp) {
5776 this.__defineGetter__(aProp, function AIW_propertyGetter() aInstall[aProp]);
5777 }, this);
5778
5779 this.__defineGetter__("iconURL", function AIW_iconURL() aInstall.icons[32]);
5780
5781 this.__defineGetter__("existingAddon", function AIW_existingAddonGetter() {
5782 return createWrapper(aInstall.existingAddon);
5783 });
5784 this.__defineGetter__("addon", function AIW_addonGetter() createWrapper(aInstall.addon));
5785 this.__defineGetter__("sourceURI", function AIW_sourceURIGetter() aInstall.sourceURI);
5786
5787 this.__defineGetter__("linkedInstalls", function AIW_linkedInstallsGetter() {
5788 if (!aInstall.linkedInstalls)
5789 return null;
5790 return [i.wrapper for each (i in aInstall.linkedInstalls)];
5791 });
5792
5793 this.install = function AIW_install() {
5794 aInstall.install();
5795 }
5796
5797 this.cancel = function AIW_cancel() {
5798 aInstall.cancel();
5799 }
5800
5801 this.addListener = function AIW_addListener(listener) {
5802 aInstall.addListener(listener);
5803 }
5804
5805 this.removeListener = function AIW_removeListener(listener) {
5806 aInstall.removeListener(listener);
5807 }
5808 }
5809
5810 AddonInstallWrapper.prototype = {};
5811
5812 /**
5813 * Creates a new update checker.
5814 *
5815 * @param aAddon
5816 * The add-on to check for updates
5817 * @param aListener
5818 * An UpdateListener to notify of updates
5819 * @param aReason
5820 * The reason for the update check
5821 * @param aAppVersion
5822 * An optional application version to check for updates for
5823 * @param aPlatformVersion
5824 * An optional platform version to check for updates for
5825 * @throws if the aListener or aReason arguments are not valid
5826 */
5827 function UpdateChecker(aAddon, aListener, aReason, aAppVersion, aPlatformVersion) {
5828 if (!aListener || !aReason)
5829 throw Cr.NS_ERROR_INVALID_ARG;
5830
5831 Components.utils.import("resource://gre/modules/addons/AddonUpdateChecker.jsm");
5832
5833 this.addon = aAddon;
5834 aAddon._updateCheck = this;
5835 XPIProvider.doing(this);
5836 this.listener = aListener;
5837 this.appVersion = aAppVersion;
5838 this.platformVersion = aPlatformVersion;
5839 this.syncCompatibility = (aReason == AddonManager.UPDATE_WHEN_NEW_APP_INSTALLED);
5840
5841 let updateURL = aAddon.updateURL;
5842 if (!updateURL) {
5843 if (aReason == AddonManager.UPDATE_WHEN_PERIODIC_UPDATE &&
5844 Services.prefs.getPrefType(PREF_EM_UPDATE_BACKGROUND_URL) == Services.prefs.PREF_STRING) {
5845 updateURL = Services.prefs.getCharPref(PREF_EM_UPDATE_BACKGROUND_URL);
5846 } else {
5847 updateURL = Services.prefs.getCharPref(PREF_EM_UPDATE_URL);
5848 }
5849 }
5850
5851 const UPDATE_TYPE_COMPATIBILITY = 32;
5852 const UPDATE_TYPE_NEWVERSION = 64;
5853
5854 aReason |= UPDATE_TYPE_COMPATIBILITY;
5855 if ("onUpdateAvailable" in this.listener)
5856 aReason |= UPDATE_TYPE_NEWVERSION;
5857
5858 let url = escapeAddonURI(aAddon, updateURL, aReason, aAppVersion);
5859 this._parser = AddonUpdateChecker.checkForUpdates(aAddon.id, aAddon.updateKey,
5860 url, this);
5861 }
5862
5863 UpdateChecker.prototype = {
5864 addon: null,
5865 listener: null,
5866 appVersion: null,
5867 platformVersion: null,
5868 syncCompatibility: null,
5869
5870 /**
5871 * Calls a method on the listener passing any number of arguments and
5872 * consuming any exceptions.
5873 *
5874 * @param aMethod
5875 * The method to call on the listener
5876 */
5877 callListener: function UC_callListener(aMethod, ...aArgs) {
5878 if (!(aMethod in this.listener))
5879 return;
5880
5881 try {
5882 this.listener[aMethod].apply(this.listener, aArgs);
5883 }
5884 catch (e) {
5885 logger.warn("Exception calling UpdateListener method " + aMethod, e);
5886 }
5887 },
5888
5889 /**
5890 * Called when AddonUpdateChecker completes the update check
5891 *
5892 * @param updates
5893 * The list of update details for the add-on
5894 */
5895 onUpdateCheckComplete: function UC_onUpdateCheckComplete(aUpdates) {
5896 XPIProvider.done(this.addon._updateCheck);
5897 this.addon._updateCheck = null;
5898 let AUC = AddonUpdateChecker;
5899
5900 let ignoreMaxVersion = false;
5901 let ignoreStrictCompat = false;
5902 if (!AddonManager.checkCompatibility) {
5903 ignoreMaxVersion = true;
5904 ignoreStrictCompat = true;
5905 } else if (this.addon.type in COMPATIBLE_BY_DEFAULT_TYPES &&
5906 !AddonManager.strictCompatibility &&
5907 !this.addon.strictCompatibility &&
5908 !this.addon.hasBinaryComponents) {
5909 ignoreMaxVersion = true;
5910 }
5911
5912 // Always apply any compatibility update for the current version
5913 let compatUpdate = AUC.getCompatibilityUpdate(aUpdates, this.addon.version,
5914 this.syncCompatibility,
5915 null, null,
5916 ignoreMaxVersion,
5917 ignoreStrictCompat);
5918 // Apply the compatibility update to the database
5919 if (compatUpdate)
5920 this.addon.applyCompatibilityUpdate(compatUpdate, this.syncCompatibility);
5921
5922 // If the request is for an application or platform version that is
5923 // different to the current application or platform version then look for a
5924 // compatibility update for those versions.
5925 if ((this.appVersion &&
5926 Services.vc.compare(this.appVersion, Services.appinfo.version) != 0) ||
5927 (this.platformVersion &&
5928 Services.vc.compare(this.platformVersion, Services.appinfo.platformVersion) != 0)) {
5929 compatUpdate = AUC.getCompatibilityUpdate(aUpdates, this.addon.version,
5930 false, this.appVersion,
5931 this.platformVersion,
5932 ignoreMaxVersion,
5933 ignoreStrictCompat);
5934 }
5935
5936 if (compatUpdate)
5937 this.callListener("onCompatibilityUpdateAvailable", createWrapper(this.addon));
5938 else
5939 this.callListener("onNoCompatibilityUpdateAvailable", createWrapper(this.addon));
5940
5941 function sendUpdateAvailableMessages(aSelf, aInstall) {
5942 if (aInstall) {
5943 aSelf.callListener("onUpdateAvailable", createWrapper(aSelf.addon),
5944 aInstall.wrapper);
5945 }
5946 else {
5947 aSelf.callListener("onNoUpdateAvailable", createWrapper(aSelf.addon));
5948 }
5949 aSelf.callListener("onUpdateFinished", createWrapper(aSelf.addon),
5950 AddonManager.UPDATE_STATUS_NO_ERROR);
5951 }
5952
5953 let compatOverrides = AddonManager.strictCompatibility ?
5954 null :
5955 this.addon.compatibilityOverrides;
5956
5957 let update = AUC.getNewestCompatibleUpdate(aUpdates,
5958 this.appVersion,
5959 this.platformVersion,
5960 ignoreMaxVersion,
5961 ignoreStrictCompat,
5962 compatOverrides);
5963
5964 if (update && Services.vc.compare(this.addon.version, update.version) < 0) {
5965 for (let currentInstall of XPIProvider.installs) {
5966 // Skip installs that don't match the available update
5967 if (currentInstall.existingAddon != this.addon ||
5968 currentInstall.version != update.version)
5969 continue;
5970
5971 // If the existing install has not yet started downloading then send an
5972 // available update notification. If it is already downloading then
5973 // don't send any available update notification
5974 if (currentInstall.state == AddonManager.STATE_AVAILABLE) {
5975 logger.debug("Found an existing AddonInstall for " + this.addon.id);
5976 sendUpdateAvailableMessages(this, currentInstall);
5977 }
5978 else
5979 sendUpdateAvailableMessages(this, null);
5980 return;
5981 }
5982
5983 let self = this;
5984 AddonInstall.createUpdate(function onUpdateCheckComplete_createUpdate(aInstall) {
5985 sendUpdateAvailableMessages(self, aInstall);
5986 }, this.addon, update);
5987 }
5988 else {
5989 sendUpdateAvailableMessages(this, null);
5990 }
5991 },
5992
5993 /**
5994 * Called when AddonUpdateChecker fails the update check
5995 *
5996 * @param aError
5997 * An error status
5998 */
5999 onUpdateCheckError: function UC_onUpdateCheckError(aError) {
6000 XPIProvider.done(this.addon._updateCheck);
6001 this.addon._updateCheck = null;
6002 this.callListener("onNoCompatibilityUpdateAvailable", createWrapper(this.addon));
6003 this.callListener("onNoUpdateAvailable", createWrapper(this.addon));
6004 this.callListener("onUpdateFinished", createWrapper(this.addon), aError);
6005 },
6006
6007 /**
6008 * Called to cancel an in-progress update check
6009 */
6010 cancel: function UC_cancel() {
6011 let parser = this._parser;
6012 if (parser) {
6013 this._parser = null;
6014 // This will call back to onUpdateCheckError with a CANCELLED error
6015 parser.cancel();
6016 }
6017 }
6018 };
6019
6020 /**
6021 * The AddonInternal is an internal only representation of add-ons. It may
6022 * have come from the database (see DBAddonInternal in XPIProviderUtils.jsm)
6023 * or an install manifest.
6024 */
6025 function AddonInternal() {
6026 }
6027
6028 AddonInternal.prototype = {
6029 _selectedLocale: null,
6030 active: false,
6031 visible: false,
6032 userDisabled: false,
6033 appDisabled: false,
6034 softDisabled: false,
6035 sourceURI: null,
6036 releaseNotesURI: null,
6037 foreignInstall: false,
6038
6039 get selectedLocale() {
6040 if (this._selectedLocale)
6041 return this._selectedLocale;
6042 let locale = findClosestLocale(this.locales);
6043 this._selectedLocale = locale ? locale : this.defaultLocale;
6044 return this._selectedLocale;
6045 },
6046
6047 get providesUpdatesSecurely() {
6048 return !!(this.updateKey || !this.updateURL ||
6049 this.updateURL.substring(0, 6) == "https:");
6050 },
6051
6052 get isCompatible() {
6053 return this.isCompatibleWith();
6054 },
6055
6056 get isPlatformCompatible() {
6057 if (this.targetPlatforms.length == 0)
6058 return true;
6059
6060 let matchedOS = false;
6061
6062 // If any targetPlatform matches the OS and contains an ABI then we will
6063 // only match a targetPlatform that contains both the current OS and ABI
6064 let needsABI = false;
6065
6066 // Some platforms do not specify an ABI, test against null in that case.
6067 let abi = null;
6068 try {
6069 abi = Services.appinfo.XPCOMABI;
6070 }
6071 catch (e) { }
6072
6073 for (let platform of this.targetPlatforms) {
6074 if (platform.os == Services.appinfo.OS) {
6075 if (platform.abi) {
6076 needsABI = true;
6077 if (platform.abi === abi)
6078 return true;
6079 }
6080 else {
6081 matchedOS = true;
6082 }
6083 }
6084 }
6085
6086 return matchedOS && !needsABI;
6087 },
6088
6089 isCompatibleWith: function AddonInternal_isCompatibleWith(aAppVersion, aPlatformVersion) {
6090 // Experiments are installed through an external mechanism that
6091 // limits target audience to compatible clients. We trust it knows what
6092 // it's doing and skip compatibility checks.
6093 //
6094 // This decision does forfeit defense in depth. If the experiments system
6095 // is ever wrong about targeting an add-on to a specific application
6096 // or platform, the client will likely see errors.
6097 if (this.type == "experiment") {
6098 return true;
6099 }
6100
6101 let app = this.matchingTargetApplication;
6102 if (!app)
6103 return false;
6104
6105 if (!aAppVersion)
6106 aAppVersion = Services.appinfo.version;
6107 if (!aPlatformVersion)
6108 aPlatformVersion = Services.appinfo.platformVersion;
6109
6110 let version;
6111 if (app.id == Services.appinfo.ID)
6112 version = aAppVersion;
6113 else if (app.id == TOOLKIT_ID)
6114 version = aPlatformVersion
6115
6116 // Only extensions and dictionaries can be compatible by default; themes
6117 // and language packs always use strict compatibility checking.
6118 if (this.type in COMPATIBLE_BY_DEFAULT_TYPES &&
6119 !AddonManager.strictCompatibility && !this.strictCompatibility &&
6120 !this.hasBinaryComponents) {
6121
6122 // The repository can specify compatibility overrides.
6123 // Note: For now, only blacklisting is supported by overrides.
6124 if (this._repositoryAddon &&
6125 this._repositoryAddon.compatibilityOverrides) {
6126 let overrides = this._repositoryAddon.compatibilityOverrides;
6127 let override = AddonRepository.findMatchingCompatOverride(this.version,
6128 overrides);
6129 if (override && override.type == "incompatible")
6130 return false;
6131 }
6132
6133 // Extremely old extensions should not be compatible by default.
6134 let minCompatVersion;
6135 if (app.id == Services.appinfo.ID)
6136 minCompatVersion = XPIProvider.minCompatibleAppVersion;
6137 else if (app.id == TOOLKIT_ID)
6138 minCompatVersion = XPIProvider.minCompatiblePlatformVersion;
6139
6140 if (minCompatVersion &&
6141 Services.vc.compare(minCompatVersion, app.maxVersion) > 0)
6142 return false;
6143
6144 return Services.vc.compare(version, app.minVersion) >= 0;
6145 }
6146
6147 return (Services.vc.compare(version, app.minVersion) >= 0) &&
6148 (Services.vc.compare(version, app.maxVersion) <= 0)
6149 },
6150
6151 get matchingTargetApplication() {
6152 let app = null;
6153 for (let targetApp of this.targetApplications) {
6154 if (targetApp.id == Services.appinfo.ID)
6155 return targetApp;
6156 if (targetApp.id == TOOLKIT_ID)
6157 app = targetApp;
6158 }
6159 return app;
6160 },
6161
6162 get blocklistState() {
6163 let staticItem = findMatchingStaticBlocklistItem(this);
6164 if (staticItem)
6165 return staticItem.level;
6166
6167 let bs = Cc["@mozilla.org/extensions/blocklist;1"].
6168 getService(Ci.nsIBlocklistService);
6169 return bs.getAddonBlocklistState(createWrapper(this));
6170 },
6171
6172 get blocklistURL() {
6173 let staticItem = findMatchingStaticBlocklistItem(this);
6174 if (staticItem) {
6175 let url = Services.urlFormatter.formatURLPref("extensions.blocklist.itemURL");
6176 return url.replace(/%blockID%/g, staticItem.blockID);
6177 }
6178
6179 let bs = Cc["@mozilla.org/extensions/blocklist;1"].
6180 getService(Ci.nsIBlocklistService);
6181 return bs.getAddonBlocklistURL(createWrapper(this));
6182 },
6183
6184 applyCompatibilityUpdate: function AddonInternal_applyCompatibilityUpdate(aUpdate, aSyncCompatibility) {
6185 this.targetApplications.forEach(function(aTargetApp) {
6186 aUpdate.targetApplications.forEach(function(aUpdateTarget) {
6187 if (aTargetApp.id == aUpdateTarget.id && (aSyncCompatibility ||
6188 Services.vc.compare(aTargetApp.maxVersion, aUpdateTarget.maxVersion) < 0)) {
6189 aTargetApp.minVersion = aUpdateTarget.minVersion;
6190 aTargetApp.maxVersion = aUpdateTarget.maxVersion;
6191 }
6192 });
6193 });
6194 this.appDisabled = !isUsableAddon(this);
6195 },
6196
6197 /**
6198 * toJSON is called by JSON.stringify in order to create a filtered version
6199 * of this object to be serialized to a JSON file. A new object is returned
6200 * with copies of all non-private properties. Functions, getters and setters
6201 * are not copied.
6202 *
6203 * @param aKey
6204 * The key that this object is being serialized as in the JSON.
6205 * Unused here since this is always the main object serialized
6206 *
6207 * @return an object containing copies of the properties of this object
6208 * ignoring private properties, functions, getters and setters
6209 */
6210 toJSON: function AddonInternal_toJSON(aKey) {
6211 let obj = {};
6212 for (let prop in this) {
6213 // Ignore private properties
6214 if (prop.substring(0, 1) == "_")
6215 continue;
6216
6217 // Ignore getters
6218 if (this.__lookupGetter__(prop))
6219 continue;
6220
6221 // Ignore setters
6222 if (this.__lookupSetter__(prop))
6223 continue;
6224
6225 // Ignore functions
6226 if (typeof this[prop] == "function")
6227 continue;
6228
6229 obj[prop] = this[prop];
6230 }
6231
6232 return obj;
6233 },
6234
6235 /**
6236 * When an add-on install is pending its metadata will be cached in a file.
6237 * This method reads particular properties of that metadata that may be newer
6238 * than that in the install manifest, like compatibility information.
6239 *
6240 * @param aObj
6241 * A JS object containing the cached metadata
6242 */
6243 importMetadata: function AddonInternal_importMetaData(aObj) {
6244 PENDING_INSTALL_METADATA.forEach(function(aProp) {
6245 if (!(aProp in aObj))
6246 return;
6247
6248 this[aProp] = aObj[aProp];
6249 }, this);
6250
6251 // Compatibility info may have changed so update appDisabled
6252 this.appDisabled = !isUsableAddon(this);
6253 }
6254 };
6255
6256 /**
6257 * Creates an AddonWrapper for an AddonInternal.
6258 *
6259 * @param addon
6260 * The AddonInternal to wrap
6261 * @return an AddonWrapper or null if addon was null
6262 */
6263 function createWrapper(aAddon) {
6264 if (!aAddon)
6265 return null;
6266 if (!aAddon._wrapper) {
6267 aAddon._hasResourceCache = new Map();
6268 aAddon._wrapper = new AddonWrapper(aAddon);
6269 }
6270 return aAddon._wrapper;
6271 }
6272
6273 /**
6274 * The AddonWrapper wraps an Addon to provide the data visible to consumers of
6275 * the public API.
6276 */
6277 function AddonWrapper(aAddon) {
6278 #ifdef MOZ_EM_DEBUG
6279 this.__defineGetter__("__AddonInternal__", function AW_debugGetter() {
6280 return aAddon;
6281 });
6282 #endif
6283
6284 function chooseValue(aObj, aProp) {
6285 let repositoryAddon = aAddon._repositoryAddon;
6286 let objValue = aObj[aProp];
6287
6288 if (repositoryAddon && (aProp in repositoryAddon) &&
6289 (objValue === undefined || objValue === null)) {
6290 return [repositoryAddon[aProp], true];
6291 }
6292
6293 return [objValue, false];
6294 }
6295
6296 ["id", "syncGUID", "version", "type", "isCompatible", "isPlatformCompatible",
6297 "providesUpdatesSecurely", "blocklistState", "blocklistURL", "appDisabled",
6298 "softDisabled", "skinnable", "size", "foreignInstall", "hasBinaryComponents",
6299 "strictCompatibility", "compatibilityOverrides", "updateURL"].forEach(function(aProp) {
6300 this.__defineGetter__(aProp, function AddonWrapper_propertyGetter() aAddon[aProp]);
6301 }, this);
6302
6303 ["fullDescription", "developerComments", "eula", "supportURL",
6304 "contributionURL", "contributionAmount", "averageRating", "reviewCount",
6305 "reviewURL", "totalDownloads", "weeklyDownloads", "dailyUsers",
6306 "repositoryStatus"].forEach(function(aProp) {
6307 this.__defineGetter__(aProp, function AddonWrapper_repoPropertyGetter() {
6308 if (aAddon._repositoryAddon)
6309 return aAddon._repositoryAddon[aProp];
6310
6311 return null;
6312 });
6313 }, this);
6314
6315 this.__defineGetter__("aboutURL", function AddonWrapper_aboutURLGetter() {
6316 return this.isActive ? aAddon["aboutURL"] : null;
6317 });
6318
6319 ["installDate", "updateDate"].forEach(function(aProp) {
6320 this.__defineGetter__(aProp, function AddonWrapper_datePropertyGetter() new Date(aAddon[aProp]));
6321 }, this);
6322
6323 ["sourceURI", "releaseNotesURI"].forEach(function(aProp) {
6324 this.__defineGetter__(aProp, function AddonWrapper_URIPropertyGetter() {
6325 let [target, fromRepo] = chooseValue(aAddon, aProp);
6326 if (!target)
6327 return null;
6328 if (fromRepo)
6329 return target;
6330 return NetUtil.newURI(target);
6331 });
6332 }, this);
6333
6334 this.__defineGetter__("optionsURL", function AddonWrapper_optionsURLGetter() {
6335 if (this.isActive && aAddon.optionsURL)
6336 return aAddon.optionsURL;
6337
6338 if (this.isActive && this.hasResource("options.xul"))
6339 return this.getResourceURI("options.xul").spec;
6340
6341 return null;
6342 }, this);
6343
6344 this.__defineGetter__("optionsType", function AddonWrapper_optionsTypeGetter() {
6345 if (!this.isActive)
6346 return null;
6347
6348 let hasOptionsXUL = this.hasResource("options.xul");
6349 let hasOptionsURL = !!this.optionsURL;
6350
6351 if (aAddon.optionsType) {
6352 switch (parseInt(aAddon.optionsType, 10)) {
6353 case AddonManager.OPTIONS_TYPE_DIALOG:
6354 case AddonManager.OPTIONS_TYPE_TAB:
6355 return hasOptionsURL ? aAddon.optionsType : null;
6356 case AddonManager.OPTIONS_TYPE_INLINE:
6357 case AddonManager.OPTIONS_TYPE_INLINE_INFO:
6358 return (hasOptionsXUL || hasOptionsURL) ? aAddon.optionsType : null;
6359 }
6360 return null;
6361 }
6362
6363 if (hasOptionsXUL)
6364 return AddonManager.OPTIONS_TYPE_INLINE;
6365
6366 if (hasOptionsURL)
6367 return AddonManager.OPTIONS_TYPE_DIALOG;
6368
6369 return null;
6370 }, this);
6371
6372 this.__defineGetter__("iconURL", function AddonWrapper_iconURLGetter() {
6373 return this.icons[32];
6374 }, this);
6375
6376 this.__defineGetter__("icon64URL", function AddonWrapper_icon64URLGetter() {
6377 return this.icons[64];
6378 }, this);
6379
6380 this.__defineGetter__("icons", function AddonWrapper_iconsGetter() {
6381 let icons = {};
6382 if (aAddon._repositoryAddon) {
6383 for (let size in aAddon._repositoryAddon.icons) {
6384 icons[size] = aAddon._repositoryAddon.icons[size];
6385 }
6386 }
6387 if (this.isActive && aAddon.iconURL) {
6388 icons[32] = aAddon.iconURL;
6389 } else if (this.hasResource("icon.png")) {
6390 icons[32] = this.getResourceURI("icon.png").spec;
6391 }
6392 if (this.isActive && aAddon.icon64URL) {
6393 icons[64] = aAddon.icon64URL;
6394 } else if (this.hasResource("icon64.png")) {
6395 icons[64] = this.getResourceURI("icon64.png").spec;
6396 }
6397 Object.freeze(icons);
6398 return icons;
6399 }, this);
6400
6401 PROP_LOCALE_SINGLE.forEach(function(aProp) {
6402 this.__defineGetter__(aProp, function AddonWrapper_singleLocaleGetter() {
6403 // Override XPI creator if repository creator is defined
6404 if (aProp == "creator" &&
6405 aAddon._repositoryAddon && aAddon._repositoryAddon.creator) {
6406 return aAddon._repositoryAddon.creator;
6407 }
6408
6409 let result = null;
6410
6411 if (aAddon.active) {
6412 try {
6413 let pref = PREF_EM_EXTENSION_FORMAT + aAddon.id + "." + aProp;
6414 let value = Services.prefs.getComplexValue(pref,
6415 Ci.nsIPrefLocalizedString);
6416 if (value.data)
6417 result = value.data;
6418 }
6419 catch (e) {
6420 }
6421 }
6422
6423 if (result == null)
6424 [result, ] = chooseValue(aAddon.selectedLocale, aProp);
6425
6426 if (aProp == "creator")
6427 return result ? new AddonManagerPrivate.AddonAuthor(result) : null;
6428
6429 return result;
6430 });
6431 }, this);
6432
6433 PROP_LOCALE_MULTI.forEach(function(aProp) {
6434 this.__defineGetter__(aProp, function AddonWrapper_multiLocaleGetter() {
6435 let results = null;
6436 let usedRepository = false;
6437
6438 if (aAddon.active) {
6439 let pref = PREF_EM_EXTENSION_FORMAT + aAddon.id + "." +
6440 aProp.substring(0, aProp.length - 1);
6441 let list = Services.prefs.getChildList(pref, {});
6442 if (list.length > 0) {
6443 list.sort();
6444 results = [];
6445 list.forEach(function(aPref) {
6446 let value = Services.prefs.getComplexValue(aPref,
6447 Ci.nsIPrefLocalizedString);
6448 if (value.data)
6449 results.push(value.data);
6450 });
6451 }
6452 }
6453
6454 if (results == null)
6455 [results, usedRepository] = chooseValue(aAddon.selectedLocale, aProp);
6456
6457 if (results && !usedRepository) {
6458 results = results.map(function mapResult(aResult) {
6459 return new AddonManagerPrivate.AddonAuthor(aResult);
6460 });
6461 }
6462
6463 return results;
6464 });
6465 }, this);
6466
6467 this.__defineGetter__("screenshots", function AddonWrapper_screenshotsGetter() {
6468 let repositoryAddon = aAddon._repositoryAddon;
6469 if (repositoryAddon && ("screenshots" in repositoryAddon)) {
6470 let repositoryScreenshots = repositoryAddon.screenshots;
6471 if (repositoryScreenshots && repositoryScreenshots.length > 0)
6472 return repositoryScreenshots;
6473 }
6474
6475 if (aAddon.type == "theme" && this.hasResource("preview.png")) {
6476 let url = this.getResourceURI("preview.png").spec;
6477 return [new AddonManagerPrivate.AddonScreenshot(url)];
6478 }
6479
6480 return null;
6481 });
6482
6483 this.__defineGetter__("applyBackgroundUpdates", function AddonWrapper_applyBackgroundUpdatesGetter() {
6484 return aAddon.applyBackgroundUpdates;
6485 });
6486 this.__defineSetter__("applyBackgroundUpdates", function AddonWrapper_applyBackgroundUpdatesSetter(val) {
6487 if (this.type == "experiment") {
6488 logger.warn("Setting applyBackgroundUpdates on an experiment is not supported.");
6489 return;
6490 }
6491
6492 if (val != AddonManager.AUTOUPDATE_DEFAULT &&
6493 val != AddonManager.AUTOUPDATE_DISABLE &&
6494 val != AddonManager.AUTOUPDATE_ENABLE) {
6495 val = val ? AddonManager.AUTOUPDATE_DEFAULT :
6496 AddonManager.AUTOUPDATE_DISABLE;
6497 }
6498
6499 if (val == aAddon.applyBackgroundUpdates)
6500 return val;
6501
6502 XPIDatabase.setAddonProperties(aAddon, {
6503 applyBackgroundUpdates: val
6504 });
6505 AddonManagerPrivate.callAddonListeners("onPropertyChanged", this, ["applyBackgroundUpdates"]);
6506
6507 return val;
6508 });
6509
6510 this.__defineSetter__("syncGUID", function AddonWrapper_syncGUIDGetter(val) {
6511 if (aAddon.syncGUID == val)
6512 return val;
6513
6514 if (aAddon.inDatabase)
6515 XPIDatabase.setAddonSyncGUID(aAddon, val);
6516
6517 aAddon.syncGUID = val;
6518
6519 return val;
6520 });
6521
6522 this.__defineGetter__("install", function AddonWrapper_installGetter() {
6523 if (!("_install" in aAddon) || !aAddon._install)
6524 return null;
6525 return aAddon._install.wrapper;
6526 });
6527
6528 this.__defineGetter__("pendingUpgrade", function AddonWrapper_pendingUpgradeGetter() {
6529 return createWrapper(aAddon.pendingUpgrade);
6530 });
6531
6532 this.__defineGetter__("scope", function AddonWrapper_scopeGetter() {
6533 if (aAddon._installLocation)
6534 return aAddon._installLocation.scope;
6535
6536 return AddonManager.SCOPE_PROFILE;
6537 });
6538
6539 this.__defineGetter__("pendingOperations", function AddonWrapper_pendingOperationsGetter() {
6540 let pending = 0;
6541 if (!(aAddon.inDatabase)) {
6542 // Add-on is pending install if there is no associated install (shouldn't
6543 // happen here) or if the install is in the process of or has successfully
6544 // completed the install. If an add-on is pending install then we ignore
6545 // any other pending operations.
6546 if (!aAddon._install || aAddon._install.state == AddonManager.STATE_INSTALLING ||
6547 aAddon._install.state == AddonManager.STATE_INSTALLED)
6548 return AddonManager.PENDING_INSTALL;
6549 }
6550 else if (aAddon.pendingUninstall) {
6551 // If an add-on is pending uninstall then we ignore any other pending
6552 // operations
6553 return AddonManager.PENDING_UNINSTALL;
6554 }
6555
6556 if (aAddon.active && isAddonDisabled(aAddon))
6557 pending |= AddonManager.PENDING_DISABLE;
6558 else if (!aAddon.active && !isAddonDisabled(aAddon))
6559 pending |= AddonManager.PENDING_ENABLE;
6560
6561 if (aAddon.pendingUpgrade)
6562 pending |= AddonManager.PENDING_UPGRADE;
6563
6564 return pending;
6565 });
6566
6567 this.__defineGetter__("operationsRequiringRestart", function AddonWrapper_operationsRequiringRestartGetter() {
6568 let ops = 0;
6569 if (XPIProvider.installRequiresRestart(aAddon))
6570 ops |= AddonManager.OP_NEEDS_RESTART_INSTALL;
6571 if (XPIProvider.uninstallRequiresRestart(aAddon))
6572 ops |= AddonManager.OP_NEEDS_RESTART_UNINSTALL;
6573 if (XPIProvider.enableRequiresRestart(aAddon))
6574 ops |= AddonManager.OP_NEEDS_RESTART_ENABLE;
6575 if (XPIProvider.disableRequiresRestart(aAddon))
6576 ops |= AddonManager.OP_NEEDS_RESTART_DISABLE;
6577
6578 return ops;
6579 });
6580
6581 this.__defineGetter__("isDebuggable", function AddonWrapper_isDebuggable() {
6582 return this.isActive && aAddon.bootstrap;
6583 });
6584
6585 this.__defineGetter__("permissions", function AddonWrapper_permisionsGetter() {
6586 let permissions = 0;
6587
6588 // Add-ons that aren't installed cannot be modified in any way
6589 if (!(aAddon.inDatabase))
6590 return permissions;
6591
6592 // Experiments can only be uninstalled. An uninstall reflects the user
6593 // intent of "disable this experiment." This is partially managed by the
6594 // experiments manager.
6595 if (aAddon.type == "experiment") {
6596 return AddonManager.PERM_CAN_UNINSTALL;
6597 }
6598
6599 if (!aAddon.appDisabled) {
6600 if (this.userDisabled) {
6601 permissions |= AddonManager.PERM_CAN_ENABLE;
6602 }
6603 else if (aAddon.type != "theme") {
6604 permissions |= AddonManager.PERM_CAN_DISABLE;
6605 }
6606 }
6607
6608 // Add-ons that are in locked install locations, or are pending uninstall
6609 // cannot be upgraded or uninstalled
6610 if (!aAddon._installLocation.locked && !aAddon.pendingUninstall) {
6611 // Add-ons that are installed by a file link cannot be upgraded
6612 if (!aAddon._installLocation.isLinkedAddon(aAddon.id)) {
6613 permissions |= AddonManager.PERM_CAN_UPGRADE;
6614 }
6615
6616 permissions |= AddonManager.PERM_CAN_UNINSTALL;
6617 }
6618
6619 return permissions;
6620 });
6621
6622 this.__defineGetter__("isActive", function AddonWrapper_isActiveGetter() {
6623 if (Services.appinfo.inSafeMode)
6624 return false;
6625 return aAddon.active;
6626 });
6627
6628 this.__defineGetter__("userDisabled", function AddonWrapper_userDisabledGetter() {
6629 if (XPIProvider._enabledExperiments.has(aAddon.id)) {
6630 return false;
6631 }
6632
6633 return aAddon.softDisabled || aAddon.userDisabled;
6634 });
6635 this.__defineSetter__("userDisabled", function AddonWrapper_userDisabledSetter(val) {
6636 if (val == this.userDisabled) {
6637 return val;
6638 }
6639
6640 if (aAddon.type == "experiment") {
6641 if (val) {
6642 XPIProvider._enabledExperiments.delete(aAddon.id);
6643 } else {
6644 XPIProvider._enabledExperiments.add(aAddon.id);
6645 }
6646 }
6647
6648 if (aAddon.inDatabase) {
6649 if (aAddon.type == "theme" && val) {
6650 if (aAddon.internalName == XPIProvider.defaultSkin)
6651 throw new Error("Cannot disable the default theme");
6652 XPIProvider.enableDefaultTheme();
6653 }
6654 else {
6655 XPIProvider.updateAddonDisabledState(aAddon, val);
6656 }
6657 }
6658 else {
6659 aAddon.userDisabled = val;
6660 // When enabling remove the softDisabled flag
6661 if (!val)
6662 aAddon.softDisabled = false;
6663 }
6664
6665 return val;
6666 });
6667
6668 this.__defineSetter__("softDisabled", function AddonWrapper_softDisabledSetter(val) {
6669 if (val == aAddon.softDisabled)
6670 return val;
6671
6672 if (aAddon.inDatabase) {
6673 // When softDisabling a theme just enable the active theme
6674 if (aAddon.type == "theme" && val && !aAddon.userDisabled) {
6675 if (aAddon.internalName == XPIProvider.defaultSkin)
6676 throw new Error("Cannot disable the default theme");
6677 XPIProvider.enableDefaultTheme();
6678 }
6679 else {
6680 XPIProvider.updateAddonDisabledState(aAddon, undefined, val);
6681 }
6682 }
6683 else {
6684 // Only set softDisabled if not already disabled
6685 if (!aAddon.userDisabled)
6686 aAddon.softDisabled = val;
6687 }
6688
6689 return val;
6690 });
6691
6692 this.isCompatibleWith = function AddonWrapper_isCompatiblewith(aAppVersion, aPlatformVersion) {
6693 return aAddon.isCompatibleWith(aAppVersion, aPlatformVersion);
6694 };
6695
6696 this.uninstall = function AddonWrapper_uninstall() {
6697 if (!(aAddon.inDatabase))
6698 throw new Error("Cannot uninstall an add-on that isn't installed");
6699 if (aAddon.pendingUninstall)
6700 throw new Error("Add-on is already marked to be uninstalled");
6701 XPIProvider.uninstallAddon(aAddon);
6702 };
6703
6704 this.cancelUninstall = function AddonWrapper_cancelUninstall() {
6705 if (!(aAddon.inDatabase))
6706 throw new Error("Cannot cancel uninstall for an add-on that isn't installed");
6707 if (!aAddon.pendingUninstall)
6708 throw new Error("Add-on is not marked to be uninstalled");
6709 XPIProvider.cancelUninstallAddon(aAddon);
6710 };
6711
6712 this.findUpdates = function AddonWrapper_findUpdates(aListener, aReason, aAppVersion, aPlatformVersion) {
6713 // Short-circuit updates for experiments because updates are handled
6714 // through the Experiments Manager.
6715 if (this.type == "experiment") {
6716 AddonManagerPrivate.callNoUpdateListeners(this, aListener, aReason,
6717 aAppVersion, aPlatformVersion);
6718 return;
6719 }
6720
6721 new UpdateChecker(aAddon, aListener, aReason, aAppVersion, aPlatformVersion);
6722 };
6723
6724 // Returns true if there was an update in progress, false if there was no update to cancel
6725 this.cancelUpdate = function AddonWrapper_cancelUpdate() {
6726 if (aAddon._updateCheck) {
6727 aAddon._updateCheck.cancel();
6728 return true;
6729 }
6730 return false;
6731 };
6732
6733 this.hasResource = function AddonWrapper_hasResource(aPath) {
6734 if (aAddon._hasResourceCache.has(aPath))
6735 return aAddon._hasResourceCache.get(aPath);
6736
6737 let bundle = aAddon._sourceBundle.clone();
6738
6739 // Bundle may not exist any more if the addon has just been uninstalled,
6740 // but explicitly first checking .exists() results in unneeded file I/O.
6741 try {
6742 var isDir = bundle.isDirectory();
6743 } catch (e) {
6744 aAddon._hasResourceCache.set(aPath, false);
6745 return false;
6746 }
6747
6748 if (isDir) {
6749 if (aPath) {
6750 aPath.split("/").forEach(function(aPart) {
6751 bundle.append(aPart);
6752 });
6753 }
6754 let result = bundle.exists();
6755 aAddon._hasResourceCache.set(aPath, result);
6756 return result;
6757 }
6758
6759 let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].
6760 createInstance(Ci.nsIZipReader);
6761 try {
6762 zipReader.open(bundle);
6763 let result = zipReader.hasEntry(aPath);
6764 aAddon._hasResourceCache.set(aPath, result);
6765 return result;
6766 }
6767 catch (e) {
6768 aAddon._hasResourceCache.set(aPath, false);
6769 return false;
6770 }
6771 finally {
6772 zipReader.close();
6773 }
6774 },
6775
6776 /**
6777 * Returns a URI to the selected resource or to the add-on bundle if aPath
6778 * is null. URIs to the bundle will always be file: URIs. URIs to resources
6779 * will be file: URIs if the add-on is unpacked or jar: URIs if the add-on is
6780 * still an XPI file.
6781 *
6782 * @param aPath
6783 * The path in the add-on to get the URI for or null to get a URI to
6784 * the file or directory the add-on is installed as.
6785 * @return an nsIURI
6786 */
6787 this.getResourceURI = function AddonWrapper_getResourceURI(aPath) {
6788 if (!aPath)
6789 return NetUtil.newURI(aAddon._sourceBundle);
6790
6791 return getURIForResourceInFile(aAddon._sourceBundle, aPath);
6792 }
6793 }
6794
6795 /**
6796 * An object which identifies a directory install location for add-ons. The
6797 * location consists of a directory which contains the add-ons installed in the
6798 * location.
6799 *
6800 * Each add-on installed in the location is either a directory containing the
6801 * add-on's files or a text file containing an absolute path to the directory
6802 * containing the add-ons files. The directory or text file must have the same
6803 * name as the add-on's ID.
6804 *
6805 * There may also a special directory named "staged" which can contain
6806 * directories with the same name as an add-on ID. If the directory is empty
6807 * then it means the add-on will be uninstalled from this location during the
6808 * next startup. If the directory contains the add-on's files then they will be
6809 * installed during the next startup.
6810 *
6811 * @param aName
6812 * The string identifier for the install location
6813 * @param aDirectory
6814 * The nsIFile directory for the install location
6815 * @param aScope
6816 * The scope of add-ons installed in this location
6817 * @param aLocked
6818 * true if add-ons cannot be installed, uninstalled or upgraded in the
6819 * install location
6820 */
6821 function DirectoryInstallLocation(aName, aDirectory, aScope, aLocked) {
6822 this._name = aName;
6823 this.locked = aLocked;
6824 this._directory = aDirectory;
6825 this._scope = aScope
6826 this._IDToFileMap = {};
6827 this._FileToIDMap = {};
6828 this._linkedAddons = [];
6829 this._stagingDirLock = 0;
6830
6831 if (!aDirectory.exists())
6832 return;
6833 if (!aDirectory.isDirectory())
6834 throw new Error("Location must be a directory.");
6835
6836 this._readAddons();
6837 }
6838
6839 DirectoryInstallLocation.prototype = {
6840 _name : "",
6841 _directory : null,
6842 _IDToFileMap : null, // mapping from add-on ID to nsIFile
6843 _FileToIDMap : null, // mapping from add-on path to add-on ID
6844
6845 /**
6846 * Reads a directory linked to in a file.
6847 *
6848 * @param file
6849 * The file containing the directory path
6850 * @return An nsIFile object representing the linked directory.
6851 */
6852 _readDirectoryFromFile: function DirInstallLocation__readDirectoryFromFile(aFile) {
6853 let fis = Cc["@mozilla.org/network/file-input-stream;1"].
6854 createInstance(Ci.nsIFileInputStream);
6855 fis.init(aFile, -1, -1, false);
6856 let line = { value: "" };
6857 if (fis instanceof Ci.nsILineInputStream)
6858 fis.readLine(line);
6859 fis.close();
6860 if (line.value) {
6861 let linkedDirectory = Cc["@mozilla.org/file/local;1"].
6862 createInstance(Ci.nsIFile);
6863
6864 try {
6865 linkedDirectory.initWithPath(line.value);
6866 }
6867 catch (e) {
6868 linkedDirectory.setRelativeDescriptor(aFile.parent, line.value);
6869 }
6870
6871 if (!linkedDirectory.exists()) {
6872 logger.warn("File pointer " + aFile.path + " points to " + linkedDirectory.path +
6873 " which does not exist");
6874 return null;
6875 }
6876
6877 if (!linkedDirectory.isDirectory()) {
6878 logger.warn("File pointer " + aFile.path + " points to " + linkedDirectory.path +
6879 " which is not a directory");
6880 return null;
6881 }
6882
6883 return linkedDirectory;
6884 }
6885
6886 logger.warn("File pointer " + aFile.path + " does not contain a path");
6887 return null;
6888 },
6889
6890 /**
6891 * Finds all the add-ons installed in this location.
6892 */
6893 _readAddons: function DirInstallLocation__readAddons() {
6894 // Use a snapshot of the directory contents to avoid possible issues with
6895 // iterating over a directory while removing files from it (the YAFFS2
6896 // embedded filesystem has this issue, see bug 772238).
6897 let entries = getDirectoryEntries(this._directory);
6898 for (let entry of entries) {
6899 let id = entry.leafName;
6900
6901 if (id == DIR_STAGE || id == DIR_XPI_STAGE || id == DIR_TRASH)
6902 continue;
6903
6904 let directLoad = false;
6905 if (entry.isFile() &&
6906 id.substring(id.length - 4).toLowerCase() == ".xpi") {
6907 directLoad = true;
6908 id = id.substring(0, id.length - 4);
6909 }
6910
6911 if (!gIDTest.test(id)) {
6912 logger.debug("Ignoring file entry whose name is not a valid add-on ID: " +
6913 entry.path);
6914 continue;
6915 }
6916
6917 if (entry.isFile() && !directLoad) {
6918 let newEntry = this._readDirectoryFromFile(entry);
6919 if (!newEntry) {
6920 logger.debug("Deleting stale pointer file " + entry.path);
6921 try {
6922 entry.remove(true);
6923 }
6924 catch (e) {
6925 logger.warn("Failed to remove stale pointer file " + entry.path, e);
6926 // Failing to remove the stale pointer file is ignorable
6927 }
6928 continue;
6929 }
6930
6931 entry = newEntry;
6932 this._linkedAddons.push(id);
6933 }
6934
6935 this._IDToFileMap[id] = entry;
6936 this._FileToIDMap[entry.path] = id;
6937 }
6938 },
6939
6940 /**
6941 * Gets the name of this install location.
6942 */
6943 get name() {
6944 return this._name;
6945 },
6946
6947 /**
6948 * Gets the scope of this install location.
6949 */
6950 get scope() {
6951 return this._scope;
6952 },
6953
6954 /**
6955 * Gets an array of nsIFiles for add-ons installed in this location.
6956 */
6957 get addonLocations() {
6958 let locations = [];
6959 for (let id in this._IDToFileMap) {
6960 locations.push(this._IDToFileMap[id].clone());
6961 }
6962 return locations;
6963 },
6964
6965 /**
6966 * Gets the staging directory to put add-ons that are pending install and
6967 * uninstall into.
6968 *
6969 * @return an nsIFile
6970 */
6971 getStagingDir: function DirInstallLocation_getStagingDir() {
6972 let dir = this._directory.clone();
6973 dir.append(DIR_STAGE);
6974 return dir;
6975 },
6976
6977 requestStagingDir: function() {
6978 this._stagingDirLock++;
6979
6980 if (this._stagingDirPromise)
6981 return this._stagingDirPromise;
6982
6983 OS.File.makeDir(this._directory.path);
6984 let stagepath = OS.Path.join(this._directory.path, DIR_STAGE);
6985 return this._stagingDirPromise = OS.File.makeDir(stagepath).then(null, (e) => {
6986 if (e instanceof OS.File.Error && e.becauseExists)
6987 return;
6988 logger.error("Failed to create staging directory", e);
6989 throw e;
6990 });
6991 },
6992
6993 releaseStagingDir: function() {
6994 this._stagingDirLock--;
6995
6996 if (this._stagingDirLock == 0) {
6997 this._stagingDirPromise = null;
6998 this.cleanStagingDir();
6999 }
7000
7001 return Promise.resolve();
7002 },
7003
7004 /**
7005 * Removes the specified files or directories in the staging directory and
7006 * then if the staging directory is empty attempts to remove it.
7007 *
7008 * @param aLeafNames
7009 * An array of file or directory to remove from the directory, the
7010 * array may be empty
7011 */
7012 cleanStagingDir: function(aLeafNames = []) {
7013 let dir = this.getStagingDir();
7014
7015 for (let name of aLeafNames) {
7016 let file = dir.clone();
7017 file.append(name);
7018 recursiveRemove(file);
7019 }
7020
7021 if (this._stagingDirLock > 0)
7022 return;
7023
7024 let dirEntries = dir.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
7025 try {
7026 if (dirEntries.nextFile)
7027 return;
7028 }
7029 finally {
7030 dirEntries.close();
7031 }
7032
7033 try {
7034 setFilePermissions(dir, FileUtils.PERMS_DIRECTORY);
7035 dir.remove(false);
7036 }
7037 catch (e) {
7038 logger.warn("Failed to remove staging dir", e);
7039 // Failing to remove the staging directory is ignorable
7040 }
7041 },
7042
7043 /**
7044 * Gets the directory used by old versions for staging XPI and JAR files ready
7045 * to be installed.
7046 *
7047 * @return an nsIFile
7048 */
7049 getXPIStagingDir: function DirInstallLocation_getXPIStagingDir() {
7050 let dir = this._directory.clone();
7051 dir.append(DIR_XPI_STAGE);
7052 return dir;
7053 },
7054
7055 /**
7056 * Returns a directory that is normally on the same filesystem as the rest of
7057 * the install location and can be used for temporarily storing files during
7058 * safe move operations. Calling this method will delete the existing trash
7059 * directory and its contents.
7060 *
7061 * @return an nsIFile
7062 */
7063 getTrashDir: function DirInstallLocation_getTrashDir() {
7064 let trashDir = this._directory.clone();
7065 trashDir.append(DIR_TRASH);
7066 if (trashDir.exists())
7067 recursiveRemove(trashDir);
7068 trashDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
7069 return trashDir;
7070 },
7071
7072 /**
7073 * Installs an add-on into the install location.
7074 *
7075 * @param aId
7076 * The ID of the add-on to install
7077 * @param aSource
7078 * The source nsIFile to install from
7079 * @param aExistingAddonID
7080 * The ID of an existing add-on to uninstall at the same time
7081 * @param aCopy
7082 * If false the source files will be moved to the new location,
7083 * otherwise they will only be copied
7084 * @return an nsIFile indicating where the add-on was installed to
7085 */
7086 installAddon: function DirInstallLocation_installAddon(aId, aSource,
7087 aExistingAddonID,
7088 aCopy) {
7089 let trashDir = this.getTrashDir();
7090
7091 let transaction = new SafeInstallOperation();
7092
7093 let self = this;
7094 function moveOldAddon(aId) {
7095 let file = self._directory.clone();
7096 file.append(aId);
7097
7098 if (file.exists())
7099 transaction.move(file, trashDir);
7100
7101 file = self._directory.clone();
7102 file.append(aId + ".xpi");
7103 if (file.exists()) {
7104 flushJarCache(file);
7105 transaction.move(file, trashDir);
7106 }
7107 }
7108
7109 // If any of these operations fails the finally block will clean up the
7110 // temporary directory
7111 try {
7112 moveOldAddon(aId);
7113 if (aExistingAddonID && aExistingAddonID != aId)
7114 moveOldAddon(aExistingAddonID);
7115
7116 if (aCopy) {
7117 transaction.copy(aSource, this._directory);
7118 }
7119 else {
7120 if (aSource.isFile())
7121 flushJarCache(aSource);
7122
7123 transaction.move(aSource, this._directory);
7124 }
7125 }
7126 finally {
7127 // It isn't ideal if this cleanup fails but it isn't worth rolling back
7128 // the install because of it.
7129 try {
7130 recursiveRemove(trashDir);
7131 }
7132 catch (e) {
7133 logger.warn("Failed to remove trash directory when installing " + aId, e);
7134 }
7135 }
7136
7137 let newFile = this._directory.clone();
7138 newFile.append(aSource.leafName);
7139 try {
7140 newFile.lastModifiedTime = Date.now();
7141 } catch (e) {
7142 logger.warn("failed to set lastModifiedTime on " + newFile.path, e);
7143 }
7144 this._FileToIDMap[newFile.path] = aId;
7145 this._IDToFileMap[aId] = newFile;
7146
7147 if (aExistingAddonID && aExistingAddonID != aId &&
7148 aExistingAddonID in this._IDToFileMap) {
7149 delete this._FileToIDMap[this._IDToFileMap[aExistingAddonID]];
7150 delete this._IDToFileMap[aExistingAddonID];
7151 }
7152
7153 return newFile;
7154 },
7155
7156 /**
7157 * Uninstalls an add-on from this location.
7158 *
7159 * @param aId
7160 * The ID of the add-on to uninstall
7161 * @throws if the ID does not match any of the add-ons installed
7162 */
7163 uninstallAddon: function DirInstallLocation_uninstallAddon(aId) {
7164 let file = this._IDToFileMap[aId];
7165 if (!file) {
7166 logger.warn("Attempted to remove " + aId + " from " +
7167 this._name + " but it was already gone");
7168 return;
7169 }
7170
7171 file = this._directory.clone();
7172 file.append(aId);
7173 if (!file.exists())
7174 file.leafName += ".xpi";
7175
7176 if (!file.exists()) {
7177 logger.warn("Attempted to remove " + aId + " from " +
7178 this._name + " but it was already gone");
7179
7180 delete this._FileToIDMap[file.path];
7181 delete this._IDToFileMap[aId];
7182 return;
7183 }
7184
7185 let trashDir = this.getTrashDir();
7186
7187 if (file.leafName != aId) {
7188 logger.debug("uninstallAddon: flushing jar cache " + file.path + " for addon " + aId);
7189 flushJarCache(file);
7190 }
7191
7192 let transaction = new SafeInstallOperation();
7193
7194 try {
7195 transaction.move(file, trashDir);
7196 }
7197 finally {
7198 // It isn't ideal if this cleanup fails, but it is probably better than
7199 // rolling back the uninstall at this point
7200 try {
7201 recursiveRemove(trashDir);
7202 }
7203 catch (e) {
7204 logger.warn("Failed to remove trash directory when uninstalling " + aId, e);
7205 }
7206 }
7207
7208 delete this._FileToIDMap[file.path];
7209 delete this._IDToFileMap[aId];
7210 },
7211
7212 /**
7213 * Gets the ID of the add-on installed in the given nsIFile.
7214 *
7215 * @param aFile
7216 * The nsIFile to look in
7217 * @return the ID
7218 * @throws if the file does not represent an installed add-on
7219 */
7220 getIDForLocation: function DirInstallLocation_getIDForLocation(aFile) {
7221 if (aFile.path in this._FileToIDMap)
7222 return this._FileToIDMap[aFile.path];
7223 throw new Error("Unknown add-on location " + aFile.path);
7224 },
7225
7226 /**
7227 * Gets the directory that the add-on with the given ID is installed in.
7228 *
7229 * @param aId
7230 * The ID of the add-on
7231 * @return The nsIFile
7232 * @throws if the ID does not match any of the add-ons installed
7233 */
7234 getLocationForID: function DirInstallLocation_getLocationForID(aId) {
7235 if (aId in this._IDToFileMap)
7236 return this._IDToFileMap[aId].clone();
7237 throw new Error("Unknown add-on ID " + aId);
7238 },
7239
7240 /**
7241 * Returns true if the given addon was installed in this location by a text
7242 * file pointing to its real path.
7243 *
7244 * @param aId
7245 * The ID of the addon
7246 */
7247 isLinkedAddon: function DirInstallLocation__isLinkedAddon(aId) {
7248 return this._linkedAddons.indexOf(aId) != -1;
7249 }
7250 };
7251
7252 #ifdef XP_WIN
7253 /**
7254 * An object that identifies a registry install location for add-ons. The location
7255 * consists of a registry key which contains string values mapping ID to the
7256 * path where an add-on is installed
7257 *
7258 * @param aName
7259 * The string identifier of this Install Location.
7260 * @param aRootKey
7261 * The root key (one of the ROOT_KEY_ values from nsIWindowsRegKey).
7262 * @param scope
7263 * The scope of add-ons installed in this location
7264 */
7265 function WinRegInstallLocation(aName, aRootKey, aScope) {
7266 this.locked = true;
7267 this._name = aName;
7268 this._rootKey = aRootKey;
7269 this._scope = aScope;
7270 this._IDToFileMap = {};
7271 this._FileToIDMap = {};
7272
7273 let path = this._appKeyPath + "\\Extensions";
7274 let key = Cc["@mozilla.org/windows-registry-key;1"].
7275 createInstance(Ci.nsIWindowsRegKey);
7276
7277 // Reading the registry may throw an exception, and that's ok. In error
7278 // cases, we just leave ourselves in the empty state.
7279 try {
7280 key.open(this._rootKey, path, Ci.nsIWindowsRegKey.ACCESS_READ);
7281 }
7282 catch (e) {
7283 return;
7284 }
7285
7286 this._readAddons(key);
7287 key.close();
7288 }
7289
7290 WinRegInstallLocation.prototype = {
7291 _name : "",
7292 _rootKey : null,
7293 _scope : null,
7294 _IDToFileMap : null, // mapping from ID to nsIFile
7295 _FileToIDMap : null, // mapping from path to ID
7296
7297 /**
7298 * Retrieves the path of this Application's data key in the registry.
7299 */
7300 get _appKeyPath() {
7301 let appVendor = Services.appinfo.vendor;
7302 let appName = Services.appinfo.name;
7303
7304 #ifdef MOZ_THUNDERBIRD
7305 // XXX Thunderbird doesn't specify a vendor string
7306 if (appVendor == "")
7307 appVendor = "Mozilla";
7308 #endif
7309
7310 // XULRunner-based apps may intentionally not specify a vendor
7311 if (appVendor != "")
7312 appVendor += "\\";
7313
7314 return "SOFTWARE\\" + appVendor + appName;
7315 },
7316
7317 /**
7318 * Read the registry and build a mapping between ID and path for each
7319 * installed add-on.
7320 *
7321 * @param key
7322 * The key that contains the ID to path mapping
7323 */
7324 _readAddons: function RegInstallLocation__readAddons(aKey) {
7325 let count = aKey.valueCount;
7326 for (let i = 0; i < count; ++i) {
7327 let id = aKey.getValueName(i);
7328
7329 let file = Cc["@mozilla.org/file/local;1"].
7330 createInstance(Ci.nsIFile);
7331 file.initWithPath(aKey.readStringValue(id));
7332
7333 if (!file.exists()) {
7334 logger.warn("Ignoring missing add-on in " + file.path);
7335 continue;
7336 }
7337
7338 this._IDToFileMap[id] = file;
7339 this._FileToIDMap[file.path] = id;
7340 }
7341 },
7342
7343 /**
7344 * Gets the name of this install location.
7345 */
7346 get name() {
7347 return this._name;
7348 },
7349
7350 /**
7351 * Gets the scope of this install location.
7352 */
7353 get scope() {
7354 return this._scope;
7355 },
7356
7357 /**
7358 * Gets an array of nsIFiles for add-ons installed in this location.
7359 */
7360 get addonLocations() {
7361 let locations = [];
7362 for (let id in this._IDToFileMap) {
7363 locations.push(this._IDToFileMap[id].clone());
7364 }
7365 return locations;
7366 },
7367
7368 /**
7369 * Gets the ID of the add-on installed in the given nsIFile.
7370 *
7371 * @param aFile
7372 * The nsIFile to look in
7373 * @return the ID
7374 * @throws if the file does not represent an installed add-on
7375 */
7376 getIDForLocation: function RegInstallLocation_getIDForLocation(aFile) {
7377 if (aFile.path in this._FileToIDMap)
7378 return this._FileToIDMap[aFile.path];
7379 throw new Error("Unknown add-on location");
7380 },
7381
7382 /**
7383 * Gets the nsIFile that the add-on with the given ID is installed in.
7384 *
7385 * @param aId
7386 * The ID of the add-on
7387 * @return the nsIFile
7388 */
7389 getLocationForID: function RegInstallLocation_getLocationForID(aId) {
7390 if (aId in this._IDToFileMap)
7391 return this._IDToFileMap[aId].clone();
7392 throw new Error("Unknown add-on ID");
7393 },
7394
7395 /**
7396 * @see DirectoryInstallLocation
7397 */
7398 isLinkedAddon: function RegInstallLocation_isLinkedAddon(aId) {
7399 return true;
7400 }
7401 };
7402 #endif
7403
7404 let addonTypes = [
7405 new AddonManagerPrivate.AddonType("extension", URI_EXTENSION_STRINGS,
7406 STRING_TYPE_NAME,
7407 AddonManager.VIEW_TYPE_LIST, 4000),
7408 new AddonManagerPrivate.AddonType("theme", URI_EXTENSION_STRINGS,
7409 STRING_TYPE_NAME,
7410 AddonManager.VIEW_TYPE_LIST, 5000),
7411 new AddonManagerPrivate.AddonType("dictionary", URI_EXTENSION_STRINGS,
7412 STRING_TYPE_NAME,
7413 AddonManager.VIEW_TYPE_LIST, 7000,
7414 AddonManager.TYPE_UI_HIDE_EMPTY),
7415 new AddonManagerPrivate.AddonType("locale", URI_EXTENSION_STRINGS,
7416 STRING_TYPE_NAME,
7417 AddonManager.VIEW_TYPE_LIST, 8000,
7418 AddonManager.TYPE_UI_HIDE_EMPTY),
7419 ];
7420
7421 // We only register experiments support if the application supports them.
7422 // Ideally, we would install an observer to watch the pref. Installing
7423 // an observer for this pref is not necessary here and may be buggy with
7424 // regards to registering this XPIProvider twice.
7425 if (Prefs.getBoolPref("experiments.supported", false)) {
7426 addonTypes.push(
7427 new AddonManagerPrivate.AddonType("experiment",
7428 URI_EXTENSION_STRINGS,
7429 STRING_TYPE_NAME,
7430 AddonManager.VIEW_TYPE_LIST, 11000,
7431 AddonManager.TYPE_UI_HIDE_EMPTY));
7432 }
7433
7434 AddonManagerPrivate.registerProvider(XPIProvider, addonTypes);

mercurial