From d0fc8fa5c20afccffd1dbb6400ffc03e460bbf64 Mon Sep 17 00:00:00 2001 From: Harshit Date: Sun, 14 Jun 2026 18:43:27 +0000 Subject: [PATCH] Bug 2039411 - Set the default PDF handler through the OS "Open with" picker and intercept the round-trip. r=nrishel,omc-reviewers,sachung Differential Revision: https://phabricator.services.mozilla.com/D300462 --- browser/components/shell/ShellService.sys.mjs | 62 +++- .../WindowsSetDefaultAppCmdHandler.sys.mjs | 97 ++++++ .../shell/WindowsSetDefaultRedirect.sys.mjs | 135 ++++++++ browser/components/shell/components.conf | 18 ++ browser/components/shell/content/blank.pdf | Bin 0 -> 8959 bytes .../components/shell/content/confused_fox.pdf | Bin 0 -> 53103 bytes browser/components/shell/moz.build | 10 + .../shell/nsIWindowsShellService.idl | 36 ++- .../shell/nsWindowsShellService.cpp | 17 +- browser/components/shell/test/browser.toml | 6 + .../test/browser_setDefaultPDFHandler.js | 53 ++-- .../browser_windowsSetDefaultAppCmdHandler.js | 293 ++++++++++++++++++ browser/installer/package-manifest.in | 7 + .../lib/SpecialMessageActions.sys.mjs | 13 +- .../SpecialMessageActionSchemas.json | 4 + .../SpecialMessageActionSchemas/index.md | 4 + .../browser_sma_default_pdf_handler.js | 6 +- 17 files changed, 710 insertions(+), 51 deletions(-) create mode 100644 browser/components/shell/WindowsSetDefaultAppCmdHandler.sys.mjs create mode 100644 browser/components/shell/WindowsSetDefaultRedirect.sys.mjs create mode 100644 browser/components/shell/components.conf create mode 100644 browser/components/shell/content/blank.pdf create mode 100644 browser/components/shell/content/confused_fox.pdf create mode 100644 browser/components/shell/test/browser_windowsSetDefaultAppCmdHandler.js diff --git a/browser/components/shell/ShellService.sys.mjs b/browser/components/shell/ShellService.sys.mjs index ee2cdd8fc7ba..d047c15358fe 100644 --- a/browser/components/shell/ShellService.sys.mjs +++ b/browser/components/shell/ShellService.sys.mjs @@ -12,6 +12,8 @@ ChromeUtils.defineESModuleGetters(lazy, { ASRouter: "resource:///modules/asrouter/ASRouter.sys.mjs", ScheduledTask: "resource://gre/modules/ScheduledTask.sys.mjs", Subprocess: "resource://gre/modules/Subprocess.sys.mjs", + WindowsSetDefaultRedirect: + "moz-src:///browser/components/shell/WindowsSetDefaultRedirect.sys.mjs", WindowsVersionInfo: "resource://gre/modules/components-utils/WindowsVersionInfo.sys.mjs", }); @@ -422,7 +424,36 @@ let ShellServiceInternal = { ); }, - async setAsDefaultPDFHandler(onlyIfKnownBrowser = false) { + /** + * Returns the on-disk nsIFile for a PDF bundled under the browser directory + * in NS_GRE_DIR. + * + * @param {string} aLeafName - The bundled PDF's file name, e.g. + * "confused_fox.pdf". + * @returns {nsIFile} The bundled file (which may not exist on disk). + */ + getBundledPdfFile(aLeafName) { + const file = Services.dirsvc.get("GreD", Ci.nsIFile); + file.append("browser"); + file.append(aLeafName); + return file; + }, + + /** + * Set Firefox as the Windows default PDF handler. + * + * @param {boolean} [onlyIfKnownBrowser] - When true, only proceed if the + * current default PDF handler is a known browser. + * @param {boolean} [openInFirefox] - Only meaningful on the "Open with" + * picker code path. After the user picks Firefox, the OS relaunches Firefox + * with the bundled stub PDF; this flag decides whether we then open a PDF in + * a new tab (true), to land the user in Firefox, or silently absorb that + * relaunch (false). + */ + async setAsDefaultPDFHandler( + onlyIfKnownBrowser = false, + openInFirefox = false + ) { if (AppConstants.platform != "win") { throw new Error("Windows-only"); } @@ -453,10 +484,6 @@ let ShellServiceInternal = { ); } - const winShell = this.shellService.QueryInterface( - Ci.nsIWindowsShellService - ); - // Optional second attempt via the undocumented IOpenWithLauncher API, // which surfaces the OS "Open with" picker so the user can pick Firefox // themselves. Gated by a pref so it can be remotely disabled if it @@ -469,10 +496,27 @@ let ShellServiceInternal = { ) ) { method = "open_with"; + const openWithArg = this.getBundledPdfFile("confused_fox.pdf").path; + // Arm the round-trip: the OS hands `openWithArg` back to Firefox if the user + // selects us. We redirect that launch to the bundled PDF); otherwise overrideUri + // is null and the launch is suppressed. + const overrideUri = openInFirefox + ? Services.io.newFileURI(this.getBundledPdfFile("blank.pdf")).spec + : null; + lazy.WindowsSetDefaultRedirect.arm( + openWithArg, + overrideUri, + lazy.WindowsSetDefaultRedirect.TYPE.FILE + ); + try { - winShell.launchOpenWithDefaultPickerForFileType(".pdf"); + const flags = this._isWindows11() + ? Ci.nsIWindowsShellService.OPEN_WITH_SET_HANDLER + : Ci.nsIWindowsShellService.OPEN_WITH_SET_HANDLER_WIN10; + this.shellService.launchSetDefaultAppPicker(openWithArg, flags); success = true; } catch (e) { + lazy.WindowsSetDefaultRedirect.clear(); // The picker API itself failed (e.g. COM error). Fall through to the // modern settings dialog rather than leaving the user without any // default-handler UI. @@ -487,7 +531,7 @@ let ShellServiceInternal = { if (!success && this._isWindows11()) { method = "settings"; try { - winShell.launchModernSettingsDialogDefaultApps(); + this.shellService.launchModernSettingsDialogDefaultApps(); Glean.browser.setDefaultPdfHandlerModernSettingsResult.Success.add(1); success = true; } catch (e) { @@ -524,9 +568,7 @@ let ShellServiceInternal = { */ isDefaultHandlerFor(aFileExtensionOrProtocol) { if (AppConstants.platform == "win") { - return this.shellService - .QueryInterface(Ci.nsIWindowsShellService) - .isDefaultHandlerFor(aFileExtensionOrProtocol); + return this.shellService.isDefaultHandlerFor(aFileExtensionOrProtocol); } return false; }, diff --git a/browser/components/shell/WindowsSetDefaultAppCmdHandler.sys.mjs b/browser/components/shell/WindowsSetDefaultAppCmdHandler.sys.mjs new file mode 100644 index 000000000000..88de305e30d8 --- /dev/null +++ b/browser/components/shell/WindowsSetDefaultAppCmdHandler.sys.mjs @@ -0,0 +1,97 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +/** + * Command-line handler for the Windows IOpenWithLauncher round-trip. + * + * When the ShellService.setAsDefault{PDF,Protocol}Handler launches the windows only + * "Open with" picker, it hands it a bundled stub file path for file-type + * defaults, or a URL for protocol defaults, and stashes a one-shot + * { openWithArg, overrideUri } redirect. + * + * If the user picks Firefox, Windows invokes Firefox with + * `-osint -url `. This handler runs before BrowserContentHandler and + * intercepts the launch: it asks ShellService whether the -url + * value matches the pending openWithArg and, if so, suppresses the + * open and optionally redirects to the stashed overrideUri so the user lands + * somewhere meaningful in Firefox. + */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs", + WindowsSetDefaultRedirect: + "moz-src:///browser/components/shell/WindowsSetDefaultRedirect.sys.mjs", +}); + +ChromeUtils.defineLazyGetter(lazy, "logConsole", () => { + return console.createInstance({ + prefix: "WindowsSetDefaultAppCmdHandler", + maxLogLevel: "Warn", + }); +}); + +export class CommandLineHandler { + static classID = Components.ID("{da7de528-7a15-452e-b5a7-521099997ca1}"); + static contractID = "@mozilla.org/browser/windows-default-clh;1"; + + QueryInterface = ChromeUtils.generateQI([Ci.nsICommandLineHandler]); + + handle(aCmdLine) { + if (aCmdLine.findFlag("osint", false) < 0) { + return; + } + + const urlIdx = aCmdLine.findFlag("url", false); + if (urlIdx < 0) { + return; + } + + const cmdArg = aCmdLine.getArgument(urlIdx + 1); + + // null: not our openWithArg, leave -url for BrowserContentHandler + const redirect = lazy.WindowsSetDefaultRedirect.consume(cmdArg); + if (!redirect) { + return; + } + + const { overrideUri } = redirect; + + lazy.logConsole.debug( + `Claimed IOpenWithLauncher openWithArg ${cmdArg}: state=${aCmdLine.state}, overrideUri=${overrideUri}` + ); + + // Consume the arg and suppress the default open so BrowserContentHandler + // doesn't act on it + aCmdLine.handleFlagWithParam("url", false); + aCmdLine.preventDefault = true; + if (overrideUri === null) { + return; + } + + lazy.logConsole.info( + `Redirecting IOpenWithLauncher round-trip to ${overrideUri}` + ); + + try { + const win = lazy.BrowserWindowTracker.getTopWindow(); + if (win) { + win.openTrustedLinkIn(overrideUri, "tab"); + return; + } + + const args = Cc["@mozilla.org/supports-string;1"].createInstance( + Ci.nsISupportsString + ); + args.data = overrideUri; + lazy.BrowserWindowTracker.openWindow({ args }); + } catch (e) { + lazy.logConsole.error( + `Failed to open redirect target ${overrideUri}:`, + e + ); + } + } +} diff --git a/browser/components/shell/WindowsSetDefaultRedirect.sys.mjs b/browser/components/shell/WindowsSetDefaultRedirect.sys.mjs new file mode 100644 index 000000000000..d2c8d5812ae7 --- /dev/null +++ b/browser/components/shell/WindowsSetDefaultRedirect.sys.mjs @@ -0,0 +1,135 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +/** + * The IOpenWithLauncher api call protocol shared by the producer + * (ShellService.setAsDefault{PDF,Protocol}Handler) and the consumer + * (WindowsSetDefaultAppCmdHandler). + * + * ShellService arms a one-shot redirect before launching the OS "Open with" + * picker via IOpenWithLauncher; once the user picks Firefox, the OS relaunches + * Firefox with the same value and the command-line handler consumes it. This + * module owns that shared state (the pref shape and the matching rules). + */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + FileUtils: "resource://gre/modules/FileUtils.sys.mjs", +}); + +// This pref is an object { openWithArg, overrideUri, type } consumed by +// WindowsSetDefaultAppCmdHandler when the user picks a default (file type or +// protocol) using the IOpenWithLauncher API. It is reset anytime the dialog is +// used again, or when we intercept the OS reopening one of our openWithArgs. +export const SET_DEFAULT_REDIRECT_PREF = + "browser.shell.setDefaultApp.pendingRedirect"; + +export class WindowsSetDefaultRedirect { + // Supported default types to set using IOpenWithLauncher. + static TYPE = { + FILE: 1 << 0, + PROTOCOL: 1 << 1, + }; + + /** + * Stash a one-shot redirect for the IOpenWithLauncher call. + * + * @param {string} openWithArg + * The value handed to launchSetDefaultAppPicker, which the OS hands back as + * "-osint -url " once the user picks a new default. Depending on + * type, this is either a file path on the system (file-type defaults) or a + * URL (protocol defaults). + * @param {?string} overrideUri + * URI spec to open when openWithArg comes back, or null to consume the relaunch + * and open nothing. + * @param {number} type + * One of WindowsSetDefaultRedirect.TYPE, identifying whether openWithArg is a + * file path or a URL. + */ + static arm(openWithArg, overrideUri, type) { + // Clear any stale object left by an older call. + Services.prefs.clearUserPref(SET_DEFAULT_REDIRECT_PREF); + + Services.prefs.setStringPref( + SET_DEFAULT_REDIRECT_PREF, + JSON.stringify({ openWithArg, overrideUri: overrideUri ?? null, type }) + ); + } + + /** + * Clear a pending redirect. + */ + static clear() { + Services.prefs.clearUserPref(SET_DEFAULT_REDIRECT_PREF); + } + + /** + * If `arg` is the openWithArg stashed by the most recent + * launchSetDefaultAppPicker call, consume the one-shot redirect and return + * its `{ overrideUri }`, where overrideUri is a URI spec to open or null to + * just suppress the relaunch. Returns null when `arg` is unrelated to a + * pending attempt to set a default. + * + * @param {string} arg - The -url value the OS handed back. + * @returns {?{overrideUri: ?string}} + */ + static consume(arg) { + const state = this.#read(); + if (!state || !this.#matches(state, arg)) { + return null; + } + Services.prefs.clearUserPref(SET_DEFAULT_REDIRECT_PREF); + return { overrideUri: state.overrideUri ?? null }; + } + + /** + * Read and validate the pending redirect stashed by arm(). + * + * @returns {?{openWithArg: string, overrideUri: ?string, type: number}} The + * stored state, or null when the pref is unset, holds the wrong type, or is + * malformed JSON. + */ + static #read() { + let raw; + try { + raw = Services.prefs.getStringPref(SET_DEFAULT_REDIRECT_PREF, ""); + } catch (e) { + return null; + } + if (!raw) { + return null; + } + try { + const state = JSON.parse(raw); + return state && typeof state.openWithArg === "string" ? state : null; + } catch (e) { + return null; + } + } + + /** + * Checks if the -url value the OS handed back matches the stashed redirect. + * + * @param {{openWithArg: string, type: number}} state - The stashed redirect. + * @param {string} arg - The -url value from the OS relaunch. + * @returns {boolean} + */ + static #matches(state, arg) { + switch (state.type) { + case this.TYPE.PROTOCOL: + return state.openWithArg === arg; + case this.TYPE.FILE: + try { + return new lazy.FileUtils.File(state.openWithArg).equals( + new lazy.FileUtils.File(arg) + ); + } catch (e) { + return false; + } + default: + return false; + } + } +} diff --git a/browser/components/shell/components.conf b/browser/components/shell/components.conf new file mode 100644 index 000000000000..58a246cd8245 --- /dev/null +++ b/browser/components/shell/components.conf @@ -0,0 +1,18 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# 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/. + +Classes = [] + +if buildconfig.substs["OS_ARCH"] == "WINNT": + Classes += [ + { + "cid": "{da7de528-7a15-452e-b5a7-521099997ca1}", + "contract_ids": ["@mozilla.org/browser/windows-default-clh;1"], + "categories": { + "command-line-handler": "l-windows-default", + }, + "esModule": "moz-src:///browser/components/shell/WindowsSetDefaultAppCmdHandler.sys.mjs", + "constructor": "CommandLineHandler", + }, + ] diff --git a/browser/components/shell/content/blank.pdf b/browser/components/shell/content/blank.pdf new file mode 100644 index 0000000000000000000000000000000000000000..ffff6d4bcf1981828a12363f8b0d79683c7c1e23 GIT binary patch literal 8959 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

1ATBh&wUZ13C{oLW% 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 V6JQT(8H3kp$OP2*@g~-${|Cx97v}%~ literal 0 HcmV?d00001 diff --git a/browser/components/shell/moz.build b/browser/components/shell/moz.build index 01fc43e1d938..55052eb44fe1 100644 --- a/browser/components/shell/moz.build +++ b/browser/components/shell/moz.build @@ -73,6 +73,15 @@ elif CONFIG["OS_ARCH"] == "WINNT": "crypt32", "propsys", ] + FINAL_TARGET_FILES += ["content/blank.pdf", "content/confused_fox.pdf"] + + MOZ_SRC_FILES += [ + "WindowsSetDefaultAppCmdHandler.sys.mjs", + ] + + XPCOM_MANIFESTS += [ + "components.conf", + ] XPIDL_MODULE = "shellservice" @@ -83,6 +92,7 @@ MOZ_SRC_FILES += [ "HeadlessShell.sys.mjs", "ScreenshotChild.sys.mjs", "ShellService.sys.mjs", + "WindowsSetDefaultRedirect.sys.mjs", ] MOZ_SRC_FILES += [ diff --git a/browser/components/shell/nsIWindowsShellService.idl b/browser/components/shell/nsIWindowsShellService.idl index aae7ba134a1a..d34c6a1edabf 100644 --- a/browser/components/shell/nsIWindowsShellService.idl +++ b/browser/components/shell/nsIWindowsShellService.idl @@ -10,6 +10,27 @@ interface nsIFile; [scriptable, uuid(fb9b59db-5a91-4e67-92b6-35e7d6e6d3fd)] interface nsIWindowsShellService : nsIShellService { + /* + * Bit flags for launchSetDefaultAppPicker's aFlags argument. They map 1:1 + * onto the flags accepted by the undocumented Windows IOpenWithLauncher COM + * interface's Launch method. + * The mapping below was determined empirically and determines the picker's + * messaging and actions. + */ + // Suppresses the default open-in-selected-app behavior. Remote-controlled + // by Microsoft. + const long OPEN_WITH_SUPPRESS_OPEN = 1 << 2; + // Switches picker messaging to protocol mode ("Select a default handler + // for ___ links"). + const long OPEN_WITH_PROTOCOL_MESSAGING = 1 << 3; + // Opens with the selected app just once; does not set a default. + const long OPEN_WITH_OPEN_ONCE = 1 << 6; + // Asks the user to set a default for the given file type or protocol. Messaging is + // ideal on Windows 11 only. + const long OPEN_WITH_SET_HANDLER = 1 << 7; + // Like OPEN_WITH_SET_HANDLER, but with ideal messaging for Windows 10. + const long OPEN_WITH_SET_HANDLER_WIN10 = 1 << 13; + /* * Creates a new shortcut (.lnk) file. This shortcut will be recorded in * a new shortcuts log file located in %PROGRAMDATA%\Mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38 @@ -349,16 +370,19 @@ interface nsIWindowsShellService : nsIShellService ); /* - * Launch the default app picker for a given file type via the Windows - * IOpenWithLauncher COM interface. + * Launch the default app picker via the Windows IOpenWithLauncher COM + * interface. The caller picks the target (a file path whose extension + * determines the picker for file-type defaults, or a URL for protocol + * defaults) and the flag bits that drive the picker's messaging and actions. * - * @param aFileType - * The file extension (e.g. ".pdf") for which to open - * the default app picker. + * @param aTarget + * File path or URL. + * @param aFlags + * Flag combination that determines the behavior of the IOpenWithLauncher dialog. * @throws NS_ERROR_FAILURE * If IOpenWithLauncher is unavailable or fails. */ - void launchOpenWithDefaultPickerForFileType(in AString aFileType); + void launchSetDefaultAppPicker(in AString aTarget, in long aFlags); /* * Open the Windows modern settings dialog for choosing default apps diff --git a/browser/components/shell/nsWindowsShellService.cpp b/browser/components/shell/nsWindowsShellService.cpp index b2b1bc836114..2b36517e5bd9 100644 --- a/browser/components/shell/nsWindowsShellService.cpp +++ b/browser/components/shell/nsWindowsShellService.cpp @@ -459,13 +459,21 @@ nsWindowsShellService::CanSetDefaultBrowserUserChoice(bool* aResult) { class __declspec(novtable) IOpenWithLauncher : public IUnknown { public: + // lpszPath selects what the picker offers to set as default. It accepts + // several shapes: + // - a file path: "C:\\path\\to\\file.pdf" + // - a file type: ".pdf" + // - a protocol: "http" + // - a protocol URI: "https://example.com", "mailto:foo@example.com" + // flags determines the messaging and actions available of the + // IOpenWithLauncher dialog. virtual HRESULT STDMETHODCALLTYPE Launch(HWND hWndParent, LPCWSTR lpszPath, int flags) = 0; }; NS_IMETHODIMP -nsWindowsShellService::LaunchOpenWithDefaultPickerForFileType( - const nsAString& aFileType) { +nsWindowsShellService::LaunchSetDefaultAppPicker(const nsAString& aTarget, + int32_t aFlags) { static constexpr GUID IID_IOpenWithLauncher = { 0x6a283fe2, 0xecfa, @@ -500,10 +508,7 @@ nsWindowsShellService::LaunchOpenWithDefaultPickerForFileType( // Make sure the dialog is foregrounded. CoAllowSetForegroundWindow(pOWL, nullptr); - // The flag is a bit of a mystery; on Win11+ 0x84 gives ideal messaging, on - // Win10 we use 0x2004. - int flag = mozilla::IsWin11OrLater() ? 0x84 : 0x2004; - hr = pOWL->Launch(nullptr, aFileType.Data(), flag); + hr = pOWL->Launch(nullptr, PromiseFlatString(aTarget).get(), aFlags); return SUCCEEDED(hr) ? NS_OK : NS_ERROR_FAILURE; } diff --git a/browser/components/shell/test/browser.toml b/browser/components/shell/test/browser.toml index 11e34f2bba1c..c409640d839e 100644 --- a/browser/components/shell/test/browser.toml +++ b/browser/components/shell/test/browser.toml @@ -120,3 +120,9 @@ tags = "os_integration" ["browser_setDesktopBackgroundPreview.js"] support-files = ["large.png", "canvas.html"] tags = "os_integration" + +["browser_windowsSetDefaultAppCmdHandler.js"] +run-if = [ + "os == 'win'", +] +tags = "os_integration" diff --git a/browser/components/shell/test/browser_setDefaultPDFHandler.js b/browser/components/shell/test/browser_setDefaultPDFHandler.js index 04cc1f069303..3ffa6cb573c4 100644 --- a/browser/components/shell/test/browser_setDefaultPDFHandler.js +++ b/browser/components/shell/test/browser_setDefaultPDFHandler.js @@ -8,6 +8,9 @@ ChromeUtils.defineESModuleGetters(this, { sinon: "resource://testing-common/Sinon.sys.mjs", }); +const confusedFoxPath = ShellService.getBundledPdfFile("confused_fox.pdf").path; +const SET_HANDLER_WIN11 = Ci.nsIWindowsShellService.OPEN_WITH_SET_HANDLER; + const setDefaultBrowserUserChoiceStub = sinon.stub(); const setDefaultExtensionHandlersUserChoiceStub = sinon .stub() @@ -34,17 +37,15 @@ const _userChoiceImpossibleTelemetryResultStub = sinon const setDefaultStub = sinon.stub(); // We'll dynamically update this as needed during the tests. const queryCurrentDefaultHandlerForStub = sinon.stub(); -const launchOpenWithDefaultPickerForFileTypeStub = sinon.stub(); +const launchSetDefaultAppPickerStub = sinon.stub(); const launchModernSettingsDialogDefaultAppsStub = sinon.stub(); const shellStub = sinon.stub(ShellService, "shellService").value({ setDefaultBrowser: setDefaultStub, queryCurrentDefaultHandlerFor: queryCurrentDefaultHandlerForStub, - QueryInterface: () => ({ - launchOpenWithDefaultPickerForFileType: - launchOpenWithDefaultPickerForFileTypeStub, - launchModernSettingsDialogDefaultApps: - launchModernSettingsDialogDefaultAppsStub, - }), + launchSetDefaultAppPicker: launchSetDefaultAppPickerStub, + launchModernSettingsDialogDefaultApps: + launchModernSettingsDialogDefaultAppsStub, + QueryInterface: ChromeUtils.generateQI([]), }); registerCleanupFunction(() => { @@ -228,7 +229,7 @@ add_task(async function test_setAsDefaultPDFHandler_knownBrowser() { const expectedArguments = [aumi, [".pdf", "FirefoxPDF"]]; const resetStubs = () => { setDefaultExtensionHandlersUserChoiceStub.resetHistory(); - launchOpenWithDefaultPickerForFileTypeStub.resetHistory(); + launchSetDefaultAppPickerStub.resetHistory(); launchModernSettingsDialogDefaultAppsStub.resetHistory(); }; @@ -250,7 +251,7 @@ add_task(async function test_setAsDefaultPDFHandler_knownBrowser() { "Called default browser agent with expected arguments" ); Assert.ok( - launchOpenWithDefaultPickerForFileTypeStub.notCalled, + launchSetDefaultAppPickerStub.notCalled, "Did not fall back to open-with picker" ); Assert.ok( @@ -271,7 +272,7 @@ add_task(async function test_setAsDefaultPDFHandler_knownBrowser() { "Called default browser agent with expected arguments" ); Assert.ok( - launchOpenWithDefaultPickerForFileTypeStub.notCalled, + launchSetDefaultAppPickerStub.notCalled, "Did not fall back to open-with picker" ); Assert.ok( @@ -289,7 +290,7 @@ add_task(async function test_setAsDefaultPDFHandler_knownBrowser() { "Did not use userChoice" ); Assert.ok( - launchOpenWithDefaultPickerForFileTypeStub.notCalled, + launchSetDefaultAppPickerStub.notCalled, "Did not fall back to open-with picker" ); Assert.ok( @@ -310,7 +311,7 @@ add_task(async function test_setAsDefaultPDFHandler_knownBrowser() { "Called default browser agent with expected arguments" ); Assert.ok( - launchOpenWithDefaultPickerForFileTypeStub.notCalled, + launchSetDefaultAppPickerStub.notCalled, "Did not fall back to open-with picker" ); Assert.ok( @@ -365,8 +366,11 @@ add_task(async function test_setAsDefaultPDFHandler_fallback() { Assert.ok(userChoiceStub.called, "Attempted userChoice"); Assert.ok( - launchOpenWithDefaultPickerForFileTypeStub.calledWith(".pdf"), - "Fell back to open-with picker for .pdf" + launchSetDefaultAppPickerStub.calledWith( + confusedFoxPath, + SET_HANDLER_WIN11 + ), + "Fell back to open-with picker with bundled PDF path and Win11 flag" ); Assert.ok( launchModernSettingsDialogDefaultAppsStub.notCalled, @@ -392,7 +396,7 @@ add_task(async function test_setAsDefaultPDFHandler_fallback() { ); userChoiceStub.resetHistory(); isDefaultHandlerForStub.resetHistory(); - launchOpenWithDefaultPickerForFileTypeStub.resetHistory(); + launchSetDefaultAppPickerStub.resetHistory(); launchModernSettingsDialogDefaultAppsStub.resetHistory(); info( @@ -413,22 +417,25 @@ add_task(async function test_setAsDefaultPDFHandler_fallback() { isDefaultHandlerForStub.returns(true); userChoiceStub.resetHistory(); isDefaultHandlerForStub.resetHistory(); - launchOpenWithDefaultPickerForFileTypeStub.resetHistory(); + launchSetDefaultAppPickerStub.resetHistory(); launchModernSettingsDialogDefaultAppsStub.resetHistory(); info( "When userChoice fails and open-with picker fails, should fall back to settings dialog" ); Services.fog.testResetFOG(); - launchOpenWithDefaultPickerForFileTypeStub.throws( + launchSetDefaultAppPickerStub.throws( new Error("mock IOpenWithLauncher failure") ); await ShellService.setAsDefaultPDFHandler(false); Assert.ok(userChoiceStub.called, "Attempted userChoice"); Assert.ok( - launchOpenWithDefaultPickerForFileTypeStub.calledWith(".pdf"), - "Attempted open-with picker for .pdf" + launchSetDefaultAppPickerStub.calledWith( + confusedFoxPath, + SET_HANDLER_WIN11 + ), + "Attempted open-with picker with bundled PDF path and Win11 flag" ); Assert.ok( launchModernSettingsDialogDefaultAppsStub.called, @@ -463,7 +470,7 @@ add_task(async function test_setAsDefaultPDFHandler_fallback() { ); userChoiceStub.resetHistory(); isDefaultHandlerForStub.resetHistory(); - launchOpenWithDefaultPickerForFileTypeStub.resetHistory(); + launchSetDefaultAppPickerStub.resetHistory(); launchModernSettingsDialogDefaultAppsStub.resetHistory(); info( @@ -499,7 +506,7 @@ add_task(async function test_setAsDefaultPDFHandler_fallback() { "Event result_is_default is false when no method set the default" ); } finally { - launchOpenWithDefaultPickerForFileTypeStub.reset(); + launchSetDefaultAppPickerStub.reset(); launchModernSettingsDialogDefaultAppsStub.reset(); sandbox.restore(); await SpecialPowers.popPrefEnv(); @@ -529,7 +536,7 @@ add_task(async function test_setAsDefaultPDFHandler_useOpenWithDisabled() { await ShellService.setAsDefaultPDFHandler(false); Assert.ok( - launchOpenWithDefaultPickerForFileTypeStub.notCalled, + launchSetDefaultAppPickerStub.notCalled, "Did not invoke open-with picker when pref is disabled" ); Assert.ok( @@ -550,7 +557,7 @@ add_task(async function test_setAsDefaultPDFHandler_useOpenWithDisabled() { "Event result_is_default reflects isDefaultHandlerFor" ); } finally { - launchOpenWithDefaultPickerForFileTypeStub.reset(); + launchSetDefaultAppPickerStub.reset(); launchModernSettingsDialogDefaultAppsStub.reset(); sandbox.restore(); await SpecialPowers.popPrefEnv(); diff --git a/browser/components/shell/test/browser_windowsSetDefaultAppCmdHandler.js b/browser/components/shell/test/browser_windowsSetDefaultAppCmdHandler.js new file mode 100644 index 000000000000..62c31979f9c3 --- /dev/null +++ b/browser/components/shell/test/browser_windowsSetDefaultAppCmdHandler.js @@ -0,0 +1,293 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { CommandLineHandler } = ChromeUtils.importESModule( + "moz-src:///browser/components/shell/WindowsSetDefaultAppCmdHandler.sys.mjs" +); + +ChromeUtils.defineESModuleGetters(this, { + BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs", + SET_DEFAULT_REDIRECT_PREF: + "moz-src:///browser/components/shell/WindowsSetDefaultRedirect.sys.mjs", + ShellService: "moz-src:///browser/components/shell/ShellService.sys.mjs", + WindowsSetDefaultRedirect: + "moz-src:///browser/components/shell/WindowsSetDefaultRedirect.sys.mjs", + sinon: "resource://testing-common/Sinon.sys.mjs", +}); + +Assert.equal(AppConstants.platform, "win", "Platform is Windows"); + +const confusedFoxPath = ShellService.getBundledPdfFile("confused_fox.pdf").path; +const blankURISpec = Services.io.newFileURI( + ShellService.getBundledPdfFile("blank.pdf") +).spec; + +const workingDir = Services.dirsvc.get("GreD", Ci.nsIFile); + +// Build a real nsICommandLine from an args array, so we exercise the same +// flag-parsing the OS-initiated launch goes through +function makeCmdLine(args, state = Ci.nsICommandLine.STATE_INITIAL_LAUNCH) { + return Cu.createCommandLine(args, workingDir, state); +} + +// Arm the one-shot redirect via the real ShellService helper, the way +// setAsDefault{*}Handler does right before launching the OS picker. These +// tests all use file openWithArgs, so default the type. +function armRedirect( + openWithArg, + overrideUri, + type = WindowsSetDefaultRedirect.TYPE.FILE +) { + WindowsSetDefaultRedirect.arm(openWithArg, overrideUri, type); +} + +let fakeWin; +let getTopWindowStub; +let openWindowStub; + +add_setup(function () { + fakeWin = { openTrustedLinkIn: sinon.stub() }; + // Stubbed rather than spied: getTopWindow must return our fake window (and + // null on demand), and openWindow must not actually open a window in the + // test harness. We only inspect the calls. + getTopWindowStub = sinon.stub(BrowserWindowTracker, "getTopWindow"); + openWindowStub = sinon.stub(BrowserWindowTracker, "openWindow"); +}); + +registerCleanupFunction(() => { + sinon.restore(); + Services.prefs.clearUserPref(SET_DEFAULT_REDIRECT_PREF); +}); + +function resetState() { + fakeWin.openTrustedLinkIn.resetHistory(); + getTopWindowStub.reset(); + getTopWindowStub.returns(fakeWin); + openWindowStub.reset(); + Services.prefs.clearUserPref(SET_DEFAULT_REDIRECT_PREF); +} + +add_task(async function test_no_osint_returns_early() { + resetState(); + armRedirect(confusedFoxPath, blankURISpec); + const cmdLine = makeCmdLine(["-url", confusedFoxPath]); + new CommandLineHandler().handle(cmdLine); + + Assert.equal( + cmdLine.preventDefault, + false, + "preventDefault left untouched without -osint" + ); + Assert.greaterOrEqual( + cmdLine.findFlag("url", false), + 0, + "-url remains for the next handler" + ); + Assert.ok( + fakeWin.openTrustedLinkIn.notCalled, + "No tab opened without -osint" + ); + Assert.ok(openWindowStub.notCalled, "No window opened without -osint"); +}); + +add_task(async function test_osint_without_url_returns_early() { + resetState(); + armRedirect(confusedFoxPath, blankURISpec); + const cmdLine = makeCmdLine(["-osint"]); + new CommandLineHandler().handle(cmdLine); + + Assert.equal( + cmdLine.preventDefault, + false, + "preventDefault left untouched without -url" + ); + Assert.ok(fakeWin.openTrustedLinkIn.notCalled, "No tab opened without -url"); + Assert.ok(openWindowStub.notCalled, "No window opened without -url"); +}); + +add_task(async function test_no_pending_redirect_leaves_arg() { + // No armed redirect: even a -url that looks like our stub PDF is a real, + // user-initiated open (e.g. they double-clicked the file), so we must leave + // it for BrowserContentHandler. + resetState(); + + const cmdLine = makeCmdLine(["-osint", "-url", confusedFoxPath]); + new CommandLineHandler().handle(cmdLine); + + Assert.equal( + cmdLine.preventDefault, + false, + "preventDefault not set when no redirect is pending" + ); + Assert.greaterOrEqual( + cmdLine.findFlag("url", false), + 0, + "-url preserved for BrowserContentHandler" + ); + Assert.ok( + fakeWin.openTrustedLinkIn.notCalled, + "No tab opened when no redirect is pending" + ); + Assert.ok( + openWindowStub.notCalled, + "No window opened when no redirect is pending" + ); +}); + +add_task(async function test_unrelated_url_arg_is_ignored() { + resetState(); + armRedirect(confusedFoxPath, blankURISpec); + + const cmdLine = makeCmdLine([ + "-osint", + "-url", + "https://example.com/some-page", + ]); + new CommandLineHandler().handle(cmdLine); + + Assert.equal( + cmdLine.preventDefault, + false, + "preventDefault not set for a -url that isn't the pending openWithArg" + ); + Assert.greaterOrEqual( + cmdLine.findFlag("url", false), + 0, + "-url preserved for subsequent handler" + ); + Assert.ok( + fakeWin.openTrustedLinkIn.notCalled, + "No tab opened for an unrelated -url" + ); + Assert.ok( + Services.prefs.prefHasUserValue(SET_DEFAULT_REDIRECT_PREF), + "Pending redirect untouched when the openWithArg doesn't match" + ); +}); + +add_task(async function test_suppress_only_when_target_null() { + resetState(); + armRedirect(confusedFoxPath, null); + + const cmdLine = makeCmdLine(["-osint", "-url", confusedFoxPath]); + new CommandLineHandler().handle(cmdLine); + + Assert.equal( + cmdLine.preventDefault, + true, + "openWithArg suppressed so BrowserContentHandler skips it" + ); + Assert.equal( + cmdLine.findFlag("url", false), + -1, + "-url consumed even with no redirect target" + ); + Assert.ok( + fakeWin.openTrustedLinkIn.notCalled, + "No redirect when target is null" + ); + Assert.ok(openWindowStub.notCalled, "No fallback window when target is null"); + Assert.ok( + !Services.prefs.prefHasUserValue(SET_DEFAULT_REDIRECT_PREF), + "Redirect intent consumed" + ); +}); + +add_task(async function test_redirects_to_top_window() { + resetState(); + armRedirect(confusedFoxPath, blankURISpec); + + const cmdLine = makeCmdLine(["-osint", "-url", confusedFoxPath]); + new CommandLineHandler().handle(cmdLine); + + Assert.equal( + cmdLine.preventDefault, + true, + "Default open suppressed for the pending openWithArg" + ); + Assert.equal(cmdLine.findFlag("url", false), -1, "-url consumed"); + Assert.ok( + fakeWin.openTrustedLinkIn.calledOnce, + "Redirected into the top window" + ); + Assert.deepEqual(fakeWin.openTrustedLinkIn.firstCall.args, [ + blankURISpec, + "tab", + ]); + Assert.ok(openWindowStub.notCalled, "No new window when one exists"); + Assert.ok( + !Services.prefs.prefHasUserValue(SET_DEFAULT_REDIRECT_PREF), + "Redirect intent is one-shot and cleared after use" + ); +}); + +add_task(async function test_opens_new_window_when_no_top() { + resetState(); + getTopWindowStub.returns(null); + armRedirect(confusedFoxPath, blankURISpec); + + const cmdLine = makeCmdLine(["-osint", "-url", confusedFoxPath]); + new CommandLineHandler().handle(cmdLine); + + Assert.equal( + cmdLine.preventDefault, + true, + "Default open suppressed even when no top window exists" + ); + Assert.ok( + fakeWin.openTrustedLinkIn.notCalled, + "Top-window path skipped when getTopWindow returns null" + ); + Assert.ok(openWindowStub.calledOnce, "Falls back to openWindow"); + + const opts = openWindowStub.firstCall.args[0]; + Assert.ok( + opts && opts.args, + "openWindow called with a {args} options object" + ); + Assert.ok( + opts.args instanceof Ci.nsISupportsString, + "args is an nsISupportsString" + ); + Assert.equal( + opts.args.data, + blankURISpec, + "nsISupportsString carries the redirect URI" + ); + Assert.ok( + !Services.prefs.prefHasUserValue(SET_DEFAULT_REDIRECT_PREF), + "Redirect intent is one-shot and cleared after use" + ); +}); + +add_task(async function test_intent_is_one_shot() { + resetState(); + armRedirect(confusedFoxPath, blankURISpec); + + new CommandLineHandler().handle( + makeCmdLine(["-osint", "-url", confusedFoxPath]) + ); + Assert.ok( + fakeWin.openTrustedLinkIn.calledOnce, + "First call honors the pending redirect" + ); + + fakeWin.openTrustedLinkIn.resetHistory(); + const second = makeCmdLine(["-osint", "-url", confusedFoxPath]); + new CommandLineHandler().handle(second); + + Assert.equal( + second.preventDefault, + false, + "Second call leaves the openWithArg alone because the intent was consumed" + ); + Assert.greaterOrEqual( + second.findFlag("url", false), + 0, + "-url preserved on the second call (no pending redirect)" + ); + Assert.ok( + fakeWin.openTrustedLinkIn.notCalled, + "Second call does not redirect" + ); +}); diff --git a/browser/installer/package-manifest.in b/browser/installer/package-manifest.in index 83e52b260c38..203811a0fb98 100644 --- a/browser/installer/package-manifest.in +++ b/browser/installer/package-manifest.in @@ -221,6 +221,13 @@ @BINPATH@/@MOZ_DXC_DLL_NAME@ #endif +; Bundled stub PDFs handed to the OS "Open with" picker by the +; set-default-PDF-handler flow (see WindowsSetDefaultAppCmdHandler). +#ifdef XP_WIN +@RESPATH@/browser/confused_fox.pdf +@RESPATH@/browser/blank.pdf +#endif + ; [Browser Chrome Files] @RESPATH@/browser/chrome.manifest @RESPATH@/browser/chrome/browser@JAREXT@ diff --git a/toolkit/components/messaging-system/lib/SpecialMessageActions.sys.mjs b/toolkit/components/messaging-system/lib/SpecialMessageActions.sys.mjs index 6aca21afcc4a..ad02aa48f3b8 100644 --- a/toolkit/components/messaging-system/lib/SpecialMessageActions.sys.mjs +++ b/toolkit/components/messaging-system/lib/SpecialMessageActions.sys.mjs @@ -185,8 +185,14 @@ export const SpecialMessageActions = { * * @param {Window} window Reference to a window object */ - async setDefaultPDFHandler(window, onlyIfKnownBrowser = false) { - await window.getShellService().setAsDefaultPDFHandler(onlyIfKnownBrowser); + async setDefaultPDFHandler( + window, + onlyIfKnownBrowser = false, + openInFirefox = false + ) { + await window + .getShellService() + .setAsDefaultPDFHandler(onlyIfKnownBrowser, openInFirefox); }, /** @@ -815,7 +821,8 @@ export const SpecialMessageActions = { case "SET_DEFAULT_PDF_HANDLER": await this.setDefaultPDFHandler( window, - action.data?.onlyIfKnownBrowser ?? false + action.data?.onlyIfKnownBrowser ?? false, + action.data?.openInFirefox ?? false ); break; case "DECLINE_DEFAULT_PDF_HANDLER": diff --git a/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/SpecialMessageActionSchemas.json b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/SpecialMessageActionSchemas.json index beb347d42f7e..01270166f1ab 100644 --- a/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/SpecialMessageActionSchemas.json +++ b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/SpecialMessageActionSchemas.json @@ -478,6 +478,10 @@ "onlyIfKnownBrowser": { "type": "boolean", "description": "Only set Firefox as the default PDF handler if the current PDF handler is a known browser." + }, + "openInFirefox": { + "type": "boolean", + "description": "If the OS hands the stub PDF back to Firefox after the user picks Firefox in the open-with dialog, open a follow-up PDF in a new tab instead of suppressing the launch." } }, "additionalProperties": false diff --git a/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/index.md b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/index.md index c3daa6fa36e7..49322eeb6816 100644 --- a/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/index.md +++ b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/index.md @@ -267,6 +267,10 @@ Windows only. // Only set Firefox as the default PDF handler if the current PDF handler is a // known browser. onlyIfKnownBrowser?: boolean; + // If the OS hands the stub PDF back to Firefox after the user picks Firefox + // in the open-with dialog, open a follow-up PDF in a new tab instead of + // suppressing the launch. + openInFirefox?: boolean; } ``` diff --git a/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_default_pdf_handler.js b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_default_pdf_handler.js index edb26cc3c296..012e6b9d0295 100644 --- a/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_default_pdf_handler.js +++ b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_default_pdf_handler.js @@ -24,7 +24,7 @@ add_task(async function test_set_default_pdf_handler_no_data() { "setAsDefaultPDFHandler was called by the action" ); Assert.ok( - stub.calledWithExactly(false), + stub.calledWithExactly(false, false), "setAsDefaultPDFHandler called with onlyIfKnownBrowser = false" ); }); @@ -55,7 +55,7 @@ add_task(async function test_set_default_pdf_handler_data_false() { "setAsDefaultPDFHandler was called by the action" ); Assert.ok( - stub.calledWithExactly(false), + stub.calledWithExactly(false, false), "setAsDefaultPDFHandler called with onlyIfKnownBrowser = false" ); }); @@ -86,7 +86,7 @@ add_task(async function test_set_default_pdf_handler_data_true() { "setAsDefaultPDFHandler was called by the action" ); Assert.ok( - stub.calledWithExactly(true), + stub.calledWithExactly(true, false), "setAsDefaultPDFHandler called with onlyIfKnownBrowser = true" ); });