b2g/components/ContentPermissionPrompt.js

changeset 0
6474c204b198
equal deleted inserted replaced
-1:000000000000 0:4057accb2f04
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/. */
4
5 "use strict"
6
7 function debug(str) {
8 //dump("-*- ContentPermissionPrompt: " + str + "\n");
9 }
10
11 const Ci = Components.interfaces;
12 const Cr = Components.results;
13 const Cu = Components.utils;
14 const Cc = Components.classes;
15
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"];
24
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");
31
32 var permissionManager = Cc["@mozilla.org/permissionmanager;1"].getService(Ci.nsIPermissionManager);
33 var secMan = Cc["@mozilla.org/scriptsecuritymanager;1"].getService(Ci.nsIScriptSecurityManager);
34
35 let permissionSpecificChecker = {};
36
37 XPCOMUtils.defineLazyServiceGetter(this,
38 "AudioManager",
39 "@mozilla.org/telephony/audiomanager;1",
40 "nsIAudioManager");
41
42 XPCOMUtils.defineLazyModuleGetter(this, "SystemAppProxy",
43 "resource://gre/modules/SystemAppProxy.jsm");
44
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 }
81
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 }
96
97 function ContentPermissionPrompt() {}
98
99 ContentPermissionPrompt.prototype = {
100
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 });
112
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 }
125
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 },
142
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 }
156
157 return false;
158 },
159
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 }
167
168 let appsService = Cc["@mozilla.org/AppsService;1"]
169 .getService(Ci.nsIAppsService);
170 let app = appsService.getAppByLocalId(request.principal.appId);
171
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);
181
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 }
192
193 return false;
194 },
195
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 }
203
204 return false;
205 },
206
207 _id: 0,
208 prompt: function(request) {
209 if (secMan.isSystemPrincipal(request.principal)) {
210 request.allow();
211 return;
212 }
213
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 };
227
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 }
236
237 if (typesInfo.length == 0) {
238 request.cancel();
239 return;
240 }
241
242 if(!this.checkMultipleRequest(typesInfo)) {
243 request.cancel();
244 return;
245 }
246
247 if (this.handledByApp(request, typesInfo) ||
248 this.handledByPermissionType(request, typesInfo)) {
249 return;
250 }
251
252 // returns true if the request was handled
253 if (this.handleExistingPermission(request, typesInfo)) {
254 return;
255 }
256
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 });
263
264 let frame = request.element;
265 let requestId = this._id++;
266
267 if (!frame) {
268 this.delegatePrompt(request, requestId, typesInfo);
269 return;
270 }
271
272 frame = frame.wrappedJSObject;
273 var cancelRequest = function() {
274 frame.removeEventListener("mozbrowservisibilitychange", onVisibilityChange);
275 request.cancel();
276 }
277
278 var self = this;
279 var onVisibilityChange = function(evt) {
280 if (evt.detail.visible === true)
281 return;
282
283 self.cancelPrompt(request, requestId, typesInfo);
284 cancelRequest();
285 }
286
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 }
295
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);
299
300 self.delegatePrompt(request, requestId, typesInfo, function onCallback() {
301 frame.removeEventListener("mozbrowservisibilitychange", onVisibilityChange);
302 });
303 };
304
305 // Something went wrong. Let's cancel the request just in case.
306 domRequest.onerror = function gv_error() {
307 cancelRequest();
308 }
309 },
310
311 cancelPrompt: function(request, requestId, typesInfo) {
312 this.sendToBrowserWindow("cancel-permission-prompt", request, requestId,
313 typesInfo);
314 },
315
316 delegatePrompt: function(request, requestId, typesInfo, callback) {
317
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 }
328
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);
343
344 if (callback) {
345 callback();
346 }
347 request.cancel();
348 });
349 },
350
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);
358
359 callback(detail.type, detail.remember, detail.choices);
360 })
361 }
362
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 }
374
375 let details = {
376 type: type,
377 permissions: permissions,
378 id: requestId,
379 origin: principal.origin,
380 isApp: isApp,
381 remember: remember
382 };
383
384 if (isApp) {
385 details.manifestURL = DOMApplicationRegistry.getManifestURLByLocalId(principal.appId);
386 }
387 SystemAppProxy.dispatchEvent(details);
388 },
389
390 classID: Components.ID("{8c719f03-afe0-4aac-91ff-6c215895d467}"),
391
392 QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPermissionPrompt])
393 };
394
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 })();
406
407 //module initialization
408 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ContentPermissionPrompt]);

mercurial