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 7const Cc = Components.classes; 8const Ci = Components.interfaces; 9const Cr = Components.results; 10const Cu = Components.utils; 11 12this.EXPORTED_SYMBOLS = ["XPIProvider"]; 13 14const CONSTANTS = {}; 15Cu.import("resource://gre/modules/addons/AddonConstants.jsm", CONSTANTS); 16const { ADDON_SIGNING, REQUIRE_SIGNING } = CONSTANTS 17 18Cu.import("resource://gre/modules/Services.jsm"); 19Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 20Cu.import("resource://gre/modules/AddonManager.jsm"); 21Cu.import("resource://gre/modules/Preferences.jsm"); 22 23XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository", 24 "resource://gre/modules/addons/AddonRepository.jsm"); 25XPCOMUtils.defineLazyModuleGetter(this, "ChromeManifestParser", 26 "resource://gre/modules/ChromeManifestParser.jsm"); 27XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager", 28 "resource://gre/modules/LightweightThemeManager.jsm"); 29XPCOMUtils.defineLazyModuleGetter(this, "ExtensionData", 30 "resource://gre/modules/Extension.jsm"); 31XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement", 32 "resource://gre/modules/ExtensionManagement.jsm"); 33XPCOMUtils.defineLazyModuleGetter(this, "Locale", 34 "resource://gre/modules/Locale.jsm"); 35XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", 36 "resource://gre/modules/FileUtils.jsm"); 37XPCOMUtils.defineLazyModuleGetter(this, "ZipUtils", 38 "resource://gre/modules/ZipUtils.jsm"); 39XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", 40 "resource://gre/modules/NetUtil.jsm"); 41XPCOMUtils.defineLazyModuleGetter(this, "PermissionsUtils", 42 "resource://gre/modules/PermissionsUtils.jsm"); 43XPCOMUtils.defineLazyModuleGetter(this, "Promise", 44 "resource://gre/modules/Promise.jsm"); 45XPCOMUtils.defineLazyModuleGetter(this, "Task", 46 "resource://gre/modules/Task.jsm"); 47XPCOMUtils.defineLazyModuleGetter(this, "OS", 48 "resource://gre/modules/osfile.jsm"); 49XPCOMUtils.defineLazyModuleGetter(this, "BrowserToolboxProcess", 50 "resource://devtools/client/framework/ToolboxProcess.jsm"); 51XPCOMUtils.defineLazyModuleGetter(this, "ConsoleAPI", 52 "resource://gre/modules/Console.jsm"); 53XPCOMUtils.defineLazyModuleGetter(this, "ProductAddonChecker", 54 "resource://gre/modules/addons/ProductAddonChecker.jsm"); 55XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils", 56 "resource://gre/modules/UpdateUtils.jsm"); 57XPCOMUtils.defineLazyModuleGetter(this, "AppConstants", 58 "resource://gre/modules/AppConstants.jsm"); 59XPCOMUtils.defineLazyModuleGetter(this, "isAddonPartOfE10SRollout", 60 "resource://gre/modules/addons/E10SAddonsRollout.jsm"); 61XPCOMUtils.defineLazyModuleGetter(this, "LegacyExtensionsUtils", 62 "resource://gre/modules/LegacyExtensionsUtils.jsm"); 63 64XPCOMUtils.defineLazyServiceGetter(this, "Blocklist", 65 "@mozilla.org/extensions/blocklist;1", 66 Ci.nsIBlocklistService); 67XPCOMUtils.defineLazyServiceGetter(this, 68 "ChromeRegistry", 69 "@mozilla.org/chrome/chrome-registry;1", 70 "nsIChromeRegistry"); 71XPCOMUtils.defineLazyServiceGetter(this, 72 "ResProtocolHandler", 73 "@mozilla.org/network/protocol;1?name=resource", 74 "nsIResProtocolHandler"); 75XPCOMUtils.defineLazyServiceGetter(this, 76 "AddonPolicyService", 77 "@mozilla.org/addons/policy-service;1", 78 "nsIAddonPolicyService"); 79XPCOMUtils.defineLazyServiceGetter(this, 80 "AddonPathService", 81 "@mozilla.org/addon-path-service;1", 82 "amIAddonPathService"); 83 84XPCOMUtils.defineLazyGetter(this, "CertUtils", function() { 85 let certUtils = {}; 86 Components.utils.import("resource://gre/modules/CertUtils.jsm", certUtils); 87 return certUtils; 88}); 89 90Cu.importGlobalProperties(["URL"]); 91 92const nsIFile = Components.Constructor("@mozilla.org/file/local;1", "nsIFile", 93 "initWithPath"); 94 95const PREF_DB_SCHEMA = "extensions.databaseSchema"; 96const PREF_INSTALL_CACHE = "extensions.installCache"; 97const PREF_XPI_STATE = "extensions.xpiState"; 98const PREF_BOOTSTRAP_ADDONS = "extensions.bootstrappedAddons"; 99const PREF_PENDING_OPERATIONS = "extensions.pendingOperations"; 100const PREF_EM_DSS_ENABLED = "extensions.dss.enabled"; 101const PREF_DSS_SWITCHPENDING = "extensions.dss.switchPending"; 102const PREF_DSS_SKIN_TO_SELECT = "extensions.lastSelectedSkin"; 103const PREF_GENERAL_SKINS_SELECTEDSKIN = "general.skins.selectedSkin"; 104const PREF_EM_UPDATE_URL = "extensions.update.url"; 105const PREF_EM_UPDATE_BACKGROUND_URL = "extensions.update.background.url"; 106const PREF_EM_ENABLED_ADDONS = "extensions.enabledAddons"; 107const PREF_EM_EXTENSION_FORMAT = "extensions."; 108const PREF_EM_ENABLED_SCOPES = "extensions.enabledScopes"; 109const PREF_EM_SHOW_MISMATCH_UI = "extensions.showMismatchUI"; 110const PREF_XPI_ENABLED = "xpinstall.enabled"; 111const PREF_XPI_WHITELIST_REQUIRED = "xpinstall.whitelist.required"; 112const PREF_XPI_DIRECT_WHITELISTED = "xpinstall.whitelist.directRequest"; 113const PREF_XPI_FILE_WHITELISTED = "xpinstall.whitelist.fileRequest"; 114// xpinstall.signatures.required only supported in dev builds 115const PREF_XPI_SIGNATURES_REQUIRED = "xpinstall.signatures.required"; 116const PREF_XPI_SIGNATURES_DEV_ROOT = "xpinstall.signatures.dev-root"; 117const PREF_XPI_PERMISSIONS_BRANCH = "xpinstall."; 118const PREF_XPI_UNPACK = "extensions.alwaysUnpack"; 119const PREF_INSTALL_REQUIREBUILTINCERTS = "extensions.install.requireBuiltInCerts"; 120const PREF_INSTALL_REQUIRESECUREORIGIN = "extensions.install.requireSecureOrigin"; 121const PREF_INSTALL_DISTRO_ADDONS = "extensions.installDistroAddons"; 122const PREF_BRANCH_INSTALLED_ADDON = "extensions.installedDistroAddon."; 123const PREF_INTERPOSITION_ENABLED = "extensions.interposition.enabled"; 124const PREF_SYSTEM_ADDON_SET = "extensions.systemAddonSet"; 125const PREF_SYSTEM_ADDON_UPDATE_URL = "extensions.systemAddon.update.url"; 126const PREF_E10S_BLOCK_ENABLE = "extensions.e10sBlocksEnabling"; 127const PREF_E10S_ADDON_BLOCKLIST = "extensions.e10s.rollout.blocklist"; 128const PREF_E10S_ADDON_POLICY = "extensions.e10s.rollout.policy"; 129const PREF_E10S_HAS_NONEXEMPT_ADDON = "extensions.e10s.rollout.hasAddon"; 130 131const PREF_EM_MIN_COMPAT_APP_VERSION = "extensions.minCompatibleAppVersion"; 132const PREF_EM_MIN_COMPAT_PLATFORM_VERSION = "extensions.minCompatiblePlatformVersion"; 133 134const PREF_CHECKCOMAT_THEMEOVERRIDE = "extensions.checkCompatibility.temporaryThemeOverride_minAppVersion"; 135 136const PREF_EM_HOTFIX_ID = "extensions.hotfix.id"; 137const PREF_EM_CERT_CHECKATTRIBUTES = "extensions.hotfix.cert.checkAttributes"; 138const PREF_EM_HOTFIX_CERTS = "extensions.hotfix.certs."; 139 140const URI_EXTENSION_UPDATE_DIALOG = "chrome://mozapps/content/extensions/update.xul"; 141const URI_EXTENSION_STRINGS = "chrome://mozapps/locale/extensions/extensions.properties"; 142 143const STRING_TYPE_NAME = "type.%ID%.name"; 144 145const DIR_EXTENSIONS = "extensions"; 146const DIR_SYSTEM_ADDONS = "features"; 147const DIR_STAGE = "staged"; 148const DIR_TRASH = "trash"; 149 150const FILE_DATABASE = "extensions.json"; 151const FILE_OLD_CACHE = "extensions.cache"; 152const FILE_RDF_MANIFEST = "install.rdf"; 153const FILE_WEB_MANIFEST = "manifest.json"; 154const FILE_XPI_ADDONS_LIST = "extensions.ini"; 155 156const KEY_PROFILEDIR = "ProfD"; 157const KEY_ADDON_APP_DIR = "XREAddonAppDir"; 158const KEY_TEMPDIR = "TmpD"; 159const KEY_APP_DISTRIBUTION = "XREAppDist"; 160const KEY_APP_FEATURES = "XREAppFeat"; 161 162const KEY_APP_PROFILE = "app-profile"; 163const KEY_APP_SYSTEM_ADDONS = "app-system-addons"; 164const KEY_APP_SYSTEM_DEFAULTS = "app-system-defaults"; 165const KEY_APP_GLOBAL = "app-global"; 166const KEY_APP_SYSTEM_LOCAL = "app-system-local"; 167const KEY_APP_SYSTEM_SHARE = "app-system-share"; 168const KEY_APP_SYSTEM_USER = "app-system-user"; 169const KEY_APP_TEMPORARY = "app-temporary"; 170 171const NOTIFICATION_FLUSH_PERMISSIONS = "flush-pending-permissions"; 172const XPI_PERMISSION = "install"; 173 174const RDFURI_INSTALL_MANIFEST_ROOT = "urn:mozilla:install-manifest"; 175const PREFIX_NS_EM = "http://www.mozilla.org/2004/em-rdf#"; 176 177const TOOLKIT_ID = "toolkit@mozilla.org"; 178 179const XPI_SIGNATURE_CHECK_PERIOD = 24 * 60 * 60; 180 181XPCOMUtils.defineConstant(this, "DB_SCHEMA", 19); 182 183const NOTIFICATION_TOOLBOXPROCESS_LOADED = "ToolboxProcessLoaded"; 184 185// Properties that exist in the install manifest 186const PROP_METADATA = ["id", "version", "type", "internalName", "updateURL", 187 "updateKey", "optionsURL", "optionsType", "aboutURL", 188 "iconURL", "icon64URL"]; 189const PROP_LOCALE_SINGLE = ["name", "description", "creator", "homepageURL"]; 190const PROP_LOCALE_MULTI = ["developers", "translators", "contributors"]; 191const PROP_TARGETAPP = ["id", "minVersion", "maxVersion"]; 192 193// Properties to cache and reload when an addon installation is pending 194const PENDING_INSTALL_METADATA = 195 ["syncGUID", "targetApplications", "userDisabled", "softDisabled", 196 "existingAddonID", "sourceURI", "releaseNotesURI", "installDate", 197 "updateDate", "applyBackgroundUpdates", "compatibilityOverrides"]; 198 199// Note: When adding/changing/removing items here, remember to change the 200// DB schema version to ensure changes are picked up ASAP. 201const STATIC_BLOCKLIST_PATTERNS = [ 202 { creator: "Mozilla Corp.", 203 level: Blocklist.STATE_BLOCKED, 204 blockID: "i162" }, 205 { creator: "Mozilla.org", 206 level: Blocklist.STATE_BLOCKED, 207 blockID: "i162" } 208]; 209 210 211const BOOTSTRAP_REASONS = { 212 APP_STARTUP : 1, 213 APP_SHUTDOWN : 2, 214 ADDON_ENABLE : 3, 215 ADDON_DISABLE : 4, 216 ADDON_INSTALL : 5, 217 ADDON_UNINSTALL : 6, 218 ADDON_UPGRADE : 7, 219 ADDON_DOWNGRADE : 8 220}; 221 222// Map new string type identifiers to old style nsIUpdateItem types 223const TYPES = { 224 extension: 2, 225 theme: 4, 226 locale: 8, 227 multipackage: 32, 228 dictionary: 64, 229 experiment: 128, 230}; 231 232if (!AppConstants.RELEASE_OR_BETA) 233 TYPES.apiextension = 256; 234 235// Some add-on types that we track internally are presented as other types 236// externally 237const TYPE_ALIASES = { 238 "webextension": "extension", 239 "apiextension": "extension", 240}; 241 242const CHROME_TYPES = new Set([ 243 "extension", 244 "locale", 245 "experiment", 246]); 247 248const RESTARTLESS_TYPES = new Set([ 249 "webextension", 250 "dictionary", 251 "experiment", 252 "locale", 253 "apiextension", 254]); 255 256const SIGNED_TYPES = new Set([ 257 "webextension", 258 "extension", 259 "experiment", 260 "apiextension", 261]); 262 263// This is a random number array that can be used as "salt" when generating 264// an automatic ID based on the directory path of an add-on. It will prevent 265// someone from creating an ID for a permanent add-on that could be replaced 266// by a temporary add-on (because that would be confusing, I guess). 267const TEMP_INSTALL_ID_GEN_SESSION = 268 new Uint8Array(Float64Array.of(Math.random()).buffer); 269 270// Whether add-on signing is required. 271function mustSign(aType) { 272 if (!SIGNED_TYPES.has(aType)) 273 return false; 274 return REQUIRE_SIGNING || Preferences.get(PREF_XPI_SIGNATURES_REQUIRED, false); 275} 276 277// Keep track of where we are in startup for telemetry 278// event happened during XPIDatabase.startup() 279const XPI_STARTING = "XPIStarting"; 280// event happened after startup() but before the final-ui-startup event 281const XPI_BEFORE_UI_STARTUP = "BeforeFinalUIStartup"; 282// event happened after final-ui-startup 283const XPI_AFTER_UI_STARTUP = "AfterFinalUIStartup"; 284 285const COMPATIBLE_BY_DEFAULT_TYPES = { 286 extension: true, 287 dictionary: true 288}; 289 290const MSG_JAR_FLUSH = "AddonJarFlush"; 291const MSG_MESSAGE_MANAGER_CACHES_FLUSH = "AddonMessageManagerCachesFlush"; 292 293var gGlobalScope = this; 294 295/** 296 * Valid IDs fit this pattern. 297 */ 298var 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; 299 300Cu.import("resource://gre/modules/Log.jsm"); 301const LOGGER_ID = "addons.xpi"; 302 303// Create a new logger for use by all objects in this Addons XPI Provider module 304// (Requires AddonManager.jsm) 305var logger = Log.repository.getLogger(LOGGER_ID); 306 307const LAZY_OBJECTS = ["XPIDatabase", "XPIDatabaseReconcile"]; 308/* globals XPIDatabase, XPIDatabaseReconcile*/ 309 310var gLazyObjectsLoaded = false; 311 312function loadLazyObjects() { 313 let uri = "resource://gre/modules/addons/XPIProviderUtils.js"; 314 let scope = Cu.Sandbox(Services.scriptSecurityManager.getSystemPrincipal(), { 315 sandboxName: uri, 316 wantGlobalProperties: ["TextDecoder"], 317 }); 318 319 let shared = { 320 ADDON_SIGNING, 321 SIGNED_TYPES, 322 BOOTSTRAP_REASONS, 323 DB_SCHEMA, 324 AddonInternal, 325 XPIProvider, 326 XPIStates, 327 syncLoadManifestFromFile, 328 isUsableAddon, 329 recordAddonTelemetry, 330 applyBlocklistChanges, 331 flushChromeCaches, 332 canRunInSafeMode, 333 } 334 335 for (let key of Object.keys(shared)) 336 scope[key] = shared[key]; 337 338 Services.scriptloader.loadSubScript(uri, scope); 339 340 for (let name of LAZY_OBJECTS) { 341 delete gGlobalScope[name]; 342 gGlobalScope[name] = scope[name]; 343 } 344 gLazyObjectsLoaded = true; 345 return scope; 346} 347 348LAZY_OBJECTS.forEach(name => { 349 Object.defineProperty(gGlobalScope, name, { 350 get: function() { 351 let objs = loadLazyObjects(); 352 return objs[name]; 353 }, 354 configurable: true 355 }); 356}); 357 358 359// Behaves like Promise.all except waits for all promises to resolve/reject 360// before resolving/rejecting itself 361function waitForAllPromises(promises) { 362 return new Promise((resolve, reject) => { 363 let shouldReject = false; 364 let rejectValue = null; 365 366 let newPromises = promises.map( 367 p => p.catch(value => { 368 shouldReject = true; 369 rejectValue = value; 370 }) 371 ); 372 Promise.all(newPromises) 373 .then((results) => shouldReject ? reject(rejectValue) : resolve(results)); 374 }); 375} 376 377function findMatchingStaticBlocklistItem(aAddon) { 378 for (let item of STATIC_BLOCKLIST_PATTERNS) { 379 if ("creator" in item && typeof item.creator == "string") { 380 if ((aAddon.defaultLocale && aAddon.defaultLocale.creator == item.creator) || 381 (aAddon.selectedLocale && aAddon.selectedLocale.creator == item.creator)) { 382 return item; 383 } 384 } 385 } 386 return null; 387} 388 389/** 390 * Converts an iterable of addon objects into a map with the add-on's ID as key. 391 */ 392function addonMap(addons) { 393 return new Map(addons.map(a => [a.id, a])); 394} 395 396/** 397 * Sets permissions on a file 398 * 399 * @param aFile 400 * The file or directory to operate on. 401 * @param aPermissions 402 * The permisions to set 403 */ 404function setFilePermissions(aFile, aPermissions) { 405 try { 406 aFile.permissions = aPermissions; 407 } 408 catch (e) { 409 logger.warn("Failed to set permissions " + aPermissions.toString(8) + " on " + 410 aFile.path, e); 411 } 412} 413 414/** 415 * Write a given string to a file 416 * 417 * @param file 418 * The nsIFile instance to write into 419 * @param string 420 * The string to write 421 */ 422function writeStringToFile(file, string) { 423 let stream = Cc["@mozilla.org/network/file-output-stream;1"]. 424 createInstance(Ci.nsIFileOutputStream); 425 let converter = Cc["@mozilla.org/intl/converter-output-stream;1"]. 426 createInstance(Ci.nsIConverterOutputStream); 427 428 try { 429 stream.init(file, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | 430 FileUtils.MODE_TRUNCATE, FileUtils.PERMS_FILE, 431 0); 432 converter.init(stream, "UTF-8", 0, 0x0000); 433 converter.writeString(string); 434 } 435 finally { 436 converter.close(); 437 stream.close(); 438 } 439} 440 441/** 442 * A safe way to install a file or the contents of a directory to a new 443 * directory. The file or directory is moved or copied recursively and if 444 * anything fails an attempt is made to rollback the entire operation. The 445 * operation may also be rolled back to its original state after it has 446 * completed by calling the rollback method. 447 * 448 * Operations can be chained. Calling move or copy multiple times will remember 449 * the whole set and if one fails all of the operations will be rolled back. 450 */ 451function SafeInstallOperation() { 452 this._installedFiles = []; 453 this._createdDirs = []; 454} 455 456SafeInstallOperation.prototype = { 457 _installedFiles: null, 458 _createdDirs: null, 459 460 _installFile: function(aFile, aTargetDirectory, aCopy) { 461 let oldFile = aCopy ? null : aFile.clone(); 462 let newFile = aFile.clone(); 463 try { 464 if (aCopy) { 465 newFile.copyTo(aTargetDirectory, null); 466 // copyTo does not update the nsIFile with the new. 467 newFile = aTargetDirectory.clone(); 468 newFile.append(aFile.leafName); 469 // Windows roaming profiles won't properly sync directories if a new file 470 // has an older lastModifiedTime than a previous file, so update. 471 newFile.lastModifiedTime = Date.now(); 472 } 473 else { 474 newFile.moveTo(aTargetDirectory, null); 475 } 476 } 477 catch (e) { 478 logger.error("Failed to " + (aCopy ? "copy" : "move") + " file " + aFile.path + 479 " to " + aTargetDirectory.path, e); 480 throw e; 481 } 482 this._installedFiles.push({ oldFile: oldFile, newFile: newFile }); 483 }, 484 485 _installDirectory: function(aDirectory, aTargetDirectory, aCopy) { 486 if (aDirectory.contains(aTargetDirectory)) { 487 let err = new Error(`Not installing ${aDirectory} into its own descendent ${aTargetDirectory}`); 488 logger.error(err); 489 throw err; 490 } 491 492 let newDir = aTargetDirectory.clone(); 493 newDir.append(aDirectory.leafName); 494 try { 495 newDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); 496 } 497 catch (e) { 498 logger.error("Failed to create directory " + newDir.path, e); 499 throw e; 500 } 501 this._createdDirs.push(newDir); 502 503 // Use a snapshot of the directory contents to avoid possible issues with 504 // iterating over a directory while removing files from it (the YAFFS2 505 // embedded filesystem has this issue, see bug 772238), and to remove 506 // normal files before their resource forks on OSX (see bug 733436). 507 let entries = getDirectoryEntries(aDirectory, true); 508 for (let entry of entries) { 509 try { 510 this._installDirEntry(entry, newDir, aCopy); 511 } 512 catch (e) { 513 logger.error("Failed to " + (aCopy ? "copy" : "move") + " entry " + 514 entry.path, e); 515 throw e; 516 } 517 } 518 519 // If this is only a copy operation then there is nothing else to do 520 if (aCopy) 521 return; 522 523 // The directory should be empty by this point. If it isn't this will throw 524 // and all of the operations will be rolled back 525 try { 526 setFilePermissions(aDirectory, FileUtils.PERMS_DIRECTORY); 527 aDirectory.remove(false); 528 } 529 catch (e) { 530 logger.error("Failed to remove directory " + aDirectory.path, e); 531 throw e; 532 } 533 534 // Note we put the directory move in after all the file moves so the 535 // directory is recreated before all the files are moved back 536 this._installedFiles.push({ oldFile: aDirectory, newFile: newDir }); 537 }, 538 539 _installDirEntry: function(aDirEntry, aTargetDirectory, aCopy) { 540 let isDir = null; 541 542 try { 543 isDir = aDirEntry.isDirectory() && !aDirEntry.isSymlink(); 544 } 545 catch (e) { 546 // If the file has already gone away then don't worry about it, this can 547 // happen on OSX where the resource fork is automatically moved with the 548 // data fork for the file. See bug 733436. 549 if (e.result == Cr.NS_ERROR_FILE_TARGET_DOES_NOT_EXIST) 550 return; 551 552 logger.error("Failure " + (aCopy ? "copying" : "moving") + " " + aDirEntry.path + 553 " to " + aTargetDirectory.path); 554 throw e; 555 } 556 557 try { 558 if (isDir) 559 this._installDirectory(aDirEntry, aTargetDirectory, aCopy); 560 else 561 this._installFile(aDirEntry, aTargetDirectory, aCopy); 562 } 563 catch (e) { 564 logger.error("Failure " + (aCopy ? "copying" : "moving") + " " + aDirEntry.path + 565 " to " + aTargetDirectory.path); 566 throw e; 567 } 568 }, 569 570 /** 571 * Moves a file or directory into a new directory. If an error occurs then all 572 * files that have been moved will be moved back to their original location. 573 * 574 * @param aFile 575 * The file or directory to be moved. 576 * @param aTargetDirectory 577 * The directory to move into, this is expected to be an empty 578 * directory. 579 */ 580 moveUnder: function(aFile, aTargetDirectory) { 581 try { 582 this._installDirEntry(aFile, aTargetDirectory, false); 583 } 584 catch (e) { 585 this.rollback(); 586 throw e; 587 } 588 }, 589 590 /** 591 * Renames a file to a new location. If an error occurs then all 592 * files that have been moved will be moved back to their original location. 593 * 594 * @param aOldLocation 595 * The old location of the file. 596 * @param aNewLocation 597 * The new location of the file. 598 */ 599 moveTo: function(aOldLocation, aNewLocation) { 600 try { 601 let oldFile = aOldLocation.clone(), newFile = aNewLocation.clone(); 602 oldFile.moveTo(newFile.parent, newFile.leafName); 603 this._installedFiles.push({ oldFile: oldFile, newFile: newFile, isMoveTo: true}); 604 } 605 catch (e) { 606 this.rollback(); 607 throw e; 608 } 609 }, 610 611 /** 612 * Copies a file or directory into a new directory. If an error occurs then 613 * all new files that have been created will be removed. 614 * 615 * @param aFile 616 * The file or directory to be copied. 617 * @param aTargetDirectory 618 * The directory to copy into, this is expected to be an empty 619 * directory. 620 */ 621 copy: function(aFile, aTargetDirectory) { 622 try { 623 this._installDirEntry(aFile, aTargetDirectory, true); 624 } 625 catch (e) { 626 this.rollback(); 627 throw e; 628 } 629 }, 630 631 /** 632 * Rolls back all the moves that this operation performed. If an exception 633 * occurs here then both old and new directories are left in an indeterminate 634 * state 635 */ 636 rollback: function() { 637 while (this._installedFiles.length > 0) { 638 let move = this._installedFiles.pop(); 639 if (move.isMoveTo) { 640 move.newFile.moveTo(move.oldDir.parent, move.oldDir.leafName); 641 } 642 else if (move.newFile.isDirectory() && !move.newFile.isSymlink()) { 643 let oldDir = move.oldFile.parent.clone(); 644 oldDir.append(move.oldFile.leafName); 645 oldDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); 646 } 647 else if (!move.oldFile) { 648 // No old file means this was a copied file 649 move.newFile.remove(true); 650 } 651 else { 652 move.newFile.moveTo(move.oldFile.parent, null); 653 } 654 } 655 656 while (this._createdDirs.length > 0) 657 recursiveRemove(this._createdDirs.pop()); 658 } 659}; 660 661/** 662 * Sets the userDisabled and softDisabled properties of an add-on based on what 663 * values those properties had for a previous instance of the add-on. The 664 * previous instance may be a previous install or in the case of an application 665 * version change the same add-on. 666 * 667 * NOTE: this may modify aNewAddon in place; callers should save the database if 668 * necessary 669 * 670 * @param aOldAddon 671 * The previous instance of the add-on 672 * @param aNewAddon 673 * The new instance of the add-on 674 * @param aAppVersion 675 * The optional application version to use when checking the blocklist 676 * or undefined to use the current application 677 * @param aPlatformVersion 678 * The optional platform version to use when checking the blocklist or 679 * undefined to use the current platform 680 */ 681function applyBlocklistChanges(aOldAddon, aNewAddon, aOldAppVersion, 682 aOldPlatformVersion) { 683 // Copy the properties by default 684 aNewAddon.userDisabled = aOldAddon.userDisabled; 685 aNewAddon.softDisabled = aOldAddon.softDisabled; 686 687 let oldBlocklistState = Blocklist.getAddonBlocklistState(aOldAddon.wrapper, 688 aOldAppVersion, 689 aOldPlatformVersion); 690 let newBlocklistState = Blocklist.getAddonBlocklistState(aNewAddon.wrapper); 691 692 // If the blocklist state hasn't changed then the properties don't need to 693 // change 694 if (newBlocklistState == oldBlocklistState) 695 return; 696 697 if (newBlocklistState == Blocklist.STATE_SOFTBLOCKED) { 698 if (aNewAddon.type != "theme") { 699 // The add-on has become softblocked, set softDisabled if it isn't already 700 // userDisabled 701 aNewAddon.softDisabled = !aNewAddon.userDisabled; 702 } 703 else { 704 // Themes just get userDisabled to switch back to the default theme 705 aNewAddon.userDisabled = true; 706 } 707 } 708 else { 709 // If the new add-on is not softblocked then it cannot be softDisabled 710 aNewAddon.softDisabled = false; 711 } 712} 713 714/** 715 * Evaluates whether an add-on is allowed to run in safe mode. 716 * 717 * @param aAddon 718 * The add-on to check 719 * @return true if the add-on should run in safe mode 720 */ 721function canRunInSafeMode(aAddon) { 722 // Even though the updated system add-ons aren't generally run in safe mode we 723 // include them here so their uninstall functions get called when switching 724 // back to the default set. 725 726 // TODO product should make the call about temporary add-ons running 727 // in safe mode. assuming for now that they are. 728 if (aAddon._installLocation.name == KEY_APP_TEMPORARY) 729 return true; 730 731 return aAddon._installLocation.name == KEY_APP_SYSTEM_DEFAULTS || 732 aAddon._installLocation.name == KEY_APP_SYSTEM_ADDONS; 733} 734 735/** 736 * Calculates whether an add-on should be appDisabled or not. 737 * 738 * @param aAddon 739 * The add-on to check 740 * @return true if the add-on should not be appDisabled 741 */ 742function isUsableAddon(aAddon) { 743 // Hack to ensure the default theme is always usable 744 if (aAddon.type == "theme" && aAddon.internalName == XPIProvider.defaultSkin) 745 return true; 746 747 if (mustSign(aAddon.type) && !aAddon.isCorrectlySigned) { 748 logger.warn(`Add-on ${aAddon.id} is not correctly signed.`); 749 return false; 750 } 751 752 if (aAddon.blocklistState == Blocklist.STATE_BLOCKED) { 753 logger.warn(`Add-on ${aAddon.id} is blocklisted.`); 754 return false; 755 } 756 757 // Experiments are installed through an external mechanism that 758 // limits target audience to compatible clients. We trust it knows what 759 // it's doing and skip compatibility checks. 760 // 761 // This decision does forfeit defense in depth. If the experiments system 762 // is ever wrong about targeting an add-on to a specific application 763 // or platform, the client will likely see errors. 764 if (aAddon.type == "experiment") 765 return true; 766 767 if (AddonManager.checkUpdateSecurity && !aAddon.providesUpdatesSecurely) { 768 logger.warn(`Updates for add-on ${aAddon.id} must be provided over HTTPS.`); 769 return false; 770 } 771 772 773 if (!aAddon.isPlatformCompatible) { 774 logger.warn(`Add-on ${aAddon.id} is not compatible with platform.`); 775 return false; 776 } 777 778 if (aAddon.dependencies.length) { 779 let isActive = id => { 780 let active = XPIProvider.activeAddons.get(id); 781 return active && !active.disable; 782 }; 783 784 if (aAddon.dependencies.some(id => !isActive(id))) 785 return false; 786 } 787 788 if (AddonManager.checkCompatibility) { 789 if (!aAddon.isCompatible) { 790 logger.warn(`Add-on ${aAddon.id} is not compatible with application version.`); 791 return false; 792 } 793 } 794 else { 795 let app = aAddon.matchingTargetApplication; 796 if (!app) { 797 logger.warn(`Add-on ${aAddon.id} is not compatible with target application.`); 798 return false; 799 } 800 801 // XXX Temporary solution to let applications opt-in to make themes safer 802 // following significant UI changes even if checkCompatibility=false has 803 // been set, until we get bug 962001. 804 if (aAddon.type == "theme" && app.id == Services.appinfo.ID) { 805 try { 806 let minCompatVersion = Services.prefs.getCharPref(PREF_CHECKCOMAT_THEMEOVERRIDE); 807 if (minCompatVersion && 808 Services.vc.compare(minCompatVersion, app.maxVersion) > 0) { 809 logger.warn(`Theme ${aAddon.id} is not compatible with application version.`); 810 return false; 811 } 812 } catch (e) {} 813 } 814 } 815 816 return true; 817} 818 819XPCOMUtils.defineLazyServiceGetter(this, "gRDF", "@mozilla.org/rdf/rdf-service;1", 820 Ci.nsIRDFService); 821 822function EM_R(aProperty) { 823 return gRDF.GetResource(PREFIX_NS_EM + aProperty); 824} 825 826function createAddonDetails(id, aAddon) { 827 return { 828 id: id || aAddon.id, 829 type: aAddon.type, 830 version: aAddon.version, 831 multiprocessCompatible: aAddon.multiprocessCompatible, 832 mpcOptedOut: aAddon.mpcOptedOut, 833 runInSafeMode: aAddon.runInSafeMode, 834 dependencies: aAddon.dependencies, 835 hasEmbeddedWebExtension: aAddon.hasEmbeddedWebExtension, 836 }; 837} 838 839/** 840 * Converts an internal add-on type to the type presented through the API. 841 * 842 * @param aType 843 * The internal add-on type 844 * @return an external add-on type 845 */ 846function getExternalType(aType) { 847 if (aType in TYPE_ALIASES) 848 return TYPE_ALIASES[aType]; 849 return aType; 850} 851 852function getManifestFileForDir(aDir) { 853 let file = aDir.clone(); 854 file.append(FILE_RDF_MANIFEST); 855 if (file.exists() && file.isFile()) 856 return file; 857 file.leafName = FILE_WEB_MANIFEST; 858 if (file.exists() && file.isFile()) 859 return file; 860 return null; 861} 862 863function getManifestEntryForZipReader(aZipReader) { 864 if (aZipReader.hasEntry(FILE_RDF_MANIFEST)) 865 return FILE_RDF_MANIFEST; 866 if (aZipReader.hasEntry(FILE_WEB_MANIFEST)) 867 return FILE_WEB_MANIFEST; 868 return null; 869} 870 871/** 872 * Converts a list of API types to a list of API types and any aliases for those 873 * types. 874 * 875 * @param aTypes 876 * An array of types or null for all types 877 * @return an array of types or null for all types 878 */ 879function getAllAliasesForTypes(aTypes) { 880 if (!aTypes) 881 return null; 882 883 // Build a set of all requested types and their aliases 884 let typeset = new Set(aTypes); 885 886 for (let alias of Object.keys(TYPE_ALIASES)) { 887 // Ignore any requested internal types 888 typeset.delete(alias); 889 890 // Add any alias for the internal type 891 if (typeset.has(TYPE_ALIASES[alias])) 892 typeset.add(alias); 893 } 894 895 return [...typeset]; 896} 897 898/** 899 * Converts an RDF literal, resource or integer into a string. 900 * 901 * @param aLiteral 902 * The RDF object to convert 903 * @return a string if the object could be converted or null 904 */ 905function getRDFValue(aLiteral) { 906 if (aLiteral instanceof Ci.nsIRDFLiteral) 907 return aLiteral.Value; 908 if (aLiteral instanceof Ci.nsIRDFResource) 909 return aLiteral.Value; 910 if (aLiteral instanceof Ci.nsIRDFInt) 911 return aLiteral.Value; 912 return null; 913} 914 915/** 916 * Gets an RDF property as a string 917 * 918 * @param aDs 919 * The RDF datasource to read the property from 920 * @param aResource 921 * The RDF resource to read the property from 922 * @param aProperty 923 * The property to read 924 * @return a string if the property existed or null 925 */ 926function getRDFProperty(aDs, aResource, aProperty) { 927 return getRDFValue(aDs.GetTarget(aResource, EM_R(aProperty), true)); 928} 929 930/** 931 * Reads an AddonInternal object from a manifest stream. 932 * 933 * @param aUri 934 * A |file:| or |jar:| URL for the manifest 935 * @return an AddonInternal object 936 * @throws if the install manifest in the stream is corrupt or could not 937 * be read 938 */ 939var loadManifestFromWebManifest = Task.async(function*(aUri) { 940 // We're passed the URI for the manifest file. Get the URI for its 941 // parent directory. 942 let uri = NetUtil.newURI("./", null, aUri); 943 944 let extension = new ExtensionData(uri); 945 946 let manifest = yield extension.readManifest(); 947 948 // Read the list of available locales, and pre-load messages for 949 // all locales. 950 let locales = yield extension.initAllLocales(); 951 952 // If there were any errors loading the extension, bail out now. 953 if (extension.errors.length) 954 throw new Error("Extension is invalid"); 955 956 let bss = (manifest.browser_specific_settings && manifest.browser_specific_settings.gecko) 957 || (manifest.applications && manifest.applications.gecko) || {}; 958 if (manifest.browser_specific_settings && manifest.applications) { 959 logger.warn("Ignoring applications property in manifest"); 960 } 961 962 // A * is illegal in strict_min_version 963 if (bss.strict_min_version && bss.strict_min_version.split(".").some(part => part == "*")) { 964 throw new Error("The use of '*' in strict_min_version is invalid"); 965 } 966 967 let addon = new AddonInternal(); 968 addon.id = bss.id; 969 addon.version = manifest.version; 970 addon.type = "webextension"; 971 addon.unpack = false; 972 addon.strictCompatibility = true; 973 addon.bootstrap = true; 974 addon.hasBinaryComponents = false; 975 addon.multiprocessCompatible = true; 976 addon.internalName = null; 977 addon.updateURL = bss.update_url; 978 addon.updateKey = null; 979 addon.optionsURL = null; 980 addon.optionsType = null; 981 addon.aboutURL = null; 982 addon.dependencies = Object.freeze(Array.from(extension.dependencies)); 983 984 if (manifest.options_ui) { 985 // Store just the relative path here, the AddonWrapper getURL 986 // wrapper maps this to a full URL. 987 addon.optionsURL = manifest.options_ui.page; 988 if (manifest.options_ui.open_in_tab) 989 addon.optionsType = AddonManager.OPTIONS_TYPE_TAB; 990 else 991 addon.optionsType = AddonManager.OPTIONS_TYPE_INLINE_BROWSER; 992 } 993 994 // WebExtensions don't use iconURLs 995 addon.iconURL = null; 996 addon.icon64URL = null; 997 addon.icons = manifest.icons || {}; 998 999 addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DEFAULT; 1000 1001 function getLocale(aLocale) { 1002 // Use the raw manifest, here, since we need values with their 1003 // localization placeholders still in place. 1004 let rawManifest = extension.rawManifest; 1005 1006 // As a convenience, allow author to be set if its a string bug 1313567. 1007 let creator = typeof(rawManifest.author) === 'string' ? rawManifest.author : null; 1008 let homepageURL = rawManifest.homepage_url; 1009 1010 // Allow developer to override creator and homepage_url. 1011 if (rawManifest.developer) { 1012 if (rawManifest.developer.name) { 1013 creator = rawManifest.developer.name; 1014 } 1015 if (rawManifest.developer.url) { 1016 homepageURL = rawManifest.developer.url; 1017 } 1018 } 1019 1020 let result = { 1021 name: extension.localize(rawManifest.name, aLocale), 1022 description: extension.localize(rawManifest.description, aLocale), 1023 creator: extension.localize(creator, aLocale), 1024 homepageURL: extension.localize(homepageURL, aLocale), 1025 1026 developers: null, 1027 translators: null, 1028 contributors: null, 1029 locales: [aLocale], 1030 }; 1031 return result; 1032 } 1033 1034 addon.defaultLocale = getLocale(extension.defaultLocale); 1035 addon.locales = Array.from(locales.keys(), getLocale); 1036 1037 delete addon.defaultLocale.locales; 1038 1039 addon.targetApplications = [{ 1040 id: TOOLKIT_ID, 1041 minVersion: bss.strict_min_version, 1042 maxVersion: bss.strict_max_version, 1043 }]; 1044 1045 addon.targetPlatforms = []; 1046 addon.userDisabled = false; 1047 addon.softDisabled = addon.blocklistState == Blocklist.STATE_SOFTBLOCKED; 1048 1049 return addon; 1050}); 1051 1052/** 1053 * Reads an AddonInternal object from an RDF stream. 1054 * 1055 * @param aUri 1056 * The URI that the manifest is being read from 1057 * @param aStream 1058 * An open stream to read the RDF from 1059 * @return an AddonInternal object 1060 * @throws if the install manifest in the RDF stream is corrupt or could not 1061 * be read 1062 */ 1063let loadManifestFromRDF = Task.async(function*(aUri, aStream) { 1064 function getPropertyArray(aDs, aSource, aProperty) { 1065 let values = []; 1066 let targets = aDs.GetTargets(aSource, EM_R(aProperty), true); 1067 while (targets.hasMoreElements()) 1068 values.push(getRDFValue(targets.getNext())); 1069 1070 return values; 1071 } 1072 1073 /** 1074 * Reads locale properties from either the main install manifest root or 1075 * an em:localized section in the install manifest. 1076 * 1077 * @param aDs 1078 * The nsIRDFDatasource to read from 1079 * @param aSource 1080 * The nsIRDFResource to read the properties from 1081 * @param isDefault 1082 * True if the locale is to be read from the main install manifest 1083 * root 1084 * @param aSeenLocales 1085 * An array of locale names already seen for this install manifest. 1086 * Any locale names seen as a part of this function will be added to 1087 * this array 1088 * @return an object containing the locale properties 1089 */ 1090 function readLocale(aDs, aSource, isDefault, aSeenLocales) { 1091 let locale = { }; 1092 if (!isDefault) { 1093 locale.locales = []; 1094 let targets = ds.GetTargets(aSource, EM_R("locale"), true); 1095 while (targets.hasMoreElements()) { 1096 let localeName = getRDFValue(targets.getNext()); 1097 if (!localeName) { 1098 logger.warn("Ignoring empty locale in localized properties"); 1099 continue; 1100 } 1101 if (aSeenLocales.indexOf(localeName) != -1) { 1102 logger.warn("Ignoring duplicate locale in localized properties"); 1103 continue; 1104 } 1105 aSeenLocales.push(localeName); 1106 locale.locales.push(localeName); 1107 } 1108 1109 if (locale.locales.length == 0) { 1110 logger.warn("Ignoring localized properties with no listed locales"); 1111 return null; 1112 } 1113 } 1114 1115 for (let prop of PROP_LOCALE_SINGLE) { 1116 locale[prop] = getRDFProperty(aDs, aSource, prop); 1117 } 1118 1119 for (let prop of PROP_LOCALE_MULTI) { 1120 // Don't store empty arrays 1121 let props = getPropertyArray(aDs, aSource, 1122 prop.substring(0, prop.length - 1)); 1123 if (props.length > 0) 1124 locale[prop] = props; 1125 } 1126 1127 return locale; 1128 } 1129 1130 let rdfParser = Cc["@mozilla.org/rdf/xml-parser;1"]. 1131 createInstance(Ci.nsIRDFXMLParser) 1132 let ds = Cc["@mozilla.org/rdf/datasource;1?name=in-memory-datasource"]. 1133 createInstance(Ci.nsIRDFDataSource); 1134 let listener = rdfParser.parseAsync(ds, aUri); 1135 let channel = Cc["@mozilla.org/network/input-stream-channel;1"]. 1136 createInstance(Ci.nsIInputStreamChannel); 1137 channel.setURI(aUri); 1138 channel.contentStream = aStream; 1139 channel.QueryInterface(Ci.nsIChannel); 1140 channel.contentType = "text/xml"; 1141 1142 listener.onStartRequest(channel, null); 1143 1144 try { 1145 let pos = 0; 1146 let count = aStream.available(); 1147 while (count > 0) { 1148 listener.onDataAvailable(channel, null, aStream, pos, count); 1149 pos += count; 1150 count = aStream.available(); 1151 } 1152 listener.onStopRequest(channel, null, Components.results.NS_OK); 1153 } 1154 catch (e) { 1155 listener.onStopRequest(channel, null, e.result); 1156 throw e; 1157 } 1158 1159 let root = gRDF.GetResource(RDFURI_INSTALL_MANIFEST_ROOT); 1160 let addon = new AddonInternal(); 1161 for (let prop of PROP_METADATA) { 1162 addon[prop] = getRDFProperty(ds, root, prop); 1163 } 1164 addon.unpack = getRDFProperty(ds, root, "unpack") == "true"; 1165 1166 if (!addon.type) { 1167 addon.type = addon.internalName ? "theme" : "extension"; 1168 } 1169 else { 1170 let type = addon.type; 1171 addon.type = null; 1172 for (let name in TYPES) { 1173 if (TYPES[name] == type) { 1174 addon.type = name; 1175 break; 1176 } 1177 } 1178 } 1179 1180 if (!(addon.type in TYPES)) 1181 throw new Error("Install manifest specifies unknown type: " + addon.type); 1182 1183 if (addon.type != "multipackage") { 1184 if (!addon.id) 1185 throw new Error("No ID in install manifest"); 1186 if (!gIDTest.test(addon.id)) 1187 throw new Error("Illegal add-on ID " + addon.id); 1188 if (!addon.version) 1189 throw new Error("No version in install manifest"); 1190 } 1191 1192 addon.strictCompatibility = !(addon.type in COMPATIBLE_BY_DEFAULT_TYPES) || 1193 getRDFProperty(ds, root, "strictCompatibility") == "true"; 1194 1195 // Only read these properties for extensions. 1196 if (addon.type == "extension") { 1197 addon.bootstrap = getRDFProperty(ds, root, "bootstrap") == "true"; 1198 1199 let mpcValue = getRDFProperty(ds, root, "multiprocessCompatible"); 1200 addon.multiprocessCompatible = mpcValue == "true"; 1201 addon.mpcOptedOut = mpcValue == "false"; 1202 1203 addon.hasEmbeddedWebExtension = getRDFProperty(ds, root, "hasEmbeddedWebExtension") == "true"; 1204 1205 if (addon.optionsType && 1206 addon.optionsType != AddonManager.OPTIONS_TYPE_DIALOG && 1207 addon.optionsType != AddonManager.OPTIONS_TYPE_INLINE && 1208 addon.optionsType != AddonManager.OPTIONS_TYPE_TAB && 1209 addon.optionsType != AddonManager.OPTIONS_TYPE_INLINE_INFO) { 1210 throw new Error("Install manifest specifies unknown type: " + addon.optionsType); 1211 } 1212 1213 if (addon.hasEmbeddedWebExtension) { 1214 let uri = NetUtil.newURI("webextension/manifest.json", null, aUri); 1215 let embeddedAddon = yield loadManifestFromWebManifest(uri); 1216 if (embeddedAddon.optionsURL) { 1217 if (addon.optionsType || addon.optionsURL) 1218 logger.warn(`Addon ${addon.id} specifies optionsType or optionsURL ` + 1219 `in both install.rdf and manifest.json`); 1220 1221 addon.optionsURL = embeddedAddon.optionsURL; 1222 addon.optionsType = embeddedAddon.optionsType; 1223 } 1224 } 1225 } 1226 else { 1227 // Some add-on types are always restartless. 1228 if (RESTARTLESS_TYPES.has(addon.type)) { 1229 addon.bootstrap = true; 1230 } 1231 1232 // Only extensions are allowed to provide an optionsURL, optionsType or aboutURL. For 1233 // all other types they are silently ignored 1234 addon.optionsURL = null; 1235 addon.optionsType = null; 1236 addon.aboutURL = null; 1237 1238 if (addon.type == "theme") { 1239 if (!addon.internalName) 1240 throw new Error("Themes must include an internalName property"); 1241 addon.skinnable = getRDFProperty(ds, root, "skinnable") == "true"; 1242 } 1243 } 1244 1245 addon.defaultLocale = readLocale(ds, root, true); 1246 1247 let seenLocales = []; 1248 addon.locales = []; 1249 let targets = ds.GetTargets(root, EM_R("localized"), true); 1250 while (targets.hasMoreElements()) { 1251 let target = targets.getNext().QueryInterface(Ci.nsIRDFResource); 1252 let locale = readLocale(ds, target, false, seenLocales); 1253 if (locale) 1254 addon.locales.push(locale); 1255 } 1256 1257 let dependencies = new Set(); 1258 targets = ds.GetTargets(root, EM_R("dependency"), true); 1259 while (targets.hasMoreElements()) { 1260 let target = targets.getNext().QueryInterface(Ci.nsIRDFResource); 1261 let id = getRDFProperty(ds, target, "id"); 1262 dependencies.add(id); 1263 } 1264 addon.dependencies = Object.freeze(Array.from(dependencies)); 1265 1266 let seenApplications = []; 1267 addon.targetApplications = []; 1268 targets = ds.GetTargets(root, EM_R("targetApplication"), true); 1269 while (targets.hasMoreElements()) { 1270 let target = targets.getNext().QueryInterface(Ci.nsIRDFResource); 1271 let targetAppInfo = {}; 1272 for (let prop of PROP_TARGETAPP) { 1273 targetAppInfo[prop] = getRDFProperty(ds, target, prop); 1274 } 1275 if (!targetAppInfo.id || !targetAppInfo.minVersion || 1276 !targetAppInfo.maxVersion) { 1277 logger.warn("Ignoring invalid targetApplication entry in install manifest"); 1278 continue; 1279 } 1280 if (seenApplications.indexOf(targetAppInfo.id) != -1) { 1281 logger.warn("Ignoring duplicate targetApplication entry for " + targetAppInfo.id + 1282 " in install manifest"); 1283 continue; 1284 } 1285 seenApplications.push(targetAppInfo.id); 1286 addon.targetApplications.push(targetAppInfo); 1287 } 1288 1289 // Note that we don't need to check for duplicate targetPlatform entries since 1290 // the RDF service coalesces them for us. 1291 let targetPlatforms = getPropertyArray(ds, root, "targetPlatform"); 1292 addon.targetPlatforms = []; 1293 for (let targetPlatform of targetPlatforms) { 1294 let platform = { 1295 os: null, 1296 abi: null 1297 }; 1298 1299 let pos = targetPlatform.indexOf("_"); 1300 if (pos != -1) { 1301 platform.os = targetPlatform.substring(0, pos); 1302 platform.abi = targetPlatform.substring(pos + 1); 1303 } 1304 else { 1305 platform.os = targetPlatform; 1306 } 1307 1308 addon.targetPlatforms.push(platform); 1309 } 1310 1311 // A theme's userDisabled value is true if the theme is not the selected skin 1312 // or if there is an active lightweight theme. We ignore whether softblocking 1313 // is in effect since it would change the active theme. 1314 if (addon.type == "theme") { 1315 addon.userDisabled = !!LightweightThemeManager.currentTheme || 1316 addon.internalName != XPIProvider.selectedSkin; 1317 } 1318 else if (addon.type == "experiment") { 1319 // Experiments are disabled by default. It is up to the Experiments Manager 1320 // to enable them (it drives installation). 1321 addon.userDisabled = true; 1322 } 1323 else { 1324 addon.userDisabled = false; 1325 } 1326 1327 addon.softDisabled = addon.blocklistState == Blocklist.STATE_SOFTBLOCKED; 1328 addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DEFAULT; 1329 1330 // Experiments are managed and updated through an external "experiments 1331 // manager." So disable some built-in mechanisms. 1332 if (addon.type == "experiment") { 1333 addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DISABLE; 1334 addon.updateURL = null; 1335 addon.updateKey = null; 1336 } 1337 1338 // icons will be filled by the calling function 1339 addon.icons = {}; 1340 1341 return addon; 1342}); 1343 1344function defineSyncGUID(aAddon) { 1345 // Define .syncGUID as a lazy property which is also settable 1346 Object.defineProperty(aAddon, "syncGUID", { 1347 get: () => { 1348 // Generate random GUID used for Sync. 1349 let guid = Cc["@mozilla.org/uuid-generator;1"] 1350 .getService(Ci.nsIUUIDGenerator) 1351 .generateUUID().toString(); 1352 1353 delete aAddon.syncGUID; 1354 aAddon.syncGUID = guid; 1355 return guid; 1356 }, 1357 set: (val) => { 1358 delete aAddon.syncGUID; 1359 aAddon.syncGUID = val; 1360 }, 1361 configurable: true, 1362 enumerable: true, 1363 }); 1364} 1365 1366// Generate a unique ID based on the path to this temporary add-on location. 1367function generateTemporaryInstallID(aFile) { 1368 const hasher = Cc["@mozilla.org/security/hash;1"] 1369 .createInstance(Ci.nsICryptoHash); 1370 hasher.init(hasher.SHA1); 1371 const data = new TextEncoder().encode(aFile.path); 1372 // Make it so this ID cannot be guessed. 1373 const sess = TEMP_INSTALL_ID_GEN_SESSION; 1374 hasher.update(sess, sess.length); 1375 hasher.update(data, data.length); 1376 let id = `${getHashStringForCrypto(hasher)}@temporary-addon`; 1377 logger.info(`Generated temp id ${id} (${sess.join("")}) for ${aFile.path}`); 1378 return id; 1379} 1380 1381/** 1382 * Loads an AddonInternal object from an add-on extracted in a directory. 1383 * 1384 * @param aDir 1385 * The nsIFile directory holding the add-on 1386 * @return an AddonInternal object 1387 * @throws if the directory does not contain a valid install manifest 1388 */ 1389var loadManifestFromDir = Task.async(function*(aDir, aInstallLocation) { 1390 function getFileSize(aFile) { 1391 if (aFile.isSymlink()) 1392 return 0; 1393 1394 if (!aFile.isDirectory()) 1395 return aFile.fileSize; 1396 1397 let size = 0; 1398 let entries = aFile.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator); 1399 let entry; 1400 while ((entry = entries.nextFile)) 1401 size += getFileSize(entry); 1402 entries.close(); 1403 return size; 1404 } 1405 1406 function* loadFromRDF(aUri) { 1407 let fis = Cc["@mozilla.org/network/file-input-stream;1"]. 1408 createInstance(Ci.nsIFileInputStream); 1409 fis.init(aUri.file, -1, -1, false); 1410 let bis = Cc["@mozilla.org/network/buffered-input-stream;1"]. 1411 createInstance(Ci.nsIBufferedInputStream); 1412 bis.init(fis, 4096); 1413 try { 1414 var addon = yield loadManifestFromRDF(aUri, bis); 1415 } finally { 1416 bis.close(); 1417 fis.close(); 1418 } 1419 1420 let iconFile = aDir.clone(); 1421 iconFile.append("icon.png"); 1422 1423 if (iconFile.exists()) { 1424 addon.icons[32] = "icon.png"; 1425 addon.icons[48] = "icon.png"; 1426 } 1427 1428 let icon64File = aDir.clone(); 1429 icon64File.append("icon64.png"); 1430 1431 if (icon64File.exists()) { 1432 addon.icons[64] = "icon64.png"; 1433 } 1434 1435 let file = aDir.clone(); 1436 file.append("chrome.manifest"); 1437 let chromeManifest = ChromeManifestParser.parseSync(Services.io.newFileURI(file)); 1438 addon.hasBinaryComponents = ChromeManifestParser.hasType(chromeManifest, 1439 "binary-component"); 1440 return addon; 1441 } 1442 1443 let file = getManifestFileForDir(aDir); 1444 if (!file) { 1445 throw new Error("Directory " + aDir.path + " does not contain a valid " + 1446 "install manifest"); 1447 } 1448 1449 let uri = Services.io.newFileURI(file).QueryInterface(Ci.nsIFileURL); 1450 1451 let addon; 1452 if (file.leafName == FILE_WEB_MANIFEST) { 1453 addon = yield loadManifestFromWebManifest(uri); 1454 if (!addon.id) { 1455 if (aInstallLocation == TemporaryInstallLocation) { 1456 addon.id = generateTemporaryInstallID(aDir); 1457 } else { 1458 addon.id = aDir.leafName; 1459 } 1460 } 1461 } else { 1462 addon = yield loadFromRDF(uri); 1463 } 1464 1465 addon._sourceBundle = aDir.clone(); 1466 addon._installLocation = aInstallLocation; 1467 addon.size = getFileSize(aDir); 1468 addon.signedState = yield verifyDirSignedState(aDir, addon) 1469 .then(({signedState}) => signedState); 1470 addon.appDisabled = !isUsableAddon(addon); 1471 1472 defineSyncGUID(addon); 1473 1474 return addon; 1475}); 1476 1477/** 1478 * Loads an AddonInternal object from an nsIZipReader for an add-on. 1479 * 1480 * @param aZipReader 1481 * An open nsIZipReader for the add-on's files 1482 * @return an AddonInternal object 1483 * @throws if the XPI file does not contain a valid install manifest 1484 */ 1485var loadManifestFromZipReader = Task.async(function*(aZipReader, aInstallLocation) { 1486 function* loadFromRDF(aUri) { 1487 let zis = aZipReader.getInputStream(entry); 1488 let bis = Cc["@mozilla.org/network/buffered-input-stream;1"]. 1489 createInstance(Ci.nsIBufferedInputStream); 1490 bis.init(zis, 4096); 1491 try { 1492 var addon = yield loadManifestFromRDF(aUri, bis); 1493 } finally { 1494 bis.close(); 1495 zis.close(); 1496 } 1497 1498 if (aZipReader.hasEntry("icon.png")) { 1499 addon.icons[32] = "icon.png"; 1500 addon.icons[48] = "icon.png"; 1501 } 1502 1503 if (aZipReader.hasEntry("icon64.png")) { 1504 addon.icons[64] = "icon64.png"; 1505 } 1506 1507 // Binary components can only be loaded from unpacked addons. 1508 if (addon.unpack) { 1509 let uri = buildJarURI(aZipReader.file, "chrome.manifest"); 1510 let chromeManifest = ChromeManifestParser.parseSync(uri); 1511 addon.hasBinaryComponents = ChromeManifestParser.hasType(chromeManifest, 1512 "binary-component"); 1513 } else { 1514 addon.hasBinaryComponents = false; 1515 } 1516 1517 return addon; 1518 } 1519 1520 let entry = getManifestEntryForZipReader(aZipReader); 1521 if (!entry) { 1522 throw new Error("File " + aZipReader.file.path + " does not contain a valid " + 1523 "install manifest"); 1524 } 1525 1526 let uri = buildJarURI(aZipReader.file, entry); 1527 1528 let isWebExtension = (entry == FILE_WEB_MANIFEST); 1529 1530 let addon = isWebExtension ? 1531 yield loadManifestFromWebManifest(uri) : 1532 yield loadFromRDF(uri); 1533 1534 addon._sourceBundle = aZipReader.file; 1535 addon._installLocation = aInstallLocation; 1536 1537 addon.size = 0; 1538 let entries = aZipReader.findEntries(null); 1539 while (entries.hasMore()) 1540 addon.size += aZipReader.getEntry(entries.getNext()).realSize; 1541 1542 let {signedState, cert} = yield verifyZipSignedState(aZipReader.file, addon); 1543 addon.signedState = signedState; 1544 if (isWebExtension && !addon.id) { 1545 if (cert) { 1546 addon.id = cert.commonName; 1547 if (!gIDTest.test(addon.id)) { 1548 throw new Error(`Webextension is signed with an invalid id (${addon.id})`); 1549 } 1550 } 1551 if (!addon.id && aInstallLocation == TemporaryInstallLocation) { 1552 addon.id = generateTemporaryInstallID(aZipReader.file); 1553 } 1554 } 1555 addon.appDisabled = !isUsableAddon(addon); 1556 1557 defineSyncGUID(addon); 1558 1559 return addon; 1560}); 1561 1562/** 1563 * Loads an AddonInternal object from an add-on in an XPI file. 1564 * 1565 * @param aXPIFile 1566 * An nsIFile pointing to the add-on's XPI file 1567 * @return an AddonInternal object 1568 * @throws if the XPI file does not contain a valid install manifest 1569 */ 1570var loadManifestFromZipFile = Task.async(function*(aXPIFile, aInstallLocation) { 1571 let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"]. 1572 createInstance(Ci.nsIZipReader); 1573 try { 1574 zipReader.open(aXPIFile); 1575 1576 // Can't return this promise because that will make us close the zip reader 1577 // before it has finished loading the manifest. Wait for the result and then 1578 // return. 1579 let manifest = yield loadManifestFromZipReader(zipReader, aInstallLocation); 1580 return manifest; 1581 } 1582 finally { 1583 zipReader.close(); 1584 } 1585}); 1586 1587function loadManifestFromFile(aFile, aInstallLocation) { 1588 if (aFile.isFile()) 1589 return loadManifestFromZipFile(aFile, aInstallLocation); 1590 return loadManifestFromDir(aFile, aInstallLocation); 1591} 1592 1593/** 1594 * A synchronous method for loading an add-on's manifest. This should only ever 1595 * be used during startup or a sync load of the add-ons DB 1596 */ 1597function syncLoadManifestFromFile(aFile, aInstallLocation) { 1598 let success = undefined; 1599 let result = null; 1600 1601 loadManifestFromFile(aFile, aInstallLocation).then(val => { 1602 success = true; 1603 result = val; 1604 }, val => { 1605 success = false; 1606 result = val 1607 }); 1608 1609 let thread = Services.tm.currentThread; 1610 1611 while (success === undefined) 1612 thread.processNextEvent(true); 1613 1614 if (!success) 1615 throw result; 1616 return result; 1617} 1618 1619/** 1620 * Gets an nsIURI for a file within another file, either a directory or an XPI 1621 * file. If aFile is a directory then this will return a file: URI, if it is an 1622 * XPI file then it will return a jar: URI. 1623 * 1624 * @param aFile 1625 * The file containing the resources, must be either a directory or an 1626 * XPI file 1627 * @param aPath 1628 * The path to find the resource at, "/" separated. If aPath is empty 1629 * then the uri to the root of the contained files will be returned 1630 * @return an nsIURI pointing at the resource 1631 */ 1632function getURIForResourceInFile(aFile, aPath) { 1633 if (aFile.isDirectory()) { 1634 let resource = aFile.clone(); 1635 if (aPath) 1636 aPath.split("/").forEach(part => resource.append(part)); 1637 1638 return NetUtil.newURI(resource); 1639 } 1640 1641 return buildJarURI(aFile, aPath); 1642} 1643 1644/** 1645 * Creates a jar: URI for a file inside a ZIP file. 1646 * 1647 * @param aJarfile 1648 * The ZIP file as an nsIFile 1649 * @param aPath 1650 * The path inside the ZIP file 1651 * @return an nsIURI for the file 1652 */ 1653function buildJarURI(aJarfile, aPath) { 1654 let uri = Services.io.newFileURI(aJarfile); 1655 uri = "jar:" + uri.spec + "!/" + aPath; 1656 return NetUtil.newURI(uri); 1657} 1658 1659/** 1660 * Sends local and remote notifications to flush a JAR file cache entry 1661 * 1662 * @param aJarFile 1663 * The ZIP/XPI/JAR file as a nsIFile 1664 */ 1665function flushJarCache(aJarFile) { 1666 Services.obs.notifyObservers(aJarFile, "flush-cache-entry", null); 1667 Services.mm.broadcastAsyncMessage(MSG_JAR_FLUSH, aJarFile.path); 1668} 1669 1670function flushChromeCaches() { 1671 // Init this, so it will get the notification. 1672 Services.obs.notifyObservers(null, "startupcache-invalidate", null); 1673 // Flush message manager cached scripts 1674 Services.obs.notifyObservers(null, "message-manager-flush-caches", null); 1675 // Also dispatch this event to child processes 1676 Services.mm.broadcastAsyncMessage(MSG_MESSAGE_MANAGER_CACHES_FLUSH, null); 1677} 1678 1679/** 1680 * Creates and returns a new unique temporary file. The caller should delete 1681 * the file when it is no longer needed. 1682 * 1683 * @return an nsIFile that points to a randomly named, initially empty file in 1684 * the OS temporary files directory 1685 */ 1686function getTemporaryFile() { 1687 let file = FileUtils.getDir(KEY_TEMPDIR, []); 1688 let random = Math.random().toString(36).replace(/0./, '').substr(-3); 1689 file.append("tmp-" + random + ".xpi"); 1690 file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE); 1691 1692 return file; 1693} 1694 1695/** 1696 * Verifies that a zip file's contents are all signed by the same principal. 1697 * Directory entries and anything in the META-INF directory are not checked. 1698 * 1699 * @param aZip 1700 * A nsIZipReader to check 1701 * @param aCertificate 1702 * The nsIX509Cert to compare against 1703 * @return true if all the contents that should be signed were signed by the 1704 * principal 1705 */ 1706function verifyZipSigning(aZip, aCertificate) { 1707 var count = 0; 1708 var entries = aZip.findEntries(null); 1709 while (entries.hasMore()) { 1710 var entry = entries.getNext(); 1711 // Nothing in META-INF is in the manifest. 1712 if (entry.substr(0, 9) == "META-INF/") 1713 continue; 1714 // Directory entries aren't in the manifest. 1715 if (entry.substr(-1) == "/") 1716 continue; 1717 count++; 1718 var entryCertificate = aZip.getSigningCert(entry); 1719 if (!entryCertificate || !aCertificate.equals(entryCertificate)) { 1720 return false; 1721 } 1722 } 1723 return aZip.manifestEntriesCount == count; 1724} 1725 1726/** 1727 * Returns the signedState for a given return code and certificate by verifying 1728 * it against the expected ID. 1729 */ 1730function getSignedStatus(aRv, aCert, aAddonID) { 1731 let expectedCommonName = aAddonID; 1732 if (aAddonID && aAddonID.length > 64) { 1733 let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]. 1734 createInstance(Ci.nsIScriptableUnicodeConverter); 1735 converter.charset = "UTF-8"; 1736 let data = converter.convertToByteArray(aAddonID, {}); 1737 1738 let crypto = Cc["@mozilla.org/security/hash;1"]. 1739 createInstance(Ci.nsICryptoHash); 1740 crypto.init(Ci.nsICryptoHash.SHA256); 1741 crypto.update(data, data.length); 1742 expectedCommonName = getHashStringForCrypto(crypto); 1743 } 1744 1745 switch (aRv) { 1746 case Cr.NS_OK: 1747 if (expectedCommonName && expectedCommonName != aCert.commonName) 1748 return AddonManager.SIGNEDSTATE_BROKEN; 1749 1750 let hotfixID = Preferences.get(PREF_EM_HOTFIX_ID, undefined); 1751 if (hotfixID && hotfixID == aAddonID && Preferences.get(PREF_EM_CERT_CHECKATTRIBUTES, false)) { 1752 // The hotfix add-on has some more rigorous certificate checks 1753 try { 1754 CertUtils.validateCert(aCert, 1755 CertUtils.readCertPrefs(PREF_EM_HOTFIX_CERTS)); 1756 } 1757 catch (e) { 1758 logger.warn("The hotfix add-on was not signed by the expected " + 1759 "certificate and so will not be installed.", e); 1760 return AddonManager.SIGNEDSTATE_BROKEN; 1761 } 1762 } 1763 1764 if (aCert.organizationalUnit == "Mozilla Components") 1765 return AddonManager.SIGNEDSTATE_SYSTEM; 1766 1767 return /preliminary/i.test(aCert.organizationalUnit) 1768 ? AddonManager.SIGNEDSTATE_PRELIMINARY 1769 : AddonManager.SIGNEDSTATE_SIGNED; 1770 case Cr.NS_ERROR_SIGNED_JAR_NOT_SIGNED: 1771 return AddonManager.SIGNEDSTATE_MISSING; 1772 case Cr.NS_ERROR_SIGNED_JAR_MANIFEST_INVALID: 1773 case Cr.NS_ERROR_SIGNED_JAR_ENTRY_INVALID: 1774 case Cr.NS_ERROR_SIGNED_JAR_ENTRY_MISSING: 1775 case Cr.NS_ERROR_SIGNED_JAR_ENTRY_TOO_LARGE: 1776 case Cr.NS_ERROR_SIGNED_JAR_UNSIGNED_ENTRY: 1777 case Cr.NS_ERROR_SIGNED_JAR_MODIFIED_ENTRY: 1778 return AddonManager.SIGNEDSTATE_BROKEN; 1779 default: 1780 // Any other error indicates that either the add-on isn't signed or it 1781 // is signed by a signature that doesn't chain to the trusted root. 1782 return AddonManager.SIGNEDSTATE_UNKNOWN; 1783 } 1784} 1785 1786function shouldVerifySignedState(aAddon) { 1787 // Updated system add-ons should always have their signature checked 1788 if (aAddon._installLocation.name == KEY_APP_SYSTEM_ADDONS) 1789 return true; 1790 1791 // We don't care about signatures for default system add-ons 1792 if (aAddon._installLocation.name == KEY_APP_SYSTEM_DEFAULTS) 1793 return false; 1794 1795 // Hotfixes should always have their signature checked 1796 let hotfixID = Preferences.get(PREF_EM_HOTFIX_ID, undefined); 1797 if (hotfixID && aAddon.id == hotfixID) 1798 return true; 1799 1800 // Otherwise only check signatures if signing is enabled and the add-on is one 1801 // of the signed types. 1802 return ADDON_SIGNING && SIGNED_TYPES.has(aAddon.type); 1803} 1804 1805let gCertDB = Cc["@mozilla.org/security/x509certdb;1"] 1806 .getService(Ci.nsIX509CertDB); 1807 1808/** 1809 * Verifies that a zip file's contents are all correctly signed by an 1810 * AMO-issued certificate 1811 * 1812 * @param aFile 1813 * the xpi file to check 1814 * @param aAddon 1815 * the add-on object to verify 1816 * @return a Promise that resolves to an object with properties: 1817 * signedState: an AddonManager.SIGNEDSTATE_* constant 1818 * cert: an nsIX509Cert 1819 */ 1820function verifyZipSignedState(aFile, aAddon) { 1821 if (!shouldVerifySignedState(aAddon)) 1822 return Promise.resolve({ 1823 signedState: AddonManager.SIGNEDSTATE_NOT_REQUIRED, 1824 cert: null 1825 }); 1826 1827 let root = Ci.nsIX509CertDB.AddonsPublicRoot; 1828 if (!REQUIRE_SIGNING && Preferences.get(PREF_XPI_SIGNATURES_DEV_ROOT, false)) 1829 root = Ci.nsIX509CertDB.AddonsStageRoot; 1830 1831 return new Promise(resolve => { 1832 let callback = { 1833 openSignedAppFileFinished: function(aRv, aZipReader, aCert) { 1834 if (aZipReader) 1835 aZipReader.close(); 1836 resolve({ 1837 signedState: getSignedStatus(aRv, aCert, aAddon.id), 1838 cert: aCert 1839 }); 1840 } 1841 }; 1842 // This allows the certificate DB to get the raw JS callback object so the 1843 // test code can pass through objects that XPConnect would reject. 1844 callback.wrappedJSObject = callback; 1845 1846 gCertDB.openSignedAppFileAsync(root, aFile, callback); 1847 }); 1848} 1849 1850/** 1851 * Verifies that a directory's contents are all correctly signed by an 1852 * AMO-issued certificate 1853 * 1854 * @param aDir 1855 * the directory to check 1856 * @param aAddon 1857 * the add-on object to verify 1858 * @return a Promise that resolves to an object with properties: 1859 * signedState: an AddonManager.SIGNEDSTATE_* constant 1860 * cert: an nsIX509Cert 1861 */ 1862function verifyDirSignedState(aDir, aAddon) { 1863 if (!shouldVerifySignedState(aAddon)) 1864 return Promise.resolve({ 1865 signedState: AddonManager.SIGNEDSTATE_NOT_REQUIRED, 1866 cert: null, 1867 }); 1868 1869 let root = Ci.nsIX509CertDB.AddonsPublicRoot; 1870 if (!REQUIRE_SIGNING && Preferences.get(PREF_XPI_SIGNATURES_DEV_ROOT, false)) 1871 root = Ci.nsIX509CertDB.AddonsStageRoot; 1872 1873 return new Promise(resolve => { 1874 let callback = { 1875 verifySignedDirectoryFinished: function(aRv, aCert) { 1876 resolve({ 1877 signedState: getSignedStatus(aRv, aCert, aAddon.id), 1878 cert: null, 1879 }); 1880 } 1881 }; 1882 // This allows the certificate DB to get the raw JS callback object so the 1883 // test code can pass through objects that XPConnect would reject. 1884 callback.wrappedJSObject = callback; 1885 1886 gCertDB.verifySignedDirectoryAsync(root, aDir, callback); 1887 }); 1888} 1889 1890/** 1891 * Verifies that a bundle's contents are all correctly signed by an 1892 * AMO-issued certificate 1893 * 1894 * @param aBundle 1895 * the nsIFile for the bundle to check, either a directory or zip file 1896 * @param aAddon 1897 * the add-on object to verify 1898 * @return a Promise that resolves to an AddonManager.SIGNEDSTATE_* constant. 1899 */ 1900function verifyBundleSignedState(aBundle, aAddon) { 1901 let promise = aBundle.isFile() ? verifyZipSignedState(aBundle, aAddon) 1902 : verifyDirSignedState(aBundle, aAddon); 1903 return promise.then(({signedState}) => signedState); 1904} 1905 1906/** 1907 * Replaces %...% strings in an addon url (update and updateInfo) with 1908 * appropriate values. 1909 * 1910 * @param aAddon 1911 * The AddonInternal representing the add-on 1912 * @param aUri 1913 * The uri to escape 1914 * @param aUpdateType 1915 * An optional number representing the type of update, only applicable 1916 * when creating a url for retrieving an update manifest 1917 * @param aAppVersion 1918 * The optional application version to use for %APP_VERSION% 1919 * @return the appropriately escaped uri. 1920 */ 1921function escapeAddonURI(aAddon, aUri, aUpdateType, aAppVersion) 1922{ 1923 let uri = AddonManager.escapeAddonURI(aAddon, aUri, aAppVersion); 1924 1925 // If there is an updateType then replace the UPDATE_TYPE string 1926 if (aUpdateType) 1927 uri = uri.replace(/%UPDATE_TYPE%/g, aUpdateType); 1928 1929 // If this add-on has compatibility information for either the current 1930 // application or toolkit then replace the ITEM_MAXAPPVERSION with the 1931 // maxVersion 1932 let app = aAddon.matchingTargetApplication; 1933 if (app) 1934 var maxVersion = app.maxVersion; 1935 else 1936 maxVersion = ""; 1937 uri = uri.replace(/%ITEM_MAXAPPVERSION%/g, maxVersion); 1938 1939 let compatMode = "normal"; 1940 if (!AddonManager.checkCompatibility) 1941 compatMode = "ignore"; 1942 else if (AddonManager.strictCompatibility) 1943 compatMode = "strict"; 1944 uri = uri.replace(/%COMPATIBILITY_MODE%/g, compatMode); 1945 1946 return uri; 1947} 1948 1949function removeAsync(aFile) { 1950 return Task.spawn(function*() { 1951 let info = null; 1952 try { 1953 info = yield OS.File.stat(aFile.path); 1954 if (info.isDir) 1955 yield OS.File.removeDir(aFile.path); 1956 else 1957 yield OS.File.remove(aFile.path); 1958 } 1959 catch (e) { 1960 if (!(e instanceof OS.File.Error) || ! e.becauseNoSuchFile) 1961 throw e; 1962 // The file has already gone away 1963 return; 1964 } 1965 }); 1966} 1967 1968/** 1969 * Recursively removes a directory or file fixing permissions when necessary. 1970 * 1971 * @param aFile 1972 * The nsIFile to remove 1973 */ 1974function recursiveRemove(aFile) { 1975 let isDir = null; 1976 1977 try { 1978 isDir = aFile.isDirectory(); 1979 } 1980 catch (e) { 1981 // If the file has already gone away then don't worry about it, this can 1982 // happen on OSX where the resource fork is automatically moved with the 1983 // data fork for the file. See bug 733436. 1984 if (e.result == Cr.NS_ERROR_FILE_TARGET_DOES_NOT_EXIST) 1985 return; 1986 if (e.result == Cr.NS_ERROR_FILE_NOT_FOUND) 1987 return; 1988 1989 throw e; 1990 } 1991 1992 setFilePermissions(aFile, isDir ? FileUtils.PERMS_DIRECTORY 1993 : FileUtils.PERMS_FILE); 1994 1995 try { 1996 aFile.remove(true); 1997 return; 1998 } 1999 catch (e) { 2000 if (!aFile.isDirectory() || aFile.isSymlink()) { 2001 logger.error("Failed to remove file " + aFile.path, e); 2002 throw e; 2003 } 2004 } 2005 2006 // Use a snapshot of the directory contents to avoid possible issues with 2007 // iterating over a directory while removing files from it (the YAFFS2 2008 // embedded filesystem has this issue, see bug 772238), and to remove 2009 // normal files before their resource forks on OSX (see bug 733436). 2010 let entries = getDirectoryEntries(aFile, true); 2011 entries.forEach(recursiveRemove); 2012 2013 try { 2014 aFile.remove(true); 2015 } 2016 catch (e) { 2017 logger.error("Failed to remove empty directory " + aFile.path, e); 2018 throw e; 2019 } 2020} 2021 2022/** 2023 * Returns the timestamp and leaf file name of the most recently modified 2024 * entry in a directory, 2025 * or simply the file's own timestamp if it is not a directory. 2026 * Also returns the total number of items (directories and files) visited in the scan 2027 * 2028 * @param aFile 2029 * A non-null nsIFile object 2030 * @return [File Name, Epoch time, items visited], as described above. 2031 */ 2032function recursiveLastModifiedTime(aFile) { 2033 try { 2034 let modTime = aFile.lastModifiedTime; 2035 let fileName = aFile.leafName; 2036 if (aFile.isFile()) 2037 return [fileName, modTime, 1]; 2038 2039 if (aFile.isDirectory()) { 2040 let entries = aFile.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator); 2041 let entry; 2042 let totalItems = 1; 2043 while ((entry = entries.nextFile)) { 2044 let [subName, subTime, items] = recursiveLastModifiedTime(entry); 2045 totalItems += items; 2046 if (subTime > modTime) { 2047 modTime = subTime; 2048 fileName = subName; 2049 } 2050 } 2051 entries.close(); 2052 return [fileName, modTime, totalItems]; 2053 } 2054 } 2055 catch (e) { 2056 logger.warn("Problem getting last modified time for " + aFile.path, e); 2057 } 2058 2059 // If the file is something else, just ignore it. 2060 return ["", 0, 0]; 2061} 2062 2063/** 2064 * Gets a snapshot of directory entries. 2065 * 2066 * @param aDir 2067 * Directory to look at 2068 * @param aSortEntries 2069 * True to sort entries by filename 2070 * @return An array of nsIFile, or an empty array if aDir is not a readable directory 2071 */ 2072function getDirectoryEntries(aDir, aSortEntries) { 2073 let dirEnum; 2074 try { 2075 dirEnum = aDir.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator); 2076 let entries = []; 2077 while (dirEnum.hasMoreElements()) 2078 entries.push(dirEnum.nextFile); 2079 2080 if (aSortEntries) { 2081 entries.sort(function(a, b) { 2082 return a.path > b.path ? -1 : 1; 2083 }); 2084 } 2085 2086 return entries 2087 } 2088 catch (e) { 2089 logger.warn("Can't iterate directory " + aDir.path, e); 2090 return []; 2091 } 2092 finally { 2093 if (dirEnum) { 2094 dirEnum.close(); 2095 } 2096 } 2097} 2098 2099/** 2100 * Record a bit of per-addon telemetry 2101 * @param aAddon the addon to record 2102 */ 2103function recordAddonTelemetry(aAddon) { 2104 let locale = aAddon.defaultLocale; 2105 if (locale) { 2106 if (locale.name) 2107 XPIProvider.setTelemetry(aAddon.id, "name", locale.name); 2108 if (locale.creator) 2109 XPIProvider.setTelemetry(aAddon.id, "creator", locale.creator); 2110 } 2111} 2112 2113/** 2114 * The on-disk state of an individual XPI, created from an Object 2115 * as stored in the 'extensions.xpiState' pref. 2116 */ 2117function XPIState(saved) { 2118 for (let [short, long] of XPIState.prototype.fields) { 2119 if (short in saved) { 2120 this[long] = saved[short]; 2121 } 2122 } 2123} 2124 2125XPIState.prototype = { 2126 fields: [['d', 'descriptor'], 2127 ['e', 'enabled'], 2128 ['v', 'version'], 2129 ['st', 'scanTime'], 2130 ['mt', 'manifestTime']], 2131 /** 2132 * Return the last modified time, based on enabled/disabled 2133 */ 2134 get mtime() { 2135 if (!this.enabled && ('manifestTime' in this) && this.manifestTime > this.scanTime) { 2136 return this.manifestTime; 2137 } 2138 return this.scanTime; 2139 }, 2140 2141 toJSON() { 2142 let json = {}; 2143 for (let [short, long] of XPIState.prototype.fields) { 2144 if (long in this) { 2145 json[short] = this[long]; 2146 } 2147 } 2148 return json; 2149 }, 2150 2151 /** 2152 * Update the last modified time for an add-on on disk. 2153 * @param aFile: nsIFile path of the add-on. 2154 * @param aId: The add-on ID. 2155 * @return True if the time stamp has changed. 2156 */ 2157 getModTime(aFile, aId) { 2158 let changed = false; 2159 let scanStarted = Cu.now(); 2160 // For an unknown or enabled add-on, we do a full recursive scan. 2161 if (!('scanTime' in this) || this.enabled) { 2162 logger.debug('getModTime: Recursive scan of ' + aId); 2163 let [modFile, modTime, items] = recursiveLastModifiedTime(aFile); 2164 XPIProvider._mostRecentlyModifiedFile[aId] = modFile; 2165 XPIProvider.setTelemetry(aId, "scan_items", items); 2166 if (modTime != this.scanTime) { 2167 this.scanTime = modTime; 2168 changed = true; 2169 } 2170 } 2171 // if the add-on is disabled, modified time is the install manifest time, if 2172 // any. If no manifest exists, we assume this is a packed .xpi and use 2173 // the time stamp of {path} 2174 try { 2175 // Get the install manifest update time, if any. 2176 let maniFile = getManifestFileForDir(aFile); 2177 if (!(aId in XPIProvider._mostRecentlyModifiedFile)) { 2178 XPIProvider._mostRecentlyModifiedFile[aId] = maniFile.leafName; 2179 } 2180 let maniTime = maniFile.lastModifiedTime; 2181 if (maniTime != this.manifestTime) { 2182 this.manifestTime = maniTime; 2183 changed = true; 2184 } 2185 } catch (e) { 2186 // No manifest 2187 delete this.manifestTime; 2188 try { 2189 let dtime = aFile.lastModifiedTime; 2190 if (dtime != this.scanTime) { 2191 changed = true; 2192 this.scanTime = dtime; 2193 } 2194 } catch (e) { 2195 logger.warn("Can't get modified time of ${file}: ${e}", {file: aFile.path, e: e}); 2196 changed = true; 2197 this.scanTime = 0; 2198 } 2199 } 2200 // Record duration of file-modified check 2201 XPIProvider.setTelemetry(aId, "scan_MS", Math.round(Cu.now() - scanStarted)); 2202 2203 return changed; 2204 }, 2205 2206 /** 2207 * Update the XPIState to match an XPIDatabase entry; if 'enabled' is changed to true, 2208 * update the last-modified time. This should probably be made async, but for now we 2209 * don't want to maintain parallel sync and async versions of the scan. 2210 * Caller is responsible for doing XPIStates.save() if necessary. 2211 * @param aDBAddon The DBAddonInternal for this add-on. 2212 * @param aUpdated The add-on was updated, so we must record new modified time. 2213 */ 2214 syncWithDB(aDBAddon, aUpdated = false) { 2215 logger.debug("Updating XPIState for " + JSON.stringify(aDBAddon)); 2216 // If the add-on changes from disabled to enabled, we should re-check the modified time. 2217 // If this is a newly found add-on, it won't have an 'enabled' field but we 2218 // did a full recursive scan in that case, so we don't need to do it again. 2219 // We don't use aDBAddon.active here because it's not updated until after restart. 2220 let mustGetMod = (aDBAddon.visible && !aDBAddon.disabled && !this.enabled); 2221 this.enabled = (aDBAddon.visible && !aDBAddon.disabled); 2222 this.version = aDBAddon.version; 2223 // XXX Eventually also copy bootstrap, etc. 2224 if (aUpdated || mustGetMod) { 2225 this.getModTime(new nsIFile(this.descriptor), aDBAddon.id); 2226 if (this.scanTime != aDBAddon.updateDate) { 2227 aDBAddon.updateDate = this.scanTime; 2228 XPIDatabase.saveChanges(); 2229 } 2230 } 2231 }, 2232}; 2233 2234// Constructor for an ES6 Map that knows how to convert itself into a 2235// regular object for toJSON(). 2236function SerializableMap() { 2237 let m = new Map(); 2238 m.toJSON = function() { 2239 let out = {} 2240 for (let [key, val] of m) { 2241 out[key] = val; 2242 } 2243 return out; 2244 }; 2245 return m; 2246} 2247 2248/** 2249 * Keeps track of the state of XPI add-ons on the file system. 2250 */ 2251this.XPIStates = { 2252 // Map(location name -> Map(add-on ID -> XPIState)) 2253 db: null, 2254 2255 get size() { 2256 if (!this.db) { 2257 return 0; 2258 } 2259 let count = 0; 2260 for (let location of this.db.values()) { 2261 count += location.size; 2262 } 2263 return count; 2264 }, 2265 2266 /** 2267 * Load extension state data from preferences. 2268 */ 2269 loadExtensionState() { 2270 let state = {}; 2271 2272 // Clear out old directory state cache. 2273 Preferences.reset(PREF_INSTALL_CACHE); 2274 2275 let cache = Preferences.get(PREF_XPI_STATE, "{}"); 2276 try { 2277 state = JSON.parse(cache); 2278 } catch (e) { 2279 logger.warn("Error parsing extensions.xpiState ${state}: ${error}", 2280 {state: cache, error: e}); 2281 } 2282 logger.debug("Loaded add-on state from prefs: ${}", state); 2283 return state; 2284 }, 2285 2286 /** 2287 * Walk through all install locations, highest priority first, 2288 * comparing the on-disk state of extensions to what is stored in prefs. 2289 * @return true if anything has changed. 2290 */ 2291 getInstallState() { 2292 let oldState = this.loadExtensionState(); 2293 let changed = false; 2294 this.db = new SerializableMap(); 2295 2296 for (let location of XPIProvider.installLocations) { 2297 // The list of add-on like file/directory names in the install location. 2298 let addons = location.getAddonLocations(); 2299 // The results of scanning this location. 2300 let foundAddons = new SerializableMap(); 2301 2302 // What our old state thinks should be in this location. 2303 let locState = {}; 2304 if (location.name in oldState) { 2305 locState = oldState[location.name]; 2306 // We've seen this location. 2307 delete oldState[location.name]; 2308 } 2309 2310 for (let [id, file] of addons) { 2311 if (!(id in locState)) { 2312 logger.debug("New add-on ${id} in ${location}", {id: id, location: location.name}); 2313 let xpiState = new XPIState({d: file.persistentDescriptor}); 2314 changed = xpiState.getModTime(file, id) || changed; 2315 foundAddons.set(id, xpiState); 2316 } else { 2317 let xpiState = new XPIState(locState[id]); 2318 // We found this add-on in the file system 2319 delete locState[id]; 2320 2321 changed = xpiState.getModTime(file, id) || changed; 2322 2323 if (file.persistentDescriptor != xpiState.descriptor) { 2324 xpiState.descriptor = file.persistentDescriptor; 2325 changed = true; 2326 } 2327 if (changed) { 2328 logger.debug("Changed add-on ${id} in ${location}", {id: id, location: location.name}); 2329 } 2330 else { 2331 logger.debug("Existing add-on ${id} in ${location}", {id: id, location: location.name}); 2332 } 2333 foundAddons.set(id, xpiState); 2334 } 2335 XPIProvider.setTelemetry(id, "location", location.name); 2336 } 2337 2338 // Anything left behind in oldState was removed from the file system. 2339 for (let id in locState) { 2340 changed = true; 2341 break; 2342 } 2343 // If we found anything, add this location to our database. 2344 if (foundAddons.size != 0) { 2345 this.db.set(location.name, foundAddons); 2346 } 2347 } 2348 2349 // If there's anything left in oldState, an install location that held add-ons 2350 // was removed from the browser configuration. 2351 for (let location in oldState) { 2352 changed = true; 2353 break; 2354 } 2355 2356 logger.debug("getInstallState changed: ${rv}, state: ${state}", 2357 {rv: changed, state: this.db}); 2358 return changed; 2359 }, 2360 2361 /** 2362 * Get the Map of XPI states for a particular location. 2363 * @param aLocation The name of the install location. 2364 * @return Map (id -> XPIState) or null if there are no add-ons in the location. 2365 */ 2366 getLocation(aLocation) { 2367 return this.db.get(aLocation); 2368 }, 2369 2370 /** 2371 * Get the XPI state for a specific add-on in a location. 2372 * If the state is not in our cache, return null. 2373 * @param aLocation The name of the location where the add-on is installed. 2374 * @param aId The add-on ID 2375 * @return The XPIState entry for the add-on, or null. 2376 */ 2377 getAddon(aLocation, aId) { 2378 let location = this.db.get(aLocation); 2379 if (!location) { 2380 return null; 2381 } 2382 return location.get(aId); 2383 }, 2384 2385 /** 2386 * Find the highest priority location of an add-on by ID and return the 2387 * location and the XPIState. 2388 * @param aId The add-on ID 2389 * @return [locationName, XPIState] if the add-on is found, [undefined, undefined] 2390 * if the add-on is not found. 2391 */ 2392 findAddon(aId) { 2393 // Fortunately the Map iterator returns in order of insertion, which is 2394 // also our highest -> lowest priority order. 2395 for (let [name, location] of this.db) { 2396 if (location.has(aId)) { 2397 return [name, location.get(aId)]; 2398 } 2399 } 2400 return [undefined, undefined]; 2401 }, 2402 2403 /** 2404 * Add a new XPIState for an add-on and synchronize it with the DBAddonInternal. 2405 * @param aAddon DBAddonInternal for the new add-on. 2406 */ 2407 addAddon(aAddon) { 2408 let location = this.db.get(aAddon.location); 2409 if (!location) { 2410 // First add-on in this location. 2411 location = new SerializableMap(); 2412 this.db.set(aAddon.location, location); 2413 } 2414 logger.debug("XPIStates adding add-on ${id} in ${location}: ${descriptor}", aAddon); 2415 let xpiState = new XPIState({d: aAddon.descriptor}); 2416 location.set(aAddon.id, xpiState); 2417 xpiState.syncWithDB(aAddon, true); 2418 XPIProvider.setTelemetry(aAddon.id, "location", aAddon.location); 2419 }, 2420 2421 /** 2422 * Save the current state of installed add-ons. 2423 * XXX this *totally* should be a .json file using DeferredSave... 2424 */ 2425 save() { 2426 let cache = JSON.stringify(this.db); 2427 Services.prefs.setCharPref(PREF_XPI_STATE, cache); 2428 }, 2429 2430 /** 2431 * Remove the XPIState for an add-on and save the new state. 2432 * @param aLocation The name of the add-on location. 2433 * @param aId The ID of the add-on. 2434 */ 2435 removeAddon(aLocation, aId) { 2436 logger.debug("Removing XPIState for " + aLocation + ":" + aId); 2437 let location = this.db.get(aLocation); 2438 if (!location) { 2439 return; 2440 } 2441 location.delete(aId); 2442 if (location.size == 0) { 2443 this.db.delete(aLocation); 2444 } 2445 this.save(); 2446 }, 2447}; 2448 2449this.XPIProvider = { 2450 get name() { 2451 return "XPIProvider"; 2452 }, 2453 2454 // An array of known install locations 2455 installLocations: null, 2456 // A dictionary of known install locations by name 2457 installLocationsByName: null, 2458 // An array of currently active AddonInstalls 2459 installs: null, 2460 // The default skin for the application 2461 defaultSkin: "classic/1.0", 2462 // The current skin used by the application 2463 currentSkin: null, 2464 // The selected skin to be used by the application when it is restarted. This 2465 // will be the same as currentSkin when it is the skin to be used when the 2466 // application is restarted 2467 selectedSkin: null, 2468 // The value of the minCompatibleAppVersion preference 2469 minCompatibleAppVersion: null, 2470 // The value of the minCompatiblePlatformVersion preference 2471 minCompatiblePlatformVersion: null, 2472 // A dictionary of the file descriptors for bootstrappable add-ons by ID 2473 bootstrappedAddons: {}, 2474 // A Map of active addons to their bootstrapScope by ID 2475 activeAddons: new Map(), 2476 // True if the platform could have activated extensions 2477 extensionsActive: false, 2478 // True if all of the add-ons found during startup were installed in the 2479 // application install location 2480 allAppGlobal: true, 2481 // A string listing the enabled add-ons for annotating crash reports 2482 enabledAddons: null, 2483 // Keep track of startup phases for telemetry 2484 runPhase: XPI_STARTING, 2485 // Keep track of the newest file in each add-on, in case we want to 2486 // report it to telemetry. 2487 _mostRecentlyModifiedFile: {}, 2488 // Per-addon telemetry information 2489 _telemetryDetails: {}, 2490 // A Map from an add-on install to its ID 2491 _addonFileMap: new Map(), 2492 // Flag to know if ToolboxProcess.jsm has already been loaded by someone or not 2493 _toolboxProcessLoaded: false, 2494 // Have we started shutting down bootstrap add-ons? 2495 _closing: false, 2496 2497 /** 2498 * Returns an array of the add-on values in `bootstrappedAddons`, 2499 * sorted so that all of an add-on's dependencies appear in the array 2500 * before itself. 2501 * 2502 * @returns {Array<object>} 2503 * A sorted array of add-on objects. Each value is a copy of the 2504 * corresponding value in the `bootstrappedAddons` object, with an 2505 * additional `id` property, which corresponds to the key in that 2506 * object, which is the same as the add-ons ID. 2507 */ 2508 sortBootstrappedAddons: function() { 2509 let addons = {}; 2510 2511 // Sort the list of IDs so that the ordering is deterministic. 2512 for (let id of Object.keys(this.bootstrappedAddons).sort()) { 2513 addons[id] = Object.assign({id}, this.bootstrappedAddons[id]); 2514 } 2515 2516 let res = new Set(); 2517 let seen = new Set(); 2518 2519 let add = addon => { 2520 seen.add(addon.id); 2521 2522 for (let id of addon.dependencies || []) { 2523 if (id in addons && !seen.has(id)) { 2524 add(addons[id]); 2525 } 2526 } 2527 2528 res.add(addon.id); 2529 } 2530 2531 Object.values(addons).forEach(add); 2532 2533 return Array.from(res, id => addons[id]); 2534 }, 2535 2536 /* 2537 * Set a value in the telemetry hash for a given ID 2538 */ 2539 setTelemetry: function(aId, aName, aValue) { 2540 if (!this._telemetryDetails[aId]) 2541 this._telemetryDetails[aId] = {}; 2542 this._telemetryDetails[aId][aName] = aValue; 2543 }, 2544 2545 // Keep track of in-progress operations that support cancel() 2546 _inProgress: [], 2547 2548 doing: function(aCancellable) { 2549 this._inProgress.push(aCancellable); 2550 }, 2551 2552 done: function(aCancellable) { 2553 let i = this._inProgress.indexOf(aCancellable); 2554 if (i != -1) { 2555 this._inProgress.splice(i, 1); 2556 return true; 2557 } 2558 return false; 2559 }, 2560 2561 cancelAll: function() { 2562 // Cancelling one may alter _inProgress, so don't use a simple iterator 2563 while (this._inProgress.length > 0) { 2564 let c = this._inProgress.shift(); 2565 try { 2566 c.cancel(); 2567 } 2568 catch (e) { 2569 logger.warn("Cancel failed", e); 2570 } 2571 } 2572 }, 2573 2574 /** 2575 * Adds or updates a URI mapping for an Addon.id. 2576 * 2577 * Mappings should not be removed at any point. This is so that the mappings 2578 * will be still valid after an add-on gets disabled or uninstalled, as 2579 * consumers may still have URIs of (leaked) resources they want to map. 2580 */ 2581 _addURIMapping: function(aID, aFile) { 2582 logger.info("Mapping " + aID + " to " + aFile.path); 2583 this._addonFileMap.set(aID, aFile.path); 2584 2585 AddonPathService.insertPath(aFile.path, aID); 2586 }, 2587 2588 /** 2589 * Resolve a URI back to physical file. 2590 * 2591 * Of course, this works only for URIs pointing to local resources. 2592 * 2593 * @param aURI 2594 * URI to resolve 2595 * @return 2596 * resolved nsIFileURL 2597 */ 2598 _resolveURIToFile: function(aURI) { 2599 switch (aURI.scheme) { 2600 case "jar": 2601 case "file": 2602 if (aURI instanceof Ci.nsIJARURI) { 2603 return this._resolveURIToFile(aURI.JARFile); 2604 } 2605 return aURI; 2606 2607 case "chrome": 2608 aURI = ChromeRegistry.convertChromeURL(aURI); 2609 return this._resolveURIToFile(aURI); 2610 2611 case "resource": 2612 aURI = Services.io.newURI(ResProtocolHandler.resolveURI(aURI), null, 2613 null); 2614 return this._resolveURIToFile(aURI); 2615 2616 case "view-source": 2617 aURI = Services.io.newURI(aURI.path, null, null); 2618 return this._resolveURIToFile(aURI); 2619 2620 case "about": 2621 if (aURI.spec == "about:blank") { 2622 // Do not attempt to map about:blank 2623 return null; 2624 } 2625 2626 let chan; 2627 try { 2628 chan = NetUtil.newChannel({ 2629 uri: aURI, 2630 loadUsingSystemPrincipal: true 2631 }); 2632 } 2633 catch (ex) { 2634 return null; 2635 } 2636 // Avoid looping 2637 if (chan.URI.equals(aURI)) { 2638 return null; 2639 } 2640 // We want to clone the channel URI to avoid accidentially keeping 2641 // unnecessary references to the channel or implementation details 2642 // around. 2643 return this._resolveURIToFile(chan.URI.clone()); 2644 2645 default: 2646 return null; 2647 } 2648 }, 2649 2650 /** 2651 * Starts the XPI provider initializes the install locations and prefs. 2652 * 2653 * @param aAppChanged 2654 * A tri-state value. Undefined means the current profile was created 2655 * for this session, true means the profile already existed but was 2656 * last used with an application with a different version number, 2657 * false means that the profile was last used by this version of the 2658 * application. 2659 * @param aOldAppVersion 2660 * The version of the application last run with this profile or null 2661 * if it is a new profile or the version is unknown 2662 * @param aOldPlatformVersion 2663 * The version of the platform last run with this profile or null 2664 * if it is a new profile or the version is unknown 2665 */ 2666 startup: function(aAppChanged, aOldAppVersion, aOldPlatformVersion) { 2667 function addDirectoryInstallLocation(aName, aKey, aPaths, aScope, aLocked) { 2668 try { 2669 var dir = FileUtils.getDir(aKey, aPaths); 2670 } 2671 catch (e) { 2672 // Some directories aren't defined on some platforms, ignore them 2673 logger.debug("Skipping unavailable install location " + aName); 2674 return; 2675 } 2676 2677 try { 2678 var location = aLocked ? new DirectoryInstallLocation(aName, dir, aScope) 2679 : new MutableDirectoryInstallLocation(aName, dir, aScope); 2680 } 2681 catch (e) { 2682 logger.warn("Failed to add directory install location " + aName, e); 2683 return; 2684 } 2685 2686 XPIProvider.installLocations.push(location); 2687 XPIProvider.installLocationsByName[location.name] = location; 2688 } 2689 2690 function addSystemAddonInstallLocation(aName, aKey, aPaths, aScope) { 2691 try { 2692 var dir = FileUtils.getDir(aKey, aPaths); 2693 } 2694 catch (e) { 2695 // Some directories aren't defined on some platforms, ignore them 2696 logger.debug("Skipping unavailable install location " + aName); 2697 return; 2698 } 2699 2700 try { 2701 var location = new SystemAddonInstallLocation(aName, dir, aScope, aAppChanged !== false); 2702 } 2703 catch (e) { 2704 logger.warn("Failed to add system add-on install location " + aName, e); 2705 return; 2706 } 2707 2708 XPIProvider.installLocations.push(location); 2709 XPIProvider.installLocationsByName[location.name] = location; 2710 } 2711 2712 function addRegistryInstallLocation(aName, aRootkey, aScope) { 2713 try { 2714 var location = new WinRegInstallLocation(aName, aRootkey, aScope); 2715 } 2716 catch (e) { 2717 logger.warn("Failed to add registry install location " + aName, e); 2718 return; 2719 } 2720 2721 XPIProvider.installLocations.push(location); 2722 XPIProvider.installLocationsByName[location.name] = location; 2723 } 2724 2725 try { 2726 AddonManagerPrivate.recordTimestamp("XPI_startup_begin"); 2727 2728 logger.debug("startup"); 2729 this.runPhase = XPI_STARTING; 2730 this.installs = new Set(); 2731 this.installLocations = []; 2732 this.installLocationsByName = {}; 2733 // Hook for tests to detect when saving database at shutdown time fails 2734 this._shutdownError = null; 2735 // Clear this at startup for xpcshell test restarts 2736 this._telemetryDetails = {}; 2737 // Register our details structure with AddonManager 2738 AddonManagerPrivate.setTelemetryDetails("XPI", this._telemetryDetails); 2739 2740 let hasRegistry = ("nsIWindowsRegKey" in Ci); 2741 2742 let enabledScopes = Preferences.get(PREF_EM_ENABLED_SCOPES, 2743 AddonManager.SCOPE_ALL); 2744 2745 // These must be in order of priority, highest to lowest, 2746 // for processFileChanges etc. to work 2747 2748 XPIProvider.installLocations.push(TemporaryInstallLocation); 2749 XPIProvider.installLocationsByName[TemporaryInstallLocation.name] = 2750 TemporaryInstallLocation; 2751 2752 // The profile location is always enabled 2753 addDirectoryInstallLocation(KEY_APP_PROFILE, KEY_PROFILEDIR, 2754 [DIR_EXTENSIONS], 2755 AddonManager.SCOPE_PROFILE, false); 2756 2757 addSystemAddonInstallLocation(KEY_APP_SYSTEM_ADDONS, KEY_PROFILEDIR, 2758 [DIR_SYSTEM_ADDONS], 2759 AddonManager.SCOPE_PROFILE); 2760 2761 addDirectoryInstallLocation(KEY_APP_SYSTEM_DEFAULTS, KEY_APP_FEATURES, 2762 [], AddonManager.SCOPE_PROFILE, true); 2763 2764 if (enabledScopes & AddonManager.SCOPE_USER) { 2765 addDirectoryInstallLocation(KEY_APP_SYSTEM_USER, "XREUSysExt", 2766 [Services.appinfo.ID], 2767 AddonManager.SCOPE_USER, true); 2768 if (hasRegistry) { 2769 addRegistryInstallLocation("winreg-app-user", 2770 Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, 2771 AddonManager.SCOPE_USER); 2772 } 2773 } 2774 2775 addDirectoryInstallLocation(KEY_APP_GLOBAL, KEY_ADDON_APP_DIR, 2776 [DIR_EXTENSIONS], 2777 AddonManager.SCOPE_APPLICATION, true); 2778 2779 if (enabledScopes & AddonManager.SCOPE_SYSTEM) { 2780 addDirectoryInstallLocation(KEY_APP_SYSTEM_SHARE, "XRESysSExtPD", 2781 [Services.appinfo.ID], 2782 AddonManager.SCOPE_SYSTEM, true); 2783 addDirectoryInstallLocation(KEY_APP_SYSTEM_LOCAL, "XRESysLExtPD", 2784 [Services.appinfo.ID], 2785 AddonManager.SCOPE_SYSTEM, true); 2786 if (hasRegistry) { 2787 addRegistryInstallLocation("winreg-app-global", 2788 Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE, 2789 AddonManager.SCOPE_SYSTEM); 2790 } 2791 } 2792 2793 let defaultPrefs = new Preferences({ defaultBranch: true }); 2794 this.defaultSkin = defaultPrefs.get(PREF_GENERAL_SKINS_SELECTEDSKIN, 2795 "classic/1.0"); 2796 this.currentSkin = Preferences.get(PREF_GENERAL_SKINS_SELECTEDSKIN, 2797 this.defaultSkin); 2798 this.selectedSkin = this.currentSkin; 2799 this.applyThemeChange(); 2800 2801 this.minCompatibleAppVersion = Preferences.get(PREF_EM_MIN_COMPAT_APP_VERSION, 2802 null); 2803 this.minCompatiblePlatformVersion = Preferences.get(PREF_EM_MIN_COMPAT_PLATFORM_VERSION, 2804 null); 2805 this.enabledAddons = ""; 2806 2807 Services.prefs.addObserver(PREF_EM_MIN_COMPAT_APP_VERSION, this, false); 2808 Services.prefs.addObserver(PREF_EM_MIN_COMPAT_PLATFORM_VERSION, this, false); 2809 Services.prefs.addObserver(PREF_E10S_ADDON_BLOCKLIST, this, false); 2810 Services.prefs.addObserver(PREF_E10S_ADDON_POLICY, this, false); 2811 if (!REQUIRE_SIGNING) 2812 Services.prefs.addObserver(PREF_XPI_SIGNATURES_REQUIRED, this, false); 2813 Services.obs.addObserver(this, NOTIFICATION_FLUSH_PERMISSIONS, false); 2814 2815 // Cu.isModuleLoaded can fail here for external XUL apps where there is 2816 // no chrome.manifest that defines resource://devtools. 2817 if (ResProtocolHandler.hasSubstitution("devtools")) { 2818 if (Cu.isModuleLoaded("resource://devtools/client/framework/ToolboxProcess.jsm")) { 2819 // If BrowserToolboxProcess is already loaded, set the boolean to true 2820 // and do whatever is needed 2821 this._toolboxProcessLoaded = true; 2822 BrowserToolboxProcess.on("connectionchange", 2823 this.onDebugConnectionChange.bind(this)); 2824 } else { 2825 // Else, wait for it to load 2826 Services.obs.addObserver(this, NOTIFICATION_TOOLBOXPROCESS_LOADED, false); 2827 } 2828 } 2829 2830 let flushCaches = this.checkForChanges(aAppChanged, aOldAppVersion, 2831 aOldPlatformVersion); 2832 2833 // Changes to installed extensions may have changed which theme is selected 2834 this.applyThemeChange(); 2835 2836 AddonManagerPrivate.markProviderSafe(this); 2837 2838 if (aAppChanged && !this.allAppGlobal && 2839 Preferences.get(PREF_EM_SHOW_MISMATCH_UI, true)) { 2840 let addonsToUpdate = this.shouldForceUpdateCheck(aAppChanged); 2841 if (addonsToUpdate) { 2842 this.showUpgradeUI(addonsToUpdate); 2843 flushCaches = true; 2844 } 2845 } 2846 2847 if (flushCaches) { 2848 Services.obs.notifyObservers(null, "startupcache-invalidate", null); 2849 // UI displayed early in startup (like the compatibility UI) may have 2850 // caused us to cache parts of the skin or locale in memory. These must 2851 // be flushed to allow extension provided skins and locales to take full 2852 // effect 2853 Services.obs.notifyObservers(null, "chrome-flush-skin-caches", null); 2854 Services.obs.notifyObservers(null, "chrome-flush-caches", null); 2855 } 2856 2857 this.enabledAddons = Preferences.get(PREF_EM_ENABLED_ADDONS, ""); 2858 2859 if ("nsICrashReporter" in Ci && 2860 Services.appinfo instanceof Ci.nsICrashReporter) { 2861 // Annotate the crash report with relevant add-on information. 2862 try { 2863 Services.appinfo.annotateCrashReport("Theme", this.currentSkin); 2864 } catch (e) { } 2865 try { 2866 Services.appinfo.annotateCrashReport("EMCheckCompatibility", 2867 AddonManager.checkCompatibility); 2868 } catch (e) { } 2869 this.addAddonsToCrashReporter(); 2870 } 2871 2872 try { 2873 AddonManagerPrivate.recordTimestamp("XPI_bootstrap_addons_begin"); 2874 2875 for (let addon of this.sortBootstrappedAddons()) { 2876 try { 2877 let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); 2878 file.persistentDescriptor = addon.descriptor; 2879 let reason = BOOTSTRAP_REASONS.APP_STARTUP; 2880 // Eventually set INSTALLED reason when a bootstrap addon 2881 // is dropped in profile folder and automatically installed 2882 if (AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_INSTALLED) 2883 .indexOf(addon.id) !== -1) 2884 reason = BOOTSTRAP_REASONS.ADDON_INSTALL; 2885 this.callBootstrapMethod(createAddonDetails(addon.id, addon), 2886 file, "startup", reason); 2887 } 2888 catch (e) { 2889 logger.error("Failed to load bootstrap addon " + addon.id + " from " + 2890 addon.descriptor, e); 2891 } 2892 } 2893 AddonManagerPrivate.recordTimestamp("XPI_bootstrap_addons_end"); 2894 } 2895 catch (e) { 2896 logger.error("bootstrap startup failed", e); 2897 AddonManagerPrivate.recordException("XPI-BOOTSTRAP", "startup failed", e); 2898 } 2899 2900 // Let these shutdown a little earlier when they still have access to most 2901 // of XPCOM 2902 Services.obs.addObserver({ 2903 observe: function(aSubject, aTopic, aData) { 2904 XPIProvider._closing = true; 2905 for (let addon of XPIProvider.sortBootstrappedAddons().reverse()) { 2906 // If no scope has been loaded for this add-on then there is no need 2907 // to shut it down (should only happen when a bootstrapped add-on is 2908 // pending enable) 2909 if (!XPIProvider.activeAddons.has(addon.id)) 2910 continue; 2911 2912 let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); 2913 file.persistentDescriptor = addon.descriptor; 2914 let addonDetails = createAddonDetails(addon.id, addon); 2915 2916 // If the add-on was pending disable then shut it down and remove it 2917 // from the persisted data. 2918 if (addon.disable) { 2919 XPIProvider.callBootstrapMethod(addonDetails, file, "shutdown", 2920 BOOTSTRAP_REASONS.ADDON_DISABLE); 2921 delete XPIProvider.bootstrappedAddons[addon.id]; 2922 } 2923 else { 2924 XPIProvider.callBootstrapMethod(addonDetails, file, "shutdown", 2925 BOOTSTRAP_REASONS.APP_SHUTDOWN); 2926 } 2927 } 2928 Services.obs.removeObserver(this, "quit-application-granted"); 2929 } 2930 }, "quit-application-granted", false); 2931 2932 // Detect final-ui-startup for telemetry reporting 2933 Services.obs.addObserver({ 2934 observe: function(aSubject, aTopic, aData) { 2935 AddonManagerPrivate.recordTimestamp("XPI_finalUIStartup"); 2936 XPIProvider.runPhase = XPI_AFTER_UI_STARTUP; 2937 Services.obs.removeObserver(this, "final-ui-startup"); 2938 } 2939 }, "final-ui-startup", false); 2940 2941 AddonManagerPrivate.recordTimestamp("XPI_startup_end"); 2942 2943 this.extensionsActive = true; 2944 this.runPhase = XPI_BEFORE_UI_STARTUP; 2945 2946 let timerManager = Cc["@mozilla.org/updates/timer-manager;1"]. 2947 getService(Ci.nsIUpdateTimerManager); 2948 timerManager.registerTimer("xpi-signature-verification", () => { 2949 this.verifySignatures(); 2950 }, XPI_SIGNATURE_CHECK_PERIOD); 2951 } 2952 catch (e) { 2953 logger.error("startup failed", e); 2954 AddonManagerPrivate.recordException("XPI", "startup failed", e); 2955 } 2956 }, 2957 2958 /** 2959 * Shuts down the database and releases all references. 2960 * Return: Promise{integer} resolves / rejects with the result of 2961 * flushing the XPI Database if it was loaded, 2962 * 0 otherwise. 2963 */ 2964 shutdown: function() { 2965 logger.debug("shutdown"); 2966 2967 // Stop anything we were doing asynchronously 2968 this.cancelAll(); 2969 2970 this.bootstrappedAddons = {}; 2971 this.activeAddons.clear(); 2972 this.enabledAddons = null; 2973 this.allAppGlobal = true; 2974 2975 // If there are pending operations then we must update the list of active 2976 // add-ons 2977 if (Preferences.get(PREF_PENDING_OPERATIONS, false)) { 2978 AddonManagerPrivate.recordSimpleMeasure("XPIDB_pending_ops", 1); 2979 XPIDatabase.updateActiveAddons(); 2980 Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, 2981 !XPIDatabase.writeAddonsList()); 2982 } 2983 2984 this.installs = null; 2985 this.installLocations = null; 2986 this.installLocationsByName = null; 2987 2988 // This is needed to allow xpcshell tests to simulate a restart 2989 this.extensionsActive = false; 2990 this._addonFileMap.clear(); 2991 2992 if (gLazyObjectsLoaded) { 2993 let done = XPIDatabase.shutdown(); 2994 done.then( 2995 ret => { 2996 logger.debug("Notifying XPI shutdown observers"); 2997 Services.obs.notifyObservers(null, "xpi-provider-shutdown", null); 2998 }, 2999 err => { 3000 logger.debug("Notifying XPI shutdown observers"); 3001 this._shutdownError = err; 3002 Services.obs.notifyObservers(null, "xpi-provider-shutdown", err); 3003 } 3004 ); 3005 return done; 3006 } 3007 logger.debug("Notifying XPI shutdown observers"); 3008 Services.obs.notifyObservers(null, "xpi-provider-shutdown", null); 3009 return undefined; 3010 }, 3011 3012 /** 3013 * Applies any pending theme change to the preferences. 3014 */ 3015 applyThemeChange: function() { 3016 if (!Preferences.get(PREF_DSS_SWITCHPENDING, false)) 3017 return; 3018 3019 // Tell the Chrome Registry which Skin to select 3020 try { 3021 this.selectedSkin = Preferences.get(PREF_DSS_SKIN_TO_SELECT); 3022 Services.prefs.setCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN, 3023 this.selectedSkin); 3024 Services.prefs.clearUserPref(PREF_DSS_SKIN_TO_SELECT); 3025 logger.debug("Changed skin to " + this.selectedSkin); 3026 this.currentSkin = this.selectedSkin; 3027 } 3028 catch (e) { 3029 logger.error("Error applying theme change", e); 3030 } 3031 Services.prefs.clearUserPref(PREF_DSS_SWITCHPENDING); 3032 }, 3033 3034 /** 3035 * If the application has been upgraded and there are add-ons outside the 3036 * application directory then we may need to synchronize compatibility 3037 * information but only if the mismatch UI isn't disabled. 3038 * 3039 * @returns False if no update check is needed, otherwise an array of add-on 3040 * IDs to check for updates. Array may be empty if no add-ons can be/need 3041 * to be updated, but the metadata check needs to be performed. 3042 */ 3043 shouldForceUpdateCheck: function(aAppChanged) { 3044 AddonManagerPrivate.recordSimpleMeasure("XPIDB_metadata_age", AddonRepository.metadataAge()); 3045 3046 let startupChanges = AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_DISABLED); 3047 logger.debug("shouldForceUpdateCheck startupChanges: " + startupChanges.toSource()); 3048 AddonManagerPrivate.recordSimpleMeasure("XPIDB_startup_disabled", startupChanges.length); 3049 3050 let forceUpdate = []; 3051 if (startupChanges.length > 0) { 3052 let addons = XPIDatabase.getAddons(); 3053 for (let addon of addons) { 3054 if ((startupChanges.indexOf(addon.id) != -1) && 3055 (addon.permissions() & AddonManager.PERM_CAN_UPGRADE) && 3056 !addon.isCompatible) { 3057 logger.debug("shouldForceUpdateCheck: can upgrade disabled add-on " + addon.id); 3058 forceUpdate.push(addon.id); 3059 } 3060 } 3061 } 3062 3063 if (AddonRepository.isMetadataStale()) { 3064 logger.debug("shouldForceUpdateCheck: metadata is stale"); 3065 return forceUpdate; 3066 } 3067 if (forceUpdate.length > 0) { 3068 return forceUpdate; 3069 } 3070 3071 return false; 3072 }, 3073 3074 /** 3075 * Shows the "Compatibility Updates" UI. 3076 * 3077 * @param aAddonIDs 3078 * Array opf addon IDs that were disabled by the application update, and 3079 * should therefore be checked for updates. 3080 */ 3081 showUpgradeUI: function(aAddonIDs) { 3082 logger.debug("XPI_showUpgradeUI: " + aAddonIDs.toSource()); 3083 Services.telemetry.getHistogramById("ADDON_MANAGER_UPGRADE_UI_SHOWN").add(1); 3084 3085 // Flip a flag to indicate that we interrupted startup with an interactive prompt 3086 Services.startup.interrupted = true; 3087 3088 var variant = Cc["@mozilla.org/variant;1"]. 3089 createInstance(Ci.nsIWritableVariant); 3090 variant.setFromVariant(aAddonIDs); 3091 3092 // This *must* be modal as it has to block startup. 3093 var features = "chrome,centerscreen,dialog,titlebar,modal"; 3094 var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"]. 3095 getService(Ci.nsIWindowWatcher); 3096 ww.openWindow(null, URI_EXTENSION_UPDATE_DIALOG, "", features, variant); 3097 3098 // Ensure any changes to the add-ons list are flushed to disk 3099 Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, 3100 !XPIDatabase.writeAddonsList()); 3101 }, 3102 3103 updateSystemAddons: Task.async(function*() { 3104 let systemAddonLocation = XPIProvider.installLocationsByName[KEY_APP_SYSTEM_ADDONS]; 3105 if (!systemAddonLocation) 3106 return; 3107 3108 // Don't do anything in safe mode 3109 if (Services.appinfo.inSafeMode) 3110 return; 3111 3112 // Download the list of system add-ons 3113 let url = Preferences.get(PREF_SYSTEM_ADDON_UPDATE_URL, null); 3114 if (!url) { 3115 yield systemAddonLocation.cleanDirectories(); 3116 return; 3117 } 3118 3119 url = UpdateUtils.formatUpdateURL(url); 3120 3121 logger.info(`Starting system add-on update check from ${url}.`); 3122 let res = yield ProductAddonChecker.getProductAddonList(url); 3123 3124 // If there was no list then do nothing. 3125 if (!res || !res.gmpAddons) { 3126 logger.info("No system add-ons list was returned."); 3127 yield systemAddonLocation.cleanDirectories(); 3128 return; 3129 } 3130 3131 let addonList = new Map( 3132 res.gmpAddons.map(spec => [spec.id, { spec, path: null, addon: null }])); 3133 3134 let getAddonsInLocation = (location) => { 3135 return new Promise(resolve => { 3136 XPIDatabase.getAddonsInLocation(location, resolve); 3137 }); 3138 }; 3139 3140 let setMatches = (wanted, existing) => { 3141 if (wanted.size != existing.size) 3142 return false; 3143 3144 for (let [id, addon] of existing) { 3145 let wantedInfo = wanted.get(id); 3146 3147 if (!wantedInfo) 3148 return false; 3149 if (wantedInfo.spec.version != addon.version) 3150 return false; 3151 } 3152 3153 return true; 3154 }; 3155 3156 // If this matches the current set in the profile location then do nothing. 3157 let updatedAddons = addonMap(yield getAddonsInLocation(KEY_APP_SYSTEM_ADDONS)); 3158 if (setMatches(addonList, updatedAddons)) { 3159 logger.info("Retaining existing updated system add-ons."); 3160 yield systemAddonLocation.cleanDirectories(); 3161 return; 3162 } 3163 3164 // If this matches the current set in the default location then reset the 3165 // updated set. 3166 let defaultAddons = addonMap(yield getAddonsInLocation(KEY_APP_SYSTEM_DEFAULTS)); 3167 if (setMatches(addonList, defaultAddons)) { 3168 logger.info("Resetting system add-ons."); 3169 systemAddonLocation.resetAddonSet(); 3170 yield systemAddonLocation.cleanDirectories(); 3171 return; 3172 } 3173 3174 // Download all the add-ons 3175 let downloadAddon = Task.async(function*(item) { 3176 try { 3177 let sourceAddon = updatedAddons.get(item.spec.id); 3178 if (sourceAddon && sourceAddon.version == item.spec.version) { 3179 // Copying the file to a temporary location has some benefits. If the 3180 // file is locked and cannot be read then we'll fall back to 3181 // downloading a fresh copy. It also means we don't have to remember 3182 // whether to delete the temporary copy later. 3183 try { 3184 let path = OS.Path.join(OS.Constants.Path.tmpDir, "tmpaddon"); 3185 let unique = yield OS.File.openUnique(path); 3186 unique.file.close(); 3187 yield OS.File.copy(sourceAddon._sourceBundle.path, unique.path); 3188 // Make sure to update file modification times so this is detected 3189 // as a new add-on. 3190 yield OS.File.setDates(unique.path); 3191 item.path = unique.path; 3192 } 3193 catch (e) { 3194 logger.warn(`Failed make temporary copy of ${sourceAddon._sourceBundle.path}.`, e); 3195 } 3196 } 3197 if (!item.path) { 3198 item.path = yield ProductAddonChecker.downloadAddon(item.spec); 3199 } 3200 item.addon = yield loadManifestFromFile(nsIFile(item.path), systemAddonLocation); 3201 } 3202 catch (e) { 3203 logger.error(`Failed to download system add-on ${item.spec.id}`, e); 3204 } 3205 }); 3206 yield Promise.all(Array.from(addonList.values()).map(downloadAddon)); 3207 3208 // The download promises all resolve regardless, now check if they all 3209 // succeeded 3210 let validateAddon = (item) => { 3211 if (item.spec.id != item.addon.id) { 3212 logger.warn(`Downloaded system add-on expected to be ${item.spec.id} but was ${item.addon.id}.`); 3213 return false; 3214 } 3215 3216 if (item.spec.version != item.addon.version) { 3217 logger.warn(`Expected system add-on ${item.spec.id} to be version ${item.spec.version} but was ${item.addon.version}.`); 3218 return false; 3219 } 3220 3221 if (!systemAddonLocation.isValidAddon(item.addon)) 3222 return false; 3223 3224 return true; 3225 } 3226 3227 if (!Array.from(addonList.values()).every(item => item.path && item.addon && validateAddon(item))) { 3228 throw new Error("Rejecting updated system add-on set that either could not " + 3229 "be downloaded or contained unusable add-ons."); 3230 } 3231 3232 // Install into the install location 3233 logger.info("Installing new system add-on set"); 3234 yield systemAddonLocation.installAddonSet(Array.from(addonList.values()) 3235 .map(a => a.addon)); 3236 }), 3237 3238 /** 3239 * Verifies that all installed add-ons are still correctly signed. 3240 */ 3241 verifySignatures: function() { 3242 XPIDatabase.getAddonList(a => true, (addons) => { 3243 Task.spawn(function*() { 3244 let changes = { 3245 enabled: [], 3246 disabled: [] 3247 }; 3248 3249 for (let addon of addons) { 3250 // The add-on might have vanished, we'll catch that on the next startup 3251 if (!addon._sourceBundle.exists()) 3252 continue; 3253 3254 let signedState = yield verifyBundleSignedState(addon._sourceBundle, addon); 3255 3256 if (signedState != addon.signedState) { 3257 addon.signedState = signedState; 3258 AddonManagerPrivate.callAddonListeners("onPropertyChanged", 3259 addon.wrapper, 3260 ["signedState"]); 3261 } 3262 3263 let disabled = XPIProvider.updateAddonDisabledState(addon); 3264 if (disabled !== undefined) 3265 changes[disabled ? "disabled" : "enabled"].push(addon.id); 3266 } 3267 3268 XPIDatabase.saveChanges(); 3269 3270 Services.obs.notifyObservers(null, "xpi-signature-changed", JSON.stringify(changes)); 3271 }).then(null, err => { 3272 logger.error("XPI_verifySignature: " + err); 3273 }) 3274 }); 3275 }, 3276 3277 /** 3278 * Persists changes to XPIProvider.bootstrappedAddons to its store (a pref). 3279 */ 3280 persistBootstrappedAddons: function() { 3281 // Experiments are disabled upon app load, so don't persist references. 3282 let filtered = {}; 3283 for (let id in this.bootstrappedAddons) { 3284 let entry = this.bootstrappedAddons[id]; 3285 if (entry.type == "experiment") { 3286 continue; 3287 } 3288 3289 filtered[id] = entry; 3290 } 3291 3292 Services.prefs.setCharPref(PREF_BOOTSTRAP_ADDONS, 3293 JSON.stringify(filtered)); 3294 }, 3295 3296 /** 3297 * Adds a list of currently active add-ons to the next crash report. 3298 */ 3299 addAddonsToCrashReporter: function() { 3300 if (!("nsICrashReporter" in Ci) || 3301 !(Services.appinfo instanceof Ci.nsICrashReporter)) 3302 return; 3303 3304 // In safe mode no add-ons are loaded so we should not include them in the 3305 // crash report 3306 if (Services.appinfo.inSafeMode) 3307 return; 3308 3309 let data = this.enabledAddons; 3310 for (let id in this.bootstrappedAddons) { 3311 data += (data ? "," : "") + encodeURIComponent(id) + ":" + 3312 encodeURIComponent(this.bootstrappedAddons[id].version); 3313 } 3314 3315 try { 3316 Services.appinfo.annotateCrashReport("Add-ons", data); 3317 } 3318 catch (e) { } 3319 3320 let TelemetrySession = 3321 Cu.import("resource://gre/modules/TelemetrySession.jsm", {}).TelemetrySession; 3322 TelemetrySession.setAddOns(data); 3323 }, 3324 3325 /** 3326 * Check the staging directories of install locations for any add-ons to be 3327 * installed or add-ons to be uninstalled. 3328 * 3329 * @param aManifests 3330 * A dictionary to add detected install manifests to for the purpose 3331 * of passing through updated compatibility information 3332 * @return true if an add-on was installed or uninstalled 3333 */ 3334 processPendingFileChanges: function(aManifests) { 3335 let changed = false; 3336 for (let location of this.installLocations) { 3337 aManifests[location.name] = {}; 3338 // We can't install or uninstall anything in locked locations 3339 if (location.locked) { 3340 continue; 3341 } 3342 3343 let stagingDir = location.getStagingDir(); 3344 3345 try { 3346 if (!stagingDir || !stagingDir.exists() || !stagingDir.isDirectory()) 3347 continue; 3348 } 3349 catch (e) { 3350 logger.warn("Failed to find staging directory", e); 3351 continue; 3352 } 3353 3354 let seenFiles = []; 3355 // Use a snapshot of the directory contents to avoid possible issues with 3356 // iterating over a directory while removing files from it (the YAFFS2 3357 // embedded filesystem has this issue, see bug 772238), and to remove 3358 // normal files before their resource forks on OSX (see bug 733436). 3359 let stagingDirEntries = getDirectoryEntries(stagingDir, true); 3360 for (let stageDirEntry of stagingDirEntries) { 3361 let id = stageDirEntry.leafName; 3362 3363 let isDir; 3364 try { 3365 isDir = stageDirEntry.isDirectory(); 3366 } 3367 catch (e) { 3368 if (e.result != Cr.NS_ERROR_FILE_TARGET_DOES_NOT_EXIST) 3369 throw e; 3370 // If the file has already gone away then don't worry about it, this 3371 // can happen on OSX where the resource fork is automatically moved 3372 // with the data fork for the file. See bug 733436. 3373 continue; 3374 } 3375 3376 if (!isDir) { 3377 if (id.substring(id.length - 4).toLowerCase() == ".xpi") { 3378 id = id.substring(0, id.length - 4); 3379 } 3380 else { 3381 if (id.substring(id.length - 5).toLowerCase() != ".json") { 3382 logger.warn("Ignoring file: " + stageDirEntry.path); 3383 seenFiles.push(stageDirEntry.leafName); 3384 } 3385 continue; 3386 } 3387 } 3388 3389 // Check that the directory's name is a valid ID. 3390 if (!gIDTest.test(id)) { 3391 logger.warn("Ignoring directory whose name is not a valid add-on ID: " + 3392 stageDirEntry.path); 3393 seenFiles.push(stageDirEntry.leafName); 3394 continue; 3395 } 3396 3397 changed = true; 3398 3399 if (isDir) { 3400 // Check if the directory contains an install manifest. 3401 let manifest = getManifestFileForDir(stageDirEntry); 3402 3403 // If the install manifest doesn't exist uninstall this add-on in this 3404 // install location. 3405 if (!manifest) { 3406 logger.debug("Processing uninstall of " + id + " in " + location.name); 3407 3408 try { 3409 let addonFile = location.getLocationForID(id); 3410 let addonToUninstall = syncLoadManifestFromFile(addonFile, location); 3411 if (addonToUninstall.bootstrap) { 3412 this.callBootstrapMethod(addonToUninstall, addonToUninstall._sourceBundle, 3413 "uninstall", BOOTSTRAP_REASONS.ADDON_UNINSTALL); 3414 } 3415 } 3416 catch (e) { 3417 logger.warn("Failed to call uninstall for " + id, e); 3418 } 3419 3420 try { 3421 location.uninstallAddon(id); 3422 seenFiles.push(stageDirEntry.leafName); 3423 } 3424 catch (e) { 3425 logger.error("Failed to uninstall add-on " + id + " in " + location.name, e); 3426 } 3427 // The file check later will spot the removal and cleanup the database 3428 continue; 3429 } 3430 } 3431 3432 aManifests[location.name][id] = null; 3433 let existingAddonID = id; 3434 3435 let jsonfile = stagingDir.clone(); 3436 jsonfile.append(id + ".json"); 3437 // Assume this was a foreign install if there is no cached metadata file 3438 let foreignInstall = !jsonfile.exists(); 3439 let addon; 3440 3441 try { 3442 addon = syncLoadManifestFromFile(stageDirEntry, location); 3443 } 3444 catch (e) { 3445 logger.error("Unable to read add-on manifest from " + stageDirEntry.path, e); 3446 // This add-on can't be installed so just remove it now 3447 seenFiles.push(stageDirEntry.leafName); 3448 seenFiles.push(jsonfile.leafName); 3449 continue; 3450 } 3451 3452 if (mustSign(addon.type) && 3453 addon.signedState <= AddonManager.SIGNEDSTATE_MISSING) { 3454 logger.warn("Refusing to install staged add-on " + id + " with signed state " + addon.signedState); 3455 seenFiles.push(stageDirEntry.leafName); 3456 seenFiles.push(jsonfile.leafName); 3457 continue; 3458 } 3459 3460 // Check for a cached metadata for this add-on, it may contain updated 3461 // compatibility information 3462 if (!foreignInstall) { 3463 logger.debug("Found updated metadata for " + id + " in " + location.name); 3464 let fis = Cc["@mozilla.org/network/file-input-stream;1"]. 3465 createInstance(Ci.nsIFileInputStream); 3466 let json = Cc["@mozilla.org/dom/json;1"]. 3467 createInstance(Ci.nsIJSON); 3468 3469 try { 3470 fis.init(jsonfile, -1, 0, 0); 3471 let metadata = json.decodeFromStream(fis, jsonfile.fileSize); 3472 addon.importMetadata(metadata); 3473 3474 // Pass this through to addMetadata so it knows this add-on was 3475 // likely installed through the UI 3476 aManifests[location.name][id] = addon; 3477 } 3478 catch (e) { 3479 // If some data can't be recovered from the cached metadata then it 3480 // is unlikely to be a problem big enough to justify throwing away 3481 // the install, just log an error and continue 3482 logger.error("Unable to read metadata from " + jsonfile.path, e); 3483 } 3484 finally { 3485 fis.close(); 3486 } 3487 } 3488 seenFiles.push(jsonfile.leafName); 3489 3490 existingAddonID = addon.existingAddonID || id; 3491 3492 var oldBootstrap = null; 3493 logger.debug("Processing install of " + id + " in " + location.name); 3494 if (existingAddonID in this.bootstrappedAddons) { 3495 try { 3496 var existingAddon = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); 3497 existingAddon.persistentDescriptor = this.bootstrappedAddons[existingAddonID].descriptor; 3498 if (existingAddon.exists()) { 3499 oldBootstrap = this.bootstrappedAddons[existingAddonID]; 3500 3501 // We'll be replacing a currently active bootstrapped add-on so 3502 // call its uninstall method 3503 let newVersion = addon.version; 3504 let oldVersion = oldBootstrap.version; 3505 let uninstallReason = Services.vc.compare(oldVersion, newVersion) < 0 ? 3506 BOOTSTRAP_REASONS.ADDON_UPGRADE : 3507 BOOTSTRAP_REASONS.ADDON_DOWNGRADE; 3508 3509 this.callBootstrapMethod(createAddonDetails(existingAddonID, oldBootstrap), 3510 existingAddon, "uninstall", uninstallReason, 3511 { newVersion: newVersion }); 3512 this.unloadBootstrapScope(existingAddonID); 3513 flushChromeCaches(); 3514 } 3515 } 3516 catch (e) { 3517 } 3518 } 3519 3520 try { 3521 addon._sourceBundle = location.installAddon({ 3522 id, 3523 source: stageDirEntry, 3524 existingAddonID 3525 }); 3526 } 3527 catch (e) { 3528 logger.error("Failed to install staged add-on " + id + " in " + location.name, 3529 e); 3530 // Re-create the staged install 3531 new StagedAddonInstall(location, stageDirEntry, addon); 3532 // Make sure not to delete the cached manifest json file 3533 seenFiles.pop(); 3534 3535 delete aManifests[location.name][id]; 3536 3537 if (oldBootstrap) { 3538 // Re-install the old add-on 3539 this.callBootstrapMethod(createAddonDetails(existingAddonID, oldBootstrap), 3540 existingAddon, "install", 3541 BOOTSTRAP_REASONS.ADDON_INSTALL); 3542 } 3543 continue; 3544 } 3545 } 3546 3547 try { 3548 location.cleanStagingDir(seenFiles); 3549 } 3550 catch (e) { 3551 // Non-critical, just saves some perf on startup if we clean this up. 3552 logger.debug("Error cleaning staging dir " + stagingDir.path, e); 3553 } 3554 } 3555 return changed; 3556 }, 3557 3558 /** 3559 * Installs any add-ons located in the extensions directory of the 3560 * application's distribution specific directory into the profile unless a 3561 * newer version already exists or the user has previously uninstalled the 3562 * distributed add-on. 3563 * 3564 * @param aManifests 3565 * A dictionary to add new install manifests to to save having to 3566 * reload them later 3567 * @param aAppChanged 3568 * See checkForChanges 3569 * @return true if any new add-ons were installed 3570 */ 3571 installDistributionAddons: function(aManifests, aAppChanged) { 3572 let distroDir; 3573 try { 3574 distroDir = FileUtils.getDir(KEY_APP_DISTRIBUTION, [DIR_EXTENSIONS]); 3575 } 3576 catch (e) { 3577 return false; 3578 } 3579 3580 if (!distroDir.exists()) 3581 return false; 3582 3583 if (!distroDir.isDirectory()) 3584 return false; 3585 3586 let changed = false; 3587 let profileLocation = this.installLocationsByName[KEY_APP_PROFILE]; 3588 3589 let entries = distroDir.directoryEntries 3590 .QueryInterface(Ci.nsIDirectoryEnumerator); 3591 let entry; 3592 while ((entry = entries.nextFile)) { 3593 3594 let id = entry.leafName; 3595 3596 if (entry.isFile()) { 3597 if (id.substring(id.length - 4).toLowerCase() == ".xpi") { 3598 id = id.substring(0, id.length - 4); 3599 } 3600 else { 3601 logger.debug("Ignoring distribution add-on that isn't an XPI: " + entry.path); 3602 continue; 3603 } 3604 } 3605 else if (!entry.isDirectory()) { 3606 logger.debug("Ignoring distribution add-on that isn't a file or directory: " + 3607 entry.path); 3608 continue; 3609 } 3610 3611 if (!gIDTest.test(id)) { 3612 logger.debug("Ignoring distribution add-on whose name is not a valid add-on ID: " + 3613 entry.path); 3614 continue; 3615 } 3616 3617 /* If this is not an upgrade and we've already handled this extension 3618 * just continue */ 3619 if (!aAppChanged && Preferences.isSet(PREF_BRANCH_INSTALLED_ADDON + id)) { 3620 continue; 3621 } 3622 3623 let addon; 3624 try { 3625 addon = syncLoadManifestFromFile(entry, profileLocation); 3626 } 3627 catch (e) { 3628 logger.warn("File entry " + entry.path + " contains an invalid add-on", e); 3629 continue; 3630 } 3631 3632 if (addon.id != id) { 3633 logger.warn("File entry " + entry.path + " contains an add-on with an " + 3634 "incorrect ID") 3635 continue; 3636 } 3637 3638 let existingEntry = null; 3639 try { 3640 existingEntry = profileLocation.getLocationForID(id); 3641 } 3642 catch (e) { 3643 } 3644 3645 if (existingEntry) { 3646 let existingAddon; 3647 try { 3648 existingAddon = syncLoadManifestFromFile(existingEntry, profileLocation); 3649 3650 if (Services.vc.compare(addon.version, existingAddon.version) <= 0) 3651 continue; 3652 } 3653 catch (e) { 3654 // Bad add-on in the profile so just proceed and install over the top 3655 logger.warn("Profile contains an add-on with a bad or missing install " + 3656 "manifest at " + existingEntry.path + ", overwriting", e); 3657 } 3658 } 3659 else if (Preferences.get(PREF_BRANCH_INSTALLED_ADDON + id, false)) { 3660 continue; 3661 } 3662 3663 // Install the add-on 3664 try { 3665 addon._sourceBundle = profileLocation.installAddon({ id, source: entry, action: "copy" }); 3666 logger.debug("Installed distribution add-on " + id); 3667 3668 Services.prefs.setBoolPref(PREF_BRANCH_INSTALLED_ADDON + id, true) 3669 3670 // aManifests may contain a copy of a newly installed add-on's manifest 3671 // and we'll have overwritten that so instead cache our install manifest 3672 // which will later be put into the database in processFileChanges 3673 if (!(KEY_APP_PROFILE in aManifests)) 3674 aManifests[KEY_APP_PROFILE] = {}; 3675 aManifests[KEY_APP_PROFILE][id] = addon; 3676 changed = true; 3677 } 3678 catch (e) { 3679 logger.error("Failed to install distribution add-on " + entry.path, e); 3680 } 3681 } 3682 3683 entries.close(); 3684 3685 return changed; 3686 }, 3687 3688 /** 3689 * Imports the xpinstall permissions from preferences into the permissions 3690 * manager for the user to change later. 3691 */ 3692 importPermissions: function() { 3693 PermissionsUtils.importFromPrefs(PREF_XPI_PERMISSIONS_BRANCH, 3694 XPI_PERMISSION); 3695 }, 3696 3697 getDependentAddons: function(aAddon) { 3698 return Array.from(XPIDatabase.getAddons()) 3699 .filter(addon => addon.dependencies.includes(aAddon.id)); 3700 }, 3701 3702 /** 3703 * Checks for any changes that have occurred since the last time the 3704 * application was launched. 3705 * 3706 * @param aAppChanged 3707 * A tri-state value. Undefined means the current profile was created 3708 * for this session, true means the profile already existed but was 3709 * last used with an application with a different version number, 3710 * false means that the profile was last used by this version of the 3711 * application. 3712 * @param aOldAppVersion 3713 * The version of the application last run with this profile or null 3714 * if it is a new profile or the version is unknown 3715 * @param aOldPlatformVersion 3716 * The version of the platform last run with this profile or null 3717 * if it is a new profile or the version is unknown 3718 * @return true if a change requiring a restart was detected 3719 */ 3720 checkForChanges: function(aAppChanged, aOldAppVersion, 3721 aOldPlatformVersion) { 3722 logger.debug("checkForChanges"); 3723 3724 // Keep track of whether and why we need to open and update the database at 3725 // startup time. 3726 let updateReasons = []; 3727 if (aAppChanged) { 3728 updateReasons.push("appChanged"); 3729 } 3730 3731 // Load the list of bootstrapped add-ons first so processFileChanges can 3732 // modify it 3733 // XXX eventually get rid of bootstrappedAddons 3734 try { 3735 this.bootstrappedAddons = JSON.parse(Preferences.get(PREF_BOOTSTRAP_ADDONS, 3736 "{}")); 3737 } catch (e) { 3738 logger.warn("Error parsing enabled bootstrapped extensions cache", e); 3739 } 3740 3741 // First install any new add-ons into the locations, if there are any 3742 // changes then we must update the database with the information in the 3743 // install locations 3744 let manifests = {}; 3745 let updated = this.processPendingFileChanges(manifests); 3746 if (updated) { 3747 updateReasons.push("pendingFileChanges"); 3748 } 3749 3750 // This will be true if the previous session made changes that affect the 3751 // active state of add-ons but didn't commit them properly (normally due 3752 // to the application crashing) 3753 let hasPendingChanges = Preferences.get(PREF_PENDING_OPERATIONS); 3754 if (hasPendingChanges) { 3755 updateReasons.push("hasPendingChanges"); 3756 } 3757 3758 // If the application has changed then check for new distribution add-ons 3759 if (Preferences.get(PREF_INSTALL_DISTRO_ADDONS, true)) 3760 { 3761 updated = this.installDistributionAddons(manifests, aAppChanged); 3762 if (updated) { 3763 updateReasons.push("installDistributionAddons"); 3764 } 3765 } 3766 3767 // Telemetry probe added around getInstallState() to check perf 3768 let telemetryCaptureTime = Cu.now(); 3769 let installChanged = XPIStates.getInstallState(); 3770 let telemetry = Services.telemetry; 3771 telemetry.getHistogramById("CHECK_ADDONS_MODIFIED_MS").add(Math.round(Cu.now() - telemetryCaptureTime)); 3772 if (installChanged) { 3773 updateReasons.push("directoryState"); 3774 } 3775 3776 let haveAnyAddons = (XPIStates.size > 0); 3777 3778 // If the schema appears to have changed then we should update the database 3779 if (DB_SCHEMA != Preferences.get(PREF_DB_SCHEMA, 0)) { 3780 // If we don't have any add-ons, just update the pref, since we don't need to 3781 // write the database 3782 if (!haveAnyAddons) { 3783 logger.debug("Empty XPI database, setting schema version preference to " + DB_SCHEMA); 3784 Services.prefs.setIntPref(PREF_DB_SCHEMA, DB_SCHEMA); 3785 } 3786 else { 3787 updateReasons.push("schemaChanged"); 3788 } 3789 } 3790 3791 // If the database doesn't exist and there are add-ons installed then we 3792 // must update the database however if there are no add-ons then there is 3793 // no need to update the database. 3794 let dbFile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_DATABASE], true); 3795 if (!dbFile.exists() && haveAnyAddons) { 3796 updateReasons.push("needNewDatabase"); 3797 } 3798 3799 // XXX This will go away when we fold bootstrappedAddons into XPIStates. 3800 if (updateReasons.length == 0) { 3801 let bootstrapDescriptors = new Set(Object.keys(this.bootstrappedAddons) 3802 .map(b => this.bootstrappedAddons[b].descriptor)); 3803 3804 for (let location of XPIStates.db.values()) { 3805 for (let state of location.values()) { 3806 bootstrapDescriptors.delete(state.descriptor); 3807 } 3808 } 3809 3810 if (bootstrapDescriptors.size > 0) { 3811 logger.warn("Bootstrap state is invalid (missing add-ons: " 3812 + Array.from(bootstrapDescriptors).join(", ") + ")"); 3813 updateReasons.push("missingBootstrapAddon"); 3814 } 3815 } 3816 3817 // Catch and log any errors during the main startup 3818 try { 3819 let extensionListChanged = false; 3820 // If the database needs to be updated then open it and then update it 3821 // from the filesystem 3822 if (updateReasons.length > 0) { 3823 AddonManagerPrivate.recordSimpleMeasure("XPIDB_startup_load_reasons", updateReasons); 3824 XPIDatabase.syncLoadDB(false); 3825 try { 3826 extensionListChanged = XPIDatabaseReconcile.processFileChanges(manifests, 3827 aAppChanged, 3828 aOldAppVersion, 3829 aOldPlatformVersion, 3830 updateReasons.includes("schemaChanged")); 3831 } 3832 catch (e) { 3833 logger.error("Failed to process extension changes at startup", e); 3834 } 3835 } 3836 3837 if (aAppChanged) { 3838 // When upgrading the app and using a custom skin make sure it is still 3839 // compatible otherwise switch back the default 3840 if (this.currentSkin != this.defaultSkin) { 3841 let oldSkin = XPIDatabase.getVisibleAddonForInternalName(this.currentSkin); 3842 if (!oldSkin || oldSkin.disabled) 3843 this.enableDefaultTheme(); 3844 } 3845 3846 // When upgrading remove the old extensions cache to force older 3847 // versions to rescan the entire list of extensions 3848 let oldCache = FileUtils.getFile(KEY_PROFILEDIR, [FILE_OLD_CACHE], true); 3849 try { 3850 if (oldCache.exists()) 3851 oldCache.remove(true); 3852 } 3853 catch (e) { 3854 logger.warn("Unable to remove old extension cache " + oldCache.path, e); 3855 } 3856 } 3857 3858 // If the application crashed before completing any pending operations then 3859 // we should perform them now. 3860 if (extensionListChanged || hasPendingChanges) { 3861 logger.debug("Updating database with changes to installed add-ons"); 3862 XPIDatabase.updateActiveAddons(); 3863 Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, 3864 !XPIDatabase.writeAddonsList()); 3865 Services.prefs.setCharPref(PREF_BOOTSTRAP_ADDONS, 3866 JSON.stringify(this.bootstrappedAddons)); 3867 return true; 3868 } 3869 3870 logger.debug("No changes found"); 3871 } 3872 catch (e) { 3873 logger.error("Error during startup file checks", e); 3874 } 3875 3876 // Check that the add-ons list still exists 3877 let addonsList = FileUtils.getFile(KEY_PROFILEDIR, [FILE_XPI_ADDONS_LIST], 3878 true); 3879 // the addons list file should exist if and only if we have add-ons installed 3880 if (addonsList.exists() != haveAnyAddons) { 3881 logger.debug("Add-ons list is invalid, rebuilding"); 3882 XPIDatabase.writeAddonsList(); 3883 } 3884 3885 return false; 3886 }, 3887 3888 /** 3889 * Called to test whether this provider supports installing a particular 3890 * mimetype. 3891 * 3892 * @param aMimetype 3893 * The mimetype to check for 3894 * @return true if the mimetype is application/x-xpinstall 3895 */ 3896 supportsMimetype: function(aMimetype) { 3897 return aMimetype == "application/x-xpinstall"; 3898 }, 3899 3900 /** 3901 * Called to test whether installing XPI add-ons is enabled. 3902 * 3903 * @return true if installing is enabled 3904 */ 3905 isInstallEnabled: function() { 3906 // Default to enabled if the preference does not exist 3907 return Preferences.get(PREF_XPI_ENABLED, true); 3908 }, 3909 3910 /** 3911 * Called to test whether installing XPI add-ons by direct URL requests is 3912 * whitelisted. 3913 * 3914 * @return true if installing by direct requests is whitelisted 3915 */ 3916 isDirectRequestWhitelisted: function() { 3917 // Default to whitelisted if the preference does not exist. 3918 return Preferences.get(PREF_XPI_DIRECT_WHITELISTED, true); 3919 }, 3920 3921 /** 3922 * Called to test whether installing XPI add-ons from file referrers is 3923 * whitelisted. 3924 * 3925 * @return true if installing from file referrers is whitelisted 3926 */ 3927 isFileRequestWhitelisted: function() { 3928 // Default to whitelisted if the preference does not exist. 3929 return Preferences.get(PREF_XPI_FILE_WHITELISTED, true); 3930 }, 3931 3932 /** 3933 * Called to test whether installing XPI add-ons from a URI is allowed. 3934 * 3935 * @param aInstallingPrincipal 3936 * The nsIPrincipal that initiated the install 3937 * @return true if installing is allowed 3938 */ 3939 isInstallAllowed: function(aInstallingPrincipal) { 3940 if (!this.isInstallEnabled()) 3941 return false; 3942 3943 let uri = aInstallingPrincipal.URI; 3944 3945 // Direct requests without a referrer are either whitelisted or blocked. 3946 if (!uri) 3947 return this.isDirectRequestWhitelisted(); 3948 3949 // Local referrers can be whitelisted. 3950 if (this.isFileRequestWhitelisted() && 3951 (uri.schemeIs("chrome") || uri.schemeIs("file"))) 3952 return true; 3953 3954 this.importPermissions(); 3955 3956 let permission = Services.perms.testPermissionFromPrincipal(aInstallingPrincipal, XPI_PERMISSION); 3957 if (permission == Ci.nsIPermissionManager.DENY_ACTION) 3958 return false; 3959 3960 let requireWhitelist = Preferences.get(PREF_XPI_WHITELIST_REQUIRED, true); 3961 if (requireWhitelist && (permission != Ci.nsIPermissionManager.ALLOW_ACTION)) 3962 return false; 3963 3964 let requireSecureOrigin = Preferences.get(PREF_INSTALL_REQUIRESECUREORIGIN, true); 3965 let safeSchemes = ["https", "chrome", "file"]; 3966 if (requireSecureOrigin && safeSchemes.indexOf(uri.scheme) == -1) 3967 return false; 3968 3969 return true; 3970 }, 3971 3972 /** 3973 * Called to get an AddonInstall to download and install an add-on from a URL. 3974 * 3975 * @param aUrl 3976 * The URL to be installed 3977 * @param aHash 3978 * A hash for the install 3979 * @param aName 3980 * A name for the install 3981 * @param aIcons 3982 * Icon URLs for the install 3983 * @param aVersion 3984 * A version for the install 3985 * @param aBrowser 3986 * The browser performing the install 3987 * @param aCallback 3988 * A callback to pass the AddonInstall to 3989 */ 3990 getInstallForURL: function(aUrl, aHash, aName, aIcons, aVersion, aBrowser, 3991 aCallback) { 3992 createDownloadInstall(function(aInstall) { 3993 aCallback(aInstall.wrapper); 3994 }, aUrl, aHash, aName, aIcons, aVersion, aBrowser); 3995 }, 3996 3997 /** 3998 * Called to get an AddonInstall to install an add-on from a local file. 3999 * 4000 * @param aFile 4001 * The file to be installed 4002 * @param aCallback 4003 * A callback to pass the AddonInstall to 4004 */ 4005 getInstallForFile: function(aFile, aCallback) { 4006 createLocalInstall(aFile).then(install => { 4007 aCallback(install ? install.wrapper : null); 4008 }); 4009 }, 4010 4011 /** 4012 * Temporarily installs add-on from a local XPI file or directory. 4013 * As this is intended for development, the signature is not checked and 4014 * the add-on does not persist on application restart. 4015 * 4016 * @param aFile 4017 * An nsIFile for the unpacked add-on directory or XPI file. 4018 * 4019 * @return See installAddonFromLocation return value. 4020 */ 4021 installTemporaryAddon: function(aFile) { 4022 return this.installAddonFromLocation(aFile, TemporaryInstallLocation); 4023 }, 4024 4025 /** 4026 * Permanently installs add-on from a local XPI file or directory. 4027 * The signature is checked but the add-on persist on application restart. 4028 * 4029 * @param aFile 4030 * An nsIFile for the unpacked add-on directory or XPI file. 4031 * 4032 * @return See installAddonFromLocation return value. 4033 */ 4034 installAddonFromSources: Task.async(function*(aFile) { 4035 let location = XPIProvider.installLocationsByName[KEY_APP_PROFILE]; 4036 return this.installAddonFromLocation(aFile, location, "proxy"); 4037 }), 4038 4039 /** 4040 * Installs add-on from a local XPI file or directory. 4041 * 4042 * @param aFile 4043 * An nsIFile for the unpacked add-on directory or XPI file. 4044 * @param aInstallLocation 4045 * Define a custom install location object to use for the install. 4046 * @param aInstallAction 4047 * Optional action mode to use when installing the addon 4048 * (see MutableDirectoryInstallLocation.installAddon) 4049 * 4050 * @return a Promise that resolves to an Addon object on success, or rejects 4051 * if the add-on is not a valid restartless add-on or if the 4052 * same ID is already installed. 4053 */ 4054 installAddonFromLocation: Task.async(function*(aFile, aInstallLocation, aInstallAction) { 4055 if (aFile.exists() && aFile.isFile()) { 4056 flushJarCache(aFile); 4057 } 4058 let addon = yield loadManifestFromFile(aFile, aInstallLocation); 4059 4060 aInstallLocation.installAddon({ id: addon.id, source: aFile, action: aInstallAction }); 4061 4062 if (addon.appDisabled) { 4063 let message = `Add-on ${addon.id} is not compatible with application version.`; 4064 4065 let app = addon.matchingTargetApplication; 4066 if (app) { 4067 if (app.minVersion) { 4068 message += ` add-on minVersion: ${app.minVersion}.`; 4069 } 4070 if (app.maxVersion) { 4071 message += ` add-on maxVersion: ${app.maxVersion}.`; 4072 } 4073 } 4074 throw new Error(message); 4075 } 4076 4077 if (!addon.bootstrap) { 4078 throw new Error("Only restartless (bootstrap) add-ons" 4079 + " can be installed from sources:", addon.id); 4080 } 4081 let installReason = BOOTSTRAP_REASONS.ADDON_INSTALL; 4082 let oldAddon = yield new Promise( 4083 resolve => XPIDatabase.getVisibleAddonForID(addon.id, resolve)); 4084 if (oldAddon) { 4085 if (!oldAddon.bootstrap) { 4086 logger.warn("Non-restartless Add-on is already installed", addon.id); 4087 throw new Error("Non-restartless add-on with ID " 4088 + oldAddon.id + " is already installed"); 4089 } 4090 else { 4091 logger.warn("Addon with ID " + oldAddon.id + " already installed," 4092 + " older version will be disabled"); 4093 4094 let existingAddonID = oldAddon.id; 4095 let existingAddon = oldAddon._sourceBundle; 4096 4097 // We'll be replacing a currently active bootstrapped add-on so 4098 // call its uninstall method 4099 let newVersion = addon.version; 4100 let oldVersion = oldAddon.version; 4101 if (Services.vc.compare(newVersion, oldVersion) >= 0) { 4102 installReason = BOOTSTRAP_REASONS.ADDON_UPGRADE; 4103 } else { 4104 installReason = BOOTSTRAP_REASONS.ADDON_DOWNGRADE; 4105 } 4106 let uninstallReason = installReason; 4107 4108 if (oldAddon.active) { 4109 XPIProvider.callBootstrapMethod(oldAddon, existingAddon, 4110 "shutdown", uninstallReason, 4111 { newVersion }); 4112 } 4113 this.callBootstrapMethod(oldAddon, existingAddon, 4114 "uninstall", uninstallReason, { newVersion }); 4115 this.unloadBootstrapScope(existingAddonID); 4116 flushChromeCaches(); 4117 } 4118 } 4119 4120 let file = addon._sourceBundle; 4121 4122 XPIProvider._addURIMapping(addon.id, file); 4123 XPIProvider.callBootstrapMethod(addon, file, "install", installReason); 4124 addon.state = AddonManager.STATE_INSTALLED; 4125 logger.debug("Install of temporary addon in " + aFile.path + " completed."); 4126 addon.visible = true; 4127 addon.enabled = true; 4128 addon.active = true; 4129 4130 addon = XPIDatabase.addAddonMetadata(addon, file.persistentDescriptor); 4131 4132 XPIStates.addAddon(addon); 4133 XPIDatabase.saveChanges(); 4134 XPIStates.save(); 4135 4136 AddonManagerPrivate.callAddonListeners("onInstalling", addon.wrapper, 4137 false); 4138 XPIProvider.callBootstrapMethod(addon, file, "startup", 4139 BOOTSTRAP_REASONS.ADDON_ENABLE); 4140 AddonManagerPrivate.callInstallListeners("onExternalInstall", 4141 null, addon.wrapper, 4142 oldAddon ? oldAddon.wrapper : null, 4143 false); 4144 AddonManagerPrivate.callAddonListeners("onInstalled", addon.wrapper); 4145 4146 return addon.wrapper; 4147 }), 4148 4149 /** 4150 * Returns an Addon corresponding to an instance ID. 4151 * @param aInstanceID 4152 * An Addon Instance ID 4153 * @return {Promise} 4154 * @resolves The found Addon or null if no such add-on exists. 4155 * @rejects Never 4156 * @throws if the aInstanceID argument is not specified 4157 */ 4158 getAddonByInstanceID: function(aInstanceID) { 4159 if (!aInstanceID || typeof aInstanceID != "symbol") 4160 throw Components.Exception("aInstanceID must be a Symbol()", 4161 Cr.NS_ERROR_INVALID_ARG); 4162 4163 for (let [id, val] of this.activeAddons) { 4164 if (aInstanceID == val.instanceID) { 4165 if (val.safeWrapper) { 4166 return Promise.resolve(val.safeWrapper); 4167 } 4168 4169 return new Promise(resolve => { 4170 this.getAddonByID(id, function(addon) { 4171 val.safeWrapper = new PrivateWrapper(addon); 4172 resolve(val.safeWrapper); 4173 }); 4174 }); 4175 } 4176 } 4177 4178 return Promise.resolve(null); 4179 }, 4180 4181 /** 4182 * Removes an AddonInstall from the list of active installs. 4183 * 4184 * @param install 4185 * The AddonInstall to remove 4186 */ 4187 removeActiveInstall: function(aInstall) { 4188 this.installs.delete(aInstall); 4189 }, 4190 4191 /** 4192 * Called to get an Addon with a particular ID. 4193 * 4194 * @param aId 4195 * The ID of the add-on to retrieve 4196 * @param aCallback 4197 * A callback to pass the Addon to 4198 */ 4199 getAddonByID: function(aId, aCallback) { 4200 XPIDatabase.getVisibleAddonForID (aId, function(aAddon) { 4201 aCallback(aAddon ? aAddon.wrapper : null); 4202 }); 4203 }, 4204 4205 /** 4206 * Called to get Addons of a particular type. 4207 * 4208 * @param aTypes 4209 * An array of types to fetch. Can be null to get all types. 4210 * @param aCallback 4211 * A callback to pass an array of Addons to 4212 */ 4213 getAddonsByTypes: function(aTypes, aCallback) { 4214 let typesToGet = getAllAliasesForTypes(aTypes); 4215 4216 XPIDatabase.getVisibleAddons(typesToGet, function(aAddons) { 4217 aCallback(aAddons.map(a => a.wrapper)); 4218 }); 4219 }, 4220 4221 /** 4222 * Obtain an Addon having the specified Sync GUID. 4223 * 4224 * @param aGUID 4225 * String GUID of add-on to retrieve 4226 * @param aCallback 4227 * A callback to pass the Addon to. Receives null if not found. 4228 */ 4229 getAddonBySyncGUID: function(aGUID, aCallback) { 4230 XPIDatabase.getAddonBySyncGUID(aGUID, function(aAddon) { 4231 aCallback(aAddon ? aAddon.wrapper : null); 4232 }); 4233 }, 4234 4235 /** 4236 * Called to get Addons that have pending operations. 4237 * 4238 * @param aTypes 4239 * An array of types to fetch. Can be null to get all types 4240 * @param aCallback 4241 * A callback to pass an array of Addons to 4242 */ 4243 getAddonsWithOperationsByTypes: function(aTypes, aCallback) { 4244 let typesToGet = getAllAliasesForTypes(aTypes); 4245 4246 XPIDatabase.getVisibleAddonsWithPendingOperations(typesToGet, function(aAddons) { 4247 let results = aAddons.map(a => a.wrapper); 4248 for (let install of XPIProvider.installs) { 4249 if (install.state == AddonManager.STATE_INSTALLED && 4250 !(install.addon.inDatabase)) 4251 results.push(install.addon.wrapper); 4252 } 4253 aCallback(results); 4254 }); 4255 }, 4256 4257 /** 4258 * Called to get the current AddonInstalls, optionally limiting to a list of 4259 * types. 4260 * 4261 * @param aTypes 4262 * An array of types or null to get all types 4263 * @param aCallback 4264 * A callback to pass the array of AddonInstalls to 4265 */ 4266 getInstallsByTypes: function(aTypes, aCallback) { 4267 let results = [...this.installs]; 4268 if (aTypes) { 4269 results = results.filter(install => { 4270 return aTypes.includes(getExternalType(install.type)); 4271 }); 4272 } 4273 4274 aCallback(results.map(install => install.wrapper)); 4275 }, 4276 4277 /** 4278 * Synchronously map a URI to the corresponding Addon ID. 4279 * 4280 * Mappable URIs are limited to in-application resources belonging to the 4281 * add-on, such as Javascript compartments, XUL windows, XBL bindings, etc. 4282 * but do not include URIs from meta data, such as the add-on homepage. 4283 * 4284 * @param aURI 4285 * nsIURI to map or null 4286 * @return string containing the Addon ID 4287 * @see AddonManager.mapURIToAddonID 4288 * @see amIAddonManager.mapURIToAddonID 4289 */ 4290 mapURIToAddonID: function(aURI) { 4291 // Returns `null` instead of empty string if the URI can't be mapped. 4292 return AddonPathService.mapURIToAddonId(aURI) || null; 4293 }, 4294 4295 /** 4296 * Called when a new add-on has been enabled when only one add-on of that type 4297 * can be enabled. 4298 * 4299 * @param aId 4300 * The ID of the newly enabled add-on 4301 * @param aType 4302 * The type of the newly enabled add-on 4303 * @param aPendingRestart 4304 * true if the newly enabled add-on will only become enabled after a 4305 * restart 4306 */ 4307 addonChanged: function(aId, aType, aPendingRestart) { 4308 // We only care about themes in this provider 4309 if (aType != "theme") 4310 return; 4311 4312 if (!aId) { 4313 // Fallback to the default theme when no theme was enabled 4314 this.enableDefaultTheme(); 4315 return; 4316 } 4317 4318 // Look for the previously enabled theme and find the internalName of the 4319 // currently selected theme 4320 let previousTheme = null; 4321 let newSkin = this.defaultSkin; 4322 let addons = XPIDatabase.getAddonsByType("theme"); 4323 for (let theme of addons) { 4324 if (!theme.visible) 4325 return; 4326 if (theme.id == aId) 4327 newSkin = theme.internalName; 4328 else if (theme.userDisabled == false && !theme.pendingUninstall) 4329 previousTheme = theme; 4330 } 4331 4332 if (aPendingRestart) { 4333 Services.prefs.setBoolPref(PREF_DSS_SWITCHPENDING, true); 4334 Services.prefs.setCharPref(PREF_DSS_SKIN_TO_SELECT, newSkin); 4335 } 4336 else if (newSkin == this.currentSkin) { 4337 try { 4338 Services.prefs.clearUserPref(PREF_DSS_SWITCHPENDING); 4339 } 4340 catch (e) { } 4341 try { 4342 Services.prefs.clearUserPref(PREF_DSS_SKIN_TO_SELECT); 4343 } 4344 catch (e) { } 4345 } 4346 else { 4347 Services.prefs.setCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN, newSkin); 4348 this.currentSkin = newSkin; 4349 } 4350 this.selectedSkin = newSkin; 4351 4352 // Flush the preferences to disk so they don't get out of sync with the 4353 // database 4354 Services.prefs.savePrefFile(null); 4355 4356 // Mark the previous theme as disabled. This won't cause recursion since 4357 // only enabled calls notifyAddonChanged. 4358 if (previousTheme) 4359 this.updateAddonDisabledState(previousTheme, true); 4360 }, 4361 4362 /** 4363 * Update the appDisabled property for all add-ons. 4364 */ 4365 updateAddonAppDisabledStates: function() { 4366 let addons = XPIDatabase.getAddons(); 4367 for (let addon of addons) { 4368 this.updateAddonDisabledState(addon); 4369 } 4370 }, 4371 4372 /** 4373 * Update the repositoryAddon property for all add-ons. 4374 * 4375 * @param aCallback 4376 * Function to call when operation is complete. 4377 */ 4378 updateAddonRepositoryData: function(aCallback) { 4379 XPIDatabase.getVisibleAddons(null, aAddons => { 4380 let pending = aAddons.length; 4381 logger.debug("updateAddonRepositoryData found " + pending + " visible add-ons"); 4382 if (pending == 0) { 4383 aCallback(); 4384 return; 4385 } 4386 4387 function notifyComplete() { 4388 if (--pending == 0) 4389 aCallback(); 4390 } 4391 4392 for (let addon of aAddons) { 4393 AddonRepository.getCachedAddonByID(addon.id, aRepoAddon => { 4394 if (aRepoAddon) { 4395 logger.debug("updateAddonRepositoryData got info for " + addon.id); 4396 addon._repositoryAddon = aRepoAddon; 4397 addon.compatibilityOverrides = aRepoAddon.compatibilityOverrides; 4398 this.updateAddonDisabledState(addon); 4399 } 4400 4401 notifyComplete(); 4402 }); 4403 } 4404 }); 4405 }, 4406 4407 /** 4408 * When the previously selected theme is removed this method will be called 4409 * to enable the default theme. 4410 */ 4411 enableDefaultTheme: function() { 4412 logger.debug("Activating default theme"); 4413 let addon = XPIDatabase.getVisibleAddonForInternalName(this.defaultSkin); 4414 if (addon) { 4415 if (addon.userDisabled) { 4416 this.updateAddonDisabledState(addon, false); 4417 } 4418 else if (!this.extensionsActive) { 4419 // During startup we may end up trying to enable the default theme when 4420 // the database thinks it is already enabled (see f.e. bug 638847). In 4421 // this case just force the theme preferences to be correct 4422 Services.prefs.setCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN, 4423 addon.internalName); 4424 this.currentSkin = this.selectedSkin = addon.internalName; 4425 Preferences.reset(PREF_DSS_SKIN_TO_SELECT); 4426 Preferences.reset(PREF_DSS_SWITCHPENDING); 4427 } 4428 else { 4429 logger.warn("Attempting to activate an already active default theme"); 4430 } 4431 } 4432 else { 4433 logger.warn("Unable to activate the default theme"); 4434 } 4435 }, 4436 4437 onDebugConnectionChange: function(aEvent, aWhat, aConnection) { 4438 if (aWhat != "opened") 4439 return; 4440 4441 for (let [id, val] of this.activeAddons) { 4442 aConnection.setAddonOptions( 4443 id, { global: val.debugGlobal || val.bootstrapScope }); 4444 } 4445 }, 4446 4447 /** 4448 * Notified when a preference we're interested in has changed. 4449 * 4450 * @see nsIObserver 4451 */ 4452 observe: function(aSubject, aTopic, aData) { 4453 if (aTopic == NOTIFICATION_FLUSH_PERMISSIONS) { 4454 if (!aData || aData == XPI_PERMISSION) { 4455 this.importPermissions(); 4456 } 4457 return; 4458 } 4459 else if (aTopic == NOTIFICATION_TOOLBOXPROCESS_LOADED) { 4460 Services.obs.removeObserver(this, NOTIFICATION_TOOLBOXPROCESS_LOADED, false); 4461 this._toolboxProcessLoaded = true; 4462 BrowserToolboxProcess.on("connectionchange", 4463 this.onDebugConnectionChange.bind(this)); 4464 } 4465 4466 if (aTopic == "nsPref:changed") { 4467 switch (aData) { 4468 case PREF_EM_MIN_COMPAT_APP_VERSION: 4469 this.minCompatibleAppVersion = Preferences.get(PREF_EM_MIN_COMPAT_APP_VERSION, 4470 null); 4471 this.updateAddonAppDisabledStates(); 4472 break; 4473 case PREF_EM_MIN_COMPAT_PLATFORM_VERSION: 4474 this.minCompatiblePlatformVersion = Preferences.get(PREF_EM_MIN_COMPAT_PLATFORM_VERSION, 4475 null); 4476 this.updateAddonAppDisabledStates(); 4477 break; 4478 case PREF_XPI_SIGNATURES_REQUIRED: 4479 this.updateAddonAppDisabledStates(); 4480 break; 4481 4482 case PREF_E10S_ADDON_BLOCKLIST: 4483 case PREF_E10S_ADDON_POLICY: 4484 XPIDatabase.updateAddonsBlockingE10s(); 4485 break; 4486 } 4487 } 4488 }, 4489 4490 /** 4491 * Determine if an add-on should be blocking e10s if enabled. 4492 * 4493 * @param aAddon 4494 * The add-on to test 4495 * @return true if enabling the add-on should block e10s 4496 */ 4497 isBlockingE10s: function(aAddon) { 4498 if (aAddon.type != "extension" && 4499 aAddon.type != "webextension" && 4500 aAddon.type != "theme") 4501 return false; 4502 4503 // The hotfix is exempt 4504 let hotfixID = Preferences.get(PREF_EM_HOTFIX_ID, undefined); 4505 if (hotfixID && hotfixID == aAddon.id) 4506 return false; 4507 4508 // The default theme is exempt 4509 if (aAddon.type == "theme" && 4510 aAddon.internalName == XPIProvider.defaultSkin) 4511 return false; 4512 4513 // System add-ons are exempt 4514 let locName = aAddon._installLocation ? aAddon._installLocation.name 4515 : undefined; 4516 if (locName == KEY_APP_SYSTEM_DEFAULTS || 4517 locName == KEY_APP_SYSTEM_ADDONS) 4518 return false; 4519 4520 if (isAddonPartOfE10SRollout(aAddon)) { 4521 Preferences.set(PREF_E10S_HAS_NONEXEMPT_ADDON, true); 4522 return false; 4523 } 4524 4525 logger.debug("Add-on " + aAddon.id + " blocks e10s rollout."); 4526 return true; 4527 }, 4528 4529 /** 4530 * In some cases having add-ons active blocks e10s but turning off e10s 4531 * requires a restart so some add-ons that are normally restartless will 4532 * require a restart to install or enable. 4533 * 4534 * @param aAddon 4535 * The add-on to test 4536 * @return true if enabling the add-on requires a restart 4537 */ 4538 e10sBlocksEnabling: function(aAddon) { 4539 // If the preference isn't set then don't block anything 4540 if (!Preferences.get(PREF_E10S_BLOCK_ENABLE, false)) 4541 return false; 4542 4543 // If e10s isn't active then don't block anything 4544 if (!Services.appinfo.browserTabsRemoteAutostart) 4545 return false; 4546 4547 return this.isBlockingE10s(aAddon); 4548 }, 4549 4550 /** 4551 * Tests whether enabling an add-on will require a restart. 4552 * 4553 * @param aAddon 4554 * The add-on to test 4555 * @return true if the operation requires a restart 4556 */ 4557 enableRequiresRestart: function(aAddon) { 4558 // If the platform couldn't have activated extensions then we can make 4559 // changes without any restart. 4560 if (!this.extensionsActive) 4561 return false; 4562 4563 // If the application is in safe mode then any change can be made without 4564 // restarting 4565 if (Services.appinfo.inSafeMode) 4566 return false; 4567 4568 // Anything that is active is already enabled 4569 if (aAddon.active) 4570 return false; 4571 4572 if (aAddon.type == "theme") { 4573 // If dynamic theme switching is enabled then switching themes does not 4574 // require a restart 4575 if (Preferences.get(PREF_EM_DSS_ENABLED)) 4576 return false; 4577 4578 // If the theme is already the theme in use then no restart is necessary. 4579 // This covers the case where the default theme is in use but a 4580 // lightweight theme is considered active. 4581 return aAddon.internalName != this.currentSkin; 4582 } 4583 4584 if (this.e10sBlocksEnabling(aAddon)) 4585 return true; 4586 4587 return !aAddon.bootstrap; 4588 }, 4589 4590 /** 4591 * Tests whether disabling an add-on will require a restart. 4592 * 4593 * @param aAddon 4594 * The add-on to test 4595 * @return true if the operation requires a restart 4596 */ 4597 disableRequiresRestart: function(aAddon) { 4598 // If the platform couldn't have activated up extensions then we can make 4599 // changes without any restart. 4600 if (!this.extensionsActive) 4601 return false; 4602 4603 // If the application is in safe mode then any change can be made without 4604 // restarting 4605 if (Services.appinfo.inSafeMode) 4606 return false; 4607 4608 // Anything that isn't active is already disabled 4609 if (!aAddon.active) 4610 return false; 4611 4612 if (aAddon.type == "theme") { 4613 // If dynamic theme switching is enabled then switching themes does not 4614 // require a restart 4615 if (Preferences.get(PREF_EM_DSS_ENABLED)) 4616 return false; 4617 4618 // Non-default themes always require a restart to disable since it will 4619 // be switching from one theme to another or to the default theme and a 4620 // lightweight theme. 4621 if (aAddon.internalName != this.defaultSkin) 4622 return true; 4623 4624 // The default theme requires a restart to disable if we are in the 4625 // process of switching to a different theme. Note that this makes the 4626 // disabled flag of operationsRequiringRestart incorrect for the default 4627 // theme (it will be false most of the time). Bug 520124 would be required 4628 // to fix it. For the UI this isn't a problem since we never try to 4629 // disable or uninstall the default theme. 4630 return this.selectedSkin != this.currentSkin; 4631 } 4632 4633 return !aAddon.bootstrap; 4634 }, 4635 4636 /** 4637 * Tests whether installing an add-on will require a restart. 4638 * 4639 * @param aAddon 4640 * The add-on to test 4641 * @return true if the operation requires a restart 4642 */ 4643 installRequiresRestart: function(aAddon) { 4644 // If the platform couldn't have activated up extensions then we can make 4645 // changes without any restart. 4646 if (!this.extensionsActive) 4647 return false; 4648 4649 // If the application is in safe mode then any change can be made without 4650 // restarting 4651 if (Services.appinfo.inSafeMode) 4652 return false; 4653 4654 // Add-ons that are already installed don't require a restart to install. 4655 // This wouldn't normally be called for an already installed add-on (except 4656 // for forming the operationsRequiringRestart flags) so is really here as 4657 // a safety measure. 4658 if (aAddon.inDatabase) 4659 return false; 4660 4661 // If we have an AddonInstall for this add-on then we can see if there is 4662 // an existing installed add-on with the same ID 4663 if ("_install" in aAddon && aAddon._install) { 4664 // If there is an existing installed add-on and uninstalling it would 4665 // require a restart then installing the update will also require a 4666 // restart 4667 let existingAddon = aAddon._install.existingAddon; 4668 if (existingAddon && this.uninstallRequiresRestart(existingAddon)) 4669 return true; 4670 } 4671 4672 // If the add-on is not going to be active after installation then it 4673 // doesn't require a restart to install. 4674 if (aAddon.disabled) 4675 return false; 4676 4677 if (this.e10sBlocksEnabling(aAddon)) 4678 return true; 4679 4680 // Themes will require a restart (even if dynamic switching is enabled due 4681 // to some caching issues) and non-bootstrapped add-ons will require a 4682 // restart 4683 return aAddon.type == "theme" || !aAddon.bootstrap; 4684 }, 4685 4686 /** 4687 * Tests whether uninstalling an add-on will require a restart. 4688 * 4689 * @param aAddon 4690 * The add-on to test 4691 * @return true if the operation requires a restart 4692 */ 4693 uninstallRequiresRestart: function(aAddon) { 4694 // If the platform couldn't have activated up extensions then we can make 4695 // changes without any restart. 4696 if (!this.extensionsActive) 4697 return false; 4698 4699 // If the application is in safe mode then any change can be made without 4700 // restarting 4701 if (Services.appinfo.inSafeMode) 4702 return false; 4703 4704 // If the add-on can be disabled without a restart then it can also be 4705 // uninstalled without a restart 4706 return this.disableRequiresRestart(aAddon); 4707 }, 4708 4709 /** 4710 * Loads a bootstrapped add-on's bootstrap.js into a sandbox and the reason 4711 * values as constants in the scope. This will also add information about the 4712 * add-on to the bootstrappedAddons dictionary and notify the crash reporter 4713 * that new add-ons have been loaded. 4714 * 4715 * @param aId 4716 * The add-on's ID 4717 * @param aFile 4718 * The nsIFile for the add-on 4719 * @param aVersion 4720 * The add-on's version 4721 * @param aType 4722 * The type for the add-on 4723 * @param aMultiprocessCompatible 4724 * Boolean indicating whether the add-on is compatible with electrolysis. 4725 * @param aRunInSafeMode 4726 * Boolean indicating whether the add-on can run in safe mode. 4727 * @param aDependencies 4728 * An array of add-on IDs on which this add-on depends. 4729 * @param hasEmbeddedWebExtension 4730 * Boolean indicating whether the add-on has an embedded webextension. 4731 * @return a JavaScript scope 4732 */ 4733 loadBootstrapScope: function(aId, aFile, aVersion, aType, 4734 aMultiprocessCompatible, aRunInSafeMode, 4735 aDependencies, hasEmbeddedWebExtension) { 4736 // Mark the add-on as active for the crash reporter before loading 4737 this.bootstrappedAddons[aId] = { 4738 version: aVersion, 4739 type: aType, 4740 descriptor: aFile.persistentDescriptor, 4741 multiprocessCompatible: aMultiprocessCompatible, 4742 runInSafeMode: aRunInSafeMode, 4743 dependencies: aDependencies, 4744 hasEmbeddedWebExtension, 4745 }; 4746 this.persistBootstrappedAddons(); 4747 this.addAddonsToCrashReporter(); 4748 4749 this.activeAddons.set(aId, { 4750 debugGlobal: null, 4751 safeWrapper: null, 4752 bootstrapScope: null, 4753 // a Symbol passed to this add-on, which it can use to identify itself 4754 instanceID: Symbol(aId), 4755 }); 4756 let activeAddon = this.activeAddons.get(aId); 4757 4758 // Locales only contain chrome and can't have bootstrap scripts 4759 if (aType == "locale") { 4760 return; 4761 } 4762 4763 logger.debug("Loading bootstrap scope from " + aFile.path); 4764 4765 let principal = Cc["@mozilla.org/systemprincipal;1"]. 4766 createInstance(Ci.nsIPrincipal); 4767 if (!aMultiprocessCompatible && Preferences.get(PREF_INTERPOSITION_ENABLED, false)) { 4768 let interposition = Cc["@mozilla.org/addons/multiprocess-shims;1"]. 4769 getService(Ci.nsIAddonInterposition); 4770 Cu.setAddonInterposition(aId, interposition); 4771 Cu.allowCPOWsInAddon(aId, true); 4772 } 4773 4774 if (!aFile.exists()) { 4775 activeAddon.bootstrapScope = 4776 new Cu.Sandbox(principal, { sandboxName: aFile.path, 4777 wantGlobalProperties: ["indexedDB"], 4778 addonId: aId, 4779 metadata: { addonID: aId } }); 4780 logger.error("Attempted to load bootstrap scope from missing directory " + aFile.path); 4781 return; 4782 } 4783 4784 let uri = getURIForResourceInFile(aFile, "bootstrap.js").spec; 4785 if (aType == "dictionary") 4786 uri = "resource://gre/modules/addons/SpellCheckDictionaryBootstrap.js" 4787 else if (aType == "webextension") 4788 uri = "resource://gre/modules/addons/WebExtensionBootstrap.js" 4789 else if (aType == "apiextension") 4790 uri = "resource://gre/modules/addons/APIExtensionBootstrap.js" 4791 4792 activeAddon.bootstrapScope = 4793 new Cu.Sandbox(principal, { sandboxName: uri, 4794 wantGlobalProperties: ["indexedDB"], 4795 addonId: aId, 4796 metadata: { addonID: aId, URI: uri } }); 4797 4798 let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"]. 4799 createInstance(Ci.mozIJSSubScriptLoader); 4800 4801 try { 4802 // Copy the reason values from the global object into the bootstrap scope. 4803 for (let name in BOOTSTRAP_REASONS) 4804 activeAddon.bootstrapScope[name] = BOOTSTRAP_REASONS[name]; 4805 4806 // Add other stuff that extensions want. 4807 const features = [ "Worker", "ChromeWorker" ]; 4808 4809 for (let feature of features) 4810 activeAddon.bootstrapScope[feature] = gGlobalScope[feature]; 4811 4812 // Define a console for the add-on 4813 activeAddon.bootstrapScope["console"] = new ConsoleAPI( 4814 { consoleID: "addon/" + aId }); 4815 4816 // As we don't want our caller to control the JS version used for the 4817 // bootstrap file, we run loadSubScript within the context of the 4818 // sandbox with the latest JS version set explicitly. 4819 activeAddon.bootstrapScope.__SCRIPT_URI_SPEC__ = uri; 4820 Components.utils.evalInSandbox( 4821 "Components.classes['@mozilla.org/moz/jssubscript-loader;1'] \ 4822 .createInstance(Components.interfaces.mozIJSSubScriptLoader) \ 4823 .loadSubScript(__SCRIPT_URI_SPEC__);", 4824 activeAddon.bootstrapScope, "ECMAv5"); 4825 } 4826 catch (e) { 4827 logger.warn("Error loading bootstrap.js for " + aId, e); 4828 } 4829 4830 // Only access BrowserToolboxProcess if ToolboxProcess.jsm has been 4831 // initialized as otherwise, when it will be initialized, all addons' 4832 // globals will be added anyways 4833 if (this._toolboxProcessLoaded) { 4834 BrowserToolboxProcess.setAddonOptions(aId, 4835 { global: activeAddon.bootstrapScope }); 4836 } 4837 }, 4838 4839 /** 4840 * Unloads a bootstrap scope by dropping all references to it and then 4841 * updating the list of active add-ons with the crash reporter. 4842 * 4843 * @param aId 4844 * The add-on's ID 4845 */ 4846 unloadBootstrapScope: function(aId) { 4847 // In case the add-on was not multiprocess-compatible, deregister 4848 // any interpositions for it. 4849 Cu.setAddonInterposition(aId, null); 4850 Cu.allowCPOWsInAddon(aId, false); 4851 4852 this.activeAddons.delete(aId); 4853 delete this.bootstrappedAddons[aId]; 4854 this.persistBootstrappedAddons(); 4855 this.addAddonsToCrashReporter(); 4856 4857 // Only access BrowserToolboxProcess if ToolboxProcess.jsm has been 4858 // initialized as otherwise, there won't be any addon globals added to it 4859 if (this._toolboxProcessLoaded) { 4860 BrowserToolboxProcess.setAddonOptions(aId, { global: null }); 4861 } 4862 }, 4863 4864 /** 4865 * Calls a bootstrap method for an add-on. 4866 * 4867 * @param aAddon 4868 * An object representing the add-on, with `id`, `type` and `version` 4869 * @param aFile 4870 * The nsIFile for the add-on 4871 * @param aMethod 4872 * The name of the bootstrap method to call 4873 * @param aReason 4874 * The reason flag to pass to the bootstrap's startup method 4875 * @param aExtraParams 4876 * An object of additional key/value pairs to pass to the method in 4877 * the params argument 4878 */ 4879 callBootstrapMethod: function(aAddon, aFile, aMethod, aReason, aExtraParams) { 4880 if (!aAddon.id || !aAddon.version || !aAddon.type) { 4881 throw new Error("aAddon must include an id, version, and type"); 4882 } 4883 4884 // Only run in safe mode if allowed to 4885 let runInSafeMode = "runInSafeMode" in aAddon ? aAddon.runInSafeMode : canRunInSafeMode(aAddon); 4886 if (Services.appinfo.inSafeMode && !runInSafeMode) 4887 return; 4888 4889 let timeStart = new Date(); 4890 if (CHROME_TYPES.has(aAddon.type) && aMethod == "startup") { 4891 logger.debug("Registering manifest for " + aFile.path); 4892 Components.manager.addBootstrappedManifestLocation(aFile); 4893 } 4894 4895 try { 4896 // Load the scope if it hasn't already been loaded 4897 let activeAddon = this.activeAddons.get(aAddon.id); 4898 if (!activeAddon) { 4899 this.loadBootstrapScope(aAddon.id, aFile, aAddon.version, aAddon.type, 4900 aAddon.multiprocessCompatible || false, 4901 runInSafeMode, aAddon.dependencies, 4902 aAddon.hasEmbeddedWebExtension || false); 4903 activeAddon = this.activeAddons.get(aAddon.id); 4904 } 4905 4906 if (aMethod == "startup" || aMethod == "shutdown") { 4907 if (!aExtraParams) { 4908 aExtraParams = {}; 4909 } 4910 aExtraParams["instanceID"] = this.activeAddons.get(aAddon.id).instanceID; 4911 } 4912 4913 // Nothing to call for locales 4914 if (aAddon.type == "locale") 4915 return; 4916 4917 let method = undefined; 4918 try { 4919 method = Components.utils.evalInSandbox(`${aMethod};`, 4920 activeAddon.bootstrapScope, "ECMAv5"); 4921 } 4922 catch (e) { 4923 // An exception will be caught if the expected method is not defined. 4924 // That will be logged below. 4925 } 4926 4927 if (!method) { 4928 logger.warn("Add-on " + aAddon.id + " is missing bootstrap method " + aMethod); 4929 return; 4930 } 4931 4932 // Extensions are automatically deinitialized in the correct order at shutdown. 4933 if (aMethod == "shutdown" && aReason != BOOTSTRAP_REASONS.APP_SHUTDOWN) { 4934 activeAddon.disable = true; 4935 for (let addon of this.getDependentAddons(aAddon)) { 4936 if (addon.active) 4937 this.updateAddonDisabledState(addon); 4938 } 4939 } 4940 4941 let params = { 4942 id: aAddon.id, 4943 version: aAddon.version, 4944 installPath: aFile.clone(), 4945 resourceURI: getURIForResourceInFile(aFile, "") 4946 }; 4947 4948 if (aExtraParams) { 4949 for (let key in aExtraParams) { 4950 params[key] = aExtraParams[key]; 4951 } 4952 } 4953 4954 if (aAddon.hasEmbeddedWebExtension) { 4955 if (aMethod == "startup") { 4956 const webExtension = LegacyExtensionsUtils.getEmbeddedExtensionFor(params); 4957 params.webExtension = { 4958 startup: () => webExtension.startup(), 4959 }; 4960 } else if (aMethod == "shutdown") { 4961 LegacyExtensionsUtils.getEmbeddedExtensionFor(params).shutdown(); 4962 } 4963 } 4964 4965 logger.debug("Calling bootstrap method " + aMethod + " on " + aAddon.id + " version " + 4966 aAddon.version); 4967 try { 4968 method(params, aReason); 4969 } 4970 catch (e) { 4971 logger.warn("Exception running bootstrap method " + aMethod + " on " + aAddon.id, e); 4972 } 4973 } 4974 finally { 4975 // Extensions are automatically initialized in the correct order at startup. 4976 if (aMethod == "startup" && aReason != BOOTSTRAP_REASONS.APP_STARTUP) { 4977 for (let addon of this.getDependentAddons(aAddon)) 4978 this.updateAddonDisabledState(addon); 4979 } 4980 4981 if (CHROME_TYPES.has(aAddon.type) && aMethod == "shutdown" && aReason != BOOTSTRAP_REASONS.APP_SHUTDOWN) { 4982 logger.debug("Removing manifest for " + aFile.path); 4983 Components.manager.removeBootstrappedManifestLocation(aFile); 4984 4985 let manifest = getURIForResourceInFile(aFile, "chrome.manifest"); 4986 for (let line of ChromeManifestParser.parseSync(manifest)) { 4987 if (line.type == "resource") { 4988 ResProtocolHandler.setSubstitution(line.args[0], null); 4989 } 4990 } 4991 } 4992 this.setTelemetry(aAddon.id, aMethod + "_MS", new Date() - timeStart); 4993 } 4994 }, 4995 4996 /** 4997 * Updates the disabled state for an add-on. Its appDisabled property will be 4998 * calculated and if the add-on is changed the database will be saved and 4999 * appropriate notifications will be sent out to the registered AddonListeners. 5000 * 5001 * @param aAddon 5002 * The DBAddonInternal to update 5003 * @param aUserDisabled 5004 * Value for the userDisabled property. If undefined the value will 5005 * not change 5006 * @param aSoftDisabled 5007 * Value for the softDisabled property. If undefined the value will 5008 * not change. If true this will force userDisabled to be true 5009 * @return a tri-state indicating the action taken for the add-on: 5010 * - undefined: The add-on did not change state 5011 * - true: The add-on because disabled 5012 * - false: The add-on became enabled 5013 * @throws if addon is not a DBAddonInternal 5014 */ 5015 updateAddonDisabledState: function(aAddon, aUserDisabled, aSoftDisabled) { 5016 if (!(aAddon.inDatabase)) 5017 throw new Error("Can only update addon states for installed addons."); 5018 if (aUserDisabled !== undefined && aSoftDisabled !== undefined) { 5019 throw new Error("Cannot change userDisabled and softDisabled at the " + 5020 "same time"); 5021 } 5022 5023 if (aUserDisabled === undefined) { 5024 aUserDisabled = aAddon.userDisabled; 5025 } 5026 else if (!aUserDisabled) { 5027 // If enabling the add-on then remove softDisabled 5028 aSoftDisabled = false; 5029 } 5030 5031 // If not changing softDisabled or the add-on is already userDisabled then 5032 // use the existing value for softDisabled 5033 if (aSoftDisabled === undefined || aUserDisabled) 5034 aSoftDisabled = aAddon.softDisabled; 5035 5036 let appDisabled = !isUsableAddon(aAddon); 5037 // No change means nothing to do here 5038 if (aAddon.userDisabled == aUserDisabled && 5039 aAddon.appDisabled == appDisabled && 5040 aAddon.softDisabled == aSoftDisabled) 5041 return undefined; 5042 5043 let wasDisabled = aAddon.disabled; 5044 let isDisabled = aUserDisabled || aSoftDisabled || appDisabled; 5045 5046 // If appDisabled changes but addon.disabled doesn't, 5047 // no onDisabling/onEnabling is sent - so send a onPropertyChanged. 5048 let appDisabledChanged = aAddon.appDisabled != appDisabled; 5049 5050 // Update the properties in the database. 5051 XPIDatabase.setAddonProperties(aAddon, { 5052 userDisabled: aUserDisabled, 5053 appDisabled: appDisabled, 5054 softDisabled: aSoftDisabled 5055 }); 5056 5057 let wrapper = aAddon.wrapper; 5058 5059 if (appDisabledChanged) { 5060 AddonManagerPrivate.callAddonListeners("onPropertyChanged", 5061 wrapper, 5062 ["appDisabled"]); 5063 } 5064 5065 // If the add-on is not visible or the add-on is not changing state then 5066 // there is no need to do anything else 5067 if (!aAddon.visible || (wasDisabled == isDisabled)) 5068 return undefined; 5069 5070 // Flag that active states in the database need to be updated on shutdown 5071 Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true); 5072 5073 // Have we just gone back to the current state? 5074 if (isDisabled != aAddon.active) { 5075 AddonManagerPrivate.callAddonListeners("onOperationCancelled", wrapper); 5076 } 5077 else { 5078 if (isDisabled) { 5079 var needsRestart = this.disableRequiresRestart(aAddon); 5080 AddonManagerPrivate.callAddonListeners("onDisabling", wrapper, 5081 needsRestart); 5082 } 5083 else { 5084 needsRestart = this.enableRequiresRestart(aAddon); 5085 AddonManagerPrivate.callAddonListeners("onEnabling", wrapper, 5086 needsRestart); 5087 } 5088 5089 if (!needsRestart) { 5090 XPIDatabase.updateAddonActive(aAddon, !isDisabled); 5091 5092 if (isDisabled) { 5093 if (aAddon.bootstrap) { 5094 this.callBootstrapMethod(aAddon, aAddon._sourceBundle, "shutdown", 5095 BOOTSTRAP_REASONS.ADDON_DISABLE); 5096 this.unloadBootstrapScope(aAddon.id); 5097 } 5098 AddonManagerPrivate.callAddonListeners("onDisabled", wrapper); 5099 } 5100 else { 5101 if (aAddon.bootstrap) { 5102 this.callBootstrapMethod(aAddon, aAddon._sourceBundle, "startup", 5103 BOOTSTRAP_REASONS.ADDON_ENABLE); 5104 } 5105 AddonManagerPrivate.callAddonListeners("onEnabled", wrapper); 5106 } 5107 } 5108 else if (aAddon.bootstrap) { 5109 // Something blocked the restartless add-on from enabling or disabling 5110 // make sure it happens on the next startup 5111 if (isDisabled) { 5112 this.bootstrappedAddons[aAddon.id].disable = true; 5113 } 5114 else { 5115 this.bootstrappedAddons[aAddon.id] = { 5116 version: aAddon.version, 5117 type: aAddon.type, 5118 descriptor: aAddon._sourceBundle.persistentDescriptor, 5119 multiprocessCompatible: aAddon.multiprocessCompatible, 5120 runInSafeMode: canRunInSafeMode(aAddon), 5121 dependencies: aAddon.dependencies, 5122 hasEmbeddedWebExtension: aAddon.hasEmbeddedWebExtension, 5123 }; 5124 this.persistBootstrappedAddons(); 5125 } 5126 } 5127 } 5128 5129 // Sync with XPIStates. 5130 let xpiState = XPIStates.getAddon(aAddon.location, aAddon.id); 5131 if (xpiState) { 5132 xpiState.syncWithDB(aAddon); 5133 XPIStates.save(); 5134 } else { 5135 // There should always be an xpiState 5136 logger.warn("No XPIState for ${id} in ${location}", aAddon); 5137 } 5138 5139 // Notify any other providers that a new theme has been enabled 5140 if (aAddon.type == "theme" && !isDisabled) 5141 AddonManagerPrivate.notifyAddonChanged(aAddon.id, aAddon.type, needsRestart); 5142 5143 return isDisabled; 5144 }, 5145 5146 /** 5147 * Uninstalls an add-on, immediately if possible or marks it as pending 5148 * uninstall if not. 5149 * 5150 * @param aAddon 5151 * The DBAddonInternal to uninstall 5152 * @param aForcePending 5153 * Force this addon into the pending uninstall state, even if 5154 * it isn't marked as requiring a restart (used e.g. while the 5155 * add-on manager is open and offering an "undo" button) 5156 * @throws if the addon cannot be uninstalled because it is in an install 5157 * location that does not allow it 5158 */ 5159 uninstallAddon: function(aAddon, aForcePending) { 5160 if (!(aAddon.inDatabase)) 5161 throw new Error("Cannot uninstall addon " + aAddon.id + " because it is not installed"); 5162 5163 if (aAddon._installLocation.locked) 5164 throw new Error("Cannot uninstall addon " + aAddon.id 5165 + " from locked install location " + aAddon._installLocation.name); 5166 5167 // Inactive add-ons don't require a restart to uninstall 5168 let requiresRestart = this.uninstallRequiresRestart(aAddon); 5169 5170 // if makePending is true, we don't actually apply the uninstall, 5171 // we just mark the addon as having a pending uninstall 5172 let makePending = aForcePending || requiresRestart; 5173 5174 if (makePending && aAddon.pendingUninstall) 5175 throw new Error("Add-on is already marked to be uninstalled"); 5176 5177 aAddon._hasResourceCache.clear(); 5178 5179 if (aAddon._updateCheck) { 5180 logger.debug("Cancel in-progress update check for " + aAddon.id); 5181 aAddon._updateCheck.cancel(); 5182 } 5183 5184 let wasPending = aAddon.pendingUninstall; 5185 5186 if (makePending) { 5187 // We create an empty directory in the staging directory to indicate 5188 // that an uninstall is necessary on next startup. Temporary add-ons are 5189 // automatically uninstalled on shutdown anyway so there is no need to 5190 // do this for them. 5191 if (aAddon._installLocation.name != KEY_APP_TEMPORARY) { 5192 let stage = aAddon._installLocation.getStagingDir(); 5193 stage.append(aAddon.id); 5194 if (!stage.exists()) 5195 stage.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); 5196 } 5197 5198 XPIDatabase.setAddonProperties(aAddon, { 5199 pendingUninstall: true 5200 }); 5201 Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true); 5202 let xpiState = XPIStates.getAddon(aAddon.location, aAddon.id); 5203 if (xpiState) { 5204 xpiState.enabled = false; 5205 XPIStates.save(); 5206 } else { 5207 logger.warn("Can't find XPI state while uninstalling ${id} from ${location}", aAddon); 5208 } 5209 } 5210 5211 // If the add-on is not visible then there is no need to notify listeners. 5212 if (!aAddon.visible) 5213 return; 5214 5215 let wrapper = aAddon.wrapper; 5216 5217 // If the add-on wasn't already pending uninstall then notify listeners. 5218 if (!wasPending) { 5219 // Passing makePending as the requiresRestart parameter is a little 5220 // strange as in some cases this operation can complete without a restart 5221 // so really this is now saying that the uninstall isn't going to happen 5222 // immediately but will happen later. 5223 AddonManagerPrivate.callAddonListeners("onUninstalling", wrapper, 5224 makePending); 5225 } 5226 5227 // Reveal the highest priority add-on with the same ID 5228 function revealAddon(aAddon) { 5229 XPIDatabase.makeAddonVisible(aAddon); 5230 5231 let wrappedAddon = aAddon.wrapper; 5232 AddonManagerPrivate.callAddonListeners("onInstalling", wrappedAddon, false); 5233 5234 if (!aAddon.disabled && !XPIProvider.enableRequiresRestart(aAddon)) { 5235 XPIDatabase.updateAddonActive(aAddon, true); 5236 } 5237 5238 if (aAddon.bootstrap) { 5239 XPIProvider.callBootstrapMethod(aAddon, aAddon._sourceBundle, 5240 "install", BOOTSTRAP_REASONS.ADDON_INSTALL); 5241 5242 if (aAddon.active) { 5243 XPIProvider.callBootstrapMethod(aAddon, aAddon._sourceBundle, 5244 "startup", BOOTSTRAP_REASONS.ADDON_INSTALL); 5245 } 5246 else { 5247 XPIProvider.unloadBootstrapScope(aAddon.id); 5248 } 5249 } 5250 5251 // We always send onInstalled even if a restart is required to enable 5252 // the revealed add-on 5253 AddonManagerPrivate.callAddonListeners("onInstalled", wrappedAddon); 5254 } 5255 5256 function findAddonAndReveal(aId) { 5257 let [locationName, ] = XPIStates.findAddon(aId); 5258 if (locationName) { 5259 XPIDatabase.getAddonInLocation(aId, locationName, revealAddon); 5260 } 5261 } 5262 5263 if (!makePending) { 5264 if (aAddon.bootstrap) { 5265 if (aAddon.active) { 5266 this.callBootstrapMethod(aAddon, aAddon._sourceBundle, "shutdown", 5267 BOOTSTRAP_REASONS.ADDON_UNINSTALL); 5268 } 5269 5270 this.callBootstrapMethod(aAddon, aAddon._sourceBundle, "uninstall", 5271 BOOTSTRAP_REASONS.ADDON_UNINSTALL); 5272 this.unloadBootstrapScope(aAddon.id); 5273 flushChromeCaches(); 5274 } 5275 aAddon._installLocation.uninstallAddon(aAddon.id); 5276 XPIDatabase.removeAddonMetadata(aAddon); 5277 XPIStates.removeAddon(aAddon.location, aAddon.id); 5278 AddonManagerPrivate.callAddonListeners("onUninstalled", wrapper); 5279 5280 findAddonAndReveal(aAddon.id); 5281 } 5282 else if (aAddon.bootstrap && aAddon.active && !this.disableRequiresRestart(aAddon)) { 5283 this.callBootstrapMethod(aAddon, aAddon._sourceBundle, "shutdown", 5284 BOOTSTRAP_REASONS.ADDON_UNINSTALL); 5285 this.unloadBootstrapScope(aAddon.id); 5286 XPIDatabase.updateAddonActive(aAddon, false); 5287 } 5288 5289 // Notify any other providers that a new theme has been enabled 5290 if (aAddon.type == "theme" && aAddon.active) 5291 AddonManagerPrivate.notifyAddonChanged(null, aAddon.type, requiresRestart); 5292 }, 5293 5294 /** 5295 * Cancels the pending uninstall of an add-on. 5296 * 5297 * @param aAddon 5298 * The DBAddonInternal to cancel uninstall for 5299 */ 5300 cancelUninstallAddon: function(aAddon) { 5301 if (!(aAddon.inDatabase)) 5302 throw new Error("Can only cancel uninstall for installed addons."); 5303 if (!aAddon.pendingUninstall) 5304 throw new Error("Add-on is not marked to be uninstalled"); 5305 5306 if (aAddon._installLocation.name != KEY_APP_TEMPORARY) 5307 aAddon._installLocation.cleanStagingDir([aAddon.id]); 5308 5309 XPIDatabase.setAddonProperties(aAddon, { 5310 pendingUninstall: false 5311 }); 5312 5313 if (!aAddon.visible) 5314 return; 5315 5316 Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true); 5317 5318 // TODO hide hidden add-ons (bug 557710) 5319 let wrapper = aAddon.wrapper; 5320 AddonManagerPrivate.callAddonListeners("onOperationCancelled", wrapper); 5321 5322 if (aAddon.bootstrap && !aAddon.disabled && !this.enableRequiresRestart(aAddon)) { 5323 this.callBootstrapMethod(aAddon, aAddon._sourceBundle, "startup", 5324 BOOTSTRAP_REASONS.ADDON_INSTALL); 5325 XPIDatabase.updateAddonActive(aAddon, true); 5326 } 5327 5328 // Notify any other providers that this theme is now enabled again. 5329 if (aAddon.type == "theme" && aAddon.active) 5330 AddonManagerPrivate.notifyAddonChanged(aAddon.id, aAddon.type, false); 5331 } 5332}; 5333 5334function getHashStringForCrypto(aCrypto) { 5335 // return the two-digit hexadecimal code for a byte 5336 let toHexString = charCode => ("0" + charCode.toString(16)).slice(-2); 5337 5338 // convert the binary hash data to a hex string. 5339 let binary = aCrypto.finish(false); 5340 let hash = Array.from(binary, c => toHexString(c.charCodeAt(0))) 5341 return hash.join("").toLowerCase(); 5342} 5343 5344/** 5345 * Base class for objects that manage the installation of an addon. 5346 * This class isn't instantiated directly, see the derived classes below. 5347 */ 5348class AddonInstall { 5349 /** 5350 * Instantiates an AddonInstall. 5351 * 5352 * @param aInstallLocation 5353 * The install location the add-on will be installed into 5354 * @param aUrl 5355 * The nsIURL to get the add-on from. If this is an nsIFileURL then 5356 * the add-on will not need to be downloaded 5357 * @param aHash 5358 * An optional hash for the add-on 5359 * @param aExistingAddon 5360 * The add-on this install will update if known 5361 */ 5362 constructor(aInstallLocation, aUrl, aHash, aExistingAddon) { 5363 this.wrapper = new AddonInstallWrapper(this); 5364 this.installLocation = aInstallLocation; 5365 this.sourceURI = aUrl; 5366 5367 if (aHash) { 5368 let hashSplit = aHash.toLowerCase().split(":"); 5369 this.originalHash = { 5370 algorithm: hashSplit[0], 5371 data: hashSplit[1] 5372 }; 5373 } 5374 this.hash = this.originalHash; 5375 this.existingAddon = aExistingAddon; 5376 this.releaseNotesURI = null; 5377 5378 this.listeners = []; 5379 this.icons = {}; 5380 this.error = 0; 5381 5382 this.progress = 0; 5383 this.maxProgress = -1; 5384 5385 // Giving each instance of AddonInstall a reference to the logger. 5386 this.logger = logger; 5387 5388 this.name = null; 5389 this.type = null; 5390 this.version = null; 5391 5392 this.file = null; 5393 this.ownsTempFile = null; 5394 this.certificate = null; 5395 this.certName = null; 5396 5397 this.linkedInstalls = null; 5398 this.addon = null; 5399 this.state = null; 5400 5401 XPIProvider.installs.add(this); 5402 } 5403 5404 /** 5405 * Starts installation of this add-on from whatever state it is currently at 5406 * if possible. 5407 * 5408 * Note this method is overridden to handle additional state in 5409 * the subclassses below. 5410 * 5411 * @throws if installation cannot proceed from the current state 5412 */ 5413 install() { 5414 switch (this.state) { 5415 case AddonManager.STATE_DOWNLOADED: 5416 this.startInstall(); 5417 break; 5418 case AddonManager.STATE_POSTPONED: 5419 logger.debug(`Postponing install of ${this.addon.id}`); 5420 break; 5421 case AddonManager.STATE_DOWNLOADING: 5422 case AddonManager.STATE_CHECKING: 5423 case AddonManager.STATE_INSTALLING: 5424 // Installation is already running 5425 return; 5426 default: 5427 throw new Error("Cannot start installing from this state"); 5428 } 5429 } 5430 5431 /** 5432 * Cancels installation of this add-on. 5433 * 5434 * Note this method is overridden to handle additional state in 5435 * the subclass DownloadAddonInstall. 5436 * 5437 * @throws if installation cannot be cancelled from the current state 5438 */ 5439 cancel() { 5440 switch (this.state) { 5441 case AddonManager.STATE_AVAILABLE: 5442 case AddonManager.STATE_DOWNLOADED: 5443 logger.debug("Cancelling download of " + this.sourceURI.spec); 5444 this.state = AddonManager.STATE_CANCELLED; 5445 XPIProvider.removeActiveInstall(this); 5446 AddonManagerPrivate.callInstallListeners("onDownloadCancelled", 5447 this.listeners, this.wrapper); 5448 this.removeTemporaryFile(); 5449 break; 5450 case AddonManager.STATE_INSTALLED: 5451 logger.debug("Cancelling install of " + this.addon.id); 5452 let xpi = this.installLocation.getStagingDir(); 5453 xpi.append(this.addon.id + ".xpi"); 5454 flushJarCache(xpi); 5455 this.installLocation.cleanStagingDir([this.addon.id, this.addon.id + ".xpi", 5456 this.addon.id + ".json"]); 5457 this.state = AddonManager.STATE_CANCELLED; 5458 XPIProvider.removeActiveInstall(this); 5459 5460 if (this.existingAddon) { 5461 delete this.existingAddon.pendingUpgrade; 5462 this.existingAddon.pendingUpgrade = null; 5463 } 5464 5465 AddonManagerPrivate.callAddonListeners("onOperationCancelled", this.addon.wrapper); 5466 5467 AddonManagerPrivate.callInstallListeners("onInstallCancelled", 5468 this.listeners, this.wrapper); 5469 break; 5470 case AddonManager.STATE_POSTPONED: 5471 logger.debug(`Cancelling postponed install of ${this.addon.id}`); 5472 this.state = AddonManager.STATE_CANCELLED; 5473 XPIProvider.removeActiveInstall(this); 5474 AddonManagerPrivate.callInstallListeners("onInstallCancelled", 5475 this.listeners, this.wrapper); 5476 this.removeTemporaryFile(); 5477 5478 let stagingDir = this.installLocation.getStagingDir(); 5479 let stagedAddon = stagingDir.clone(); 5480 5481 this.unstageInstall(stagedAddon); 5482 default: 5483 throw new Error("Cannot cancel install of " + this.sourceURI.spec + 5484 " from this state (" + this.state + ")"); 5485 } 5486 } 5487 5488 /** 5489 * Adds an InstallListener for this instance if the listener is not already 5490 * registered. 5491 * 5492 * @param aListener 5493 * The InstallListener to add 5494 */ 5495 addListener(aListener) { 5496 if (!this.listeners.some(function(i) { return i == aListener; })) 5497 this.listeners.push(aListener); 5498 } 5499 5500 /** 5501 * Removes an InstallListener for this instance if it is registered. 5502 * 5503 * @param aListener 5504 * The InstallListener to remove 5505 */ 5506 removeListener(aListener) { 5507 this.listeners = this.listeners.filter(function(i) { 5508 return i != aListener; 5509 }); 5510 } 5511 5512 /** 5513 * Removes the temporary file owned by this AddonInstall if there is one. 5514 */ 5515 removeTemporaryFile() { 5516 // Only proceed if this AddonInstall owns its XPI file 5517 if (!this.ownsTempFile) { 5518 this.logger.debug("removeTemporaryFile: " + this.sourceURI.spec + " does not own temp file"); 5519 return; 5520 } 5521 5522 try { 5523 this.logger.debug("removeTemporaryFile: " + this.sourceURI.spec + " removing temp file " + 5524 this.file.path); 5525 this.file.remove(true); 5526 this.ownsTempFile = false; 5527 } 5528 catch (e) { 5529 this.logger.warn("Failed to remove temporary file " + this.file.path + " for addon " + 5530 this.sourceURI.spec, 5531 e); 5532 } 5533 } 5534 5535 /** 5536 * Updates the sourceURI and releaseNotesURI values on the Addon being 5537 * installed by this AddonInstall instance. 5538 */ 5539 updateAddonURIs() { 5540 this.addon.sourceURI = this.sourceURI.spec; 5541 if (this.releaseNotesURI) 5542 this.addon.releaseNotesURI = this.releaseNotesURI.spec; 5543 } 5544 5545 /** 5546 * Fills out linkedInstalls with AddonInstall instances for the other files 5547 * in a multi-package XPI. 5548 * 5549 * @param aFiles 5550 * An array of { entryName, file } for each remaining file from the 5551 * multi-package XPI. 5552 */ 5553 _createLinkedInstalls(aFiles) { 5554 return Task.spawn((function*() { 5555 if (aFiles.length == 0) 5556 return; 5557 5558 // Create new AddonInstall instances for every remaining file 5559 if (!this.linkedInstalls) 5560 this.linkedInstalls = []; 5561 5562 for (let { entryName, file } of aFiles) { 5563 logger.debug("Creating linked install from " + entryName); 5564 let install = yield createLocalInstall(file); 5565 5566 // Make the new install own its temporary file 5567 install.ownsTempFile = true; 5568 5569 this.linkedInstalls.push(install); 5570 5571 // If one of the internal XPIs was multipackage then move its linked 5572 // installs to the outer install 5573 if (install.linkedInstalls) { 5574 this.linkedInstalls.push(...install.linkedInstalls); 5575 install.linkedInstalls = null; 5576 } 5577 5578 install.sourceURI = this.sourceURI; 5579 install.releaseNotesURI = this.releaseNotesURI; 5580 if (install.state != AddonManager.STATE_DOWNLOAD_FAILED) 5581 install.updateAddonURIs(); 5582 } 5583 }).bind(this)); 5584 } 5585 5586 /** 5587 * Loads add-on manifests from a multi-package XPI file. Each of the 5588 * XPI and JAR files contained in the XPI will be extracted. Any that 5589 * do not contain valid add-ons will be ignored. The first valid add-on will 5590 * be installed by this AddonInstall instance, the rest will have new 5591 * AddonInstall instances created for them. 5592 * 5593 * @param aZipReader 5594 * An open nsIZipReader for the multi-package XPI's files. This will 5595 * be closed before this method returns. 5596 */ 5597 _loadMultipackageManifests(aZipReader) { 5598 return Task.spawn((function*() { 5599 let files = []; 5600 let entries = aZipReader.findEntries("(*.[Xx][Pp][Ii]|*.[Jj][Aa][Rr])"); 5601 while (entries.hasMore()) { 5602 let entryName = entries.getNext(); 5603 let file = getTemporaryFile(); 5604 try { 5605 aZipReader.extract(entryName, file); 5606 files.push({ entryName, file }); 5607 } 5608 catch (e) { 5609 logger.warn("Failed to extract " + entryName + " from multi-package " + 5610 "XPI", e); 5611 file.remove(false); 5612 } 5613 } 5614 5615 aZipReader.close(); 5616 5617 if (files.length == 0) { 5618 return Promise.reject([AddonManager.ERROR_CORRUPT_FILE, 5619 "Multi-package XPI does not contain any packages to install"]); 5620 } 5621 5622 let addon = null; 5623 5624 // Find the first file that is a valid install and use it for 5625 // the add-on that this AddonInstall instance will install. 5626 for (let { entryName, file } of files) { 5627 this.removeTemporaryFile(); 5628 try { 5629 yield this.loadManifest(file); 5630 logger.debug("Base multi-package XPI install came from " + entryName); 5631 this.file = file; 5632 this.ownsTempFile = true; 5633 5634 yield this._createLinkedInstalls(files.filter(f => f.file != file)); 5635 return undefined; 5636 } 5637 catch (e) { 5638 // _createLinkedInstalls will log errors when it tries to process this 5639 // file 5640 } 5641 } 5642 5643 // No valid add-on was found, delete all the temporary files 5644 for (let { file } of files) { 5645 try { 5646 file.remove(true); 5647 } catch (e) { 5648 this.logger.warn("Could not remove temp file " + file.path); 5649 } 5650 } 5651 5652 return Promise.reject([AddonManager.ERROR_CORRUPT_FILE, 5653 "Multi-package XPI does not contain any valid packages to install"]); 5654 }).bind(this)); 5655 } 5656 5657 /** 5658 * Called after the add-on is a local file and the signature and install 5659 * manifest can be read. 5660 * 5661 * @param aCallback 5662 * A function to call when the manifest has been loaded 5663 * @throws if the add-on does not contain a valid install manifest or the 5664 * XPI is incorrectly signed 5665 */ 5666 loadManifest(file) { 5667 return Task.spawn((function*() { 5668 let zipreader = Cc["@mozilla.org/libjar/zip-reader;1"]. 5669 createInstance(Ci.nsIZipReader); 5670 try { 5671 zipreader.open(file); 5672 } 5673 catch (e) { 5674 zipreader.close(); 5675 return Promise.reject([AddonManager.ERROR_CORRUPT_FILE, e]); 5676 } 5677 5678 try { 5679 // loadManifestFromZipReader performs the certificate verification for us 5680 this.addon = yield loadManifestFromZipReader(zipreader, this.installLocation); 5681 } 5682 catch (e) { 5683 zipreader.close(); 5684 return Promise.reject([AddonManager.ERROR_CORRUPT_FILE, e]); 5685 } 5686 5687 // A multi-package XPI is a container, the add-ons it holds each 5688 // have their own id. Everything else had better have an id here. 5689 if (!this.addon.id && this.addon.type != "multipackage") { 5690 let err = new Error(`Cannot find id for addon ${file.path}`); 5691 return Promise.reject([AddonManager.ERROR_CORRUPT_FILE, err]); 5692 } 5693 5694 if (this.existingAddon) { 5695 // Check various conditions related to upgrades 5696 if (this.addon.id != this.existingAddon.id) { 5697 zipreader.close(); 5698 return Promise.reject([AddonManager.ERROR_INCORRECT_ID, 5699 `Refusing to upgrade addon ${this.existingAddon.id} to different ID {this.addon.id}`]); 5700 } 5701 5702 if (this.addon.type == "multipackage") { 5703 zipreader.close(); 5704 return Promise.reject([AddonManager.ERROR_UNEXPECTED_ADDON_TYPE, 5705 `Refusing to upgrade addon ${this.existingAddon.id} to a multi-package xpi`]); 5706 } 5707 5708 if (this.existingAddon.type == "webextension" && this.addon.type != "webextension") { 5709 zipreader.close(); 5710 return Promise.reject([AddonManager.ERROR_UNEXPECTED_ADDON_TYPE, 5711 "WebExtensions may not be upated to other extension types"]); 5712 } 5713 } 5714 5715 if (mustSign(this.addon.type)) { 5716 if (this.addon.signedState <= AddonManager.SIGNEDSTATE_MISSING) { 5717 // This add-on isn't properly signed by a signature that chains to the 5718 // trusted root. 5719 let state = this.addon.signedState; 5720 this.addon = null; 5721 zipreader.close(); 5722 5723 if (state == AddonManager.SIGNEDSTATE_MISSING) 5724 return Promise.reject([AddonManager.ERROR_SIGNEDSTATE_REQUIRED, 5725 "signature is required but missing"]) 5726 5727 return Promise.reject([AddonManager.ERROR_CORRUPT_FILE, 5728 "signature verification failed"]) 5729 } 5730 } 5731 else if (this.addon.signedState == AddonManager.SIGNEDSTATE_UNKNOWN || 5732 this.addon.signedState == AddonManager.SIGNEDSTATE_NOT_REQUIRED) { 5733 // Check object signing certificate, if any 5734 let x509 = zipreader.getSigningCert(null); 5735 if (x509) { 5736 logger.debug("Verifying XPI signature"); 5737 if (verifyZipSigning(zipreader, x509)) { 5738 this.certificate = x509; 5739 if (this.certificate.commonName.length > 0) { 5740 this.certName = this.certificate.commonName; 5741 } else { 5742 this.certName = this.certificate.organization; 5743 } 5744 } else { 5745 zipreader.close(); 5746 return Promise.reject([AddonManager.ERROR_CORRUPT_FILE, 5747 "XPI is incorrectly signed"]); 5748 } 5749 } 5750 } 5751 5752 if (this.addon.type == "multipackage") 5753 return this._loadMultipackageManifests(zipreader); 5754 5755 zipreader.close(); 5756 5757 this.updateAddonURIs(); 5758 5759 this.addon._install = this; 5760 this.name = this.addon.selectedLocale.name; 5761 this.type = this.addon.type; 5762 this.version = this.addon.version; 5763 5764 // Setting the iconURL to something inside the XPI locks the XPI and 5765 // makes it impossible to delete on Windows. 5766 5767 // Try to load from the existing cache first 5768 let repoAddon = yield new Promise(resolve => AddonRepository.getCachedAddonByID(this.addon.id, resolve)); 5769 5770 // It wasn't there so try to re-download it 5771 if (!repoAddon) { 5772 yield new Promise(resolve => AddonRepository.cacheAddons([this.addon.id], resolve)); 5773 repoAddon = yield new Promise(resolve => AddonRepository.getCachedAddonByID(this.addon.id, resolve)); 5774 } 5775 5776 this.addon._repositoryAddon = repoAddon; 5777 this.name = this.name || this.addon._repositoryAddon.name; 5778 this.addon.compatibilityOverrides = repoAddon ? 5779 repoAddon.compatibilityOverrides : 5780 null; 5781 this.addon.appDisabled = !isUsableAddon(this.addon); 5782 return undefined; 5783 }).bind(this)); 5784 } 5785 5786 // TODO This relies on the assumption that we are always installing into the 5787 // highest priority install location so the resulting add-on will be visible 5788 // overriding any existing copy in another install location (bug 557710). 5789 /** 5790 * Installs the add-on into the install location. 5791 */ 5792 startInstall() { 5793 this.state = AddonManager.STATE_INSTALLING; 5794 if (!AddonManagerPrivate.callInstallListeners("onInstallStarted", 5795 this.listeners, this.wrapper)) { 5796 this.state = AddonManager.STATE_DOWNLOADED; 5797 XPIProvider.removeActiveInstall(this); 5798 AddonManagerPrivate.callInstallListeners("onInstallCancelled", 5799 this.listeners, this.wrapper) 5800 return; 5801 } 5802 5803 // Find and cancel any pending installs for the same add-on in the same 5804 // install location 5805 for (let aInstall of XPIProvider.installs) { 5806 if (aInstall.state == AddonManager.STATE_INSTALLED && 5807 aInstall.installLocation == this.installLocation && 5808 aInstall.addon.id == this.addon.id) { 5809 logger.debug("Cancelling previous pending install of " + aInstall.addon.id); 5810 aInstall.cancel(); 5811 } 5812 } 5813 5814 let isUpgrade = this.existingAddon && 5815 this.existingAddon._installLocation == this.installLocation; 5816 let requiresRestart = XPIProvider.installRequiresRestart(this.addon); 5817 5818 logger.debug("Starting install of " + this.addon.id + " from " + this.sourceURI.spec); 5819 AddonManagerPrivate.callAddonListeners("onInstalling", 5820 this.addon.wrapper, 5821 requiresRestart); 5822 5823 let stagedAddon = this.installLocation.getStagingDir(); 5824 5825 Task.spawn((function*() { 5826 let installedUnpacked = 0; 5827 5828 yield this.installLocation.requestStagingDir(); 5829 5830 // remove any previously staged files 5831 yield this.unstageInstall(stagedAddon); 5832 5833 stagedAddon.append(this.addon.id); 5834 stagedAddon.leafName = this.addon.id + ".xpi"; 5835 5836 installedUnpacked = yield this.stageInstall(requiresRestart, stagedAddon, isUpgrade); 5837 5838 if (requiresRestart) { 5839 this.state = AddonManager.STATE_INSTALLED; 5840 AddonManagerPrivate.callInstallListeners("onInstallEnded", 5841 this.listeners, this.wrapper, 5842 this.addon.wrapper); 5843 } 5844 else { 5845 // The install is completed so it should be removed from the active list 5846 XPIProvider.removeActiveInstall(this); 5847 5848 // Deactivate and remove the old add-on as necessary 5849 let reason = BOOTSTRAP_REASONS.ADDON_INSTALL; 5850 if (this.existingAddon) { 5851 if (Services.vc.compare(this.existingAddon.version, this.addon.version) < 0) 5852 reason = BOOTSTRAP_REASONS.ADDON_UPGRADE; 5853 else 5854 reason = BOOTSTRAP_REASONS.ADDON_DOWNGRADE; 5855 5856 if (this.existingAddon.bootstrap) { 5857 let file = this.existingAddon._sourceBundle; 5858 if (this.existingAddon.active) { 5859 XPIProvider.callBootstrapMethod(this.existingAddon, file, 5860 "shutdown", reason, 5861 { newVersion: this.addon.version }); 5862 } 5863 5864 XPIProvider.callBootstrapMethod(this.existingAddon, file, 5865 "uninstall", reason, 5866 { newVersion: this.addon.version }); 5867 XPIProvider.unloadBootstrapScope(this.existingAddon.id); 5868 flushChromeCaches(); 5869 } 5870 5871 if (!isUpgrade && this.existingAddon.active) { 5872 XPIDatabase.updateAddonActive(this.existingAddon, false); 5873 } 5874 } 5875 5876 // Install the new add-on into its final location 5877 let existingAddonID = this.existingAddon ? this.existingAddon.id : null; 5878 let file = this.installLocation.installAddon({ 5879 id: this.addon.id, 5880 source: stagedAddon, 5881 existingAddonID 5882 }); 5883 5884 // Update the metadata in the database 5885 this.addon._sourceBundle = file; 5886 this.addon.visible = true; 5887 5888 if (isUpgrade) { 5889 this.addon = XPIDatabase.updateAddonMetadata(this.existingAddon, this.addon, 5890 file.persistentDescriptor); 5891 let state = XPIStates.getAddon(this.installLocation.name, this.addon.id); 5892 if (state) { 5893 state.syncWithDB(this.addon, true); 5894 } else { 5895 logger.warn("Unexpected missing XPI state for add-on ${id}", this.addon); 5896 } 5897 } 5898 else { 5899 this.addon.active = (this.addon.visible && !this.addon.disabled); 5900 this.addon = XPIDatabase.addAddonMetadata(this.addon, file.persistentDescriptor); 5901 XPIStates.addAddon(this.addon); 5902 this.addon.installDate = this.addon.updateDate; 5903 XPIDatabase.saveChanges(); 5904 } 5905 XPIStates.save(); 5906 5907 let extraParams = {}; 5908 if (this.existingAddon) { 5909 extraParams.oldVersion = this.existingAddon.version; 5910 } 5911 5912 if (this.addon.bootstrap) { 5913 XPIProvider.callBootstrapMethod(this.addon, file, "install", 5914 reason, extraParams); 5915 } 5916 5917 AddonManagerPrivate.callAddonListeners("onInstalled", 5918 this.addon.wrapper); 5919 5920 logger.debug("Install of " + this.sourceURI.spec + " completed."); 5921 this.state = AddonManager.STATE_INSTALLED; 5922 AddonManagerPrivate.callInstallListeners("onInstallEnded", 5923 this.listeners, this.wrapper, 5924 this.addon.wrapper); 5925 5926 if (this.addon.bootstrap) { 5927 if (this.addon.active) { 5928 XPIProvider.callBootstrapMethod(this.addon, file, "startup", 5929 reason, extraParams); 5930 } 5931 else { 5932 // XXX this makes it dangerous to do some things in onInstallEnded 5933 // listeners because important cleanup hasn't been done yet 5934 XPIProvider.unloadBootstrapScope(this.addon.id); 5935 } 5936 } 5937 XPIProvider.setTelemetry(this.addon.id, "unpacked", installedUnpacked); 5938 recordAddonTelemetry(this.addon); 5939 } 5940 }).bind(this)).then(null, (e) => { 5941 logger.warn(`Failed to install ${this.file.path} from ${this.sourceURI.spec} to ${stagedAddon.path}`, e); 5942 5943 if (stagedAddon.exists()) 5944 recursiveRemove(stagedAddon); 5945 this.state = AddonManager.STATE_INSTALL_FAILED; 5946 this.error = AddonManager.ERROR_FILE_ACCESS; 5947 XPIProvider.removeActiveInstall(this); 5948 AddonManagerPrivate.callAddonListeners("onOperationCancelled", 5949 this.addon.wrapper); 5950 AddonManagerPrivate.callInstallListeners("onInstallFailed", 5951 this.listeners, 5952 this.wrapper); 5953 }).then(() => { 5954 this.removeTemporaryFile(); 5955 return this.installLocation.releaseStagingDir(); 5956 }); 5957 } 5958 5959 /** 5960 * Stages an upgrade for next application restart. 5961 */ 5962 stageInstall(restartRequired, stagedAddon, isUpgrade) { 5963 return Task.spawn((function*() { 5964 let stagedJSON = stagedAddon.clone(); 5965 stagedJSON.leafName = this.addon.id + ".json"; 5966 5967 let installedUnpacked = 0; 5968 5969 // First stage the file regardless of whether restarting is necessary 5970 if (this.addon.unpack || Preferences.get(PREF_XPI_UNPACK, false)) { 5971 logger.debug("Addon " + this.addon.id + " will be installed as " + 5972 "an unpacked directory"); 5973 stagedAddon.leafName = this.addon.id; 5974 yield OS.File.makeDir(stagedAddon.path); 5975 yield ZipUtils.extractFilesAsync(this.file, stagedAddon); 5976 installedUnpacked = 1; 5977 } 5978 else { 5979 logger.debug(`Addon ${this.addon.id} will be installed as a packed xpi`); 5980 stagedAddon.leafName = this.addon.id + ".xpi"; 5981 5982 yield OS.File.copy(this.file.path, stagedAddon.path); 5983 } 5984 5985 if (restartRequired) { 5986 // Point the add-on to its extracted files as the xpi may get deleted 5987 this.addon._sourceBundle = stagedAddon; 5988 5989 // Cache the AddonInternal as it may have updated compatibility info 5990 writeStringToFile(stagedJSON, JSON.stringify(this.addon)); 5991 5992 logger.debug("Staged install of " + this.addon.id + " from " + this.sourceURI.spec + " ready; waiting for restart."); 5993 if (isUpgrade) { 5994 delete this.existingAddon.pendingUpgrade; 5995 this.existingAddon.pendingUpgrade = this.addon; 5996 } 5997 } 5998 5999 return installedUnpacked; 6000 }).bind(this)); 6001 } 6002 6003 /** 6004 * Removes any previously staged upgrade. 6005 */ 6006 unstageInstall(stagedAddon) { 6007 return Task.spawn((function*() { 6008 let stagedJSON = stagedAddon.clone(); 6009 let removedAddon = stagedAddon.clone(); 6010 6011 stagedJSON.append(this.addon.id + ".json"); 6012 6013 if (stagedJSON.exists()) { 6014 stagedJSON.remove(true); 6015 } 6016 6017 removedAddon.append(this.addon.id); 6018 yield removeAsync(removedAddon); 6019 removedAddon.leafName = this.addon.id + ".xpi"; 6020 yield removeAsync(removedAddon); 6021 }).bind(this)); 6022 } 6023 6024 /** 6025 * Postone a pending update, until restart or until the add-on resumes. 6026 * 6027 * @param {Function} resumeFunction - a function for the add-on to run 6028 * when resuming. 6029 */ 6030 postpone(resumeFunction) { 6031 return Task.spawn((function*() { 6032 this.state = AddonManager.STATE_POSTPONED; 6033 6034 let stagingDir = this.installLocation.getStagingDir(); 6035 let stagedAddon = stagingDir.clone(); 6036 6037 yield this.installLocation.requestStagingDir(); 6038 yield this.unstageInstall(stagedAddon); 6039 6040 stagedAddon.append(this.addon.id); 6041 stagedAddon.leafName = this.addon.id + ".xpi"; 6042 6043 yield this.stageInstall(true, stagedAddon, true); 6044 6045 AddonManagerPrivate.callInstallListeners("onInstallPostponed", 6046 this.listeners, this.wrapper) 6047 6048 // upgrade has been staged for restart, provide a way for it to call the 6049 // resume function. 6050 if (resumeFunction) { 6051 let callback = AddonManagerPrivate.getUpgradeListener(this.addon.id); 6052 if (callback) { 6053 callback({ 6054 version: this.version, 6055 install: () => { 6056 switch (this.state) { 6057 case AddonManager.STATE_POSTPONED: 6058 resumeFunction(); 6059 break; 6060 default: 6061 logger.warn(`${this.addon.id} cannot resume postponed upgrade from state (${this.state})`); 6062 break; 6063 } 6064 }, 6065 }); 6066 } 6067 } 6068 this.installLocation.releaseStagingDir(); 6069 }).bind(this)); 6070 } 6071} 6072 6073class LocalAddonInstall extends AddonInstall { 6074 /** 6075 * Initialises this install to be an install from a local file. 6076 * 6077 * @returns Promise 6078 * A Promise that resolves when the object is ready to use. 6079 */ 6080 init() { 6081 return Task.spawn((function*() { 6082 this.file = this.sourceURI.QueryInterface(Ci.nsIFileURL).file; 6083 6084 if (!this.file.exists()) { 6085 logger.warn("XPI file " + this.file.path + " does not exist"); 6086 this.state = AddonManager.STATE_DOWNLOAD_FAILED; 6087 this.error = AddonManager.ERROR_NETWORK_FAILURE; 6088 XPIProvider.removeActiveInstall(this); 6089 return; 6090 } 6091 6092 this.state = AddonManager.STATE_DOWNLOADED; 6093 this.progress = this.file.fileSize; 6094 this.maxProgress = this.file.fileSize; 6095 6096 if (this.hash) { 6097 let crypto = Cc["@mozilla.org/security/hash;1"]. 6098 createInstance(Ci.nsICryptoHash); 6099 try { 6100 crypto.initWithString(this.hash.algorithm); 6101 } 6102 catch (e) { 6103 logger.warn("Unknown hash algorithm '" + this.hash.algorithm + "' for addon " + this.sourceURI.spec, e); 6104 this.state = AddonManager.STATE_DOWNLOAD_FAILED; 6105 this.error = AddonManager.ERROR_INCORRECT_HASH; 6106 XPIProvider.removeActiveInstall(this); 6107 return; 6108 } 6109 6110 let fis = Cc["@mozilla.org/network/file-input-stream;1"]. 6111 createInstance(Ci.nsIFileInputStream); 6112 fis.init(this.file, -1, -1, false); 6113 crypto.updateFromStream(fis, this.file.fileSize); 6114 let calculatedHash = getHashStringForCrypto(crypto); 6115 if (calculatedHash != this.hash.data) { 6116 logger.warn("File hash (" + calculatedHash + ") did not match provided hash (" + 6117 this.hash.data + ")"); 6118 this.state = AddonManager.STATE_DOWNLOAD_FAILED; 6119 this.error = AddonManager.ERROR_INCORRECT_HASH; 6120 XPIProvider.removeActiveInstall(this); 6121 return; 6122 } 6123 } 6124 6125 try { 6126 yield this.loadManifest(this.file); 6127 } catch ([error, message]) { 6128 logger.warn("Invalid XPI", message); 6129 this.state = AddonManager.STATE_DOWNLOAD_FAILED; 6130 this.error = error; 6131 XPIProvider.removeActiveInstall(this); 6132 AddonManagerPrivate.callInstallListeners("onNewInstall", 6133 this.listeners, 6134 this.wrapper); 6135 return; 6136 } 6137 6138 let addon = yield new Promise(resolve => { 6139 XPIDatabase.getVisibleAddonForID(this.addon.id, resolve); 6140 }); 6141 6142 this.existingAddon = addon; 6143 if (addon) 6144 applyBlocklistChanges(addon, this.addon); 6145 this.addon.updateDate = Date.now(); 6146 this.addon.installDate = addon ? addon.installDate : this.addon.updateDate; 6147 6148 if (!this.addon.isCompatible) { 6149 this.state = AddonManager.STATE_CHECKING; 6150 6151 yield new Promise(resolve => { 6152 new UpdateChecker(this.addon, { 6153 onUpdateFinished: aAddon => { 6154 this.state = AddonManager.STATE_DOWNLOADED; 6155 AddonManagerPrivate.callInstallListeners("onNewInstall", 6156 this.listeners, 6157 this.wrapper); 6158 resolve(); 6159 } 6160 }, AddonManager.UPDATE_WHEN_ADDON_INSTALLED); 6161 }); 6162 } 6163 else { 6164 AddonManagerPrivate.callInstallListeners("onNewInstall", 6165 this.listeners, 6166 this.wrapper); 6167 6168 } 6169 }).bind(this)); 6170 } 6171 6172 install() { 6173 if (this.state == AddonManager.STATE_DOWNLOAD_FAILED) { 6174 // For a local install, this state means that verification of the 6175 // file failed (e.g., the hash or signature or manifest contents 6176 // were invalid). It doesn't make sense to retry anything in this 6177 // case but we have callers who don't know if their AddonInstall 6178 // object is a local file or a download so accomodate them here. 6179 AddonManagerPrivate.callInstallListeners("onDownloadFailed", 6180 this.listeners, this.wrapper); 6181 return; 6182 } 6183 super.install(); 6184 } 6185} 6186 6187class DownloadAddonInstall extends AddonInstall { 6188 /** 6189 * Instantiates a DownloadAddonInstall 6190 * 6191 * @param installLocation 6192 * The InstallLocation the add-on will be installed into 6193 * @param url 6194 * The nsIURL to get the add-on from 6195 * @param name 6196 * An optional name for the add-on 6197 * @param hash 6198 * An optional hash for the add-on 6199 * @param existingAddon 6200 * The add-on this install will update if known 6201 * @param browser 6202 * The browser performing the install, used to display 6203 * authentication prompts. 6204 * @param type 6205 * An optional type for the add-on 6206 * @param icons 6207 * Optional icons for the add-on 6208 * @param version 6209 * An optional version for the add-on 6210 */ 6211 constructor(installLocation, url, hash, existingAddon, browser, 6212 name, type, icons, version) { 6213 super(installLocation, url, hash, existingAddon); 6214 6215 this.browser = browser; 6216 6217 this.state = AddonManager.STATE_AVAILABLE; 6218 this.name = name; 6219 this.type = type; 6220 this.version = version; 6221 this.icons = icons; 6222 6223 this.stream = null; 6224 this.crypto = null; 6225 this.badCertHandler = null; 6226 this.restartDownload = false; 6227 6228 AddonManagerPrivate.callInstallListeners("onNewInstall", this.listeners, 6229 this.wrapper); 6230 } 6231 6232 install() { 6233 switch (this.state) { 6234 case AddonManager.STATE_AVAILABLE: 6235 this.startDownload(); 6236 break; 6237 case AddonManager.STATE_DOWNLOAD_FAILED: 6238 case AddonManager.STATE_INSTALL_FAILED: 6239 case AddonManager.STATE_CANCELLED: 6240 this.removeTemporaryFile(); 6241 this.state = AddonManager.STATE_AVAILABLE; 6242 this.error = 0; 6243 this.progress = 0; 6244 this.maxProgress = -1; 6245 this.hash = this.originalHash; 6246 this.startDownload(); 6247 break; 6248 default: 6249 super.install(); 6250 } 6251 } 6252 6253 cancel() { 6254 if (this.state == AddonManager.STATE_DOWNLOADING) { 6255 if (this.channel) { 6256 logger.debug("Cancelling download of " + this.sourceURI.spec); 6257 this.channel.cancel(Cr.NS_BINDING_ABORTED); 6258 } 6259 } else { 6260 super.cancel(); 6261 } 6262 } 6263 6264 observe(aSubject, aTopic, aData) { 6265 // Network is going offline 6266 this.cancel(); 6267 } 6268 6269 /** 6270 * Starts downloading the add-on's XPI file. 6271 */ 6272 startDownload() { 6273 this.state = AddonManager.STATE_DOWNLOADING; 6274 if (!AddonManagerPrivate.callInstallListeners("onDownloadStarted", 6275 this.listeners, this.wrapper)) { 6276 logger.debug("onDownloadStarted listeners cancelled installation of addon " + this.sourceURI.spec); 6277 this.state = AddonManager.STATE_CANCELLED; 6278 XPIProvider.removeActiveInstall(this); 6279 AddonManagerPrivate.callInstallListeners("onDownloadCancelled", 6280 this.listeners, this.wrapper) 6281 return; 6282 } 6283 6284 // If a listener changed our state then do not proceed with the download 6285 if (this.state != AddonManager.STATE_DOWNLOADING) 6286 return; 6287 6288 if (this.channel) { 6289 // A previous download attempt hasn't finished cleaning up yet, signal 6290 // that it should restart when complete 6291 logger.debug("Waiting for previous download to complete"); 6292 this.restartDownload = true; 6293 return; 6294 } 6295 6296 this.openChannel(); 6297 } 6298 6299 openChannel() { 6300 this.restartDownload = false; 6301 6302 try { 6303 this.file = getTemporaryFile(); 6304 this.ownsTempFile = true; 6305 this.stream = Cc["@mozilla.org/network/file-output-stream;1"]. 6306 createInstance(Ci.nsIFileOutputStream); 6307 this.stream.init(this.file, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | 6308 FileUtils.MODE_TRUNCATE, FileUtils.PERMS_FILE, 0); 6309 } 6310 catch (e) { 6311 logger.warn("Failed to start download for addon " + this.sourceURI.spec, e); 6312 this.state = AddonManager.STATE_DOWNLOAD_FAILED; 6313 this.error = AddonManager.ERROR_FILE_ACCESS; 6314 XPIProvider.removeActiveInstall(this); 6315 AddonManagerPrivate.callInstallListeners("onDownloadFailed", 6316 this.listeners, this.wrapper); 6317 return; 6318 } 6319 6320 let listener = Cc["@mozilla.org/network/stream-listener-tee;1"]. 6321 createInstance(Ci.nsIStreamListenerTee); 6322 listener.init(this, this.stream); 6323 try { 6324 let requireBuiltIn = Preferences.get(PREF_INSTALL_REQUIREBUILTINCERTS, true); 6325 this.badCertHandler = new CertUtils.BadCertHandler(!requireBuiltIn); 6326 6327 this.channel = NetUtil.newChannel({ 6328 uri: this.sourceURI, 6329 loadUsingSystemPrincipal: true 6330 }); 6331 this.channel.notificationCallbacks = this; 6332 if (this.channel instanceof Ci.nsIHttpChannel) { 6333 this.channel.setRequestHeader("Moz-XPI-Update", "1", true); 6334 if (this.channel instanceof Ci.nsIHttpChannelInternal) 6335 this.channel.forceAllowThirdPartyCookie = true; 6336 } 6337 this.channel.asyncOpen2(listener); 6338 6339 Services.obs.addObserver(this, "network:offline-about-to-go-offline", false); 6340 } 6341 catch (e) { 6342 logger.warn("Failed to start download for addon " + this.sourceURI.spec, e); 6343 this.state = AddonManager.STATE_DOWNLOAD_FAILED; 6344 this.error = AddonManager.ERROR_NETWORK_FAILURE; 6345 XPIProvider.removeActiveInstall(this); 6346 AddonManagerPrivate.callInstallListeners("onDownloadFailed", 6347 this.listeners, this.wrapper); 6348 } 6349 } 6350 6351 /** 6352 * Update the crypto hasher with the new data and call the progress listeners. 6353 * 6354 * @see nsIStreamListener 6355 */ 6356 onDataAvailable(aRequest, aContext, aInputstream, aOffset, aCount) { 6357 this.crypto.updateFromStream(aInputstream, aCount); 6358 this.progress += aCount; 6359 if (!AddonManagerPrivate.callInstallListeners("onDownloadProgress", 6360 this.listeners, this.wrapper)) { 6361 // TODO cancel the download and make it available again (bug 553024) 6362 } 6363 } 6364 6365 /** 6366 * Check the redirect response for a hash of the target XPI and verify that 6367 * we don't end up on an insecure channel. 6368 * 6369 * @see nsIChannelEventSink 6370 */ 6371 asyncOnChannelRedirect(aOldChannel, aNewChannel, aFlags, aCallback) { 6372 if (!this.hash && aOldChannel.originalURI.schemeIs("https") && 6373 aOldChannel instanceof Ci.nsIHttpChannel) { 6374 try { 6375 let hashStr = aOldChannel.getResponseHeader("X-Target-Digest"); 6376 let hashSplit = hashStr.toLowerCase().split(":"); 6377 this.hash = { 6378 algorithm: hashSplit[0], 6379 data: hashSplit[1] 6380 }; 6381 } 6382 catch (e) { 6383 } 6384 } 6385 6386 // Verify that we don't end up on an insecure channel if we haven't got a 6387 // hash to verify with (see bug 537761 for discussion) 6388 if (!this.hash) 6389 this.badCertHandler.asyncOnChannelRedirect(aOldChannel, aNewChannel, aFlags, aCallback); 6390 else 6391 aCallback.onRedirectVerifyCallback(Cr.NS_OK); 6392 6393 this.channel = aNewChannel; 6394 } 6395 6396 /** 6397 * This is the first chance to get at real headers on the channel. 6398 * 6399 * @see nsIStreamListener 6400 */ 6401 onStartRequest(aRequest, aContext) { 6402 this.crypto = Cc["@mozilla.org/security/hash;1"]. 6403 createInstance(Ci.nsICryptoHash); 6404 if (this.hash) { 6405 try { 6406 this.crypto.initWithString(this.hash.algorithm); 6407 } 6408 catch (e) { 6409 logger.warn("Unknown hash algorithm '" + this.hash.algorithm + "' for addon " + this.sourceURI.spec, e); 6410 this.state = AddonManager.STATE_DOWNLOAD_FAILED; 6411 this.error = AddonManager.ERROR_INCORRECT_HASH; 6412 XPIProvider.removeActiveInstall(this); 6413 AddonManagerPrivate.callInstallListeners("onDownloadFailed", 6414 this.listeners, this.wrapper); 6415 aRequest.cancel(Cr.NS_BINDING_ABORTED); 6416 return; 6417 } 6418 } 6419 else { 6420 // We always need something to consume data from the inputstream passed 6421 // to onDataAvailable so just create a dummy cryptohasher to do that. 6422 this.crypto.initWithString("sha1"); 6423 } 6424 6425 this.progress = 0; 6426 if (aRequest instanceof Ci.nsIChannel) { 6427 try { 6428 this.maxProgress = aRequest.contentLength; 6429 } 6430 catch (e) { 6431 } 6432 logger.debug("Download started for " + this.sourceURI.spec + " to file " + 6433 this.file.path); 6434 } 6435 } 6436 6437 /** 6438 * The download is complete. 6439 * 6440 * @see nsIStreamListener 6441 */ 6442 onStopRequest(aRequest, aContext, aStatus) { 6443 this.stream.close(); 6444 this.channel = null; 6445 this.badCerthandler = null; 6446 Services.obs.removeObserver(this, "network:offline-about-to-go-offline"); 6447 6448 // If the download was cancelled then update the state and send events 6449 if (aStatus == Cr.NS_BINDING_ABORTED) { 6450 if (this.state == AddonManager.STATE_DOWNLOADING) { 6451 logger.debug("Cancelled download of " + this.sourceURI.spec); 6452 this.state = AddonManager.STATE_CANCELLED; 6453 XPIProvider.removeActiveInstall(this); 6454 AddonManagerPrivate.callInstallListeners("onDownloadCancelled", 6455 this.listeners, this.wrapper); 6456 // If a listener restarted the download then there is no need to 6457 // remove the temporary file 6458 if (this.state != AddonManager.STATE_CANCELLED) 6459 return; 6460 } 6461 6462 this.removeTemporaryFile(); 6463 if (this.restartDownload) 6464 this.openChannel(); 6465 return; 6466 } 6467 6468 logger.debug("Download of " + this.sourceURI.spec + " completed."); 6469 6470 if (Components.isSuccessCode(aStatus)) { 6471 if (!(aRequest instanceof Ci.nsIHttpChannel) || aRequest.requestSucceeded) { 6472 if (!this.hash && (aRequest instanceof Ci.nsIChannel)) { 6473 try { 6474 CertUtils.checkCert(aRequest, 6475 !Preferences.get(PREF_INSTALL_REQUIREBUILTINCERTS, true)); 6476 } 6477 catch (e) { 6478 this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE, e); 6479 return; 6480 } 6481 } 6482 6483 // convert the binary hash data to a hex string. 6484 let calculatedHash = getHashStringForCrypto(this.crypto); 6485 this.crypto = null; 6486 if (this.hash && calculatedHash != this.hash.data) { 6487 this.downloadFailed(AddonManager.ERROR_INCORRECT_HASH, 6488 "Downloaded file hash (" + calculatedHash + 6489 ") did not match provided hash (" + this.hash.data + ")"); 6490 return; 6491 } 6492 6493 this.loadManifest(this.file).then(() => { 6494 if (this.addon.isCompatible) { 6495 this.downloadCompleted(); 6496 } 6497 else { 6498 // TODO Should we send some event here (bug 557716)? 6499 this.state = AddonManager.STATE_CHECKING; 6500 new UpdateChecker(this.addon, { 6501 onUpdateFinished: aAddon => this.downloadCompleted(), 6502 }, AddonManager.UPDATE_WHEN_ADDON_INSTALLED); 6503 } 6504 }, ([error, message]) => { 6505 this.removeTemporaryFile(); 6506 this.downloadFailed(error, message); 6507 }); 6508 } 6509 else if (aRequest instanceof Ci.nsIHttpChannel) { 6510 this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE, 6511 aRequest.responseStatus + " " + 6512 aRequest.responseStatusText); 6513 } 6514 else { 6515 this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE, aStatus); 6516 } 6517 } 6518 else { 6519 this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE, aStatus); 6520 } 6521 } 6522 6523 /** 6524 * Notify listeners that the download failed. 6525 * 6526 * @param aReason 6527 * Something to log about the failure 6528 * @param error 6529 * The error code to pass to the listeners 6530 */ 6531 downloadFailed(aReason, aError) { 6532 logger.warn("Download of " + this.sourceURI.spec + " failed", aError); 6533 this.state = AddonManager.STATE_DOWNLOAD_FAILED; 6534 this.error = aReason; 6535 XPIProvider.removeActiveInstall(this); 6536 AddonManagerPrivate.callInstallListeners("onDownloadFailed", this.listeners, 6537 this.wrapper); 6538 6539 // If the listener hasn't restarted the download then remove any temporary 6540 // file 6541 if (this.state == AddonManager.STATE_DOWNLOAD_FAILED) { 6542 logger.debug("downloadFailed: removing temp file for " + this.sourceURI.spec); 6543 this.removeTemporaryFile(); 6544 } 6545 else 6546 logger.debug("downloadFailed: listener changed AddonInstall state for " + 6547 this.sourceURI.spec + " to " + this.state); 6548 } 6549 6550 /** 6551 * Notify listeners that the download completed. 6552 */ 6553 downloadCompleted() { 6554 XPIDatabase.getVisibleAddonForID(this.addon.id, aAddon => { 6555 if (aAddon) 6556 this.existingAddon = aAddon; 6557 6558 this.state = AddonManager.STATE_DOWNLOADED; 6559 this.addon.updateDate = Date.now(); 6560 6561 if (this.existingAddon) { 6562 this.addon.existingAddonID = this.existingAddon.id; 6563 this.addon.installDate = this.existingAddon.installDate; 6564 applyBlocklistChanges(this.existingAddon, this.addon); 6565 } 6566 else { 6567 this.addon.installDate = this.addon.updateDate; 6568 } 6569 6570 if (AddonManagerPrivate.callInstallListeners("onDownloadEnded", 6571 this.listeners, 6572 this.wrapper)) { 6573 // If a listener changed our state then do not proceed with the install 6574 if (this.state != AddonManager.STATE_DOWNLOADED) 6575 return; 6576 6577 // If an upgrade listener is registered for this add-on, pass control 6578 // over the upgrade to the add-on. 6579 if (AddonManagerPrivate.hasUpgradeListener(this.addon.id)) { 6580 logger.info(`add-on ${this.addon.id} has an upgrade listener, postponing upgrade until restart`); 6581 let resumeFn = () => { 6582 logger.info(`${this.addon.id} has resumed a previously postponed upgrade`); 6583 this.state = AddonManager.STATE_DOWNLOADED; 6584 this.install(); 6585 } 6586 this.postpone(resumeFn); 6587 } else { 6588 // no upgrade listener present, so proceed with normal install 6589 this.install(); 6590 if (this.linkedInstalls) { 6591 for (let install of this.linkedInstalls) { 6592 if (install.state == AddonManager.STATE_DOWNLOADED) 6593 install.install(); 6594 } 6595 } 6596 } 6597 } 6598 }); 6599 } 6600 6601 getInterface(iid) { 6602 if (iid.equals(Ci.nsIAuthPrompt2)) { 6603 let win = null; 6604 if (this.browser) { 6605 win = this.browser.contentWindow || this.browser.ownerDocument.defaultView; 6606 } 6607 6608 let factory = Cc["@mozilla.org/prompter;1"]. 6609 getService(Ci.nsIPromptFactory); 6610 let prompt = factory.getPrompt(win, Ci.nsIAuthPrompt2); 6611 6612 if (this.browser && prompt instanceof Ci.nsILoginManagerPrompter) 6613 prompt.browser = this.browser; 6614 6615 return prompt; 6616 } 6617 else if (iid.equals(Ci.nsIChannelEventSink)) { 6618 return this; 6619 } 6620 6621 return this.badCertHandler.getInterface(iid); 6622 } 6623 6624 /** 6625 * Postone a pending update, until restart or until the add-on resumes. 6626 * 6627 * @param {Function} resumeFn - a function for the add-on to run 6628 * when resuming. 6629 */ 6630 postpone(resumeFn) { 6631 return Task.spawn((function*() { 6632 this.state = AddonManager.STATE_POSTPONED; 6633 6634 let stagingDir = this.installLocation.getStagingDir(); 6635 let stagedAddon = stagingDir.clone(); 6636 6637 yield this.installLocation.requestStagingDir(); 6638 yield this.unstageInstall(stagedAddon); 6639 6640 stagedAddon.append(this.addon.id); 6641 stagedAddon.leafName = this.addon.id + ".xpi"; 6642 6643 yield this.stageInstall(true, stagedAddon, true); 6644 6645 AddonManagerPrivate.callInstallListeners("onInstallPostponed", 6646 this.listeners, this.wrapper) 6647 6648 // upgrade has been staged for restart, provide a way for it to call the 6649 // resume function. 6650 let callback = AddonManagerPrivate.getUpgradeListener(this.addon.id); 6651 if (callback) { 6652 callback({ 6653 version: this.version, 6654 install: () => { 6655 switch (this.state) { 6656 case AddonManager.STATE_POSTPONED: 6657 if (resumeFn) { 6658 resumeFn(); 6659 } 6660 break; 6661 default: 6662 logger.warn(`${this.addon.id} cannot resume postponed upgrade from state (${this.state})`); 6663 break; 6664 } 6665 }, 6666 }); 6667 } 6668 // Release the staging directory lock, but since the staging dir is populated 6669 // it will not be removed until resumed or installed by restart. 6670 // See also cleanStagingDir() 6671 this.installLocation.releaseStagingDir(); 6672 }).bind(this)); 6673 } 6674} 6675 6676/** 6677 * This class exists just for the specific case of staged add-ons that 6678 * fail to install at startup. When that happens, the add-on remains 6679 * staged but we want to keep track of it like other installs so that we 6680 * can clean it up if the same add-on is installed again (see the comment 6681 * about "pending installs for the same add-on" in AddonInstall.startInstall) 6682 */ 6683class StagedAddonInstall extends AddonInstall { 6684 constructor(installLocation, dir, manifest) { 6685 super(installLocation, dir); 6686 6687 this.name = manifest.name; 6688 this.type = manifest.type; 6689 this.version = manifest.version; 6690 this.icons = manifest.icons; 6691 this.releaseNotesURI = manifest.releaseNotesURI ? 6692 NetUtil.newURI(manifest.releaseNotesURI) : 6693 null; 6694 this.sourceURI = manifest.sourceURI ? 6695 NetUtil.newURI(manifest.sourceURI) : 6696 null; 6697 this.file = null; 6698 this.addon = manifest; 6699 6700 this.state = AddonManager.STATE_INSTALLED; 6701 } 6702} 6703 6704/** 6705 * Creates a new AddonInstall to install an add-on from a local file. 6706 * 6707 * @param file 6708 * The file to install 6709 * @param location 6710 * The location to install to 6711 * @returns Promise 6712 * A Promise that resolves with the new install object. 6713 */ 6714function createLocalInstall(file, location) { 6715 if (!location) { 6716 location = XPIProvider.installLocationsByName[KEY_APP_PROFILE]; 6717 } 6718 let url = Services.io.newFileURI(file); 6719 6720 try { 6721 let install = new LocalAddonInstall(location, url); 6722 return install.init().then(() => install); 6723 } 6724 catch (e) { 6725 logger.error("Error creating install", e); 6726 XPIProvider.removeActiveInstall(this); 6727 return Promise.resolve(null); 6728 } 6729} 6730 6731/** 6732 * Creates a new AddonInstall to download and install a URL. 6733 * 6734 * @param aCallback 6735 * The callback to pass the new AddonInstall to 6736 * @param aUri 6737 * The URI to download 6738 * @param aHash 6739 * A hash for the add-on 6740 * @param aName 6741 * A name for the add-on 6742 * @param aIcons 6743 * An icon URLs for the add-on 6744 * @param aVersion 6745 * A version for the add-on 6746 * @param aBrowser 6747 * The browser performing the install 6748 */ 6749function createDownloadInstall(aCallback, aUri, aHash, aName, aIcons, 6750 aVersion, aBrowser) { 6751 let location = XPIProvider.installLocationsByName[KEY_APP_PROFILE]; 6752 let url = NetUtil.newURI(aUri); 6753 6754 if (url instanceof Ci.nsIFileURL) { 6755 let install = new LocalAddonInstall(location, url, aHash); 6756 install.init().then(() => { aCallback(install); }); 6757 } else { 6758 let install = new DownloadAddonInstall(location, url, aHash, null, 6759 aBrowser, aName, null, aIcons, 6760 aVersion); 6761 aCallback(install); 6762 } 6763} 6764 6765/** 6766 * Creates a new AddonInstall for an update. 6767 * 6768 * @param aCallback 6769 * The callback to pass the new AddonInstall to 6770 * @param aAddon 6771 * The add-on being updated 6772 * @param aUpdate 6773 * The metadata about the new version from the update manifest 6774 */ 6775function createUpdate(aCallback, aAddon, aUpdate) { 6776 let url = NetUtil.newURI(aUpdate.updateURL); 6777 6778 Task.spawn(function*() { 6779 let install; 6780 if (url instanceof Ci.nsIFileURL) { 6781 install = new LocalAddonInstall(aAddon._installLocation, url, 6782 aUpdate.updateHash, aAddon); 6783 yield install.init(); 6784 } else { 6785 install = new DownloadAddonInstall(aAddon._installLocation, url, 6786 aUpdate.updateHash, aAddon, null, 6787 aAddon.selectedLocale.name, aAddon.type, 6788 aAddon.icons, aUpdate.version); 6789 } 6790 try { 6791 if (aUpdate.updateInfoURL) 6792 install.releaseNotesURI = NetUtil.newURI(escapeAddonURI(aAddon, aUpdate.updateInfoURL)); 6793 } 6794 catch (e) { 6795 // If the releaseNotesURI cannot be parsed then just ignore it. 6796 } 6797 6798 aCallback(install); 6799 }); 6800} 6801 6802// This map is shared between AddonInstallWrapper and AddonWrapper 6803const wrapperMap = new WeakMap(); 6804let installFor = wrapper => wrapperMap.get(wrapper); 6805let addonFor = installFor; 6806 6807/** 6808 * Creates a wrapper for an AddonInstall that only exposes the public API 6809 * 6810 * @param install 6811 * The AddonInstall to create a wrapper for 6812 */ 6813function AddonInstallWrapper(aInstall) { 6814 wrapperMap.set(this, aInstall); 6815} 6816 6817AddonInstallWrapper.prototype = { 6818 get __AddonInstallInternal__() { 6819 return AppConstants.DEBUG ? installFor(this) : undefined; 6820 }, 6821 6822 get type() { 6823 return getExternalType(installFor(this).type); 6824 }, 6825 6826 get iconURL() { 6827 return installFor(this).icons[32]; 6828 }, 6829 6830 get existingAddon() { 6831 let install = installFor(this); 6832 return install.existingAddon ? install.existingAddon.wrapper : null; 6833 }, 6834 6835 get addon() { 6836 let install = installFor(this); 6837 return install.addon ? install.addon.wrapper : null; 6838 }, 6839 6840 get sourceURI() { 6841 return installFor(this).sourceURI; 6842 }, 6843 6844 get linkedInstalls() { 6845 let install = installFor(this); 6846 if (!install.linkedInstalls) 6847 return null; 6848 return install.linkedInstalls.map(i => i.wrapper); 6849 }, 6850 6851 install: function() { 6852 installFor(this).install(); 6853 }, 6854 6855 cancel: function() { 6856 installFor(this).cancel(); 6857 }, 6858 6859 addListener: function(listener) { 6860 installFor(this).addListener(listener); 6861 }, 6862 6863 removeListener: function(listener) { 6864 installFor(this).removeListener(listener); 6865 }, 6866}; 6867 6868["name", "version", "icons", "releaseNotesURI", "file", "state", "error", 6869 "progress", "maxProgress", "certificate", "certName"].forEach(function(aProp) { 6870 Object.defineProperty(AddonInstallWrapper.prototype, aProp, { 6871 get: function() { 6872 return installFor(this)[aProp]; 6873 }, 6874 enumerable: true, 6875 }); 6876}); 6877 6878/** 6879 * Creates a new update checker. 6880 * 6881 * @param aAddon 6882 * The add-on to check for updates 6883 * @param aListener 6884 * An UpdateListener to notify of updates 6885 * @param aReason 6886 * The reason for the update check 6887 * @param aAppVersion 6888 * An optional application version to check for updates for 6889 * @param aPlatformVersion 6890 * An optional platform version to check for updates for 6891 * @throws if the aListener or aReason arguments are not valid 6892 */ 6893function UpdateChecker(aAddon, aListener, aReason, aAppVersion, aPlatformVersion) { 6894 if (!aListener || !aReason) 6895 throw Cr.NS_ERROR_INVALID_ARG; 6896 6897 Components.utils.import("resource://gre/modules/addons/AddonUpdateChecker.jsm"); 6898 6899 this.addon = aAddon; 6900 aAddon._updateCheck = this; 6901 XPIProvider.doing(this); 6902 this.listener = aListener; 6903 this.appVersion = aAppVersion; 6904 this.platformVersion = aPlatformVersion; 6905 this.syncCompatibility = (aReason == AddonManager.UPDATE_WHEN_NEW_APP_INSTALLED); 6906 6907 let updateURL = aAddon.updateURL; 6908 if (!updateURL) { 6909 if (aReason == AddonManager.UPDATE_WHEN_PERIODIC_UPDATE && 6910 Services.prefs.getPrefType(PREF_EM_UPDATE_BACKGROUND_URL) == Services.prefs.PREF_STRING) { 6911 updateURL = Services.prefs.getCharPref(PREF_EM_UPDATE_BACKGROUND_URL); 6912 } else { 6913 updateURL = Services.prefs.getCharPref(PREF_EM_UPDATE_URL); 6914 } 6915 } 6916 6917 const UPDATE_TYPE_COMPATIBILITY = 32; 6918 const UPDATE_TYPE_NEWVERSION = 64; 6919 6920 aReason |= UPDATE_TYPE_COMPATIBILITY; 6921 if ("onUpdateAvailable" in this.listener) 6922 aReason |= UPDATE_TYPE_NEWVERSION; 6923 6924 let url = escapeAddonURI(aAddon, updateURL, aReason, aAppVersion); 6925 this._parser = AddonUpdateChecker.checkForUpdates(aAddon.id, aAddon.updateKey, 6926 url, this); 6927} 6928 6929UpdateChecker.prototype = { 6930 addon: null, 6931 listener: null, 6932 appVersion: null, 6933 platformVersion: null, 6934 syncCompatibility: null, 6935 6936 /** 6937 * Calls a method on the listener passing any number of arguments and 6938 * consuming any exceptions. 6939 * 6940 * @param aMethod 6941 * The method to call on the listener 6942 */ 6943 callListener: function(aMethod, ...aArgs) { 6944 if (!(aMethod in this.listener)) 6945 return; 6946 6947 try { 6948 this.listener[aMethod].apply(this.listener, aArgs); 6949 } 6950 catch (e) { 6951 logger.warn("Exception calling UpdateListener method " + aMethod, e); 6952 } 6953 }, 6954 6955 /** 6956 * Called when AddonUpdateChecker completes the update check 6957 * 6958 * @param updates 6959 * The list of update details for the add-on 6960 */ 6961 onUpdateCheckComplete: function(aUpdates) { 6962 XPIProvider.done(this.addon._updateCheck); 6963 this.addon._updateCheck = null; 6964 let AUC = AddonUpdateChecker; 6965 6966 let ignoreMaxVersion = false; 6967 let ignoreStrictCompat = false; 6968 if (!AddonManager.checkCompatibility) { 6969 ignoreMaxVersion = true; 6970 ignoreStrictCompat = true; 6971 } else if (this.addon.type in COMPATIBLE_BY_DEFAULT_TYPES && 6972 !AddonManager.strictCompatibility && 6973 !this.addon.strictCompatibility && 6974 !this.addon.hasBinaryComponents) { 6975 ignoreMaxVersion = true; 6976 } 6977 6978 // Always apply any compatibility update for the current version 6979 let compatUpdate = AUC.getCompatibilityUpdate(aUpdates, this.addon.version, 6980 this.syncCompatibility, 6981 null, null, 6982 ignoreMaxVersion, 6983 ignoreStrictCompat); 6984 // Apply the compatibility update to the database 6985 if (compatUpdate) 6986 this.addon.applyCompatibilityUpdate(compatUpdate, this.syncCompatibility); 6987 6988 // If the request is for an application or platform version that is 6989 // different to the current application or platform version then look for a 6990 // compatibility update for those versions. 6991 if ((this.appVersion && 6992 Services.vc.compare(this.appVersion, Services.appinfo.version) != 0) || 6993 (this.platformVersion && 6994 Services.vc.compare(this.platformVersion, Services.appinfo.platformVersion) != 0)) { 6995 compatUpdate = AUC.getCompatibilityUpdate(aUpdates, this.addon.version, 6996 false, this.appVersion, 6997 this.platformVersion, 6998 ignoreMaxVersion, 6999 ignoreStrictCompat); 7000 } 7001 7002 if (compatUpdate) 7003 this.callListener("onCompatibilityUpdateAvailable", this.addon.wrapper); 7004 else 7005 this.callListener("onNoCompatibilityUpdateAvailable", this.addon.wrapper); 7006 7007 function sendUpdateAvailableMessages(aSelf, aInstall) { 7008 if (aInstall) { 7009 aSelf.callListener("onUpdateAvailable", aSelf.addon.wrapper, 7010 aInstall.wrapper); 7011 } 7012 else { 7013 aSelf.callListener("onNoUpdateAvailable", aSelf.addon.wrapper); 7014 } 7015 aSelf.callListener("onUpdateFinished", aSelf.addon.wrapper, 7016 AddonManager.UPDATE_STATUS_NO_ERROR); 7017 } 7018 7019 let compatOverrides = AddonManager.strictCompatibility ? 7020 null : 7021 this.addon.compatibilityOverrides; 7022 7023 let update = AUC.getNewestCompatibleUpdate(aUpdates, 7024 this.appVersion, 7025 this.platformVersion, 7026 ignoreMaxVersion, 7027 ignoreStrictCompat, 7028 compatOverrides); 7029 7030 if (update && Services.vc.compare(this.addon.version, update.version) < 0 7031 && !this.addon._installLocation.locked) { 7032 for (let currentInstall of XPIProvider.installs) { 7033 // Skip installs that don't match the available update 7034 if (currentInstall.existingAddon != this.addon || 7035 currentInstall.version != update.version) 7036 continue; 7037 7038 // If the existing install has not yet started downloading then send an 7039 // available update notification. If it is already downloading then 7040 // don't send any available update notification 7041 if (currentInstall.state == AddonManager.STATE_AVAILABLE) { 7042 logger.debug("Found an existing AddonInstall for " + this.addon.id); 7043 sendUpdateAvailableMessages(this, currentInstall); 7044 } 7045 else 7046 sendUpdateAvailableMessages(this, null); 7047 return; 7048 } 7049 7050 createUpdate(aInstall => { 7051 sendUpdateAvailableMessages(this, aInstall); 7052 }, this.addon, update); 7053 } 7054 else { 7055 sendUpdateAvailableMessages(this, null); 7056 } 7057 }, 7058 7059 /** 7060 * Called when AddonUpdateChecker fails the update check 7061 * 7062 * @param aError 7063 * An error status 7064 */ 7065 onUpdateCheckError: function(aError) { 7066 XPIProvider.done(this.addon._updateCheck); 7067 this.addon._updateCheck = null; 7068 this.callListener("onNoCompatibilityUpdateAvailable", this.addon.wrapper); 7069 this.callListener("onNoUpdateAvailable", this.addon.wrapper); 7070 this.callListener("onUpdateFinished", this.addon.wrapper, aError); 7071 }, 7072 7073 /** 7074 * Called to cancel an in-progress update check 7075 */ 7076 cancel: function() { 7077 let parser = this._parser; 7078 if (parser) { 7079 this._parser = null; 7080 // This will call back to onUpdateCheckError with a CANCELLED error 7081 parser.cancel(); 7082 } 7083 } 7084}; 7085 7086/** 7087 * The AddonInternal is an internal only representation of add-ons. It may 7088 * have come from the database (see DBAddonInternal in XPIProviderUtils.jsm) 7089 * or an install manifest. 7090 */ 7091function AddonInternal() { 7092 this._hasResourceCache = new Map(); 7093 7094 XPCOMUtils.defineLazyGetter(this, "wrapper", () => { 7095 return new AddonWrapper(this); 7096 }); 7097} 7098 7099AddonInternal.prototype = { 7100 _selectedLocale: null, 7101 _hasResourceCache: null, 7102 active: false, 7103 visible: false, 7104 userDisabled: false, 7105 appDisabled: false, 7106 softDisabled: false, 7107 sourceURI: null, 7108 releaseNotesURI: null, 7109 foreignInstall: false, 7110 seen: true, 7111 skinnable: false, 7112 7113 /** 7114 * @property {Array<string>} dependencies 7115 * An array of bootstrapped add-on IDs on which this add-on depends. 7116 * The add-on will remain appDisabled if any of the dependent 7117 * add-ons is not installed and enabled. 7118 */ 7119 dependencies: Object.freeze([]), 7120 hasEmbeddedWebExtension: false, 7121 7122 get selectedLocale() { 7123 if (this._selectedLocale) 7124 return this._selectedLocale; 7125 let locale = Locale.findClosestLocale(this.locales); 7126 this._selectedLocale = locale ? locale : this.defaultLocale; 7127 return this._selectedLocale; 7128 }, 7129 7130 get providesUpdatesSecurely() { 7131 return !!(this.updateKey || !this.updateURL || 7132 this.updateURL.substring(0, 6) == "https:"); 7133 }, 7134 7135 get isCorrectlySigned() { 7136 switch (this._installLocation.name) { 7137 case KEY_APP_SYSTEM_ADDONS: 7138 // System add-ons must be signed by the system key. 7139 return this.signedState == AddonManager.SIGNEDSTATE_SYSTEM 7140 7141 case KEY_APP_SYSTEM_DEFAULTS: 7142 case KEY_APP_TEMPORARY: 7143 // Temporary and built-in system add-ons do not require signing. 7144 return true; 7145 7146 case KEY_APP_SYSTEM_SHARE: 7147 case KEY_APP_SYSTEM_LOCAL: 7148 // On UNIX platforms except OSX, an additional location for system 7149 // add-ons exists in /usr/{lib,share}/mozilla/extensions. Add-ons 7150 // installed there do not require signing. 7151 if (Services.appinfo.OS != "Darwin") 7152 return true; 7153 break; 7154 } 7155 7156 if (this.signedState === AddonManager.SIGNEDSTATE_NOT_REQUIRED) 7157 return true; 7158 return this.signedState > AddonManager.SIGNEDSTATE_MISSING; 7159 }, 7160 7161 get isCompatible() { 7162 return this.isCompatibleWith(); 7163 }, 7164 7165 get disabled() { 7166 return (this.userDisabled || this.appDisabled || this.softDisabled); 7167 }, 7168 7169 get isPlatformCompatible() { 7170 if (this.targetPlatforms.length == 0) 7171 return true; 7172 7173 let matchedOS = false; 7174 7175 // If any targetPlatform matches the OS and contains an ABI then we will 7176 // only match a targetPlatform that contains both the current OS and ABI 7177 let needsABI = false; 7178 7179 // Some platforms do not specify an ABI, test against null in that case. 7180 let abi = null; 7181 try { 7182 abi = Services.appinfo.XPCOMABI; 7183 } 7184 catch (e) { } 7185 7186 // Something is causing errors in here 7187 try { 7188 for (let platform of this.targetPlatforms) { 7189 if (platform.os == "Linux" || platform.os == Services.appinfo.OS) { 7190 if (platform.abi) { 7191 needsABI = true; 7192 if (platform.abi === abi) 7193 return true; 7194 } 7195 else { 7196 matchedOS = true; 7197 } 7198 } 7199 } 7200 } catch (e) { 7201 let message = "Problem with addon " + this.id + " targetPlatforms " 7202 + JSON.stringify(this.targetPlatforms); 7203 logger.error(message, e); 7204 AddonManagerPrivate.recordException("XPI", message, e); 7205 // don't trust this add-on 7206 return false; 7207 } 7208 7209 return matchedOS && !needsABI; 7210 }, 7211 7212 isCompatibleWith: function(aAppVersion, aPlatformVersion) { 7213 let app = this.matchingTargetApplication; 7214 if (!app) 7215 return false; 7216 7217 // set reasonable defaults for minVersion and maxVersion 7218 let minVersion = app.minVersion || "0"; 7219 let maxVersion = app.maxVersion || "*"; 7220 7221 if (!aAppVersion) 7222 aAppVersion = Services.appinfo.version; 7223 if (!aPlatformVersion) 7224 aPlatformVersion = Services.appinfo.platformVersion; 7225 7226 let version; 7227 if (app.id == Services.appinfo.ID) 7228 version = aAppVersion; 7229 else if (app.id == TOOLKIT_ID) 7230 version = aPlatformVersion 7231 7232 // Only extensions and dictionaries can be compatible by default; themes 7233 // and language packs always use strict compatibility checking. 7234 if (this.type in COMPATIBLE_BY_DEFAULT_TYPES && 7235 !AddonManager.strictCompatibility && !this.strictCompatibility && 7236 !this.hasBinaryComponents) { 7237 7238 // The repository can specify compatibility overrides. 7239 // Note: For now, only blacklisting is supported by overrides. 7240 if (this._repositoryAddon && 7241 this._repositoryAddon.compatibilityOverrides) { 7242 let overrides = this._repositoryAddon.compatibilityOverrides; 7243 let override = AddonRepository.findMatchingCompatOverride(this.version, 7244 overrides); 7245 if (override && override.type == "incompatible") 7246 return false; 7247 } 7248 7249 // Extremely old extensions should not be compatible by default. 7250 let minCompatVersion; 7251 if (app.id == Services.appinfo.ID) 7252 minCompatVersion = XPIProvider.minCompatibleAppVersion; 7253 else if (app.id == TOOLKIT_ID) 7254 minCompatVersion = XPIProvider.minCompatiblePlatformVersion; 7255 7256 if (minCompatVersion && 7257 Services.vc.compare(minCompatVersion, maxVersion) > 0) 7258 return false; 7259 7260 return Services.vc.compare(version, minVersion) >= 0; 7261 } 7262 7263 return (Services.vc.compare(version, minVersion) >= 0) && 7264 (Services.vc.compare(version, maxVersion) <= 0) 7265 }, 7266 7267 get matchingTargetApplication() { 7268 let app = null; 7269 for (let targetApp of this.targetApplications) { 7270 if (targetApp.id == Services.appinfo.ID) 7271 return targetApp; 7272 if (targetApp.id == TOOLKIT_ID) 7273 app = targetApp; 7274 } 7275 return app; 7276 }, 7277 7278 get blocklistState() { 7279 let staticItem = findMatchingStaticBlocklistItem(this); 7280 if (staticItem) 7281 return staticItem.level; 7282 7283 return Blocklist.getAddonBlocklistState(this.wrapper); 7284 }, 7285 7286 get blocklistURL() { 7287 let staticItem = findMatchingStaticBlocklistItem(this); 7288 if (staticItem) { 7289 let url = Services.urlFormatter.formatURLPref("extensions.blocklist.itemURL"); 7290 return url.replace(/%blockID%/g, staticItem.blockID); 7291 } 7292 7293 return Blocklist.getAddonBlocklistURL(this.wrapper); 7294 }, 7295 7296 applyCompatibilityUpdate: function(aUpdate, aSyncCompatibility) { 7297 for (let targetApp of this.targetApplications) { 7298 for (let updateTarget of aUpdate.targetApplications) { 7299 if (targetApp.id == updateTarget.id && (aSyncCompatibility || 7300 Services.vc.compare(targetApp.maxVersion, updateTarget.maxVersion) < 0)) { 7301 targetApp.minVersion = updateTarget.minVersion; 7302 targetApp.maxVersion = updateTarget.maxVersion; 7303 } 7304 } 7305 } 7306 if (aUpdate.multiprocessCompatible !== undefined) 7307 this.multiprocessCompatible = aUpdate.multiprocessCompatible; 7308 this.appDisabled = !isUsableAddon(this); 7309 }, 7310 7311 /** 7312 * getDataDirectory tries to execute the callback with two arguments: 7313 * 1) the path of the data directory within the profile, 7314 * 2) any exception generated from trying to build it. 7315 */ 7316 getDataDirectory: function(callback) { 7317 let parentPath = OS.Path.join(OS.Constants.Path.profileDir, "extension-data"); 7318 let dirPath = OS.Path.join(parentPath, this.id); 7319 7320 Task.spawn(function*() { 7321 yield OS.File.makeDir(parentPath, {ignoreExisting: true}); 7322 yield OS.File.makeDir(dirPath, {ignoreExisting: true}); 7323 }).then(() => callback(dirPath, null), 7324 e => callback(dirPath, e)); 7325 }, 7326 7327 /** 7328 * toJSON is called by JSON.stringify in order to create a filtered version 7329 * of this object to be serialized to a JSON file. A new object is returned 7330 * with copies of all non-private properties. Functions, getters and setters 7331 * are not copied. 7332 * 7333 * @param aKey 7334 * The key that this object is being serialized as in the JSON. 7335 * Unused here since this is always the main object serialized 7336 * 7337 * @return an object containing copies of the properties of this object 7338 * ignoring private properties, functions, getters and setters 7339 */ 7340 toJSON: function(aKey) { 7341 let obj = {}; 7342 for (let prop in this) { 7343 // Ignore the wrapper property 7344 if (prop == "wrapper") 7345 continue; 7346 7347 // Ignore private properties 7348 if (prop.substring(0, 1) == "_") 7349 continue; 7350 7351 // Ignore getters 7352 if (this.__lookupGetter__(prop)) 7353 continue; 7354 7355 // Ignore setters 7356 if (this.__lookupSetter__(prop)) 7357 continue; 7358 7359 // Ignore functions 7360 if (typeof this[prop] == "function") 7361 continue; 7362 7363 obj[prop] = this[prop]; 7364 } 7365 7366 return obj; 7367 }, 7368 7369 /** 7370 * When an add-on install is pending its metadata will be cached in a file. 7371 * This method reads particular properties of that metadata that may be newer 7372 * than that in the install manifest, like compatibility information. 7373 * 7374 * @param aObj 7375 * A JS object containing the cached metadata 7376 */ 7377 importMetadata: function(aObj) { 7378 for (let prop of PENDING_INSTALL_METADATA) { 7379 if (!(prop in aObj)) 7380 continue; 7381 7382 this[prop] = aObj[prop]; 7383 } 7384 7385 // Compatibility info may have changed so update appDisabled 7386 this.appDisabled = !isUsableAddon(this); 7387 }, 7388 7389 permissions: function() { 7390 let permissions = 0; 7391 7392 // Add-ons that aren't installed cannot be modified in any way 7393 if (!(this.inDatabase)) 7394 return permissions; 7395 7396 if (!this.appDisabled) { 7397 if (this.userDisabled || this.softDisabled) { 7398 permissions |= AddonManager.PERM_CAN_ENABLE; 7399 } 7400 else if (this.type != "theme") { 7401 permissions |= AddonManager.PERM_CAN_DISABLE; 7402 } 7403 } 7404 7405 // Add-ons that are in locked install locations, or are pending uninstall 7406 // cannot be upgraded or uninstalled 7407 if (!this._installLocation.locked && !this.pendingUninstall) { 7408 // Experiments cannot be upgraded. 7409 // System add-on upgrades are triggered through a different mechanism (see updateSystemAddons()) 7410 let isSystem = (this._installLocation.name == KEY_APP_SYSTEM_DEFAULTS || 7411 this._installLocation.name == KEY_APP_SYSTEM_ADDONS); 7412 // Add-ons that are installed by a file link cannot be upgraded. 7413 if (this.type != "experiment" && 7414 !this._installLocation.isLinkedAddon(this.id) && !isSystem) { 7415 permissions |= AddonManager.PERM_CAN_UPGRADE; 7416 } 7417 7418 permissions |= AddonManager.PERM_CAN_UNINSTALL; 7419 } 7420 7421 return permissions; 7422 }, 7423}; 7424 7425/** 7426 * The AddonWrapper wraps an Addon to provide the data visible to consumers of 7427 * the public API. 7428 */ 7429function AddonWrapper(aAddon) { 7430 wrapperMap.set(this, aAddon); 7431} 7432 7433AddonWrapper.prototype = { 7434 get __AddonInternal__() { 7435 return AppConstants.DEBUG ? addonFor(this) : undefined; 7436 }, 7437 7438 get seen() { 7439 return addonFor(this).seen; 7440 }, 7441 7442 get hasEmbeddedWebExtension() { 7443 return addonFor(this).hasEmbeddedWebExtension; 7444 }, 7445 7446 markAsSeen: function() { 7447 addonFor(this).seen = true; 7448 XPIDatabase.saveChanges(); 7449 }, 7450 7451 get type() { 7452 return getExternalType(addonFor(this).type); 7453 }, 7454 7455 get isWebExtension() { 7456 return addonFor(this).type == "webextension"; 7457 }, 7458 7459 get temporarilyInstalled() { 7460 return addonFor(this)._installLocation == TemporaryInstallLocation; 7461 }, 7462 7463 get aboutURL() { 7464 return this.isActive ? addonFor(this)["aboutURL"] : null; 7465 }, 7466 7467 get optionsURL() { 7468 if (!this.isActive) { 7469 return null; 7470 } 7471 7472 let addon = addonFor(this); 7473 if (addon.optionsURL) { 7474 if (this.isWebExtension || this.hasEmbeddedWebExtension) { 7475 // The internal object's optionsURL property comes from the addons 7476 // DB and should be a relative URL. However, extensions with 7477 // options pages installed before bug 1293721 was fixed got absolute 7478 // URLs in the addons db. This code handles both cases. 7479 let base = ExtensionManagement.getURLForExtension(addon.id); 7480 if (!base) { 7481 return null; 7482 } 7483 return new URL(addon.optionsURL, base).href; 7484 } 7485 return addon.optionsURL; 7486 } 7487 7488 if (this.hasResource("options.xul")) 7489 return this.getResourceURI("options.xul").spec; 7490 7491 return null; 7492 }, 7493 7494 get optionsType() { 7495 if (!this.isActive) 7496 return null; 7497 7498 let addon = addonFor(this); 7499 let hasOptionsXUL = this.hasResource("options.xul"); 7500 let hasOptionsURL = !!this.optionsURL; 7501 7502 if (addon.optionsType) { 7503 switch (parseInt(addon.optionsType, 10)) { 7504 case AddonManager.OPTIONS_TYPE_DIALOG: 7505 case AddonManager.OPTIONS_TYPE_TAB: 7506 return hasOptionsURL ? addon.optionsType : null; 7507 case AddonManager.OPTIONS_TYPE_INLINE: 7508 case AddonManager.OPTIONS_TYPE_INLINE_INFO: 7509 case AddonManager.OPTIONS_TYPE_INLINE_BROWSER: 7510 return (hasOptionsXUL || hasOptionsURL) ? addon.optionsType : null; 7511 } 7512 return null; 7513 } 7514 7515 if (hasOptionsXUL) 7516 return AddonManager.OPTIONS_TYPE_INLINE; 7517 7518 if (hasOptionsURL) 7519 return AddonManager.OPTIONS_TYPE_DIALOG; 7520 7521 return null; 7522 }, 7523 7524 get iconURL() { 7525 return AddonManager.getPreferredIconURL(this, 48); 7526 }, 7527 7528 get icon64URL() { 7529 return AddonManager.getPreferredIconURL(this, 64); 7530 }, 7531 7532 get icons() { 7533 let addon = addonFor(this); 7534 let icons = {}; 7535 7536 if (addon._repositoryAddon) { 7537 for (let size in addon._repositoryAddon.icons) { 7538 icons[size] = addon._repositoryAddon.icons[size]; 7539 } 7540 } 7541 7542 if (addon.icons) { 7543 for (let size in addon.icons) { 7544 icons[size] = this.getResourceURI(addon.icons[size]).spec; 7545 } 7546 } else { 7547 // legacy add-on that did not update its icon data yet 7548 if (this.hasResource("icon.png")) { 7549 icons[32] = icons[48] = this.getResourceURI("icon.png").spec; 7550 } 7551 if (this.hasResource("icon64.png")) { 7552 icons[64] = this.getResourceURI("icon64.png").spec; 7553 } 7554 } 7555 7556 if (this.isActive && addon.iconURL) { 7557 icons[32] = addon.iconURL; 7558 icons[48] = addon.iconURL; 7559 } 7560 7561 if (this.isActive && addon.icon64URL) { 7562 icons[64] = addon.icon64URL; 7563 } 7564 7565 Object.freeze(icons); 7566 return icons; 7567 }, 7568 7569 get screenshots() { 7570 let addon = addonFor(this); 7571 let repositoryAddon = addon._repositoryAddon; 7572 if (repositoryAddon && ("screenshots" in repositoryAddon)) { 7573 let repositoryScreenshots = repositoryAddon.screenshots; 7574 if (repositoryScreenshots && repositoryScreenshots.length > 0) 7575 return repositoryScreenshots; 7576 } 7577 7578 if (addon.type == "theme" && this.hasResource("preview.png")) { 7579 let url = this.getResourceURI("preview.png").spec; 7580 return [new AddonManagerPrivate.AddonScreenshot(url)]; 7581 } 7582 7583 return null; 7584 }, 7585 7586 get applyBackgroundUpdates() { 7587 return addonFor(this).applyBackgroundUpdates; 7588 }, 7589 set applyBackgroundUpdates(val) { 7590 let addon = addonFor(this); 7591 if (this.type == "experiment") { 7592 logger.warn("Setting applyBackgroundUpdates on an experiment is not supported."); 7593 return addon.applyBackgroundUpdates; 7594 } 7595 7596 if (val != AddonManager.AUTOUPDATE_DEFAULT && 7597 val != AddonManager.AUTOUPDATE_DISABLE && 7598 val != AddonManager.AUTOUPDATE_ENABLE) { 7599 val = val ? AddonManager.AUTOUPDATE_DEFAULT : 7600 AddonManager.AUTOUPDATE_DISABLE; 7601 } 7602 7603 if (val == addon.applyBackgroundUpdates) 7604 return val; 7605 7606 XPIDatabase.setAddonProperties(addon, { 7607 applyBackgroundUpdates: val 7608 }); 7609 AddonManagerPrivate.callAddonListeners("onPropertyChanged", this, ["applyBackgroundUpdates"]); 7610 7611 return val; 7612 }, 7613 7614 set syncGUID(val) { 7615 let addon = addonFor(this); 7616 if (addon.syncGUID == val) 7617 return val; 7618 7619 if (addon.inDatabase) 7620 XPIDatabase.setAddonSyncGUID(addon, val); 7621 7622 addon.syncGUID = val; 7623 7624 return val; 7625 }, 7626 7627 get install() { 7628 let addon = addonFor(this); 7629 if (!("_install" in addon) || !addon._install) 7630 return null; 7631 return addon._install.wrapper; 7632 }, 7633 7634 get pendingUpgrade() { 7635 let addon = addonFor(this); 7636 return addon.pendingUpgrade ? addon.pendingUpgrade.wrapper : null; 7637 }, 7638 7639 get scope() { 7640 let addon = addonFor(this); 7641 if (addon._installLocation) 7642 return addon._installLocation.scope; 7643 7644 return AddonManager.SCOPE_PROFILE; 7645 }, 7646 7647 get pendingOperations() { 7648 let addon = addonFor(this); 7649 let pending = 0; 7650 if (!(addon.inDatabase)) { 7651 // Add-on is pending install if there is no associated install (shouldn't 7652 // happen here) or if the install is in the process of or has successfully 7653 // completed the install. If an add-on is pending install then we ignore 7654 // any other pending operations. 7655 if (!addon._install || addon._install.state == AddonManager.STATE_INSTALLING || 7656 addon._install.state == AddonManager.STATE_INSTALLED) 7657 return AddonManager.PENDING_INSTALL; 7658 } 7659 else if (addon.pendingUninstall) { 7660 // If an add-on is pending uninstall then we ignore any other pending 7661 // operations 7662 return AddonManager.PENDING_UNINSTALL; 7663 } 7664 7665 if (addon.active && addon.disabled) 7666 pending |= AddonManager.PENDING_DISABLE; 7667 else if (!addon.active && !addon.disabled) 7668 pending |= AddonManager.PENDING_ENABLE; 7669 7670 if (addon.pendingUpgrade) 7671 pending |= AddonManager.PENDING_UPGRADE; 7672 7673 return pending; 7674 }, 7675 7676 get operationsRequiringRestart() { 7677 let addon = addonFor(this); 7678 let ops = 0; 7679 if (XPIProvider.installRequiresRestart(addon)) 7680 ops |= AddonManager.OP_NEEDS_RESTART_INSTALL; 7681 if (XPIProvider.uninstallRequiresRestart(addon)) 7682 ops |= AddonManager.OP_NEEDS_RESTART_UNINSTALL; 7683 if (XPIProvider.enableRequiresRestart(addon)) 7684 ops |= AddonManager.OP_NEEDS_RESTART_ENABLE; 7685 if (XPIProvider.disableRequiresRestart(addon)) 7686 ops |= AddonManager.OP_NEEDS_RESTART_DISABLE; 7687 7688 return ops; 7689 }, 7690 7691 get isDebuggable() { 7692 return this.isActive && addonFor(this).bootstrap; 7693 }, 7694 7695 get permissions() { 7696 return addonFor(this).permissions(); 7697 }, 7698 7699 get isActive() { 7700 let addon = addonFor(this); 7701 if (!addon.active) 7702 return false; 7703 if (!Services.appinfo.inSafeMode) 7704 return true; 7705 return addon.bootstrap && canRunInSafeMode(addon); 7706 }, 7707 7708 get userDisabled() { 7709 let addon = addonFor(this); 7710 return addon.softDisabled || addon.userDisabled; 7711 }, 7712 set userDisabled(val) { 7713 let addon = addonFor(this); 7714 if (val == this.userDisabled) { 7715 return val; 7716 } 7717 7718 if (addon.inDatabase) { 7719 if (addon.type == "theme" && val) { 7720 if (addon.internalName == XPIProvider.defaultSkin) 7721 throw new Error("Cannot disable the default theme"); 7722 XPIProvider.enableDefaultTheme(); 7723 } 7724 else { 7725 // hidden and system add-ons should not be user disasbled, 7726 // as there is no UI to re-enable them. 7727 if (this.hidden) { 7728 throw new Error(`Cannot disable hidden add-on ${addon.id}`); 7729 } 7730 XPIProvider.updateAddonDisabledState(addon, val); 7731 } 7732 } 7733 else { 7734 addon.userDisabled = val; 7735 // When enabling remove the softDisabled flag 7736 if (!val) 7737 addon.softDisabled = false; 7738 } 7739 7740 return val; 7741 }, 7742 7743 set softDisabled(val) { 7744 let addon = addonFor(this); 7745 if (val == addon.softDisabled) 7746 return val; 7747 7748 if (addon.inDatabase) { 7749 // When softDisabling a theme just enable the active theme 7750 if (addon.type == "theme" && val && !addon.userDisabled) { 7751 if (addon.internalName == XPIProvider.defaultSkin) 7752 throw new Error("Cannot disable the default theme"); 7753 XPIProvider.enableDefaultTheme(); 7754 } 7755 else { 7756 XPIProvider.updateAddonDisabledState(addon, undefined, val); 7757 } 7758 } 7759 else if (!addon.userDisabled) { 7760 // Only set softDisabled if not already disabled 7761 addon.softDisabled = val; 7762 } 7763 7764 return val; 7765 }, 7766 7767 get hidden() { 7768 let addon = addonFor(this); 7769 if (addon._installLocation.name == KEY_APP_TEMPORARY) 7770 return false; 7771 7772 return (addon._installLocation.name == KEY_APP_SYSTEM_DEFAULTS || 7773 addon._installLocation.name == KEY_APP_SYSTEM_ADDONS); 7774 }, 7775 7776 get isSystem() { 7777 let addon = addonFor(this); 7778 return (addon._installLocation.name == KEY_APP_SYSTEM_DEFAULTS || 7779 addon._installLocation.name == KEY_APP_SYSTEM_ADDONS); 7780 }, 7781 7782 // Returns true if Firefox Sync should sync this addon. Only non-hotfixes 7783 // directly in the profile are considered syncable. 7784 get isSyncable() { 7785 let addon = addonFor(this); 7786 let hotfixID = Preferences.get(PREF_EM_HOTFIX_ID, undefined); 7787 if (hotfixID && hotfixID == addon.id) { 7788 return false; 7789 } 7790 return (addon._installLocation.name == KEY_APP_PROFILE); 7791 }, 7792 7793 isCompatibleWith: function(aAppVersion, aPlatformVersion) { 7794 return addonFor(this).isCompatibleWith(aAppVersion, aPlatformVersion); 7795 }, 7796 7797 uninstall: function(alwaysAllowUndo) { 7798 let addon = addonFor(this); 7799 XPIProvider.uninstallAddon(addon, alwaysAllowUndo); 7800 }, 7801 7802 cancelUninstall: function() { 7803 let addon = addonFor(this); 7804 XPIProvider.cancelUninstallAddon(addon); 7805 }, 7806 7807 findUpdates: function(aListener, aReason, aAppVersion, aPlatformVersion) { 7808 // Short-circuit updates for experiments because updates are handled 7809 // through the Experiments Manager. 7810 if (this.type == "experiment") { 7811 AddonManagerPrivate.callNoUpdateListeners(this, aListener, aReason, 7812 aAppVersion, aPlatformVersion); 7813 return; 7814 } 7815 7816 new UpdateChecker(addonFor(this), aListener, aReason, aAppVersion, aPlatformVersion); 7817 }, 7818 7819 // Returns true if there was an update in progress, false if there was no update to cancel 7820 cancelUpdate: function() { 7821 let addon = addonFor(this); 7822 if (addon._updateCheck) { 7823 addon._updateCheck.cancel(); 7824 return true; 7825 } 7826 return false; 7827 }, 7828 7829 hasResource: function(aPath) { 7830 let addon = addonFor(this); 7831 if (addon._hasResourceCache.has(aPath)) 7832 return addon._hasResourceCache.get(aPath); 7833 7834 let bundle = addon._sourceBundle.clone(); 7835 7836 // Bundle may not exist any more if the addon has just been uninstalled, 7837 // but explicitly first checking .exists() results in unneeded file I/O. 7838 try { 7839 var isDir = bundle.isDirectory(); 7840 } catch (e) { 7841 addon._hasResourceCache.set(aPath, false); 7842 return false; 7843 } 7844 7845 if (isDir) { 7846 if (aPath) 7847 aPath.split("/").forEach(part => bundle.append(part)); 7848 let result = bundle.exists(); 7849 addon._hasResourceCache.set(aPath, result); 7850 return result; 7851 } 7852 7853 let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"]. 7854 createInstance(Ci.nsIZipReader); 7855 try { 7856 zipReader.open(bundle); 7857 let result = zipReader.hasEntry(aPath); 7858 addon._hasResourceCache.set(aPath, result); 7859 return result; 7860 } 7861 catch (e) { 7862 addon._hasResourceCache.set(aPath, false); 7863 return false; 7864 } 7865 finally { 7866 zipReader.close(); 7867 } 7868 }, 7869 7870 /** 7871 * Reloads the add-on. 7872 * 7873 * For temporarily installed add-ons, this uninstalls and re-installs the 7874 * add-on. Otherwise, the addon is disabled and then re-enabled, and the cache 7875 * is flushed. 7876 * 7877 * @return Promise 7878 */ 7879 reload: function() { 7880 return new Promise((resolve) => { 7881 const addon = addonFor(this); 7882 7883 logger.debug(`reloading add-on ${addon.id}`); 7884 7885 if (!this.temporarilyInstalled) { 7886 let addonFile = addon.getResourceURI; 7887 XPIProvider.updateAddonDisabledState(addon, true); 7888 Services.obs.notifyObservers(addonFile, "flush-cache-entry", null); 7889 XPIProvider.updateAddonDisabledState(addon, false) 7890 resolve(); 7891 } else { 7892 // This function supports re-installing an existing add-on. 7893 resolve(AddonManager.installTemporaryAddon(addon._sourceBundle)); 7894 } 7895 }); 7896 }, 7897 7898 /** 7899 * Returns a URI to the selected resource or to the add-on bundle if aPath 7900 * is null. URIs to the bundle will always be file: URIs. URIs to resources 7901 * will be file: URIs if the add-on is unpacked or jar: URIs if the add-on is 7902 * still an XPI file. 7903 * 7904 * @param aPath 7905 * The path in the add-on to get the URI for or null to get a URI to 7906 * the file or directory the add-on is installed as. 7907 * @return an nsIURI 7908 */ 7909 getResourceURI: function(aPath) { 7910 let addon = addonFor(this); 7911 if (!aPath) 7912 return NetUtil.newURI(addon._sourceBundle); 7913 7914 return getURIForResourceInFile(addon._sourceBundle, aPath); 7915 } 7916}; 7917 7918/** 7919 * The PrivateWrapper is used to expose certain functionality only when being 7920 * called with the add-on instanceID, disallowing other add-ons to access it. 7921 */ 7922function PrivateWrapper(aAddon) { 7923 AddonWrapper.call(this, aAddon); 7924} 7925 7926PrivateWrapper.prototype = Object.create(AddonWrapper.prototype); 7927Object.assign(PrivateWrapper.prototype, { 7928 addonId() { 7929 return this.id; 7930 }, 7931 7932 /** 7933 * Retrieves the preferred global context to be used from the 7934 * add-on debugging window. 7935 * 7936 * @returns global 7937 * The object set as global context. Must be a window object. 7938 */ 7939 getDebugGlobal(global) { 7940 let activeAddon = XPIProvider.activeAddons.get(this.id); 7941 if (activeAddon) { 7942 return activeAddon.debugGlobal; 7943 } 7944 7945 return null; 7946 }, 7947 7948 /** 7949 * Defines a global context to be used in the console 7950 * of the add-on debugging window. 7951 * 7952 * @param global 7953 * The object to set as global context. Must be a window object. 7954 */ 7955 setDebugGlobal(global) { 7956 if (!global) { 7957 // If the new global is null, notify the listeners regardless 7958 // from the current state of the addon. 7959 // NOTE: this happen after the addon has been disabled and 7960 // the global will never be set to null otherwise. 7961 AddonManagerPrivate.callAddonListeners("onPropertyChanged", 7962 addonFor(this), 7963 ["debugGlobal"]); 7964 } else { 7965 let activeAddon = XPIProvider.activeAddons.get(this.id); 7966 if (activeAddon) { 7967 let globalChanged = activeAddon.debugGlobal != global; 7968 activeAddon.debugGlobal = global; 7969 7970 if (globalChanged) { 7971 AddonManagerPrivate.callAddonListeners("onPropertyChanged", 7972 addonFor(this), 7973 ["debugGlobal"]); 7974 } 7975 } 7976 } 7977 } 7978}); 7979 7980function chooseValue(aAddon, aObj, aProp) { 7981 let repositoryAddon = aAddon._repositoryAddon; 7982 let objValue = aObj[aProp]; 7983 7984 if (repositoryAddon && (aProp in repositoryAddon) && 7985 (objValue === undefined || objValue === null)) { 7986 return [repositoryAddon[aProp], true]; 7987 } 7988 7989 return [objValue, false]; 7990} 7991 7992function defineAddonWrapperProperty(name, getter) { 7993 Object.defineProperty(AddonWrapper.prototype, name, { 7994 get: getter, 7995 enumerable: true, 7996 }); 7997} 7998 7999["id", "syncGUID", "version", "isCompatible", "isPlatformCompatible", 8000 "providesUpdatesSecurely", "blocklistState", "blocklistURL", "appDisabled", 8001 "softDisabled", "skinnable", "size", "foreignInstall", "hasBinaryComponents", 8002 "strictCompatibility", "compatibilityOverrides", "updateURL", "dependencies", 8003 "getDataDirectory", "multiprocessCompatible", "signedState", "mpcOptedOut", 8004 "isCorrectlySigned"].forEach(function(aProp) { 8005 defineAddonWrapperProperty(aProp, function() { 8006 let addon = addonFor(this); 8007 return (aProp in addon) ? addon[aProp] : undefined; 8008 }); 8009}); 8010 8011["fullDescription", "developerComments", "eula", "supportURL", 8012 "contributionURL", "contributionAmount", "averageRating", "reviewCount", 8013 "reviewURL", "totalDownloads", "weeklyDownloads", "dailyUsers", 8014 "repositoryStatus"].forEach(function(aProp) { 8015 defineAddonWrapperProperty(aProp, function() { 8016 let addon = addonFor(this); 8017 if (addon._repositoryAddon) 8018 return addon._repositoryAddon[aProp]; 8019 8020 return null; 8021 }); 8022}); 8023 8024["installDate", "updateDate"].forEach(function(aProp) { 8025 defineAddonWrapperProperty(aProp, function() { 8026 return new Date(addonFor(this)[aProp]); 8027 }); 8028}); 8029 8030["sourceURI", "releaseNotesURI"].forEach(function(aProp) { 8031 defineAddonWrapperProperty(aProp, function() { 8032 let addon = addonFor(this); 8033 8034 // Temporary Installed Addons do not have a "sourceURI", 8035 // But we can use the "_sourceBundle" as an alternative, 8036 // which points to the path of the addon xpi installed 8037 // or its source dir (if it has been installed from a 8038 // directory). 8039 if (aProp == "sourceURI" && this.temporarilyInstalled) { 8040 return Services.io.newFileURI(addon._sourceBundle); 8041 } 8042 8043 let [target, fromRepo] = chooseValue(addon, addon, aProp); 8044 if (!target) 8045 return null; 8046 if (fromRepo) 8047 return target; 8048 return NetUtil.newURI(target); 8049 }); 8050}); 8051 8052PROP_LOCALE_SINGLE.forEach(function(aProp) { 8053 defineAddonWrapperProperty(aProp, function() { 8054 let addon = addonFor(this); 8055 // Override XPI creator if repository creator is defined 8056 if (aProp == "creator" && 8057 addon._repositoryAddon && addon._repositoryAddon.creator) { 8058 return addon._repositoryAddon.creator; 8059 } 8060 8061 let result = null; 8062 8063 if (addon.active) { 8064 try { 8065 let pref = PREF_EM_EXTENSION_FORMAT + addon.id + "." + aProp; 8066 let value = Preferences.get(pref, null, Ci.nsIPrefLocalizedString); 8067 if (value) 8068 result = value; 8069 } 8070 catch (e) { 8071 } 8072 } 8073 8074 let rest; 8075 if (result == null) 8076 [result, ...rest] = chooseValue(addon, addon.selectedLocale, aProp); 8077 8078 if (aProp == "creator") 8079 return result ? new AddonManagerPrivate.AddonAuthor(result) : null; 8080 8081 return result; 8082 }); 8083}); 8084 8085PROP_LOCALE_MULTI.forEach(function(aProp) { 8086 defineAddonWrapperProperty(aProp, function() { 8087 let addon = addonFor(this); 8088 let results = null; 8089 let usedRepository = false; 8090 8091 if (addon.active) { 8092 let pref = PREF_EM_EXTENSION_FORMAT + addon.id + "." + 8093 aProp.substring(0, aProp.length - 1); 8094 let list = Services.prefs.getChildList(pref, {}); 8095 if (list.length > 0) { 8096 list.sort(); 8097 results = []; 8098 for (let childPref of list) { 8099 let value = Preferences.get(childPref, null, Ci.nsIPrefLocalizedString); 8100 if (value) 8101 results.push(value); 8102 } 8103 } 8104 } 8105 8106 if (results == null) 8107 [results, usedRepository] = chooseValue(addon, addon.selectedLocale, aProp); 8108 8109 if (results && !usedRepository) { 8110 results = results.map(function(aResult) { 8111 return new AddonManagerPrivate.AddonAuthor(aResult); 8112 }); 8113 } 8114 8115 return results; 8116 }); 8117}); 8118 8119/** 8120 * An object which identifies a directory install location for add-ons. The 8121 * location consists of a directory which contains the add-ons installed in the 8122 * location. 8123 * 8124 * Each add-on installed in the location is either a directory containing the 8125 * add-on's files or a text file containing an absolute path to the directory 8126 * containing the add-ons files. The directory or text file must have the same 8127 * name as the add-on's ID. 8128 * 8129 * @param aName 8130 * The string identifier for the install location 8131 * @param aDirectory 8132 * The nsIFile directory for the install location 8133 * @param aScope 8134 * The scope of add-ons installed in this location 8135 */ 8136function DirectoryInstallLocation(aName, aDirectory, aScope) { 8137 this._name = aName; 8138 this.locked = true; 8139 this._directory = aDirectory; 8140 this._scope = aScope 8141 this._IDToFileMap = {}; 8142 this._linkedAddons = []; 8143 8144 if (!aDirectory || !aDirectory.exists()) 8145 return; 8146 if (!aDirectory.isDirectory()) 8147 throw new Error("Location must be a directory."); 8148 8149 this._readAddons(); 8150} 8151 8152DirectoryInstallLocation.prototype = { 8153 _name : "", 8154 _directory : null, 8155 _IDToFileMap : null, // mapping from add-on ID to nsIFile 8156 8157 /** 8158 * Reads a directory linked to in a file. 8159 * 8160 * @param file 8161 * The file containing the directory path 8162 * @return An nsIFile object representing the linked directory. 8163 */ 8164 _readDirectoryFromFile: function(aFile) { 8165 let linkedDirectory; 8166 if (aFile.isSymlink()) { 8167 linkedDirectory = aFile.clone(); 8168 try { 8169 linkedDirectory.normalize(); 8170 } catch (e) { 8171 logger.warn("Symbolic link " + aFile.path + " points to a path" + 8172 " which does not exist"); 8173 return null; 8174 } 8175 } 8176 else { 8177 let fis = Cc["@mozilla.org/network/file-input-stream;1"]. 8178 createInstance(Ci.nsIFileInputStream); 8179 fis.init(aFile, -1, -1, false); 8180 let line = { value: "" }; 8181 if (fis instanceof Ci.nsILineInputStream) 8182 fis.readLine(line); 8183 fis.close(); 8184 if (line.value) { 8185 linkedDirectory = Cc["@mozilla.org/file/local;1"]. 8186 createInstance(Ci.nsIFile); 8187 8188 try { 8189 linkedDirectory.initWithPath(line.value); 8190 } 8191 catch (e) { 8192 linkedDirectory.setRelativeDescriptor(aFile.parent, line.value); 8193 } 8194 } 8195 } 8196 8197 if (linkedDirectory) { 8198 if (!linkedDirectory.exists()) { 8199 logger.warn("File pointer " + aFile.path + " points to " + linkedDirectory.path + 8200 " which does not exist"); 8201 return null; 8202 } 8203 8204 if (!linkedDirectory.isDirectory()) { 8205 logger.warn("File pointer " + aFile.path + " points to " + linkedDirectory.path + 8206 " which is not a directory"); 8207 return null; 8208 } 8209 8210 return linkedDirectory; 8211 } 8212 8213 logger.warn("File pointer " + aFile.path + " does not contain a path"); 8214 return null; 8215 }, 8216 8217 /** 8218 * Finds all the add-ons installed in this location. 8219 */ 8220 _readAddons: function() { 8221 // Use a snapshot of the directory contents to avoid possible issues with 8222 // iterating over a directory while removing files from it (the YAFFS2 8223 // embedded filesystem has this issue, see bug 772238). 8224 let entries = getDirectoryEntries(this._directory); 8225 for (let entry of entries) { 8226 let id = entry.leafName; 8227 8228 if (id == DIR_STAGE || id == DIR_TRASH) 8229 continue; 8230 8231 let directLoad = false; 8232 if (entry.isFile() && 8233 id.substring(id.length - 4).toLowerCase() == ".xpi") { 8234 directLoad = true; 8235 id = id.substring(0, id.length - 4); 8236 } 8237 8238 if (!gIDTest.test(id)) { 8239 logger.debug("Ignoring file entry whose name is not a valid add-on ID: " + 8240 entry.path); 8241 continue; 8242 } 8243 8244 if (!directLoad && (entry.isFile() || entry.isSymlink())) { 8245 let newEntry = this._readDirectoryFromFile(entry); 8246 if (!newEntry) { 8247 logger.debug("Deleting stale pointer file " + entry.path); 8248 try { 8249 entry.remove(true); 8250 } 8251 catch (e) { 8252 logger.warn("Failed to remove stale pointer file " + entry.path, e); 8253 // Failing to remove the stale pointer file is ignorable 8254 } 8255 continue; 8256 } 8257 8258 entry = newEntry; 8259 this._linkedAddons.push(id); 8260 } 8261 8262 this._IDToFileMap[id] = entry; 8263 XPIProvider._addURIMapping(id, entry); 8264 } 8265 }, 8266 8267 /** 8268 * Gets the name of this install location. 8269 */ 8270 get name() { 8271 return this._name; 8272 }, 8273 8274 /** 8275 * Gets the scope of this install location. 8276 */ 8277 get scope() { 8278 return this._scope; 8279 }, 8280 8281 /** 8282 * Gets an array of nsIFiles for add-ons installed in this location. 8283 */ 8284 getAddonLocations: function() { 8285 let locations = new Map(); 8286 for (let id in this._IDToFileMap) { 8287 locations.set(id, this._IDToFileMap[id].clone()); 8288 } 8289 return locations; 8290 }, 8291 8292 /** 8293 * Gets the directory that the add-on with the given ID is installed in. 8294 * 8295 * @param aId 8296 * The ID of the add-on 8297 * @return The nsIFile 8298 * @throws if the ID does not match any of the add-ons installed 8299 */ 8300 getLocationForID: function(aId) { 8301 if (aId in this._IDToFileMap) 8302 return this._IDToFileMap[aId].clone(); 8303 throw new Error("Unknown add-on ID " + aId); 8304 }, 8305 8306 /** 8307 * Returns true if the given addon was installed in this location by a text 8308 * file pointing to its real path. 8309 * 8310 * @param aId 8311 * The ID of the addon 8312 */ 8313 isLinkedAddon: function(aId) { 8314 return this._linkedAddons.indexOf(aId) != -1; 8315 } 8316}; 8317 8318/** 8319 * An extension of DirectoryInstallLocation which adds methods to installing 8320 * and removing add-ons from the directory at runtime. 8321 * 8322 * @param aName 8323 * The string identifier for the install location 8324 * @param aDirectory 8325 * The nsIFile directory for the install location 8326 * @param aScope 8327 * The scope of add-ons installed in this location 8328 */ 8329function MutableDirectoryInstallLocation(aName, aDirectory, aScope) { 8330 DirectoryInstallLocation.call(this, aName, aDirectory, aScope); 8331 this.locked = false; 8332 this._stagingDirLock = 0; 8333} 8334 8335MutableDirectoryInstallLocation.prototype = Object.create(DirectoryInstallLocation.prototype); 8336Object.assign(MutableDirectoryInstallLocation.prototype, { 8337 /** 8338 * Gets the staging directory to put add-ons that are pending install and 8339 * uninstall into. 8340 * 8341 * @return an nsIFile 8342 */ 8343 getStagingDir: function() { 8344 let dir = this._directory.clone(); 8345 dir.append(DIR_STAGE); 8346 return dir; 8347 }, 8348 8349 requestStagingDir: function() { 8350 this._stagingDirLock++; 8351 8352 if (this._stagingDirPromise) 8353 return this._stagingDirPromise; 8354 8355 OS.File.makeDir(this._directory.path); 8356 let stagepath = OS.Path.join(this._directory.path, DIR_STAGE); 8357 return this._stagingDirPromise = OS.File.makeDir(stagepath).then(null, (e) => { 8358 if (e instanceof OS.File.Error && e.becauseExists) 8359 return; 8360 logger.error("Failed to create staging directory", e); 8361 throw e; 8362 }); 8363 }, 8364 8365 releaseStagingDir: function() { 8366 this._stagingDirLock--; 8367 8368 if (this._stagingDirLock == 0) { 8369 this._stagingDirPromise = null; 8370 this.cleanStagingDir(); 8371 } 8372 8373 return Promise.resolve(); 8374 }, 8375 8376 /** 8377 * Removes the specified files or directories in the staging directory and 8378 * then if the staging directory is empty attempts to remove it. 8379 * 8380 * @param aLeafNames 8381 * An array of file or directory to remove from the directory, the 8382 * array may be empty 8383 */ 8384 cleanStagingDir: function(aLeafNames = []) { 8385 let dir = this.getStagingDir(); 8386 8387 for (let name of aLeafNames) { 8388 let file = dir.clone(); 8389 file.append(name); 8390 recursiveRemove(file); 8391 } 8392 8393 if (this._stagingDirLock > 0) 8394 return; 8395 8396 let dirEntries = dir.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator); 8397 try { 8398 if (dirEntries.nextFile) 8399 return; 8400 } 8401 finally { 8402 dirEntries.close(); 8403 } 8404 8405 try { 8406 setFilePermissions(dir, FileUtils.PERMS_DIRECTORY); 8407 dir.remove(false); 8408 } 8409 catch (e) { 8410 logger.warn("Failed to remove staging dir", e); 8411 // Failing to remove the staging directory is ignorable 8412 } 8413 }, 8414 8415 /** 8416 * Returns a directory that is normally on the same filesystem as the rest of 8417 * the install location and can be used for temporarily storing files during 8418 * safe move operations. Calling this method will delete the existing trash 8419 * directory and its contents. 8420 * 8421 * @return an nsIFile 8422 */ 8423 getTrashDir: function() { 8424 let trashDir = this._directory.clone(); 8425 trashDir.append(DIR_TRASH); 8426 let trashDirExists = trashDir.exists(); 8427 try { 8428 if (trashDirExists) 8429 recursiveRemove(trashDir); 8430 trashDirExists = false; 8431 } catch (e) { 8432 logger.warn("Failed to remove trash directory", e); 8433 } 8434 if (!trashDirExists) 8435 trashDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); 8436 8437 return trashDir; 8438 }, 8439 8440 /** 8441 * Installs an add-on into the install location. 8442 * 8443 * @param id 8444 * The ID of the add-on to install 8445 * @param source 8446 * The source nsIFile to install from 8447 * @param existingAddonID 8448 * The ID of an existing add-on to uninstall at the same time 8449 * @param action 8450 * What to we do with the given source file: 8451 * "move" 8452 * Default action, the source files will be moved to the new 8453 * location, 8454 * "copy" 8455 * The source files will be copied, 8456 * "proxy" 8457 * A "proxy file" is going to refer to the source file path 8458 * @return an nsIFile indicating where the add-on was installed to 8459 */ 8460 installAddon: function({ id, source, existingAddonID, action = "move" }) { 8461 let trashDir = this.getTrashDir(); 8462 8463 let transaction = new SafeInstallOperation(); 8464 8465 let moveOldAddon = aId => { 8466 let file = this._directory.clone(); 8467 file.append(aId); 8468 8469 if (file.exists()) 8470 transaction.moveUnder(file, trashDir); 8471 8472 file = this._directory.clone(); 8473 file.append(aId + ".xpi"); 8474 if (file.exists()) { 8475 flushJarCache(file); 8476 transaction.moveUnder(file, trashDir); 8477 } 8478 } 8479 8480 // If any of these operations fails the finally block will clean up the 8481 // temporary directory 8482 try { 8483 moveOldAddon(id); 8484 if (existingAddonID && existingAddonID != id) { 8485 moveOldAddon(existingAddonID); 8486 8487 { 8488 // Move the data directories. 8489 /* XXX ajvincent We can't use OS.File: installAddon isn't compatible 8490 * with Promises, nor is SafeInstallOperation. Bug 945540 has been filed 8491 * for porting to OS.File. 8492 */ 8493 let oldDataDir = FileUtils.getDir( 8494 KEY_PROFILEDIR, ["extension-data", existingAddonID], false, true 8495 ); 8496 8497 if (oldDataDir.exists()) { 8498 let newDataDir = FileUtils.getDir( 8499 KEY_PROFILEDIR, ["extension-data", id], false, true 8500 ); 8501 if (newDataDir.exists()) { 8502 let trashData = trashDir.clone(); 8503 trashData.append("data-directory"); 8504 transaction.moveUnder(newDataDir, trashData); 8505 } 8506 8507 transaction.moveTo(oldDataDir, newDataDir); 8508 } 8509 } 8510 } 8511 8512 if (action == "copy") { 8513 transaction.copy(source, this._directory); 8514 } 8515 else if (action == "move") { 8516 if (source.isFile()) 8517 flushJarCache(source); 8518 8519 transaction.moveUnder(source, this._directory); 8520 } 8521 // Do nothing for the proxy file as we sideload an addon permanently 8522 } 8523 finally { 8524 // It isn't ideal if this cleanup fails but it isn't worth rolling back 8525 // the install because of it. 8526 try { 8527 recursiveRemove(trashDir); 8528 } 8529 catch (e) { 8530 logger.warn("Failed to remove trash directory when installing " + id, e); 8531 } 8532 } 8533 8534 let newFile = this._directory.clone(); 8535 8536 if (action == "proxy") { 8537 // When permanently installing sideloaded addon, we just put a proxy file 8538 // refering to the addon sources 8539 newFile.append(id); 8540 8541 writeStringToFile(newFile, source.path); 8542 } else { 8543 newFile.append(source.leafName); 8544 } 8545 8546 try { 8547 newFile.lastModifiedTime = Date.now(); 8548 } catch (e) { 8549 logger.warn("failed to set lastModifiedTime on " + newFile.path, e); 8550 } 8551 this._IDToFileMap[id] = newFile; 8552 XPIProvider._addURIMapping(id, newFile); 8553 8554 if (existingAddonID && existingAddonID != id && 8555 existingAddonID in this._IDToFileMap) { 8556 delete this._IDToFileMap[existingAddonID]; 8557 } 8558 8559 return newFile; 8560 }, 8561 8562 /** 8563 * Uninstalls an add-on from this location. 8564 * 8565 * @param aId 8566 * The ID of the add-on to uninstall 8567 * @throws if the ID does not match any of the add-ons installed 8568 */ 8569 uninstallAddon: function(aId) { 8570 let file = this._IDToFileMap[aId]; 8571 if (!file) { 8572 logger.warn("Attempted to remove " + aId + " from " + 8573 this._name + " but it was already gone"); 8574 return; 8575 } 8576 8577 file = this._directory.clone(); 8578 file.append(aId); 8579 if (!file.exists()) 8580 file.leafName += ".xpi"; 8581 8582 if (!file.exists()) { 8583 logger.warn("Attempted to remove " + aId + " from " + 8584 this._name + " but it was already gone"); 8585 8586 delete this._IDToFileMap[aId]; 8587 return; 8588 } 8589 8590 let trashDir = this.getTrashDir(); 8591 8592 if (file.leafName != aId) { 8593 logger.debug("uninstallAddon: flushing jar cache " + file.path + " for addon " + aId); 8594 flushJarCache(file); 8595 } 8596 8597 let transaction = new SafeInstallOperation(); 8598 8599 try { 8600 transaction.moveUnder(file, trashDir); 8601 } 8602 finally { 8603 // It isn't ideal if this cleanup fails, but it is probably better than 8604 // rolling back the uninstall at this point 8605 try { 8606 recursiveRemove(trashDir); 8607 } 8608 catch (e) { 8609 logger.warn("Failed to remove trash directory when uninstalling " + aId, e); 8610 } 8611 } 8612 8613 delete this._IDToFileMap[aId]; 8614 }, 8615}); 8616 8617/** 8618 * An object which identifies a directory install location for system add-ons 8619 * upgrades. 8620 * 8621 * The location consists of a directory which contains the add-ons installed. 8622 * 8623 * @param aName 8624 * The string identifier for the install location 8625 * @param aDirectory 8626 * The nsIFile directory for the install location 8627 * @param aScope 8628 * The scope of add-ons installed in this location 8629 * @param aResetSet 8630 * True to throw away the current add-on set 8631 */ 8632function SystemAddonInstallLocation(aName, aDirectory, aScope, aResetSet) { 8633 this._baseDir = aDirectory; 8634 this._nextDir = null; 8635 8636 this._stagingDirLock = 0; 8637 8638 if (aResetSet) 8639 this.resetAddonSet(); 8640 8641 this._addonSet = this._loadAddonSet(); 8642 8643 this._directory = null; 8644 if (this._addonSet.directory) { 8645 this._directory = aDirectory.clone(); 8646 this._directory.append(this._addonSet.directory); 8647 logger.info("SystemAddonInstallLocation scanning directory " + this._directory.path); 8648 } 8649 else { 8650 logger.info("SystemAddonInstallLocation directory is missing"); 8651 } 8652 8653 DirectoryInstallLocation.call(this, aName, this._directory, aScope); 8654 this.locked = false; 8655} 8656 8657SystemAddonInstallLocation.prototype = Object.create(DirectoryInstallLocation.prototype); 8658Object.assign(SystemAddonInstallLocation.prototype, { 8659 /** 8660 * Removes the specified files or directories in the staging directory and 8661 * then if the staging directory is empty attempts to remove it. 8662 * 8663 * @param aLeafNames 8664 * An array of file or directory to remove from the directory, the 8665 * array may be empty 8666 */ 8667 cleanStagingDir: function(aLeafNames = []) { 8668 let dir = this.getStagingDir(); 8669 8670 for (let name of aLeafNames) { 8671 let file = dir.clone(); 8672 file.append(name); 8673 recursiveRemove(file); 8674 } 8675 8676 if (this._stagingDirLock > 0) 8677 return; 8678 8679 let dirEntries = dir.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator); 8680 try { 8681 if (dirEntries.nextFile) 8682 return; 8683 } 8684 finally { 8685 dirEntries.close(); 8686 } 8687 8688 try { 8689 setFilePermissions(dir, FileUtils.PERMS_DIRECTORY); 8690 dir.remove(false); 8691 } 8692 catch (e) { 8693 logger.warn("Failed to remove staging dir", e); 8694 // Failing to remove the staging directory is ignorable 8695 } 8696 }, 8697 8698 /** 8699 * Gets the staging directory to put add-ons that are pending install and 8700 * uninstall into. 8701 * 8702 * @return {nsIFile} - staging directory for system add-on upgrades. 8703 */ 8704 getStagingDir: function() { 8705 this._addonSet = this._loadAddonSet(); 8706 let dir = null; 8707 if (this._addonSet.directory) { 8708 this._directory = this._baseDir.clone(); 8709 this._directory.append(this._addonSet.directory); 8710 dir = this._directory.clone(); 8711 dir.append(DIR_STAGE); 8712 } 8713 else { 8714 logger.info("SystemAddonInstallLocation directory is missing"); 8715 } 8716 8717 return dir; 8718 }, 8719 8720 requestStagingDir: function() { 8721 this._stagingDirLock++; 8722 if (this._stagingDirPromise) 8723 return this._stagingDirPromise; 8724 8725 this._addonSet = this._loadAddonSet(); 8726 if (this._addonSet.directory) { 8727 this._directory = this._baseDir.clone(); 8728 this._directory.append(this._addonSet.directory); 8729 } 8730 8731 OS.File.makeDir(this._directory.path); 8732 let stagepath = OS.Path.join(this._directory.path, DIR_STAGE); 8733 return this._stagingDirPromise = OS.File.makeDir(stagepath).then(null, (e) => { 8734 if (e instanceof OS.File.Error && e.becauseExists) 8735 return; 8736 logger.error("Failed to create staging directory", e); 8737 throw e; 8738 }); 8739 }, 8740 8741 releaseStagingDir: function() { 8742 this._stagingDirLock--; 8743 8744 if (this._stagingDirLock == 0) { 8745 this._stagingDirPromise = null; 8746 this.cleanStagingDir(); 8747 } 8748 8749 return Promise.resolve(); 8750 }, 8751 8752 /** 8753 * Reads the current set of system add-ons 8754 */ 8755 _loadAddonSet: function() { 8756 try { 8757 let setStr = Preferences.get(PREF_SYSTEM_ADDON_SET, null); 8758 if (setStr) { 8759 let addonSet = JSON.parse(setStr); 8760 if ((typeof addonSet == "object") && addonSet.schema == 1) 8761 return addonSet; 8762 } 8763 } 8764 catch (e) { 8765 logger.error("Malformed system add-on set, resetting."); 8766 } 8767 8768 return { schema: 1, addons: {} }; 8769 }, 8770 8771 /** 8772 * Saves the current set of system add-ons 8773 * 8774 * @param {Object} aAddonSet - object containing schema, directory and set 8775 * of system add-on IDs and versions. 8776 */ 8777 _saveAddonSet: function(aAddonSet) { 8778 Preferences.set(PREF_SYSTEM_ADDON_SET, JSON.stringify(aAddonSet)); 8779 }, 8780 8781 getAddonLocations: function() { 8782 // Updated system add-ons are ignored in safe mode 8783 if (Services.appinfo.inSafeMode) 8784 return new Map(); 8785 8786 let addons = DirectoryInstallLocation.prototype.getAddonLocations.call(this); 8787 8788 // Strip out any unexpected add-ons from the list 8789 for (let id of addons.keys()) { 8790 if (!(id in this._addonSet.addons)) 8791 addons.delete(id); 8792 } 8793 8794 return addons; 8795 }, 8796 8797 /** 8798 * Tests whether updated system add-ons are expected. 8799 */ 8800 isActive: function() { 8801 return this._directory != null; 8802 }, 8803 8804 isValidAddon: function(aAddon) { 8805 if (aAddon.appDisabled) { 8806 logger.warn(`System add-on ${aAddon.id} isn't compatible with the application.`); 8807 return false; 8808 } 8809 8810 if (aAddon.unpack) { 8811 logger.warn(`System add-on ${aAddon.id} isn't a packed add-on.`); 8812 return false; 8813 } 8814 8815 if (!aAddon.bootstrap) { 8816 logger.warn(`System add-on ${aAddon.id} isn't restartless.`); 8817 return false; 8818 } 8819 8820 if (!aAddon.multiprocessCompatible) { 8821 logger.warn(`System add-on ${aAddon.id} isn't multiprocess compatible.`); 8822 return false; 8823 } 8824 8825 return true; 8826 }, 8827 8828 /** 8829 * Tests whether the loaded add-on information matches what is expected. 8830 */ 8831 isValid: function(aAddons) { 8832 for (let id of Object.keys(this._addonSet.addons)) { 8833 if (!aAddons.has(id)) { 8834 logger.warn(`Expected add-on ${id} is missing from the system add-on location.`); 8835 return false; 8836 } 8837 8838 let addon = aAddons.get(id); 8839 if (addon.version != this._addonSet.addons[id].version) { 8840 logger.warn(`Expected system add-on ${id} to be version ${this._addonSet.addons[id].version} but was ${addon.version}.`); 8841 return false; 8842 } 8843 8844 if (!this.isValidAddon(addon)) 8845 return false; 8846 } 8847 8848 return true; 8849 }, 8850 8851 /** 8852 * Resets the add-on set so on the next startup the default set will be used. 8853 */ 8854 resetAddonSet: function() { 8855 8856 if (this._addonSet) { 8857 logger.info("Removing all system add-on upgrades."); 8858 8859 // remove everything from the pref first, if uninstall 8860 // fails then at least they will not be re-activated on 8861 // next restart. 8862 this._saveAddonSet({ schema: 1, addons: {} }); 8863 8864 for (let id of Object.keys(this._addonSet.addons)) { 8865 AddonManager.getAddonByID(id, addon => { 8866 if (addon) { 8867 addon.uninstall(); 8868 } 8869 }); 8870 } 8871 } 8872 }, 8873 8874 /** 8875 * Removes any directories not currently in use or pending use after a 8876 * restart. Any errors that happen here don't really matter as we'll attempt 8877 * to cleanup again next time. 8878 */ 8879 cleanDirectories: Task.async(function*() { 8880 8881 // System add-ons directory does not exist 8882 if (!(yield OS.File.exists(this._baseDir.path))) { 8883 return; 8884 } 8885 8886 let iterator; 8887 try { 8888 iterator = new OS.File.DirectoryIterator(this._baseDir.path); 8889 } 8890 catch (e) { 8891 logger.error("Failed to clean updated system add-ons directories.", e); 8892 return; 8893 } 8894 8895 try { 8896 let entries = []; 8897 8898 yield iterator.forEach(entry => { 8899 // Skip the directory currently in use 8900 if (this._directory && this._directory.path == entry.path) 8901 return; 8902 8903 // Skip the next directory 8904 if (this._nextDir && this._nextDir.path == entry.path) 8905 return; 8906 8907 entries.push(entry); 8908 }); 8909 8910 for (let entry of entries) { 8911 if (entry.isDir) { 8912 yield OS.File.removeDir(entry.path, { 8913 ignoreAbsent: true, 8914 ignorePermissions: true, 8915 }); 8916 } 8917 else { 8918 yield OS.File.remove(entry.path, { 8919 ignoreAbsent: true, 8920 }); 8921 } 8922 } 8923 } 8924 catch (e) { 8925 logger.error("Failed to clean updated system add-ons directories.", e); 8926 } 8927 finally { 8928 iterator.close(); 8929 } 8930 }), 8931 8932 /** 8933 * Installs a new set of system add-ons into the location and updates the 8934 * add-on set in prefs. 8935 * 8936 * @param {Array} aAddons - An array of addons to install. 8937 */ 8938 installAddonSet: Task.async(function*(aAddons) { 8939 // Make sure the base dir exists 8940 yield OS.File.makeDir(this._baseDir.path, { ignoreExisting: true }); 8941 8942 let addonSet = this._loadAddonSet(); 8943 8944 // Remove any add-ons that are no longer part of the set. 8945 for (let addonID of Object.keys(addonSet.addons)) { 8946 if (!aAddons.includes(addonID)) { 8947 AddonManager.getAddonByID(addonID, a => a.uninstall()); 8948 } 8949 } 8950 8951 let newDir = this._baseDir.clone(); 8952 8953 let uuidGen = Cc["@mozilla.org/uuid-generator;1"]. 8954 getService(Ci.nsIUUIDGenerator); 8955 newDir.append("blank"); 8956 8957 while (true) { 8958 newDir.leafName = uuidGen.generateUUID().toString(); 8959 8960 try { 8961 yield OS.File.makeDir(newDir.path, { ignoreExisting: false }); 8962 break; 8963 } 8964 catch (e) { 8965 logger.debug("Could not create new system add-on updates dir, retrying", e); 8966 } 8967 } 8968 8969 // Record the new upgrade directory. 8970 let state = { schema: 1, directory: newDir.leafName, addons: {} }; 8971 this._saveAddonSet(state); 8972 8973 this._nextDir = newDir; 8974 let location = this; 8975 8976 let installs = []; 8977 for (let addon of aAddons) { 8978 let install = yield createLocalInstall(addon._sourceBundle, location); 8979 installs.push(install); 8980 } 8981 8982 let installAddon = Task.async(function*(install) { 8983 // Make the new install own its temporary file. 8984 install.ownsTempFile = true; 8985 install.install(); 8986 }); 8987 8988 let postponeAddon = Task.async(function*(install) { 8989 let resumeFn; 8990 if (AddonManagerPrivate.hasUpgradeListener(install.addon.id)) { 8991 logger.info(`system add-on ${install.addon.id} has an upgrade listener, postponing upgrade set until restart`); 8992 resumeFn = () => { 8993 logger.info(`${install.addon.id} has resumed a previously postponed addon set`); 8994 install.installLocation.resumeAddonSet(installs); 8995 } 8996 } 8997 yield install.postpone(resumeFn); 8998 }); 8999 9000 let previousState; 9001 9002 try { 9003 // All add-ons in position, create the new state and store it in prefs 9004 state = { schema: 1, directory: newDir.leafName, addons: {} }; 9005 for (let addon of aAddons) { 9006 state.addons[addon.id] = { 9007 version: addon.version 9008 } 9009 } 9010 9011 previousState = this._loadAddonSet(); 9012 this._saveAddonSet(state); 9013 9014 let blockers = aAddons.filter( 9015 addon => AddonManagerPrivate.hasUpgradeListener(addon.id) 9016 ); 9017 9018 if (blockers.length > 0) { 9019 yield waitForAllPromises(installs.map(postponeAddon)); 9020 } else { 9021 yield waitForAllPromises(installs.map(installAddon)); 9022 } 9023 } 9024 catch (e) { 9025 // Roll back to previous upgrade set (if present) on restart. 9026 if (previousState) { 9027 this._saveAddonSet(previousState); 9028 } 9029 // Otherwise, roll back to built-in set on restart. 9030 // TODO try to do these restartlessly 9031 this.resetAddonSet(); 9032 9033 try { 9034 yield OS.File.removeDir(newDir.path, { ignorePermissions: true }); 9035 } 9036 catch (e) { 9037 logger.warn(`Failed to remove failed system add-on directory ${newDir.path}.`, e); 9038 } 9039 throw e; 9040 } 9041 }), 9042 9043 /** 9044 * Resumes upgrade of a previously-delayed add-on set. 9045 */ 9046 resumeAddonSet: Task.async(function*(installs) { 9047 let resumeAddon = Task.async(function*(install) { 9048 install.state = AddonManager.STATE_DOWNLOADED; 9049 install.installLocation.releaseStagingDir(); 9050 install.install(); 9051 }); 9052 9053 let addonSet = this._loadAddonSet(); 9054 let addonIDs = Object.keys(addonSet.addons); 9055 9056 let blockers = installs.filter( 9057 install => AddonManagerPrivate.hasUpgradeListener(install.addon.id) 9058 ); 9059 9060 if (blockers.length > 1) { 9061 logger.warn("Attempted to resume system add-on install but upgrade blockers are still present"); 9062 } else { 9063 yield waitForAllPromises(installs.map(resumeAddon)); 9064 } 9065 }), 9066 9067 /** 9068 * Returns a directory that is normally on the same filesystem as the rest of 9069 * the install location and can be used for temporarily storing files during 9070 * safe move operations. Calling this method will delete the existing trash 9071 * directory and its contents. 9072 * 9073 * @return an nsIFile 9074 */ 9075 getTrashDir: function() { 9076 let trashDir = this._directory.clone(); 9077 trashDir.append(DIR_TRASH); 9078 let trashDirExists = trashDir.exists(); 9079 try { 9080 if (trashDirExists) 9081 recursiveRemove(trashDir); 9082 trashDirExists = false; 9083 } catch (e) { 9084 logger.warn("Failed to remove trash directory", e); 9085 } 9086 if (!trashDirExists) 9087 trashDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); 9088 9089 return trashDir; 9090 }, 9091 9092 /** 9093 * Installs an add-on into the install location. 9094 * 9095 * @param id 9096 * The ID of the add-on to install 9097 * @param source 9098 * The source nsIFile to install from 9099 * @return an nsIFile indicating where the add-on was installed to 9100 */ 9101 installAddon: function({id, source}) { 9102 let trashDir = this.getTrashDir(); 9103 let transaction = new SafeInstallOperation(); 9104 9105 // If any of these operations fails the finally block will clean up the 9106 // temporary directory 9107 try { 9108 if (source.isFile()) { 9109 flushJarCache(source); 9110 } 9111 9112 transaction.moveUnder(source, this._directory); 9113 } 9114 finally { 9115 // It isn't ideal if this cleanup fails but it isn't worth rolling back 9116 // the install because of it. 9117 try { 9118 recursiveRemove(trashDir); 9119 } 9120 catch (e) { 9121 logger.warn("Failed to remove trash directory when installing " + id, e); 9122 } 9123 } 9124 9125 let newFile = this._directory.clone(); 9126 newFile.append(source.leafName); 9127 9128 try { 9129 newFile.lastModifiedTime = Date.now(); 9130 } catch (e) { 9131 logger.warn("failed to set lastModifiedTime on " + newFile.path, e); 9132 } 9133 this._IDToFileMap[id] = newFile; 9134 XPIProvider._addURIMapping(id, newFile); 9135 9136 return newFile; 9137 }, 9138 9139 // old system add-on upgrade dirs get automatically removed 9140 uninstallAddon: (aAddon) => {}, 9141}); 9142 9143/** 9144 * An object which identifies an install location for temporary add-ons. 9145 */ 9146const TemporaryInstallLocation = { 9147 locked: false, 9148 name: KEY_APP_TEMPORARY, 9149 scope: AddonManager.SCOPE_TEMPORARY, 9150 getAddonLocations: () => [], 9151 isLinkedAddon: () => false, 9152 installAddon: () => {}, 9153 uninstallAddon: (aAddon) => {}, 9154 getStagingDir: () => {}, 9155} 9156 9157/** 9158 * An object that identifies a registry install location for add-ons. The location 9159 * consists of a registry key which contains string values mapping ID to the 9160 * path where an add-on is installed 9161 * 9162 * @param aName 9163 * The string identifier of this Install Location. 9164 * @param aRootKey 9165 * The root key (one of the ROOT_KEY_ values from nsIWindowsRegKey). 9166 * @param scope 9167 * The scope of add-ons installed in this location 9168 */ 9169function WinRegInstallLocation(aName, aRootKey, aScope) { 9170 this.locked = true; 9171 this._name = aName; 9172 this._rootKey = aRootKey; 9173 this._scope = aScope; 9174 this._IDToFileMap = {}; 9175 9176 let path = this._appKeyPath + "\\Extensions"; 9177 let key = Cc["@mozilla.org/windows-registry-key;1"]. 9178 createInstance(Ci.nsIWindowsRegKey); 9179 9180 // Reading the registry may throw an exception, and that's ok. In error 9181 // cases, we just leave ourselves in the empty state. 9182 try { 9183 key.open(this._rootKey, path, Ci.nsIWindowsRegKey.ACCESS_READ); 9184 } 9185 catch (e) { 9186 return; 9187 } 9188 9189 this._readAddons(key); 9190 key.close(); 9191} 9192 9193WinRegInstallLocation.prototype = { 9194 _name : "", 9195 _rootKey : null, 9196 _scope : null, 9197 _IDToFileMap : null, // mapping from ID to nsIFile 9198 9199 /** 9200 * Retrieves the path of this Application's data key in the registry. 9201 */ 9202 get _appKeyPath() { 9203 let appVendor = Services.appinfo.vendor; 9204 let appName = Services.appinfo.name; 9205 9206 // XXX Thunderbird doesn't specify a vendor string 9207 if (AppConstants.MOZ_APP_NAME == "thunderbird" && appVendor == "") 9208 appVendor = "Mozilla"; 9209 9210 // XULRunner-based apps may intentionally not specify a vendor 9211 if (appVendor != "") 9212 appVendor += "\\"; 9213 9214 return "SOFTWARE\\" + appVendor + appName; 9215 }, 9216 9217 /** 9218 * Read the registry and build a mapping between ID and path for each 9219 * installed add-on. 9220 * 9221 * @param key 9222 * The key that contains the ID to path mapping 9223 */ 9224 _readAddons: function(aKey) { 9225 let count = aKey.valueCount; 9226 for (let i = 0; i < count; ++i) { 9227 let id = aKey.getValueName(i); 9228 9229 let file = new nsIFile(aKey.readStringValue(id)); 9230 9231 if (!file.exists()) { 9232 logger.warn("Ignoring missing add-on in " + file.path); 9233 continue; 9234 } 9235 9236 this._IDToFileMap[id] = file; 9237 XPIProvider._addURIMapping(id, file); 9238 } 9239 }, 9240 9241 /** 9242 * Gets the name of this install location. 9243 */ 9244 get name() { 9245 return this._name; 9246 }, 9247 9248 /** 9249 * Gets the scope of this install location. 9250 */ 9251 get scope() { 9252 return this._scope; 9253 }, 9254 9255 /** 9256 * Gets an array of nsIFiles for add-ons installed in this location. 9257 */ 9258 getAddonLocations: function() { 9259 let locations = new Map(); 9260 for (let id in this._IDToFileMap) { 9261 locations.set(id, this._IDToFileMap[id].clone()); 9262 } 9263 return locations; 9264 }, 9265 9266 /** 9267 * @see DirectoryInstallLocation 9268 */ 9269 isLinkedAddon: function(aId) { 9270 return true; 9271 } 9272}; 9273 9274var addonTypes = [ 9275 new AddonManagerPrivate.AddonType("extension", URI_EXTENSION_STRINGS, 9276 STRING_TYPE_NAME, 9277 AddonManager.VIEW_TYPE_LIST, 4000, 9278 AddonManager.TYPE_SUPPORTS_UNDO_RESTARTLESS_UNINSTALL), 9279 new AddonManagerPrivate.AddonType("theme", URI_EXTENSION_STRINGS, 9280 STRING_TYPE_NAME, 9281 AddonManager.VIEW_TYPE_LIST, 5000), 9282 new AddonManagerPrivate.AddonType("dictionary", URI_EXTENSION_STRINGS, 9283 STRING_TYPE_NAME, 9284 AddonManager.VIEW_TYPE_LIST, 7000, 9285 AddonManager.TYPE_UI_HIDE_EMPTY | AddonManager.TYPE_SUPPORTS_UNDO_RESTARTLESS_UNINSTALL), 9286 new AddonManagerPrivate.AddonType("locale", URI_EXTENSION_STRINGS, 9287 STRING_TYPE_NAME, 9288 AddonManager.VIEW_TYPE_LIST, 8000, 9289 AddonManager.TYPE_UI_HIDE_EMPTY | AddonManager.TYPE_SUPPORTS_UNDO_RESTARTLESS_UNINSTALL), 9290]; 9291 9292// We only register experiments support if the application supports them. 9293// Ideally, we would install an observer to watch the pref. Installing 9294// an observer for this pref is not necessary here and may be buggy with 9295// regards to registering this XPIProvider twice. 9296if (Preferences.get("experiments.supported", false)) { 9297 addonTypes.push( 9298 new AddonManagerPrivate.AddonType("experiment", 9299 URI_EXTENSION_STRINGS, 9300 STRING_TYPE_NAME, 9301 AddonManager.VIEW_TYPE_LIST, 11000, 9302 AddonManager.TYPE_UI_HIDE_EMPTY | AddonManager.TYPE_SUPPORTS_UNDO_RESTARTLESS_UNINSTALL)); 9303} 9304 9305AddonManagerPrivate.registerProvider(XPIProvider, addonTypes); 9306