1/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ 2/* vim: set sts=2 sw=2 et tw=80: */ 3/* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6"use strict"; 7 8var EXPORTED_SYMBOLS = [ 9 "Dictionary", 10 "Extension", 11 "ExtensionData", 12 "Langpack", 13 "Management", 14 "SitePermission", 15 "ExtensionAddonObserver", 16]; 17 18/* exported Extension, ExtensionData */ 19 20/* 21 * This file is the main entry point for extensions. When an extension 22 * loads, its bootstrap.js file creates a Extension instance 23 * and calls .startup() on it. It calls .shutdown() when the extension 24 * unloads. Extension manages any extension-specific state in 25 * the chrome process. 26 * 27 * TODO(rpl): we are current restricting the extensions to a single process 28 * (set as the current default value of the "dom.ipc.processCount.extension" 29 * preference), if we switch to use more than one extension process, we have to 30 * be sure that all the browser's frameLoader are associated to the same process, 31 * e.g. by enabling the `maychangeremoteness` attribute, and/or setting 32 * `initialBrowsingContextGroupId` attribute to the correct value. 33 * 34 * At that point we are going to keep track of the existing browsers associated to 35 * a webextension to ensure that they are all running in the same process (and we 36 * are also going to do the same with the browser element provided to the 37 * addon debugging Remote Debugging actor, e.g. because the addon has been 38 * reloaded by the user, we have to ensure that the new extension pages are going 39 * to run in the same process of the existing addon debugging browser element). 40 */ 41 42const { XPCOMUtils } = ChromeUtils.import( 43 "resource://gre/modules/XPCOMUtils.jsm" 44); 45const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); 46 47XPCOMUtils.defineLazyModuleGetters(this, { 48 AddonManager: "resource://gre/modules/AddonManager.jsm", 49 AddonManagerPrivate: "resource://gre/modules/AddonManager.jsm", 50 AddonSettings: "resource://gre/modules/addons/AddonSettings.jsm", 51 AppConstants: "resource://gre/modules/AppConstants.jsm", 52 AsyncShutdown: "resource://gre/modules/AsyncShutdown.jsm", 53 E10SUtils: "resource://gre/modules/E10SUtils.jsm", 54 ExtensionPermissions: "resource://gre/modules/ExtensionPermissions.jsm", 55 ExtensionPreferencesManager: 56 "resource://gre/modules/ExtensionPreferencesManager.jsm", 57 ExtensionProcessScript: "resource://gre/modules/ExtensionProcessScript.jsm", 58 ExtensionStorage: "resource://gre/modules/ExtensionStorage.jsm", 59 ExtensionStorageIDB: "resource://gre/modules/ExtensionStorageIDB.jsm", 60 ExtensionTelemetry: "resource://gre/modules/ExtensionTelemetry.jsm", 61 LightweightThemeManager: "resource://gre/modules/LightweightThemeManager.jsm", 62 Log: "resource://gre/modules/Log.jsm", 63 NetUtil: "resource://gre/modules/NetUtil.jsm", 64 PluralForm: "resource://gre/modules/PluralForm.jsm", 65 Schemas: "resource://gre/modules/Schemas.jsm", 66 ServiceWorkerCleanUp: "resource://gre/modules/ServiceWorkerCleanUp.jsm", 67 XPIProvider: "resource://gre/modules/addons/XPIProvider.jsm", 68 69 // These are used for manipulating jar entry paths, which always use Unix 70 // separators. 71 basename: "resource://gre/modules/osfile/ospath_unix.jsm", 72 dirname: "resource://gre/modules/osfile/ospath_unix.jsm", 73}); 74 75XPCOMUtils.defineLazyGetter(this, "resourceProtocol", () => 76 Services.io 77 .getProtocolHandler("resource") 78 .QueryInterface(Ci.nsIResProtocolHandler) 79); 80 81const { ExtensionCommon } = ChromeUtils.import( 82 "resource://gre/modules/ExtensionCommon.jsm" 83); 84const { ExtensionParent } = ChromeUtils.import( 85 "resource://gre/modules/ExtensionParent.jsm" 86); 87const { ExtensionUtils } = ChromeUtils.import( 88 "resource://gre/modules/ExtensionUtils.jsm" 89); 90 91XPCOMUtils.defineLazyServiceGetters(this, { 92 aomStartup: [ 93 "@mozilla.org/addons/addon-manager-startup;1", 94 "amIAddonManagerStartup", 95 ], 96 spellCheck: ["@mozilla.org/spellchecker/engine;1", "mozISpellCheckingEngine"], 97}); 98 99XPCOMUtils.defineLazyPreferenceGetter( 100 this, 101 "processCount", 102 "dom.ipc.processCount.extension" 103); 104 105// Temporary pref to be turned on when ready. 106XPCOMUtils.defineLazyPreferenceGetter( 107 this, 108 "userContextIsolation", 109 "extensions.userContextIsolation.enabled", 110 false 111); 112 113XPCOMUtils.defineLazyPreferenceGetter( 114 this, 115 "userContextIsolationDefaultRestricted", 116 "extensions.userContextIsolation.defaults.restricted", 117 "[]" 118); 119 120// This pref modifies behavior for MV2. MV3 is enabled regardless. 121XPCOMUtils.defineLazyPreferenceGetter( 122 this, 123 "eventPagesEnabled", 124 "extensions.eventPages.enabled" 125); 126 127var { 128 GlobalManager, 129 ParentAPIManager, 130 StartupCache, 131 apiManager: Management, 132} = ExtensionParent; 133 134const { getUniqueId, promiseTimeout } = ExtensionUtils; 135 136const { EventEmitter } = ExtensionCommon; 137 138XPCOMUtils.defineLazyGetter(this, "console", ExtensionCommon.getConsole); 139 140XPCOMUtils.defineLazyGetter( 141 this, 142 "LocaleData", 143 () => ExtensionCommon.LocaleData 144); 145 146XPCOMUtils.defineLazyGetter(this, "LAZY_NO_PROMPT_PERMISSIONS", async () => { 147 // Wait until all extension API schemas have been loaded and parsed. 148 await Management.lazyInit(); 149 return new Set( 150 Schemas.getPermissionNames([ 151 "PermissionNoPrompt", 152 "OptionalPermissionNoPrompt", 153 ]) 154 ); 155}); 156 157XPCOMUtils.defineLazyGetter(this, "LAZY_SCHEMA_SITE_PERMISSIONS", async () => { 158 // Wait until all extension API schemas have been loaded and parsed. 159 await Management.lazyInit(); 160 return Schemas.getPermissionNames(["SitePermission"]); 161}); 162 163const { sharedData } = Services.ppmm; 164 165const PRIVATE_ALLOWED_PERMISSION = "internal:privateBrowsingAllowed"; 166const SVG_CONTEXT_PROPERTIES_PERMISSION = 167 "internal:svgContextPropertiesAllowed"; 168 169// The userContextID reserved for the extension storage (its purpose is ensuring that the IndexedDB 170// storage used by the browser.storage.local API is not directly accessible from the extension code, 171// it is defined and reserved as "userContextIdInternal.webextStorageLocal" in ContextualIdentityService.jsm). 172const WEBEXT_STORAGE_USER_CONTEXT_ID = -1 >>> 0; 173 174// The maximum time to wait for extension child shutdown blockers to complete. 175const CHILD_SHUTDOWN_TIMEOUT_MS = 8000; 176 177// Permissions that are only available to privileged extensions. 178const PRIVILEGED_PERMS = new Set([ 179 "activityLog", 180 "mozillaAddons", 181 "geckoViewAddons", 182 "telemetry", 183 "urlbar", 184 "nativeMessagingFromContent", 185 "normandyAddonStudy", 186 "networkStatus", 187]); 188 189if (AppConstants.platform == "android") { 190 PRIVILEGED_PERMS.add("nativeMessaging"); 191} 192 193const INSTALL_AND_UPDATE_STARTUP_REASONS = new Set([ 194 "ADDON_INSTALL", 195 "ADDON_UPGRADE", 196 "ADDON_DOWNGRADE", 197]); 198 199// Returns true if the extension is owned by Mozilla (is either privileged, 200// using one of the @mozilla.com/@mozilla.org protected addon id suffixes). 201// 202// This method throws if the extension's startupReason is not one of the expected 203// ones (either ADDON_INSTALL, ADDON_UPGRADE or ADDON_DOWNGRADE). 204// 205// NOTE: This methos is internally referring to "addonData.recommendationState" to 206// identify a Mozilla line extension. That property is part of the addonData only when 207// the extension is installed or updated, and so we enforce the expected 208// startup reason values to prevent it from silently returning different results 209// if called with an unexpected startupReason. 210function isMozillaExtension(extension) { 211 const { addonData, id, isPrivileged, startupReason } = extension; 212 213 if (!INSTALL_AND_UPDATE_STARTUP_REASONS.has(startupReason)) { 214 throw new Error( 215 `isMozillaExtension called with unexpected startupReason: ${startupReason}` 216 ); 217 } 218 219 if (isPrivileged) { 220 return true; 221 } 222 223 if (id.endsWith("@mozilla.com") || id.endsWith("@mozilla.org")) { 224 return true; 225 } 226 227 // This check is a subset of what is being checked in AddonWrapper's 228 // recommendationStates (states expire dates for line extensions are 229 // not consideredcimportant in determining that the extension is 230 // provided by mozilla, and so they are omitted here on purpose). 231 const isMozillaLineExtension = addonData.recommendationState?.states?.includes( 232 "line" 233 ); 234 const isSigned = addonData.signedState > AddonManager.SIGNEDSTATE_MISSING; 235 236 return isSigned && isMozillaLineExtension; 237} 238 239/** 240 * Classify an individual permission from a webextension manifest 241 * as a host/origin permission, an api permission, or a regular permission. 242 * 243 * @param {string} perm The permission string to classify 244 * @param {boolean} restrictSchemes 245 * @param {boolean} isPrivileged whether or not the webextension is privileged 246 * 247 * @returns {object} 248 * An object with exactly one of the following properties: 249 * "origin" to indicate this is a host/origin permission. 250 * "api" to indicate this is an api permission 251 * (as used for webextensions experiments). 252 * "permission" to indicate this is a regular permission. 253 * "invalid" to indicate that the given permission cannot be used. 254 */ 255function classifyPermission(perm, restrictSchemes, isPrivileged) { 256 let match = /^(\w+)(?:\.(\w+)(?:\.\w+)*)?$/.exec(perm); 257 if (!match) { 258 try { 259 let { pattern } = new MatchPattern(perm, { 260 restrictSchemes, 261 ignorePath: true, 262 }); 263 return { origin: pattern }; 264 } catch (e) { 265 return { invalid: perm }; 266 } 267 } else if (match[1] == "experiments" && match[2]) { 268 return { api: match[2] }; 269 } else if (!isPrivileged && PRIVILEGED_PERMS.has(match[1])) { 270 return { invalid: perm }; 271 } 272 return { permission: perm }; 273} 274 275const LOGGER_ID_BASE = "addons.webextension."; 276const UUID_MAP_PREF = "extensions.webextensions.uuids"; 277const LEAVE_STORAGE_PREF = "extensions.webextensions.keepStorageOnUninstall"; 278const LEAVE_UUID_PREF = "extensions.webextensions.keepUuidOnUninstall"; 279 280const COMMENT_REGEXP = new RegExp( 281 String.raw` 282 ^ 283 ( 284 (?: 285 [^"\n] | 286 " (?:[^"\\\n] | \\.)* " 287 )*? 288 ) 289 290 //.* 291 `.replace(/\s+/g, ""), 292 "gm" 293); 294 295// All moz-extension URIs use a machine-specific UUID rather than the 296// extension's own ID in the host component. This makes it more 297// difficult for web pages to detect whether a user has a given add-on 298// installed (by trying to load a moz-extension URI referring to a 299// web_accessible_resource from the extension). UUIDMap.get() 300// returns the UUID for a given add-on ID. 301var UUIDMap = { 302 _read() { 303 let pref = Services.prefs.getStringPref(UUID_MAP_PREF, "{}"); 304 try { 305 return JSON.parse(pref); 306 } catch (e) { 307 Cu.reportError(`Error parsing ${UUID_MAP_PREF}.`); 308 return {}; 309 } 310 }, 311 312 _write(map) { 313 Services.prefs.setStringPref(UUID_MAP_PREF, JSON.stringify(map)); 314 }, 315 316 get(id, create = true) { 317 let map = this._read(); 318 319 if (id in map) { 320 return map[id]; 321 } 322 323 let uuid = null; 324 if (create) { 325 uuid = Services.uuid.generateUUID().number; 326 uuid = uuid.slice(1, -1); // Strip { and } off the UUID. 327 328 map[id] = uuid; 329 this._write(map); 330 } 331 return uuid; 332 }, 333 334 remove(id) { 335 let map = this._read(); 336 delete map[id]; 337 this._write(map); 338 }, 339}; 340 341function clearCacheForExtensionPrincipal(principal, clearAll = false) { 342 if (!principal.schemeIs("moz-extension")) { 343 return Promise.reject(new Error("Unexpected non extension principal")); 344 } 345 346 // TODO(Bug 1750053): replace the two specific flags with a "clear all caches one" 347 // (along with covering the other kind of cached data with tests). 348 const clearDataFlags = clearAll 349 ? Ci.nsIClearDataService.CLEAR_ALL_CACHES 350 : Ci.nsIClearDataService.CLEAR_IMAGE_CACHE | 351 Ci.nsIClearDataService.CLEAR_CSS_CACHE; 352 353 return new Promise(resolve => 354 Services.clearData.deleteDataFromPrincipal( 355 principal, 356 false, 357 clearDataFlags, 358 () => resolve() 359 ) 360 ); 361} 362 363/** 364 * Observer AddonManager events and translate them into extension events, 365 * as well as handle any last cleanup after uninstalling an extension. 366 */ 367var ExtensionAddonObserver = { 368 initialized: false, 369 370 init() { 371 if (!this.initialized) { 372 AddonManager.addAddonListener(this); 373 this.initialized = true; 374 } 375 }, 376 377 // AddonTestUtils will call this as necessary. 378 uninit() { 379 if (this.initialized) { 380 AddonManager.removeAddonListener(this); 381 this.initialized = false; 382 } 383 }, 384 385 onEnabling(addon) { 386 if (addon.type !== "extension") { 387 return; 388 } 389 Management._callHandlers([addon.id], "enabling", "onEnabling"); 390 }, 391 392 onDisabled(addon) { 393 if (addon.type !== "extension") { 394 return; 395 } 396 if (Services.appinfo.inSafeMode) { 397 // Ensure ExtensionPreferencesManager updates its data and 398 // modules can run any disable logic they need to. We only 399 // handle safeMode here because there is a bunch of additional 400 // logic that happens in Extension.shutdown when running in 401 // normal mode. 402 Management._callHandlers([addon.id], "disable", "onDisable"); 403 } 404 }, 405 406 onUninstalling(addon) { 407 let extension = GlobalManager.extensionMap.get(addon.id); 408 if (extension) { 409 // Let any other interested listeners respond 410 // (e.g., display the uninstall URL) 411 Management.emit("uninstalling", extension); 412 } 413 }, 414 415 onUninstalled(addon) { 416 // Cleanup anything that is used by non-extension addon types 417 // since only extensions have uuid's. 418 ExtensionPermissions.removeAll(addon.id); 419 420 let uuid = UUIDMap.get(addon.id, false); 421 if (!uuid) { 422 return; 423 } 424 425 let baseURI = Services.io.newURI(`moz-extension://${uuid}/`); 426 let principal = Services.scriptSecurityManager.createContentPrincipal( 427 baseURI, 428 {} 429 ); 430 431 // Clear all cached resources (e.g. CSS and images); 432 AsyncShutdown.profileChangeTeardown.addBlocker( 433 `Clear cache for ${addon.id}`, 434 clearCacheForExtensionPrincipal(principal, /* clearAll */ true) 435 ); 436 437 // Clear all the registered service workers for the extension 438 // principal (the one that may have been registered through the 439 // manifest.json file and the ones that may have been registered 440 // from an extension page through the service worker API). 441 // 442 // Any stored data would be cleared below (if the pref 443 // "extensions.webextensions.keepStorageOnUninstall has not been 444 // explicitly set to true, which is usually only done in 445 // tests and by some extensions developers for testing purpose). 446 // 447 // TODO: ServiceWorkerCleanUp may go away once Bug 1183245 448 // is fixed, and so this may actually go away, replaced by 449 // marking the registration as disabled or to be removed on 450 // shutdown (where we do know if the extension is shutting 451 // down because is being uninstalled) and then cleared from 452 // the persisted serviceworker registration on the next 453 // startup. 454 AsyncShutdown.profileChangeTeardown.addBlocker( 455 `Clear ServiceWorkers for ${addon.id}`, 456 ServiceWorkerCleanUp.removeFromPrincipal(principal) 457 ); 458 459 if (!Services.prefs.getBoolPref(LEAVE_STORAGE_PREF, false)) { 460 // Clear browser.storage.local backends. 461 AsyncShutdown.profileChangeTeardown.addBlocker( 462 `Clear Extension Storage ${addon.id} (File Backend)`, 463 ExtensionStorage.clear(addon.id, { shouldNotifyListeners: false }) 464 ); 465 466 // Clear any IndexedDB and Cache API storage created by the extension. 467 // If LSNG is enabled, this also clears localStorage. 468 Services.qms.clearStoragesForPrincipal(principal); 469 470 // Clear any storage.local data stored in the IDBBackend. 471 let storagePrincipal = Services.scriptSecurityManager.createContentPrincipal( 472 baseURI, 473 { 474 userContextId: WEBEXT_STORAGE_USER_CONTEXT_ID, 475 } 476 ); 477 Services.qms.clearStoragesForPrincipal(storagePrincipal); 478 479 ExtensionStorageIDB.clearMigratedExtensionPref(addon.id); 480 481 // If LSNG is not enabled, we need to clear localStorage explicitly using 482 // the old API. 483 if (!Services.domStorageManager.nextGenLocalStorageEnabled) { 484 // Clear localStorage created by the extension 485 let storage = Services.domStorageManager.getStorage( 486 null, 487 principal, 488 principal 489 ); 490 if (storage) { 491 storage.clear(); 492 } 493 } 494 495 // Remove any permissions related to the unlimitedStorage permission 496 // if we are also removing all the data stored by the extension. 497 Services.perms.removeFromPrincipal( 498 principal, 499 "WebExtensions-unlimitedStorage" 500 ); 501 Services.perms.removeFromPrincipal(principal, "indexedDB"); 502 Services.perms.removeFromPrincipal(principal, "persistent-storage"); 503 } 504 505 if (!Services.prefs.getBoolPref(LEAVE_UUID_PREF, false)) { 506 // Clear the entry in the UUID map 507 UUIDMap.remove(addon.id); 508 } 509 }, 510}; 511 512ExtensionAddonObserver.init(); 513 514const manifestTypes = new Map([ 515 ["theme", "manifest.ThemeManifest"], 516 ["sitepermission", "manifest.WebExtensionSitePermissionsManifest"], 517 ["langpack", "manifest.WebExtensionLangpackManifest"], 518 ["dictionary", "manifest.WebExtensionDictionaryManifest"], 519 ["extension", "manifest.WebExtensionManifest"], 520]); 521 522/** 523 * Represents the data contained in an extension, contained either 524 * in a directory or a zip file, which may or may not be installed. 525 * This class implements the functionality of the Extension class, 526 * primarily related to manifest parsing and localization, which is 527 * useful prior to extension installation or initialization. 528 * 529 * No functionality of this class is guaranteed to work before 530 * `loadManifest` has been called, and completed. 531 */ 532class ExtensionData { 533 constructor(rootURI, isPrivileged = false) { 534 this.rootURI = rootURI; 535 this.resourceURL = rootURI.spec; 536 this.isPrivileged = isPrivileged; 537 538 this.manifest = null; 539 this.type = null; 540 this.id = null; 541 this.uuid = null; 542 this.localeData = null; 543 this.fluentL10n = null; 544 this._promiseLocales = null; 545 546 this.apiNames = new Set(); 547 this.dependencies = new Set(); 548 this.permissions = new Set(); 549 550 this.startupData = null; 551 552 this.errors = []; 553 this.warnings = []; 554 this.eventPagesEnabled = eventPagesEnabled; 555 } 556 557 /** 558 * A factory function that allows the construction of ExtensionData, with 559 * the isPrivileged flag computed asynchronously. 560 * 561 * @param {nsIURI} rootURI 562 * The URI pointing to the extension root. 563 * @param {function(type, id)} checkPrivileged 564 * An (async) function that takes the addon type and addon ID and returns 565 * whether the given add-on is privileged. 566 * @returns {ExtensionData} 567 */ 568 static async constructAsync({ rootURI, checkPrivileged }) { 569 let extension = new ExtensionData(rootURI); 570 // checkPrivileged depends on the extension type and id. 571 await extension.initializeAddonTypeAndID(); 572 let { type, id } = extension; 573 // Map the extension type to the type name used by the add-on manager. 574 // TODO bug 1757084: Remove this. 575 type = type == "langpack" ? "locale" : type; 576 extension.isPrivileged = await checkPrivileged(type, id); 577 return extension; 578 } 579 580 static getIsPrivileged({ signedState, builtIn, temporarilyInstalled }) { 581 return ( 582 signedState === AddonManager.SIGNEDSTATE_PRIVILEGED || 583 signedState === AddonManager.SIGNEDSTATE_SYSTEM || 584 builtIn || 585 (AddonSettings.EXPERIMENTS_ENABLED && temporarilyInstalled) 586 ); 587 } 588 589 get builtinMessages() { 590 return null; 591 } 592 593 get logger() { 594 let id = this.id || "<unknown>"; 595 return Log.repository.getLogger(LOGGER_ID_BASE + id); 596 } 597 598 /** 599 * Report an error about the extension's manifest file. 600 * @param {string} message The error message 601 */ 602 manifestError(message) { 603 this.packagingError(`Reading manifest: ${message}`); 604 } 605 606 /** 607 * Report a warning about the extension's manifest file. 608 * @param {string} message The warning message 609 */ 610 manifestWarning(message) { 611 this.packagingWarning(`Reading manifest: ${message}`); 612 } 613 614 // Report an error about the extension's general packaging. 615 packagingError(message) { 616 this.errors.push(message); 617 this.logError(message); 618 } 619 620 packagingWarning(message) { 621 this.warnings.push(message); 622 this.logWarning(message); 623 } 624 625 logWarning(message) { 626 this._logMessage(message, "warn"); 627 } 628 629 logError(message) { 630 this._logMessage(message, "error"); 631 } 632 633 _logMessage(message, severity) { 634 this.logger[severity](`Loading extension '${this.id}': ${message}`); 635 } 636 637 ensureNoErrors() { 638 if (this.errors.length) { 639 // startup() repeatedly checks whether there are errors after parsing the 640 // extension/manifest before proceeding with starting up. 641 throw new Error(this.errors.join("\n")); 642 } 643 } 644 645 /** 646 * Returns the moz-extension: URL for the given path within this 647 * extension. 648 * 649 * Must not be called unless either the `id` or `uuid` property has 650 * already been set. 651 * 652 * @param {string} path The path portion of the URL. 653 * @returns {string} 654 */ 655 getURL(path = "") { 656 if (!(this.id || this.uuid)) { 657 throw new Error( 658 "getURL may not be called before an `id` or `uuid` has been set" 659 ); 660 } 661 if (!this.uuid) { 662 this.uuid = UUIDMap.get(this.id); 663 } 664 return `moz-extension://${this.uuid}/${path}`; 665 } 666 667 /** 668 * Discovers the file names within a directory or JAR file. 669 * 670 * @param {Ci.nsIFileURL|Ci.nsIJARURI} path 671 * The path to the directory or jar file to look at. 672 * @param {boolean} [directoriesOnly] 673 * If true, this will return only the directories present within the directory. 674 * @returns {string[]} 675 * An array of names of files/directories (only the name, not the path). 676 */ 677 async _readDirectory(path, directoriesOnly = false) { 678 if (this.rootURI instanceof Ci.nsIFileURL) { 679 let uri = Services.io.newURI("./" + path, null, this.rootURI); 680 let fullPath = uri.QueryInterface(Ci.nsIFileURL).file.path; 681 682 let results = []; 683 try { 684 let children = await IOUtils.getChildren(fullPath); 685 for (let child of children) { 686 if ( 687 !directoriesOnly || 688 (await IOUtils.stat(child)).type == "directory" 689 ) { 690 results.push(PathUtils.filename(child)); 691 } 692 } 693 } catch (ex) { 694 // Fall-through, return what we have. 695 } 696 return results; 697 } 698 699 let uri = this.rootURI.QueryInterface(Ci.nsIJARURI); 700 701 // Append the sub-directory path to the base JAR URI and normalize the 702 // result. 703 let entry = `${uri.JAREntry}/${path}/` 704 .replace(/\/\/+/g, "/") 705 .replace(/^\//, ""); 706 uri = Services.io.newURI(`jar:${uri.JARFile.spec}!/${entry}`); 707 708 let results = []; 709 for (let name of aomStartup.enumerateJARSubtree(uri)) { 710 if (!name.startsWith(entry)) { 711 throw new Error("Unexpected ZipReader entry"); 712 } 713 714 // The enumerator returns the full path of all entries. 715 // Trim off the leading path, and filter out entries from 716 // subdirectories. 717 name = name.slice(entry.length); 718 if ( 719 name && 720 !/\/./.test(name) && 721 (!directoriesOnly || name.endsWith("/")) 722 ) { 723 results.push(name.replace("/", "")); 724 } 725 } 726 727 return results; 728 } 729 730 readJSON(path) { 731 return new Promise((resolve, reject) => { 732 let uri = this.rootURI.resolve(`./${path}`); 733 734 NetUtil.asyncFetch( 735 { uri, loadUsingSystemPrincipal: true }, 736 (inputStream, status) => { 737 if (!Components.isSuccessCode(status)) { 738 // Convert status code to a string 739 let e = Components.Exception("", status); 740 reject(new Error(`Error while loading '${uri}' (${e.name})`)); 741 return; 742 } 743 try { 744 let text = NetUtil.readInputStreamToString( 745 inputStream, 746 inputStream.available(), 747 { charset: "utf-8" } 748 ); 749 750 text = text.replace(COMMENT_REGEXP, "$1"); 751 752 resolve(JSON.parse(text)); 753 } catch (e) { 754 reject(e); 755 } 756 } 757 ); 758 }); 759 } 760 761 get restrictSchemes() { 762 return !(this.isPrivileged && this.hasPermission("mozillaAddons")); 763 } 764 765 /** 766 * Given an array of host and permissions, generate a structured permissions object 767 * that contains seperate host origins and permissions arrays. 768 * 769 * @param {Array} permissionsArray 770 * @param {Array} [hostPermissions] 771 * @returns {Object} permissions object 772 */ 773 permissionsObject(permissionsArray = [], hostPermissions = []) { 774 let permissions = new Set(); 775 let origins = new Set(); 776 let { restrictSchemes, isPrivileged } = this; 777 778 for (let perm of permissionsArray.concat(hostPermissions)) { 779 let type = classifyPermission(perm, restrictSchemes, isPrivileged); 780 if (type.origin) { 781 origins.add(perm); 782 } else if (type.permission) { 783 permissions.add(perm); 784 } 785 } 786 787 return { 788 permissions, 789 origins, 790 }; 791 } 792 793 /** 794 * Returns an object representing any capabilities that the extension 795 * has access to based on fixed properties in the manifest. The result 796 * includes the contents of the "permissions" property as well as other 797 * capabilities that are derived from manifest fields that users should 798 * be informed of (e.g., origins where content scripts are injected). 799 */ 800 get manifestPermissions() { 801 if (this.type !== "extension") { 802 return null; 803 } 804 805 let { permissions, origins } = this.permissionsObject( 806 this.manifest.permissions, 807 this.manifest.host_permissions 808 ); 809 810 if ( 811 this.manifest.devtools_page && 812 !this.manifest.optional_permissions.includes("devtools") 813 ) { 814 permissions.add("devtools"); 815 } 816 817 for (let entry of this.manifest.content_scripts || []) { 818 for (let origin of entry.matches) { 819 origins.add(origin); 820 } 821 } 822 823 return { 824 permissions: Array.from(permissions), 825 origins: Array.from(origins), 826 }; 827 } 828 829 get manifestOptionalPermissions() { 830 if (this.type !== "extension") { 831 return null; 832 } 833 834 let { permissions, origins } = this.permissionsObject( 835 this.manifest.optional_permissions 836 ); 837 return { 838 permissions: Array.from(permissions), 839 origins: Array.from(origins), 840 }; 841 } 842 843 /** 844 * Returns an object representing all capabilities this extension has 845 * access to, including fixed ones from the manifest as well as dynamically 846 * granted permissions. 847 */ 848 get activePermissions() { 849 if (this.type !== "extension") { 850 return null; 851 } 852 853 let result = { 854 origins: this.allowedOrigins.patterns 855 .map(matcher => matcher.pattern) 856 // moz-extension://id/* is always added to allowedOrigins, but it 857 // is not a valid host permission in the API. So, remove it. 858 .filter(pattern => !pattern.startsWith("moz-extension:")), 859 apis: [...this.apiNames], 860 }; 861 862 const EXP_PATTERN = /^experiments\.\w+/; 863 result.permissions = [...this.permissions].filter( 864 p => !result.origins.includes(p) && !EXP_PATTERN.test(p) 865 ); 866 return result; 867 } 868 869 // Returns whether the front end should prompt for this permission 870 static async shouldPromptFor(permission) { 871 return !(await LAZY_NO_PROMPT_PERMISSIONS).has(permission); 872 } 873 874 // Compute the difference between two sets of permissions, suitable 875 // for presenting to the user. 876 static comparePermissions(oldPermissions, newPermissions) { 877 let oldMatcher = new MatchPatternSet(oldPermissions.origins, { 878 restrictSchemes: false, 879 }); 880 return { 881 // formatPermissionStrings ignores any scheme, so only look at the domain. 882 origins: newPermissions.origins.filter( 883 perm => 884 !oldMatcher.subsumesDomain( 885 new MatchPattern(perm, { restrictSchemes: false }) 886 ) 887 ), 888 permissions: newPermissions.permissions.filter( 889 perm => !oldPermissions.permissions.includes(perm) 890 ), 891 }; 892 } 893 894 // Return those permissions in oldPermissions that also exist in newPermissions. 895 static intersectPermissions(oldPermissions, newPermissions) { 896 let matcher = new MatchPatternSet(newPermissions.origins, { 897 restrictSchemes: false, 898 }); 899 900 return { 901 origins: oldPermissions.origins.filter(perm => 902 matcher.subsumesDomain( 903 new MatchPattern(perm, { restrictSchemes: false }) 904 ) 905 ), 906 permissions: oldPermissions.permissions.filter(perm => 907 newPermissions.permissions.includes(perm) 908 ), 909 }; 910 } 911 912 /** 913 * When updating the addon, find and migrate permissions that have moved from required 914 * to optional. This also handles any updates required for permission removal. 915 * 916 * @param {string} id The id of the addon being updated 917 * @param {Object} oldPermissions 918 * @param {Object} oldOptionalPermissions 919 * @param {Object} newPermissions 920 * @param {Object} newOptionalPermissions 921 */ 922 static async migratePermissions( 923 id, 924 oldPermissions, 925 oldOptionalPermissions, 926 newPermissions, 927 newOptionalPermissions 928 ) { 929 let migrated = ExtensionData.intersectPermissions( 930 oldPermissions, 931 newOptionalPermissions 932 ); 933 // If a permission is optional in this version and was mandatory in the previous 934 // version, it was already accepted by the user at install time so add it to the 935 // list of granted optional permissions now. 936 await ExtensionPermissions.add(id, migrated); 937 938 // Now we need to update ExtensionPreferencesManager, removing any settings 939 // for old permissions that no longer exist. 940 let permSet = new Set( 941 newPermissions.permissions.concat(newOptionalPermissions.permissions) 942 ); 943 let oldPerms = oldPermissions.permissions.concat( 944 oldOptionalPermissions.permissions 945 ); 946 947 let removed = oldPerms.filter(x => !permSet.has(x)); 948 // Force the removal here to ensure the settings are removed prior 949 // to startup. This will remove both required or optional permissions, 950 // whereas the call from within ExtensionPermissions would only result 951 // in a removal for optional permissions that were removed. 952 await ExtensionPreferencesManager.removeSettingsForPermissions(id, removed); 953 954 // Remove any optional permissions that have been removed from the manifest. 955 await ExtensionPermissions.remove(id, { 956 permissions: removed, 957 origins: [], 958 }); 959 } 960 961 canUseExperiment(manifest) { 962 return this.experimentsAllowed && manifest.experiment_apis; 963 } 964 965 get manifestVersion() { 966 return this.manifest.manifest_version; 967 } 968 969 get persistentBackground() { 970 let { manifest } = this; 971 if ( 972 !manifest.background || 973 manifest.background.service_worker || 974 this.manifestVersion > 2 975 ) { 976 return false; 977 } 978 // V2 addons can only use event pages if the pref is also flipped and 979 // persistent is explicilty set to false. 980 return !this.eventPagesEnabled || manifest.background.persistent; 981 } 982 983 async getExtensionVersionWithoutValidation() { 984 return (await this.readJSON("manifest.json")).version; 985 } 986 987 /** 988 * Load a locale and return a localized manifest. The extension must 989 * be initialized, and manifest parsed prior to calling. 990 * 991 * @param {string} locale to load, if necessary. 992 * @returns {object} normalized manifest. 993 */ 994 async getLocalizedManifest(locale) { 995 if (!this.type || !this.localeData) { 996 throw new Error("The extension has not been initialized."); 997 } 998 // Upon update or reinstall, the Extension.manifest may be read from 999 // StartupCache.manifest, however rawManifest is *not*. We need the 1000 // raw manifest in order to get a localized manifest. 1001 if (!this.rawManifest) { 1002 this.rawManifest = await this.readJSON("manifest.json"); 1003 } 1004 1005 if (!this.localeData.has(locale)) { 1006 // Locales are not avialable until some additional 1007 // initialization is done. We could just call initAllLocales, 1008 // but that is heavy handed, especially when we likely only 1009 // need one out of 20. 1010 let locales = await this.promiseLocales(); 1011 if (locales.get(locale)) { 1012 await this.initLocale(locale); 1013 } 1014 if (!this.localeData.has(locale)) { 1015 throw new Error(`The extension does not contain the locale ${locale}`); 1016 } 1017 } 1018 let normalized = await this._getNormalizedManifest(locale); 1019 if (normalized.error) { 1020 throw new Error(normalized.error); 1021 } 1022 return normalized.value; 1023 } 1024 1025 async _getNormalizedManifest(locale) { 1026 let manifestType = manifestTypes.get(this.type); 1027 1028 let context = { 1029 url: this.baseURI && this.baseURI.spec, 1030 principal: this.principal, 1031 logError: error => { 1032 this.manifestWarning(error); 1033 }, 1034 preprocessors: {}, 1035 manifestVersion: this.manifestVersion, 1036 }; 1037 1038 if (this.fluentL10n || this.localeData) { 1039 context.preprocessors.localize = (value, context) => 1040 this.localize(value, locale); 1041 } 1042 1043 return Schemas.normalize(this.rawManifest, manifestType, context); 1044 } 1045 1046 async initializeAddonTypeAndID() { 1047 if (this.type) { 1048 // Already initialized. 1049 return; 1050 } 1051 this.rawManifest = await this.readJSON("manifest.json"); 1052 let manifest = this.rawManifest; 1053 1054 if (manifest.theme) { 1055 this.type = "theme"; 1056 } else if (manifest.langpack_id) { 1057 // TODO bug 1757084: This should be "locale". 1058 this.type = "langpack"; 1059 } else if (manifest.dictionaries) { 1060 this.type = "dictionary"; 1061 } else if (manifest.site_permissions) { 1062 this.type = "sitepermission"; 1063 } else { 1064 this.type = "extension"; 1065 } 1066 1067 if (!this.id) { 1068 let bss = 1069 manifest.browser_specific_settings?.gecko || 1070 manifest.applications?.gecko; 1071 let id = bss?.id; 1072 // This is a basic type check. 1073 // When parseManifest is called, the ID is validated more thoroughly 1074 // because the id is defined to be an ExtensionID type in 1075 // toolkit/components/extensions/schemas/manifest.json 1076 if (typeof id == "string") { 1077 this.id = id; 1078 } 1079 } 1080 } 1081 1082 // eslint-disable-next-line complexity 1083 async parseManifest() { 1084 await Promise.all([this.initializeAddonTypeAndID(), Management.lazyInit()]); 1085 1086 let manifest = this.rawManifest; 1087 this.manifest = manifest; 1088 1089 if (manifest.default_locale) { 1090 await this.initLocale(); 1091 } 1092 1093 // When parsing the manifest from an ExtensionData instance, we don't 1094 // have isPrivileged, so ignore fluent localization in that pass. 1095 // This means that fluent cannot be used to localize manifest properties 1096 // read from the add-on manager (e.g., author, homepage, etc.) 1097 if (manifest.l10n_resources && this.constructor != ExtensionData) { 1098 if (this.isPrivileged) { 1099 this.fluentL10n = new Localization(manifest.l10n_resources, true); 1100 } else { 1101 // Warn but don't make this fatal. 1102 Cu.reportError("Ignoring l10n_resources in unprivileged extension"); 1103 } 1104 } 1105 1106 let normalized = await this._getNormalizedManifest(); 1107 if (normalized.error) { 1108 this.manifestError(normalized.error); 1109 return null; 1110 } 1111 1112 manifest = normalized.value; 1113 1114 // browser_specific_settings is documented, but most internal code is written 1115 // using applications. Use browser_specific_settings if it is in the manifest. If 1116 // both are set, we probably should make it an error, but we don't know if addons 1117 // in the wild have done that, so let the chips fall where they may. 1118 if (manifest.browser_specific_settings?.gecko) { 1119 if (manifest.applications) { 1120 this.manifestWarning( 1121 `"applications" property ignored and overridden by "browser_specific_settings"` 1122 ); 1123 } 1124 manifest.applications = manifest.browser_specific_settings; 1125 } 1126 1127 if ( 1128 this.manifestVersion < 3 && 1129 manifest.background && 1130 !this.eventPagesEnabled && 1131 !manifest.background.persistent 1132 ) { 1133 this.logWarning("Event pages are not currently supported."); 1134 } 1135 1136 let apiNames = new Set(); 1137 let dependencies = new Set(); 1138 let originPermissions = new Set(); 1139 let permissions = new Set(); 1140 let webAccessibleResources = []; 1141 1142 let schemaPromises = new Map(); 1143 1144 // Note: this.id and this.type were computed in initializeAddonTypeAndID. 1145 // The format of `this.id` was confirmed to be a valid extensionID by the 1146 // Schema validation as part of the _getNormalizedManifest() call. 1147 let result = { 1148 apiNames, 1149 dependencies, 1150 id: this.id, 1151 manifest, 1152 modules: null, 1153 originPermissions, 1154 permissions, 1155 schemaURLs: null, 1156 type: this.type, 1157 webAccessibleResources, 1158 }; 1159 1160 if (this.type === "extension") { 1161 let { isPrivileged } = this; 1162 let restrictSchemes = !( 1163 isPrivileged && manifest.permissions.includes("mozillaAddons") 1164 ); 1165 1166 let host_permissions = manifest.host_permissions ?? []; 1167 1168 for (let perm of manifest.permissions.concat(host_permissions)) { 1169 if (perm === "geckoProfiler" && !isPrivileged) { 1170 const acceptedExtensions = Services.prefs.getStringPref( 1171 "extensions.geckoProfiler.acceptedExtensionIds", 1172 "" 1173 ); 1174 if (!acceptedExtensions.split(",").includes(this.id)) { 1175 this.manifestError( 1176 "Only specific extensions are allowed to access the geckoProfiler." 1177 ); 1178 continue; 1179 } 1180 } 1181 1182 let type = classifyPermission(perm, restrictSchemes, isPrivileged); 1183 if (type.origin) { 1184 perm = type.origin; 1185 originPermissions.add(perm); 1186 } else if (type.api) { 1187 apiNames.add(type.api); 1188 } else if (type.invalid) { 1189 this.manifestWarning(`Invalid extension permission: ${perm}`); 1190 continue; 1191 } 1192 1193 // Unfortunately, we treat <all_urls> as an API permission as well. 1194 if (!type.origin || perm === "<all_urls>") { 1195 permissions.add(perm); 1196 } 1197 } 1198 1199 if (this.id) { 1200 // An extension always gets permission to its own url. 1201 let matcher = new MatchPattern(this.getURL(), { ignorePath: true }); 1202 originPermissions.add(matcher.pattern); 1203 1204 // Apply optional permissions 1205 let perms = await ExtensionPermissions.get(this.id); 1206 for (let perm of perms.permissions) { 1207 permissions.add(perm); 1208 } 1209 for (let origin of perms.origins) { 1210 originPermissions.add(origin); 1211 } 1212 } 1213 1214 for (let api of apiNames) { 1215 dependencies.add(`${api}@experiments.addons.mozilla.org`); 1216 } 1217 1218 let moduleData = data => ({ 1219 url: this.rootURI.resolve(data.script), 1220 events: data.events, 1221 paths: data.paths, 1222 scopes: data.scopes, 1223 }); 1224 1225 let computeModuleInit = (scope, modules) => { 1226 let manager = new ExtensionCommon.SchemaAPIManager(scope); 1227 return manager.initModuleJSON([modules]); 1228 }; 1229 1230 result.contentScripts = []; 1231 for (let options of manifest.content_scripts || []) { 1232 result.contentScripts.push({ 1233 allFrames: options.all_frames, 1234 matchAboutBlank: options.match_about_blank, 1235 frameID: options.frame_id, 1236 runAt: options.run_at, 1237 1238 matches: options.matches, 1239 excludeMatches: options.exclude_matches || [], 1240 includeGlobs: options.include_globs, 1241 excludeGlobs: options.exclude_globs, 1242 1243 jsPaths: options.js || [], 1244 cssPaths: options.css || [], 1245 }); 1246 } 1247 1248 if (this.canUseExperiment(manifest)) { 1249 let parentModules = {}; 1250 let childModules = {}; 1251 1252 for (let [name, data] of Object.entries(manifest.experiment_apis)) { 1253 let schema = this.getURL(data.schema); 1254 1255 if (!schemaPromises.has(schema)) { 1256 schemaPromises.set( 1257 schema, 1258 this.readJSON(data.schema).then(json => 1259 Schemas.processSchema(json) 1260 ) 1261 ); 1262 } 1263 1264 if (data.parent) { 1265 parentModules[name] = moduleData(data.parent); 1266 } 1267 1268 if (data.child) { 1269 childModules[name] = moduleData(data.child); 1270 } 1271 } 1272 1273 result.modules = { 1274 child: computeModuleInit("addon_child", childModules), 1275 parent: computeModuleInit("addon_parent", parentModules), 1276 }; 1277 } 1278 1279 // Normalize all patterns to contain a single leading / 1280 if (manifest.web_accessible_resources) { 1281 // Normalize into V3 objects 1282 let wac = 1283 this.manifestVersion >= 3 1284 ? manifest.web_accessible_resources 1285 : [{ resources: manifest.web_accessible_resources }]; 1286 webAccessibleResources.push( 1287 ...wac.map(obj => { 1288 obj.resources = obj.resources.map(path => 1289 path.replace(/^\/*/, "/") 1290 ); 1291 return obj; 1292 }) 1293 ); 1294 } 1295 } else if (this.type == "langpack") { 1296 // Langpack startup is performance critical, so we want to compute as much 1297 // as possible here to make startup not trigger async DB reads. 1298 // We'll store the four items below in the startupData. 1299 1300 // 1. Compute the chrome resources to be registered for this langpack. 1301 const platform = AppConstants.platform; 1302 const chromeEntries = []; 1303 for (const [language, entry] of Object.entries(manifest.languages)) { 1304 for (const [alias, path] of Object.entries( 1305 entry.chrome_resources || {} 1306 )) { 1307 if (typeof path === "string") { 1308 chromeEntries.push(["locale", alias, language, path]); 1309 } else if (platform in path) { 1310 // If the path is not a string, it's an object with path per 1311 // platform where the keys are taken from AppConstants.platform 1312 chromeEntries.push(["locale", alias, language, path[platform]]); 1313 } 1314 } 1315 } 1316 1317 // 2. Compute langpack ID. 1318 const productCodeName = AppConstants.MOZ_BUILD_APP.replace("/", "-"); 1319 1320 // The result path looks like this: 1321 // Firefox - `langpack-pl-browser` 1322 // Fennec - `langpack-pl-mobile-android` 1323 const langpackId = `langpack-${manifest.langpack_id}-${productCodeName}`; 1324 1325 // 3. Compute L10nRegistry sources for this langpack. 1326 const l10nRegistrySources = {}; 1327 1328 // Check if there's a root directory `/localization` in the langpack. 1329 // If there is one, add it with the name `toolkit` as a FileSource. 1330 const entries = await this._readDirectory("localization"); 1331 if (entries.length) { 1332 l10nRegistrySources.toolkit = ""; 1333 } 1334 1335 // Add any additional sources listed in the manifest 1336 if (manifest.sources) { 1337 for (const [sourceName, { base_path }] of Object.entries( 1338 manifest.sources 1339 )) { 1340 l10nRegistrySources[sourceName] = base_path; 1341 } 1342 } 1343 1344 // 4. Save the list of languages handled by this langpack. 1345 const languages = Object.keys(manifest.languages); 1346 1347 this.startupData = { 1348 chromeEntries, 1349 langpackId, 1350 l10nRegistrySources, 1351 languages, 1352 }; 1353 } else if (this.type == "dictionary") { 1354 let dictionaries = {}; 1355 for (let [lang, path] of Object.entries(manifest.dictionaries)) { 1356 path = path.replace(/^\/+/, ""); 1357 1358 let dir = dirname(path); 1359 if (dir === ".") { 1360 dir = ""; 1361 } 1362 let leafName = basename(path); 1363 let affixPath = leafName.slice(0, -3) + "aff"; 1364 1365 let entries = await this._readDirectory(dir); 1366 if (!entries.includes(leafName)) { 1367 this.manifestError( 1368 `Invalid dictionary path specified for '${lang}': ${path}` 1369 ); 1370 } 1371 if (!entries.includes(affixPath)) { 1372 this.manifestError( 1373 `Invalid dictionary path specified for '${lang}': Missing affix file: ${path}` 1374 ); 1375 } 1376 1377 dictionaries[lang] = path; 1378 } 1379 1380 this.startupData = { dictionaries }; 1381 } 1382 1383 if (schemaPromises.size) { 1384 let schemas = new Map(); 1385 for (let [url, promise] of schemaPromises) { 1386 schemas.set(url, await promise); 1387 } 1388 result.schemaURLs = schemas; 1389 } 1390 1391 return result; 1392 } 1393 1394 // Reads the extension's |manifest.json| file, and stores its 1395 // parsed contents in |this.manifest|. 1396 async loadManifest() { 1397 let [manifestData] = await Promise.all([ 1398 this.parseManifest(), 1399 Management.lazyInit(), 1400 ]); 1401 1402 if (!manifestData) { 1403 return; 1404 } 1405 1406 // Do not override the add-on id that has been already assigned. 1407 if (!this.id) { 1408 this.id = manifestData.id; 1409 } 1410 1411 this.manifest = manifestData.manifest; 1412 this.apiNames = manifestData.apiNames; 1413 this.contentScripts = manifestData.contentScripts; 1414 this.dependencies = manifestData.dependencies; 1415 this.permissions = manifestData.permissions; 1416 this.schemaURLs = manifestData.schemaURLs; 1417 this.type = manifestData.type; 1418 1419 this.modules = manifestData.modules; 1420 1421 this.apiManager = this.getAPIManager(); 1422 await this.apiManager.lazyInit(); 1423 1424 this.webAccessibleResources = manifestData.webAccessibleResources; 1425 1426 this.allowedOrigins = new MatchPatternSet(manifestData.originPermissions, { 1427 restrictSchemes: this.restrictSchemes, 1428 }); 1429 1430 return this.manifest; 1431 } 1432 1433 hasPermission(perm, includeOptional = false) { 1434 // If the permission is a "manifest property" permission, we check if the extension 1435 // does have the required property in its manifest. 1436 let manifest_ = "manifest:"; 1437 if (perm.startsWith(manifest_)) { 1438 // Handle nested "manifest property" permission (e.g. as in "manifest:property.nested"). 1439 let value = this.manifest; 1440 for (let prop of perm.substr(manifest_.length).split(".")) { 1441 if (!value) { 1442 break; 1443 } 1444 value = value[prop]; 1445 } 1446 1447 return value != null; 1448 } 1449 1450 if (this.permissions.has(perm)) { 1451 return true; 1452 } 1453 1454 if (includeOptional && this.manifest.optional_permissions.includes(perm)) { 1455 return true; 1456 } 1457 1458 return false; 1459 } 1460 1461 getAPIManager() { 1462 let apiManagers = [Management]; 1463 1464 for (let id of this.dependencies) { 1465 let policy = WebExtensionPolicy.getByID(id); 1466 if (policy) { 1467 if (policy.extension.experimentAPIManager) { 1468 apiManagers.push(policy.extension.experimentAPIManager); 1469 } else if (AppConstants.DEBUG) { 1470 Cu.reportError(`Cannot find experimental API exported from ${id}`); 1471 } 1472 } 1473 } 1474 1475 if (this.modules) { 1476 this.experimentAPIManager = new ExtensionCommon.LazyAPIManager( 1477 "main", 1478 this.modules.parent, 1479 this.schemaURLs 1480 ); 1481 1482 apiManagers.push(this.experimentAPIManager); 1483 } 1484 1485 if (apiManagers.length == 1) { 1486 return apiManagers[0]; 1487 } 1488 1489 return new ExtensionCommon.MultiAPIManager("main", apiManagers.reverse()); 1490 } 1491 1492 localizeMessage(...args) { 1493 return this.localeData.localizeMessage(...args); 1494 } 1495 1496 localize(str, locale) { 1497 // If the extension declares fluent resources in the manifest, try 1498 // first to localize with fluent. Also use the original webextension 1499 // method (_locales/xx.json) so extensions can migrate bit by bit. 1500 // Note also that fluent keys typically use hyphense, so hyphens are 1501 // allowed in the __MSG_foo__ keys used by fluent, though they are 1502 // not allowed in the keys used for json translations. 1503 if (this.fluentL10n) { 1504 str = str.replace(/__MSG_([-A-Za-z0-9@_]+?)__/g, (matched, message) => { 1505 let translation = this.fluentL10n.formatValueSync(message); 1506 return translation !== undefined ? translation : matched; 1507 }); 1508 } 1509 if (this.localeData) { 1510 str = this.localeData.localize(str, locale); 1511 } 1512 return str; 1513 } 1514 1515 // If a "default_locale" is specified in that manifest, returns it 1516 // as a Gecko-compatible locale string. Otherwise, returns null. 1517 get defaultLocale() { 1518 if (this.manifest.default_locale != null) { 1519 return this.normalizeLocaleCode(this.manifest.default_locale); 1520 } 1521 1522 return null; 1523 } 1524 1525 // Returns true if an addon is builtin to Firefox or 1526 // distributed via Normandy into a system location. 1527 get isAppProvided() { 1528 return this.addonData.builtIn || this.addonData.isSystem; 1529 } 1530 1531 // Normalizes a Chrome-compatible locale code to the appropriate 1532 // Gecko-compatible variant. Currently, this means simply 1533 // replacing underscores with hyphens. 1534 normalizeLocaleCode(locale) { 1535 return locale.replace(/_/g, "-"); 1536 } 1537 1538 // Reads the locale file for the given Gecko-compatible locale code, and 1539 // stores its parsed contents in |this.localeMessages.get(locale)|. 1540 async readLocaleFile(locale) { 1541 let locales = await this.promiseLocales(); 1542 let dir = locales.get(locale) || locale; 1543 let file = `_locales/${dir}/messages.json`; 1544 1545 try { 1546 let messages = await this.readJSON(file); 1547 return this.localeData.addLocale(locale, messages, this); 1548 } catch (e) { 1549 this.packagingError(`Loading locale file ${file}: ${e}`); 1550 return new Map(); 1551 } 1552 } 1553 1554 async _promiseLocaleMap() { 1555 let locales = new Map(); 1556 1557 let entries = await this._readDirectory("_locales", true); 1558 for (let name of entries) { 1559 let locale = this.normalizeLocaleCode(name); 1560 locales.set(locale, name); 1561 } 1562 1563 return locales; 1564 } 1565 1566 _setupLocaleData(locales) { 1567 if (this.localeData) { 1568 return this.localeData.locales; 1569 } 1570 1571 this.localeData = new LocaleData({ 1572 defaultLocale: this.defaultLocale, 1573 locales, 1574 builtinMessages: this.builtinMessages, 1575 }); 1576 1577 return locales; 1578 } 1579 1580 // Reads the list of locales available in the extension, and returns a 1581 // Promise which resolves to a Map upon completion. 1582 // Each map key is a Gecko-compatible locale code, and each value is the 1583 // "_locales" subdirectory containing that locale: 1584 // 1585 // Map(gecko-locale-code -> locale-directory-name) 1586 promiseLocales() { 1587 if (!this._promiseLocales) { 1588 this._promiseLocales = (async () => { 1589 let locales = this._promiseLocaleMap(); 1590 return this._setupLocaleData(locales); 1591 })(); 1592 } 1593 1594 return this._promiseLocales; 1595 } 1596 1597 // Reads the locale messages for all locales, and returns a promise which 1598 // resolves to a Map of locale messages upon completion. Each key in the map 1599 // is a Gecko-compatible locale code, and each value is a locale data object 1600 // as returned by |readLocaleFile|. 1601 async initAllLocales() { 1602 let locales = await this.promiseLocales(); 1603 1604 await Promise.all( 1605 Array.from(locales.keys(), locale => this.readLocaleFile(locale)) 1606 ); 1607 1608 let defaultLocale = this.defaultLocale; 1609 if (defaultLocale) { 1610 if (!locales.has(defaultLocale)) { 1611 this.manifestError( 1612 'Value for "default_locale" property must correspond to ' + 1613 'a directory in "_locales/". Not found: ' + 1614 JSON.stringify(`_locales/${this.manifest.default_locale}/`) 1615 ); 1616 } 1617 } else if (locales.size) { 1618 this.manifestError( 1619 'The "default_locale" property is required when a ' + 1620 '"_locales/" directory is present.' 1621 ); 1622 } 1623 1624 return this.localeData.messages; 1625 } 1626 1627 // Reads the locale file for the given Gecko-compatible locale code, or the 1628 // default locale if no locale code is given, and sets it as the currently 1629 // selected locale on success. 1630 // 1631 // Pre-loads the default locale for fallback message processing, regardless 1632 // of the locale specified. 1633 // 1634 // If no locales are unavailable, resolves to |null|. 1635 async initLocale(locale = this.defaultLocale) { 1636 if (locale == null) { 1637 return null; 1638 } 1639 1640 let promises = [this.readLocaleFile(locale)]; 1641 1642 let { defaultLocale } = this; 1643 if (locale != defaultLocale && !this.localeData.has(defaultLocale)) { 1644 promises.push(this.readLocaleFile(defaultLocale)); 1645 } 1646 1647 let results = await Promise.all(promises); 1648 1649 this.localeData.selectedLocale = locale; 1650 return results[0]; 1651 } 1652 1653 /** 1654 * Classify host permissions 1655 * @param {array<string>} origins 1656 * permission origins 1657 * @returns {object} 1658 * "object.allUrls" contains the permission used to obtain all urls access 1659 * "object.wildcards" set contains permissions with wildcards 1660 * "object.sites" set contains explicit host permissions 1661 */ 1662 static classifyOriginPermissions(origins = []) { 1663 let allUrls = null, 1664 wildcards = new Set(), 1665 sites = new Set(); 1666 for (let permission of origins) { 1667 if (permission == "<all_urls>") { 1668 allUrls = permission; 1669 break; 1670 } 1671 1672 // Privileged extensions may request access to "about:"-URLs, such as 1673 // about:reader. 1674 let match = /^[a-z*]+:\/\/([^/]*)\/|^about:/.exec(permission); 1675 if (!match) { 1676 throw new Error(`Unparseable host permission ${permission}`); 1677 } 1678 // Note: the scheme is ignored in the permission warnings. If this ever 1679 // changes, update the comparePermissions method as needed. 1680 if (!match[1] || match[1] == "*") { 1681 allUrls = permission; 1682 } else if (match[1].startsWith("*.")) { 1683 wildcards.add(match[1].slice(2)); 1684 } else { 1685 sites.add(match[1]); 1686 } 1687 } 1688 return { allUrls, wildcards, sites }; 1689 } 1690 1691 /** 1692 * Formats all the strings for a permissions dialog/notification. 1693 * 1694 * @param {object} info Information about the permissions being requested. 1695 * 1696 * @param {array<string>} info.permissions.origins 1697 * Origin permissions requested. 1698 * @param {array<string>} info.permissions.permissions 1699 * Regular (non-origin) permissions requested. 1700 * @param {array<string>} info.optionalPermissions.origins 1701 * Optional origin permissions listed in the manifest. 1702 * @param {array<string>} info.optionalPermissions.permissions 1703 * Optional (non-origin) permissions listed in the manifest. 1704 * @param {boolean} info.unsigned 1705 * True if the prompt is for installing an unsigned addon. 1706 * @param {string} info.type 1707 * The type of prompt being shown. May be one of "update", 1708 * "sideload", "optional", or omitted for a regular 1709 * install prompt. 1710 * @param {string} info.appName 1711 * The localized name of the application, to be substituted 1712 * in computed strings as needed. 1713 * @param {nsIStringBundle} bundle 1714 * The string bundle to use for l10n. 1715 * @param {object} options 1716 * @param {boolean} options.collapseOrigins 1717 * Wether to limit the number of displayed host permissions. 1718 * Default is false. 1719 * @param {function} options.getKeyForPermission 1720 * An optional callback function that returns the locale key for a given 1721 * permission name (set by default to a callback returning the locale 1722 * key following the default convention `webextPerms.description.PERMNAME`). 1723 * Overriding the default mapping can become necessary, when a permission 1724 * description needs to be modified and a non-default locale key has to be 1725 * used. There is at least one non-default locale key used in Thunderbird. 1726 * 1727 * @returns {object} An object with properties containing localized strings 1728 * for various elements of a permission dialog. The "header" 1729 * property on this object is the notification header text 1730 * and it has the string "<>" as a placeholder for the 1731 * addon name. 1732 * 1733 * "object.msgs" is an array of localized strings describing required permissions 1734 * 1735 * "object.optionalPermissions" is a map of permission name to localized 1736 * strings describing the permission. 1737 * 1738 * "object.optionalOrigins" is a map of a host permission to localized strings 1739 * describing the host permission, where appropriate. Currently only 1740 * all url style permissions are included. 1741 */ 1742 static formatPermissionStrings( 1743 info, 1744 bundle, 1745 { 1746 collapseOrigins = false, 1747 getKeyForPermission = perm => `webextPerms.description.${perm}`, 1748 } = {} 1749 ) { 1750 let result = { 1751 msgs: [], 1752 optionalPermissions: {}, 1753 optionalOrigins: {}, 1754 }; 1755 1756 const haveAccessKeys = AppConstants.platform !== "android"; 1757 1758 let headerKey; 1759 result.text = ""; 1760 result.listIntro = ""; 1761 result.acceptText = bundle.GetStringFromName("webextPerms.add.label"); 1762 result.cancelText = bundle.GetStringFromName("webextPerms.cancel.label"); 1763 if (haveAccessKeys) { 1764 result.acceptKey = bundle.GetStringFromName("webextPerms.add.accessKey"); 1765 result.cancelKey = bundle.GetStringFromName( 1766 "webextPerms.cancel.accessKey" 1767 ); 1768 } 1769 1770 // Generate a map of site_permission names to permission strings for site 1771 // permissions. Since SitePermission addons cannot have regular permissions, 1772 // we reuse msgs to pass the strings to the permissions panel. 1773 if (info.sitePermissions) { 1774 for (let permission of info.sitePermissions) { 1775 try { 1776 result.msgs.push( 1777 bundle.GetStringFromName( 1778 `webextSitePerms.description.${permission}` 1779 ) 1780 ); 1781 } catch (err) { 1782 Cu.reportError( 1783 `site_permission ${permission} missing readable text property` 1784 ); 1785 // We must never have a DOM api permission that is hidden so in 1786 // the case of any error, we'll use the plain permission string. 1787 // test_ext_sitepermissions.js tests for no missing messages, this 1788 // is just an extra fallback. 1789 result.msgs.push(permission); 1790 } 1791 } 1792 1793 // Generate header message 1794 headerKey = info.unsigned 1795 ? "webextSitePerms.headerUnsignedWithPerms" 1796 : "webextSitePerms.headerWithPerms"; 1797 // We simplify the origin to make it more user friendly. The origin is 1798 // assured to be available via schema requirement. 1799 result.header = bundle.formatStringFromName(headerKey, [ 1800 "<>", 1801 new URL(info.siteOrigin).hostname, 1802 ]); 1803 return result; 1804 } 1805 1806 let perms = info.permissions || { origins: [], permissions: [] }; 1807 let optional_permissions = info.optionalPermissions || { 1808 origins: [], 1809 permissions: [], 1810 }; 1811 1812 // First classify our host permissions 1813 let { allUrls, wildcards, sites } = ExtensionData.classifyOriginPermissions( 1814 perms.origins 1815 ); 1816 1817 // Format the host permissions. If we have a wildcard for all urls, 1818 // a single string will suffice. Otherwise, show domain wildcards 1819 // first, then individual host permissions. 1820 if (allUrls) { 1821 result.msgs.push( 1822 bundle.GetStringFromName("webextPerms.hostDescription.allUrls") 1823 ); 1824 } else { 1825 // Formats a list of host permissions. If we have 4 or fewer, display 1826 // them all, otherwise display the first 3 followed by an item that 1827 // says "...plus N others" 1828 let format = (list, itemKey, moreKey) => { 1829 function formatItems(items) { 1830 result.msgs.push( 1831 ...items.map(item => bundle.formatStringFromName(itemKey, [item])) 1832 ); 1833 } 1834 if (list.length < 5 || !collapseOrigins) { 1835 formatItems(list); 1836 } else { 1837 formatItems(list.slice(0, 3)); 1838 1839 let remaining = list.length - 3; 1840 result.msgs.push( 1841 PluralForm.get( 1842 remaining, 1843 bundle.GetStringFromName(moreKey) 1844 ).replace("#1", remaining) 1845 ); 1846 } 1847 }; 1848 1849 format( 1850 Array.from(wildcards), 1851 "webextPerms.hostDescription.wildcard", 1852 "webextPerms.hostDescription.tooManyWildcards" 1853 ); 1854 format( 1855 Array.from(sites), 1856 "webextPerms.hostDescription.oneSite", 1857 "webextPerms.hostDescription.tooManySites" 1858 ); 1859 } 1860 1861 // Next, show the native messaging permission if it is present. 1862 const NATIVE_MSG_PERM = "nativeMessaging"; 1863 if (perms.permissions.includes(NATIVE_MSG_PERM)) { 1864 result.msgs.push( 1865 bundle.formatStringFromName(getKeyForPermission(NATIVE_MSG_PERM), [ 1866 info.appName, 1867 ]) 1868 ); 1869 } 1870 1871 // Finally, show remaining permissions, in the same order as AMO. 1872 // The permissions are sorted alphabetically by the permission 1873 // string to match AMO. 1874 let permissionsCopy = perms.permissions.slice(0); 1875 for (let permission of permissionsCopy.sort()) { 1876 // Handled above 1877 if (permission == NATIVE_MSG_PERM) { 1878 continue; 1879 } 1880 try { 1881 result.msgs.push( 1882 bundle.GetStringFromName(getKeyForPermission(permission)) 1883 ); 1884 } catch (err) { 1885 // We deliberately do not include all permissions in the prompt. 1886 // So if we don't find one then just skip it. 1887 } 1888 } 1889 1890 // Generate a map of permission names to permission strings for optional 1891 // permissions. The key is necessary to handle toggling those permissions. 1892 for (let permission of optional_permissions.permissions) { 1893 if (permission == NATIVE_MSG_PERM) { 1894 result.optionalPermissions[ 1895 permission 1896 ] = bundle.formatStringFromName(getKeyForPermission(permission), [ 1897 info.appName, 1898 ]); 1899 continue; 1900 } 1901 try { 1902 result.optionalPermissions[permission] = bundle.GetStringFromName( 1903 getKeyForPermission(permission) 1904 ); 1905 } catch (err) { 1906 // We deliberately do not have strings for all permissions. 1907 // So if we don't find one then just skip it. 1908 } 1909 } 1910 allUrls = ExtensionData.classifyOriginPermissions( 1911 optional_permissions.origins 1912 ).allUrls; 1913 if (allUrls) { 1914 result.optionalOrigins[allUrls] = bundle.GetStringFromName( 1915 "webextPerms.hostDescription.allUrls" 1916 ); 1917 } 1918 1919 if (info.type == "sideload") { 1920 headerKey = "webextPerms.sideloadHeader"; 1921 let key = !result.msgs.length 1922 ? "webextPerms.sideloadTextNoPerms" 1923 : "webextPerms.sideloadText2"; 1924 result.text = bundle.GetStringFromName(key); 1925 result.acceptText = bundle.GetStringFromName( 1926 "webextPerms.sideloadEnable.label" 1927 ); 1928 result.cancelText = bundle.GetStringFromName( 1929 "webextPerms.sideloadCancel.label" 1930 ); 1931 if (haveAccessKeys) { 1932 result.acceptKey = bundle.GetStringFromName( 1933 "webextPerms.sideloadEnable.accessKey" 1934 ); 1935 result.cancelKey = bundle.GetStringFromName( 1936 "webextPerms.sideloadCancel.accessKey" 1937 ); 1938 } 1939 } else if (info.type == "update") { 1940 headerKey = "webextPerms.updateText2"; 1941 result.text = ""; 1942 result.acceptText = bundle.GetStringFromName( 1943 "webextPerms.updateAccept.label" 1944 ); 1945 if (haveAccessKeys) { 1946 result.acceptKey = bundle.GetStringFromName( 1947 "webextPerms.updateAccept.accessKey" 1948 ); 1949 } 1950 } else if (info.type == "optional") { 1951 headerKey = "webextPerms.optionalPermsHeader"; 1952 result.text = ""; 1953 result.listIntro = bundle.GetStringFromName( 1954 "webextPerms.optionalPermsListIntro" 1955 ); 1956 result.acceptText = bundle.GetStringFromName( 1957 "webextPerms.optionalPermsAllow.label" 1958 ); 1959 result.cancelText = bundle.GetStringFromName( 1960 "webextPerms.optionalPermsDeny.label" 1961 ); 1962 if (haveAccessKeys) { 1963 result.acceptKey = bundle.GetStringFromName( 1964 "webextPerms.optionalPermsAllow.accessKey" 1965 ); 1966 result.cancelKey = bundle.GetStringFromName( 1967 "webextPerms.optionalPermsDeny.accessKey" 1968 ); 1969 } 1970 } else { 1971 headerKey = "webextPerms.header"; 1972 if (result.msgs.length) { 1973 headerKey = info.unsigned 1974 ? "webextPerms.headerUnsignedWithPerms" 1975 : "webextPerms.headerWithPerms"; 1976 } else if (info.unsigned) { 1977 headerKey = "webextPerms.headerUnsigned"; 1978 } 1979 } 1980 result.header = bundle.formatStringFromName(headerKey, ["<>"]); 1981 return result; 1982 } 1983} 1984 1985const PROXIED_EVENTS = new Set([ 1986 "test-harness-message", 1987 "add-permissions", 1988 "remove-permissions", 1989]); 1990 1991class BootstrapScope { 1992 install(data, reason) {} 1993 uninstall(data, reason) { 1994 AsyncShutdown.profileChangeTeardown.addBlocker( 1995 `Uninstalling add-on: ${data.id}`, 1996 Management.emit("uninstall", { id: data.id }).then(() => { 1997 Management.emit("uninstall-complete", { id: data.id }); 1998 }) 1999 ); 2000 } 2001 2002 fetchState() { 2003 if (this.extension) { 2004 return { state: this.extension.state }; 2005 } 2006 return null; 2007 } 2008 2009 async update(data, reason) { 2010 // Retain any previously granted permissions that may have migrated 2011 // into the optional list. 2012 if (data.oldPermissions) { 2013 // New permissions may be null, ensure we have an empty 2014 // permission set in that case. 2015 let emptyPermissions = { permissions: [], origins: [] }; 2016 await ExtensionData.migratePermissions( 2017 data.id, 2018 data.oldPermissions, 2019 data.oldOptionalPermissions, 2020 data.userPermissions || emptyPermissions, 2021 data.optionalPermissions || emptyPermissions 2022 ); 2023 } 2024 2025 return Management.emit("update", { 2026 id: data.id, 2027 resourceURI: data.resourceURI, 2028 isPrivileged: data.isPrivileged, 2029 }); 2030 } 2031 2032 startup(data, reason) { 2033 // eslint-disable-next-line no-use-before-define 2034 this.extension = new Extension( 2035 data, 2036 this.BOOTSTRAP_REASON_TO_STRING_MAP[reason] 2037 ); 2038 return this.extension.startup(); 2039 } 2040 2041 async shutdown(data, reason) { 2042 let result = await this.extension.shutdown( 2043 this.BOOTSTRAP_REASON_TO_STRING_MAP[reason] 2044 ); 2045 this.extension = null; 2046 return result; 2047 } 2048} 2049 2050XPCOMUtils.defineLazyGetter( 2051 BootstrapScope.prototype, 2052 "BOOTSTRAP_REASON_TO_STRING_MAP", 2053 () => { 2054 const { BOOTSTRAP_REASONS } = AddonManagerPrivate; 2055 2056 return Object.freeze({ 2057 [BOOTSTRAP_REASONS.APP_STARTUP]: "APP_STARTUP", 2058 [BOOTSTRAP_REASONS.APP_SHUTDOWN]: "APP_SHUTDOWN", 2059 [BOOTSTRAP_REASONS.ADDON_ENABLE]: "ADDON_ENABLE", 2060 [BOOTSTRAP_REASONS.ADDON_DISABLE]: "ADDON_DISABLE", 2061 [BOOTSTRAP_REASONS.ADDON_INSTALL]: "ADDON_INSTALL", 2062 [BOOTSTRAP_REASONS.ADDON_UNINSTALL]: "ADDON_UNINSTALL", 2063 [BOOTSTRAP_REASONS.ADDON_UPGRADE]: "ADDON_UPGRADE", 2064 [BOOTSTRAP_REASONS.ADDON_DOWNGRADE]: "ADDON_DOWNGRADE", 2065 }); 2066 } 2067); 2068 2069class DictionaryBootstrapScope extends BootstrapScope { 2070 install(data, reason) {} 2071 uninstall(data, reason) {} 2072 2073 startup(data, reason) { 2074 // eslint-disable-next-line no-use-before-define 2075 this.dictionary = new Dictionary(data); 2076 return this.dictionary.startup(this.BOOTSTRAP_REASON_TO_STRING_MAP[reason]); 2077 } 2078 2079 shutdown(data, reason) { 2080 this.dictionary.shutdown(this.BOOTSTRAP_REASON_TO_STRING_MAP[reason]); 2081 this.dictionary = null; 2082 } 2083} 2084 2085class LangpackBootstrapScope extends BootstrapScope { 2086 install(data, reason) {} 2087 uninstall(data, reason) {} 2088 update(data, reason) {} 2089 2090 startup(data, reason) { 2091 // eslint-disable-next-line no-use-before-define 2092 this.langpack = new Langpack(data); 2093 return this.langpack.startup(this.BOOTSTRAP_REASON_TO_STRING_MAP[reason]); 2094 } 2095 2096 shutdown(data, reason) { 2097 this.langpack.shutdown(this.BOOTSTRAP_REASON_TO_STRING_MAP[reason]); 2098 this.langpack = null; 2099 } 2100} 2101 2102class SitePermissionBootstrapScope extends BootstrapScope { 2103 install(data, reason) {} 2104 uninstall(data, reason) {} 2105 2106 startup(data, reason) { 2107 // eslint-disable-next-line no-use-before-define 2108 this.sitepermission = new SitePermission(data); 2109 return this.sitepermission.startup( 2110 this.BOOTSTRAP_REASON_TO_STRING_MAP[reason] 2111 ); 2112 } 2113 2114 shutdown(data, reason) { 2115 this.sitepermission.shutdown(this.BOOTSTRAP_REASON_TO_STRING_MAP[reason]); 2116 this.sitepermission = null; 2117 } 2118} 2119 2120let activeExtensionIDs = new Set(); 2121 2122let pendingExtensions = new Map(); 2123 2124/** 2125 * This class is the main representation of an active WebExtension 2126 * in the main process. 2127 * @extends ExtensionData 2128 */ 2129class Extension extends ExtensionData { 2130 constructor(addonData, startupReason) { 2131 super(addonData.resourceURI, addonData.isPrivileged); 2132 2133 this.startupStates = new Set(); 2134 this.state = "Not started"; 2135 this.userContextIsolation = userContextIsolation; 2136 2137 this.sharedDataKeys = new Set(); 2138 2139 this.uuid = UUIDMap.get(addonData.id); 2140 this.instanceId = getUniqueId(); 2141 2142 this.MESSAGE_EMIT_EVENT = `Extension:EmitEvent:${this.instanceId}`; 2143 Services.ppmm.addMessageListener(this.MESSAGE_EMIT_EVENT, this); 2144 2145 if (addonData.cleanupFile) { 2146 Services.obs.addObserver(this, "xpcom-shutdown"); 2147 this.cleanupFile = addonData.cleanupFile || null; 2148 delete addonData.cleanupFile; 2149 } 2150 2151 if (addonData.TEST_NO_ADDON_MANAGER) { 2152 this.dontSaveStartupData = true; 2153 } 2154 if (addonData.TEST_NO_DELAYED_STARTUP) { 2155 this.testNoDelayedStartup = true; 2156 } 2157 2158 this.addonData = addonData; 2159 this.startupData = addonData.startupData || {}; 2160 this.startupReason = startupReason; 2161 2162 if (["ADDON_UPGRADE", "ADDON_DOWNGRADE"].includes(startupReason)) { 2163 StartupCache.clearAddonData(addonData.id); 2164 } 2165 2166 this.remote = !WebExtensionPolicy.isExtensionProcess; 2167 this.remoteType = this.remote ? E10SUtils.EXTENSION_REMOTE_TYPE : null; 2168 2169 if (this.remote && processCount !== 1) { 2170 throw new Error( 2171 "Out-of-process WebExtensions are not supported with multiple child processes" 2172 ); 2173 } 2174 2175 // This is filled in the first time an extension child is created. 2176 this.parentMessageManager = null; 2177 2178 this.id = addonData.id; 2179 this.version = addonData.version; 2180 this.baseURL = this.getURL(""); 2181 this.baseURI = Services.io.newURI(this.baseURL).QueryInterface(Ci.nsIURL); 2182 this.principal = this.createPrincipal(); 2183 2184 this.views = new Set(); 2185 this._backgroundPageFrameLoader = null; 2186 2187 this.onStartup = null; 2188 2189 this.hasShutdown = false; 2190 this.onShutdown = new Set(); 2191 2192 this.uninstallURL = null; 2193 2194 this.allowedOrigins = null; 2195 this._optionalOrigins = null; 2196 this.webAccessibleResources = null; 2197 2198 this.registeredContentScripts = new Map(); 2199 2200 this.emitter = new EventEmitter(); 2201 2202 if (this.startupData.lwtData && this.startupReason == "APP_STARTUP") { 2203 LightweightThemeManager.fallbackThemeData = this.startupData.lwtData; 2204 } 2205 2206 /* eslint-disable mozilla/balanced-listeners */ 2207 this.on("add-permissions", (ignoreEvent, permissions) => { 2208 for (let perm of permissions.permissions) { 2209 this.permissions.add(perm); 2210 } 2211 2212 if (permissions.origins.length) { 2213 let patterns = this.allowedOrigins.patterns.map(host => host.pattern); 2214 2215 this.allowedOrigins = new MatchPatternSet( 2216 new Set([...patterns, ...permissions.origins]), 2217 { 2218 restrictSchemes: this.restrictSchemes, 2219 ignorePath: true, 2220 } 2221 ); 2222 } 2223 2224 this.policy.permissions = Array.from(this.permissions); 2225 this.policy.allowedOrigins = this.allowedOrigins; 2226 2227 this.cachePermissions(); 2228 this.updatePermissions(); 2229 }); 2230 2231 this.on("remove-permissions", (ignoreEvent, permissions) => { 2232 for (let perm of permissions.permissions) { 2233 this.permissions.delete(perm); 2234 } 2235 2236 let origins = permissions.origins.map( 2237 origin => new MatchPattern(origin, { ignorePath: true }).pattern 2238 ); 2239 2240 this.allowedOrigins = new MatchPatternSet( 2241 this.allowedOrigins.patterns.filter( 2242 host => !origins.includes(host.pattern) 2243 ) 2244 ); 2245 2246 this.policy.permissions = Array.from(this.permissions); 2247 this.policy.allowedOrigins = this.allowedOrigins; 2248 2249 this.cachePermissions(); 2250 this.updatePermissions(); 2251 }); 2252 /* eslint-enable mozilla/balanced-listeners */ 2253 } 2254 2255 set state(startupState) { 2256 this.startupStates.clear(); 2257 this.startupStates.add(startupState); 2258 } 2259 2260 get state() { 2261 return `${Array.from(this.startupStates).join(", ")}`; 2262 } 2263 2264 async addStartupStatePromise(name, fn) { 2265 this.startupStates.add(name); 2266 try { 2267 await fn(); 2268 } finally { 2269 this.startupStates.delete(name); 2270 } 2271 } 2272 2273 // Some helpful properties added elsewhere: 2274 2275 static getBootstrapScope() { 2276 return new BootstrapScope(); 2277 } 2278 2279 get browsingContextGroupId() { 2280 return this.policy.browsingContextGroupId; 2281 } 2282 2283 get groupFrameLoader() { 2284 let frameLoader = this._backgroundPageFrameLoader; 2285 for (let view of this.views) { 2286 if (view.viewType === "background" && view.xulBrowser) { 2287 return view.xulBrowser.frameLoader; 2288 } 2289 if (!frameLoader && view.xulBrowser) { 2290 frameLoader = view.xulBrowser.frameLoader; 2291 } 2292 } 2293 return frameLoader || ExtensionParent.DebugUtils.getFrameLoader(this.id); 2294 } 2295 2296 on(hook, f) { 2297 return this.emitter.on(hook, f); 2298 } 2299 2300 off(hook, f) { 2301 return this.emitter.off(hook, f); 2302 } 2303 2304 once(hook, f) { 2305 return this.emitter.once(hook, f); 2306 } 2307 2308 emit(event, ...args) { 2309 if (PROXIED_EVENTS.has(event)) { 2310 Services.ppmm.broadcastAsyncMessage(this.MESSAGE_EMIT_EVENT, { 2311 event, 2312 args, 2313 }); 2314 } 2315 2316 return this.emitter.emit(event, ...args); 2317 } 2318 2319 receiveMessage({ name, data }) { 2320 if (name === this.MESSAGE_EMIT_EVENT) { 2321 this.emitter.emit(data.event, ...data.args); 2322 } 2323 } 2324 2325 testMessage(...args) { 2326 this.emit("test-harness-message", ...args); 2327 } 2328 2329 createPrincipal(uri = this.baseURI, originAttributes = {}) { 2330 return Services.scriptSecurityManager.createContentPrincipal( 2331 uri, 2332 originAttributes 2333 ); 2334 } 2335 2336 // Checks that the given URL is a child of our baseURI. 2337 isExtensionURL(url) { 2338 let uri = Services.io.newURI(url); 2339 2340 let common = this.baseURI.getCommonBaseSpec(uri); 2341 return common == this.baseURL; 2342 } 2343 2344 checkLoadURL(url, options = {}) { 2345 // As an optimization, f the URL starts with the extension's base URL, 2346 // don't do any further checks. It's always allowed to load it. 2347 if (url.startsWith(this.baseURL)) { 2348 return true; 2349 } 2350 2351 return ExtensionCommon.checkLoadURL(url, this.principal, options); 2352 } 2353 2354 async promiseLocales(locale) { 2355 let locales = await StartupCache.locales.get( 2356 [this.id, "@@all_locales"], 2357 () => this._promiseLocaleMap() 2358 ); 2359 2360 return this._setupLocaleData(locales); 2361 } 2362 2363 readLocaleFile(locale) { 2364 return StartupCache.locales 2365 .get([this.id, this.version, locale], () => super.readLocaleFile(locale)) 2366 .then(result => { 2367 this.localeData.messages.set(locale, result); 2368 }); 2369 } 2370 2371 get manifestCacheKey() { 2372 return [this.id, this.version, Services.locale.appLocaleAsBCP47]; 2373 } 2374 2375 get temporarilyInstalled() { 2376 return !!this.addonData.temporarilyInstalled; 2377 } 2378 2379 get experimentsAllowed() { 2380 return AddonSettings.EXPERIMENTS_ENABLED || this.isPrivileged; 2381 } 2382 2383 saveStartupData() { 2384 if (this.dontSaveStartupData) { 2385 return; 2386 } 2387 XPIProvider.setStartupData(this.id, this.startupData); 2388 } 2389 2390 parseManifest() { 2391 return StartupCache.manifests.get(this.manifestCacheKey, () => 2392 super.parseManifest() 2393 ); 2394 } 2395 2396 async cachePermissions() { 2397 let manifestData = await this.parseManifest(); 2398 2399 manifestData.originPermissions = this.allowedOrigins.patterns.map( 2400 pat => pat.pattern 2401 ); 2402 manifestData.permissions = this.permissions; 2403 return StartupCache.manifests.set(this.manifestCacheKey, manifestData); 2404 } 2405 2406 async loadManifest() { 2407 let manifest = await super.loadManifest(); 2408 2409 this.ensureNoErrors(); 2410 2411 return manifest; 2412 } 2413 2414 get extensionPageCSP() { 2415 const { content_security_policy } = this.manifest; 2416 // While only manifest v3 should contain an object, 2417 // we'll remain lenient here. 2418 if ( 2419 content_security_policy && 2420 typeof content_security_policy === "object" 2421 ) { 2422 return content_security_policy.extension_pages; 2423 } 2424 return content_security_policy; 2425 } 2426 2427 get backgroundScripts() { 2428 return this.manifest.background?.scripts; 2429 } 2430 2431 get backgroundWorkerScript() { 2432 return this.manifest.background?.service_worker; 2433 } 2434 2435 get optionalPermissions() { 2436 return this.manifest.optional_permissions; 2437 } 2438 2439 get privateBrowsingAllowed() { 2440 return this.policy.privateBrowsingAllowed; 2441 } 2442 2443 canAccessWindow(window) { 2444 return this.policy.canAccessWindow(window); 2445 } 2446 2447 // TODO bug 1699481: move this logic to WebExtensionPolicy 2448 canAccessContainer(userContextId) { 2449 userContextId = userContextId ?? 0; // firefox-default has userContextId as 0. 2450 let defaultRestrictedContainers = JSON.parse( 2451 userContextIsolationDefaultRestricted 2452 ); 2453 let extensionRestrictedContainers = JSON.parse( 2454 Services.prefs.getStringPref( 2455 `extensions.userContextIsolation.${this.id}.restricted`, 2456 "[]" 2457 ) 2458 ); 2459 if ( 2460 extensionRestrictedContainers.includes(userContextId) || 2461 defaultRestrictedContainers.includes(userContextId) 2462 ) { 2463 return false; 2464 } 2465 2466 return true; 2467 } 2468 2469 // Representation of the extension to send to content 2470 // processes. This should include anything the content process might 2471 // need. 2472 serialize() { 2473 return { 2474 id: this.id, 2475 uuid: this.uuid, 2476 name: this.name, 2477 manifestVersion: this.manifestVersion, 2478 extensionPageCSP: this.extensionPageCSP, 2479 instanceId: this.instanceId, 2480 resourceURL: this.resourceURL, 2481 contentScripts: this.contentScripts, 2482 webAccessibleResources: this.webAccessibleResources, 2483 allowedOrigins: this.allowedOrigins.patterns.map(pat => pat.pattern), 2484 permissions: this.permissions, 2485 optionalPermissions: this.optionalPermissions, 2486 isPrivileged: this.isPrivileged, 2487 temporarilyInstalled: this.temporarilyInstalled, 2488 }; 2489 } 2490 2491 // Extended serialized data which is only needed in the extensions process, 2492 // and is never deserialized in web content processes. 2493 serializeExtended() { 2494 return { 2495 backgroundScripts: this.backgroundScripts, 2496 backgroundWorkerScript: this.backgroundWorkerScript, 2497 childModules: this.modules && this.modules.child, 2498 dependencies: this.dependencies, 2499 schemaURLs: this.schemaURLs, 2500 }; 2501 } 2502 2503 broadcast(msg, data) { 2504 return new Promise(resolve => { 2505 let { ppmm } = Services; 2506 let children = new Set(); 2507 for (let i = 0; i < ppmm.childCount; i++) { 2508 children.add(ppmm.getChildAt(i)); 2509 } 2510 2511 let maybeResolve; 2512 function listener(data) { 2513 children.delete(data.target); 2514 maybeResolve(); 2515 } 2516 function observer(subject, topic, data) { 2517 children.delete(subject); 2518 maybeResolve(); 2519 } 2520 2521 maybeResolve = () => { 2522 if (children.size === 0) { 2523 ppmm.removeMessageListener(msg + "Complete", listener); 2524 Services.obs.removeObserver(observer, "message-manager-close"); 2525 Services.obs.removeObserver(observer, "message-manager-disconnect"); 2526 resolve(); 2527 } 2528 }; 2529 ppmm.addMessageListener(msg + "Complete", listener, true); 2530 Services.obs.addObserver(observer, "message-manager-close"); 2531 Services.obs.addObserver(observer, "message-manager-disconnect"); 2532 2533 ppmm.broadcastAsyncMessage(msg, data); 2534 }); 2535 } 2536 2537 setSharedData(key, value) { 2538 key = `extension/${this.id}/${key}`; 2539 this.sharedDataKeys.add(key); 2540 2541 sharedData.set(key, value); 2542 } 2543 2544 getSharedData(key, value) { 2545 key = `extension/${this.id}/${key}`; 2546 return sharedData.get(key); 2547 } 2548 2549 initSharedData() { 2550 this.setSharedData("", this.serialize()); 2551 this.setSharedData("extendedData", this.serializeExtended()); 2552 this.setSharedData("locales", this.localeData.serialize()); 2553 this.setSharedData("manifest", this.manifest); 2554 this.updateContentScripts(); 2555 } 2556 2557 updateContentScripts() { 2558 this.setSharedData("contentScripts", this.registeredContentScripts); 2559 } 2560 2561 runManifest(manifest) { 2562 let promises = []; 2563 let addPromise = (name, fn) => { 2564 promises.push(this.addStartupStatePromise(name, fn)); 2565 }; 2566 2567 for (let directive in manifest) { 2568 if (manifest[directive] !== null) { 2569 addPromise(`asyncEmitManifestEntry("${directive}")`, () => 2570 Management.asyncEmitManifestEntry(this, directive) 2571 ); 2572 } 2573 } 2574 2575 activeExtensionIDs.add(this.id); 2576 sharedData.set("extensions/activeIDs", activeExtensionIDs); 2577 2578 pendingExtensions.delete(this.id); 2579 sharedData.set("extensions/pending", pendingExtensions); 2580 2581 Services.ppmm.sharedData.flush(); 2582 this.broadcast("Extension:Startup", this.id); 2583 2584 return Promise.all(promises); 2585 } 2586 2587 /** 2588 * Call the close() method on the given object when this extension 2589 * is shut down. This can happen during browser shutdown, or when 2590 * an extension is manually disabled or uninstalled. 2591 * 2592 * @param {object} obj 2593 * An object on which to call the close() method when this 2594 * extension is shut down. 2595 */ 2596 callOnClose(obj) { 2597 this.onShutdown.add(obj); 2598 } 2599 2600 forgetOnClose(obj) { 2601 this.onShutdown.delete(obj); 2602 } 2603 2604 get builtinMessages() { 2605 return new Map([["@@extension_id", this.uuid]]); 2606 } 2607 2608 // Reads the locale file for the given Gecko-compatible locale code, or if 2609 // no locale is given, the available locale closest to the UI locale. 2610 // Sets the currently selected locale on success. 2611 async initLocale(locale = undefined) { 2612 if (locale === undefined) { 2613 let locales = await this.promiseLocales(); 2614 2615 let matches = Services.locale.negotiateLanguages( 2616 Services.locale.appLocalesAsBCP47, 2617 Array.from(locales.keys()), 2618 this.defaultLocale 2619 ); 2620 2621 locale = matches[0]; 2622 } 2623 2624 return super.initLocale(locale); 2625 } 2626 2627 /** 2628 * Clear cached resources associated to the extension principal 2629 * when an extension is installed (in case we were unable to do that at 2630 * uninstall time) or when it is being upgraded or downgraded. 2631 * 2632 * @param {string|undefined} reason 2633 * BOOTSTRAP_REASON string, if provided. The value is expected to be 2634 * `undefined` for extension objects without a corresponding AddonManager 2635 * addon wrapper (e.g. test extensions created using `ExtensionTestUtils` 2636 * without `useAddonManager` optional property). 2637 * 2638 * @returns {Promise<void>} 2639 * Promise resolved when the nsIClearDataService async method call 2640 * has been completed. 2641 */ 2642 async clearCache(reason) { 2643 switch (reason) { 2644 case "ADDON_INSTALL": 2645 case "ADDON_UPGRADE": 2646 case "ADDON_DOWNGRADE": 2647 return clearCacheForExtensionPrincipal(this.principal); 2648 } 2649 } 2650 2651 /** 2652 * Update site permissions as necessary. 2653 * 2654 * @param {string|undefined} reason 2655 * If provided, this is a BOOTSTRAP_REASON string. If reason is undefined, 2656 * addon permissions are being added or removed that may effect the site permissions. 2657 */ 2658 updatePermissions(reason) { 2659 const { principal } = this; 2660 2661 const testPermission = perm => 2662 Services.perms.testPermissionFromPrincipal(principal, perm); 2663 2664 const addUnlimitedStoragePermissions = () => { 2665 // Set the indexedDB permission and a custom "WebExtensions-unlimitedStorage" to 2666 // remember that the permission hasn't been selected manually by the user. 2667 Services.perms.addFromPrincipal( 2668 principal, 2669 "WebExtensions-unlimitedStorage", 2670 Services.perms.ALLOW_ACTION 2671 ); 2672 Services.perms.addFromPrincipal( 2673 principal, 2674 "indexedDB", 2675 Services.perms.ALLOW_ACTION 2676 ); 2677 Services.perms.addFromPrincipal( 2678 principal, 2679 "persistent-storage", 2680 Services.perms.ALLOW_ACTION 2681 ); 2682 }; 2683 2684 // Only update storage permissions when the extension changes in 2685 // some way. 2686 if (reason !== "APP_STARTUP" && reason !== "APP_SHUTDOWN") { 2687 if (this.hasPermission("unlimitedStorage")) { 2688 addUnlimitedStoragePermissions(); 2689 } else { 2690 // Remove the indexedDB permission if it has been enabled using the 2691 // unlimitedStorage WebExtensions permissions. 2692 Services.perms.removeFromPrincipal( 2693 principal, 2694 "WebExtensions-unlimitedStorage" 2695 ); 2696 Services.perms.removeFromPrincipal(principal, "indexedDB"); 2697 Services.perms.removeFromPrincipal(principal, "persistent-storage"); 2698 } 2699 } else if ( 2700 reason === "APP_STARTUP" && 2701 this.hasPermission("unlimitedStorage") && 2702 (testPermission("indexedDB") !== Services.perms.ALLOW_ACTION || 2703 testPermission("persistent-storage") !== Services.perms.ALLOW_ACTION) 2704 ) { 2705 // If the extension does have the unlimitedStorage permission, but the 2706 // expected site permissions are missing during the app startup, then 2707 // add them back (See Bug 1454192). 2708 addUnlimitedStoragePermissions(); 2709 } 2710 2711 // Never change geolocation permissions at shutdown, since it uses a 2712 // session-only permission. 2713 if (reason !== "APP_SHUTDOWN") { 2714 if (this.hasPermission("geolocation")) { 2715 if (testPermission("geo") === Services.perms.UNKNOWN_ACTION) { 2716 Services.perms.addFromPrincipal( 2717 principal, 2718 "geo", 2719 Services.perms.ALLOW_ACTION, 2720 Services.perms.EXPIRE_SESSION 2721 ); 2722 } 2723 } else if ( 2724 reason !== "APP_STARTUP" && 2725 testPermission("geo") === Services.perms.ALLOW_ACTION 2726 ) { 2727 Services.perms.removeFromPrincipal(principal, "geo"); 2728 } 2729 } 2730 } 2731 2732 async startup() { 2733 this.state = "Startup"; 2734 2735 // readyPromise is resolved with the policy upon success, 2736 // and with null if startup was interrupted. 2737 let resolveReadyPromise; 2738 let readyPromise = new Promise(resolve => { 2739 resolveReadyPromise = resolve; 2740 }); 2741 2742 // Create a temporary policy object for the devtools and add-on 2743 // manager callers that depend on it being available early. 2744 this.policy = new WebExtensionPolicy({ 2745 id: this.id, 2746 mozExtensionHostname: this.uuid, 2747 baseURL: this.resourceURL, 2748 isPrivileged: this.isPrivileged, 2749 temporarilyInstalled: this.temporarilyInstalled, 2750 allowedOrigins: new MatchPatternSet([]), 2751 localizeCallback() {}, 2752 readyPromise, 2753 }); 2754 2755 this.policy.extension = this; 2756 if (!WebExtensionPolicy.getByID(this.id)) { 2757 this.policy.active = true; 2758 } 2759 2760 pendingExtensions.set(this.id, { 2761 mozExtensionHostname: this.uuid, 2762 baseURL: this.resourceURL, 2763 isPrivileged: this.isPrivileged, 2764 }); 2765 sharedData.set("extensions/pending", pendingExtensions); 2766 2767 ExtensionTelemetry.extensionStartup.stopwatchStart(this); 2768 try { 2769 this.state = "Startup: Loading manifest"; 2770 await this.loadManifest(); 2771 this.state = "Startup: Loaded manifest"; 2772 2773 if (!this.hasShutdown) { 2774 this.state = "Startup: Init locale"; 2775 await this.initLocale(); 2776 this.state = "Startup: Initted locale"; 2777 } 2778 2779 this.ensureNoErrors(); 2780 2781 if (this.hasShutdown) { 2782 // Startup was interrupted and shutdown() has taken care of unloading 2783 // the extension and running cleanup logic. 2784 return; 2785 } 2786 2787 await this.clearCache(this.startupReason); 2788 2789 // We automatically add permissions to system/built-in extensions. 2790 // Extensions expliticy stating not_allowed will never get permission. 2791 let isAllowed = this.permissions.has(PRIVATE_ALLOWED_PERMISSION); 2792 if (this.manifest.incognito === "not_allowed") { 2793 // If an extension previously had permission, but upgrades/downgrades to 2794 // a version that specifies "not_allowed" in manifest, remove the 2795 // permission. 2796 if (isAllowed) { 2797 ExtensionPermissions.remove(this.id, { 2798 permissions: [PRIVATE_ALLOWED_PERMISSION], 2799 origins: [], 2800 }); 2801 this.permissions.delete(PRIVATE_ALLOWED_PERMISSION); 2802 } 2803 } else if ( 2804 !isAllowed && 2805 this.isPrivileged && 2806 !this.temporarilyInstalled 2807 ) { 2808 // Add to EP so it is preserved after ADDON_INSTALL. We don't wait on the add here 2809 // since we are pushing the value into this.permissions. EP will eventually save. 2810 ExtensionPermissions.add(this.id, { 2811 permissions: [PRIVATE_ALLOWED_PERMISSION], 2812 origins: [], 2813 }); 2814 this.permissions.add(PRIVATE_ALLOWED_PERMISSION); 2815 } 2816 2817 // We only want to update the SVG_CONTEXT_PROPERTIES_PERMISSION during install and 2818 // upgrade/downgrade startups. 2819 if (INSTALL_AND_UPDATE_STARTUP_REASONS.has(this.startupReason)) { 2820 if (isMozillaExtension(this)) { 2821 // Add to EP so it is preserved after ADDON_INSTALL. We don't wait on the add here 2822 // since we are pushing the value into this.permissions. EP will eventually save. 2823 ExtensionPermissions.add(this.id, { 2824 permissions: [SVG_CONTEXT_PROPERTIES_PERMISSION], 2825 origins: [], 2826 }); 2827 this.permissions.add(SVG_CONTEXT_PROPERTIES_PERMISSION); 2828 } else { 2829 ExtensionPermissions.remove(this.id, { 2830 permissions: [SVG_CONTEXT_PROPERTIES_PERMISSION], 2831 origins: [], 2832 }); 2833 this.permissions.delete(SVG_CONTEXT_PROPERTIES_PERMISSION); 2834 } 2835 } 2836 2837 // Ensure devtools permission is set 2838 if ( 2839 this.manifest.devtools_page && 2840 !this.manifest.optional_permissions.includes("devtools") 2841 ) { 2842 ExtensionPermissions.add(this.id, { 2843 permissions: ["devtools"], 2844 origins: [], 2845 }); 2846 this.permissions.add("devtools"); 2847 } 2848 2849 GlobalManager.init(this); 2850 2851 this.initSharedData(); 2852 2853 this.policy.active = false; 2854 this.policy = ExtensionProcessScript.initExtension(this); 2855 this.policy.extension = this; 2856 2857 this.updatePermissions(this.startupReason); 2858 2859 // Select the storage.local backend if it is already known, 2860 // and start the data migration if needed. 2861 if (this.hasPermission("storage")) { 2862 if (!ExtensionStorageIDB.isBackendEnabled) { 2863 this.setSharedData("storageIDBBackend", false); 2864 } else if (ExtensionStorageIDB.isMigratedExtension(this)) { 2865 this.setSharedData("storageIDBBackend", true); 2866 this.setSharedData( 2867 "storageIDBPrincipal", 2868 ExtensionStorageIDB.getStoragePrincipal(this) 2869 ); 2870 } else if ( 2871 this.startupReason === "ADDON_INSTALL" && 2872 !Services.prefs.getBoolPref(LEAVE_STORAGE_PREF, false) 2873 ) { 2874 // If the extension has been just installed, set it as migrated, 2875 // because there will not be any data to migrate. 2876 ExtensionStorageIDB.setMigratedExtensionPref(this, true); 2877 this.setSharedData("storageIDBBackend", true); 2878 this.setSharedData( 2879 "storageIDBPrincipal", 2880 ExtensionStorageIDB.getStoragePrincipal(this) 2881 ); 2882 } 2883 } 2884 2885 resolveReadyPromise(this.policy); 2886 2887 // The "startup" Management event sent on the extension instance itself 2888 // is emitted just before the Management "startup" event, 2889 // and it is used to run code that needs to be executed before 2890 // any of the "startup" listeners. 2891 this.emit("startup", this); 2892 2893 this.startupStates.clear(); 2894 await Promise.all([ 2895 this.addStartupStatePromise("Startup: Emit startup", () => 2896 Management.emit("startup", this) 2897 ), 2898 this.addStartupStatePromise("Startup: Run manifest", () => 2899 this.runManifest(this.manifest) 2900 ), 2901 ]); 2902 this.state = "Startup: Ran manifest"; 2903 2904 Management.emit("ready", this); 2905 this.emit("ready"); 2906 2907 this.state = "Startup: Complete"; 2908 } catch (e) { 2909 this.state = `Startup: Error: ${e}`; 2910 2911 Cu.reportError(e); 2912 2913 if (this.policy) { 2914 this.policy.active = false; 2915 } 2916 2917 this.cleanupGeneratedFile(); 2918 2919 throw e; 2920 } finally { 2921 ExtensionTelemetry.extensionStartup.stopwatchFinish(this); 2922 // Mark readyPromise as resolved in case it has not happened before, 2923 // e.g. due to an early return or an error. 2924 resolveReadyPromise(null); 2925 } 2926 } 2927 2928 cleanupGeneratedFile() { 2929 if (!this.cleanupFile) { 2930 return; 2931 } 2932 2933 let file = this.cleanupFile; 2934 this.cleanupFile = null; 2935 2936 Services.obs.removeObserver(this, "xpcom-shutdown"); 2937 2938 return this.broadcast("Extension:FlushJarCache", { path: file.path }) 2939 .then(() => { 2940 // We can't delete this file until everyone using it has 2941 // closed it (because Windows is dumb). So we wait for all the 2942 // child processes (including the parent) to flush their JAR 2943 // caches. These caches may keep the file open. 2944 file.remove(false); 2945 }) 2946 .catch(Cu.reportError); 2947 } 2948 2949 async shutdown(reason) { 2950 this.state = "Shutdown"; 2951 2952 this.hasShutdown = true; 2953 2954 if (!this.policy) { 2955 return; 2956 } 2957 2958 if ( 2959 this.hasPermission("storage") && 2960 ExtensionStorageIDB.selectedBackendPromises.has(this) 2961 ) { 2962 this.state = "Shutdown: Storage"; 2963 2964 // Wait the data migration to complete. 2965 try { 2966 await ExtensionStorageIDB.selectedBackendPromises.get(this); 2967 } catch (err) { 2968 Cu.reportError( 2969 `Error while waiting for extension data migration on shutdown: ${this.policy.debugName} - ${err.message}::${err.stack}` 2970 ); 2971 } 2972 this.state = "Shutdown: Storage complete"; 2973 } 2974 2975 if (this.rootURI instanceof Ci.nsIJARURI) { 2976 this.state = "Shutdown: Flush jar cache"; 2977 let file = this.rootURI.JARFile.QueryInterface(Ci.nsIFileURL).file; 2978 Services.ppmm.broadcastAsyncMessage("Extension:FlushJarCache", { 2979 path: file.path, 2980 }); 2981 this.state = "Shutdown: Flushed jar cache"; 2982 } 2983 2984 const isAppShutdown = reason === "APP_SHUTDOWN"; 2985 if (this.cleanupFile || !isAppShutdown) { 2986 StartupCache.clearAddonData(this.id); 2987 } 2988 2989 activeExtensionIDs.delete(this.id); 2990 sharedData.set("extensions/activeIDs", activeExtensionIDs); 2991 2992 for (let key of this.sharedDataKeys) { 2993 sharedData.delete(key); 2994 } 2995 2996 Services.ppmm.removeMessageListener(this.MESSAGE_EMIT_EVENT, this); 2997 2998 this.updatePermissions(reason); 2999 3000 // The service worker registrations related to the extensions are unregistered 3001 // only when the extension is not shutting down as part of the application 3002 // shutdown (a previously registered service worker is expected to stay 3003 // active across browser restarts), the service worker may have been 3004 // registered through the manifest.json background.service_worker property 3005 // or from an extension page through the service worker API if allowed 3006 // through the about:config pref. 3007 if (!isAppShutdown) { 3008 this.state = "Shutdown: ServiceWorkers"; 3009 // TODO: ServiceWorkerCleanUp may go away once Bug 1183245 is fixed. 3010 await ServiceWorkerCleanUp.removeFromPrincipal(this.principal); 3011 this.state = "Shutdown: ServiceWorkers completed"; 3012 } 3013 3014 if (!this.manifest) { 3015 this.state = "Shutdown: Complete: No manifest"; 3016 this.policy.active = false; 3017 3018 return this.cleanupGeneratedFile(); 3019 } 3020 3021 GlobalManager.uninit(this); 3022 3023 for (let obj of this.onShutdown) { 3024 obj.close(); 3025 } 3026 3027 ParentAPIManager.shutdownExtension(this.id, reason); 3028 3029 Management.emit("shutdown", this); 3030 this.emit("shutdown", isAppShutdown); 3031 3032 const TIMED_OUT = Symbol(); 3033 3034 this.state = "Shutdown: Emit shutdown"; 3035 let result = await Promise.race([ 3036 this.broadcast("Extension:Shutdown", { id: this.id }), 3037 promiseTimeout(CHILD_SHUTDOWN_TIMEOUT_MS).then(() => TIMED_OUT), 3038 ]); 3039 this.state = `Shutdown: Emitted shutdown: ${result === TIMED_OUT}`; 3040 if (result === TIMED_OUT) { 3041 Cu.reportError( 3042 `Timeout while waiting for extension child to shutdown: ${this.policy.debugName}` 3043 ); 3044 } 3045 3046 this.policy.active = false; 3047 3048 this.state = `Shutdown: Complete (${this.cleanupFile})`; 3049 return this.cleanupGeneratedFile(); 3050 } 3051 3052 observe(subject, topic, data) { 3053 if (topic === "xpcom-shutdown") { 3054 this.cleanupGeneratedFile(); 3055 } 3056 } 3057 3058 get name() { 3059 return this.manifest.name; 3060 } 3061 3062 get optionalOrigins() { 3063 if (this._optionalOrigins == null) { 3064 let { restrictSchemes, isPrivileged } = this; 3065 let origins = this.manifest.optional_permissions.filter( 3066 perm => classifyPermission(perm, restrictSchemes, isPrivileged).origin 3067 ); 3068 this._optionalOrigins = new MatchPatternSet(origins, { 3069 restrictSchemes, 3070 ignorePath: true, 3071 }); 3072 } 3073 return this._optionalOrigins; 3074 } 3075} 3076 3077class Dictionary extends ExtensionData { 3078 constructor(addonData, startupReason) { 3079 super(addonData.resourceURI); 3080 this.id = addonData.id; 3081 this.startupData = addonData.startupData; 3082 } 3083 3084 static getBootstrapScope() { 3085 return new DictionaryBootstrapScope(); 3086 } 3087 3088 async startup(reason) { 3089 this.dictionaries = {}; 3090 for (let [lang, path] of Object.entries(this.startupData.dictionaries)) { 3091 let uri = Services.io.newURI( 3092 path.slice(0, -4) + ".aff", 3093 null, 3094 this.rootURI 3095 ); 3096 this.dictionaries[lang] = uri; 3097 3098 spellCheck.addDictionary(lang, uri); 3099 } 3100 3101 Management.emit("ready", this); 3102 } 3103 3104 async shutdown(reason) { 3105 if (reason !== "APP_SHUTDOWN") { 3106 XPIProvider.unregisterDictionaries(this.dictionaries); 3107 } 3108 } 3109} 3110 3111class Langpack extends ExtensionData { 3112 constructor(addonData, startupReason) { 3113 super(addonData.resourceURI); 3114 this.startupData = addonData.startupData; 3115 this.manifestCacheKey = [addonData.id, addonData.version]; 3116 } 3117 3118 static getBootstrapScope() { 3119 return new LangpackBootstrapScope(); 3120 } 3121 3122 async promiseLocales(locale) { 3123 let locales = await StartupCache.locales.get( 3124 [this.id, "@@all_locales"], 3125 () => this._promiseLocaleMap() 3126 ); 3127 3128 return this._setupLocaleData(locales); 3129 } 3130 3131 parseManifest() { 3132 return StartupCache.manifests.get(this.manifestCacheKey, () => 3133 super.parseManifest() 3134 ); 3135 } 3136 3137 async startup(reason) { 3138 this.chromeRegistryHandle = null; 3139 if (this.startupData.chromeEntries.length) { 3140 const manifestURI = Services.io.newURI( 3141 "manifest.json", 3142 null, 3143 this.rootURI 3144 ); 3145 this.chromeRegistryHandle = aomStartup.registerChrome( 3146 manifestURI, 3147 this.startupData.chromeEntries 3148 ); 3149 } 3150 3151 const langpackId = this.startupData.langpackId; 3152 const l10nRegistrySources = this.startupData.l10nRegistrySources; 3153 3154 resourceProtocol.setSubstitution(langpackId, this.rootURI); 3155 3156 const fileSources = Object.entries(l10nRegistrySources).map(entry => { 3157 const [sourceName, basePath] = entry; 3158 return new L10nFileSource( 3159 `${sourceName}-${langpackId}`, 3160 langpackId, 3161 this.startupData.languages, 3162 `resource://${langpackId}/${basePath}localization/{locale}/` 3163 ); 3164 }); 3165 3166 L10nRegistry.getInstance().registerSources(fileSources); 3167 3168 Services.obs.notifyObservers( 3169 { wrappedJSObject: { langpack: this } }, 3170 "webextension-langpack-startup" 3171 ); 3172 } 3173 3174 async shutdown(reason) { 3175 if (reason === "APP_SHUTDOWN") { 3176 // If we're shutting down, let's not bother updating the state of each 3177 // system. 3178 return; 3179 } 3180 3181 const sourcesToRemove = Object.keys( 3182 this.startupData.l10nRegistrySources 3183 ).map(sourceName => `${sourceName}-${this.startupData.langpackId}`); 3184 L10nRegistry.getInstance().removeSources(sourcesToRemove); 3185 3186 if (this.chromeRegistryHandle) { 3187 this.chromeRegistryHandle.destruct(); 3188 this.chromeRegistryHandle = null; 3189 } 3190 3191 resourceProtocol.setSubstitution(this.startupData.langpackId, null); 3192 } 3193} 3194 3195class SitePermission extends ExtensionData { 3196 constructor(addonData, startupReason) { 3197 super(addonData.resourceURI); 3198 this.id = addonData.id; 3199 this.hasShutdown = false; 3200 } 3201 3202 async loadManifest() { 3203 let [manifestData] = await Promise.all([this.parseManifest()]); 3204 3205 if (!manifestData) { 3206 return; 3207 } 3208 3209 this.manifest = manifestData.manifest; 3210 this.type = manifestData.type; 3211 this.sitePermissions = this.manifest.site_permissions; 3212 // 1 install_origins is mandatory for this addon type 3213 this.siteOrigin = this.manifest.install_origins[0]; 3214 3215 return this.manifest; 3216 } 3217 3218 static getBootstrapScope() { 3219 return new SitePermissionBootstrapScope(); 3220 } 3221 3222 // Array of principals that may be set by the addon. 3223 getSupportedPrincipals() { 3224 if (!this.siteOrigin) { 3225 return []; 3226 } 3227 const uri = Services.io.newURI(this.siteOrigin); 3228 return [ 3229 Services.scriptSecurityManager.createContentPrincipal(uri, {}), 3230 Services.scriptSecurityManager.createContentPrincipal(uri, { 3231 privateBrowsingId: 1, 3232 }), 3233 ]; 3234 } 3235 3236 async startup(reason) { 3237 await this.loadManifest(); 3238 3239 this.ensureNoErrors(); 3240 3241 let site_permissions = await LAZY_SCHEMA_SITE_PERMISSIONS; 3242 let perms = await ExtensionPermissions.get(this.id); 3243 3244 if (this.hasShutdown) { 3245 // Startup was interrupted and shutdown() has taken care of unloading 3246 // the extension and running cleanup logic. 3247 return; 3248 } 3249 3250 let privateAllowed = perms.permissions.includes(PRIVATE_ALLOWED_PERMISSION); 3251 let principals = this.getSupportedPrincipals(); 3252 3253 // Remove any permissions not contained in site_permissions 3254 for (let principal of principals) { 3255 let existing = Services.perms.getAllForPrincipal(principal); 3256 for (let perm of existing) { 3257 if ( 3258 site_permissions.includes(perm) && 3259 !this.sitePermissions.includes(perm) 3260 ) { 3261 Services.perms.removeFromPrincipal(principal, perm); 3262 } 3263 } 3264 } 3265 3266 // Ensure all permissions in site_permissions have been set, but do not 3267 // overwrite the permission so the user can override the values in preferences. 3268 for (let perm of this.sitePermissions) { 3269 for (let principal of principals) { 3270 let permission = Services.perms.testExactPermissionFromPrincipal( 3271 principal, 3272 perm 3273 ); 3274 if (permission == Ci.nsIPermissionManager.UNKNOWN_ACTION) { 3275 let { privateBrowsingId } = principal.originAttributes; 3276 let allow = privateBrowsingId == 0 || privateAllowed; 3277 Services.perms.addFromPrincipal( 3278 principal, 3279 perm, 3280 allow ? Services.perms.ALLOW_ACTION : Services.perms.DENY_ACTION, 3281 Services.perms.EXPIRE_NEVER 3282 ); 3283 } 3284 } 3285 } 3286 3287 Services.obs.notifyObservers( 3288 { wrappedJSObject: { sitepermissions: this } }, 3289 "webextension-sitepermissions-startup" 3290 ); 3291 } 3292 3293 async shutdown(reason) { 3294 this.hasShutdown = true; 3295 // Permissions are retained across restarts 3296 if (reason == "APP_SHUTDOWN") { 3297 return; 3298 } 3299 let principals = this.getSupportedPrincipals(); 3300 3301 for (let perm of this.sitePermissions || []) { 3302 for (let principal of principals) { 3303 Services.perms.removeFromPrincipal(principal, perm); 3304 } 3305 } 3306 } 3307} 3308