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