Bug 1565574 - Added permission required to open external protocol handlers. r=Gijs

- Added pref to toggle permission feature
- Updated ContentDispatchChooser to check for permission and  manage a multi dialog flow.

Differential Revision: https://phabricator.services.mozilla.com/D92945
This commit is contained in:
pbz 2020-10-29 13:43:46 +00:00
Родитель 2c8eea4fdc
Коммит 84589d971b
4 изменённых файлов: 353 добавлений и 117 удалений

Просмотреть файл

@ -4736,3 +4736,8 @@ pref("dom.postMessage.sharedArrayBuffer.bypassCOOP_COEP.insecure.enabled", false
// Whether to start the private browsing mode at application startup
pref("browser.privatebrowsing.autostart", false);
// Whether sites require the open-protocol-handler permission to open a
//preferred external application for a protocol. If a site doesn't have
// permission we will show a prompt.
pref("security.external_protocol_requires_permission", true);

Просмотреть файл

@ -5,112 +5,368 @@
// Constants
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
const CONTENT_HANDLING_URL = "chrome://mozapps/content/handling/dialog.xhtml";
const STRINGBUNDLE_URL = "chrome://mozapps/locale/handling/handling.properties";
const DIALOG_URL_APP_CHOOSER =
"chrome://mozapps/content/handling/appChooser.xhtml";
const DIALOG_URL_PERMISSION =
"chrome://mozapps/content/handling/permissionDialog.xhtml";
// nsContentDispatchChooser class
var EXPORTED_SYMBOLS = ["nsContentDispatchChooser"];
function nsContentDispatchChooser() {}
const PROTOCOL_HANDLER_OPEN_PERM_KEY = "open-protocol-handler";
const PERMISSION_KEY_DELIMITER = "^";
nsContentDispatchChooser.prototype = {
classID: Components.ID("e35d5067-95bc-4029-8432-e8f1e431148d"),
class nsContentDispatchChooser {
/**
* Prompt the user to open an external application.
* If the triggering principal doesn't have permission to open apps for the
* protocol of aURI, we show a permission prompt first.
* If the caller has permission and a preferred handler is set, we skip the
* dialogs and directly open the handler.
* @param {nsIHandlerInfo} aHandler - Info about protocol and handlers.
* @param {nsIURI} aURI - URI to be handled.
* @param {nsIPrincipal} aPrincipal - Principal which triggered the load.
* @param {BrowsingContext} [aBrowsingContext] - Context of the load.
*/
async handleURI(aHandler, aURI, aPrincipal, aBrowsingContext) {
let callerHasPermission = this._hasProtocolHandlerPermission(
aHandler.type,
aPrincipal
);
// nsIContentDispatchChooser
ask: function ask(aHandler, aURI, aPrincipal, aBrowsingContext, aReason) {
var bundle = Services.strings.createBundle(STRINGBUNDLE_URL);
let strings = [
bundle.GetStringFromName("protocol.title"),
"",
bundle.GetStringFromName("protocol.description"),
bundle.GetStringFromName("protocol.choices.label"),
bundle.formatStringFromName("protocol.checkbox.label", [aURI.scheme]),
bundle.GetStringFromName("protocol.checkbox.accesskey"),
bundle.formatStringFromName("protocol.checkbox.extra", [
Services.appinfo.name,
]),
];
if (aBrowsingContext) {
if (!aBrowsingContext.topChromeWindow) {
Cu.reportError(
"Can't show external protocol dialog. BrowsingContext has no chrome window associated."
);
return;
// Skip the dialog if a preferred application is set and the caller has
// permission.
if (
callerHasPermission &&
!aHandler.alwaysAskBeforeHandling &&
(aHandler.preferredAction == Ci.nsIHandlerInfo.useHelperApp ||
aHandler.preferredAction == Ci.nsIHandlerInfo.useSystemDefault)
) {
try {
aHandler.launchWithURI(aURI, aBrowsingContext);
} catch (error) {
// We are not supposed to ask, but when file not found the user most likely
// uninstalled the application which handles the uri so we will continue
// by application chooser dialog.
if (error.result == Cr.NS_ERROR_FILE_NOT_FOUND) {
aHandler.alwaysAskBeforeHandling = true;
} else {
throw error;
}
}
}
this._openTabDialog(
strings,
let shouldOpenHandler = false;
try {
shouldOpenHandler = await this._prompt(
aHandler,
aURI,
aPrincipal,
callerHasPermission,
aBrowsingContext
);
} catch (error) {
Cu.reportError(error.message);
}
if (!shouldOpenHandler) {
return;
}
// If we don't have a BrowsingContext, we need to show a standalone window.
this._openWindowDialog(
strings,
aHandler,
aURI,
aPrincipal,
aBrowsingContext
);
},
// Site was granted permission and user chose to open application.
// Launch the external handler.
aHandler.launchWithURI(aURI, aBrowsingContext);
}
_openWindowDialog(strings, aHandler, aURI, aPrincipal, aBrowsingContext) {
let params = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
let SupportsString = Components.Constructor(
"@mozilla.org/supports-string;1",
"nsISupportsString"
);
for (let text of strings) {
let string = new SupportsString();
string.data = text;
params.appendElement(string);
/**
* Get the name of the application set to handle the the protocol.
* @param {nsIHandlerInfo} aHandler - Info about protocol and handlers.
* @returns {string|null} - Human readable handler name or null if the user
* is expected to set a handler.
*/
_getHandlerName(aHandler) {
if (aHandler.alwaysAskBeforeHandling) {
return null;
}
params.appendElement(aHandler);
params.appendElement(aURI);
params.appendElement(aPrincipal);
params.appendElement(aBrowsingContext);
if (
aHandler.preferredAction == Ci.nsIHandlerInfo.useSystemDefault &&
aHandler.hasDefaultHandler
) {
return aHandler.defaultDescription;
}
return aHandler.preferredApplicationHandler?.name;
}
Services.ww.openWindow(
/**
* Show permission or/and app chooser prompt.
* @param {nsIHandlerInfo} aHandler - Info about protocol and handlers.
* @param {nsIPrincipal} aPrincipal - Principal which triggered the load.
* @param {boolean} aHasPermission - Whether the caller has permission to
* open the protocol.
* @param {BrowsingContext} [aBrowsingContext] - Context associated with the
* protocol navigation.
*/
async _prompt(aHandler, aPrincipal, aHasPermission, aBrowsingContext) {
let shouldOpenHandler = false;
let resetHandlerChoice = false;
// If caller does not have permission, prompt the user.
if (!aHasPermission) {
let canPersistPermission = this._isSupportedPrincipal(aPrincipal);
let outArgs = Cc["@mozilla.org/hash-property-bag;1"].createInstance(
Ci.nsIWritablePropertyBag
);
// Whether the permission request was granted
outArgs.setProperty("granted", false);
// If the user wants to select a new application for the protocol.
// This will cause us to show the chooser dialog, even if an app is set.
outArgs.setProperty("resetHandlerChoice", null);
// If the we should store the permission and not prompt again for it.
outArgs.setProperty("remember", null);
await this._openDialog(
DIALOG_URL_PERMISSION,
{
handler: aHandler,
principal: aPrincipal,
browsingContext: aBrowsingContext,
outArgs,
canPersistPermission,
preferredHandlerName: this._getHandlerName(aHandler),
},
aBrowsingContext
);
if (!outArgs.getProperty("granted")) {
// User denied request
return false;
}
// Check if user wants to set a new application to handle the protocol.
resetHandlerChoice = outArgs.getProperty("resetHandlerChoice");
// If the user wants to select a new app we don't persist the permission.
if (!resetHandlerChoice) {
let remember = outArgs.getProperty("remember");
this._updatePermission(aPrincipal, aHandler.type, remember);
}
shouldOpenHandler = true;
}
// Prompt if the user needs to make a handler choice for the protocol.
if (aHandler.alwaysAskBeforeHandling || resetHandlerChoice) {
// User has not set a preferred application to handle this protocol scheme.
// Open the application chooser dialog
let outArgs = Cc["@mozilla.org/hash-property-bag;1"].createInstance(
Ci.nsIWritablePropertyBag
);
outArgs.setProperty("openHandler", false);
outArgs.setProperty("preferredAction", aHandler.preferredAction);
outArgs.setProperty(
"preferredApplicationHandler",
aHandler.preferredApplicationHandler
);
outArgs.setProperty(
"alwaysAskBeforeHandling",
aHandler.alwaysAskBeforeHandling
);
let usePrivateBrowsing = aBrowsingContext?.usePrivateBrowsing;
await this._openDialog(
DIALOG_URL_APP_CHOOSER,
{
handler: aHandler,
outArgs,
usePrivateBrowsing,
enableButtonDelay: aHasPermission,
},
aBrowsingContext
);
shouldOpenHandler = outArgs.getProperty("openHandler");
// If the user accepted the dialog, apply their selection.
if (shouldOpenHandler) {
for (let prop of [
"preferredAction",
"preferredApplicationHandler",
"alwaysAskBeforeHandling",
]) {
aHandler[prop] = outArgs.getProperty(prop);
}
// Store handler data
Cc["@mozilla.org/uriloader/handler-service;1"]
.getService(Ci.nsIHandlerService)
.store(aHandler);
}
}
return shouldOpenHandler;
}
/**
* Test if a given principal has the open-protocol-handler permission for a
* specific protocol.
* @param {string} scheme - Scheme of the protocol.
* @param {nsIPrincipal} aPrincipal - Principal to test for permission.
* @returns {boolean} - true if permission is set, false otherwise.
*/
_hasProtocolHandlerPermission(scheme, aPrincipal) {
// Permission disabled by pref
if (!nsContentDispatchChooser.isPermissionEnabled) {
return true;
}
// If a handler is set to open externally by default we skip the dialog.
if (
Services.prefs.getBoolPref(
"network.protocol-handler.external." + scheme,
false
)
) {
return true;
}
if (!aPrincipal) {
return false;
}
let key = this._getSkipProtoDialogPermissionKey(scheme);
return (
Services.perms.testPermissionFromPrincipal(aPrincipal, key) ===
Services.perms.ALLOW_ACTION
);
}
/**
* Get open-protocol-handler permission key for a protocol.
* @param {string} aProtocolScheme - Scheme of the protocol.
* @returns {string} - Permission key.
*/
_getSkipProtoDialogPermissionKey(aProtocolScheme) {
return (
PROTOCOL_HANDLER_OPEN_PERM_KEY +
PERMISSION_KEY_DELIMITER +
aProtocolScheme
);
}
/**
* Opens a dialog as a SubDialog on tab level.
* If we don't have a BrowsingContext we will fallback to a standalone window.
* @param {string} aDialogURL - URL of the dialog to open.
* @param {Object} aDialogArgs - Arguments passed to the dialog.
* @param {BrowsingContext} [aBrowsingContext] - BrowsingContext associated
* with the tab the dialog is associated with.
*/
async _openDialog(aDialogURL, aDialogArgs, aBrowsingContext) {
// Make the app chooser dialog resizable
let resizable = `resizable=${
aDialogURL == DIALOG_URL_APP_CHOOSER ? "yes" : "no"
}`;
if (aBrowsingContext) {
if (!aBrowsingContext.topChromeWindow) {
throw new Error(
"Can't show external protocol dialog. BrowsingContext has no chrome window associated."
);
}
let window = aBrowsingContext.topChromeWindow;
let tabDialogBox = window.gBrowser.getTabDialogBox(
aBrowsingContext.embedderElement
);
return tabDialogBox.open(
aDialogURL,
{
features: resizable,
allowDuplicateDialogs: false,
keepOpenSameOriginNav: true,
},
aDialogArgs
);
}
// If we don't have a BrowsingContext, we need to show a standalone window.
let win = Services.ww.openWindow(
null,
CONTENT_HANDLING_URL,
aDialogURL,
null,
"chrome,dialog=yes,resizable,centerscreen",
params
);
},
_openTabDialog(strings, aHandler, aURI, aPrincipal, aBrowsingContext) {
let window = aBrowsingContext.topChromeWindow;
let tabDialogBox = window.gBrowser.getTabDialogBox(
aBrowsingContext.embedderElement
`chrome,dialog=yes,centerscreen,${resizable}`,
aDialogArgs
);
tabDialogBox.open(
CONTENT_HANDLING_URL,
{
features: "resizable=yes",
allowDuplicateDialogs: false,
keepOpenSameOriginNav: true,
},
...strings,
aHandler,
aURI,
aPrincipal,
aBrowsingContext
// Wait until window is closed.
return new Promise(resolve => {
win.addEventListener("unload", function onUnload(event) {
if (event.target.location != aDialogURL) {
return;
}
win.removeEventListener("unload", onUnload);
resolve();
});
});
}
/**
* Update the open-protocol-handler permission for the site which triggered
* the dialog. Sites with this permission may skip this dialog.
* @param {nsIPrincipal} aPrincipal - subject to update the permission for.
* @param {string} aScheme - Scheme of protocol to allow.
* @param {boolean} aAllow - Whether to set / unset the permission.
*/
_updatePermission(aPrincipal, aScheme, aAllow) {
// If enabled, store open-protocol-handler permission for content principals.
if (
!nsContentDispatchChooser.isPermissionEnabled ||
aPrincipal.isSystemPrincipal ||
!this._isSupportedPrincipal(aPrincipal)
) {
return;
}
let permKey = this._getSkipProtoDialogPermissionKey(aScheme);
if (aAllow) {
Services.perms.addFromPrincipal(
aPrincipal,
permKey,
Services.perms.ALLOW_ACTION,
Services.perms.EXPIRE_NEVER
);
} else {
Services.perms.removeFromPrincipal(aPrincipal, permKey);
}
}
/**
* Determine if we can use a principal to store permissions.
* @param {nsIPrincipal} aPrincipal - Principal to test.
* @returns {boolean} - true if we can store permissions, false otherwise.
*/
_isSupportedPrincipal(aPrincipal) {
return (
aPrincipal &&
["http", "https", "moz-extension", "file"].some(scheme =>
aPrincipal.schemeIs(scheme)
)
);
},
}
}
// nsISupports
nsContentDispatchChooser.prototype.classID = Components.ID(
"e35d5067-95bc-4029-8432-e8f1e431148d"
);
nsContentDispatchChooser.prototype.QueryInterface = ChromeUtils.generateQI([
"nsIContentDispatchChooser",
]);
QueryInterface: ChromeUtils.generateQI(["nsIContentDispatchChooser"]),
};
var EXPORTED_SYMBOLS = ["nsContentDispatchChooser"];
XPCOMUtils.defineLazyPreferenceGetter(
nsContentDispatchChooser,
"isPermissionEnabled",
"security.external_protocol_requires_permission",
true
);

Просмотреть файл

@ -1047,30 +1047,12 @@ nsExternalHelperAppService::LoadURI(nsIURI* aURI,
rv = GetProtocolHandlerInfo(scheme, getter_AddRefs(handler));
NS_ENSURE_SUCCESS(rv, rv);
nsHandlerInfoAction preferredAction;
handler->GetPreferredAction(&preferredAction);
bool alwaysAsk = true;
handler->GetAlwaysAskBeforeHandling(&alwaysAsk);
// if we are not supposed to ask, and the preferred action is to use
// a helper app or the system default, we just launch the URI.
if (!alwaysAsk && (preferredAction == nsIHandlerInfo::useHelperApp ||
preferredAction == nsIHandlerInfo::useSystemDefault)) {
rv = handler->LaunchWithURI(uri, aBrowsingContext);
// We are not supposed to ask, but when file not found the user most likely
// uninstalled the application which handles the uri so we will continue
// by application chooser dialog.
if (rv != NS_ERROR_FILE_NOT_FOUND) {
return rv;
}
}
nsCOMPtr<nsIContentDispatchChooser> chooser =
do_CreateInstance("@mozilla.org/content-dispatch-chooser;1", &rv);
NS_ENSURE_SUCCESS(rv, rv);
return chooser->Ask(handler, uri, aTriggeringPrincipal, aBrowsingContext,
nsIContentDispatchChooser::REASON_CANNOT_HANDLE);
return chooser->HandleURI(handler, uri, aTriggeringPrincipal,
aBrowsingContext);
}
//////////////////////////////////////////////////////////////////////////////////////////////////////

Просмотреть файл

@ -16,13 +16,9 @@ webidl BrowsingContext;
[scriptable, uuid(456ca3b2-02be-4f97-89a2-08c08d3ad88f)]
interface nsIContentDispatchChooser : nsISupports {
/**
* This request is passed to the helper app dialog because Gecko can not
* handle content of this type.
*/
const unsigned long REASON_CANNOT_HANDLE = 0;
/**
* Asks the user what to do with the content.
* Opens the handler associated with the given resource.
* If the caller does not have permission or no handler is set, we ask the
* user to grant permission and pick a handler.
*
* @param aHander
* The interface describing the details of how this content should or
@ -33,13 +29,10 @@ interface nsIContentDispatchChooser : nsISupports {
* The principal making the request.
* @param aBrowsingContext
* The browsing context where the load should happen.
* @param aReason
* The reason why we are asking (see above).
*/
void ask(in nsIHandlerInfo aHandler,
void handleURI(in nsIHandlerInfo aHandler,
in nsIURI aURI,
in nsIPrincipal aTriggeringPrincipal,
in BrowsingContext aBrowsingContext,
in unsigned long aReason);
in BrowsingContext aBrowsingContext);
};