summaryrefslogtreecommitdiffstats
path: root/b2g/components/ContentPermissionPrompt.js
diff options
context:
space:
mode:
Diffstat (limited to 'b2g/components/ContentPermissionPrompt.js')
-rw-r--r--b2g/components/ContentPermissionPrompt.js461
1 files changed, 461 insertions, 0 deletions
diff --git a/b2g/components/ContentPermissionPrompt.js b/b2g/components/ContentPermissionPrompt.js
new file mode 100644
index 000000000..e11b1b458
--- /dev/null
+++ b/b2g/components/ContentPermissionPrompt.js
@@ -0,0 +1,461 @@
+/* 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"
+
+function debug(str) {
+ //dump("-*- ContentPermissionPrompt: " + str + "\n");
+}
+
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+const Cc = Components.classes;
+
+const PROMPT_FOR_UNKNOWN = ["audio-capture",
+ "desktop-notification",
+ "geolocation",
+ "video-capture"];
+// Due to privary issue, permission requests like GetUserMedia should prompt
+// every time instead of providing session persistence.
+const PERMISSION_NO_SESSION = ["audio-capture", "video-capture"];
+const ALLOW_MULTIPLE_REQUESTS = ["audio-capture", "video-capture"];
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/AppsUtils.jsm");
+Cu.import("resource://gre/modules/PermissionsInstaller.jsm");
+Cu.import("resource://gre/modules/PermissionsTable.jsm");
+
+var permissionManager = Cc["@mozilla.org/permissionmanager;1"].getService(Ci.nsIPermissionManager);
+var secMan = Cc["@mozilla.org/scriptsecuritymanager;1"].getService(Ci.nsIScriptSecurityManager);
+
+var permissionSpecificChecker = {};
+
+XPCOMUtils.defineLazyModuleGetter(this, "SystemAppProxy",
+ "resource://gre/modules/SystemAppProxy.jsm");
+
+/**
+ * Determine if a permission should be prompt to user or not.
+ *
+ * @param aPerm requested permission
+ * @param aAction the action according to principal
+ * @return true if prompt is required
+ */
+function shouldPrompt(aPerm, aAction) {
+ return ((aAction == Ci.nsIPermissionManager.PROMPT_ACTION) ||
+ (aAction == Ci.nsIPermissionManager.UNKNOWN_ACTION &&
+ PROMPT_FOR_UNKNOWN.indexOf(aPerm) >= 0));
+}
+
+/**
+ * Create the default choices for the requested permissions
+ *
+ * @param aTypesInfo requested permissions
+ * @return the default choices for permissions with options, return
+ * undefined if no option in all requested permissions.
+ */
+function buildDefaultChoices(aTypesInfo) {
+ let choices;
+ for (let type of aTypesInfo) {
+ if (type.options.length > 0) {
+ if (!choices) {
+ choices = {};
+ }
+ choices[type.access] = type.options[0];
+ }
+ }
+ return choices;
+}
+
+/**
+ * aTypesInfo is an array of {permission, access, action, deny} which keeps
+ * the information of each permission. This arrary is initialized in
+ * ContentPermissionPrompt.prompt and used among functions.
+ *
+ * aTypesInfo[].permission : permission name
+ * aTypesInfo[].access : permission name + request.access
+ * aTypesInfo[].action : the default action of this permission
+ * aTypesInfo[].deny : true if security manager denied this app's origin
+ * principal.
+ * Note:
+ * aTypesInfo[].permission will be sent to prompt only when
+ * aTypesInfo[].action is PROMPT_ACTION and aTypesInfo[].deny is false.
+ */
+function rememberPermission(aTypesInfo, aPrincipal, aSession)
+{
+ function convertPermToAllow(aPerm, aPrincipal)
+ {
+ let type =
+ permissionManager.testExactPermissionFromPrincipal(aPrincipal, aPerm);
+ if (shouldPrompt(aPerm, type)) {
+ debug("add " + aPerm + " to permission manager with ALLOW_ACTION");
+ if (!aSession) {
+ permissionManager.addFromPrincipal(aPrincipal,
+ aPerm,
+ Ci.nsIPermissionManager.ALLOW_ACTION);
+ } else if (PERMISSION_NO_SESSION.indexOf(aPerm) < 0) {
+ permissionManager.addFromPrincipal(aPrincipal,
+ aPerm,
+ Ci.nsIPermissionManager.ALLOW_ACTION,
+ Ci.nsIPermissionManager.EXPIRE_SESSION, 0);
+ }
+ }
+ }
+
+ for (let i in aTypesInfo) {
+ // Expand the permission to see if we have multiple access properties
+ // to convert
+ let perm = aTypesInfo[i].permission;
+ let access = PermissionsTable[perm].access;
+ if (access) {
+ for (let idx in access) {
+ convertPermToAllow(perm + "-" + access[idx], aPrincipal);
+ }
+ } else {
+ convertPermToAllow(perm, aPrincipal);
+ }
+ }
+}
+
+function ContentPermissionPrompt() {}
+
+ContentPermissionPrompt.prototype = {
+
+ handleExistingPermission: function handleExistingPermission(request,
+ typesInfo) {
+ typesInfo.forEach(function(type) {
+ type.action =
+ Services.perms.testExactPermissionFromPrincipal(request.principal,
+ type.access);
+ if (shouldPrompt(type.access, type.action)) {
+ type.action = Ci.nsIPermissionManager.PROMPT_ACTION;
+ }
+ });
+
+ // If all permissions are allowed already and no more than one option,
+ // call allow() without prompting.
+ let checkAllowPermission = function(type) {
+ if (type.action == Ci.nsIPermissionManager.ALLOW_ACTION &&
+ type.options.length <= 1) {
+ return true;
+ }
+ return false;
+ }
+ if (typesInfo.every(checkAllowPermission)) {
+ debug("all permission requests are allowed");
+ request.allow(buildDefaultChoices(typesInfo));
+ return true;
+ }
+
+ // If all permissions are DENY_ACTION or UNKNOWN_ACTION, call cancel()
+ // without prompting.
+ let checkDenyPermission = function(type) {
+ if (type.action == Ci.nsIPermissionManager.DENY_ACTION ||
+ type.action == Ci.nsIPermissionManager.UNKNOWN_ACTION) {
+ return true;
+ }
+ return false;
+ }
+ if (typesInfo.every(checkDenyPermission)) {
+ debug("all permission requests are denied");
+ request.cancel();
+ return true;
+ }
+ return false;
+ },
+
+ // multiple requests should be audio and video
+ checkMultipleRequest: function checkMultipleRequest(typesInfo) {
+ if (typesInfo.length == 1) {
+ return true;
+ } else if (typesInfo.length > 1) {
+ let checkIfAllowMultiRequest = function(type) {
+ return (ALLOW_MULTIPLE_REQUESTS.indexOf(type.access) !== -1);
+ }
+ if (typesInfo.every(checkIfAllowMultiRequest)) {
+ debug("legal multiple requests");
+ return true;
+ }
+ }
+
+ return false;
+ },
+
+ handledByApp: function handledByApp(request, typesInfo) {
+ if (request.principal.appId == Ci.nsIScriptSecurityManager.NO_APP_ID ||
+ request.principal.appId == Ci.nsIScriptSecurityManager.UNKNOWN_APP_ID) {
+ // This should not really happen
+ request.cancel();
+ return true;
+ }
+
+ let appsService = Cc["@mozilla.org/AppsService;1"]
+ .getService(Ci.nsIAppsService);
+ let app = appsService.getAppByLocalId(request.principal.appId);
+
+ // Check each permission if it's denied by permission manager with app's
+ // URL.
+ let notDenyAppPrincipal = function(type) {
+ let url = Services.io.newURI(app.origin, null, null);
+ let principal =
+ secMan.createCodebasePrincipal(url,
+ {appId: request.principal.appId});
+ let result = Services.perms.testExactPermissionFromPrincipal(principal,
+ type.access);
+
+ if (result == Ci.nsIPermissionManager.ALLOW_ACTION ||
+ result == Ci.nsIPermissionManager.PROMPT_ACTION) {
+ type.deny = false;
+ }
+ return !type.deny;
+ }
+ // Cancel the entire request if one of the requested permissions is denied
+ if (!typesInfo.every(notDenyAppPrincipal)) {
+ request.cancel();
+ return true;
+ }
+
+ return false;
+ },
+
+ handledByPermissionType: function handledByPermissionType(request, typesInfo) {
+ for (let i in typesInfo) {
+ if (permissionSpecificChecker.hasOwnProperty(typesInfo[i].permission) &&
+ permissionSpecificChecker[typesInfo[i].permission](request)) {
+ return true;
+ }
+ }
+
+ return false;
+ },
+
+ prompt: function(request) {
+ // Initialize the typesInfo and set the default value.
+ let typesInfo = [];
+ let perms = request.types.QueryInterface(Ci.nsIArray);
+ for (let idx = 0; idx < perms.length; idx++) {
+ let perm = perms.queryElementAt(idx, Ci.nsIContentPermissionType);
+ let tmp = {
+ permission: perm.type,
+ access: (perm.access && perm.access !== "unused") ?
+ perm.type + "-" + perm.access : perm.type,
+ options: [],
+ deny: true,
+ action: Ci.nsIPermissionManager.UNKNOWN_ACTION
+ };
+
+ // Append available options, if any.
+ let options = perm.options.QueryInterface(Ci.nsIArray);
+ for (let i = 0; i < options.length; i++) {
+ let option = options.queryElementAt(i, Ci.nsISupportsString).data;
+ tmp.options.push(option);
+ }
+ typesInfo.push(tmp);
+ }
+
+ if (secMan.isSystemPrincipal(request.principal)) {
+ request.allow(buildDefaultChoices(typesInfo));
+ return;
+ }
+
+
+ if (typesInfo.length == 0) {
+ request.cancel();
+ return;
+ }
+
+ if(!this.checkMultipleRequest(typesInfo)) {
+ request.cancel();
+ return;
+ }
+
+ if (this.handledByApp(request, typesInfo) ||
+ this.handledByPermissionType(request, typesInfo)) {
+ return;
+ }
+
+ // returns true if the request was handled
+ if (this.handleExistingPermission(request, typesInfo)) {
+ return;
+ }
+
+ // prompt PROMPT_ACTION request or request with options.
+ typesInfo = typesInfo.filter(function(type) {
+ return !type.deny && (type.action == Ci.nsIPermissionManager.PROMPT_ACTION || type.options.length > 0) ;
+ });
+
+ if (!request.element) {
+ this.delegatePrompt(request, typesInfo);
+ return;
+ }
+
+ var cancelRequest = function() {
+ request.requester.onVisibilityChange = null;
+ request.cancel();
+ }
+
+ var self = this;
+
+ // If the request was initiated from a hidden iframe
+ // we don't forward it to content and cancel it right away
+ request.requester.getVisibility( {
+ notifyVisibility: function(isVisible) {
+ if (!isVisible) {
+ cancelRequest();
+ return;
+ }
+
+ // Monitor the frame visibility and cancel the request if the frame goes
+ // away but the request is still here.
+ request.requester.onVisibilityChange = {
+ notifyVisibility: function(isVisible) {
+ if (isVisible)
+ return;
+
+ self.cancelPrompt(request, typesInfo);
+ cancelRequest();
+ }
+ }
+
+ self.delegatePrompt(request, typesInfo, function onCallback() {
+ request.requester.onVisibilityChange = null;
+ });
+ }
+ });
+
+ },
+
+ cancelPrompt: function(request, typesInfo) {
+ this.sendToBrowserWindow("cancel-permission-prompt", request,
+ typesInfo);
+ },
+
+ delegatePrompt: function(request, typesInfo, callback) {
+ this.sendToBrowserWindow("permission-prompt", request, typesInfo,
+ function(type, remember, choices) {
+ if (type == "permission-allow") {
+ rememberPermission(typesInfo, request.principal, !remember);
+ if (callback) {
+ callback();
+ }
+ request.allow(choices);
+ return;
+ }
+
+ let addDenyPermission = function(type) {
+ debug("add " + type.permission +
+ " to permission manager with DENY_ACTION");
+ if (remember) {
+ Services.perms.addFromPrincipal(request.principal, type.access,
+ Ci.nsIPermissionManager.DENY_ACTION);
+ } else if (PERMISSION_NO_SESSION.indexOf(type.access) < 0) {
+ Services.perms.addFromPrincipal(request.principal, type.access,
+ Ci.nsIPermissionManager.DENY_ACTION,
+ Ci.nsIPermissionManager.EXPIRE_SESSION,
+ 0);
+ }
+ }
+ try {
+ // This will trow if we are canceling because the remote process died.
+ // Just eat the exception and call the callback that will cleanup the
+ // visibility event listener.
+ typesInfo.forEach(addDenyPermission);
+ } catch(e) { }
+
+ if (callback) {
+ callback();
+ }
+
+ try {
+ request.cancel();
+ } catch(e) { }
+ });
+ },
+
+ sendToBrowserWindow: function(type, request, typesInfo, callback) {
+ let requestId = Cc["@mozilla.org/uuid-generator;1"]
+ .getService(Ci.nsIUUIDGenerator).generateUUID().toString();
+ if (callback) {
+ SystemAppProxy.addEventListener("mozContentEvent", function contentEvent(evt) {
+ let detail = evt.detail;
+ if (detail.id != requestId)
+ return;
+ SystemAppProxy.removeEventListener("mozContentEvent", contentEvent);
+
+ callback(detail.type, detail.remember, detail.choices);
+ })
+ }
+
+ let principal = request.principal;
+ let isApp = principal.appStatus != Ci.nsIPrincipal.APP_STATUS_NOT_INSTALLED;
+ let remember = (principal.appStatus == Ci.nsIPrincipal.APP_STATUS_PRIVILEGED ||
+ principal.appStatus == Ci.nsIPrincipal.APP_STATUS_CERTIFIED)
+ ? true
+ : request.remember;
+ let isGranted = typesInfo.every(function(type) {
+ return type.action == Ci.nsIPermissionManager.ALLOW_ACTION;
+ });
+ let permissions = {};
+ for (let i in typesInfo) {
+ debug("prompt " + typesInfo[i].permission);
+ permissions[typesInfo[i].permission] = typesInfo[i].options;
+ }
+
+ let details = {
+ type: type,
+ permissions: permissions,
+ id: requestId,
+ // This system app uses the origin from permission events to
+ // compare against the mozApp.origin of app windows, so we
+ // are not concerned with origin suffixes here (appId, etc).
+ origin: principal.originNoSuffix,
+ isApp: isApp,
+ remember: remember,
+ isGranted: isGranted,
+ };
+
+ if (isApp) {
+ details.manifestURL = DOMApplicationRegistry.getManifestURLByLocalId(principal.appId);
+ }
+
+ // request.element is defined for OOP content, while request.window
+ // is defined for In-Process content.
+ // In both cases the message needs to be dispatched to the top-level
+ // <iframe mozbrowser> container in the system app.
+ // So the above code iterates over window.realFrameElement in order
+ // to crosss mozbrowser iframes boundaries and find the top-level
+ // one in the system app.
+ // window.realFrameElement will be |null| if the code try to cross
+ // content -> chrome boundaries.
+ let targetElement = request.element;
+ let targetWindow = request.window || targetElement.ownerDocument.defaultView;
+ while (targetWindow.realFrameElement) {
+ targetElement = targetWindow.realFrameElement;
+ targetWindow = targetElement.ownerDocument.defaultView;
+ }
+
+ SystemAppProxy.dispatchEvent(details, targetElement);
+ },
+
+ classID: Components.ID("{8c719f03-afe0-4aac-91ff-6c215895d467}"),
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPermissionPrompt])
+};
+
+(function() {
+ // Do not allow GetUserMedia while in call.
+ permissionSpecificChecker["audio-capture"] = function(request) {
+ let forbid = false;
+
+ if (forbid) {
+ request.cancel();
+ }
+
+ return forbid;
+ };
+})();
+
+//module initialization
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ContentPermissionPrompt]);