Bug 1857998 [wpt PR 42432] - [css-nesting-ident] Enable relaxed syntax, a=testonly
[gecko.git] / browser / actors / WebRTCParent.sys.mjs
blobf3e956ff52cffe57c995665f7e0ce5300e40a85e
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 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
7 const lazy = {};
9 ChromeUtils.defineESModuleGetters(lazy, {
10   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
11   SitePermissions: "resource:///modules/SitePermissions.sys.mjs",
12   webrtcUI: "resource:///modules/webrtcUI.sys.mjs",
13 });
15 XPCOMUtils.defineLazyServiceGetter(
16   lazy,
17   "OSPermissions",
18   "@mozilla.org/ospermissionrequest;1",
19   "nsIOSPermissionRequest"
22 export class WebRTCParent extends JSWindowActorParent {
23   didDestroy() {
24     // Media stream tracks end on unload, so call stopRecording() on them early
25     // *before* we go away, to ensure we're working with the right principal.
26     this.stopRecording(this.manager.outerWindowId);
27     lazy.webrtcUI.forgetStreamsFromBrowserContext(this.browsingContext);
28     // Must clear activePerms here to prevent them from being read by laggard
29     // stopRecording() calls, which due to IPC, may come in *after* navigation.
30     // This is to prevent granting temporary grace periods to the wrong page.
31     lazy.webrtcUI.activePerms.delete(this.manager.outerWindowId);
32   }
34   getBrowser() {
35     return this.browsingContext.top.embedderElement;
36   }
38   receiveMessage(aMessage) {
39     switch (aMessage.name) {
40       case "rtcpeer:Request": {
41         let params = Object.freeze(
42           Object.assign(
43             {
44               origin: this.manager.documentPrincipal.origin,
45             },
46             aMessage.data
47           )
48         );
50         let blockers = Array.from(lazy.webrtcUI.peerConnectionBlockers);
52         (async function () {
53           for (let blocker of blockers) {
54             try {
55               let result = await blocker(params);
56               if (result == "deny") {
57                 return false;
58               }
59             } catch (err) {
60               console.error(`error in PeerConnection blocker: ${err.message}`);
61             }
62           }
63           return true;
64         })().then(decision => {
65           let message;
66           if (decision) {
67             lazy.webrtcUI.emitter.emit("peer-request-allowed", params);
68             message = "rtcpeer:Allow";
69           } else {
70             lazy.webrtcUI.emitter.emit("peer-request-blocked", params);
71             message = "rtcpeer:Deny";
72           }
74           this.sendAsyncMessage(message, {
75             callID: params.callID,
76             windowID: params.windowID,
77           });
78         });
79         break;
80       }
81       case "rtcpeer:CancelRequest": {
82         let params = Object.freeze({
83           origin: this.manager.documentPrincipal.origin,
84           callID: aMessage.data,
85         });
86         lazy.webrtcUI.emitter.emit("peer-request-cancel", params);
87         break;
88       }
89       case "webrtc:Request": {
90         let data = aMessage.data;
92         // Record third party origins for telemetry.
93         let isThirdPartyOrigin =
94           this.manager.documentPrincipal.origin !=
95           this.manager.topWindowContext.documentPrincipal.origin;
96         data.isThirdPartyOrigin = isThirdPartyOrigin;
98         data.origin = this.manager.topWindowContext.documentPrincipal.origin;
100         let browser = this.getBrowser();
101         if (browser.fxrPermissionPrompt) {
102           // For Firefox Reality on Desktop, switch to a different mechanism to
103           // prompt the user since fewer permissions are available and since many
104           // UI dependencies are not available.
105           browser.fxrPermissionPrompt(data);
106         } else {
107           prompt(this, this.getBrowser(), data);
108         }
109         break;
110       }
111       case "webrtc:StopRecording":
112         this.stopRecording(
113           aMessage.data.windowID,
114           aMessage.data.mediaSource,
115           aMessage.data.rawID
116         );
117         break;
118       case "webrtc:CancelRequest": {
119         let browser = this.getBrowser();
120         // browser can be null when closing the window
121         if (browser) {
122           removePrompt(browser, aMessage.data);
123         }
124         break;
125       }
126       case "webrtc:UpdateIndicators": {
127         let { data } = aMessage;
128         data.documentURI = this.manager.documentURI?.spec;
129         if (data.windowId) {
130           if (!data.remove) {
131             data.principal = this.manager.topWindowContext.documentPrincipal;
132           }
133           lazy.webrtcUI.streamAddedOrRemoved(this.browsingContext, data);
134         }
135         this.updateIndicators(data);
136         break;
137       }
138     }
139   }
141   updateIndicators(aData) {
142     let browsingContext = this.browsingContext;
143     let state = lazy.webrtcUI.updateIndicators(browsingContext.top);
145     let browser = this.getBrowser();
146     if (!browser) {
147       return;
148     }
150     state.browsingContext = browsingContext;
151     state.windowId = aData.windowId;
153     let tabbrowser = browser.ownerGlobal.gBrowser;
154     if (tabbrowser) {
155       tabbrowser.updateBrowserSharing(browser, {
156         webRTC: state,
157       });
158     }
159   }
161   denyRequest(aRequest) {
162     this.sendAsyncMessage("webrtc:Deny", {
163       callID: aRequest.callID,
164       windowID: aRequest.windowID,
165     });
166   }
168   //
169   // Deny the request because the browser does not have access to the
170   // camera or microphone due to OS security restrictions. The user may
171   // have granted camera/microphone access to the site, but not have
172   // allowed the browser access in OS settings.
173   //
174   denyRequestNoPermission(aRequest) {
175     this.sendAsyncMessage("webrtc:Deny", {
176       callID: aRequest.callID,
177       windowID: aRequest.windowID,
178       noOSPermission: true,
179     });
180   }
182   //
183   // Check if we have permission to access the camera or screen-sharing and/or
184   // microphone at the OS level. Triggers a request to access the device if access
185   // is needed and the permission state has not yet been determined.
186   //
187   async checkOSPermission(camNeeded, micNeeded, scrNeeded) {
188     // Don't trigger OS permission requests for fake devices. Fake devices don't
189     // require OS permission and the dialogs are problematic in automated testing
190     // (where fake devices are used) because they require user interaction.
191     if (
192       !scrNeeded &&
193       Services.prefs.getBoolPref("media.navigator.streams.fake", false)
194     ) {
195       return true;
196     }
197     let camStatus = {},
198       micStatus = {};
199     if (camNeeded || micNeeded) {
200       lazy.OSPermissions.getMediaCapturePermissionState(camStatus, micStatus);
201     }
202     if (camNeeded) {
203       let camPermission = camStatus.value;
204       let camAccessible = await this.checkAndGetOSPermission(
205         camPermission,
206         lazy.OSPermissions.requestVideoCapturePermission
207       );
208       if (!camAccessible) {
209         return false;
210       }
211     }
212     if (micNeeded) {
213       let micPermission = micStatus.value;
214       let micAccessible = await this.checkAndGetOSPermission(
215         micPermission,
216         lazy.OSPermissions.requestAudioCapturePermission
217       );
218       if (!micAccessible) {
219         return false;
220       }
221     }
222     let scrStatus = {};
223     if (scrNeeded) {
224       lazy.OSPermissions.getScreenCapturePermissionState(scrStatus);
225       if (scrStatus.value == lazy.OSPermissions.PERMISSION_STATE_DENIED) {
226         lazy.OSPermissions.maybeRequestScreenCapturePermission();
227         return false;
228       }
229     }
230     return true;
231   }
233   //
234   // Given a device's permission, return true if the device is accessible. If
235   // the device's permission is not yet determined, request access to the device.
236   // |requestPermissionFunc| must return a promise that resolves with true
237   // if the device is accessible and false otherwise.
238   //
239   async checkAndGetOSPermission(devicePermission, requestPermissionFunc) {
240     if (
241       devicePermission == lazy.OSPermissions.PERMISSION_STATE_DENIED ||
242       devicePermission == lazy.OSPermissions.PERMISSION_STATE_RESTRICTED
243     ) {
244       return false;
245     }
246     if (devicePermission == lazy.OSPermissions.PERMISSION_STATE_NOTDETERMINED) {
247       let deviceAllowed = await requestPermissionFunc();
248       if (!deviceAllowed) {
249         return false;
250       }
251     }
252     return true;
253   }
255   stopRecording(aOuterWindowId, aMediaSource, aRawId) {
256     for (let { browsingContext, state } of lazy.webrtcUI._streams) {
257       if (browsingContext == this.browsingContext) {
258         let { principal } = state;
259         for (let { mediaSource, rawId } of state.devices) {
260           if (aRawId && (aRawId != rawId || aMediaSource != mediaSource)) {
261             continue;
262           }
263           // Deactivate this device (no aRawId means all devices).
264           this.deactivateDevicePerm(
265             aOuterWindowId,
266             mediaSource,
267             rawId,
268             principal
269           );
270         }
271       }
272     }
273   }
275   /**
276    * Add a device record to webrtcUI.activePerms, denoting a device as in use.
277    * Important to call for permission grace periods to work correctly.
278    */
279   activateDevicePerm(aOuterWindowId, aMediaSource, aId) {
280     if (!lazy.webrtcUI.activePerms.has(this.manager.outerWindowId)) {
281       lazy.webrtcUI.activePerms.set(this.manager.outerWindowId, new Map());
282     }
283     lazy.webrtcUI.activePerms
284       .get(this.manager.outerWindowId)
285       .set(aOuterWindowId + aMediaSource + aId, aMediaSource);
286   }
288   /**
289    * Remove a device record from webrtcUI.activePerms, denoting a device as
290    * no longer in use by the site. Meaning: gUM requests for this device will
291    * no longer be implicitly granted through the webrtcUI.activePerms mechanism.
292    *
293    * However, if webrtcUI.deviceGracePeriodTimeoutMs is defined, the implicit
294    * grant is extended for an additional period of time through SitePermissions.
295    */
296   deactivateDevicePerm(
297     aOuterWindowId,
298     aMediaSource,
299     aId,
300     aPermissionPrincipal
301   ) {
302     // If we don't have active permissions for the given window anymore don't
303     // set a grace period. This happens if there has been a user revoke and
304     // webrtcUI clears the permissions.
305     if (!lazy.webrtcUI.activePerms.has(this.manager.outerWindowId)) {
306       return;
307     }
308     let map = lazy.webrtcUI.activePerms.get(this.manager.outerWindowId);
309     map.delete(aOuterWindowId + aMediaSource + aId);
311     // Add a permission grace period for camera and microphone only
312     if (
313       (aMediaSource != "camera" && aMediaSource != "microphone") ||
314       !this.browsingContext.top.embedderElement
315     ) {
316       return;
317     }
318     let gracePeriodMs = lazy.webrtcUI.deviceGracePeriodTimeoutMs;
319     if (gracePeriodMs > 0) {
320       // A grace period is extended (even past navigation) to this outer window
321       // + origin + deviceId only. This avoids re-prompting without the user
322       // having to persist permission to the site, in a common case of a web
323       // conference asking them for the camera in a lobby page, before
324       // navigating to the actual meeting room page. Does not survive tab close.
325       //
326       // Caution: since navigation causes deactivation, we may be in the middle
327       // of one. We must pass in a principal & URI for SitePermissions to use
328       // instead of browser.currentURI, because the latter may point to a new
329       // page already, and we must not leak permission to unrelated pages.
330       //
331       let permissionName = [aMediaSource, aId].join("^");
332       lazy.SitePermissions.setForPrincipal(
333         aPermissionPrincipal,
334         permissionName,
335         lazy.SitePermissions.ALLOW,
336         lazy.SitePermissions.SCOPE_TEMPORARY,
337         this.browsingContext.top.embedderElement,
338         gracePeriodMs
339       );
340     }
341   }
343   /**
344    * Checks if the principal has sufficient permissions
345    * to fulfill the given request. If the request can be
346    * fulfilled, a message is sent to the child
347    * signaling that WebRTC permissions were given and
348    * this function will return true.
349    */
350   checkRequestAllowed(aRequest, aPrincipal) {
351     if (!aRequest.secure) {
352       return false;
353     }
354     // Always prompt for screen sharing
355     if (aRequest.sharingScreen) {
356       return false;
357     }
358     let {
359       callID,
360       windowID,
361       audioInputDevices,
362       videoInputDevices,
363       audioOutputDevices,
364       hasInherentAudioConstraints,
365       hasInherentVideoConstraints,
366       audioOutputId,
367     } = aRequest;
369     if (audioOutputDevices?.length) {
370       // Prompt if a specific device is not requested, available and allowed.
371       let device = audioOutputDevices.find(({ id }) => id == audioOutputId);
372       if (
373         !device ||
374         !lazy.SitePermissions.getForPrincipal(
375           aPrincipal,
376           ["speaker", device.id].join("^"),
377           this.getBrowser()
378         ).state == lazy.SitePermissions.ALLOW
379       ) {
380         return false;
381       }
382       this.sendAsyncMessage("webrtc:Allow", {
383         callID,
384         windowID,
385         devices: [device.deviceIndex],
386       });
387       return true;
388     }
390     let { perms } = Services;
391     if (
392       perms.testExactPermissionFromPrincipal(aPrincipal, "MediaManagerVideo")
393     ) {
394       perms.removeFromPrincipal(aPrincipal, "MediaManagerVideo");
395     }
397     // Don't use persistent permissions from the top-level principal
398     // if we're handling a potentially insecure third party
399     // through a wildcard ("*") allow attribute.
400     let limited = aRequest.secondOrigin;
402     let map = lazy.webrtcUI.activePerms.get(this.manager.outerWindowId);
403     // We consider a camera or mic active if it is active or was active within a
404     // grace period of milliseconds ago.
405     const isAllowed = ({ mediaSource, rawId }, permissionID) =>
406       map?.get(windowID + mediaSource + rawId) ||
407       (!limited &&
408         (lazy.SitePermissions.getForPrincipal(aPrincipal, permissionID).state ==
409           lazy.SitePermissions.ALLOW ||
410           lazy.SitePermissions.getForPrincipal(
411             aPrincipal,
412             [mediaSource, rawId].join("^"),
413             this.getBrowser()
414           ).state == lazy.SitePermissions.ALLOW));
416     let microphone;
417     if (audioInputDevices.length) {
418       for (let device of audioInputDevices) {
419         if (isAllowed(device, "microphone")) {
420           microphone = device;
421           break;
422         }
423         if (hasInherentAudioConstraints) {
424           // Inherent constraints suggest site is looking for a specific mic
425           break;
426         }
427         // Some sites don't look too hard at what they get, and spam gUM without
428         // adjusting what they ask for to match what they got last time. To keep
429         // users in charge and reduce prompts, ignore other constraints by
430         // returning the most-fit microphone a site already has access to.
431       }
432       if (!microphone) {
433         return false;
434       }
435     }
436     let camera;
437     if (videoInputDevices.length) {
438       for (let device of videoInputDevices) {
439         if (isAllowed(device, "camera")) {
440           camera = device;
441           break;
442         }
443         if (hasInherentVideoConstraints) {
444           // Inherent constraints suggest site is looking for a specific camera
445           break;
446         }
447         // Some sites don't look too hard at what they get, and spam gUM without
448         // adjusting what they ask for to match what they got last time. To keep
449         // users in charge and reduce prompts, ignore other constraints by
450         // returning the most-fit camera a site already has access to.
451       }
452       if (!camera) {
453         return false;
454       }
455     }
456     let devices = [];
457     if (camera) {
458       perms.addFromPrincipal(
459         aPrincipal,
460         "MediaManagerVideo",
461         perms.ALLOW_ACTION,
462         perms.EXPIRE_SESSION
463       );
464       devices.push(camera.deviceIndex);
465       this.activateDevicePerm(windowID, camera.mediaSource, camera.rawId);
466     }
467     if (microphone) {
468       devices.push(microphone.deviceIndex);
469       this.activateDevicePerm(
470         windowID,
471         microphone.mediaSource,
472         microphone.rawId
473       );
474     }
475     this.checkOSPermission(!!camera, !!microphone, false).then(
476       havePermission => {
477         if (havePermission) {
478           this.sendAsyncMessage("webrtc:Allow", { callID, windowID, devices });
479         } else {
480           this.denyRequestNoPermission(aRequest);
481         }
482       }
483     );
484     return true;
485   }
488 function prompt(aActor, aBrowser, aRequest) {
489   let {
490     audioInputDevices,
491     videoInputDevices,
492     audioOutputDevices,
493     sharingScreen,
494     sharingAudio,
495     requestTypes,
496   } = aRequest;
498   let principal =
499     Services.scriptSecurityManager.createContentPrincipalFromOrigin(
500       aRequest.origin
501     );
503   // For add-on principals, we immediately check for permission instead
504   // of waiting for the notification to focus. This allows for supporting
505   // cases such as browserAction popups where no prompt is shown.
506   if (principal.addonPolicy) {
507     let isPopup = false;
508     let isBackground = false;
510     for (let view of principal.addonPolicy.extension.views) {
511       if (view.viewType == "popup" && view.xulBrowser == aBrowser) {
512         isPopup = true;
513       }
514       if (view.viewType == "background" && view.xulBrowser == aBrowser) {
515         isBackground = true;
516       }
517     }
519     // Recording from background pages is considered too sensitive and will
520     // always be denied.
521     if (isBackground) {
522       aActor.denyRequest(aRequest);
523       return;
524     }
526     // If the request comes from a popup, we don't want to show the prompt,
527     // but we do want to allow the request if the user previously gave permission.
528     if (isPopup) {
529       if (!aActor.checkRequestAllowed(aRequest, principal, aBrowser)) {
530         aActor.denyRequest(aRequest);
531       }
532       return;
533     }
534   }
536   // If the user has already denied access once in this tab,
537   // deny again without even showing the notification icon.
538   for (const type of requestTypes) {
539     const permissionID =
540       type == "AudioCapture" ? "microphone" : type.toLowerCase();
541     if (
542       lazy.SitePermissions.getForPrincipal(principal, permissionID, aBrowser)
543         .state == lazy.SitePermissions.BLOCK
544     ) {
545       aActor.denyRequest(aRequest);
546       return;
547     }
548   }
550   let chromeDoc = aBrowser.ownerDocument;
551   const localization = new Localization(
552     ["browser/webrtcIndicator.ftl", "branding/brand.ftl"],
553     true
554   );
556   /** @type {"Screen" | "Camera" | null} */
557   let reqVideoInput = null;
558   if (videoInputDevices.length) {
559     reqVideoInput = sharingScreen ? "Screen" : "Camera";
560   }
561   /** @type {"AudioCapture" | "Microphone" | null} */
562   let reqAudioInput = null;
563   if (audioInputDevices.length) {
564     reqAudioInput = sharingAudio ? "AudioCapture" : "Microphone";
565   }
566   const reqAudioOutput = !!audioOutputDevices.length;
568   const stringId = getPromptMessageId(
569     reqVideoInput,
570     reqAudioInput,
571     reqAudioOutput,
572     !!aRequest.secondOrigin
573   );
574   const message = localization.formatValueSync(stringId, {
575     origin: "<>",
576     thirdParty: "{}",
577   });
579   let notification; // Used by action callbacks.
580   const actionL10nIds = [{ id: "webrtc-action-allow" }];
582   let notificationSilencingEnabled = Services.prefs.getBoolPref(
583     "privacy.webrtc.allowSilencingNotifications"
584   );
586   const isNotNowLabelEnabled =
587     reqAudioOutput || allowedOrActiveCameraOrMicrophone(aBrowser);
588   let secondaryActions = [];
589   if (reqAudioOutput || (notificationSilencingEnabled && sharingScreen)) {
590     // We want to free up the checkbox at the bottom of the permission
591     // panel for the notification silencing option, so we use a
592     // different configuration for the permissions panel when
593     // notification silencing is enabled.
595     let permissionName = reqAudioOutput ? "speaker" : "screen";
596     // When selecting speakers, we always offer 'Not now' instead of 'Block'.
597     // When selecting screens, we offer 'Not now' if and only if we have a
598     // (temporary) allow permission for some mic/cam device.
599     const id = isNotNowLabelEnabled
600       ? "webrtc-action-not-now"
601       : "webrtc-action-block";
602     actionL10nIds.push({ id }, { id: "webrtc-action-always-block" });
603     secondaryActions = [
604       {
605         callback(aState) {
606           aActor.denyRequest(aRequest);
607           if (!isNotNowLabelEnabled) {
608             lazy.SitePermissions.setForPrincipal(
609               principal,
610               permissionName,
611               lazy.SitePermissions.BLOCK,
612               lazy.SitePermissions.SCOPE_TEMPORARY,
613               notification.browser
614             );
615           }
616         },
617       },
618       {
619         callback(aState) {
620           aActor.denyRequest(aRequest);
621           lazy.SitePermissions.setForPrincipal(
622             principal,
623             permissionName,
624             lazy.SitePermissions.BLOCK,
625             lazy.SitePermissions.SCOPE_PERSISTENT,
626             notification.browser
627           );
628         },
629       },
630     ];
631   } else {
632     // We have a (temporary) allow permission for some device
633     // hence we offer a 'Not now' label instead of 'Block'.
634     const id = isNotNowLabelEnabled
635       ? "webrtc-action-not-now"
636       : "webrtc-action-block";
637     actionL10nIds.push({ id });
638     secondaryActions = [
639       {
640         callback(aState) {
641           aActor.denyRequest(aRequest);
643           const isPersistent = aState?.checkboxChecked;
645           // Choosing 'Not now' will not set a block permission
646           // we just deny the request. This enables certain use cases
647           // where sites want to switch devices, but users back out of the permission request
648           // (See Bug 1609578).
649           // Selecting 'Remember this decision' and clicking 'Not now' will set a persistent block
650           if (!isPersistent && isNotNowLabelEnabled) {
651             return;
652           }
654           // Denying a camera / microphone prompt means we set a temporary or
655           // persistent permission block. There may still be active grace period
656           // permissions at this point. We need to remove them.
657           clearTemporaryGrants(
658             notification.browser,
659             reqVideoInput === "Camera",
660             !!reqAudioInput
661           );
663           const scope = isPersistent
664             ? lazy.SitePermissions.SCOPE_PERSISTENT
665             : lazy.SitePermissions.SCOPE_TEMPORARY;
666           if (reqAudioInput) {
667             lazy.SitePermissions.setForPrincipal(
668               principal,
669               "microphone",
670               lazy.SitePermissions.BLOCK,
671               scope,
672               notification.browser
673             );
674           }
675           if (reqVideoInput) {
676             lazy.SitePermissions.setForPrincipal(
677               principal,
678               sharingScreen ? "screen" : "camera",
679               lazy.SitePermissions.BLOCK,
680               scope,
681               notification.browser
682             );
683           }
684         },
685       },
686     ];
687   }
689   // The formatMessagesSync method returns an array of results
690   // for each message that was requested, and for the ones with
691   // attributes, returns an attributes array with objects like:
692   //     { name: "label", value: "somevalue" }
693   const [mainMessage, ...secondaryMessages] = localization
694     .formatMessagesSync(actionL10nIds)
695     .map(msg =>
696       msg.attributes.reduce(
697         (acc, { name, value }) => ({ ...acc, [name]: value }),
698         {}
699       )
700     );
702   const mainAction = {
703     label: mainMessage.label,
704     accessKey: mainMessage.accesskey,
705     // The real callback will be set during the "showing" event. The
706     // empty function here is so that PopupNotifications.show doesn't
707     // reject the action.
708     callback() {},
709   };
711   for (let i = 0; i < secondaryActions.length; ++i) {
712     secondaryActions[i].label = secondaryMessages[i].label;
713     secondaryActions[i].accessKey = secondaryMessages[i].accesskey;
714   }
716   let options = {
717     name: lazy.webrtcUI.getHostOrExtensionName(principal.URI),
718     persistent: true,
719     hideClose: true,
720     eventCallback(aTopic, aNewBrowser, isCancel) {
721       if (aTopic == "swapping") {
722         return true;
723       }
725       let doc = this.browser.ownerDocument;
727       // Clean-up video streams of screensharing previews.
728       if (
729         reqVideoInput !== "Screen" ||
730         aTopic == "dismissed" ||
731         aTopic == "removed"
732       ) {
733         let video = doc.getElementById("webRTC-previewVideo");
734         video.deviceId = null; // Abort previews still being started.
735         if (video.stream) {
736           video.stream.getTracks().forEach(t => t.stop());
737           video.stream = null;
738           video.src = null;
739           doc.getElementById("webRTC-preview").hidden = true;
740         }
741         let menupopup = doc.getElementById("webRTC-selectWindow-menupopup");
742         if (menupopup._commandEventListener) {
743           menupopup.removeEventListener(
744             "command",
745             menupopup._commandEventListener
746           );
747           menupopup._commandEventListener = null;
748         }
749       }
751       if (aTopic == "removed" && notification && isCancel) {
752         // The notification has been cancelled (e.g. due to entering
753         // full-screen).  Also cancel the webRTC request.
754         aActor.denyRequest(aRequest);
755       } else if (
756         aTopic == "shown" &&
757         audioOutputDevices.length > 1 &&
758         !notification.wasDismissed
759       ) {
760         // Focus the list on first show so that arrow keys select the speaker.
761         doc.getElementById("webRTC-selectSpeaker-richlistbox").focus();
762       }
764       if (aTopic != "showing") {
765         return false;
766       }
768       // If BLOCK has been set persistently in the permission manager or has
769       // been set on the tab, then it is handled synchronously before we add
770       // the notification.
771       // Handling of ALLOW is delayed until the popupshowing event,
772       // to avoid granting permissions automatically to background tabs.
773       if (aActor.checkRequestAllowed(aRequest, principal, aBrowser)) {
774         this.remove();
775         return true;
776       }
778       /**
779        * Prepare the device selector for one kind of device.
780        * @param {Object[]} devices - available devices of this kind.
781        * @param {string} IDPrefix - indicating kind of device and so
782        *   associated UI elements.
783        * @param {string[]} describedByIDs - an array to which might be
784        *   appended ids of elements that describe the panel, for the caller to
785        *   use in the aria-describedby attribute.
786        */
787       function listDevices(devices, IDPrefix, describedByIDs) {
788         let labelID = `${IDPrefix}-single-device-label`;
789         let list;
790         let itemParent;
791         if (IDPrefix == "webRTC-selectSpeaker") {
792           list = doc.getElementById(`${IDPrefix}-richlistbox`);
793           itemParent = list;
794         } else {
795           itemParent = doc.getElementById(`${IDPrefix}-menupopup`);
796           list = itemParent.parentNode; // menulist
797         }
798         while (itemParent.lastChild) {
799           itemParent.removeChild(itemParent.lastChild);
800         }
802         // Removing the child nodes of a menupopup doesn't clear the value
803         // attribute of its menulist. Similary for richlistbox state. This can
804         // have unfortunate side effects when the list is rebuilt with a
805         // different content, so we set the selectedIndex explicitly to reset
806         // state.
807         let defaultIndex = 0;
809         for (let device of devices) {
810           let item = addDeviceToList(list, device.name, device.deviceIndex);
811           if (IDPrefix == "webRTC-selectSpeaker") {
812             item.addEventListener("dblclick", event => {
813               // Allow the chosen speakers via
814               // .popup-notification-primary-button so that
815               // "security.notification_enable_delay" is checked.
816               event.target.closest("popupnotification").button.doCommand();
817             });
818             if (device.id == aRequest.audioOutputId) {
819               defaultIndex = device.deviceIndex;
820             }
821           }
822         }
823         list.selectedIndex = defaultIndex;
825         let label = doc.getElementById(labelID);
826         if (devices.length == 1) {
827           describedByIDs.push(`${IDPrefix}-icon`, labelID);
828           label.value = devices[0].name;
829           label.hidden = false;
830           list.hidden = true;
831         } else {
832           label.hidden = true;
833           list.hidden = false;
834         }
835       }
837       let notificationElement = doc.getElementById(
838         "webRTC-shareDevices-notification"
839       );
841       function checkDisabledWindowMenuItem() {
842         let list = doc.getElementById("webRTC-selectWindow-menulist");
843         let item = list.selectedItem;
844         if (!item || item.hasAttribute("disabled")) {
845           notificationElement.setAttribute("invalidselection", "true");
846         } else {
847           notificationElement.removeAttribute("invalidselection");
848         }
849       }
851       function listScreenShareDevices(menupopup, devices) {
852         while (menupopup.lastChild) {
853           menupopup.removeChild(menupopup.lastChild);
854         }
856         // Removing the child nodes of the menupopup doesn't clear the value
857         // attribute of the menulist. This can have unfortunate side effects
858         // when the list is rebuilt with a different content, so we remove
859         // the value attribute and unset the selectedItem explicitly.
860         menupopup.parentNode.removeAttribute("value");
861         menupopup.parentNode.selectedItem = null;
863         // "Select a Window or Screen" is the default because we can't and don't
864         // want to pick a 'default' window to share (Full screen is "scary").
865         addDeviceToList(
866           menupopup.parentNode,
867           localization.formatValueSync("webrtc-pick-window-or-screen"),
868           "-1"
869         );
870         menupopup.appendChild(doc.createXULElement("menuseparator"));
872         let isPipeWireDetected = false;
874         // Build the list of 'devices'.
875         let monitorIndex = 1;
876         for (let i = 0; i < devices.length; ++i) {
877           let device = devices[i];
878           let type = device.mediaSource;
879           let name;
880           if (device.canRequestOsLevelPrompt) {
881             // When we share content by PipeWire add only one item to the device
882             // list. When it's selected PipeWire portal dialog is opened and
883             // user confirms actual window/screen sharing there.
884             // Don't mark it as scary as there's an extra confirmation step by
885             // PipeWire portal dialog.
887             isPipeWireDetected = true;
888             let item = addDeviceToList(
889               menupopup.parentNode,
890               localization.formatValueSync("webrtc-share-pipe-wire-portal"),
891               i,
892               type
893             );
894             item.deviceId = device.rawId;
895             item.mediaSource = type;
897             // In this case the OS sharing dialog will be the only option and
898             // can be safely pre-selected.
899             menupopup.parentNode.selectedItem = item;
900             continue;
901           } else if (type == "screen") {
902             // Building screen list from available screens.
903             if (device.name == "Primary Monitor") {
904               name = localization.formatValueSync("webrtc-share-entire-screen");
905             } else {
906               name = localization.formatValueSync("webrtc-share-monitor", {
907                 monitorIndex,
908               });
909               ++monitorIndex;
910             }
911           } else {
912             name = device.name;
914             if (type == "application") {
915               // The application names returned by the platform are of the form:
916               // <window count>\x1e<application name>
917               const [count, appName] = name.split("\x1e");
918               name = localization.formatValueSync("webrtc-share-application", {
919                 appName,
920                 windowCount: parseInt(count),
921               });
922             }
923           }
924           let item = addDeviceToList(menupopup.parentNode, name, i, type);
925           item.deviceId = device.rawId;
926           item.mediaSource = type;
927           if (device.scary) {
928             item.scary = true;
929           }
930         }
932         // Always re-select the "No <type>" item.
933         doc
934           .getElementById("webRTC-selectWindow-menulist")
935           .removeAttribute("value");
936         doc.getElementById("webRTC-all-windows-shared").hidden = true;
938         menupopup._commandEventListener = event => {
939           checkDisabledWindowMenuItem();
940           let video = doc.getElementById("webRTC-previewVideo");
941           if (video.stream) {
942             video.stream.getTracks().forEach(t => t.stop());
943             video.stream = null;
944           }
946           const { deviceId, mediaSource, scary } = event.target;
947           if (deviceId == undefined) {
948             doc.getElementById("webRTC-preview").hidden = true;
949             video.src = null;
950             return;
951           }
953           let warning = doc.getElementById("webRTC-previewWarning");
954           let warningBox = doc.getElementById("webRTC-previewWarningBox");
955           warningBox.hidden = !scary;
956           let chromeWin = doc.defaultView;
957           if (scary) {
958             const warnId =
959               mediaSource == "screen"
960                 ? "webrtc-share-screen-warning"
961                 : "webrtc-share-browser-warning";
962             doc.l10n.setAttributes(warning, warnId);
964             const learnMore = doc.getElementById(
965               "webRTC-previewWarning-learnMore"
966             );
967             const baseURL = Services.urlFormatter.formatURLPref(
968               "app.support.baseURL"
969             );
970             learnMore.setAttribute("href", baseURL + "screenshare-safety");
971             doc.l10n.setAttributes(learnMore, "webrtc-share-screen-learn-more");
973             // On Catalina, we don't want to blow our chance to show the
974             // OS-level helper prompt to enable screen recording if the user
975             // intends to reject anyway. OTOH showing it when they click Allow
976             // is too late. A happy middle is to show it when the user makes a
977             // choice in the picker. This already happens implicitly if the
978             // user chooses "Entire desktop", as a side-effect of our preview,
979             // we just need to also do it if they choose "Firefox". These are
980             // the lone two options when permission is absent on Catalina.
981             // Ironically, these are the two sources marked "scary" from a
982             // web-sharing perspective, which is why this code resides here.
983             // A restart doesn't appear to be necessary in spite of OS wording.
984             let scrStatus = {};
985             lazy.OSPermissions.getScreenCapturePermissionState(scrStatus);
986             if (scrStatus.value == lazy.OSPermissions.PERMISSION_STATE_DENIED) {
987               lazy.OSPermissions.maybeRequestScreenCapturePermission();
988             }
989           }
991           let perms = Services.perms;
992           let chromePrincipal =
993             Services.scriptSecurityManager.getSystemPrincipal();
994           perms.addFromPrincipal(
995             chromePrincipal,
996             "MediaManagerVideo",
997             perms.ALLOW_ACTION,
998             perms.EXPIRE_SESSION
999           );
1001           // We don't have access to any screen content besides our browser tabs
1002           // on Wayland, therefore there are no previews we can show.
1003           if (
1004             (!isPipeWireDetected || mediaSource == "browser") &&
1005             Services.prefs.getBoolPref(
1006               "media.getdisplaymedia.previews.enabled",
1007               true
1008             )
1009           ) {
1010             video.deviceId = deviceId;
1011             let constraints = {
1012               video: { mediaSource, deviceId: { exact: deviceId } },
1013             };
1014             chromeWin.navigator.mediaDevices.getUserMedia(constraints).then(
1015               stream => {
1016                 if (video.deviceId != deviceId) {
1017                   // The user has selected a different device or closed the panel
1018                   // before getUserMedia finished.
1019                   stream.getTracks().forEach(t => t.stop());
1020                   return;
1021                 }
1022                 video.srcObject = stream;
1023                 video.stream = stream;
1024                 doc.getElementById("webRTC-preview").hidden = false;
1025                 video.onloadedmetadata = function (e) {
1026                   video.play();
1027                 };
1028               },
1029               err => {
1030                 if (
1031                   err.name == "OverconstrainedError" &&
1032                   err.constraint == "deviceId"
1033                 ) {
1034                   // Window has disappeared since enumeration, which can happen.
1035                   // No preview for you.
1036                   return;
1037                 }
1038                 console.error(
1039                   `error in preview: ${err.message} ${err.constraint}`
1040                 );
1041               }
1042             );
1043           }
1044         };
1045         menupopup.addEventListener("command", menupopup._commandEventListener);
1046       }
1048       function addDeviceToList(list, deviceName, deviceIndex, type) {
1049         let item = list.appendItem(deviceName, deviceIndex);
1050         item.setAttribute("tooltiptext", deviceName);
1051         if (type) {
1052           item.setAttribute("devicetype", type);
1053         }
1055         if (deviceIndex == "-1") {
1056           item.setAttribute("disabled", true);
1057         }
1059         return item;
1060       }
1062       doc.getElementById("webRTC-selectCamera").hidden =
1063         reqVideoInput !== "Camera";
1064       doc.getElementById("webRTC-selectWindowOrScreen").hidden =
1065         reqVideoInput !== "Screen";
1066       doc.getElementById("webRTC-selectMicrophone").hidden =
1067         reqAudioInput !== "Microphone";
1068       doc.getElementById("webRTC-selectSpeaker").hidden = !reqAudioOutput;
1070       let describedByIDs = ["webRTC-shareDevices-notification-description"];
1072       if (sharingScreen) {
1073         let windowMenupopup = doc.getElementById(
1074           "webRTC-selectWindow-menupopup"
1075         );
1076         listScreenShareDevices(windowMenupopup, videoInputDevices);
1077         checkDisabledWindowMenuItem();
1078       } else {
1079         listDevices(videoInputDevices, "webRTC-selectCamera", describedByIDs);
1080         notificationElement.removeAttribute("invalidselection");
1081       }
1082       if (!sharingAudio) {
1083         listDevices(
1084           audioInputDevices,
1085           "webRTC-selectMicrophone",
1086           describedByIDs
1087         );
1088       }
1089       listDevices(audioOutputDevices, "webRTC-selectSpeaker", describedByIDs);
1091       // PopupNotifications knows to clear the aria-describedby attribute
1092       // when hiding, so we don't have to worry about cleaning it up ourselves.
1093       chromeDoc.defaultView.PopupNotifications.panel.setAttribute(
1094         "aria-describedby",
1095         describedByIDs.join(" ")
1096       );
1098       this.mainAction.callback = async function (aState) {
1099         let remember = false;
1100         let silenceNotifications = false;
1102         if (notificationSilencingEnabled && sharingScreen) {
1103           silenceNotifications = aState && aState.checkboxChecked;
1104         } else {
1105           remember = aState && aState.checkboxChecked;
1106         }
1108         let allowedDevices = [];
1109         let perms = Services.perms;
1110         if (reqVideoInput) {
1111           let listId = sharingScreen
1112             ? "webRTC-selectWindow-menulist"
1113             : "webRTC-selectCamera-menulist";
1114           let videoDeviceIndex = doc.getElementById(listId).value;
1115           let allowVideoDevice = videoDeviceIndex != "-1";
1116           if (allowVideoDevice) {
1117             allowedDevices.push(videoDeviceIndex);
1118             // Session permission will be removed after use
1119             // (it's really one-shot, not for the entire session)
1120             perms.addFromPrincipal(
1121               principal,
1122               "MediaManagerVideo",
1123               perms.ALLOW_ACTION,
1124               perms.EXPIRE_SESSION
1125             );
1126             let { mediaSource, rawId } = videoInputDevices.find(
1127               ({ deviceIndex }) => deviceIndex == videoDeviceIndex
1128             );
1129             aActor.activateDevicePerm(aRequest.windowID, mediaSource, rawId);
1130             if (remember) {
1131               lazy.SitePermissions.setForPrincipal(
1132                 principal,
1133                 "camera",
1134                 lazy.SitePermissions.ALLOW
1135               );
1136             }
1137           }
1138         }
1140         if (reqAudioInput === "Microphone") {
1141           let audioDeviceIndex = doc.getElementById(
1142             "webRTC-selectMicrophone-menulist"
1143           ).value;
1144           let allowMic = audioDeviceIndex != "-1";
1145           if (allowMic) {
1146             allowedDevices.push(audioDeviceIndex);
1147             let { mediaSource, rawId } = audioInputDevices.find(
1148               ({ deviceIndex }) => deviceIndex == audioDeviceIndex
1149             );
1150             aActor.activateDevicePerm(aRequest.windowID, mediaSource, rawId);
1151             if (remember) {
1152               lazy.SitePermissions.setForPrincipal(
1153                 principal,
1154                 "microphone",
1155                 lazy.SitePermissions.ALLOW
1156               );
1157             }
1158           }
1159         } else if (reqAudioInput === "AudioCapture") {
1160           // Only one device possible for audio capture.
1161           allowedDevices.push(0);
1162         }
1164         if (reqAudioOutput) {
1165           let audioDeviceIndex = doc.getElementById(
1166             "webRTC-selectSpeaker-richlistbox"
1167           ).value;
1168           let allowSpeaker = audioDeviceIndex != "-1";
1169           if (allowSpeaker) {
1170             allowedDevices.push(audioDeviceIndex);
1171             let { id } = audioOutputDevices.find(
1172               ({ deviceIndex }) => deviceIndex == audioDeviceIndex
1173             );
1174             lazy.SitePermissions.setForPrincipal(
1175               principal,
1176               ["speaker", id].join("^"),
1177               lazy.SitePermissions.ALLOW
1178             );
1179           }
1180         }
1182         if (!allowedDevices.length) {
1183           aActor.denyRequest(aRequest);
1184           return;
1185         }
1187         const camNeeded = reqVideoInput === "Camera";
1188         const micNeeded = !!reqAudioInput;
1189         const scrNeeded = reqVideoInput === "Screen";
1190         const havePermission = await aActor.checkOSPermission(
1191           camNeeded,
1192           micNeeded,
1193           scrNeeded
1194         );
1195         if (!havePermission) {
1196           aActor.denyRequestNoPermission(aRequest);
1197           return;
1198         }
1200         aActor.sendAsyncMessage("webrtc:Allow", {
1201           callID: aRequest.callID,
1202           windowID: aRequest.windowID,
1203           devices: allowedDevices,
1204           suppressNotifications: silenceNotifications,
1205         });
1206       };
1208       // If we haven't handled the permission yet, we want to show the doorhanger.
1209       return false;
1210     },
1211   };
1213   function shouldShowAlwaysRemember() {
1214     // Don't offer "always remember" action in PB mode
1215     if (lazy.PrivateBrowsingUtils.isBrowserPrivate(aBrowser)) {
1216       return false;
1217     }
1219     // Don't offer "always remember" action in maybe unsafe permission
1220     // delegation
1221     if (aRequest.secondOrigin) {
1222       return false;
1223     }
1225     // Speaker grants are always remembered, so no checkbox is required.
1226     if (reqAudioOutput) {
1227       return false;
1228     }
1230     return true;
1231   }
1233   if (shouldShowAlwaysRemember()) {
1234     // Disable the permanent 'Allow' action if the connection isn't secure, or for
1235     // screen/audio sharing (because we can't guess which window the user wants to
1236     // share without prompting). Note that we never enter this block for private
1237     // browsing windows.
1238     let reason = "";
1239     if (sharingScreen) {
1240       reason = "webrtc-reason-for-no-permanent-allow-screen";
1241     } else if (sharingAudio) {
1242       reason = "webrtc-reason-for-no-permanent-allow-audio";
1243     } else if (!aRequest.secure) {
1244       reason = "webrtc-reason-for-no-permanent-allow-insecure";
1245     }
1247     options.checkbox = {
1248       label: localization.formatValueSync("webrtc-remember-allow-checkbox"),
1249       checked: principal.isAddonOrExpandedAddonPrincipal,
1250       checkedState: reason
1251         ? {
1252             disableMainAction: true,
1253             warningLabel: localization.formatValueSync(reason),
1254           }
1255         : undefined,
1256     };
1257   }
1259   // If the notification silencing feature is enabled and we're sharing a
1260   // screen, then the checkbox for the permission panel is what controls
1261   // notification silencing.
1262   if (notificationSilencingEnabled && sharingScreen) {
1263     options.checkbox = {
1264       label: localization.formatValueSync("webrtc-mute-notifications-checkbox"),
1265       checked: false,
1266       checkedState: {
1267         disableMainAction: false,
1268       },
1269     };
1270   }
1272   let anchorId = "webRTC-shareDevices-notification-icon";
1273   if (reqVideoInput === "Screen") {
1274     anchorId = "webRTC-shareScreen-notification-icon";
1275   } else if (!reqVideoInput) {
1276     if (reqAudioInput && !reqAudioOutput) {
1277       anchorId = "webRTC-shareMicrophone-notification-icon";
1278     } else if (!reqAudioInput && reqAudioOutput) {
1279       anchorId = "webRTC-shareSpeaker-notification-icon";
1280     }
1281   }
1283   if (aRequest.secondOrigin) {
1284     options.secondName = lazy.webrtcUI.getHostOrExtensionName(
1285       null,
1286       aRequest.secondOrigin
1287     );
1288   }
1290   notification = chromeDoc.defaultView.PopupNotifications.show(
1291     aBrowser,
1292     "webRTC-shareDevices",
1293     message,
1294     anchorId,
1295     mainAction,
1296     secondaryActions,
1297     options
1298   );
1299   notification.callID = aRequest.callID;
1301   let schemeHistogram = Services.telemetry.getKeyedHistogramById(
1302     "PERMISSION_REQUEST_ORIGIN_SCHEME"
1303   );
1304   let userInputHistogram = Services.telemetry.getKeyedHistogramById(
1305     "PERMISSION_REQUEST_HANDLING_USER_INPUT"
1306   );
1308   let docURI = aRequest.documentURI;
1309   let scheme = 0;
1310   if (docURI.startsWith("https")) {
1311     scheme = 2;
1312   } else if (docURI.startsWith("http")) {
1313     scheme = 1;
1314   }
1316   for (let requestType of requestTypes) {
1317     if (requestType == "AudioCapture") {
1318       requestType = "Microphone";
1319     }
1320     requestType = requestType.toLowerCase();
1322     schemeHistogram.add(requestType, scheme);
1323     userInputHistogram.add(requestType, aRequest.isHandlingUserInput);
1324   }
1328  * @param {"Screen" | "Camera" | null} reqVideoInput
1329  * @param {"AudioCapture" | "Microphone" | null} reqAudioInput
1330  * @param {boolean} reqAudioOutput
1331  * @param {boolean} delegation - Is the access delegated to a third party?
1332  * @returns {string} Localization message identifier
1333  */
1334 function getPromptMessageId(
1335   reqVideoInput,
1336   reqAudioInput,
1337   reqAudioOutput,
1338   delegation
1339 ) {
1340   switch (reqVideoInput) {
1341     case "Camera":
1342       switch (reqAudioInput) {
1343         case "Microphone":
1344           return delegation
1345             ? "webrtc-allow-share-camera-and-microphone-unsafe-delegation"
1346             : "webrtc-allow-share-camera-and-microphone";
1347         case "AudioCapture":
1348           return delegation
1349             ? "webrtc-allow-share-camera-and-audio-capture-unsafe-delegation"
1350             : "webrtc-allow-share-camera-and-audio-capture";
1351         default:
1352           return delegation
1353             ? "webrtc-allow-share-camera-unsafe-delegation"
1354             : "webrtc-allow-share-camera";
1355       }
1357     case "Screen":
1358       switch (reqAudioInput) {
1359         case "Microphone":
1360           return delegation
1361             ? "webrtc-allow-share-screen-and-microphone-unsafe-delegation"
1362             : "webrtc-allow-share-screen-and-microphone";
1363         case "AudioCapture":
1364           return delegation
1365             ? "webrtc-allow-share-screen-and-audio-capture-unsafe-delegation"
1366             : "webrtc-allow-share-screen-and-audio-capture";
1367         default:
1368           return delegation
1369             ? "webrtc-allow-share-screen-unsafe-delegation"
1370             : "webrtc-allow-share-screen";
1371       }
1373     default:
1374       switch (reqAudioInput) {
1375         case "Microphone":
1376           return delegation
1377             ? "webrtc-allow-share-microphone-unsafe-delegation"
1378             : "webrtc-allow-share-microphone";
1379         case "AudioCapture":
1380           return delegation
1381             ? "webrtc-allow-share-audio-capture-unsafe-delegation"
1382             : "webrtc-allow-share-audio-capture";
1383         default:
1384           // This should be always true, if we've reached this far.
1385           if (reqAudioOutput) {
1386             return delegation
1387               ? "webrtc-allow-share-speaker-unsafe-delegation"
1388               : "webrtc-allow-share-speaker";
1389           }
1390           return undefined;
1391       }
1392   }
1396  * Checks whether we have a microphone/camera in use by checking the activePerms map
1397  * or if we have an allow permission for a microphone/camera in sitePermissions
1398  * @param {Browser} browser - Browser to find all active and allowed microphone and camera devices for
1399  * @return true if one of the above conditions is met
1400  */
1401 function allowedOrActiveCameraOrMicrophone(browser) {
1402   // Do we have an allow permission for cam/mic in the permissions manager?
1403   if (
1404     lazy.SitePermissions.getAllForBrowser(browser).some(perm => {
1405       return (
1406         perm.state == lazy.SitePermissions.ALLOW &&
1407         (perm.id.startsWith("camera") || perm.id.startsWith("microphone"))
1408       );
1409     })
1410   ) {
1411     // Return early, no need to check for active devices
1412     return true;
1413   }
1415   // Do we have an active device?
1416   return (
1417     // Find all windowIDs that belong to our browsing contexts
1418     browser.browsingContext
1419       .getAllBrowsingContextsInSubtree()
1420       // Only keep the outerWindowIds
1421       .map(bc => bc.currentWindowGlobal?.outerWindowId)
1422       .filter(id => id != null)
1423       // We have an active device if one of our windowIds has a non empty map in the activePerms map
1424       // that includes one device of type "camera" or "microphone"
1425       .some(id => {
1426         let map = lazy.webrtcUI.activePerms.get(id);
1427         if (!map) {
1428           // This windowId has no active device
1429           return false;
1430         }
1431         // Let's see if one of the devices is a camera or a microphone
1432         let types = [...map.values()];
1433         return types.includes("microphone") || types.includes("camera");
1434       })
1435   );
1438 function removePrompt(aBrowser, aCallId) {
1439   let chromeWin = aBrowser.ownerGlobal;
1440   let notification = chromeWin.PopupNotifications.getNotification(
1441     "webRTC-shareDevices",
1442     aBrowser
1443   );
1444   if (notification && notification.callID == aCallId) {
1445     notification.remove();
1446   }
1450  * Clears temporary permission grants used for WebRTC device grace periods.
1451  * @param browser - Browser element to clear permissions for.
1452  * @param {boolean} clearCamera - Clear camera grants.
1453  * @param {boolean} clearMicrophone - Clear microphone grants.
1454  */
1455 function clearTemporaryGrants(browser, clearCamera, clearMicrophone) {
1456   if (!clearCamera && !clearMicrophone) {
1457     // Nothing to clear.
1458     return;
1459   }
1460   let perms = lazy.SitePermissions.getAllForBrowser(browser);
1461   perms
1462     .filter(perm => {
1463       let [id, key] = perm.id.split(lazy.SitePermissions.PERM_KEY_DELIMITER);
1464       // We only want to clear WebRTC grace periods. These are temporary, device
1465       // specifc (double-keyed) microphone or camera permissions.
1466       return (
1467         key &&
1468         perm.state == lazy.SitePermissions.ALLOW &&
1469         perm.scope == lazy.SitePermissions.SCOPE_TEMPORARY &&
1470         ((clearCamera && id == "camera") ||
1471           (clearMicrophone && id == "microphone"))
1472       );
1473     })
1474     .forEach(perm =>
1475       lazy.SitePermissions.removeFromPrincipal(null, perm.id, browser)
1476     );