diff --git a/browser/base/content/nonbrowser-mac.js b/browser/base/content/nonbrowser-mac.js index 2a515b278bec2..c368181997655 100644 --- a/browser/base/content/nonbrowser-mac.js +++ b/browser/base/content/nonbrowser-mac.js @@ -3,11 +3,30 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +function queueFeltDockAction(isPrivate) { + const FELT_OPEN_DISPOSITION_NEW_WINDOW = 1; + const FELT_OPEN_DISPOSITION_NEW_PRIVATE_WINDOW = 2; + let payload = { + url: "", + disposition: isPrivate + ? FELT_OPEN_DISPOSITION_NEW_PRIVATE_WINDOW + : FELT_OPEN_DISPOSITION_NEW_WINDOW, + }; + const { queueFeltURL } = ChromeUtils.importESModule( + "resource:///modules/BrowserContentHandler.sys.mjs" + ); + queueFeltURL(payload); +} + var NonBrowserWindow = { delayedStartupTimeoutId: null, MAC_HIDDEN_WINDOW: "chrome://browser/content/hiddenWindowMac.xhtml", openBrowserWindowFromDockMenu(options = {}) { + if (Services.felt.isFeltUI()) { + queueFeltDockAction(options.private); + return null; + } let existingWindow = BrowserWindowTracker.getTopWindow(); options.openerWindow = existingWindow || window; let win = OpenBrowserWindow(options); diff --git a/browser/components/BrowserContentHandler.sys.mjs b/browser/components/BrowserContentHandler.sys.mjs index 6367f41adf42c..e9630b1ffdf1b 100644 --- a/browser/components/BrowserContentHandler.sys.mjs +++ b/browser/components/BrowserContentHandler.sys.mjs @@ -448,6 +448,7 @@ nsBrowserContentHandler.prototype = { /* nsICommandLineHandler */ handle: function bch_handle(cmdLine) { + const isFeltUI = Services.felt.isFeltUI(); if ( cmdLine.handleFlag("kiosk", false) || cmdLine.handleFlagWithParam("kiosk-monitor", false) @@ -469,7 +470,14 @@ nsBrowserContentHandler.prototype = { Services.prefs.lockPref("browser.gesture.pinch.out.shift"); } if (cmdLine.handleFlag("browser", false)) { - openBrowserWindow(cmdLine, lazy.gSystemPrincipal); + if (isFeltUI) { + queueFeltURL({ + url: "", + disposition: FELT_OPEN_DISPOSITION_NEW_WINDOW, + }); + } else { + openBrowserWindow(cmdLine, lazy.gSystemPrincipal); + } cmdLine.preventDefault = true; } @@ -480,7 +488,14 @@ nsBrowserContentHandler.prototype = { if (!shouldLoadURI(uri)) { continue; } - openBrowserWindow(cmdLine, principal, uri.spec); + if (isFeltUI) { + queueFeltURL({ + url: uri.spec, + disposition: FELT_OPEN_DISPOSITION_NEW_WINDOW, + }); + } else { + openBrowserWindow(cmdLine, principal, uri.spec); + } cmdLine.preventDefault = true; } } catch (e) { @@ -564,26 +579,36 @@ nsBrowserContentHandler.prototype = { false ); if (privateWindowParam) { - let forcePrivate = true; - let resolvedInfo; - if (!lazy.PrivateBrowsingUtils.enabled) { - // Load about:privatebrowsing in a normal tab, which will display an error indicating - // access to private browsing has been disabled. - forcePrivate = false; - resolvedInfo = { - uri: Services.io.newURI("about:privatebrowsing"), - principal: lazy.gSystemPrincipal, - }; + if (isFeltUI) { + let resolvedInfo = resolveURIInternal(cmdLine, privateWindowParam); + if (shouldLoadURI(resolvedInfo.uri)) { + queueFeltURL({ + url: resolvedInfo.uri.spec, + disposition: FELT_OPEN_DISPOSITION_NEW_PRIVATE_WINDOW, + }); + } } else { - resolvedInfo = resolveURIInternal(cmdLine, privateWindowParam); + let forcePrivate = true; + let resolvedInfo; + if (!lazy.PrivateBrowsingUtils.enabled) { + // Load about:privatebrowsing in a normal tab, which will display an error indicating + // access to private browsing has been disabled. + forcePrivate = false; + resolvedInfo = { + uri: Services.io.newURI("about:privatebrowsing"), + principal: lazy.gSystemPrincipal, + }; + } else { + resolvedInfo = resolveURIInternal(cmdLine, privateWindowParam); + } + handURIToExistingBrowser( + resolvedInfo.uri, + Ci.nsIBrowserDOMWindow.OPEN_NEWTAB, + cmdLine, + forcePrivate, + resolvedInfo.principal + ); } - handURIToExistingBrowser( - resolvedInfo.uri, - Ci.nsIBrowserDOMWindow.OPEN_NEWTAB, - cmdLine, - forcePrivate, - resolvedInfo.principal - ); cmdLine.preventDefault = true; } } catch (e) { @@ -592,13 +617,20 @@ nsBrowserContentHandler.prototype = { } // NS_ERROR_INVALID_ARG is thrown when flag exists, but has no param. if (cmdLine.handleFlag("private-window", false)) { - openBrowserWindow( - cmdLine, - lazy.gSystemPrincipal, - "about:privatebrowsing", - null, - lazy.PrivateBrowsingUtils.enabled - ); + if (isFeltUI) { + queueFeltURL({ + url: "", + disposition: FELT_OPEN_DISPOSITION_NEW_PRIVATE_WINDOW, + }); + } else { + openBrowserWindow( + cmdLine, + lazy.gSystemPrincipal, + "about:privatebrowsing", + null, + lazy.PrivateBrowsingUtils.enabled + ); + } cmdLine.preventDefault = true; } } @@ -1211,9 +1243,23 @@ nsBrowserContentHandler.prototype = { var gBrowserContentHandler = new nsBrowserContentHandler(); // Module-level queue for Felt external link handling -// URLs are stored here when they arrive via command line (before Felt extension loads) +// URL requests are stored here when they arrive via command line (before Felt extension loads) // FeltProcessParent imports this module and manages forwarding from this queue export let gFeltPendingURLs = []; +const FELT_OPEN_DISPOSITION_DEFAULT = 0; +const FELT_OPEN_DISPOSITION_NEW_WINDOW = 1; +const FELT_OPEN_DISPOSITION_NEW_PRIVATE_WINDOW = 2; + +export function queueFeltURL(payload) { + try { + const { queueURL } = ChromeUtils.importESModule( + "chrome://felt/content/FeltProcessParent.sys.mjs" + ); + queueURL(payload); + } catch (err) { + gFeltPendingURLs.push(payload); + } +} function handURIToExistingBrowser( uri, @@ -1498,6 +1544,7 @@ nsDefaultCommandLineHandler.prototype = { /* nsICommandLineHandler */ handle: function dch_handle(cmdLine) { + const isFeltUI = Services.felt.isFeltUI(); var urilist = []; var principalList = []; @@ -1595,7 +1642,7 @@ nsDefaultCommandLineHandler.prototype = { // Make sure that when FeltUI is requested, we do not try to open another // window. Instead, forward any URLs to be opened in the real Firefox. - if (Services.felt.isFeltUI()) { + if (isFeltUI) { console.debug(`Felt: Found FeltUI in BrowserContentHandler.`); cmdLine.preventDefault = true; @@ -1633,20 +1680,11 @@ nsDefaultCommandLineHandler.prototype = { if (urilist.length) { const urlSpecs = urilist.filter(shouldLoadURI).map(u => u.spec); if (urlSpecs.length) { - // Try to import FeltProcessParent and call queueURL() - // This will work if the extension has loaded (chrome:// URLs registered) - // If not, URLs stay in gFeltPendingURLs for later retrieval - try { - const { queueURL } = ChromeUtils.importESModule( - "chrome://felt/content/FeltProcessParent.sys.mjs" - ); - for (let urlSpec of urlSpecs) { - queueURL(urlSpec); - } - } catch (err) { - // Chrome:// URLs not registered yet (early startup) - // Add to queue for FeltProcessParent to retrieve later - gFeltPendingURLs.push(...urlSpecs); + for (let urlSpec of urlSpecs) { + queueFeltURL({ + url: urlSpec, + disposition: FELT_OPEN_DISPOSITION_DEFAULT, + }); } } } diff --git a/browser/extensions/felt/api.js b/browser/extensions/felt/api.js index 42620bc470495..9fcc4cba46a07 100644 --- a/browser/extensions/felt/api.js +++ b/browser/extensions/felt/api.js @@ -7,10 +7,14 @@ /* globals ExtensionAPI, Services, XPCOMUtils */ const lazy = {}; +const FELT_OPEN_DISPOSITION_DEFAULT = 0; +const FELT_OPEN_DISPOSITION_NEW_WINDOW = 1; +const FELT_OPEN_DISPOSITION_NEW_PRIVATE_WINDOW = 2; ChromeUtils.defineESModuleGetters(lazy, { BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs", FeltStorage: "resource:///modules/FeltStorage.sys.mjs", + PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", }); this.felt = class extends ExtensionAPI { @@ -65,7 +69,17 @@ this.felt = class extends ExtensionAPI { } }, - async _handleFeltExternalUrl(url) { + async _handleFeltExternalUrl(data) { + let { url, disposition } = this._parseOpenURLData(data); + if ( + disposition === FELT_OPEN_DISPOSITION_NEW_WINDOW || + disposition === FELT_OPEN_DISPOSITION_NEW_PRIVATE_WINDOW + ) { + let wantsPrivate = + disposition === FELT_OPEN_DISPOSITION_NEW_PRIVATE_WINDOW; + this._openFeltWindow(url, wantsPrivate); + return; + } let win = lazy.BrowserWindowTracker.getTopWindow({ private: false, }); @@ -106,6 +120,37 @@ this.felt = class extends ExtensionAPI { console.error("FeltExtension: Failed to open forwarded URL", url, err); } }, + + _parseOpenURLData(data) { + let parsed = JSON.parse(data); + return { + url: parsed.url ?? "", + disposition: parsed.disposition ?? FELT_OPEN_DISPOSITION_DEFAULT, + }; + }, + + _openFeltWindow(url, wantsPrivate) { + if (wantsPrivate && !lazy.PrivateBrowsingUtils.enabled) { + wantsPrivate = false; + url = "about:privatebrowsing"; + } + + try { + let args = null; + if (url) { + args = Cc["@mozilla.org/supports-string;1"].createInstance( + Ci.nsISupportsString + ); + args.data = url; + } + lazy.BrowserWindowTracker.openWindow({ + private: wantsPrivate, + args, + }); + } catch (err) { + console.error("FeltExtension: Failed to open forwarded window", err); + } + }, }; async onStartup() { diff --git a/browser/extensions/felt/content/FeltProcessParent.sys.mjs b/browser/extensions/felt/content/FeltProcessParent.sys.mjs index 5fb9eeb9a8fc9..7682841959410 100644 --- a/browser/extensions/felt/content/FeltProcessParent.sys.mjs +++ b/browser/extensions/felt/content/FeltProcessParent.sys.mjs @@ -20,23 +20,30 @@ console.debug(`FeltExtension: FeltParentProcess.sys.mjs`); // processing) and FeltProcessParent (which forwards URLs after Firefox is ready) import { gFeltPendingURLs } from "resource:///modules/BrowserContentHandler.sys.mjs"; -export function queueURL(url) { +export function queueURL(payload) { // If Firefox AND extension are both ready, forward immediately if ( gFeltProcessParentInstance?.firefoxReady && gFeltProcessParentInstance?.extensionReady ) { - gFeltProcessParentInstance.sendURLToFirefox(url); + gFeltProcessParentInstance.sendURLToFirefox(payload); // Ensure Felt launcher stays hidden when forwarding to running Firefox Services.felt.makeBackgroundProcess(true); } else { // Queue at module level until ready - gFeltPendingURLs.push(url); + gFeltPendingURLs.push(payload); } } let gFeltProcessParentInstance = null; +function extractURLPayload(payload) { + return { + url: payload.url ?? "", + disposition: payload.disposition ?? 0, + }; +} + /** * Manages the SSO login and launching Firefox */ @@ -375,18 +382,19 @@ export class FeltProcessParent extends JSProcessActorParent { } /** - * Send a URL to Firefox via IPC (Firefox must be ready) + * Send a URL request to Firefox via IPC (Firefox must be ready) * - * @param {string} url + * @param {object} payload - Object with url and disposition properties */ - sendURLToFirefox(url) { + sendURLToFirefox(payload) { if (!this.firefoxReady || !Services.felt) { console.error(`FeltExtension: Cannot send URL, Firefox not ready`); return; } try { - Services.felt.openURL(url); + let { url, disposition } = extractURLPayload(payload); + Services.felt.openURL(url, disposition); } catch (err) { console.error(`FeltExtension: Failed to forward URL: ${err}`); } @@ -416,11 +424,12 @@ export class FeltProcessParent extends JSProcessActorParent { } // Forward all URLs directly via IPC (both Firefox and extension are ready) - for (const url of gFeltPendingURLs) { + for (const payload of gFeltPendingURLs) { try { - Services.felt.openURL(url); + let { url, disposition } = extractURLPayload(payload); + Services.felt.openURL(url, disposition); } catch (err) { - console.error(`FeltExtension: Failed to forward URL ${url}: ${err}`); + console.error(`FeltExtension: Failed to forward URL: ${err}`); } } diff --git a/browser/extensions/felt/rust/nsIFelt.idl b/browser/extensions/felt/rust/nsIFelt.idl index b41dd01c964d8..57135afe14fc4 100644 --- a/browser/extensions/felt/rust/nsIFelt.idl +++ b/browser/extensions/felt/rust/nsIFelt.idl @@ -63,7 +63,7 @@ interface nsIFelt : nsISupports { void sendExtensionReady(); [must_use] - void openURL(in ACString url); + void openURL(in ACString url, in long disposition); [must_use] ACString getConsoleUrl(); diff --git a/browser/extensions/felt/rust/src/client.rs b/browser/extensions/felt/rust/src/client.rs index 3e5452f737806..456465c5b74a3 100644 --- a/browser/extensions/felt/rust/src/client.rs +++ b/browser/extensions/felt/rust/src/client.rs @@ -365,9 +365,13 @@ impl FeltClientThread { trace!("FeltClientThread::felt_client::ipc_loop(): ERROR setting RefreshToken({})", refresh_token); } } - Ok(FeltMessage::OpenURL(url)) => { - trace!("FeltClientThread::felt_client::ipc_loop(): OpenURL({})", url); - utils::open_url_in_firefox(url); + Ok(FeltMessage::OpenURL((url, disposition))) => { + trace!( + "FeltClientThread::felt_client::ipc_loop(): OpenURL({}, {})", + url, + disposition + ); + utils::open_url_in_firefox(url, disposition); }, Ok(msg) => { trace!("FeltClientThread::felt_client::ipc_loop(): UNEXPECTED MSG {:?}", msg); diff --git a/browser/extensions/felt/rust/src/components.rs b/browser/extensions/felt/rust/src/components.rs index 0b012caaeec16..896542c92de8f 100644 --- a/browser/extensions/felt/rust/src/components.rs +++ b/browser/extensions/felt/rust/src/components.rs @@ -192,10 +192,10 @@ impl FeltXPCOM { } } - fn OpenURL(&self, url: *const nsACString) -> nserror::nsresult { + fn OpenURL(&self, url: *const nsACString, disposition: i32) -> nserror::nsresult { let url_s = unsafe { (*url).to_string() }; - trace!("FeltXPCOM::OpenURL: {}", url_s); - self.send(FeltMessage::OpenURL(url_s)) + trace!("FeltXPCOM::OpenURL: {} {}", url_s, disposition); + self.send(FeltMessage::OpenURL((url_s, disposition))) } fn GetConsoleUrl(&self, console_url: *mut nsACString) -> nserror::nsresult { diff --git a/browser/extensions/felt/rust/src/message.rs b/browser/extensions/felt/rust/src/message.rs index 549b8eaecc057..be3edff1110e0 100644 --- a/browser/extensions/felt/rust/src/message.rs +++ b/browser/extensions/felt/rust/src/message.rs @@ -16,10 +16,10 @@ pub enum FeltMessage { StartupReady, Tokens((String, String, i64)), ExtensionReady, - OpenURL(String), + OpenURL((String, i32)), RestartForced, Restarting, LogoutShutdown, } -pub const FELT_IPC_VERSION: u32 = 2; +pub const FELT_IPC_VERSION: u32 = 3; diff --git a/browser/extensions/felt/rust/src/utils.rs b/browser/extensions/felt/rust/src/utils.rs index e8db402b14ea1..f544bd14d41ab 100644 --- a/browser/extensions/felt/rust/src/utils.rs +++ b/browser/extensions/felt/rust/src/utils.rs @@ -5,6 +5,7 @@ use nserror::NS_OK; use nsstring::nsCString; use serde::{Deserialize, Serialize}; +use serde_json::json; use std::sync::{Arc, LazyLock, OnceLock, RwLock}; use std::{ffi::CString, future::Future}; use time::OffsetDateTime; @@ -186,20 +187,28 @@ pub fn notify_observers(name: String) { }); } -pub fn open_url_in_firefox(url: String) { - trace!("open_url_in_firefox() url: {}", url); +pub fn open_url_in_firefox(url: String, disposition: i32) { + trace!( + "open_url_in_firefox() url: {} disposition: {}", + url, + disposition + ); + let payload = json!({ + "url": url, + "disposition": disposition, + }) + .to_string(); do_main_thread("felt_open_url", async move { let obssvc: RefPtr = xpcom::components::Observer::service().unwrap(); let topic = CString::new("felt-open-url").unwrap(); - let url_data = nsstring::nsString::from(&url); + let url_data = nsstring::nsString::from(&payload); let rv = unsafe { obssvc.NotifyObservers(std::ptr::null(), topic.as_ptr(), url_data.as_ptr()) }; if rv.succeeded() { trace!( - "open_url_in_firefox() successfully sent observer notification for URL: {}", - url + "open_url_in_firefox() successfully sent observer notification for URL request" ); } else { trace!("open_url_in_firefox() NotifyObservers failed: {:?}", rv);