Fri, 16 Jan 2015 18:13:44 +0100
Integrate suggestion from review to improve consistency with existing code.
michael@0 | 1 | /* This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, |
michael@0 | 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 4 | |
michael@0 | 5 | "use strict" |
michael@0 | 6 | |
michael@0 | 7 | function debug(str) { |
michael@0 | 8 | //dump("-*- ContentPermissionPrompt: " + str + "\n"); |
michael@0 | 9 | } |
michael@0 | 10 | |
michael@0 | 11 | const Ci = Components.interfaces; |
michael@0 | 12 | const Cr = Components.results; |
michael@0 | 13 | const Cu = Components.utils; |
michael@0 | 14 | const Cc = Components.classes; |
michael@0 | 15 | |
michael@0 | 16 | const PROMPT_FOR_UNKNOWN = ["audio-capture", |
michael@0 | 17 | "desktop-notification", |
michael@0 | 18 | "geolocation", |
michael@0 | 19 | "video-capture"]; |
michael@0 | 20 | // Due to privary issue, permission requests like GetUserMedia should prompt |
michael@0 | 21 | // every time instead of providing session persistence. |
michael@0 | 22 | const PERMISSION_NO_SESSION = ["audio-capture", "video-capture"]; |
michael@0 | 23 | const ALLOW_MULTIPLE_REQUESTS = ["audio-capture", "video-capture"]; |
michael@0 | 24 | |
michael@0 | 25 | Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
michael@0 | 26 | Cu.import("resource://gre/modules/Services.jsm"); |
michael@0 | 27 | Cu.import("resource://gre/modules/Webapps.jsm"); |
michael@0 | 28 | Cu.import("resource://gre/modules/AppsUtils.jsm"); |
michael@0 | 29 | Cu.import("resource://gre/modules/PermissionsInstaller.jsm"); |
michael@0 | 30 | Cu.import("resource://gre/modules/PermissionsTable.jsm"); |
michael@0 | 31 | |
michael@0 | 32 | var permissionManager = Cc["@mozilla.org/permissionmanager;1"].getService(Ci.nsIPermissionManager); |
michael@0 | 33 | var secMan = Cc["@mozilla.org/scriptsecuritymanager;1"].getService(Ci.nsIScriptSecurityManager); |
michael@0 | 34 | |
michael@0 | 35 | let permissionSpecificChecker = {}; |
michael@0 | 36 | |
michael@0 | 37 | XPCOMUtils.defineLazyServiceGetter(this, |
michael@0 | 38 | "AudioManager", |
michael@0 | 39 | "@mozilla.org/telephony/audiomanager;1", |
michael@0 | 40 | "nsIAudioManager"); |
michael@0 | 41 | |
michael@0 | 42 | XPCOMUtils.defineLazyModuleGetter(this, "SystemAppProxy", |
michael@0 | 43 | "resource://gre/modules/SystemAppProxy.jsm"); |
michael@0 | 44 | |
michael@0 | 45 | /** |
michael@0 | 46 | * aTypesInfo is an array of {permission, access, action, deny} which keeps |
michael@0 | 47 | * the information of each permission. This arrary is initialized in |
michael@0 | 48 | * ContentPermissionPrompt.prompt and used among functions. |
michael@0 | 49 | * |
michael@0 | 50 | * aTypesInfo[].permission : permission name |
michael@0 | 51 | * aTypesInfo[].access : permission name + request.access |
michael@0 | 52 | * aTypesInfo[].action : the default action of this permission |
michael@0 | 53 | * aTypesInfo[].deny : true if security manager denied this app's origin |
michael@0 | 54 | * principal. |
michael@0 | 55 | * Note: |
michael@0 | 56 | * aTypesInfo[].permission will be sent to prompt only when |
michael@0 | 57 | * aTypesInfo[].action is PROMPT_ACTION and aTypesInfo[].deny is false. |
michael@0 | 58 | */ |
michael@0 | 59 | function rememberPermission(aTypesInfo, aPrincipal, aSession) |
michael@0 | 60 | { |
michael@0 | 61 | function convertPermToAllow(aPerm, aPrincipal) |
michael@0 | 62 | { |
michael@0 | 63 | let type = |
michael@0 | 64 | permissionManager.testExactPermissionFromPrincipal(aPrincipal, aPerm); |
michael@0 | 65 | if (type == Ci.nsIPermissionManager.PROMPT_ACTION || |
michael@0 | 66 | (type == Ci.nsIPermissionManager.UNKNOWN_ACTION && |
michael@0 | 67 | PROMPT_FOR_UNKNOWN.indexOf(aPerm) >= 0)) { |
michael@0 | 68 | debug("add " + aPerm + " to permission manager with ALLOW_ACTION"); |
michael@0 | 69 | if (!aSession) { |
michael@0 | 70 | permissionManager.addFromPrincipal(aPrincipal, |
michael@0 | 71 | aPerm, |
michael@0 | 72 | Ci.nsIPermissionManager.ALLOW_ACTION); |
michael@0 | 73 | } else if (PERMISSION_NO_SESSION.indexOf(aPerm) < 0) { |
michael@0 | 74 | permissionManager.addFromPrincipal(aPrincipal, |
michael@0 | 75 | aPerm, |
michael@0 | 76 | Ci.nsIPermissionManager.ALLOW_ACTION, |
michael@0 | 77 | Ci.nsIPermissionManager.EXPIRE_SESSION, 0); |
michael@0 | 78 | } |
michael@0 | 79 | } |
michael@0 | 80 | } |
michael@0 | 81 | |
michael@0 | 82 | for (let i in aTypesInfo) { |
michael@0 | 83 | // Expand the permission to see if we have multiple access properties |
michael@0 | 84 | // to convert |
michael@0 | 85 | let perm = aTypesInfo[i].permission; |
michael@0 | 86 | let access = PermissionsTable[perm].access; |
michael@0 | 87 | if (access) { |
michael@0 | 88 | for (let idx in access) { |
michael@0 | 89 | convertPermToAllow(perm + "-" + access[idx], aPrincipal); |
michael@0 | 90 | } |
michael@0 | 91 | } else { |
michael@0 | 92 | convertPermToAllow(perm, aPrincipal); |
michael@0 | 93 | } |
michael@0 | 94 | } |
michael@0 | 95 | } |
michael@0 | 96 | |
michael@0 | 97 | function ContentPermissionPrompt() {} |
michael@0 | 98 | |
michael@0 | 99 | ContentPermissionPrompt.prototype = { |
michael@0 | 100 | |
michael@0 | 101 | handleExistingPermission: function handleExistingPermission(request, |
michael@0 | 102 | typesInfo) { |
michael@0 | 103 | typesInfo.forEach(function(type) { |
michael@0 | 104 | type.action = |
michael@0 | 105 | Services.perms.testExactPermissionFromPrincipal(request.principal, |
michael@0 | 106 | type.access); |
michael@0 | 107 | if (type.action == Ci.nsIPermissionManager.UNKNOWN_ACTION && |
michael@0 | 108 | PROMPT_FOR_UNKNOWN.indexOf(type.access) >= 0) { |
michael@0 | 109 | type.action = Ci.nsIPermissionManager.PROMPT_ACTION; |
michael@0 | 110 | } |
michael@0 | 111 | }); |
michael@0 | 112 | |
michael@0 | 113 | // If all permissions are allowed already, call allow() without prompting. |
michael@0 | 114 | let checkAllowPermission = function(type) { |
michael@0 | 115 | if (type.action == Ci.nsIPermissionManager.ALLOW_ACTION) { |
michael@0 | 116 | return true; |
michael@0 | 117 | } |
michael@0 | 118 | return false; |
michael@0 | 119 | } |
michael@0 | 120 | if (typesInfo.every(checkAllowPermission)) { |
michael@0 | 121 | debug("all permission requests are allowed"); |
michael@0 | 122 | request.allow(); |
michael@0 | 123 | return true; |
michael@0 | 124 | } |
michael@0 | 125 | |
michael@0 | 126 | // If all permissions are DENY_ACTION or UNKNOWN_ACTION, call cancel() |
michael@0 | 127 | // without prompting. |
michael@0 | 128 | let checkDenyPermission = function(type) { |
michael@0 | 129 | if (type.action == Ci.nsIPermissionManager.DENY_ACTION || |
michael@0 | 130 | type.action == Ci.nsIPermissionManager.UNKNOWN_ACTION) { |
michael@0 | 131 | return true; |
michael@0 | 132 | } |
michael@0 | 133 | return false; |
michael@0 | 134 | } |
michael@0 | 135 | if (typesInfo.every(checkDenyPermission)) { |
michael@0 | 136 | debug("all permission requests are denied"); |
michael@0 | 137 | request.cancel(); |
michael@0 | 138 | return true; |
michael@0 | 139 | } |
michael@0 | 140 | return false; |
michael@0 | 141 | }, |
michael@0 | 142 | |
michael@0 | 143 | // multiple requests should be audio and video |
michael@0 | 144 | checkMultipleRequest: function checkMultipleRequest(typesInfo) { |
michael@0 | 145 | if (typesInfo.length == 1) { |
michael@0 | 146 | return true; |
michael@0 | 147 | } else if (typesInfo.length > 1) { |
michael@0 | 148 | let checkIfAllowMultiRequest = function(type) { |
michael@0 | 149 | return (ALLOW_MULTIPLE_REQUESTS.indexOf(type.access) !== -1); |
michael@0 | 150 | } |
michael@0 | 151 | if (typesInfo.every(checkIfAllowMultiRequest)) { |
michael@0 | 152 | debug("legal multiple requests"); |
michael@0 | 153 | return true; |
michael@0 | 154 | } |
michael@0 | 155 | } |
michael@0 | 156 | |
michael@0 | 157 | return false; |
michael@0 | 158 | }, |
michael@0 | 159 | |
michael@0 | 160 | handledByApp: function handledByApp(request, typesInfo) { |
michael@0 | 161 | if (request.principal.appId == Ci.nsIScriptSecurityManager.NO_APP_ID || |
michael@0 | 162 | request.principal.appId == Ci.nsIScriptSecurityManager.UNKNOWN_APP_ID) { |
michael@0 | 163 | // This should not really happen |
michael@0 | 164 | request.cancel(); |
michael@0 | 165 | return true; |
michael@0 | 166 | } |
michael@0 | 167 | |
michael@0 | 168 | let appsService = Cc["@mozilla.org/AppsService;1"] |
michael@0 | 169 | .getService(Ci.nsIAppsService); |
michael@0 | 170 | let app = appsService.getAppByLocalId(request.principal.appId); |
michael@0 | 171 | |
michael@0 | 172 | // Check each permission if it's denied by permission manager with app's |
michael@0 | 173 | // URL. |
michael@0 | 174 | let notDenyAppPrincipal = function(type) { |
michael@0 | 175 | let url = Services.io.newURI(app.origin, null, null); |
michael@0 | 176 | let principal = secMan.getAppCodebasePrincipal(url, |
michael@0 | 177 | request.principal.appId, |
michael@0 | 178 | /*mozbrowser*/false); |
michael@0 | 179 | let result = Services.perms.testExactPermissionFromPrincipal(principal, |
michael@0 | 180 | type.access); |
michael@0 | 181 | |
michael@0 | 182 | if (result == Ci.nsIPermissionManager.ALLOW_ACTION || |
michael@0 | 183 | result == Ci.nsIPermissionManager.PROMPT_ACTION) { |
michael@0 | 184 | type.deny = false; |
michael@0 | 185 | } |
michael@0 | 186 | return !type.deny; |
michael@0 | 187 | } |
michael@0 | 188 | if (typesInfo.filter(notDenyAppPrincipal).length === 0) { |
michael@0 | 189 | request.cancel(); |
michael@0 | 190 | return true; |
michael@0 | 191 | } |
michael@0 | 192 | |
michael@0 | 193 | return false; |
michael@0 | 194 | }, |
michael@0 | 195 | |
michael@0 | 196 | handledByPermissionType: function handledByPermissionType(request, typesInfo) { |
michael@0 | 197 | for (let i in typesInfo) { |
michael@0 | 198 | if (permissionSpecificChecker.hasOwnProperty(typesInfo[i].permission) && |
michael@0 | 199 | permissionSpecificChecker[typesInfo[i].permission](request)) { |
michael@0 | 200 | return true; |
michael@0 | 201 | } |
michael@0 | 202 | } |
michael@0 | 203 | |
michael@0 | 204 | return false; |
michael@0 | 205 | }, |
michael@0 | 206 | |
michael@0 | 207 | _id: 0, |
michael@0 | 208 | prompt: function(request) { |
michael@0 | 209 | if (secMan.isSystemPrincipal(request.principal)) { |
michael@0 | 210 | request.allow(); |
michael@0 | 211 | return; |
michael@0 | 212 | } |
michael@0 | 213 | |
michael@0 | 214 | // Initialize the typesInfo and set the default value. |
michael@0 | 215 | let typesInfo = []; |
michael@0 | 216 | let perms = request.types.QueryInterface(Ci.nsIArray); |
michael@0 | 217 | for (let idx = 0; idx < perms.length; idx++) { |
michael@0 | 218 | let perm = perms.queryElementAt(idx, Ci.nsIContentPermissionType); |
michael@0 | 219 | let tmp = { |
michael@0 | 220 | permission: perm.type, |
michael@0 | 221 | access: (perm.access && perm.access !== "unused") ? |
michael@0 | 222 | perm.type + "-" + perm.access : perm.type, |
michael@0 | 223 | options: [], |
michael@0 | 224 | deny: true, |
michael@0 | 225 | action: Ci.nsIPermissionManager.UNKNOWN_ACTION |
michael@0 | 226 | }; |
michael@0 | 227 | |
michael@0 | 228 | // Append available options, if any. |
michael@0 | 229 | let options = perm.options.QueryInterface(Ci.nsIArray); |
michael@0 | 230 | for (let i = 0; i < options.length; i++) { |
michael@0 | 231 | let option = options.queryElementAt(i, Ci.nsISupportsString).data; |
michael@0 | 232 | tmp.options.push(option); |
michael@0 | 233 | } |
michael@0 | 234 | typesInfo.push(tmp); |
michael@0 | 235 | } |
michael@0 | 236 | |
michael@0 | 237 | if (typesInfo.length == 0) { |
michael@0 | 238 | request.cancel(); |
michael@0 | 239 | return; |
michael@0 | 240 | } |
michael@0 | 241 | |
michael@0 | 242 | if(!this.checkMultipleRequest(typesInfo)) { |
michael@0 | 243 | request.cancel(); |
michael@0 | 244 | return; |
michael@0 | 245 | } |
michael@0 | 246 | |
michael@0 | 247 | if (this.handledByApp(request, typesInfo) || |
michael@0 | 248 | this.handledByPermissionType(request, typesInfo)) { |
michael@0 | 249 | return; |
michael@0 | 250 | } |
michael@0 | 251 | |
michael@0 | 252 | // returns true if the request was handled |
michael@0 | 253 | if (this.handleExistingPermission(request, typesInfo)) { |
michael@0 | 254 | return; |
michael@0 | 255 | } |
michael@0 | 256 | |
michael@0 | 257 | // prompt PROMPT_ACTION request only. |
michael@0 | 258 | typesInfo.forEach(function(aType, aIndex) { |
michael@0 | 259 | if (aType.action != Ci.nsIPermissionManager.PROMPT_ACTION || aType.deny) { |
michael@0 | 260 | typesInfo.splice(aIndex); |
michael@0 | 261 | } |
michael@0 | 262 | }); |
michael@0 | 263 | |
michael@0 | 264 | let frame = request.element; |
michael@0 | 265 | let requestId = this._id++; |
michael@0 | 266 | |
michael@0 | 267 | if (!frame) { |
michael@0 | 268 | this.delegatePrompt(request, requestId, typesInfo); |
michael@0 | 269 | return; |
michael@0 | 270 | } |
michael@0 | 271 | |
michael@0 | 272 | frame = frame.wrappedJSObject; |
michael@0 | 273 | var cancelRequest = function() { |
michael@0 | 274 | frame.removeEventListener("mozbrowservisibilitychange", onVisibilityChange); |
michael@0 | 275 | request.cancel(); |
michael@0 | 276 | } |
michael@0 | 277 | |
michael@0 | 278 | var self = this; |
michael@0 | 279 | var onVisibilityChange = function(evt) { |
michael@0 | 280 | if (evt.detail.visible === true) |
michael@0 | 281 | return; |
michael@0 | 282 | |
michael@0 | 283 | self.cancelPrompt(request, requestId, typesInfo); |
michael@0 | 284 | cancelRequest(); |
michael@0 | 285 | } |
michael@0 | 286 | |
michael@0 | 287 | // If the request was initiated from a hidden iframe |
michael@0 | 288 | // we don't forward it to content and cancel it right away |
michael@0 | 289 | let domRequest = frame.getVisible(); |
michael@0 | 290 | domRequest.onsuccess = function gv_success(evt) { |
michael@0 | 291 | if (!evt.target.result) { |
michael@0 | 292 | cancelRequest(); |
michael@0 | 293 | return; |
michael@0 | 294 | } |
michael@0 | 295 | |
michael@0 | 296 | // Monitor the frame visibility and cancel the request if the frame goes |
michael@0 | 297 | // away but the request is still here. |
michael@0 | 298 | frame.addEventListener("mozbrowservisibilitychange", onVisibilityChange); |
michael@0 | 299 | |
michael@0 | 300 | self.delegatePrompt(request, requestId, typesInfo, function onCallback() { |
michael@0 | 301 | frame.removeEventListener("mozbrowservisibilitychange", onVisibilityChange); |
michael@0 | 302 | }); |
michael@0 | 303 | }; |
michael@0 | 304 | |
michael@0 | 305 | // Something went wrong. Let's cancel the request just in case. |
michael@0 | 306 | domRequest.onerror = function gv_error() { |
michael@0 | 307 | cancelRequest(); |
michael@0 | 308 | } |
michael@0 | 309 | }, |
michael@0 | 310 | |
michael@0 | 311 | cancelPrompt: function(request, requestId, typesInfo) { |
michael@0 | 312 | this.sendToBrowserWindow("cancel-permission-prompt", request, requestId, |
michael@0 | 313 | typesInfo); |
michael@0 | 314 | }, |
michael@0 | 315 | |
michael@0 | 316 | delegatePrompt: function(request, requestId, typesInfo, callback) { |
michael@0 | 317 | |
michael@0 | 318 | this.sendToBrowserWindow("permission-prompt", request, requestId, typesInfo, |
michael@0 | 319 | function(type, remember, choices) { |
michael@0 | 320 | if (type == "permission-allow") { |
michael@0 | 321 | rememberPermission(typesInfo, request.principal, !remember); |
michael@0 | 322 | if (callback) { |
michael@0 | 323 | callback(); |
michael@0 | 324 | } |
michael@0 | 325 | request.allow(choices); |
michael@0 | 326 | return; |
michael@0 | 327 | } |
michael@0 | 328 | |
michael@0 | 329 | let addDenyPermission = function(type) { |
michael@0 | 330 | debug("add " + type.permission + |
michael@0 | 331 | " to permission manager with DENY_ACTION"); |
michael@0 | 332 | if (remember) { |
michael@0 | 333 | Services.perms.addFromPrincipal(request.principal, type.access, |
michael@0 | 334 | Ci.nsIPermissionManager.DENY_ACTION); |
michael@0 | 335 | } else if (PERMISSION_NO_SESSION.indexOf(type.access) < 0) { |
michael@0 | 336 | Services.perms.addFromPrincipal(request.principal, type.access, |
michael@0 | 337 | Ci.nsIPermissionManager.DENY_ACTION, |
michael@0 | 338 | Ci.nsIPermissionManager.EXPIRE_SESSION, |
michael@0 | 339 | 0); |
michael@0 | 340 | } |
michael@0 | 341 | } |
michael@0 | 342 | typesInfo.forEach(addDenyPermission); |
michael@0 | 343 | |
michael@0 | 344 | if (callback) { |
michael@0 | 345 | callback(); |
michael@0 | 346 | } |
michael@0 | 347 | request.cancel(); |
michael@0 | 348 | }); |
michael@0 | 349 | }, |
michael@0 | 350 | |
michael@0 | 351 | sendToBrowserWindow: function(type, request, requestId, typesInfo, callback) { |
michael@0 | 352 | if (callback) { |
michael@0 | 353 | SystemAppProxy.addEventListener("mozContentEvent", function contentEvent(evt) { |
michael@0 | 354 | let detail = evt.detail; |
michael@0 | 355 | if (detail.id != requestId) |
michael@0 | 356 | return; |
michael@0 | 357 | SystemAppProxy.removeEventListener("mozContentEvent", contentEvent); |
michael@0 | 358 | |
michael@0 | 359 | callback(detail.type, detail.remember, detail.choices); |
michael@0 | 360 | }) |
michael@0 | 361 | } |
michael@0 | 362 | |
michael@0 | 363 | let principal = request.principal; |
michael@0 | 364 | let isApp = principal.appStatus != Ci.nsIPrincipal.APP_STATUS_NOT_INSTALLED; |
michael@0 | 365 | let remember = (principal.appStatus == Ci.nsIPrincipal.APP_STATUS_PRIVILEGED || |
michael@0 | 366 | principal.appStatus == Ci.nsIPrincipal.APP_STATUS_CERTIFIED) |
michael@0 | 367 | ? true |
michael@0 | 368 | : request.remember; |
michael@0 | 369 | let permissions = {}; |
michael@0 | 370 | for (let i in typesInfo) { |
michael@0 | 371 | debug("prompt " + typesInfo[i].permission); |
michael@0 | 372 | permissions[typesInfo[i].permission] = typesInfo[i].options; |
michael@0 | 373 | } |
michael@0 | 374 | |
michael@0 | 375 | let details = { |
michael@0 | 376 | type: type, |
michael@0 | 377 | permissions: permissions, |
michael@0 | 378 | id: requestId, |
michael@0 | 379 | origin: principal.origin, |
michael@0 | 380 | isApp: isApp, |
michael@0 | 381 | remember: remember |
michael@0 | 382 | }; |
michael@0 | 383 | |
michael@0 | 384 | if (isApp) { |
michael@0 | 385 | details.manifestURL = DOMApplicationRegistry.getManifestURLByLocalId(principal.appId); |
michael@0 | 386 | } |
michael@0 | 387 | SystemAppProxy.dispatchEvent(details); |
michael@0 | 388 | }, |
michael@0 | 389 | |
michael@0 | 390 | classID: Components.ID("{8c719f03-afe0-4aac-91ff-6c215895d467}"), |
michael@0 | 391 | |
michael@0 | 392 | QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPermissionPrompt]) |
michael@0 | 393 | }; |
michael@0 | 394 | |
michael@0 | 395 | (function() { |
michael@0 | 396 | // Do not allow GetUserMedia while in call. |
michael@0 | 397 | permissionSpecificChecker["audio-capture"] = function(request) { |
michael@0 | 398 | if (AudioManager.phoneState === Ci.nsIAudioManager.PHONE_STATE_IN_CALL) { |
michael@0 | 399 | request.cancel(); |
michael@0 | 400 | return true; |
michael@0 | 401 | } else { |
michael@0 | 402 | return false; |
michael@0 | 403 | } |
michael@0 | 404 | }; |
michael@0 | 405 | })(); |
michael@0 | 406 | |
michael@0 | 407 | //module initialization |
michael@0 | 408 | this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ContentPermissionPrompt]); |