michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict" michael@0: michael@0: function debug(str) { michael@0: //dump("-*- ContentPermissionPrompt: " + str + "\n"); michael@0: } michael@0: michael@0: const Ci = Components.interfaces; michael@0: const Cr = Components.results; michael@0: const Cu = Components.utils; michael@0: const Cc = Components.classes; michael@0: michael@0: const PROMPT_FOR_UNKNOWN = ["audio-capture", michael@0: "desktop-notification", michael@0: "geolocation", michael@0: "video-capture"]; michael@0: // Due to privary issue, permission requests like GetUserMedia should prompt michael@0: // every time instead of providing session persistence. michael@0: const PERMISSION_NO_SESSION = ["audio-capture", "video-capture"]; michael@0: const ALLOW_MULTIPLE_REQUESTS = ["audio-capture", "video-capture"]; michael@0: michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/Webapps.jsm"); michael@0: Cu.import("resource://gre/modules/AppsUtils.jsm"); michael@0: Cu.import("resource://gre/modules/PermissionsInstaller.jsm"); michael@0: Cu.import("resource://gre/modules/PermissionsTable.jsm"); michael@0: michael@0: var permissionManager = Cc["@mozilla.org/permissionmanager;1"].getService(Ci.nsIPermissionManager); michael@0: var secMan = Cc["@mozilla.org/scriptsecuritymanager;1"].getService(Ci.nsIScriptSecurityManager); michael@0: michael@0: let permissionSpecificChecker = {}; michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, michael@0: "AudioManager", michael@0: "@mozilla.org/telephony/audiomanager;1", michael@0: "nsIAudioManager"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "SystemAppProxy", michael@0: "resource://gre/modules/SystemAppProxy.jsm"); michael@0: michael@0: /** michael@0: * aTypesInfo is an array of {permission, access, action, deny} which keeps michael@0: * the information of each permission. This arrary is initialized in michael@0: * ContentPermissionPrompt.prompt and used among functions. michael@0: * michael@0: * aTypesInfo[].permission : permission name michael@0: * aTypesInfo[].access : permission name + request.access michael@0: * aTypesInfo[].action : the default action of this permission michael@0: * aTypesInfo[].deny : true if security manager denied this app's origin michael@0: * principal. michael@0: * Note: michael@0: * aTypesInfo[].permission will be sent to prompt only when michael@0: * aTypesInfo[].action is PROMPT_ACTION and aTypesInfo[].deny is false. michael@0: */ michael@0: function rememberPermission(aTypesInfo, aPrincipal, aSession) michael@0: { michael@0: function convertPermToAllow(aPerm, aPrincipal) michael@0: { michael@0: let type = michael@0: permissionManager.testExactPermissionFromPrincipal(aPrincipal, aPerm); michael@0: if (type == Ci.nsIPermissionManager.PROMPT_ACTION || michael@0: (type == Ci.nsIPermissionManager.UNKNOWN_ACTION && michael@0: PROMPT_FOR_UNKNOWN.indexOf(aPerm) >= 0)) { michael@0: debug("add " + aPerm + " to permission manager with ALLOW_ACTION"); michael@0: if (!aSession) { michael@0: permissionManager.addFromPrincipal(aPrincipal, michael@0: aPerm, michael@0: Ci.nsIPermissionManager.ALLOW_ACTION); michael@0: } else if (PERMISSION_NO_SESSION.indexOf(aPerm) < 0) { michael@0: permissionManager.addFromPrincipal(aPrincipal, michael@0: aPerm, michael@0: Ci.nsIPermissionManager.ALLOW_ACTION, michael@0: Ci.nsIPermissionManager.EXPIRE_SESSION, 0); michael@0: } michael@0: } michael@0: } michael@0: michael@0: for (let i in aTypesInfo) { michael@0: // Expand the permission to see if we have multiple access properties michael@0: // to convert michael@0: let perm = aTypesInfo[i].permission; michael@0: let access = PermissionsTable[perm].access; michael@0: if (access) { michael@0: for (let idx in access) { michael@0: convertPermToAllow(perm + "-" + access[idx], aPrincipal); michael@0: } michael@0: } else { michael@0: convertPermToAllow(perm, aPrincipal); michael@0: } michael@0: } michael@0: } michael@0: michael@0: function ContentPermissionPrompt() {} michael@0: michael@0: ContentPermissionPrompt.prototype = { michael@0: michael@0: handleExistingPermission: function handleExistingPermission(request, michael@0: typesInfo) { michael@0: typesInfo.forEach(function(type) { michael@0: type.action = michael@0: Services.perms.testExactPermissionFromPrincipal(request.principal, michael@0: type.access); michael@0: if (type.action == Ci.nsIPermissionManager.UNKNOWN_ACTION && michael@0: PROMPT_FOR_UNKNOWN.indexOf(type.access) >= 0) { michael@0: type.action = Ci.nsIPermissionManager.PROMPT_ACTION; michael@0: } michael@0: }); michael@0: michael@0: // If all permissions are allowed already, call allow() without prompting. michael@0: let checkAllowPermission = function(type) { michael@0: if (type.action == Ci.nsIPermissionManager.ALLOW_ACTION) { michael@0: return true; michael@0: } michael@0: return false; michael@0: } michael@0: if (typesInfo.every(checkAllowPermission)) { michael@0: debug("all permission requests are allowed"); michael@0: request.allow(); michael@0: return true; michael@0: } michael@0: michael@0: // If all permissions are DENY_ACTION or UNKNOWN_ACTION, call cancel() michael@0: // without prompting. michael@0: let checkDenyPermission = function(type) { michael@0: if (type.action == Ci.nsIPermissionManager.DENY_ACTION || michael@0: type.action == Ci.nsIPermissionManager.UNKNOWN_ACTION) { michael@0: return true; michael@0: } michael@0: return false; michael@0: } michael@0: if (typesInfo.every(checkDenyPermission)) { michael@0: debug("all permission requests are denied"); michael@0: request.cancel(); michael@0: return true; michael@0: } michael@0: return false; michael@0: }, michael@0: michael@0: // multiple requests should be audio and video michael@0: checkMultipleRequest: function checkMultipleRequest(typesInfo) { michael@0: if (typesInfo.length == 1) { michael@0: return true; michael@0: } else if (typesInfo.length > 1) { michael@0: let checkIfAllowMultiRequest = function(type) { michael@0: return (ALLOW_MULTIPLE_REQUESTS.indexOf(type.access) !== -1); michael@0: } michael@0: if (typesInfo.every(checkIfAllowMultiRequest)) { michael@0: debug("legal multiple requests"); michael@0: return true; michael@0: } michael@0: } michael@0: michael@0: return false; michael@0: }, michael@0: michael@0: handledByApp: function handledByApp(request, typesInfo) { michael@0: if (request.principal.appId == Ci.nsIScriptSecurityManager.NO_APP_ID || michael@0: request.principal.appId == Ci.nsIScriptSecurityManager.UNKNOWN_APP_ID) { michael@0: // This should not really happen michael@0: request.cancel(); michael@0: return true; michael@0: } michael@0: michael@0: let appsService = Cc["@mozilla.org/AppsService;1"] michael@0: .getService(Ci.nsIAppsService); michael@0: let app = appsService.getAppByLocalId(request.principal.appId); michael@0: michael@0: // Check each permission if it's denied by permission manager with app's michael@0: // URL. michael@0: let notDenyAppPrincipal = function(type) { michael@0: let url = Services.io.newURI(app.origin, null, null); michael@0: let principal = secMan.getAppCodebasePrincipal(url, michael@0: request.principal.appId, michael@0: /*mozbrowser*/false); michael@0: let result = Services.perms.testExactPermissionFromPrincipal(principal, michael@0: type.access); michael@0: michael@0: if (result == Ci.nsIPermissionManager.ALLOW_ACTION || michael@0: result == Ci.nsIPermissionManager.PROMPT_ACTION) { michael@0: type.deny = false; michael@0: } michael@0: return !type.deny; michael@0: } michael@0: if (typesInfo.filter(notDenyAppPrincipal).length === 0) { michael@0: request.cancel(); michael@0: return true; michael@0: } michael@0: michael@0: return false; michael@0: }, michael@0: michael@0: handledByPermissionType: function handledByPermissionType(request, typesInfo) { michael@0: for (let i in typesInfo) { michael@0: if (permissionSpecificChecker.hasOwnProperty(typesInfo[i].permission) && michael@0: permissionSpecificChecker[typesInfo[i].permission](request)) { michael@0: return true; michael@0: } michael@0: } michael@0: michael@0: return false; michael@0: }, michael@0: michael@0: _id: 0, michael@0: prompt: function(request) { michael@0: if (secMan.isSystemPrincipal(request.principal)) { michael@0: request.allow(); michael@0: return; michael@0: } michael@0: michael@0: // Initialize the typesInfo and set the default value. michael@0: let typesInfo = []; michael@0: let perms = request.types.QueryInterface(Ci.nsIArray); michael@0: for (let idx = 0; idx < perms.length; idx++) { michael@0: let perm = perms.queryElementAt(idx, Ci.nsIContentPermissionType); michael@0: let tmp = { michael@0: permission: perm.type, michael@0: access: (perm.access && perm.access !== "unused") ? michael@0: perm.type + "-" + perm.access : perm.type, michael@0: options: [], michael@0: deny: true, michael@0: action: Ci.nsIPermissionManager.UNKNOWN_ACTION michael@0: }; michael@0: michael@0: // Append available options, if any. michael@0: let options = perm.options.QueryInterface(Ci.nsIArray); michael@0: for (let i = 0; i < options.length; i++) { michael@0: let option = options.queryElementAt(i, Ci.nsISupportsString).data; michael@0: tmp.options.push(option); michael@0: } michael@0: typesInfo.push(tmp); michael@0: } michael@0: michael@0: if (typesInfo.length == 0) { michael@0: request.cancel(); michael@0: return; michael@0: } michael@0: michael@0: if(!this.checkMultipleRequest(typesInfo)) { michael@0: request.cancel(); michael@0: return; michael@0: } michael@0: michael@0: if (this.handledByApp(request, typesInfo) || michael@0: this.handledByPermissionType(request, typesInfo)) { michael@0: return; michael@0: } michael@0: michael@0: // returns true if the request was handled michael@0: if (this.handleExistingPermission(request, typesInfo)) { michael@0: return; michael@0: } michael@0: michael@0: // prompt PROMPT_ACTION request only. michael@0: typesInfo.forEach(function(aType, aIndex) { michael@0: if (aType.action != Ci.nsIPermissionManager.PROMPT_ACTION || aType.deny) { michael@0: typesInfo.splice(aIndex); michael@0: } michael@0: }); michael@0: michael@0: let frame = request.element; michael@0: let requestId = this._id++; michael@0: michael@0: if (!frame) { michael@0: this.delegatePrompt(request, requestId, typesInfo); michael@0: return; michael@0: } michael@0: michael@0: frame = frame.wrappedJSObject; michael@0: var cancelRequest = function() { michael@0: frame.removeEventListener("mozbrowservisibilitychange", onVisibilityChange); michael@0: request.cancel(); michael@0: } michael@0: michael@0: var self = this; michael@0: var onVisibilityChange = function(evt) { michael@0: if (evt.detail.visible === true) michael@0: return; michael@0: michael@0: self.cancelPrompt(request, requestId, typesInfo); michael@0: cancelRequest(); michael@0: } michael@0: michael@0: // If the request was initiated from a hidden iframe michael@0: // we don't forward it to content and cancel it right away michael@0: let domRequest = frame.getVisible(); michael@0: domRequest.onsuccess = function gv_success(evt) { michael@0: if (!evt.target.result) { michael@0: cancelRequest(); michael@0: return; michael@0: } michael@0: michael@0: // Monitor the frame visibility and cancel the request if the frame goes michael@0: // away but the request is still here. michael@0: frame.addEventListener("mozbrowservisibilitychange", onVisibilityChange); michael@0: michael@0: self.delegatePrompt(request, requestId, typesInfo, function onCallback() { michael@0: frame.removeEventListener("mozbrowservisibilitychange", onVisibilityChange); michael@0: }); michael@0: }; michael@0: michael@0: // Something went wrong. Let's cancel the request just in case. michael@0: domRequest.onerror = function gv_error() { michael@0: cancelRequest(); michael@0: } michael@0: }, michael@0: michael@0: cancelPrompt: function(request, requestId, typesInfo) { michael@0: this.sendToBrowserWindow("cancel-permission-prompt", request, requestId, michael@0: typesInfo); michael@0: }, michael@0: michael@0: delegatePrompt: function(request, requestId, typesInfo, callback) { michael@0: michael@0: this.sendToBrowserWindow("permission-prompt", request, requestId, typesInfo, michael@0: function(type, remember, choices) { michael@0: if (type == "permission-allow") { michael@0: rememberPermission(typesInfo, request.principal, !remember); michael@0: if (callback) { michael@0: callback(); michael@0: } michael@0: request.allow(choices); michael@0: return; michael@0: } michael@0: michael@0: let addDenyPermission = function(type) { michael@0: debug("add " + type.permission + michael@0: " to permission manager with DENY_ACTION"); michael@0: if (remember) { michael@0: Services.perms.addFromPrincipal(request.principal, type.access, michael@0: Ci.nsIPermissionManager.DENY_ACTION); michael@0: } else if (PERMISSION_NO_SESSION.indexOf(type.access) < 0) { michael@0: Services.perms.addFromPrincipal(request.principal, type.access, michael@0: Ci.nsIPermissionManager.DENY_ACTION, michael@0: Ci.nsIPermissionManager.EXPIRE_SESSION, michael@0: 0); michael@0: } michael@0: } michael@0: typesInfo.forEach(addDenyPermission); michael@0: michael@0: if (callback) { michael@0: callback(); michael@0: } michael@0: request.cancel(); michael@0: }); michael@0: }, michael@0: michael@0: sendToBrowserWindow: function(type, request, requestId, typesInfo, callback) { michael@0: if (callback) { michael@0: SystemAppProxy.addEventListener("mozContentEvent", function contentEvent(evt) { michael@0: let detail = evt.detail; michael@0: if (detail.id != requestId) michael@0: return; michael@0: SystemAppProxy.removeEventListener("mozContentEvent", contentEvent); michael@0: michael@0: callback(detail.type, detail.remember, detail.choices); michael@0: }) michael@0: } michael@0: michael@0: let principal = request.principal; michael@0: let isApp = principal.appStatus != Ci.nsIPrincipal.APP_STATUS_NOT_INSTALLED; michael@0: let remember = (principal.appStatus == Ci.nsIPrincipal.APP_STATUS_PRIVILEGED || michael@0: principal.appStatus == Ci.nsIPrincipal.APP_STATUS_CERTIFIED) michael@0: ? true michael@0: : request.remember; michael@0: let permissions = {}; michael@0: for (let i in typesInfo) { michael@0: debug("prompt " + typesInfo[i].permission); michael@0: permissions[typesInfo[i].permission] = typesInfo[i].options; michael@0: } michael@0: michael@0: let details = { michael@0: type: type, michael@0: permissions: permissions, michael@0: id: requestId, michael@0: origin: principal.origin, michael@0: isApp: isApp, michael@0: remember: remember michael@0: }; michael@0: michael@0: if (isApp) { michael@0: details.manifestURL = DOMApplicationRegistry.getManifestURLByLocalId(principal.appId); michael@0: } michael@0: SystemAppProxy.dispatchEvent(details); michael@0: }, michael@0: michael@0: classID: Components.ID("{8c719f03-afe0-4aac-91ff-6c215895d467}"), michael@0: michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPermissionPrompt]) michael@0: }; michael@0: michael@0: (function() { michael@0: // Do not allow GetUserMedia while in call. michael@0: permissionSpecificChecker["audio-capture"] = function(request) { michael@0: if (AudioManager.phoneState === Ci.nsIAudioManager.PHONE_STATE_IN_CALL) { michael@0: request.cancel(); michael@0: return true; michael@0: } else { michael@0: return false; michael@0: } michael@0: }; michael@0: })(); michael@0: michael@0: //module initialization michael@0: this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ContentPermissionPrompt]);