|
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]); |