зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
2c8eea4fdc
Коммит
84589d971b
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче