1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/b2g/components/ContentPermissionPrompt.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,408 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +"use strict" 1.9 + 1.10 +function debug(str) { 1.11 + //dump("-*- ContentPermissionPrompt: " + str + "\n"); 1.12 +} 1.13 + 1.14 +const Ci = Components.interfaces; 1.15 +const Cr = Components.results; 1.16 +const Cu = Components.utils; 1.17 +const Cc = Components.classes; 1.18 + 1.19 +const PROMPT_FOR_UNKNOWN = ["audio-capture", 1.20 + "desktop-notification", 1.21 + "geolocation", 1.22 + "video-capture"]; 1.23 +// Due to privary issue, permission requests like GetUserMedia should prompt 1.24 +// every time instead of providing session persistence. 1.25 +const PERMISSION_NO_SESSION = ["audio-capture", "video-capture"]; 1.26 +const ALLOW_MULTIPLE_REQUESTS = ["audio-capture", "video-capture"]; 1.27 + 1.28 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.29 +Cu.import("resource://gre/modules/Services.jsm"); 1.30 +Cu.import("resource://gre/modules/Webapps.jsm"); 1.31 +Cu.import("resource://gre/modules/AppsUtils.jsm"); 1.32 +Cu.import("resource://gre/modules/PermissionsInstaller.jsm"); 1.33 +Cu.import("resource://gre/modules/PermissionsTable.jsm"); 1.34 + 1.35 +var permissionManager = Cc["@mozilla.org/permissionmanager;1"].getService(Ci.nsIPermissionManager); 1.36 +var secMan = Cc["@mozilla.org/scriptsecuritymanager;1"].getService(Ci.nsIScriptSecurityManager); 1.37 + 1.38 +let permissionSpecificChecker = {}; 1.39 + 1.40 +XPCOMUtils.defineLazyServiceGetter(this, 1.41 + "AudioManager", 1.42 + "@mozilla.org/telephony/audiomanager;1", 1.43 + "nsIAudioManager"); 1.44 + 1.45 +XPCOMUtils.defineLazyModuleGetter(this, "SystemAppProxy", 1.46 + "resource://gre/modules/SystemAppProxy.jsm"); 1.47 + 1.48 +/** 1.49 + * aTypesInfo is an array of {permission, access, action, deny} which keeps 1.50 + * the information of each permission. This arrary is initialized in 1.51 + * ContentPermissionPrompt.prompt and used among functions. 1.52 + * 1.53 + * aTypesInfo[].permission : permission name 1.54 + * aTypesInfo[].access : permission name + request.access 1.55 + * aTypesInfo[].action : the default action of this permission 1.56 + * aTypesInfo[].deny : true if security manager denied this app's origin 1.57 + * principal. 1.58 + * Note: 1.59 + * aTypesInfo[].permission will be sent to prompt only when 1.60 + * aTypesInfo[].action is PROMPT_ACTION and aTypesInfo[].deny is false. 1.61 + */ 1.62 +function rememberPermission(aTypesInfo, aPrincipal, aSession) 1.63 +{ 1.64 + function convertPermToAllow(aPerm, aPrincipal) 1.65 + { 1.66 + let type = 1.67 + permissionManager.testExactPermissionFromPrincipal(aPrincipal, aPerm); 1.68 + if (type == Ci.nsIPermissionManager.PROMPT_ACTION || 1.69 + (type == Ci.nsIPermissionManager.UNKNOWN_ACTION && 1.70 + PROMPT_FOR_UNKNOWN.indexOf(aPerm) >= 0)) { 1.71 + debug("add " + aPerm + " to permission manager with ALLOW_ACTION"); 1.72 + if (!aSession) { 1.73 + permissionManager.addFromPrincipal(aPrincipal, 1.74 + aPerm, 1.75 + Ci.nsIPermissionManager.ALLOW_ACTION); 1.76 + } else if (PERMISSION_NO_SESSION.indexOf(aPerm) < 0) { 1.77 + permissionManager.addFromPrincipal(aPrincipal, 1.78 + aPerm, 1.79 + Ci.nsIPermissionManager.ALLOW_ACTION, 1.80 + Ci.nsIPermissionManager.EXPIRE_SESSION, 0); 1.81 + } 1.82 + } 1.83 + } 1.84 + 1.85 + for (let i in aTypesInfo) { 1.86 + // Expand the permission to see if we have multiple access properties 1.87 + // to convert 1.88 + let perm = aTypesInfo[i].permission; 1.89 + let access = PermissionsTable[perm].access; 1.90 + if (access) { 1.91 + for (let idx in access) { 1.92 + convertPermToAllow(perm + "-" + access[idx], aPrincipal); 1.93 + } 1.94 + } else { 1.95 + convertPermToAllow(perm, aPrincipal); 1.96 + } 1.97 + } 1.98 +} 1.99 + 1.100 +function ContentPermissionPrompt() {} 1.101 + 1.102 +ContentPermissionPrompt.prototype = { 1.103 + 1.104 + handleExistingPermission: function handleExistingPermission(request, 1.105 + typesInfo) { 1.106 + typesInfo.forEach(function(type) { 1.107 + type.action = 1.108 + Services.perms.testExactPermissionFromPrincipal(request.principal, 1.109 + type.access); 1.110 + if (type.action == Ci.nsIPermissionManager.UNKNOWN_ACTION && 1.111 + PROMPT_FOR_UNKNOWN.indexOf(type.access) >= 0) { 1.112 + type.action = Ci.nsIPermissionManager.PROMPT_ACTION; 1.113 + } 1.114 + }); 1.115 + 1.116 + // If all permissions are allowed already, call allow() without prompting. 1.117 + let checkAllowPermission = function(type) { 1.118 + if (type.action == Ci.nsIPermissionManager.ALLOW_ACTION) { 1.119 + return true; 1.120 + } 1.121 + return false; 1.122 + } 1.123 + if (typesInfo.every(checkAllowPermission)) { 1.124 + debug("all permission requests are allowed"); 1.125 + request.allow(); 1.126 + return true; 1.127 + } 1.128 + 1.129 + // If all permissions are DENY_ACTION or UNKNOWN_ACTION, call cancel() 1.130 + // without prompting. 1.131 + let checkDenyPermission = function(type) { 1.132 + if (type.action == Ci.nsIPermissionManager.DENY_ACTION || 1.133 + type.action == Ci.nsIPermissionManager.UNKNOWN_ACTION) { 1.134 + return true; 1.135 + } 1.136 + return false; 1.137 + } 1.138 + if (typesInfo.every(checkDenyPermission)) { 1.139 + debug("all permission requests are denied"); 1.140 + request.cancel(); 1.141 + return true; 1.142 + } 1.143 + return false; 1.144 + }, 1.145 + 1.146 + // multiple requests should be audio and video 1.147 + checkMultipleRequest: function checkMultipleRequest(typesInfo) { 1.148 + if (typesInfo.length == 1) { 1.149 + return true; 1.150 + } else if (typesInfo.length > 1) { 1.151 + let checkIfAllowMultiRequest = function(type) { 1.152 + return (ALLOW_MULTIPLE_REQUESTS.indexOf(type.access) !== -1); 1.153 + } 1.154 + if (typesInfo.every(checkIfAllowMultiRequest)) { 1.155 + debug("legal multiple requests"); 1.156 + return true; 1.157 + } 1.158 + } 1.159 + 1.160 + return false; 1.161 + }, 1.162 + 1.163 + handledByApp: function handledByApp(request, typesInfo) { 1.164 + if (request.principal.appId == Ci.nsIScriptSecurityManager.NO_APP_ID || 1.165 + request.principal.appId == Ci.nsIScriptSecurityManager.UNKNOWN_APP_ID) { 1.166 + // This should not really happen 1.167 + request.cancel(); 1.168 + return true; 1.169 + } 1.170 + 1.171 + let appsService = Cc["@mozilla.org/AppsService;1"] 1.172 + .getService(Ci.nsIAppsService); 1.173 + let app = appsService.getAppByLocalId(request.principal.appId); 1.174 + 1.175 + // Check each permission if it's denied by permission manager with app's 1.176 + // URL. 1.177 + let notDenyAppPrincipal = function(type) { 1.178 + let url = Services.io.newURI(app.origin, null, null); 1.179 + let principal = secMan.getAppCodebasePrincipal(url, 1.180 + request.principal.appId, 1.181 + /*mozbrowser*/false); 1.182 + let result = Services.perms.testExactPermissionFromPrincipal(principal, 1.183 + type.access); 1.184 + 1.185 + if (result == Ci.nsIPermissionManager.ALLOW_ACTION || 1.186 + result == Ci.nsIPermissionManager.PROMPT_ACTION) { 1.187 + type.deny = false; 1.188 + } 1.189 + return !type.deny; 1.190 + } 1.191 + if (typesInfo.filter(notDenyAppPrincipal).length === 0) { 1.192 + request.cancel(); 1.193 + return true; 1.194 + } 1.195 + 1.196 + return false; 1.197 + }, 1.198 + 1.199 + handledByPermissionType: function handledByPermissionType(request, typesInfo) { 1.200 + for (let i in typesInfo) { 1.201 + if (permissionSpecificChecker.hasOwnProperty(typesInfo[i].permission) && 1.202 + permissionSpecificChecker[typesInfo[i].permission](request)) { 1.203 + return true; 1.204 + } 1.205 + } 1.206 + 1.207 + return false; 1.208 + }, 1.209 + 1.210 + _id: 0, 1.211 + prompt: function(request) { 1.212 + if (secMan.isSystemPrincipal(request.principal)) { 1.213 + request.allow(); 1.214 + return; 1.215 + } 1.216 + 1.217 + // Initialize the typesInfo and set the default value. 1.218 + let typesInfo = []; 1.219 + let perms = request.types.QueryInterface(Ci.nsIArray); 1.220 + for (let idx = 0; idx < perms.length; idx++) { 1.221 + let perm = perms.queryElementAt(idx, Ci.nsIContentPermissionType); 1.222 + let tmp = { 1.223 + permission: perm.type, 1.224 + access: (perm.access && perm.access !== "unused") ? 1.225 + perm.type + "-" + perm.access : perm.type, 1.226 + options: [], 1.227 + deny: true, 1.228 + action: Ci.nsIPermissionManager.UNKNOWN_ACTION 1.229 + }; 1.230 + 1.231 + // Append available options, if any. 1.232 + let options = perm.options.QueryInterface(Ci.nsIArray); 1.233 + for (let i = 0; i < options.length; i++) { 1.234 + let option = options.queryElementAt(i, Ci.nsISupportsString).data; 1.235 + tmp.options.push(option); 1.236 + } 1.237 + typesInfo.push(tmp); 1.238 + } 1.239 + 1.240 + if (typesInfo.length == 0) { 1.241 + request.cancel(); 1.242 + return; 1.243 + } 1.244 + 1.245 + if(!this.checkMultipleRequest(typesInfo)) { 1.246 + request.cancel(); 1.247 + return; 1.248 + } 1.249 + 1.250 + if (this.handledByApp(request, typesInfo) || 1.251 + this.handledByPermissionType(request, typesInfo)) { 1.252 + return; 1.253 + } 1.254 + 1.255 + // returns true if the request was handled 1.256 + if (this.handleExistingPermission(request, typesInfo)) { 1.257 + return; 1.258 + } 1.259 + 1.260 + // prompt PROMPT_ACTION request only. 1.261 + typesInfo.forEach(function(aType, aIndex) { 1.262 + if (aType.action != Ci.nsIPermissionManager.PROMPT_ACTION || aType.deny) { 1.263 + typesInfo.splice(aIndex); 1.264 + } 1.265 + }); 1.266 + 1.267 + let frame = request.element; 1.268 + let requestId = this._id++; 1.269 + 1.270 + if (!frame) { 1.271 + this.delegatePrompt(request, requestId, typesInfo); 1.272 + return; 1.273 + } 1.274 + 1.275 + frame = frame.wrappedJSObject; 1.276 + var cancelRequest = function() { 1.277 + frame.removeEventListener("mozbrowservisibilitychange", onVisibilityChange); 1.278 + request.cancel(); 1.279 + } 1.280 + 1.281 + var self = this; 1.282 + var onVisibilityChange = function(evt) { 1.283 + if (evt.detail.visible === true) 1.284 + return; 1.285 + 1.286 + self.cancelPrompt(request, requestId, typesInfo); 1.287 + cancelRequest(); 1.288 + } 1.289 + 1.290 + // If the request was initiated from a hidden iframe 1.291 + // we don't forward it to content and cancel it right away 1.292 + let domRequest = frame.getVisible(); 1.293 + domRequest.onsuccess = function gv_success(evt) { 1.294 + if (!evt.target.result) { 1.295 + cancelRequest(); 1.296 + return; 1.297 + } 1.298 + 1.299 + // Monitor the frame visibility and cancel the request if the frame goes 1.300 + // away but the request is still here. 1.301 + frame.addEventListener("mozbrowservisibilitychange", onVisibilityChange); 1.302 + 1.303 + self.delegatePrompt(request, requestId, typesInfo, function onCallback() { 1.304 + frame.removeEventListener("mozbrowservisibilitychange", onVisibilityChange); 1.305 + }); 1.306 + }; 1.307 + 1.308 + // Something went wrong. Let's cancel the request just in case. 1.309 + domRequest.onerror = function gv_error() { 1.310 + cancelRequest(); 1.311 + } 1.312 + }, 1.313 + 1.314 + cancelPrompt: function(request, requestId, typesInfo) { 1.315 + this.sendToBrowserWindow("cancel-permission-prompt", request, requestId, 1.316 + typesInfo); 1.317 + }, 1.318 + 1.319 + delegatePrompt: function(request, requestId, typesInfo, callback) { 1.320 + 1.321 + this.sendToBrowserWindow("permission-prompt", request, requestId, typesInfo, 1.322 + function(type, remember, choices) { 1.323 + if (type == "permission-allow") { 1.324 + rememberPermission(typesInfo, request.principal, !remember); 1.325 + if (callback) { 1.326 + callback(); 1.327 + } 1.328 + request.allow(choices); 1.329 + return; 1.330 + } 1.331 + 1.332 + let addDenyPermission = function(type) { 1.333 + debug("add " + type.permission + 1.334 + " to permission manager with DENY_ACTION"); 1.335 + if (remember) { 1.336 + Services.perms.addFromPrincipal(request.principal, type.access, 1.337 + Ci.nsIPermissionManager.DENY_ACTION); 1.338 + } else if (PERMISSION_NO_SESSION.indexOf(type.access) < 0) { 1.339 + Services.perms.addFromPrincipal(request.principal, type.access, 1.340 + Ci.nsIPermissionManager.DENY_ACTION, 1.341 + Ci.nsIPermissionManager.EXPIRE_SESSION, 1.342 + 0); 1.343 + } 1.344 + } 1.345 + typesInfo.forEach(addDenyPermission); 1.346 + 1.347 + if (callback) { 1.348 + callback(); 1.349 + } 1.350 + request.cancel(); 1.351 + }); 1.352 + }, 1.353 + 1.354 + sendToBrowserWindow: function(type, request, requestId, typesInfo, callback) { 1.355 + if (callback) { 1.356 + SystemAppProxy.addEventListener("mozContentEvent", function contentEvent(evt) { 1.357 + let detail = evt.detail; 1.358 + if (detail.id != requestId) 1.359 + return; 1.360 + SystemAppProxy.removeEventListener("mozContentEvent", contentEvent); 1.361 + 1.362 + callback(detail.type, detail.remember, detail.choices); 1.363 + }) 1.364 + } 1.365 + 1.366 + let principal = request.principal; 1.367 + let isApp = principal.appStatus != Ci.nsIPrincipal.APP_STATUS_NOT_INSTALLED; 1.368 + let remember = (principal.appStatus == Ci.nsIPrincipal.APP_STATUS_PRIVILEGED || 1.369 + principal.appStatus == Ci.nsIPrincipal.APP_STATUS_CERTIFIED) 1.370 + ? true 1.371 + : request.remember; 1.372 + let permissions = {}; 1.373 + for (let i in typesInfo) { 1.374 + debug("prompt " + typesInfo[i].permission); 1.375 + permissions[typesInfo[i].permission] = typesInfo[i].options; 1.376 + } 1.377 + 1.378 + let details = { 1.379 + type: type, 1.380 + permissions: permissions, 1.381 + id: requestId, 1.382 + origin: principal.origin, 1.383 + isApp: isApp, 1.384 + remember: remember 1.385 + }; 1.386 + 1.387 + if (isApp) { 1.388 + details.manifestURL = DOMApplicationRegistry.getManifestURLByLocalId(principal.appId); 1.389 + } 1.390 + SystemAppProxy.dispatchEvent(details); 1.391 + }, 1.392 + 1.393 + classID: Components.ID("{8c719f03-afe0-4aac-91ff-6c215895d467}"), 1.394 + 1.395 + QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPermissionPrompt]) 1.396 +}; 1.397 + 1.398 +(function() { 1.399 + // Do not allow GetUserMedia while in call. 1.400 + permissionSpecificChecker["audio-capture"] = function(request) { 1.401 + if (AudioManager.phoneState === Ci.nsIAudioManager.PHONE_STATE_IN_CALL) { 1.402 + request.cancel(); 1.403 + return true; 1.404 + } else { 1.405 + return false; 1.406 + } 1.407 + }; 1.408 +})(); 1.409 + 1.410 +//module initialization 1.411 +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ContentPermissionPrompt]);