diff options
Diffstat (limited to 'application/basilisk/modules/PermissionUI.jsm')
-rw-r--r-- | application/basilisk/modules/PermissionUI.jsm | 601 |
1 files changed, 601 insertions, 0 deletions
diff --git a/application/basilisk/modules/PermissionUI.jsm b/application/basilisk/modules/PermissionUI.jsm new file mode 100644 index 0000000000..b4856a007c --- /dev/null +++ b/application/basilisk/modules/PermissionUI.jsm @@ -0,0 +1,601 @@ +/* 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/. */ + +"use strict"; + +this.EXPORTED_SYMBOLS = [ + "PermissionUI", +]; + +/** + * PermissionUI is responsible for exposing both a prototype + * PermissionPrompt that can be used by arbitrary browser + * components and add-ons, but also hosts the implementations of + * built-in permission prompts. + * + * If you're developing a feature that requires web content to ask + * for special permissions from the user, this module is for you. + * + * Suppose a system add-on wants to add a new prompt for a new request + * for getting more low-level access to the user's sound card, and the + * permission request is coming up from content by way of the + * nsContentPermissionHelper. The system add-on could then do the following: + * + * Cu.import("resource://gre/modules/Integration.jsm"); + * Cu.import("resource:///modules/PermissionUI.jsm"); + * + * const SoundCardIntegration = (base) => ({ + * __proto__: base, + * createPermissionPrompt(type, request) { + * if (type != "sound-api") { + * return super.createPermissionPrompt(...arguments); + * } + * + * return { + * __proto__: PermissionUI.PermissionPromptForRequestPrototype, + * get permissionKey() { + * return "sound-permission"; + * } + * // etc - see the documentation for PermissionPrompt for + * // a better idea of what things one can and should override. + * } + * }, + * }); + * + * // Add-on startup: + * Integration.contentPermission.register(SoundCardIntegration); + * // ... + * // Add-on shutdown: + * Integration.contentPermission.unregister(SoundCardIntegration); + * + * Note that PermissionPromptForRequestPrototype must be used as the + * prototype, since the prompt is wrapping an nsIContentPermissionRequest, + * and going through nsIContentPermissionPrompt. + * + * It is, however, possible to take advantage of PermissionPrompt without + * having to go through nsIContentPermissionPrompt or with a + * nsIContentPermissionRequest. The PermissionPromptPrototype can be + * imported, subclassed, and have prompt() called directly, without + * the caller having called into createPermissionPrompt. + */ +const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "Services", + "resource://gre/modules/Services.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "SitePermissions", + "resource:///modules/SitePermissions.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", + "resource://gre/modules/PrivateBrowsingUtils.jsm"); + +XPCOMUtils.defineLazyGetter(this, "gBrandBundle", function() { + return Services.strings + .createBundle("chrome://branding/locale/brand.properties"); +}); + +XPCOMUtils.defineLazyGetter(this, "gBrowserBundle", function() { + return Services.strings + .createBundle("chrome://browser/locale/browser.properties"); +}); + +this.PermissionUI = {}; + +/** + * PermissionPromptPrototype should be subclassed by callers that + * want to display prompts to the user. See each method and property + * below for guidance on what to override. + * + * Note that if you're creating a prompt for an + * nsIContentPermissionRequest, you'll want to subclass + * PermissionPromptForRequestPrototype instead. + */ +this.PermissionPromptPrototype = { + /** + * Returns the associated <xul:browser> for the request. This should + * work for the e10s and non-e10s case. + * + * Subclasses must override this. + * + * @return {<xul:browser>} + */ + get browser() { + throw new Error("Not implemented."); + }, + + /** + * Returns the nsIPrincipal associated with the request. + * + * Subclasses must override this. + * + * @return {nsIPrincipal} + */ + get principal() { + throw new Error("Not implemented."); + }, + + /** + * If the nsIPermissionManager is being queried and written + * to for this permission request, set this to the key to be + * used. If this is undefined, user permissions will not be + * read from or written to. + * + * Note that if a permission is set, in any follow-up + * prompting within the expiry window of that permission, + * the prompt will be skipped and the allow or deny choice + * will be selected automatically. + */ + get permissionKey() { + return undefined; + }, + + /** + * These are the options that will be passed to the + * PopupNotification when it is shown. See the documentation + * for PopupNotification for more details. + * + * Note that prompt() will automatically set displayURI to + * be the URI of the requesting pricipal, unless the displayURI is exactly + * set to false. + */ + get popupOptions() { + return {}; + }, + + /** + * PopupNotification requires a unique ID to open the notification. + * You must return a unique ID string here, for which PopupNotification + * will then create a <xul:popupnotification> node with the ID + * "<notificationID>-notification". + * + * If there's a custom <xul:popupnotification> you're hoping to show, + * then you need to make sure its ID has the "-notification" suffix, + * and then return the prefix here. + * + * See PopupNotification.jsm for more details. + * + * @return {string} + * The unique ID that will be used to as the + * "<unique ID>-notification" ID for the <xul:popupnotification> + * to use or create. + */ + get notificationID() { + throw new Error("Not implemented."); + }, + + /** + * The ID of the element to anchor the PopupNotification to. + * + * @return {string} + */ + get anchorID() { + return "default-notification-icon"; + }, + + /** + * The message to show the user in the PopupNotification. This + * is usually a string describing the permission that is being + * requested. + * + * Subclasses must override this. + * + * @return {string} + */ + get message() { + throw new Error("Not implemented."); + }, + + /** + * This will be called if the request is to be cancelled. + * + * Subclasses only need to override this if they provide a + * permissionKey. + */ + cancel() { + throw new Error("Not implemented.") + }, + + /** + * This will be called if the request is to be allowed. + * + * Subclasses only need to override this if they provide a + * permissionKey. + */ + allow() { + throw new Error("Not implemented."); + }, + + /** + * The actions that will be displayed in the PopupNotification + * via a dropdown menu. The first item in this array will be + * the default selection. Each action is an Object with the + * following properties: + * + * label (string): + * The label that will be displayed for this choice. + * accessKey (string): + * The access key character that will be used for this choice. + * action (SitePermissions state) + * The action that will be associated with this choice. + * This should be either SitePermissions.ALLOW or SitePermissions.BLOCK. + * + * callback (function, optional) + * A callback function that will fire if the user makes this choice, with + * a single parameter, state. State is an Object that contains the property + * checkboxChecked, which identifies whether the checkbox to remember this + * decision was checked. + */ + get promptActions() { + return []; + }, + + /** + * If the prompt will be shown to the user, this callback will + * be called just before. Subclasses may want to override this + * in order to, for example, bump a counter Telemetry probe for + * how often a particular permission request is seen. + */ + onBeforeShow() {}, + + /** + * Will determine if a prompt should be shown to the user, and if so, + * will show it. + * + * If a permissionKey is defined prompt() might automatically + * allow or cancel itself based on the user's current + * permission settings without displaying the prompt. + * + * If the <xul:browser> that the request is associated with + * does not belong to a browser window with the PopupNotifications + * global set, the prompt request is ignored. + */ + prompt() { + let chromeWin = this.browser.ownerGlobal; + if (!chromeWin.PopupNotifications) { + return; + } + + // We ignore requests from non-nsIStandardURLs + let requestingURI = this.principal.URI; + if (!(requestingURI instanceof Ci.nsIStandardURL)) { + return; + } + + if (this.permissionKey) { + // If we're reading and setting permissions, then we need + // to check to see if we already have a permission setting + // for this particular principal. + let {state} = SitePermissions.get(requestingURI, + this.permissionKey, + this.browser); + + if (state == SitePermissions.BLOCK) { + this.cancel(); + return; + } + + if (state == SitePermissions.ALLOW) { + this.allow(); + return; + } + + // Tell the browser to refresh the identity block display in case there + // are expired permission states. + this.browser.dispatchEvent(new this.browser.ownerGlobal + .CustomEvent("PermissionStateChange")); + } + + // Transform the PermissionPrompt actions into PopupNotification actions. + let popupNotificationActions = []; + for (let promptAction of this.promptActions) { + let action = { + label: promptAction.label, + accessKey: promptAction.accessKey, + callback: state => { + if (promptAction.callback) { + promptAction.callback(); + } + + if (this.permissionKey) { + + // Permanently store permission. + if (state && state.checkboxChecked) { + let scope = SitePermissions.SCOPE_PERSISTENT; + // Only remember permission for session if in PB mode. + if (PrivateBrowsingUtils.isBrowserPrivate(this.browser)) { + scope = SitePermissions.SCOPE_SESSION; + } + SitePermissions.set(this.principal.URI, + this.permissionKey, + promptAction.action, + scope); + } else if (promptAction.action == SitePermissions.BLOCK) { + // Temporarily store BLOCK permissions only. + // SitePermissions does not consider subframes when storing temporary + // permissions on a tab, thus storing ALLOW could be exploited. + SitePermissions.set(this.principal.URI, + this.permissionKey, + promptAction.action, + SitePermissions.SCOPE_TEMPORARY, + this.browser); + } + + // Grant permission if action is ALLOW. + if (promptAction.action == SitePermissions.ALLOW) { + this.allow(); + } else { + this.cancel(); + } + } + }, + }; + if (promptAction.dismiss) { + action.dismiss = promptAction.dismiss + } + + popupNotificationActions.push(action); + } + + let mainAction = popupNotificationActions.length ? + popupNotificationActions[0] : null; + let secondaryActions = popupNotificationActions.splice(1); + + let options = this.popupOptions; + + if (!options.hasOwnProperty("displayURI") || options.displayURI) { + options.displayURI = this.principal.URI; + } + // Permission prompts are always persistent; the close button is controlled by a pref. + options.persistent = true; + options.hideClose = !Services.prefs.getBoolPref("privacy.permissionPrompts.showCloseButton"); + // When the docshell of the browser is aboout to be swapped to another one, + // the "swapping" event is called. Returning true causes the notification + // to be moved to the new browser. + options.eventCallback = topic => topic == "swapping"; + + this.onBeforeShow(); + chromeWin.PopupNotifications.show(this.browser, + this.notificationID, + this.message, + this.anchorID, + mainAction, + secondaryActions, + options); + }, +}; + +PermissionUI.PermissionPromptPrototype = PermissionPromptPrototype; + +/** + * A subclass of PermissionPromptPrototype that assumes + * that this.request is an nsIContentPermissionRequest + * and fills in some of the required properties on the + * PermissionPrompt. For callers that are wrapping an + * nsIContentPermissionRequest, this should be subclassed + * rather than PermissionPromptPrototype. + */ +this.PermissionPromptForRequestPrototype = { + __proto__: PermissionPromptPrototype, + + get browser() { + // In the e10s-case, the <xul:browser> will be at request.element. + // In the single-process case, we have to use some XPCOM incantations + // to resolve to the <xul:browser>. + if (this.request.element) { + return this.request.element; + } + return this.request + .window + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell) + .chromeEventHandler; + }, + + get principal() { + return this.request.principal; + }, + + cancel() { + this.request.cancel(); + }, + + allow() { + this.request.allow(); + }, +}; + +PermissionUI.PermissionPromptForRequestPrototype = + PermissionPromptForRequestPrototype; + +/** + * Creates a PermissionPrompt for a nsIContentPermissionRequest for + * the GeoLocation API. + * + * @param request (nsIContentPermissionRequest) + * The request for a permission from content. + */ +function GeolocationPermissionPrompt(request) { + this.request = request; +} + +GeolocationPermissionPrompt.prototype = { + __proto__: PermissionPromptForRequestPrototype, + + get permissionKey() { + return "geo"; + }, + + get popupOptions() { + let pref = "browser.geolocation.warning.infoURL"; + let options = { + learnMoreURL: Services.urlFormatter.formatURLPref(pref), + displayURI: false + }; + + if (this.principal.URI.schemeIs("file")) { + options.checkbox = { show: false }; + } else { + // Don't offer "always remember" action in PB mode + options.checkbox = { + show: !PrivateBrowsingUtils.isWindowPrivate(this.browser.ownerGlobal) + }; + } + + if (options.checkbox.show) { + options.checkbox.label = gBrowserBundle.GetStringFromName("geolocation.remember"); + } + + return options; + }, + + get notificationID() { + return "geolocation"; + }, + + get anchorID() { + return "geo-notification-icon"; + }, + + get message() { + let message; + if (this.principal.URI.schemeIs("file")) { + message = gBrowserBundle.GetStringFromName("geolocation.shareWithFile3"); + } else { + let hostPort = "<>"; + try { + hostPort = this.principal.URI.hostPort; + } catch (ex) { } + message = gBrowserBundle.formatStringFromName("geolocation.shareWithSite3", + [hostPort], 1); + } + return message; + }, + + get promptActions() { + // We collect Telemetry data on Geolocation prompts and how users + // respond to them. The probe keys are a bit verbose, so let's alias them. + const SHARE_LOCATION = + Ci.nsISecurityUITelemetry.WARNING_GEOLOCATION_REQUEST_SHARE_LOCATION; + const ALWAYS_SHARE = + Ci.nsISecurityUITelemetry.WARNING_GEOLOCATION_REQUEST_ALWAYS_SHARE; + const NEVER_SHARE = + Ci.nsISecurityUITelemetry.WARNING_GEOLOCATION_REQUEST_NEVER_SHARE; + + let secHistogram = Services.telemetry.getHistogramById("SECURITY_UI"); + + return [{ + label: gBrowserBundle.GetStringFromName("geolocation.allowLocation"), + accessKey: + gBrowserBundle.GetStringFromName("geolocation.allowLocation.accesskey"), + action: SitePermissions.ALLOW, + callback(state) { + if (state && state.checkboxChecked) { + secHistogram.add(ALWAYS_SHARE); + } else { + secHistogram.add(SHARE_LOCATION); + } + }, + }, { + label: gBrowserBundle.GetStringFromName("geolocation.dontAllowLocation"), + accessKey: + gBrowserBundle.GetStringFromName("geolocation.dontAllowLocation.accesskey"), + action: SitePermissions.BLOCK, + callback(state) { + if (state && state.checkboxChecked) { + secHistogram.add(NEVER_SHARE); + } + }, + }]; + }, + + onBeforeShow() { + let secHistogram = Services.telemetry.getHistogramById("SECURITY_UI"); + const SHOW_REQUEST = Ci.nsISecurityUITelemetry.WARNING_GEOLOCATION_REQUEST; + secHistogram.add(SHOW_REQUEST); + }, +}; + +PermissionUI.GeolocationPermissionPrompt = GeolocationPermissionPrompt; + +/** + * Creates a PermissionPrompt for a nsIContentPermissionRequest for + * the Desktop Notification API. + * + * @param request (nsIContentPermissionRequest) + * The request for a permission from content. + * @return {PermissionPrompt} (see documentation in header) + */ +function DesktopNotificationPermissionPrompt(request) { + this.request = request; +} + +DesktopNotificationPermissionPrompt.prototype = { + __proto__: PermissionPromptForRequestPrototype, + + get permissionKey() { + return "desktop-notification"; + }, + + get popupOptions() { + let learnMoreURL = + Services.urlFormatter.formatURLPref("app.support.baseURL") + "push"; + + let checkbox = { + show: true, + checked: true, + label: gBrowserBundle.GetStringFromName("webNotifications.remember") + }; + + // In PB mode, the "always remember" checkbox should only remember for the + // session. + if (PrivateBrowsingUtils.isWindowPrivate(this.browser.ownerGlobal)) { + checkbox.label = + gBrowserBundle.GetStringFromName("webNotifications.rememberForSession"); + } + + return { + learnMoreURL, + checkbox, + displayURI: false + }; + }, + + get notificationID() { + return "web-notifications"; + }, + + get anchorID() { + return "web-notifications-notification-icon"; + }, + + get message() { + let hostPort = "<>"; + try { + hostPort = this.principal.URI.hostPort; + } catch (ex) { } + return gBrowserBundle.formatStringFromName("webNotifications.receiveFromSite2", + [hostPort], 1); + }, + + get promptActions() { + return [ + { + label: gBrowserBundle.GetStringFromName("webNotifications.allow"), + accessKey: + gBrowserBundle.GetStringFromName("webNotifications.allow.accesskey"), + action: SitePermissions.ALLOW, + }, + { + label: gBrowserBundle.GetStringFromName("webNotifications.dontAllow"), + accessKey: + gBrowserBundle.GetStringFromName("webNotifications.dontAllow.accesskey"), + action: SitePermissions.BLOCK, + }, + ]; + }, +}; + +PermissionUI.DesktopNotificationPermissionPrompt = + DesktopNotificationPermissionPrompt; |