From 8dd16259287f58f9273002717ec4d27e97127719 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 12 Jun 2024 07:43:14 +0200 Subject: Merging upstream version 127.0. Signed-off-by: Daniel Baumann --- .../modules/geckoview/GeckoViewNavigation.sys.mjs | 107 ++++++++++++++++----- .../geckoview/GeckoViewSessionStore.sys.mjs | 95 +++++++++++++----- .../modules/geckoview/GeckoViewSettings.sys.mjs | 16 +++ .../modules/geckoview/GeckoViewTestUtils.sys.mjs | 7 +- .../geckoview/GeckoViewTranslations.sys.mjs | 13 ++- .../geckoview/GeckoViewWebExtension.sys.mjs | 54 ++++++++--- 6 files changed, 227 insertions(+), 65 deletions(-) (limited to 'mobile/android/modules/geckoview') diff --git a/mobile/android/modules/geckoview/GeckoViewNavigation.sys.mjs b/mobile/android/modules/geckoview/GeckoViewNavigation.sys.mjs index 483f0b01f2..911b08a70e 100644 --- a/mobile/android/modules/geckoview/GeckoViewNavigation.sys.mjs +++ b/mobile/android/modules/geckoview/GeckoViewNavigation.sys.mjs @@ -291,10 +291,10 @@ export class GeckoViewNavigation extends GeckoViewModule { waitAndSetupWindow(aSessionId, aOpenWindowInfo, aName) { if (!aSessionId) { - return Promise.resolve(null); + return Promise.reject(); } - return new Promise(resolve => { + return new Promise((resolve, reject) => { const handler = { observe(aSubject, aTopic) { if ( @@ -318,6 +318,10 @@ export class GeckoViewNavigation extends GeckoViewModule { aSubject.browser.removeAttribute("remoteType"); } Services.obs.removeObserver(handler, "geckoview-window-created"); + if (!aSubject) { + reject(); + return; + } resolve(aSubject); } }, @@ -332,8 +336,45 @@ export class GeckoViewNavigation extends GeckoViewModule { debug`handleNewSession: uri=${aUri && aUri.spec} where=${aWhere} flags=${aFlags}`; + const setupPromise = this.#handleNewSessionAsync( + aUri, + aOpenWindowInfo, + aFlags, + aName + ); + + let browser = undefined; + setupPromise.then( + window => { + browser = window.browser; + }, + () => { + browser = null; + } + ); + + // Wait indefinitely for app to respond with a browser or null + Services.tm.spinEventLoopUntil( + "GeckoViewNavigation.jsm:handleNewSession", + () => this.window.closed || browser !== undefined + ); + return browser || null; + } + + #isNewTab(aWhere) { + return [ + Ci.nsIBrowserDOMWindow.OPEN_NEWTAB, + Ci.nsIBrowserDOMWindow.OPEN_NEWTAB_BACKGROUND, + ].includes(aWhere); + } + + /** + * Similar to handleNewSession. But this returns a promise to wait for new + * browser. + */ + #handleNewSessionAsync(aUri, aOpenWindowInfo, aFlags, aName) { if (!this.enabled) { - return null; + return Promise.reject(); } const newSessionId = Services.uuid @@ -356,30 +397,15 @@ export class GeckoViewNavigation extends GeckoViewModule { aName ); - let browser = undefined; - this.eventDispatcher + return this.eventDispatcher .sendRequestForResult(message) .then(didOpenSession => { if (!didOpenSession) { + // New session cannot be opened, so we should throw NS_ERROR_ABORT. return Promise.reject(); } return setupPromise; - }) - .then( - window => { - browser = window.browser; - }, - () => { - browser = null; - } - ); - - // Wait indefinitely for app to respond with a browser or null - Services.tm.spinEventLoopUntil( - "GeckoViewNavigation.sys.mjs:handleNewSession", - () => this.window.closed || browser !== undefined - ); - return browser || null; + }); } // nsIBrowserDOMWindow. @@ -393,6 +419,11 @@ export class GeckoViewNavigation extends GeckoViewModule { debug`createContentWindow: uri=${aUri && aUri.spec} where=${aWhere} flags=${aFlags}`; + if (!this.enabled) { + Components.returnCode = Cr.NS_ERROR_ABORT; + return null; + } + if ( lazy.LoadURIDelegate.load( this.window, @@ -408,13 +439,45 @@ export class GeckoViewNavigation extends GeckoViewModule { return null; } - const browser = this.handleNewSession( + const newTab = this.#isNewTab(aWhere); + const promise = this.#handleNewSessionAsync( aUri, aOpenWindowInfo, aWhere, aFlags, null ); + + // Actually, GeckoView's createContentWindow always creates new window even + // if OPEN_NEWTAB. So the browsing context will be observed via + // nsFrameLoader. + if (aOpenWindowInfo && !newTab) { + promise.catch(() => { + aOpenWindowInfo.cancel(); + }); + // If nsIOpenWindowInfo isn't null, caller should use the callback. + // Also, nsIWindowProvider.provideWindow doesn't use callback, if new + // tab option, we have to return browsing context instead of async. + return null; + } + + let browser = undefined; + promise.then( + window => { + browser = window.browser; + }, + () => { + browser = null; + } + ); + + // Wait indefinitely for app to respond with a browser or null. + // if browser is null, return error. + Services.tm.spinEventLoopUntil( + "GeckoViewNavigation.sys.mjs:createContentWindow", + () => this.window.closed || browser !== undefined + ); + if (!browser) { Components.returnCode = Cr.NS_ERROR_ABORT; return null; diff --git a/mobile/android/modules/geckoview/GeckoViewSessionStore.sys.mjs b/mobile/android/modules/geckoview/GeckoViewSessionStore.sys.mjs index faa5c5f280..5c176a17f1 100644 --- a/mobile/android/modules/geckoview/GeckoViewSessionStore.sys.mjs +++ b/mobile/android/modules/geckoview/GeckoViewSessionStore.sys.mjs @@ -117,19 +117,11 @@ export var GeckoViewSessionStore = { switch (aTopic) { case "browsing-context-did-set-embedder": { - if ( - aSubject && - aSubject === aSubject.top && - aSubject.isContent && - aSubject.embedderElement && - aSubject.embedderElement.permanentKey - ) { - const permanentKey = aSubject.embedderElement.permanentKey; - this._browserSHistoryListener - .get(permanentKey) - ?.unregister(permanentKey); - - this.getOrCreateSHistoryListener(permanentKey, aSubject, true); + if (aSubject === aSubject.top && aSubject.isContent) { + const permanentKey = aSubject.embedderElement?.permanentKey; + if (permanentKey) { + this.maybeRecreateSHistoryListener(permanentKey, aSubject); + } } break; } @@ -151,26 +143,34 @@ export var GeckoViewSessionStore = { }); }, - getOrCreateSHistoryListener( - permanentKey, - browsingContext, - collectImmediately = false - ) { + getOrCreateSHistoryListener(permanentKey, browsingContext) { if (!permanentKey || browsingContext !== browsingContext.top) { return null; } + const listener = this._browserSHistoryListener.get(permanentKey); + if (listener) { + return listener; + } + + return this.createSHistoryListener(permanentKey, browsingContext, false); + }, + + maybeRecreateSHistoryListener(permanentKey, browsingContext) { + const listener = this._browserSHistoryListener.get(permanentKey); + if (!listener || listener._browserId != browsingContext.browserId) { + listener?.unregister(permanentKey); + this.createSHistoryListener(permanentKey, browsingContext, true); + } + }, + + createSHistoryListener(permanentKey, browsingContext, collectImmediately) { const sessionHistory = browsingContext.sessionHistory; if (!sessionHistory) { return null; } - let listener = this._browserSHistoryListener.get(permanentKey); - if (listener) { - return listener; - } - - listener = new SHistoryListener(browsingContext); + const listener = new SHistoryListener(browsingContext); sessionHistory.addSHistoryListener(listener); this._browserSHistoryListener.set(permanentKey, listener); @@ -184,4 +184,51 @@ export var GeckoViewSessionStore = { return listener; }, + + updateSessionStoreFromTabListener( + browser, + browsingContext, + permanentKey, + update, + forStorage = false + ) { + permanentKey = browser?.permanentKey ?? permanentKey; + if (!permanentKey) { + return; + } + + if (browsingContext.isReplaced) { + return; + } + + const listener = this.getOrCreateSHistoryListener( + permanentKey, + browsingContext + ); + + if (listener) { + const historychange = + // If it is not the scheduled update (tab closed, window closed etc), + // try to store the loading non-web-controlled page opened in _blank + // first. + (forStorage && + lazy.SessionHistory.collectNonWebControlledBlankLoadingSession( + browsingContext + )) || + listener.collect(permanentKey, browsingContext, { + collectFull: !!update.sHistoryNeeded, + writeToCache: false, + }); + + if (historychange) { + update.data.historychange = historychange; + } + } + + const win = + browsingContext.embedderElement?.ownerGlobal || + browsingContext.currentWindowGlobal?.browsingContext?.window; + + this.onTabStateUpdate(permanentKey, win, update); + }, }; diff --git a/mobile/android/modules/geckoview/GeckoViewSettings.sys.mjs b/mobile/android/modules/geckoview/GeckoViewSettings.sys.mjs index ec927b0af6..f2dd7590ab 100644 --- a/mobile/android/modules/geckoview/GeckoViewSettings.sys.mjs +++ b/mobile/android/modules/geckoview/GeckoViewSettings.sys.mjs @@ -6,6 +6,10 @@ import { GeckoViewModule } from "resource://gre/modules/GeckoViewModule.sys.mjs" const lazy = {}; +ChromeUtils.defineESModuleGetters(lazy, { + ExtensionParent: "resource://gre/modules/ExtensionParent.sys.mjs", +}); + ChromeUtils.defineLazyGetter(lazy, "MOBILE_USER_AGENT", function () { return Cc["@mozilla.org/network/protocol;1?name=http"].getService( Ci.nsIHttpProtocolHandler @@ -74,6 +78,18 @@ export class GeckoViewSettings extends GeckoViewModule { this.viewportMode = settings.viewportMode; this.useTrackingProtection = !!settings.useTrackingProtection; + if (settings.isExtensionPopup) { + // NOTE: Only add the webextension-view-type and emit extension-browser-inserted + // once, an extension popup should never change webextension-view-type once set. + if (!this.browser.hasAttribute("webextension-view-type")) { + this.browser.setAttribute("webextension-view-type", "popup"); + lazy.ExtensionParent.apiManager.emit( + "extension-browser-inserted", + this.browser + ); + } + } + // When the page is loading from the main process (e.g. from an extension // page) we won't be able to query the actor here. this.getActor("GeckoViewSettings")?.sendAsyncMessage( diff --git a/mobile/android/modules/geckoview/GeckoViewTestUtils.sys.mjs b/mobile/android/modules/geckoview/GeckoViewTestUtils.sys.mjs index a2ca252cdd..f0fdee5d81 100644 --- a/mobile/android/modules/geckoview/GeckoViewTestUtils.sys.mjs +++ b/mobile/android/modules/geckoview/GeckoViewTestUtils.sys.mjs @@ -24,12 +24,15 @@ export const GeckoViewTabUtil = { if (subject.name === sessionId) { Services.obs.removeObserver( openingObserver, - "geckoview-window-created" + "browser-delayed-startup-finished" ); resolve(subject); } }; - Services.obs.addObserver(openingObserver, "geckoview-window-created"); + Services.obs.addObserver( + openingObserver, + "browser-delayed-startup-finished" + ); }); try { diff --git a/mobile/android/modules/geckoview/GeckoViewTranslations.sys.mjs b/mobile/android/modules/geckoview/GeckoViewTranslations.sys.mjs index 7e74e7bf30..45b43ac9f9 100644 --- a/mobile/android/modules/geckoview/GeckoViewTranslations.sys.mjs +++ b/mobile/android/modules/geckoview/GeckoViewTranslations.sys.mjs @@ -50,8 +50,11 @@ export class GeckoViewTranslations extends GeckoViewModule { aData.toLanguage ); try { - this.getActor("Translations").translate(fromLanguage, toLanguage); - aCallback.onSuccess(); + this.getActor("Translations") + .translate(fromLanguage, toLanguage) + .then(() => { + aCallback.onSuccess(); + }); } catch (error) { aCallback.onError(`Could not translate: ${error}`); } @@ -101,10 +104,11 @@ export class GeckoViewTranslations extends GeckoViewModule { type: "GeckoView:Translations:Offer", }); break; - case "TranslationsParent:LanguageState": + case "TranslationsParent:LanguageState": { const { detectedLanguages, requestedTranslationPair, + hasVisibleChange, error, isEngineReady, } = aEvent.detail.actor.languageState; @@ -112,6 +116,7 @@ export class GeckoViewTranslations extends GeckoViewModule { const data = { detectedLanguages, requestedTranslationPair, + hasVisibleChange, error, isEngineReady, }; @@ -120,7 +125,9 @@ export class GeckoViewTranslations extends GeckoViewModule { type: "GeckoView:Translations:StateChange", data, }); + break; + } } } } diff --git a/mobile/android/modules/geckoview/GeckoViewWebExtension.sys.mjs b/mobile/android/modules/geckoview/GeckoViewWebExtension.sys.mjs index 749f753626..421839264a 100644 --- a/mobile/android/modules/geckoview/GeckoViewWebExtension.sys.mjs +++ b/mobile/android/modules/geckoview/GeckoViewWebExtension.sys.mjs @@ -283,6 +283,16 @@ function exportFlags(aPolicy) { return flags; } +function normalizePermissions(perms) { + if (perms?.permissions) { + perms = { ...perms }; + perms.permissions = perms.permissions.filter( + perm => !perm.startsWith("internal:") + ); + } + return perms; +} + async function exportExtension(aAddon, aPermissions, aSourceURI) { // First, let's make sure the policy is ready if present let policy = WebExtensionPolicy.getByID(aAddon.id); @@ -360,22 +370,12 @@ async function exportExtension(aAddon, aPermissions, aSourceURI) { updateDate = null; } - const normalizePermissions = perms => { - if (perms?.permissions) { - perms = { ...perms }; - perms.permissions = perms.permissions.filter( - perm => !perm.startsWith("internal:") - ); - } - return perms; - }; - const optionalPermissions = aAddon.optionalPermissions?.permissions ?? []; - const optionalOrigins = aAddon.optionalPermissions?.origins ?? []; + const optionalOrigins = aAddon.optionalOriginsNormalized; const grantedPermissions = - normalizePermissions(await lazy.ExtensionPermissions.get(id)) ?? []; - const grantedOptionalPermissions = grantedPermissions?.permissions ?? []; - const grantedOptionalOrigins = grantedPermissions?.origins ?? []; + normalizePermissions(await lazy.ExtensionPermissions.get(id)) ?? {}; + const grantedOptionalPermissions = grantedPermissions.permissions ?? []; + const grantedOptionalOrigins = grantedPermissions.origins ?? []; return { webExtensionId: id, @@ -623,6 +623,32 @@ class AddonManagerListener { // the GeckoView side when it is actually going to be available. this.onExtensionReady = this.onExtensionReady.bind(this); lazy.Management.on("ready", this.onExtensionReady); + lazy.Management.on("change-permissions", this.onOptionalPermissionsChanged); + } + + async onOptionalPermissionsChanged(type, { extensionId }) { + // In xpcshell tests there wil be test extensions that trigger this event while the + // AddonManager has not been started at all, on the contrary on a regular browser + // instance the AddonManager is expected to be already fully started for an extension + // for the extension to be able to reach the "ready" state, and so we just silently + // early exit here if the AddonManager is not ready. + if (!lazy.AddonManager.isReady) { + return; + } + + const addon = await lazy.AddonManager.getAddonByID(extensionId); + if (!addon) { + return; + } + const extension = await exportExtension( + addon, + addon.userPermissions, + /* aSourceURI */ null + ); + lazy.EventDispatcher.instance.sendRequest({ + type: "GeckoView:WebExtension:OnOptionalPermissionsChanged", + extension, + }); } async onExtensionReady(name, extInstance) { -- cgit v1.2.3