GeckoViewWebExtension.sys.mjs (43151B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 import { GeckoViewUtils } from "resource://gre/modules/GeckoViewUtils.sys.mjs"; 6 import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs"; 7 8 const PRIVATE_BROWSING_PERM_NAME = "internal:privateBrowsingAllowed"; 9 const PRIVATE_BROWSING_PERMS = { 10 permissions: [PRIVATE_BROWSING_PERM_NAME], 11 origins: [], 12 data_collection: [], 13 }; 14 15 const TECHNICAL_AND_INTERACTION_DATA_PERM_NAME = "technicalAndInteraction"; 16 const TECHNICAL_AND_INTERACTION_DATA_PERMS = { 17 permissions: [], 18 origins: [], 19 data_collection: [TECHNICAL_AND_INTERACTION_DATA_PERM_NAME], 20 }; 21 22 const lazy = {}; 23 24 ChromeUtils.defineESModuleGetters(lazy, { 25 AddonManager: "resource://gre/modules/AddonManager.sys.mjs", 26 AddonRepository: "resource://gre/modules/addons/AddonRepository.sys.mjs", 27 AddonSettings: "resource://gre/modules/addons/AddonSettings.sys.mjs", 28 EventDispatcher: "resource://gre/modules/Messaging.sys.mjs", 29 Extension: "resource://gre/modules/Extension.sys.mjs", 30 ExtensionData: "resource://gre/modules/Extension.sys.mjs", 31 ExtensionPermissions: "resource://gre/modules/ExtensionPermissions.sys.mjs", 32 ExtensionProcessCrashObserver: "resource://gre/modules/Extension.sys.mjs", 33 GeckoViewTabBridge: "resource://gre/modules/GeckoViewTab.sys.mjs", 34 Management: "resource://gre/modules/Extension.sys.mjs", 35 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", 36 }); 37 38 const { debug, warn } = GeckoViewUtils.initLogging("Console"); 39 40 export var DownloadTracker = new (class extends EventEmitter { 41 constructor() { 42 super(); 43 44 // maps numeric IDs to DownloadItem objects 45 this._downloads = new Map(); 46 } 47 48 onEvent(event, data, callback) { 49 switch (event) { 50 case "GeckoView:WebExtension:DownloadChanged": { 51 const downloadItem = this.getDownloadItemById(data.downloadItemId); 52 53 if (!downloadItem) { 54 callback.onError("Error: Trying to update unknown download"); 55 return; 56 } 57 58 const delta = downloadItem.update(data); 59 if (delta) { 60 this.emit("download-changed", { 61 delta, 62 downloadItem, 63 }); 64 } 65 } 66 } 67 } 68 69 addDownloadItem(item) { 70 this._downloads.set(item.id, item); 71 } 72 73 /** 74 * Finds and returns a DownloadItem with a certain numeric ID 75 * 76 * @param {number} id 77 * @returns {DownloadItem} download item 78 */ 79 getDownloadItemById(id) { 80 return this._downloads.get(id); 81 } 82 })(); 83 84 /** Provides common logic between page and browser actions */ 85 export class ExtensionActionHelper { 86 constructor({ 87 tabTracker, 88 windowTracker, 89 tabContext, 90 properties, 91 extension, 92 }) { 93 this.tabTracker = tabTracker; 94 this.windowTracker = windowTracker; 95 this.tabContext = tabContext; 96 this.properties = properties; 97 this.extension = extension; 98 } 99 100 getTab(aTabId) { 101 if (aTabId !== null) { 102 return this.tabTracker.getTab(aTabId); 103 } 104 return null; 105 } 106 107 getWindow(aWindowId) { 108 if (aWindowId !== null) { 109 return this.windowTracker.getWindow(aWindowId); 110 } 111 return null; 112 } 113 114 extractProperties(aAction) { 115 const merged = {}; 116 for (const p of this.properties) { 117 merged[p] = aAction[p]; 118 } 119 return merged; 120 } 121 122 eventDispatcherFor(aTabId) { 123 if (!aTabId) { 124 return lazy.EventDispatcher.instance; 125 } 126 127 const windowId = lazy.GeckoViewTabBridge.tabIdToWindowId(aTabId); 128 const window = this.windowTracker.getWindow(windowId); 129 return window.WindowEventDispatcher; 130 } 131 132 sendRequest(aTabId, aData) { 133 return this.eventDispatcherFor(aTabId).sendRequest({ 134 ...aData, 135 aTabId, 136 extensionId: this.extension.id, 137 }); 138 } 139 } 140 141 class EmbedderPort { 142 constructor(portId, messenger) { 143 this.id = portId; 144 this.messenger = messenger; 145 this.dispatcher = lazy.EventDispatcher.byName(`port:${portId}`); 146 this.dispatcher.registerListener(this, [ 147 "GeckoView:WebExtension:PortMessageFromApp", 148 "GeckoView:WebExtension:PortDisconnect", 149 ]); 150 } 151 close() { 152 this.dispatcher.unregisterListener(this, [ 153 "GeckoView:WebExtension:PortMessageFromApp", 154 "GeckoView:WebExtension:PortDisconnect", 155 ]); 156 } 157 onPortDisconnect() { 158 this.dispatcher.sendRequest({ 159 type: "GeckoView:WebExtension:Disconnect", 160 sender: this.sender, 161 }); 162 this.close(); 163 } 164 onPortMessage(holder) { 165 this.dispatcher.sendRequest({ 166 type: "GeckoView:WebExtension:PortMessage", 167 data: holder.deserialize({}), 168 }); 169 } 170 onEvent(aEvent, aData) { 171 debug`onEvent ${aEvent} ${aData}`; 172 173 switch (aEvent) { 174 case "GeckoView:WebExtension:PortMessageFromApp": { 175 const holder = new StructuredCloneHolder( 176 "GeckoView:WebExtension:PortMessageFromApp", 177 null, 178 aData.message 179 ); 180 this.messenger.sendPortMessage(this.id, holder); 181 break; 182 } 183 184 case "GeckoView:WebExtension:PortDisconnect": { 185 this.messenger.sendPortDisconnect(this.id); 186 this.close(); 187 break; 188 } 189 } 190 } 191 } 192 193 export class GeckoViewConnection { 194 constructor(sender, target, nativeApp, allowContentMessaging) { 195 this.sender = sender; 196 this.target = target; 197 this.nativeApp = nativeApp; 198 this.allowContentMessaging = allowContentMessaging; 199 200 if (!allowContentMessaging && sender.envType !== "addon_child") { 201 throw new Error(`Unexpected messaging sender: ${JSON.stringify(sender)}`); 202 } 203 } 204 205 get dispatcher() { 206 if (this.sender.envType === "addon_child") { 207 // If this is a WebExtension Page we will have a GeckoSession associated 208 // to it and thus a dispatcher. 209 const dispatcher = GeckoViewUtils.getDispatcherForWindow( 210 this.target.ownerGlobal 211 ); 212 if (dispatcher) { 213 return dispatcher; 214 } 215 216 // No dispatcher means this message is coming from a background script, 217 // use the global event handler 218 return lazy.EventDispatcher.instance; 219 } else if ( 220 this.sender.envType === "content_child" && 221 this.allowContentMessaging 222 ) { 223 // If this message came from a content script, send the message to 224 // the corresponding tab messenger so that GeckoSession can pick it 225 // up. 226 return GeckoViewUtils.getDispatcherForWindow(this.target.ownerGlobal); 227 } 228 229 throw new Error(`Uknown sender envType: ${this.sender.envType}`); 230 } 231 232 _sendMessage({ type, portId, data }) { 233 const message = { 234 type, 235 sender: this.sender, 236 data, 237 portId, 238 extensionId: this.sender.id, 239 nativeApp: this.nativeApp, 240 }; 241 242 return this.dispatcher.sendRequestForResult(message); 243 } 244 245 sendMessage(data) { 246 return this._sendMessage({ 247 type: "GeckoView:WebExtension:Message", 248 data: data.deserialize({}), 249 }); 250 } 251 252 onConnect(portId, messenger) { 253 const port = new EmbedderPort(portId, messenger); 254 255 this._sendMessage({ 256 type: "GeckoView:WebExtension:Connect", 257 data: {}, 258 portId: port.id, 259 }); 260 261 return port; 262 } 263 } 264 265 async function filterPromptPermissions(aPermissions) { 266 if (!aPermissions) { 267 return []; 268 } 269 const promptPermissions = []; 270 for (const permission of aPermissions) { 271 if (!(await lazy.Extension.shouldPromptFor(permission))) { 272 continue; 273 } 274 promptPermissions.push(permission); 275 } 276 return promptPermissions; 277 } 278 279 // Keep in sync with WebExtension.java 280 const FLAG_NONE = 0; 281 const FLAG_ALLOW_CONTENT_MESSAGING = 1 << 0; 282 283 function exportFlags(aPolicy) { 284 let flags = FLAG_NONE; 285 if (!aPolicy) { 286 return flags; 287 } 288 const { extension } = aPolicy; 289 if (extension.hasPermission("nativeMessagingFromContent")) { 290 flags |= FLAG_ALLOW_CONTENT_MESSAGING; 291 } 292 return flags; 293 } 294 295 function normalizePermissions(perms) { 296 if (perms?.permissions) { 297 perms = { ...perms }; 298 perms.permissions = perms.permissions.filter( 299 perm => !perm.startsWith("internal:") 300 ); 301 } 302 return perms; 303 } 304 305 async function exportExtension(aAddon, aSourceURI) { 306 // First, let's make sure the policy is ready if present 307 let policy = WebExtensionPolicy.getByID(aAddon.id); 308 if (policy?.readyPromise) { 309 policy = await policy.readyPromise; 310 } 311 const { 312 amoListingURL, 313 averageRating, 314 blocklistState, 315 creator, 316 description, 317 embedderDisabled, 318 fullDescription, 319 homepageURL, 320 icons, 321 id, 322 incognito, 323 isActive, 324 isBuiltin, 325 isCorrectlySigned, 326 isRecommended, 327 name, 328 optionsType, 329 optionsURL, 330 reviewCount, 331 reviewURL, 332 signedState, 333 sourceURI, 334 temporarilyInstalled, 335 userDisabled, 336 version, 337 } = aAddon; 338 let creatorName = null; 339 let creatorURL = null; 340 if (creator) { 341 const { name, url } = creator; 342 creatorName = name; 343 creatorURL = url; 344 } 345 const openOptionsPageInTab = 346 optionsType === lazy.AddonManager.OPTIONS_TYPE_TAB; 347 const disabledFlags = []; 348 if (userDisabled) { 349 disabledFlags.push("userDisabled"); 350 } 351 if (blocklistState === Ci.nsIBlocklistService.STATE_BLOCKED) { 352 disabledFlags.push("blocklistDisabled"); 353 } else if (blocklistState === Ci.nsIBlocklistService.STATE_SOFTBLOCKED) { 354 disabledFlags.push("softBlocklistDisabled"); 355 } 356 if (embedderDisabled) { 357 disabledFlags.push("appDisabled"); 358 } 359 // Add-ons without an `isCorrectlySigned` property are correctly signed as 360 // they aren't the correct type for signing. 361 if (lazy.AddonSettings.REQUIRE_SIGNING && isCorrectlySigned === false) { 362 disabledFlags.push("signatureDisabled"); 363 } 364 if (lazy.AddonManager.checkCompatibility && !aAddon.isCompatible) { 365 disabledFlags.push("appVersionDisabled"); 366 } 367 const baseURL = policy ? policy.getURL() : ""; 368 let privateBrowsingAllowed; 369 if (policy) { 370 privateBrowsingAllowed = policy.privateBrowsingAllowed; 371 } else { 372 const { permissions } = await lazy.ExtensionPermissions.get(aAddon.id); 373 privateBrowsingAllowed = 374 permissions.includes(PRIVATE_BROWSING_PERM_NAME) || 375 lazy.PrivateBrowsingUtils.permanentPrivateBrowsing; 376 } 377 378 let updateDate; 379 try { 380 updateDate = aAddon.updateDate?.toISOString(); 381 } catch { 382 // `installDate` is used as a fallback for `updateDate` but only when the 383 // add-on is installed. Before that, `installDate` might be undefined, 384 // which would cause `updateDate` (and `installDate`) to be an "invalid 385 // date". 386 updateDate = null; 387 } 388 389 const requiredPermissions = aAddon.userPermissions?.permissions ?? []; 390 const requiredOrigins = aAddon.userPermissions?.origins ?? []; 391 const requiredDataCollectionPermissions = 392 aAddon.userPermissions?.data_collection ?? []; 393 const optionalPermissions = aAddon.optionalPermissions?.permissions ?? []; 394 const optionalOrigins = aAddon.optionalOriginsNormalized; 395 const optionalDataCollectionPermissions = 396 aAddon.optionalPermissions?.data_collection ?? []; 397 const grantedPermissions = normalizePermissions( 398 await lazy.ExtensionPermissions.get(id) 399 ); 400 const grantedOptionalPermissions = grantedPermissions?.permissions ?? []; 401 const grantedOptionalOrigins = grantedPermissions?.origins ?? []; 402 const grantedOptionalDataCollectionPermissions = 403 grantedPermissions?.data_collection ?? []; 404 405 return { 406 webExtensionId: id, 407 locationURI: aSourceURI != null ? aSourceURI.spec : "", 408 isBuiltIn: isBuiltin, 409 webExtensionFlags: exportFlags(policy), 410 metaData: { 411 amoListingURL, 412 averageRating, 413 baseURL, 414 blocklistState, 415 creatorName, 416 creatorURL, 417 description, 418 disabledFlags, 419 downloadUrl: sourceURI?.displaySpec, 420 enabled: isActive, 421 fullDescription, 422 homepageURL, 423 icons, 424 incognito, 425 isRecommended, 426 name, 427 openOptionsPageInTab, 428 optionsPageURL: optionsURL, 429 privateBrowsingAllowed, 430 reviewCount, 431 reviewURL, 432 signedState, 433 temporary: temporarilyInstalled, 434 updateDate, 435 version, 436 requiredPermissions, 437 requiredOrigins, 438 requiredDataCollectionPermissions, 439 optionalPermissions, 440 optionalOrigins, 441 optionalDataCollectionPermissions, 442 grantedOptionalPermissions, 443 grantedOptionalOrigins, 444 grantedOptionalDataCollectionPermissions, 445 }, 446 }; 447 } 448 449 class ExtensionInstallListener { 450 constructor(aResolve, aInstall, aInstallId) { 451 this.install = aInstall; 452 this.installId = aInstallId; 453 this.resolve = result => { 454 aResolve(result); 455 lazy.EventDispatcher.instance.unregisterListener(this, [ 456 "GeckoView:WebExtension:CancelInstall", 457 ]); 458 }; 459 lazy.EventDispatcher.instance.registerListener(this, [ 460 "GeckoView:WebExtension:CancelInstall", 461 ]); 462 } 463 464 async onEvent(aEvent, aData, aCallback) { 465 debug`onEvent ${aEvent} ${aData}`; 466 467 switch (aEvent) { 468 case "GeckoView:WebExtension:CancelInstall": { 469 const { installId } = aData; 470 if (this.installId !== installId) { 471 return; 472 } 473 this.cancelling = true; 474 let cancelled = false; 475 try { 476 this.install.cancel(); 477 cancelled = true; 478 } catch (ex) { 479 // install may have already failed or been cancelled 480 debug`Unable to cancel the install installId ${installId}, Error: ${ex}`; 481 // When we attempt to cancel an install but the cancellation fails for 482 // some reasons (e.g., because it is too late), we need to revert this 483 // boolean property to allow another cancellation to be possible. 484 // Otherwise, events like `onDownloadCancelled` won't resolve and that 485 // will cause problems in the embedder. 486 this.cancelling = false; 487 } 488 aCallback.onSuccess({ cancelled }); 489 break; 490 } 491 } 492 } 493 494 onDownloadCancelled(aInstall) { 495 debug`onDownloadCancelled state=${aInstall.state}`; 496 // Do not resolve we were told to CancelInstall, 497 // to prevent racing with that handler. 498 if (!this.cancelling) { 499 const { error: installError, state } = aInstall; 500 this.resolve({ installError, state }); 501 } 502 } 503 504 onDownloadFailed(aInstall) { 505 debug`onDownloadFailed state=${aInstall.state}`; 506 const { error: installError, state } = aInstall; 507 this.resolve({ installError, state }); 508 } 509 510 onDownloadEnded() { 511 // Nothing to do 512 } 513 514 onInstallCancelled(aInstall, aCancelledByUser) { 515 debug`onInstallCancelled state=${aInstall.state} cancelledByUser=${aCancelledByUser}`; 516 // Do not resolve we were told to CancelInstall, 517 // to prevent racing with that handler. 518 if (!this.cancelling) { 519 const { error: installError, state } = aInstall; 520 // An install can be cancelled by the user OR something else, e.g. when 521 // the blocklist prevents the install of a blocked add-on. 522 this.resolve({ installError, state, cancelledByUser: aCancelledByUser }); 523 } 524 } 525 526 onInstallFailed(aInstall) { 527 debug`onInstallFailed state=${aInstall.state}`; 528 const { error: installError, state } = aInstall; 529 this.resolve({ installError, state }); 530 } 531 532 onInstallPostponed(aInstall) { 533 debug`onInstallPostponed state=${aInstall.state}`; 534 const { error: installError, state } = aInstall; 535 this.resolve({ installError, state }); 536 } 537 538 async onInstallEnded(aInstall, aAddon) { 539 debug`onInstallEnded addonId=${aAddon.id}`; 540 if (lazy.PrivateBrowsingUtils.permanentPrivateBrowsing) { 541 await GeckoViewWebExtension.setPrivateBrowsingAllowed(aAddon.id, true); 542 } 543 const extension = await exportExtension(aAddon, aInstall.sourceURI); 544 this.resolve({ extension }); 545 } 546 } 547 548 class ExtensionPromptObserver { 549 constructor() { 550 Services.obs.addObserver(this, "webextension-permission-prompt"); 551 Services.obs.addObserver(this, "webextension-optional-permission-prompt"); 552 Services.obs.addObserver(this, "webextension-update-permission-prompt"); 553 } 554 555 async permissionPromptRequest(aInstall, aAddon, aInfo) { 556 const { sourceURI } = aInstall; 557 const { permissions } = aInfo; 558 559 const hasTechnicalAndInteractionDataPerm = 560 permissions.data_collection.includes( 561 TECHNICAL_AND_INTERACTION_DATA_PERM_NAME 562 ); 563 564 const extension = await exportExtension(aAddon, sourceURI); 565 const response = await lazy.EventDispatcher.instance.sendRequestForResult({ 566 type: "GeckoView:WebExtension:InstallPrompt", 567 extension, 568 permissions: await filterPromptPermissions(permissions.permissions), 569 origins: permissions.origins, 570 dataCollectionPermissions: permissions.data_collection, 571 }); 572 573 if (response.allow) { 574 if (response.privateBrowsingAllowed) { 575 await lazy.ExtensionPermissions.add(aAddon.id, PRIVATE_BROWSING_PERMS); 576 } else { 577 await lazy.ExtensionPermissions.remove( 578 aAddon.id, 579 PRIVATE_BROWSING_PERMS 580 ); 581 } 582 583 if (hasTechnicalAndInteractionDataPerm) { 584 if (response.isTechnicalAndInteractionDataGranted) { 585 await lazy.ExtensionPermissions.add( 586 aAddon.id, 587 TECHNICAL_AND_INTERACTION_DATA_PERMS 588 ); 589 } else { 590 await lazy.ExtensionPermissions.remove( 591 aAddon.id, 592 TECHNICAL_AND_INTERACTION_DATA_PERMS 593 ); 594 } 595 } 596 597 aInfo.resolve(); 598 } else { 599 aInfo.reject(); 600 } 601 } 602 603 async optionalPermissionPrompt(aExtensionId, aPermissions, resolve) { 604 const response = await lazy.EventDispatcher.instance.sendRequestForResult({ 605 type: "GeckoView:WebExtension:OptionalPrompt", 606 extensionId: aExtensionId, 607 permissions: aPermissions, 608 }); 609 resolve(response.allow); 610 } 611 612 async updatePermissionPrompt({ addon, permissions, resolve, reject }) { 613 const response = await lazy.EventDispatcher.instance.sendRequestForResult({ 614 type: "GeckoView:WebExtension:UpdatePrompt", 615 extension: await exportExtension(addon, /* aSourceURI */ null), 616 newPermissions: await filterPromptPermissions(permissions.permissions), 617 newOrigins: permissions.origins, 618 newDataCollectionPermissions: permissions.data_collection, 619 }); 620 621 if (response.allow) { 622 resolve(); 623 } else { 624 reject(); 625 } 626 } 627 628 observe(aSubject, aTopic) { 629 debug`observe ${aTopic}`; 630 631 switch (aTopic) { 632 case "webextension-permission-prompt": { 633 const { info } = aSubject.wrappedJSObject; 634 const { addon, install } = info; 635 this.permissionPromptRequest(install, addon, info); 636 break; 637 } 638 case "webextension-optional-permission-prompt": { 639 const { id, permissions, resolve } = aSubject.wrappedJSObject; 640 this.optionalPermissionPrompt(id, permissions, resolve); 641 break; 642 } 643 case "webextension-update-permission-prompt": { 644 this.updatePermissionPrompt(aSubject.wrappedJSObject); 645 break; 646 } 647 } 648 } 649 } 650 651 class AddonInstallObserver { 652 constructor() { 653 Services.obs.addObserver(this, "addon-install-failed"); 654 } 655 656 async onInstallationFailed(aAddon, aAddonName, aError) { 657 // aAddon could be null if we have a network error where we can't download the xpi file. 658 // aAddon could also be a valid object without an ID when the xpi file is corrupt. 659 let extension = null; 660 if (aAddon?.id) { 661 extension = await exportExtension(aAddon, /* aSourceURI */ null); 662 } 663 664 lazy.EventDispatcher.instance.sendRequest({ 665 type: "GeckoView:WebExtension:OnInstallationFailed", 666 extension, 667 addonId: aAddon?.id, 668 addonName: aAddonName, 669 addonVersion: aAddon?.version, 670 error: aError, 671 }); 672 } 673 674 observe(aSubject, aTopic) { 675 debug`observe ${aTopic}`; 676 switch (aTopic) { 677 case "addon-install-failed": { 678 aSubject.wrappedJSObject.installs.forEach(install => { 679 const { addon, error, name } = install; 680 // For some errors, we have a valid `addon` but not the `name` set on 681 // the `install` object yet so we check both here. 682 const addonName = name || addon?.name; 683 684 this.onInstallationFailed(addon, addonName, error); 685 }); 686 break; 687 } 688 } 689 } 690 } 691 692 new ExtensionPromptObserver(); 693 new AddonInstallObserver(); 694 695 class AddonManagerListener { 696 constructor() { 697 lazy.AddonManager.addAddonListener(this); 698 // Some extension properties are not going to be available right away after the extension 699 // have been installed (e.g. in particular metaData.optionsPageURL), the GeckoView event 700 // dispatched from onExtensionReady listener will be providing updated extension metadata to 701 // the GeckoView side when it is actually going to be available. 702 this.onExtensionReady = this.onExtensionReady.bind(this); 703 lazy.Management.on("ready", this.onExtensionReady); 704 lazy.Management.on("change-permissions", this.onOptionalPermissionsChanged); 705 } 706 707 async onOptionalPermissionsChanged(type, { extensionId }) { 708 // In xpcshell tests there wil be test extensions that trigger this event while the 709 // AddonManager has not been started at all, on the contrary on a regular browser 710 // instance the AddonManager is expected to be already fully started for an extension 711 // for the extension to be able to reach the "ready" state, and so we just silently 712 // early exit here if the AddonManager is not ready. 713 if (!lazy.AddonManager.isReady) { 714 return; 715 } 716 717 const addon = await lazy.AddonManager.getAddonByID(extensionId); 718 if (!addon) { 719 return; 720 } 721 const extension = await exportExtension(addon, /* aSourceURI */ null); 722 lazy.EventDispatcher.instance.sendRequest({ 723 type: "GeckoView:WebExtension:OnOptionalPermissionsChanged", 724 extension, 725 }); 726 } 727 728 async onExtensionReady(name, extInstance) { 729 // In xpcshell tests there wil be test extensions that trigger this event while the 730 // AddonManager has not been started at all, on the contrary on a regular browser 731 // instance the AddonManager is expected to be already fully started for an extension 732 // for the extension to be able to reach the "ready" state, and so we just silently 733 // early exit here if the AddonManager is not ready. 734 if (!lazy.AddonManager.isReady) { 735 return; 736 } 737 738 debug`onExtensionReady ${extInstance.id}`; 739 740 const addonWrapper = await lazy.AddonManager.getAddonByID(extInstance.id); 741 if (!addonWrapper) { 742 return; 743 } 744 745 const extension = await exportExtension( 746 addonWrapper, 747 /* aSourceURI */ null 748 ); 749 lazy.EventDispatcher.instance.sendRequest({ 750 type: "GeckoView:WebExtension:OnReady", 751 extension, 752 }); 753 } 754 755 async onDisabling(aAddon) { 756 debug`onDisabling ${aAddon.id}`; 757 758 const extension = await exportExtension(aAddon, /* aSourceURI */ null); 759 lazy.EventDispatcher.instance.sendRequest({ 760 type: "GeckoView:WebExtension:OnDisabling", 761 extension, 762 }); 763 } 764 765 async onDisabled(aAddon) { 766 debug`onDisabled ${aAddon.id}`; 767 768 const extension = await exportExtension(aAddon, /* aSourceURI */ null); 769 lazy.EventDispatcher.instance.sendRequest({ 770 type: "GeckoView:WebExtension:OnDisabled", 771 extension, 772 }); 773 } 774 775 async onEnabling(aAddon) { 776 debug`onEnabling ${aAddon.id}`; 777 778 const extension = await exportExtension(aAddon, /* aSourceURI */ null); 779 lazy.EventDispatcher.instance.sendRequest({ 780 type: "GeckoView:WebExtension:OnEnabling", 781 extension, 782 }); 783 } 784 785 async onEnabled(aAddon) { 786 debug`onEnabled ${aAddon.id}`; 787 788 const extension = await exportExtension(aAddon, /* aSourceURI */ null); 789 lazy.EventDispatcher.instance.sendRequest({ 790 type: "GeckoView:WebExtension:OnEnabled", 791 extension, 792 }); 793 } 794 795 async onUninstalling(aAddon) { 796 debug`onUninstalling ${aAddon.id}`; 797 798 const extension = await exportExtension(aAddon, /* aSourceURI */ null); 799 lazy.EventDispatcher.instance.sendRequest({ 800 type: "GeckoView:WebExtension:OnUninstalling", 801 extension, 802 }); 803 } 804 805 async onUninstalled(aAddon) { 806 debug`onUninstalled ${aAddon.id}`; 807 808 const extension = await exportExtension(aAddon, /* aSourceURI */ null); 809 lazy.EventDispatcher.instance.sendRequest({ 810 type: "GeckoView:WebExtension:OnUninstalled", 811 extension, 812 }); 813 } 814 815 async onInstalling(aAddon) { 816 debug`onInstalling ${aAddon.id}`; 817 818 const extension = await exportExtension(aAddon, /* aSourceURI */ null); 819 lazy.EventDispatcher.instance.sendRequest({ 820 type: "GeckoView:WebExtension:OnInstalling", 821 extension, 822 }); 823 } 824 825 async onInstalled(aAddon) { 826 debug`onInstalled ${aAddon.id}`; 827 828 const extension = await exportExtension(aAddon, /* aSourceURI */ null); 829 lazy.EventDispatcher.instance.sendRequest({ 830 type: "GeckoView:WebExtension:OnInstalled", 831 extension, 832 }); 833 } 834 } 835 836 new AddonManagerListener(); 837 838 class ExtensionProcessListener { 839 constructor() { 840 this.onExtensionProcessCrash = this.onExtensionProcessCrash.bind(this); 841 lazy.Management.on("extension-process-crash", this.onExtensionProcessCrash); 842 843 lazy.EventDispatcher.instance.registerListener(this, [ 844 "GeckoView:WebExtension:EnableProcessSpawning", 845 "GeckoView:WebExtension:DisableProcessSpawning", 846 ]); 847 } 848 849 async onEvent(aEvent, aData) { 850 debug`onEvent ${aEvent} ${aData}`; 851 852 switch (aEvent) { 853 case "GeckoView:WebExtension:EnableProcessSpawning": { 854 debug`Extension process crash -> re-enable process spawning`; 855 lazy.ExtensionProcessCrashObserver.enableProcessSpawning(); 856 break; 857 } 858 } 859 } 860 861 async onExtensionProcessCrash(name, { childID, processSpawningDisabled }) { 862 debug`Extension process crash -> childID=${childID} processSpawningDisabled=${processSpawningDisabled}`; 863 864 // When an extension process has crashed too many times, Gecko will set 865 // `processSpawningDisabled` and no longer allow the extension process 866 // spawning. We only want to send a request to the embedder when we are 867 // disabling the process spawning. If process spawning is still enabled 868 // then we short circuit and don't notify the embedder. 869 if (!processSpawningDisabled) { 870 return; 871 } 872 873 lazy.EventDispatcher.instance.sendRequest({ 874 type: "GeckoView:WebExtension:OnDisabledProcessSpawning", 875 }); 876 } 877 } 878 879 new ExtensionProcessListener(); 880 881 class MobileWindowTracker extends EventEmitter { 882 constructor() { 883 super(); 884 this._topWindow = null; 885 this._topNonPBWindow = null; 886 } 887 888 get topWindow() { 889 if (this._topWindow) { 890 return this._topWindow.get(); 891 } 892 return null; 893 } 894 895 get topNonPBWindow() { 896 if (this._topNonPBWindow) { 897 return this._topNonPBWindow.get(); 898 } 899 return null; 900 } 901 902 setTabActive(aWindow, aActive) { 903 const { browser, tab: nativeTab, docShell } = aWindow; 904 nativeTab.active = aActive; 905 906 if (aActive) { 907 this._topWindow = Cu.getWeakReference(aWindow); 908 const isPrivate = lazy.PrivateBrowsingUtils.isBrowserPrivate(browser); 909 if (!isPrivate) { 910 this._topNonPBWindow = this._topWindow; 911 } 912 this.emit("tab-activated", { 913 windowId: docShell.outerWindowID, 914 tabId: nativeTab.id, 915 isPrivate, 916 nativeTab, 917 }); 918 } 919 } 920 } 921 922 export var mobileWindowTracker = new MobileWindowTracker(); 923 924 export var GeckoViewWebExtension = { 925 observe(aSubject, aTopic) { 926 debug`observe ${aTopic}`; 927 928 switch (aTopic) { 929 case "testing-installed-addon": 930 case "testing-uninstalled-addon": { 931 // We pretend devtools installed/uninstalled this addon so we don't 932 // have to add an API just for internal testing. 933 // TODO: assert this is under a test 934 lazy.EventDispatcher.instance.sendRequest({ 935 type: "GeckoView:WebExtension:DebuggerListUpdated", 936 }); 937 break; 938 } 939 940 case "devtools-installed-addon": { 941 lazy.EventDispatcher.instance.sendRequest({ 942 type: "GeckoView:WebExtension:DebuggerListUpdated", 943 }); 944 break; 945 } 946 } 947 }, 948 949 async extensionById(aId) { 950 const addon = await lazy.AddonManager.getAddonByID(aId); 951 if (!addon) { 952 debug`Could not find extension with id=${aId}`; 953 return null; 954 } 955 return addon; 956 }, 957 958 async ensureBuiltIn(aUri, aId) { 959 await lazy.AddonManager.readyPromise; 960 // Although the add-on is privileged in practice due to it being installed 961 // as a built-in extension, we pass isPrivileged=false since the exact flag 962 // doesn't matter as we are only using ExtensionData to read the version. 963 const extensionData = new lazy.ExtensionData(aUri, false); 964 const [extensionVersion, extension] = await Promise.all([ 965 extensionData.getExtensionVersionWithoutValidation(), 966 this.extensionById(aId), 967 ]); 968 969 if (!extension || extensionVersion != extension.version) { 970 return this.installBuiltIn(aUri); 971 } 972 973 const exported = await exportExtension(extension, aUri); 974 return { extension: exported }; 975 }, 976 977 async installBuiltIn(aUri) { 978 await lazy.AddonManager.readyPromise; 979 const addon = await lazy.AddonManager.installBuiltinAddon(aUri.spec); 980 const exported = await exportExtension(addon, aUri); 981 return { extension: exported }; 982 }, 983 984 async installWebExtension(aInstallId, aUri, installMethod) { 985 const install = await lazy.AddonManager.getInstallForURL(aUri.spec, { 986 telemetryInfo: { 987 source: "geckoview-app", 988 method: installMethod || undefined, 989 }, 990 }); 991 const promise = new Promise(resolve => { 992 install.addListener( 993 new ExtensionInstallListener(resolve, install, aInstallId) 994 ); 995 }); 996 997 lazy.AddonManager.installAddonFromAOM(null, aUri, install); 998 999 return promise; 1000 }, 1001 1002 async setPrivateBrowsingAllowed(aId, aAllowed) { 1003 if (aAllowed) { 1004 await lazy.ExtensionPermissions.add(aId, PRIVATE_BROWSING_PERMS); 1005 } else { 1006 await lazy.ExtensionPermissions.remove(aId, PRIVATE_BROWSING_PERMS); 1007 } 1008 1009 // Reload the extension if it is already enabled. This ensures any change 1010 // on the private browsing permission is properly handled. 1011 const addon = await this.extensionById(aId); 1012 if (addon.isActive) { 1013 await addon.reload(); 1014 } 1015 1016 return exportExtension(addon, /* aSourceURI */ null); 1017 }, 1018 1019 async uninstallWebExtension(aId) { 1020 const extension = await this.extensionById(aId); 1021 if (!extension) { 1022 throw new Error(`Could not find an extension with id='${aId}'.`); 1023 } 1024 1025 return extension.uninstall(); 1026 }, 1027 1028 async browserActionClick(aId) { 1029 const policy = WebExtensionPolicy.getByID(aId); 1030 if (!policy) { 1031 return undefined; 1032 } 1033 1034 const browserAction = this.browserActions.get(policy.extension); 1035 if (!browserAction) { 1036 return undefined; 1037 } 1038 1039 return browserAction.triggerClickOrPopup(); 1040 }, 1041 1042 async pageActionClick(aId) { 1043 const policy = WebExtensionPolicy.getByID(aId); 1044 if (!policy) { 1045 return undefined; 1046 } 1047 1048 const pageAction = this.pageActions.get(policy.extension); 1049 if (!pageAction) { 1050 return undefined; 1051 } 1052 1053 return pageAction.triggerClickOrPopup(); 1054 }, 1055 1056 async actionDelegateAttached(aId) { 1057 const policy = WebExtensionPolicy.getByID(aId); 1058 if (!policy) { 1059 debug`Could not find extension with id=${aId}`; 1060 return; 1061 } 1062 1063 const { extension } = policy; 1064 1065 const browserAction = this.browserActions.get(extension); 1066 if (browserAction) { 1067 // Send information about this action to the delegate 1068 browserAction.updateOnChange(null); 1069 } 1070 1071 const pageAction = this.pageActions.get(extension); 1072 if (pageAction) { 1073 pageAction.updateOnChange(null); 1074 } 1075 }, 1076 1077 async enableWebExtension(aId, aSource) { 1078 const extension = await this.extensionById(aId); 1079 if (aSource === "user") { 1080 await extension.enable(); 1081 } else if (aSource === "app") { 1082 await extension.setEmbedderDisabled(false); 1083 } 1084 return exportExtension(extension, /* aSourceURI */ null); 1085 }, 1086 1087 async disableWebExtension(aId, aSource) { 1088 const extension = await this.extensionById(aId); 1089 if (aSource === "user") { 1090 await extension.disable(); 1091 } else if (aSource === "app") { 1092 await extension.setEmbedderDisabled(true); 1093 } 1094 return exportExtension(extension, /* aSourceURI */ null); 1095 }, 1096 1097 /** 1098 * @return A promise resolved with either an AddonInstall object if an update 1099 * is available or null if no update is found. 1100 */ 1101 checkForUpdate(aAddon) { 1102 return new Promise(resolve => { 1103 const listener = { 1104 onUpdateAvailable(_aAddon, install) { 1105 install.promptHandler = aInfo => 1106 lazy.AddonManager.updatePromptHandler(aInfo); 1107 resolve(install); 1108 }, 1109 onNoUpdateAvailable() { 1110 resolve(null); 1111 }, 1112 }; 1113 aAddon.findUpdates( 1114 listener, 1115 lazy.AddonManager.UPDATE_WHEN_PERIODIC_UPDATE 1116 ); 1117 }); 1118 }, 1119 1120 async updateWebExtension(aId) { 1121 // Refresh the cached metadata when necessary. This allows us to always 1122 // export relatively recent metadata to the embedder. 1123 if (lazy.AddonRepository.isMetadataStale()) { 1124 // We use a promise to avoid more than one call to `backgroundUpdateCheck()` 1125 // when `updateWebExtension()` is called for multiple add-ons in parallel. 1126 if (!this._promiseAddonRepositoryUpdate) { 1127 this._promiseAddonRepositoryUpdate = 1128 lazy.AddonRepository.backgroundUpdateCheck().finally(() => { 1129 this._promiseAddonRepositoryUpdate = null; 1130 }); 1131 } 1132 await this._promiseAddonRepositoryUpdate; 1133 } 1134 1135 // Early-return when extension updates are disabled. 1136 if (!lazy.AddonManager.updateEnabled) { 1137 return null; 1138 } 1139 1140 const extension = await this.extensionById(aId); 1141 1142 const install = await this.checkForUpdate(extension); 1143 if (!install) { 1144 return null; 1145 } 1146 const promise = new Promise(resolve => { 1147 install.addListener(new ExtensionInstallListener(resolve)); 1148 }); 1149 install.install(); 1150 return promise; 1151 }, 1152 1153 validateBuiltInLocation(aLocationUri, aCallback) { 1154 let uri; 1155 try { 1156 uri = Services.io.newURI(aLocationUri); 1157 } catch (ex) { 1158 aCallback.onError(`Could not parse uri: ${aLocationUri}. Error: ${ex}`); 1159 return null; 1160 } 1161 1162 if (uri.scheme !== "resource" || uri.host !== "android") { 1163 aCallback.onError(`Only resource://android/... URIs are allowed.`); 1164 return null; 1165 } 1166 1167 if (uri.fileName !== "") { 1168 aCallback.onError( 1169 `This URI does not point to a folder. Note: folders URIs must end with a "/".` 1170 ); 1171 return null; 1172 } 1173 1174 return uri; 1175 }, 1176 1177 /* eslint-disable complexity */ 1178 async onEvent(aEvent, aData, aCallback) { 1179 debug`onEvent ${aEvent} ${aData}`; 1180 1181 switch (aEvent) { 1182 case "GeckoView:BrowserAction:Click": { 1183 const popupUrl = await this.browserActionClick(aData.extensionId); 1184 aCallback.onSuccess(popupUrl); 1185 break; 1186 } 1187 case "GeckoView:PageAction:Click": { 1188 const popupUrl = await this.pageActionClick(aData.extensionId); 1189 aCallback.onSuccess(popupUrl); 1190 break; 1191 } 1192 case "GeckoView:WebExtension:MenuClick": { 1193 aCallback.onError(`Not implemented`); 1194 break; 1195 } 1196 case "GeckoView:WebExtension:MenuShow": { 1197 aCallback.onError(`Not implemented`); 1198 break; 1199 } 1200 case "GeckoView:WebExtension:MenuHide": { 1201 aCallback.onError(`Not implemented`); 1202 break; 1203 } 1204 1205 case "GeckoView:ActionDelegate:Attached": { 1206 this.actionDelegateAttached(aData.extensionId); 1207 break; 1208 } 1209 1210 case "GeckoView:WebExtension:Get": { 1211 const extension = await this.extensionById(aData.extensionId); 1212 if (!extension) { 1213 aCallback.onError( 1214 `Could not find extension with id: ${aData.extensionId}` 1215 ); 1216 return; 1217 } 1218 1219 aCallback.onSuccess({ 1220 extension: await exportExtension(extension, /* aSourceURI */ null), 1221 }); 1222 break; 1223 } 1224 1225 case "GeckoView:WebExtension:SetPBAllowed": { 1226 const { extensionId, allowed } = aData; 1227 try { 1228 const extension = await this.setPrivateBrowsingAllowed( 1229 extensionId, 1230 allowed 1231 ); 1232 aCallback.onSuccess({ extension }); 1233 } catch (ex) { 1234 aCallback.onError(`Unexpected error: ${ex}`); 1235 } 1236 break; 1237 } 1238 1239 case "GeckoView:WebExtension:AddOptionalPermissions": { 1240 const { 1241 extensionId, 1242 permissions, 1243 origins, 1244 dataCollectionPermissions: data_collection, 1245 } = aData; 1246 try { 1247 const addon = await this.extensionById(extensionId); 1248 const normalized = lazy.ExtensionPermissions.normalizeOptional( 1249 { permissions, origins, data_collection }, 1250 addon.optionalPermissions 1251 ); 1252 const policy = WebExtensionPolicy.getByID(addon.id); 1253 await lazy.ExtensionPermissions.add( 1254 extensionId, 1255 normalized, 1256 policy?.extension 1257 ); 1258 const extension = await exportExtension(addon, /* aSourceURI */ null); 1259 aCallback.onSuccess({ extension }); 1260 } catch (ex) { 1261 aCallback.onError(`Unexpected error: ${ex}`); 1262 } 1263 break; 1264 } 1265 1266 case "GeckoView:WebExtension:RemoveOptionalPermissions": { 1267 const { 1268 extensionId, 1269 permissions, 1270 origins, 1271 dataCollectionPermissions: data_collection, 1272 } = aData; 1273 try { 1274 const addon = await this.extensionById(extensionId); 1275 const normalized = lazy.ExtensionPermissions.normalizeOptional( 1276 { permissions, origins, data_collection }, 1277 addon.optionalPermissions 1278 ); 1279 const policy = WebExtensionPolicy.getByID(addon.id); 1280 await lazy.ExtensionPermissions.remove( 1281 addon.id, 1282 normalized, 1283 policy?.extension 1284 ); 1285 const extension = await exportExtension(addon, /* aSourceURI */ null); 1286 aCallback.onSuccess({ extension }); 1287 } catch (ex) { 1288 aCallback.onError(`Unexpected error: ${ex}`); 1289 } 1290 break; 1291 } 1292 1293 case "GeckoView:WebExtension:Install": { 1294 const { locationUri, installId, installMethod } = aData; 1295 let uri; 1296 try { 1297 uri = Services.io.newURI(locationUri); 1298 } catch (ex) { 1299 aCallback.onError(`Could not parse uri: ${locationUri}`); 1300 return; 1301 } 1302 1303 try { 1304 const result = await this.installWebExtension( 1305 installId, 1306 uri, 1307 installMethod 1308 ); 1309 if (result.extension) { 1310 aCallback.onSuccess(result); 1311 } else { 1312 aCallback.onError(result); 1313 } 1314 } catch (ex) { 1315 debug`Install exception error ${ex}`; 1316 aCallback.onError(`Unexpected error: ${ex}`); 1317 } 1318 1319 break; 1320 } 1321 1322 case "GeckoView:WebExtension:EnsureBuiltIn": { 1323 const { locationUri, webExtensionId } = aData; 1324 const uri = this.validateBuiltInLocation(locationUri, aCallback); 1325 if (!uri) { 1326 return; 1327 } 1328 1329 try { 1330 const result = await this.ensureBuiltIn(uri, webExtensionId); 1331 if (result.extension) { 1332 aCallback.onSuccess(result); 1333 } else { 1334 aCallback.onError(result); 1335 } 1336 } catch (ex) { 1337 debug`Install exception error ${ex}`; 1338 aCallback.onError(`Unexpected error: ${ex}`); 1339 } 1340 1341 break; 1342 } 1343 1344 case "GeckoView:WebExtension:InstallBuiltIn": { 1345 const uri = this.validateBuiltInLocation(aData.locationUri, aCallback); 1346 if (!uri) { 1347 return; 1348 } 1349 1350 try { 1351 const result = await this.installBuiltIn(uri); 1352 if (result.extension) { 1353 aCallback.onSuccess(result); 1354 } else { 1355 aCallback.onError(result); 1356 } 1357 } catch (ex) { 1358 debug`Install exception error ${ex}`; 1359 aCallback.onError(`Unexpected error: ${ex}`); 1360 } 1361 1362 break; 1363 } 1364 1365 case "GeckoView:WebExtension:Uninstall": { 1366 try { 1367 await this.uninstallWebExtension(aData.webExtensionId); 1368 aCallback.onSuccess(); 1369 } catch (ex) { 1370 debug`Failed uninstall ${ex}`; 1371 aCallback.onError( 1372 `This extension cannot be uninstalled. Error: ${ex}.` 1373 ); 1374 } 1375 break; 1376 } 1377 1378 case "GeckoView:WebExtension:Enable": { 1379 try { 1380 const { source, webExtensionId } = aData; 1381 if (source !== "user" && source !== "app") { 1382 throw new Error("Illegal source parameter"); 1383 } 1384 const extension = await this.enableWebExtension( 1385 webExtensionId, 1386 source 1387 ); 1388 aCallback.onSuccess({ extension }); 1389 } catch (ex) { 1390 debug`Failed enable ${ex}`; 1391 aCallback.onError(`Unexpected error: ${ex}`); 1392 } 1393 break; 1394 } 1395 1396 case "GeckoView:WebExtension:Disable": { 1397 try { 1398 const { source, webExtensionId } = aData; 1399 if (source !== "user" && source !== "app") { 1400 throw new Error("Illegal source parameter"); 1401 } 1402 const extension = await this.disableWebExtension( 1403 webExtensionId, 1404 source 1405 ); 1406 aCallback.onSuccess({ extension }); 1407 } catch (ex) { 1408 debug`Failed disable ${ex}`; 1409 aCallback.onError(`Unexpected error: ${ex}`); 1410 } 1411 break; 1412 } 1413 1414 case "GeckoView:WebExtension:List": { 1415 try { 1416 await lazy.AddonManager.readyPromise; 1417 const addons = await lazy.AddonManager.getAddonsByTypes([ 1418 "extension", 1419 ]); 1420 const extensions = await Promise.all( 1421 addons.map(addon => exportExtension(addon, /* aSourceURI */ null)) 1422 ); 1423 1424 aCallback.onSuccess({ extensions }); 1425 } catch (ex) { 1426 debug`Failed list ${ex}`; 1427 aCallback.onError(`Unexpected error: ${ex}`); 1428 } 1429 break; 1430 } 1431 1432 case "GeckoView:WebExtension:Update": { 1433 try { 1434 const { webExtensionId } = aData; 1435 const result = await this.updateWebExtension(webExtensionId); 1436 if (result === null || result.extension) { 1437 aCallback.onSuccess(result); 1438 } else { 1439 aCallback.onError(result); 1440 } 1441 } catch (ex) { 1442 debug`Failed update ${ex}`; 1443 aCallback.onError(`Unexpected error: ${ex}`); 1444 } 1445 break; 1446 } 1447 } 1448 }, 1449 }; 1450 1451 // WeakMap[Extension -> BrowserAction] 1452 GeckoViewWebExtension.browserActions = new WeakMap(); 1453 // WeakMap[Extension -> PageAction] 1454 GeckoViewWebExtension.pageActions = new WeakMap();