mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-30 00:01:50 +00:00
486 lines
16 KiB
JavaScript
486 lines
16 KiB
JavaScript
/* 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/Webapps.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);
|
|
|
|
let permissionSpecificChecker = {};
|
|
|
|
XPCOMUtils.defineLazyServiceGetter(this,
|
|
"TelephonyService",
|
|
"@mozilla.org/telephony/telephonyservice;1",
|
|
"nsITelephonyService");
|
|
|
|
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.getAppCodebasePrincipal(url,
|
|
request.principal.appId,
|
|
/*mozbrowser*/false);
|
|
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) ;
|
|
});
|
|
|
|
let frame = request.element;
|
|
|
|
if (!frame) {
|
|
this.delegatePrompt(request, typesInfo);
|
|
return;
|
|
}
|
|
|
|
frame = frame.wrappedJSObject;
|
|
var cancelRequest = function() {
|
|
frame.removeEventListener("mozbrowservisibilitychange", onVisibilityChange);
|
|
request.cancel();
|
|
}
|
|
|
|
var self = this;
|
|
var onVisibilityChange = function(evt) {
|
|
if (evt.detail.visible === true)
|
|
return;
|
|
|
|
self.cancelPrompt(request, typesInfo);
|
|
cancelRequest();
|
|
}
|
|
|
|
// If the request was initiated from a hidden iframe
|
|
// we don't forward it to content and cancel it right away
|
|
let domRequest = frame.getVisible();
|
|
domRequest.onsuccess = function gv_success(evt) {
|
|
if (!evt.target.result) {
|
|
cancelRequest();
|
|
return;
|
|
}
|
|
|
|
// Monitor the frame visibility and cancel the request if the frame goes
|
|
// away but the request is still here.
|
|
frame.addEventListener("mozbrowservisibilitychange", onVisibilityChange);
|
|
|
|
self.delegatePrompt(request, typesInfo, function onCallback() {
|
|
frame.removeEventListener("mozbrowservisibilitychange", onVisibilityChange);
|
|
});
|
|
};
|
|
|
|
// Something went wrong. Let's cancel the request just in case.
|
|
domRequest.onerror = function gv_error() {
|
|
cancelRequest();
|
|
}
|
|
},
|
|
|
|
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,
|
|
origin: principal.origin,
|
|
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;
|
|
|
|
try {
|
|
// nsITelephonyService.enumerateCalls is synchronous.
|
|
TelephonyService.enumerateCalls({
|
|
QueryInterface: XPCOMUtils.generateQI([Ci.nsITelephonyListener]),
|
|
enumerateCallStateComplete: function() {},
|
|
enumerateCallState: function(callInfo) {
|
|
if (callInfo.callState == Ci.nsITelephonyService.CALL_STATE_CONNECTED) {
|
|
forbid = true;
|
|
}
|
|
},
|
|
});
|
|
} catch (e) {
|
|
// No restriction if Telephony service doesn't exist.
|
|
return false;
|
|
}
|
|
|
|
if (forbid) {
|
|
request.cancel();
|
|
}
|
|
|
|
return forbid;
|
|
};
|
|
})();
|
|
|
|
//module initialization
|
|
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ContentPermissionPrompt]);
|