Bug 1896390 - Add getter for Iterator.constructor to eager-ecma-allowlist.js; r=nchev...
[gecko.git] / browser / actors / WebRTCParent.sys.mjs
blob63b32bbda157178fdc90501913e69a8e488fa331
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   let message;
575   let originToShow;
576   if (principal.schemeIs("file")) {
577     message = localization.formatValueSync(stringId + "-with-file");
578     originToShow = null;
579   } else {
580     message = localization.formatValueSync(stringId, {
581       origin: "<>",
582       thirdParty: "{}",
583     });
584     originToShow = lazy.webrtcUI.getHostOrExtensionName(principal.URI);
585   }
586   let notification; // Used by action callbacks.
587   const actionL10nIds = [{ id: "webrtc-action-allow" }];
589   let notificationSilencingEnabled = Services.prefs.getBoolPref(
590     "privacy.webrtc.allowSilencingNotifications"
591   );
593   const isNotNowLabelEnabled =
594     reqAudioOutput || allowedOrActiveCameraOrMicrophone(aBrowser);
595   let secondaryActions = [];
596   if (reqAudioOutput || (notificationSilencingEnabled && sharingScreen)) {
597     // We want to free up the checkbox at the bottom of the permission
598     // panel for the notification silencing option, so we use a
599     // different configuration for the permissions panel when
600     // notification silencing is enabled.
602     let permissionName = reqAudioOutput ? "speaker" : "screen";
603     // When selecting speakers, we always offer 'Not now' instead of 'Block'.
604     // When selecting screens, we offer 'Not now' if and only if we have a
605     // (temporary) allow permission for some mic/cam device.
606     const id = isNotNowLabelEnabled
607       ? "webrtc-action-not-now"
608       : "webrtc-action-block";
609     actionL10nIds.push({ id }, { id: "webrtc-action-always-block" });
610     secondaryActions = [
611       {
612         callback() {
613           aActor.denyRequest(aRequest);
614           if (!isNotNowLabelEnabled) {
615             lazy.SitePermissions.setForPrincipal(
616               principal,
617               permissionName,
618               lazy.SitePermissions.BLOCK,
619               lazy.SitePermissions.SCOPE_TEMPORARY,
620               notification.browser
621             );
622           }
623         },
624       },
625       {
626         callback() {
627           aActor.denyRequest(aRequest);
628           lazy.SitePermissions.setForPrincipal(
629             principal,
630             permissionName,
631             lazy.SitePermissions.BLOCK,
632             lazy.SitePermissions.SCOPE_PERSISTENT,
633             notification.browser
634           );
635         },
636       },
637     ];
638   } else {
639     // We have a (temporary) allow permission for some device
640     // hence we offer a 'Not now' label instead of 'Block'.
641     const id = isNotNowLabelEnabled
642       ? "webrtc-action-not-now"
643       : "webrtc-action-block";
644     actionL10nIds.push({ id });
645     secondaryActions = [
646       {
647         callback(aState) {
648           aActor.denyRequest(aRequest);
650           const isPersistent = aState?.checkboxChecked;
652           // Choosing 'Not now' will not set a block permission
653           // we just deny the request. This enables certain use cases
654           // where sites want to switch devices, but users back out of the permission request
655           // (See Bug 1609578).
656           // Selecting 'Remember this decision' and clicking 'Not now' will set a persistent block
657           if (!isPersistent && isNotNowLabelEnabled) {
658             return;
659           }
661           // Denying a camera / microphone prompt means we set a temporary or
662           // persistent permission block. There may still be active grace period
663           // permissions at this point. We need to remove them.
664           clearTemporaryGrants(
665             notification.browser,
666             reqVideoInput === "Camera",
667             !!reqAudioInput
668           );
670           const scope = isPersistent
671             ? lazy.SitePermissions.SCOPE_PERSISTENT
672             : lazy.SitePermissions.SCOPE_TEMPORARY;
673           if (reqAudioInput) {
674             if (!isPersistent) {
675               // After a temporary block, having permissions.query() calls
676               // persistently report "granted" would be misleading
677               maybeClearAlwaysAsk(
678                 principal,
679                 "microphone",
680                 notification.browser
681               );
682             }
683             lazy.SitePermissions.setForPrincipal(
684               principal,
685               "microphone",
686               lazy.SitePermissions.BLOCK,
687               scope,
688               notification.browser
689             );
690           }
691           if (reqVideoInput) {
692             if (!isPersistent && !sharingScreen) {
693               // After a temporary block, having permissions.query() calls
694               // persistently report "granted" would be misleading
695               maybeClearAlwaysAsk(principal, "camera", notification.browser);
696             }
697             lazy.SitePermissions.setForPrincipal(
698               principal,
699               sharingScreen ? "screen" : "camera",
700               lazy.SitePermissions.BLOCK,
701               scope,
702               notification.browser
703             );
704           }
705         },
706       },
707     ];
708   }
710   // The formatMessagesSync method returns an array of results
711   // for each message that was requested, and for the ones with
712   // attributes, returns an attributes array with objects like:
713   //     { name: "label", value: "somevalue" }
714   const [mainMessage, ...secondaryMessages] = localization
715     .formatMessagesSync(actionL10nIds)
716     .map(msg =>
717       msg.attributes.reduce(
718         (acc, { name, value }) => ({ ...acc, [name]: value }),
719         {}
720       )
721     );
723   const mainAction = {
724     label: mainMessage.label,
725     accessKey: mainMessage.accesskey,
726     // The real callback will be set during the "showing" event. The
727     // empty function here is so that PopupNotifications.show doesn't
728     // reject the action.
729     callback() {},
730   };
732   for (let i = 0; i < secondaryActions.length; ++i) {
733     secondaryActions[i].label = secondaryMessages[i].label;
734     secondaryActions[i].accessKey = secondaryMessages[i].accesskey;
735   }
737   let options = {
738     name: originToShow,
739     persistent: true,
740     hideClose: true,
741     eventCallback(aTopic, aNewBrowser, isCancel) {
742       if (aTopic == "swapping") {
743         return true;
744       }
746       let doc = this.browser.ownerDocument;
748       // Clean-up video streams of screensharing previews.
749       if (
750         reqVideoInput !== "Screen" ||
751         aTopic == "dismissed" ||
752         aTopic == "removed"
753       ) {
754         let video = doc.getElementById("webRTC-previewVideo");
755         video.deviceId = null; // Abort previews still being started.
756         if (video.stream) {
757           video.stream.getTracks().forEach(t => t.stop());
758           video.stream = null;
759           video.src = null;
760           doc.getElementById("webRTC-preview").hidden = true;
761         }
762         let menupopup = doc.getElementById("webRTC-selectWindow-menupopup");
763         if (menupopup._commandEventListener) {
764           menupopup.removeEventListener(
765             "command",
766             menupopup._commandEventListener
767           );
768           menupopup._commandEventListener = null;
769         }
770       }
772       if (aTopic == "removed" && notification && isCancel) {
773         // The notification has been cancelled (e.g. due to entering
774         // full-screen).  Also cancel the webRTC request.
775         aActor.denyRequest(aRequest);
776       } else if (
777         aTopic == "shown" &&
778         audioOutputDevices.length > 1 &&
779         !notification.wasDismissed
780       ) {
781         // Focus the list on first show so that arrow keys select the speaker.
782         doc.getElementById("webRTC-selectSpeaker-richlistbox").focus();
783       }
785       if (aTopic != "showing") {
786         return false;
787       }
789       // If BLOCK has been set persistently in the permission manager or has
790       // been set on the tab, then it is handled synchronously before we add
791       // the notification.
792       // Handling of ALLOW is delayed until the popupshowing event,
793       // to avoid granting permissions automatically to background tabs.
794       if (aActor.checkRequestAllowed(aRequest, principal, aBrowser)) {
795         this.remove();
796         return true;
797       }
799       /**
800        * Prepare the device selector for one kind of device.
801        * @param {Object[]} devices - available devices of this kind.
802        * @param {string} IDPrefix - indicating kind of device and so
803        *   associated UI elements.
804        * @param {string[]} describedByIDs - an array to which might be
805        *   appended ids of elements that describe the panel, for the caller to
806        *   use in the aria-describedby attribute.
807        */
808       function listDevices(devices, IDPrefix, describedByIDs) {
809         let labelID = `${IDPrefix}-single-device-label`;
810         let list;
811         let itemParent;
812         if (IDPrefix == "webRTC-selectSpeaker") {
813           list = doc.getElementById(`${IDPrefix}-richlistbox`);
814           itemParent = list;
815         } else {
816           itemParent = doc.getElementById(`${IDPrefix}-menupopup`);
817           list = itemParent.parentNode; // menulist
818         }
819         while (itemParent.lastChild) {
820           itemParent.removeChild(itemParent.lastChild);
821         }
823         // Removing the child nodes of a menupopup doesn't clear the value
824         // attribute of its menulist. Similary for richlistbox state. This can
825         // have unfortunate side effects when the list is rebuilt with a
826         // different content, so we set the selectedIndex explicitly to reset
827         // state.
828         let defaultIndex = 0;
830         for (let device of devices) {
831           let item = addDeviceToList(list, device.name, device.deviceIndex);
832           if (IDPrefix == "webRTC-selectSpeaker") {
833             item.addEventListener("dblclick", event => {
834               // Allow the chosen speakers via
835               // .popup-notification-primary-button so that
836               // "security.notification_enable_delay" is checked.
837               event.target.closest("popupnotification").button.doCommand();
838             });
839             if (device.id == aRequest.audioOutputId) {
840               defaultIndex = device.deviceIndex;
841             }
842           }
843         }
844         list.selectedIndex = defaultIndex;
846         let label = doc.getElementById(labelID);
847         if (devices.length == 1) {
848           describedByIDs.push(`${IDPrefix}-icon`, labelID);
849           label.value = devices[0].name;
850           label.hidden = false;
851           list.hidden = true;
852         } else {
853           label.hidden = true;
854           list.hidden = false;
855         }
856       }
858       let notificationElement = doc.getElementById(
859         "webRTC-shareDevices-notification"
860       );
862       function checkDisabledWindowMenuItem() {
863         let list = doc.getElementById("webRTC-selectWindow-menulist");
864         let item = list.selectedItem;
865         if (!item || item.hasAttribute("disabled")) {
866           notificationElement.setAttribute("invalidselection", "true");
867         } else {
868           notificationElement.removeAttribute("invalidselection");
869         }
870       }
872       function listScreenShareDevices(menupopup, devices) {
873         while (menupopup.lastChild) {
874           menupopup.removeChild(menupopup.lastChild);
875         }
877         // Removing the child nodes of the menupopup doesn't clear the value
878         // attribute of the menulist. This can have unfortunate side effects
879         // when the list is rebuilt with a different content, so we remove
880         // the value attribute and unset the selectedItem explicitly.
881         menupopup.parentNode.removeAttribute("value");
882         menupopup.parentNode.selectedItem = null;
884         // "Select a Window or Screen" is the default because we can't and don't
885         // want to pick a 'default' window to share (Full screen is "scary").
886         addDeviceToList(
887           menupopup.parentNode,
888           localization.formatValueSync("webrtc-pick-window-or-screen"),
889           "-1"
890         );
891         menupopup.appendChild(doc.createXULElement("menuseparator"));
893         let isPipeWireDetected = false;
895         // Build the list of 'devices'.
896         let monitorIndex = 1;
897         for (let i = 0; i < devices.length; ++i) {
898           let device = devices[i];
899           let type = device.mediaSource;
900           let name;
901           if (device.canRequestOsLevelPrompt) {
902             // When we share content by PipeWire add only one item to the device
903             // list. When it's selected PipeWire portal dialog is opened and
904             // user confirms actual window/screen sharing there.
905             // Don't mark it as scary as there's an extra confirmation step by
906             // PipeWire portal dialog.
908             isPipeWireDetected = true;
909             let item = addDeviceToList(
910               menupopup.parentNode,
911               localization.formatValueSync("webrtc-share-pipe-wire-portal"),
912               i,
913               type
914             );
915             item.deviceId = device.rawId;
916             item.mediaSource = type;
918             // In this case the OS sharing dialog will be the only option and
919             // can be safely pre-selected.
920             menupopup.parentNode.selectedItem = item;
921             continue;
922           } else if (type == "screen") {
923             // Building screen list from available screens.
924             if (device.name == "Primary Monitor") {
925               name = localization.formatValueSync("webrtc-share-entire-screen");
926             } else {
927               name = localization.formatValueSync("webrtc-share-monitor", {
928                 monitorIndex,
929               });
930               ++monitorIndex;
931             }
932           } else {
933             name = device.name;
935             if (type == "application") {
936               // The application names returned by the platform are of the form:
937               // <window count>\x1e<application name>
938               const [count, appName] = name.split("\x1e");
939               name = localization.formatValueSync("webrtc-share-application", {
940                 appName,
941                 windowCount: parseInt(count),
942               });
943             }
944           }
945           let item = addDeviceToList(menupopup.parentNode, name, i, type);
946           item.deviceId = device.rawId;
947           item.mediaSource = type;
948           if (device.scary) {
949             item.scary = true;
950           }
951         }
953         // Always re-select the "No <type>" item.
954         doc
955           .getElementById("webRTC-selectWindow-menulist")
956           .removeAttribute("value");
957         doc.getElementById("webRTC-all-windows-shared").hidden = true;
959         menupopup._commandEventListener = event => {
960           checkDisabledWindowMenuItem();
961           let video = doc.getElementById("webRTC-previewVideo");
962           if (video.stream) {
963             video.stream.getTracks().forEach(t => t.stop());
964             video.stream = null;
965           }
967           const { deviceId, mediaSource, scary } = event.target;
968           if (deviceId == undefined) {
969             doc.getElementById("webRTC-preview").hidden = true;
970             video.src = null;
971             return;
972           }
974           let warning = doc.getElementById("webRTC-previewWarning");
975           let warningBox = doc.getElementById("webRTC-previewWarningBox");
976           warningBox.hidden = !scary;
977           let chromeWin = doc.defaultView;
978           if (scary) {
979             const warnId =
980               mediaSource == "screen"
981                 ? "webrtc-share-screen-warning"
982                 : "webrtc-share-browser-warning";
983             doc.l10n.setAttributes(warning, warnId);
985             const learnMore = doc.getElementById(
986               "webRTC-previewWarning-learnMore"
987             );
988             const baseURL = Services.urlFormatter.formatURLPref(
989               "app.support.baseURL"
990             );
991             learnMore.setAttribute("href", baseURL + "screenshare-safety");
992             doc.l10n.setAttributes(learnMore, "webrtc-share-screen-learn-more");
994             // On Catalina, we don't want to blow our chance to show the
995             // OS-level helper prompt to enable screen recording if the user
996             // intends to reject anyway. OTOH showing it when they click Allow
997             // is too late. A happy middle is to show it when the user makes a
998             // choice in the picker. This already happens implicitly if the
999             // user chooses "Entire desktop", as a side-effect of our preview,
1000             // we just need to also do it if they choose "Firefox". These are
1001             // the lone two options when permission is absent on Catalina.
1002             // Ironically, these are the two sources marked "scary" from a
1003             // web-sharing perspective, which is why this code resides here.
1004             // A restart doesn't appear to be necessary in spite of OS wording.
1005             let scrStatus = {};
1006             lazy.OSPermissions.getScreenCapturePermissionState(scrStatus);
1007             if (scrStatus.value == lazy.OSPermissions.PERMISSION_STATE_DENIED) {
1008               lazy.OSPermissions.maybeRequestScreenCapturePermission();
1009             }
1010           }
1012           let perms = Services.perms;
1013           let chromePrincipal =
1014             Services.scriptSecurityManager.getSystemPrincipal();
1015           perms.addFromPrincipal(
1016             chromePrincipal,
1017             "MediaManagerVideo",
1018             perms.ALLOW_ACTION,
1019             perms.EXPIRE_SESSION
1020           );
1022           // We don't have access to any screen content besides our browser tabs
1023           // on Wayland, therefore there are no previews we can show.
1024           if (
1025             (!isPipeWireDetected || mediaSource == "browser") &&
1026             Services.prefs.getBoolPref(
1027               "media.getdisplaymedia.previews.enabled",
1028               true
1029             )
1030           ) {
1031             video.deviceId = deviceId;
1032             let constraints = {
1033               video: { mediaSource, deviceId: { exact: deviceId } },
1034             };
1035             chromeWin.navigator.mediaDevices.getUserMedia(constraints).then(
1036               stream => {
1037                 if (video.deviceId != deviceId) {
1038                   // The user has selected a different device or closed the panel
1039                   // before getUserMedia finished.
1040                   stream.getTracks().forEach(t => t.stop());
1041                   return;
1042                 }
1043                 video.srcObject = stream;
1044                 video.stream = stream;
1045                 doc.getElementById("webRTC-preview").hidden = false;
1046                 video.onloadedmetadata = function () {
1047                   video.play();
1048                 };
1049               },
1050               err => {
1051                 if (
1052                   err.name == "OverconstrainedError" &&
1053                   err.constraint == "deviceId"
1054                 ) {
1055                   // Window has disappeared since enumeration, which can happen.
1056                   // No preview for you.
1057                   return;
1058                 }
1059                 console.error(
1060                   `error in preview: ${err.message} ${err.constraint}`
1061                 );
1062               }
1063             );
1064           }
1065         };
1066         menupopup.addEventListener("command", menupopup._commandEventListener);
1067       }
1069       function addDeviceToList(list, deviceName, deviceIndex, type) {
1070         let item = list.appendItem(deviceName, deviceIndex);
1071         item.setAttribute("tooltiptext", deviceName);
1072         if (type) {
1073           item.setAttribute("devicetype", type);
1074         }
1076         if (deviceIndex == "-1") {
1077           item.setAttribute("disabled", true);
1078         }
1080         return item;
1081       }
1083       doc.getElementById("webRTC-selectCamera").hidden =
1084         reqVideoInput !== "Camera";
1085       doc.getElementById("webRTC-selectWindowOrScreen").hidden =
1086         reqVideoInput !== "Screen";
1087       doc.getElementById("webRTC-selectMicrophone").hidden =
1088         reqAudioInput !== "Microphone";
1089       doc.getElementById("webRTC-selectSpeaker").hidden = !reqAudioOutput;
1091       let describedByIDs = ["webRTC-shareDevices-notification-description"];
1093       if (sharingScreen) {
1094         let windowMenupopup = doc.getElementById(
1095           "webRTC-selectWindow-menupopup"
1096         );
1097         listScreenShareDevices(windowMenupopup, videoInputDevices);
1098         checkDisabledWindowMenuItem();
1099       } else {
1100         listDevices(videoInputDevices, "webRTC-selectCamera", describedByIDs);
1101         notificationElement.removeAttribute("invalidselection");
1102       }
1103       if (!sharingAudio) {
1104         listDevices(
1105           audioInputDevices,
1106           "webRTC-selectMicrophone",
1107           describedByIDs
1108         );
1109       }
1110       listDevices(audioOutputDevices, "webRTC-selectSpeaker", describedByIDs);
1112       // PopupNotifications knows to clear the aria-describedby attribute
1113       // when hiding, so we don't have to worry about cleaning it up ourselves.
1114       chromeDoc.defaultView.PopupNotifications.panel.setAttribute(
1115         "aria-describedby",
1116         describedByIDs.join(" ")
1117       );
1119       this.mainAction.callback = async function (aState) {
1120         let remember = false;
1121         let silenceNotifications = false;
1123         if (notificationSilencingEnabled && sharingScreen) {
1124           silenceNotifications = aState && aState.checkboxChecked;
1125         } else {
1126           remember = aState && aState.checkboxChecked;
1127         }
1129         let allowedDevices = [];
1130         let perms = Services.perms;
1131         if (reqVideoInput) {
1132           let listId = sharingScreen
1133             ? "webRTC-selectWindow-menulist"
1134             : "webRTC-selectCamera-menulist";
1135           let videoDeviceIndex = doc.getElementById(listId).value;
1136           let allowVideoDevice = videoDeviceIndex != "-1";
1137           if (allowVideoDevice) {
1138             allowedDevices.push(videoDeviceIndex);
1139             // Session permission will be removed after use
1140             // (it's really one-shot, not for the entire session)
1141             perms.addFromPrincipal(
1142               principal,
1143               "MediaManagerVideo",
1144               perms.ALLOW_ACTION,
1145               perms.EXPIRE_SESSION
1146             );
1147             let { mediaSource, rawId } = videoInputDevices.find(
1148               ({ deviceIndex }) => deviceIndex == videoDeviceIndex
1149             );
1150             aActor.activateDevicePerm(aRequest.windowID, mediaSource, rawId);
1151             if (!sharingScreen) {
1152               persistGrantOrPromptPermission(principal, "camera", remember);
1153             }
1154           }
1155         }
1157         if (reqAudioInput === "Microphone") {
1158           let audioDeviceIndex = doc.getElementById(
1159             "webRTC-selectMicrophone-menulist"
1160           ).value;
1161           let allowMic = audioDeviceIndex != "-1";
1162           if (allowMic) {
1163             allowedDevices.push(audioDeviceIndex);
1164             let { mediaSource, rawId } = audioInputDevices.find(
1165               ({ deviceIndex }) => deviceIndex == audioDeviceIndex
1166             );
1167             aActor.activateDevicePerm(aRequest.windowID, mediaSource, rawId);
1168             persistGrantOrPromptPermission(principal, "microphone", remember);
1169           }
1170         } else if (reqAudioInput === "AudioCapture") {
1171           // Only one device possible for audio capture.
1172           allowedDevices.push(0);
1173         }
1175         if (reqAudioOutput) {
1176           let audioDeviceIndex = doc.getElementById(
1177             "webRTC-selectSpeaker-richlistbox"
1178           ).value;
1179           let allowSpeaker = audioDeviceIndex != "-1";
1180           if (allowSpeaker) {
1181             allowedDevices.push(audioDeviceIndex);
1182             let { id } = audioOutputDevices.find(
1183               ({ deviceIndex }) => deviceIndex == audioDeviceIndex
1184             );
1185             lazy.SitePermissions.setForPrincipal(
1186               principal,
1187               ["speaker", id].join("^"),
1188               lazy.SitePermissions.ALLOW
1189             );
1190           }
1191         }
1193         if (!allowedDevices.length) {
1194           aActor.denyRequest(aRequest);
1195           return;
1196         }
1198         const camNeeded = reqVideoInput === "Camera";
1199         const micNeeded = !!reqAudioInput;
1200         const scrNeeded = reqVideoInput === "Screen";
1201         const havePermission = await aActor.checkOSPermission(
1202           camNeeded,
1203           micNeeded,
1204           scrNeeded
1205         );
1206         if (!havePermission) {
1207           aActor.denyRequestNoPermission(aRequest);
1208           return;
1209         }
1211         aActor.sendAsyncMessage("webrtc:Allow", {
1212           callID: aRequest.callID,
1213           windowID: aRequest.windowID,
1214           devices: allowedDevices,
1215           suppressNotifications: silenceNotifications,
1216         });
1217       };
1219       // If we haven't handled the permission yet, we want to show the doorhanger.
1220       return false;
1221     },
1222   };
1224   function shouldShowAlwaysRemember() {
1225     // Don't offer "always remember" action in PB mode
1226     if (lazy.PrivateBrowsingUtils.isBrowserPrivate(aBrowser)) {
1227       return false;
1228     }
1230     // Don't offer "always remember" action in maybe unsafe permission
1231     // delegation
1232     if (aRequest.secondOrigin) {
1233       return false;
1234     }
1236     // Speaker grants are always remembered, so no checkbox is required.
1237     if (reqAudioOutput) {
1238       return false;
1239     }
1241     return true;
1242   }
1244   function getRememberCheckboxLabel() {
1245     if (reqVideoInput == "Camera") {
1246       if (reqAudioInput == "Microphone") {
1247         return "webrtc-remember-allow-checkbox-camera-and-microphone";
1248       }
1249       return "webrtc-remember-allow-checkbox-camera";
1250     }
1252     if (reqAudioInput == "Microphone") {
1253       return "webrtc-remember-allow-checkbox-microphone";
1254     }
1256     return "webrtc-remember-allow-checkbox";
1257   }
1259   if (shouldShowAlwaysRemember()) {
1260     // Disable the permanent 'Allow' action if the connection isn't secure, or for
1261     // screen/audio sharing (because we can't guess which window the user wants to
1262     // share without prompting). Note that we never enter this block for private
1263     // browsing windows.
1264     let reason = "";
1265     if (sharingScreen) {
1266       reason = "webrtc-reason-for-no-permanent-allow-screen";
1267     } else if (sharingAudio) {
1268       reason = "webrtc-reason-for-no-permanent-allow-audio";
1269     } else if (!aRequest.secure) {
1270       reason = "webrtc-reason-for-no-permanent-allow-insecure";
1271     }
1273     options.checkbox = {
1274       label: localization.formatValueSync(getRememberCheckboxLabel()),
1275       checked: principal.isAddonOrExpandedAddonPrincipal,
1276       checkedState: reason
1277         ? {
1278             disableMainAction: true,
1279             warningLabel: localization.formatValueSync(reason),
1280           }
1281         : undefined,
1282     };
1283   }
1285   // If the notification silencing feature is enabled and we're sharing a
1286   // screen, then the checkbox for the permission panel is what controls
1287   // notification silencing.
1288   if (notificationSilencingEnabled && sharingScreen) {
1289     options.checkbox = {
1290       label: localization.formatValueSync("webrtc-mute-notifications-checkbox"),
1291       checked: false,
1292       checkedState: {
1293         disableMainAction: false,
1294       },
1295     };
1296   }
1298   let anchorId = "webRTC-shareDevices-notification-icon";
1299   if (reqVideoInput === "Screen") {
1300     anchorId = "webRTC-shareScreen-notification-icon";
1301   } else if (!reqVideoInput) {
1302     if (reqAudioInput && !reqAudioOutput) {
1303       anchorId = "webRTC-shareMicrophone-notification-icon";
1304     } else if (!reqAudioInput && reqAudioOutput) {
1305       anchorId = "webRTC-shareSpeaker-notification-icon";
1306     }
1307   }
1309   if (aRequest.secondOrigin) {
1310     options.secondName = lazy.webrtcUI.getHostOrExtensionName(
1311       null,
1312       aRequest.secondOrigin
1313     );
1314   }
1316   notification = chromeDoc.defaultView.PopupNotifications.show(
1317     aBrowser,
1318     "webRTC-shareDevices",
1319     message,
1320     anchorId,
1321     mainAction,
1322     secondaryActions,
1323     options
1324   );
1325   notification.callID = aRequest.callID;
1327   let schemeHistogram = Services.telemetry.getKeyedHistogramById(
1328     "PERMISSION_REQUEST_ORIGIN_SCHEME"
1329   );
1330   let userInputHistogram = Services.telemetry.getKeyedHistogramById(
1331     "PERMISSION_REQUEST_HANDLING_USER_INPUT"
1332   );
1334   let docURI = aRequest.documentURI;
1335   let scheme = 0;
1336   if (docURI.startsWith("https")) {
1337     scheme = 2;
1338   } else if (docURI.startsWith("http")) {
1339     scheme = 1;
1340   }
1342   for (let requestType of requestTypes) {
1343     if (requestType == "AudioCapture") {
1344       requestType = "Microphone";
1345     }
1346     requestType = requestType.toLowerCase();
1348     schemeHistogram.add(requestType, scheme);
1349     userInputHistogram.add(requestType, aRequest.isHandlingUserInput);
1350   }
1354  * @param {"Screen" | "Camera" | null} reqVideoInput
1355  * @param {"AudioCapture" | "Microphone" | null} reqAudioInput
1356  * @param {boolean} reqAudioOutput
1357  * @param {boolean} delegation - Is the access delegated to a third party?
1358  * @returns {string} Localization message identifier
1359  */
1360 function getPromptMessageId(
1361   reqVideoInput,
1362   reqAudioInput,
1363   reqAudioOutput,
1364   delegation
1365 ) {
1366   switch (reqVideoInput) {
1367     case "Camera":
1368       switch (reqAudioInput) {
1369         case "Microphone":
1370           return delegation
1371             ? "webrtc-allow-share-camera-and-microphone-unsafe-delegation"
1372             : "webrtc-allow-share-camera-and-microphone";
1373         case "AudioCapture":
1374           return delegation
1375             ? "webrtc-allow-share-camera-and-audio-capture-unsafe-delegation"
1376             : "webrtc-allow-share-camera-and-audio-capture";
1377         default:
1378           return delegation
1379             ? "webrtc-allow-share-camera-unsafe-delegation"
1380             : "webrtc-allow-share-camera";
1381       }
1383     case "Screen":
1384       switch (reqAudioInput) {
1385         case "Microphone":
1386           return delegation
1387             ? "webrtc-allow-share-screen-and-microphone-unsafe-delegation"
1388             : "webrtc-allow-share-screen-and-microphone";
1389         case "AudioCapture":
1390           return delegation
1391             ? "webrtc-allow-share-screen-and-audio-capture-unsafe-delegation"
1392             : "webrtc-allow-share-screen-and-audio-capture";
1393         default:
1394           return delegation
1395             ? "webrtc-allow-share-screen-unsafe-delegation"
1396             : "webrtc-allow-share-screen";
1397       }
1399     default:
1400       switch (reqAudioInput) {
1401         case "Microphone":
1402           return delegation
1403             ? "webrtc-allow-share-microphone-unsafe-delegation"
1404             : "webrtc-allow-share-microphone";
1405         case "AudioCapture":
1406           return delegation
1407             ? "webrtc-allow-share-audio-capture-unsafe-delegation"
1408             : "webrtc-allow-share-audio-capture";
1409         default:
1410           // This should be always true, if we've reached this far.
1411           if (reqAudioOutput) {
1412             return delegation
1413               ? "webrtc-allow-share-speaker-unsafe-delegation"
1414               : "webrtc-allow-share-speaker";
1415           }
1416           return undefined;
1417       }
1418   }
1422  * Checks whether we have a microphone/camera in use by checking the activePerms map
1423  * or if we have an allow permission for a microphone/camera in sitePermissions
1424  * @param {Browser} browser - Browser to find all active and allowed microphone and camera devices for
1425  * @return true if one of the above conditions is met
1426  */
1427 function allowedOrActiveCameraOrMicrophone(browser) {
1428   // Do we have an allow permission for cam/mic in the permissions manager?
1429   if (
1430     lazy.SitePermissions.getAllForBrowser(browser).some(perm => {
1431       return (
1432         perm.state == lazy.SitePermissions.ALLOW &&
1433         (perm.id.startsWith("camera") || perm.id.startsWith("microphone"))
1434       );
1435     })
1436   ) {
1437     // Return early, no need to check for active devices
1438     return true;
1439   }
1441   // Do we have an active device?
1442   return (
1443     // Find all windowIDs that belong to our browsing contexts
1444     browser.browsingContext
1445       .getAllBrowsingContextsInSubtree()
1446       // Only keep the outerWindowIds
1447       .map(bc => bc.currentWindowGlobal?.outerWindowId)
1448       .filter(id => id != null)
1449       // We have an active device if one of our windowIds has a non empty map in the activePerms map
1450       // that includes one device of type "camera" or "microphone"
1451       .some(id => {
1452         let map = lazy.webrtcUI.activePerms.get(id);
1453         if (!map) {
1454           // This windowId has no active device
1455           return false;
1456         }
1457         // Let's see if one of the devices is a camera or a microphone
1458         let types = [...map.values()];
1459         return types.includes("microphone") || types.includes("camera");
1460       })
1461   );
1464 function removePrompt(aBrowser, aCallId) {
1465   let chromeWin = aBrowser.ownerGlobal;
1466   let notification = chromeWin.PopupNotifications.getNotification(
1467     "webRTC-shareDevices",
1468     aBrowser
1469   );
1470   if (notification && notification.callID == aCallId) {
1471     notification.remove();
1472   }
1476  * Clears temporary permission grants used for WebRTC device grace periods.
1477  * @param browser - Browser element to clear permissions for.
1478  * @param {boolean} clearCamera - Clear camera grants.
1479  * @param {boolean} clearMicrophone - Clear microphone grants.
1480  */
1481 function clearTemporaryGrants(browser, clearCamera, clearMicrophone) {
1482   if (!clearCamera && !clearMicrophone) {
1483     // Nothing to clear.
1484     return;
1485   }
1486   let perms = lazy.SitePermissions.getAllForBrowser(browser);
1487   perms
1488     .filter(perm => {
1489       let [id, key] = perm.id.split(lazy.SitePermissions.PERM_KEY_DELIMITER);
1490       // We only want to clear WebRTC grace periods. These are temporary, device
1491       // specifc (double-keyed) microphone or camera permissions.
1492       return (
1493         key &&
1494         perm.state == lazy.SitePermissions.ALLOW &&
1495         perm.scope == lazy.SitePermissions.SCOPE_TEMPORARY &&
1496         ((clearCamera && id == "camera") ||
1497           (clearMicrophone && id == "microphone"))
1498       );
1499     })
1500     .forEach(perm =>
1501       lazy.SitePermissions.removeFromPrincipal(null, perm.id, browser)
1502     );
1506  * Persist an ALLOW state if the remember option is true.
1507  * Otherwise, persist PROMPT so that we can later tell the site
1508  * that permission was granted once before.
1509  * This makes Firefox seem much more like Chrome to sites that
1510  * expect a one-off, persistent permission grant for cam/mic.
1512  * @param principal - Principal to add permission to.
1513  * @param {string} permissionName - name of permission.
1514  * @param remember - whether the grant should be persisted.
1515  */
1516 function persistGrantOrPromptPermission(principal, permissionName, remember) {
1517   // There are cases like unsafe delegation where a prompt appears
1518   // even in ALLOW state, so make sure to not overwrite it (there's
1519   // no remember checkbox in those cases)
1520   if (
1521     lazy.SitePermissions.getForPrincipal(principal, permissionName).state ==
1522     lazy.SitePermissions.ALLOW
1523   ) {
1524     return;
1525   }
1527   lazy.SitePermissions.setForPrincipal(
1528     principal,
1529     permissionName,
1530     remember ? lazy.SitePermissions.ALLOW : lazy.SitePermissions.PROMPT
1531   );
1535  * Clears any persisted PROMPT (aka Always Ask) permission.
1536  * @param principal - Principal to remove permission from.
1537  * @param {string} permissionName - name of permission.
1538  * @param browser - Browser element to clear permission for.
1539  */
1540 function maybeClearAlwaysAsk(principal, permissionName, browser) {
1541   // For the "Always Ask" user choice, only persisted PROMPT is used,
1542   // so no need to scan through temporary permissions.
1543   if (
1544     lazy.SitePermissions.getForPrincipal(principal, permissionName).state ==
1545     lazy.SitePermissions.PROMPT
1546   ) {
1547     lazy.SitePermissions.removeFromPrincipal(
1548       principal,
1549       permissionName,
1550       browser
1551     );
1552   }