Bug 1678255 - prompt for external protocol links whose loads were also triggered externally, instead of looping forever, r=pbz,nika
This passes around the "are we external" bit of load information a bunch, such that the external protocol handling code has access to it. In this bug and bug 1667468, I think ideally I would have used a check if we're the OS default for a given protocol before continuing. However, this information is currently unavailable on Linux (bug 1599713), and worse, I believe is likely to remain unavailable in flatpak and other such restricted environments (cf. bug 1618094 - we aren't able to find out anything about protocol handlers from the OS). So instead, we prompt the user if we are about to open a link passed to us externally. There is a small chance this will be Breaking People's Workflows, where I don't know whether anyone relies on Firefox happily passing these URIs along to the relevant application (more convenient than doing all the registry/API work yourself in scripts!) or anything like that. To help with that, there's a pref, `network.protocol-handler.prompt-from-external`, that can be created and set to false to avoid prompting in this case. Differential Revision: https://phabricator.services.mozilla.com/D103967
This commit is contained in:
@@ -12766,7 +12766,8 @@ nsresult nsDocShell::OnLinkClickSync(nsIContent* aContent,
|
|||||||
extProtService->IsExposedProtocol(scheme.get(), &isExposed);
|
extProtService->IsExposedProtocol(scheme.get(), &isExposed);
|
||||||
if (NS_SUCCEEDED(rv) && !isExposed) {
|
if (NS_SUCCEEDED(rv) && !isExposed) {
|
||||||
return extProtService->LoadURI(aLoadState->URI(), triggeringPrincipal,
|
return extProtService->LoadURI(aLoadState->URI(), triggeringPrincipal,
|
||||||
mBrowsingContext);
|
mBrowsingContext,
|
||||||
|
/* aTriggeredExternally */ false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4473,7 +4473,8 @@ mozilla::ipc::IPCResult ContentParent::RecvAccumulateMixedContentHSTS(
|
|||||||
|
|
||||||
mozilla::ipc::IPCResult ContentParent::RecvLoadURIExternal(
|
mozilla::ipc::IPCResult ContentParent::RecvLoadURIExternal(
|
||||||
nsIURI* uri, nsIPrincipal* aTriggeringPrincipal,
|
nsIURI* uri, nsIPrincipal* aTriggeringPrincipal,
|
||||||
const MaybeDiscarded<BrowsingContext>& aContext) {
|
const MaybeDiscarded<BrowsingContext>& aContext,
|
||||||
|
bool aWasExternallyTriggered) {
|
||||||
if (aContext.IsDiscarded()) {
|
if (aContext.IsDiscarded()) {
|
||||||
return IPC_OK();
|
return IPC_OK();
|
||||||
}
|
}
|
||||||
@@ -4489,7 +4490,8 @@ mozilla::ipc::IPCResult ContentParent::RecvLoadURIExternal(
|
|||||||
}
|
}
|
||||||
|
|
||||||
BrowsingContext* bc = aContext.get();
|
BrowsingContext* bc = aContext.get();
|
||||||
extProtService->LoadURI(uri, aTriggeringPrincipal, bc);
|
extProtService->LoadURI(uri, aTriggeringPrincipal, bc,
|
||||||
|
aWasExternallyTriggered);
|
||||||
return IPC_OK();
|
return IPC_OK();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1087,7 +1087,8 @@ class ContentParent final
|
|||||||
|
|
||||||
mozilla::ipc::IPCResult RecvLoadURIExternal(
|
mozilla::ipc::IPCResult RecvLoadURIExternal(
|
||||||
nsIURI* uri, nsIPrincipal* triggeringPrincipal,
|
nsIURI* uri, nsIPrincipal* triggeringPrincipal,
|
||||||
const MaybeDiscarded<BrowsingContext>& aContext);
|
const MaybeDiscarded<BrowsingContext>& aContext,
|
||||||
|
bool aWasExternallyTriggered);
|
||||||
mozilla::ipc::IPCResult RecvExtProtocolChannelConnectParent(
|
mozilla::ipc::IPCResult RecvExtProtocolChannelConnectParent(
|
||||||
const uint64_t& registrarId);
|
const uint64_t& registrarId);
|
||||||
|
|
||||||
|
|||||||
@@ -1047,7 +1047,10 @@ parent:
|
|||||||
async StartVisitedQueries(nsIURI[] uri);
|
async StartVisitedQueries(nsIURI[] uri);
|
||||||
async SetURITitle(nsIURI uri, nsString title);
|
async SetURITitle(nsIURI uri, nsString title);
|
||||||
|
|
||||||
async LoadURIExternal(nsIURI uri, nsIPrincipal triggeringPrincipal, MaybeDiscardedBrowsingContext browsingContext);
|
async LoadURIExternal(nsIURI uri,
|
||||||
|
nsIPrincipal triggeringPrincipal,
|
||||||
|
MaybeDiscardedBrowsingContext browsingContext,
|
||||||
|
bool wasExternallyTriggered);
|
||||||
async ExtProtocolChannelConnectParent(uint64_t registrarId);
|
async ExtProtocolChannelConnectParent(uint64_t registrarId);
|
||||||
|
|
||||||
// PrefService message
|
// PrefService message
|
||||||
|
|||||||
@@ -19,6 +19,14 @@ var EXPORTED_SYMBOLS = [
|
|||||||
"ContentDispatchChooserTelemetry",
|
"ContentDispatchChooserTelemetry",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const gPrefs = {};
|
||||||
|
XPCOMUtils.defineLazyPreferenceGetter(
|
||||||
|
gPrefs,
|
||||||
|
"promptForExternal",
|
||||||
|
"network.protocol-handler.prompt-from-external",
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
const PROTOCOL_HANDLER_OPEN_PERM_KEY = "open-protocol-handler";
|
const PROTOCOL_HANDLER_OPEN_PERM_KEY = "open-protocol-handler";
|
||||||
const PERMISSION_KEY_DELIMITER = "^";
|
const PERMISSION_KEY_DELIMITER = "^";
|
||||||
|
|
||||||
@@ -238,13 +246,27 @@ class nsContentDispatchChooser {
|
|||||||
* @param {nsIURI} aURI - URI to be handled.
|
* @param {nsIURI} aURI - URI to be handled.
|
||||||
* @param {nsIPrincipal} [aPrincipal] - Principal which triggered the load.
|
* @param {nsIPrincipal} [aPrincipal] - Principal which triggered the load.
|
||||||
* @param {BrowsingContext} [aBrowsingContext] - Context of the load.
|
* @param {BrowsingContext} [aBrowsingContext] - Context of the load.
|
||||||
|
* @param {bool} [aTriggeredExternally] - Whether the load came from outside
|
||||||
|
* this application.
|
||||||
*/
|
*/
|
||||||
async handleURI(aHandler, aURI, aPrincipal, aBrowsingContext) {
|
async handleURI(
|
||||||
|
aHandler,
|
||||||
|
aURI,
|
||||||
|
aPrincipal,
|
||||||
|
aBrowsingContext,
|
||||||
|
aTriggeredExternally = false
|
||||||
|
) {
|
||||||
let callerHasPermission = this._hasProtocolHandlerPermission(
|
let callerHasPermission = this._hasProtocolHandlerPermission(
|
||||||
aHandler.type,
|
aHandler.type,
|
||||||
aPrincipal
|
aPrincipal
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Force showing the dialog for links passed from outside the application.
|
||||||
|
// This avoids infinite loops, see bug 1678255, bug 1667468, etc.
|
||||||
|
if (aTriggeredExternally && gPrefs.promptForExternal) {
|
||||||
|
aHandler.alwaysAskBeforeHandling = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Skip the dialog if a preferred application is set and the caller has
|
// Skip the dialog if a preferred application is set and the caller has
|
||||||
// permission.
|
// permission.
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -1010,12 +1010,13 @@ static const char kExternalProtocolDefaultPref[] =
|
|||||||
NS_IMETHODIMP
|
NS_IMETHODIMP
|
||||||
nsExternalHelperAppService::LoadURI(nsIURI* aURI,
|
nsExternalHelperAppService::LoadURI(nsIURI* aURI,
|
||||||
nsIPrincipal* aTriggeringPrincipal,
|
nsIPrincipal* aTriggeringPrincipal,
|
||||||
BrowsingContext* aBrowsingContext) {
|
BrowsingContext* aBrowsingContext,
|
||||||
|
bool aTriggeredExternally) {
|
||||||
NS_ENSURE_ARG_POINTER(aURI);
|
NS_ENSURE_ARG_POINTER(aURI);
|
||||||
|
|
||||||
if (XRE_IsContentProcess()) {
|
if (XRE_IsContentProcess()) {
|
||||||
mozilla::dom::ContentChild::GetSingleton()->SendLoadURIExternal(
|
mozilla::dom::ContentChild::GetSingleton()->SendLoadURIExternal(
|
||||||
aURI, aTriggeringPrincipal, aBrowsingContext);
|
aURI, aTriggeringPrincipal, aBrowsingContext, aTriggeredExternally);
|
||||||
return NS_OK;
|
return NS_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1117,7 +1118,7 @@ nsExternalHelperAppService::LoadURI(nsIURI* aURI,
|
|||||||
NS_ENSURE_SUCCESS(rv, rv);
|
NS_ENSURE_SUCCESS(rv, rv);
|
||||||
|
|
||||||
return chooser->HandleURI(handler, uri, aTriggeringPrincipal,
|
return chooser->HandleURI(handler, uri, aTriggeringPrincipal,
|
||||||
aBrowsingContext);
|
aBrowsingContext, aTriggeredExternally);
|
||||||
}
|
}
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|||||||
@@ -82,7 +82,8 @@ class nsExternalHelperAppService : public nsIExternalHelperAppService,
|
|||||||
NS_IMETHOD GetProtocolHandlerInfo(const nsACString& aScheme,
|
NS_IMETHOD GetProtocolHandlerInfo(const nsACString& aScheme,
|
||||||
nsIHandlerInfo** aHandlerInfo) override;
|
nsIHandlerInfo** aHandlerInfo) override;
|
||||||
NS_IMETHOD LoadURI(nsIURI* aURI, nsIPrincipal* aTriggeringPrincipal,
|
NS_IMETHOD LoadURI(nsIURI* aURI, nsIPrincipal* aTriggeringPrincipal,
|
||||||
mozilla::dom::BrowsingContext* aBrowsingContext) override;
|
mozilla::dom::BrowsingContext* aBrowsingContext,
|
||||||
|
bool aWasTriggeredExternally) override;
|
||||||
NS_IMETHOD SetProtocolHandlerDefaults(nsIHandlerInfo* aHandlerInfo,
|
NS_IMETHOD SetProtocolHandlerDefaults(nsIHandlerInfo* aHandlerInfo,
|
||||||
bool aOSHandlerExists) override;
|
bool aOSHandlerExists) override;
|
||||||
|
|
||||||
|
|||||||
@@ -165,7 +165,8 @@ nsresult nsExtProtocolChannel::OpenURL() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
RefPtr<nsIPrincipal> principal = mLoadInfo->TriggeringPrincipal();
|
RefPtr<nsIPrincipal> principal = mLoadInfo->TriggeringPrincipal();
|
||||||
rv = extProtService->LoadURI(mUrl, principal, ctx);
|
rv = extProtService->LoadURI(mUrl, principal, ctx,
|
||||||
|
mLoadInfo->GetLoadTriggeredFromExternal());
|
||||||
|
|
||||||
if (NS_SUCCEEDED(rv) && mListener) {
|
if (NS_SUCCEEDED(rv) && mListener) {
|
||||||
mStatus = NS_ERROR_NO_CONTENT;
|
mStatus = NS_ERROR_NO_CONTENT;
|
||||||
|
|||||||
@@ -29,10 +29,13 @@ interface nsIContentDispatchChooser : nsISupports {
|
|||||||
* The principal making the request.
|
* The principal making the request.
|
||||||
* @param aBrowsingContext
|
* @param aBrowsingContext
|
||||||
* The browsing context where the load should happen.
|
* The browsing context where the load should happen.
|
||||||
|
* @param aWasTriggeredExternally
|
||||||
|
* True if the load was tripped by an external app.
|
||||||
*/
|
*/
|
||||||
void handleURI(in nsIHandlerInfo aHandler,
|
void handleURI(in nsIHandlerInfo aHandler,
|
||||||
in nsIURI aURI,
|
in nsIURI aURI,
|
||||||
in nsIPrincipal aTriggeringPrincipal,
|
in nsIPrincipal aTriggeringPrincipal,
|
||||||
in BrowsingContext aBrowsingContext);
|
in BrowsingContext aBrowsingContext,
|
||||||
|
[optional] in bool aWasTriggeredExternally);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -109,13 +109,17 @@ interface nsIExternalProtocolService : nsISupports
|
|||||||
* handlers will fail. We need to do better than that; bug 394483
|
* handlers will fail. We need to do better than that; bug 394483
|
||||||
* filed in order to track.
|
* filed in order to track.
|
||||||
*
|
*
|
||||||
|
* @param aWasTriggeredExternally
|
||||||
|
* If true, indicates the load was initiated by an external app.
|
||||||
|
*
|
||||||
* @note Embedders that do not expose the http protocol should not currently
|
* @note Embedders that do not expose the http protocol should not currently
|
||||||
* use web-based protocol handlers, as handoff won't work correctly
|
* use web-based protocol handlers, as handoff won't work correctly
|
||||||
* (bug 394479).
|
* (bug 394479).
|
||||||
*/
|
*/
|
||||||
void loadURI(in nsIURI aURI,
|
void loadURI(in nsIURI aURI,
|
||||||
[optional] in nsIPrincipal aTriggeringPrincipal,
|
[optional] in nsIPrincipal aTriggeringPrincipal,
|
||||||
[optional] in BrowsingContext aBrowsingContext);
|
[optional] in BrowsingContext aBrowsingContext,
|
||||||
|
[optional] in bool aWasTriggeredExternally);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a human-readable description for the application responsible for
|
* Gets a human-readable description for the application responsible for
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ support-files =
|
|||||||
[browser_first_prompt_not_blocked_without_user_interaction.js]
|
[browser_first_prompt_not_blocked_without_user_interaction.js]
|
||||||
support-files =
|
support-files =
|
||||||
file_external_protocol_iframe.html
|
file_external_protocol_iframe.html
|
||||||
|
[browser_protocol_ask_dialog_external.js]
|
||||||
[browser_protocol_ask_dialog_permission.js]
|
[browser_protocol_ask_dialog_permission.js]
|
||||||
[browser_protocolhandler_loop.js]
|
[browser_protocolhandler_loop.js]
|
||||||
[browser_remember_download_option.js]
|
[browser_remember_download_option.js]
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
/* Any copyright is dedicated to the Public Domain.
|
||||||
|
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
ChromeUtils.import(
|
||||||
|
"resource://testing-common/HandlerServiceTestUtils.jsm",
|
||||||
|
this
|
||||||
|
);
|
||||||
|
|
||||||
|
let gHandlerService = Cc["@mozilla.org/uriloader/handler-service;1"].getService(
|
||||||
|
Ci.nsIHandlerService
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates dummy protocol handler
|
||||||
|
*/
|
||||||
|
function initTestHandlers() {
|
||||||
|
let handlerInfo = HandlerServiceTestUtils.getBlankHandlerInfo("yoink");
|
||||||
|
|
||||||
|
let appHandler = Cc[
|
||||||
|
"@mozilla.org/uriloader/local-handler-app;1"
|
||||||
|
].createInstance(Ci.nsILocalHandlerApp);
|
||||||
|
// This is a dir and not executable, but that's enough for here.
|
||||||
|
appHandler.executable = Services.dirsvc.get("XCurProcD", Ci.nsIFile);
|
||||||
|
handlerInfo.possibleApplicationHandlers.appendElement(appHandler);
|
||||||
|
handlerInfo.preferredApplicationHandler = appHandler;
|
||||||
|
handlerInfo.preferredAction = handlerInfo.useHelperApp;
|
||||||
|
handlerInfo.alwaysAskBeforeHandling = false;
|
||||||
|
gHandlerService.store(handlerInfo);
|
||||||
|
registerCleanupFunction(() => {
|
||||||
|
gHandlerService.remove(handlerInfo);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that if we get a direct request from another app / the OS to open a
|
||||||
|
* link, we always prompt, even if we think we know what the correct answer
|
||||||
|
* is. This is to avoid infinite loops in such situations where the OS and
|
||||||
|
* Firefox have conflicting ideas about the default handler, or where our
|
||||||
|
* checks with the OS don't work (Linux and/or Snap, at time of this comment).
|
||||||
|
*/
|
||||||
|
add_task(async function test_external_asks_anyway() {
|
||||||
|
await SpecialPowers.pushPrefEnv({
|
||||||
|
set: [["network.protocol-handler.prompt-from-external", true]],
|
||||||
|
});
|
||||||
|
initTestHandlers();
|
||||||
|
|
||||||
|
let cmdLineHandler = Cc["@mozilla.org/browser/final-clh;1"].getService(
|
||||||
|
Ci.nsICommandLineHandler
|
||||||
|
);
|
||||||
|
let fakeCmdLine = {
|
||||||
|
length: 1,
|
||||||
|
_arg: "yoink:yoink",
|
||||||
|
|
||||||
|
getArgument(aIndex) {
|
||||||
|
if (aIndex == 0) {
|
||||||
|
return this._arg;
|
||||||
|
}
|
||||||
|
throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
|
||||||
|
},
|
||||||
|
|
||||||
|
findFlag() {
|
||||||
|
return -1;
|
||||||
|
},
|
||||||
|
|
||||||
|
handleFlagWithParam() {
|
||||||
|
if (this._argCount) {
|
||||||
|
this._argCount = 0;
|
||||||
|
return this._arg;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
},
|
||||||
|
|
||||||
|
state: 2,
|
||||||
|
|
||||||
|
STATE_INITIAL_LAUNCH: 0,
|
||||||
|
STATE_REMOTE_AUTO: 1,
|
||||||
|
STATE_REMOTE_EXPLICIT: 2,
|
||||||
|
|
||||||
|
preventDefault: false,
|
||||||
|
|
||||||
|
resolveURI() {
|
||||||
|
return Services.io.newURI(this._arg);
|
||||||
|
},
|
||||||
|
QueryInterface: ChromeUtils.generateQI(["nsICommandLine"]),
|
||||||
|
};
|
||||||
|
let chooserDialogOpenPromise = waitForProtocolAppChooserDialog(
|
||||||
|
gBrowser,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
cmdLineHandler.handle(fakeCmdLine);
|
||||||
|
let dialog = await chooserDialogOpenPromise;
|
||||||
|
ok(dialog, "Should have prompted.");
|
||||||
|
|
||||||
|
let dialogClosedPromise = waitForProtocolAppChooserDialog(
|
||||||
|
gBrowser.selectedBrowser,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
let dialogEl = dialog._frame.contentDocument.querySelector("dialog");
|
||||||
|
dialogEl.cancelDialog();
|
||||||
|
await dialogClosedPromise;
|
||||||
|
// We will have opened a tab; close it.
|
||||||
|
BrowserTestUtils.removeTab(gBrowser.selectedTab);
|
||||||
|
});
|
||||||
@@ -125,11 +125,26 @@ async function openHelperAppDialog(launcher) {
|
|||||||
return dlg;
|
return dlg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for a subdialog event indicating a dialog either opened
|
||||||
|
* or was closed.
|
||||||
|
*
|
||||||
|
* First argument is the browser in which to listen. If a tabbrowser,
|
||||||
|
* we listen to subdialogs for any tab of that browser.
|
||||||
|
*/
|
||||||
async function waitForSubDialog(browser, url, state) {
|
async function waitForSubDialog(browser, url, state) {
|
||||||
let eventStr = state ? "dialogopen" : "dialogclose";
|
let eventStr = state ? "dialogopen" : "dialogclose";
|
||||||
|
|
||||||
let tabDialogBox = gBrowser.getTabDialogBox(browser);
|
let eventTarget;
|
||||||
let dialogStack = tabDialogBox.getTabDialogManager()._dialogStack;
|
|
||||||
|
// Tabbrowser?
|
||||||
|
if (browser.tabContainer) {
|
||||||
|
eventTarget = browser.tabContainer.ownerDocument.documentElement;
|
||||||
|
} else {
|
||||||
|
// Individual browser. Get its box:
|
||||||
|
let tabDialogBox = browser.ownerGlobal.gBrowser.getTabDialogBox(browser);
|
||||||
|
eventTarget = tabDialogBox.getTabDialogManager()._dialogStack;
|
||||||
|
}
|
||||||
|
|
||||||
let checkFn;
|
let checkFn;
|
||||||
|
|
||||||
@@ -138,7 +153,7 @@ async function waitForSubDialog(browser, url, state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let event = await BrowserTestUtils.waitForEvent(
|
let event = await BrowserTestUtils.waitForEvent(
|
||||||
dialogStack,
|
eventTarget,
|
||||||
eventStr,
|
eventStr,
|
||||||
true,
|
true,
|
||||||
checkFn
|
checkFn
|
||||||
|
|||||||
Reference in New Issue
Block a user