|
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); |