b2g/components/ContentPermissionPrompt.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

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

mercurial