Bug 1809094 - Implement tab.autoDiscardable property r=robwu,geckoview-reviewers...
[gecko.git] / toolkit / components / extensions / parent / ext-tabs-base.js
blob6240769a6e9460cb68533c5fb445a0cbfc5cec3f
1 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
2 /* vim: set sts=2 sw=2 et tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 "use strict";
8 /* globals EventEmitter */
10 ChromeUtils.defineESModuleGetters(this, {
11   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
12 });
14 XPCOMUtils.defineLazyPreferenceGetter(
15   this,
16   "containersEnabled",
17   "privacy.userContext.enabled"
20 var { DefaultMap, DefaultWeakMap, ExtensionError, parseMatchPatterns } =
21   ExtensionUtils;
23 var { defineLazyGetter } = ExtensionCommon;
25 /**
26  * The platform-specific type of native tab objects, which are wrapped by
27  * TabBase instances.
28  *
29  * @typedef {object | XULElement} NativeTab
30  */
32 /**
33  * @typedef {object} MutedInfo
34  * @property {boolean} muted
35  *        True if the tab is currently muted, false otherwise.
36  * @property {string} [reason]
37  *        The reason the tab is muted. Either "user", if the tab was muted by a
38  *        user, or "extension", if it was muted by an extension.
39  * @property {string} [extensionId]
40  *        If the tab was muted by an extension, contains the internal ID of that
41  *        extension.
42  */
44 /**
45  * A platform-independent base class for extension-specific wrappers around
46  * native tab objects.
47  *
48  * @param {Extension} extension
49  *        The extension object for which this wrapper is being created. Used to
50  *        determine permissions for access to certain properties and
51  *        functionality.
52  * @param {NativeTab} nativeTab
53  *        The native tab object which is being wrapped. The type of this object
54  *        varies by platform.
55  * @param {integer} id
56  *        The numeric ID of this tab object. This ID should be the same for
57  *        every extension, and for the lifetime of the tab.
58  */
59 class TabBase {
60   constructor(extension, nativeTab, id) {
61     this.extension = extension;
62     this.tabManager = extension.tabManager;
63     this.id = id;
64     this.nativeTab = nativeTab;
65     this.activeTabWindowID = null;
67     if (!extension.privateBrowsingAllowed && this._incognito) {
68       throw new ExtensionError(`Invalid tab ID: ${id}`);
69     }
70   }
72   /**
73    * Capture the visible area of this tab, and return the result as a data: URI.
74    *
75    * @param {BaseContext} context
76    *        The extension context for which to perform the capture.
77    * @param {number} zoom
78    *        The current zoom for the page.
79    * @param {object} [options]
80    *        The options with which to perform the capture.
81    * @param {string} [options.format = "png"]
82    *        The image format in which to encode the captured data. May be one of
83    *        "png" or "jpeg".
84    * @param {integer} [options.quality = 92]
85    *        The quality at which to encode the captured image data, ranging from
86    *        0 to 100. Has no effect for the "png" format.
87    * @param {DOMRectInit} [options.rect]
88    *        Area of the document to render, in CSS pixels, relative to the page.
89    *        If null, the currently visible viewport is rendered.
90    * @param {number} [options.scale]
91    *        The scale to render at, defaults to devicePixelRatio.
92    * @returns {Promise<string>}
93    */
94   async capture(context, zoom, options) {
95     let win = this.browser.ownerGlobal;
96     let scale = options?.scale || win.devicePixelRatio;
97     let rect = options?.rect && win.DOMRect.fromRect(options.rect);
99     // We only allow mozilla addons to use the resetScrollPosition option,
100     // since it's not standardized.
101     let resetScrollPosition = false;
102     if (!context.extension.restrictSchemes) {
103       resetScrollPosition = !!options?.resetScrollPosition;
104     }
106     let wgp = this.browsingContext.currentWindowGlobal;
107     let image = await wgp.drawSnapshot(
108       rect,
109       scale * zoom,
110       "white",
111       resetScrollPosition
112     );
114     let doc = Services.appShell.hiddenDOMWindow.document;
115     let canvas = doc.createElement("canvas");
116     canvas.width = image.width;
117     canvas.height = image.height;
119     let ctx = canvas.getContext("2d", { alpha: false });
120     ctx.drawImage(image, 0, 0);
121     image.close();
123     return canvas.toDataURL(`image/${options?.format}`, options?.quality / 100);
124   }
126   /**
127    * @property {integer | null} innerWindowID
128    *        The last known innerWindowID loaded into this tab's docShell. This
129    *        property must remain in sync with the last known values of
130    *        properties such as `url` and `title`. Any operations on the content
131    *        of an out-of-process tab will automatically fail if the
132    *        innerWindowID of the tab when the message is received does not match
133    *        the value of this property when the message was sent.
134    *        @readonly
135    */
136   get innerWindowID() {
137     return this.browser.innerWindowID;
138   }
140   /**
141    * @property {boolean} hasTabPermission
142    *        Returns true if the extension has permission to access restricted
143    *        properties of this tab, such as `url`, `title`, and `favIconUrl`.
144    *        @readonly
145    */
146   get hasTabPermission() {
147     return (
148       this.extension.hasPermission("tabs") ||
149       this.hasActiveTabPermission ||
150       this.matchesHostPermission
151     );
152   }
154   /**
155    * @property {boolean} hasActiveTabPermission
156    *        Returns true if the extension has the "activeTab" permission, and
157    *        has been granted access to this tab due to a user executing an
158    *        extension action.
159    *
160    *        If true, the extension may load scripts and CSS into this tab, and
161    *        access restricted properties, such as its `url`.
162    *        @readonly
163    */
164   get hasActiveTabPermission() {
165     return (
166       (this.extension.originControls ||
167         this.extension.hasPermission("activeTab")) &&
168       this.activeTabWindowID != null &&
169       this.activeTabWindowID === this.innerWindowID
170     );
171   }
173   /**
174    * @property {boolean} matchesHostPermission
175    *        Returns true if the extensions host permissions match the current tab url.
176    *        @readonly
177    */
178   get matchesHostPermission() {
179     return this.extension.allowedOrigins.matches(this._uri);
180   }
182   /**
183    * @property {boolean} incognito
184    *        Returns true if this is a private browsing tab, false otherwise.
185    *        @readonly
186    */
187   get _incognito() {
188     return PrivateBrowsingUtils.isBrowserPrivate(this.browser);
189   }
191   /**
192    * @property {string} _url
193    *        Returns the current URL of this tab. Does not do any permission
194    *        checks.
195    *        @readonly
196    */
197   get _url() {
198     return this.browser.currentURI.spec;
199   }
201   /**
202    * @property {string | null} url
203    *        Returns the current URL of this tab if the extension has permission
204    *        to read it, or null otherwise.
205    *        @readonly
206    */
207   get url() {
208     if (this.hasTabPermission) {
209       return this._url;
210     }
211   }
213   /**
214    * @property {nsIURI} _uri
215    *        Returns the current URI of this tab.
216    *        @readonly
217    */
218   get _uri() {
219     return this.browser.currentURI;
220   }
222   /**
223    * @property {string} _title
224    *        Returns the current title of this tab. Does not do any permission
225    *        checks.
226    *        @readonly
227    */
228   get _title() {
229     return this.browser.contentTitle || this.nativeTab.label;
230   }
232   /**
233    * @property {nsIURI | null} title
234    *        Returns the current title of this tab if the extension has permission
235    *        to read it, or null otherwise.
236    *        @readonly
237    */
238   get title() {
239     if (this.hasTabPermission) {
240       return this._title;
241     }
242   }
244   /**
245    * @property {string} _favIconUrl
246    *        Returns the current favicon URL of this tab. Does not do any permission
247    *        checks.
248    *        @readonly
249    *        @abstract
250    */
251   get _favIconUrl() {
252     throw new Error("Not implemented");
253   }
255   /**
256    * @property {nsIURI | null} faviconUrl
257    *        Returns the current faviron URL of this tab if the extension has permission
258    *        to read it, or null otherwise.
259    *        @readonly
260    */
261   get favIconUrl() {
262     if (this.hasTabPermission) {
263       return this._favIconUrl;
264     }
265   }
267   /**
268    * @property {integer} lastAccessed
269    *        Returns the last time the tab was accessed as the number of
270    *        milliseconds since epoch.
271    *        @readonly
272    *        @abstract
273    */
274   get lastAccessed() {
275     throw new Error("Not implemented");
276   }
278   /**
279    * @property {boolean} audible
280    *        Returns true if the tab is currently playing audio, false otherwise.
281    *        @readonly
282    *        @abstract
283    */
284   get audible() {
285     throw new Error("Not implemented");
286   }
288   /**
289    * @property {boolean} autoDiscardable
290    *        Returns true if the tab can be discarded on memory pressure, false otherwise.
291    *        @readonly
292    *        @abstract
293    */
294   get autoDiscardable() {
295     throw new Error("Not implemented");
296   }
298   /**
299    * @property {XULElement} browser
300    *        Returns the XUL browser for the given tab.
301    *        @readonly
302    *        @abstract
303    */
304   get browser() {
305     throw new Error("Not implemented");
306   }
308   /**
309    * @property {BrowsingContext} browsingContext
310    *        Returns the BrowsingContext for the given tab.
311    *        @readonly
312    */
313   get browsingContext() {
314     return this.browser?.browsingContext;
315   }
317   /**
318    * @property {FrameLoader} frameLoader
319    *        Returns the frameloader for the given tab.
320    *        @readonly
321    */
322   get frameLoader() {
323     return this.browser && this.browser.frameLoader;
324   }
326   /**
327    * @property {string} cookieStoreId
328    *        Returns the cookie store identifier for the given tab.
329    *        @readonly
330    *        @abstract
331    */
332   get cookieStoreId() {
333     throw new Error("Not implemented");
334   }
336   /**
337    * @property {integer} openerTabId
338    *        Returns the ID of the tab which opened this one.
339    *        @readonly
340    */
341   get openerTabId() {
342     return null;
343   }
345   /**
346    * @property {integer} discarded
347    *        Returns true if the tab is discarded.
348    *        @readonly
349    *        @abstract
350    */
351   get discarded() {
352     throw new Error("Not implemented");
353   }
355   /**
356    * @property {integer} height
357    *        Returns the pixel height of the visible area of the tab.
358    *        @readonly
359    *        @abstract
360    */
361   get height() {
362     throw new Error("Not implemented");
363   }
365   /**
366    * @property {integer} hidden
367    *        Returns true if the tab is hidden.
368    *        @readonly
369    *        @abstract
370    */
371   get hidden() {
372     throw new Error("Not implemented");
373   }
375   /**
376    * @property {integer} index
377    *        Returns the index of the tab in its window's tab list.
378    *        @readonly
379    *        @abstract
380    */
381   get index() {
382     throw new Error("Not implemented");
383   }
385   /**
386    * @property {MutedInfo} mutedInfo
387    *        Returns information about the tab's current audio muting status.
388    *        @readonly
389    *        @abstract
390    */
391   get mutedInfo() {
392     throw new Error("Not implemented");
393   }
395   /**
396    * @property {SharingState} sharingState
397    *        Returns object with tab sharingState.
398    *        @readonly
399    *        @abstract
400    */
401   get sharingState() {
402     throw new Error("Not implemented");
403   }
405   /**
406    * @property {boolean} pinned
407    *        Returns true if the tab is pinned, false otherwise.
408    *        @readonly
409    *        @abstract
410    */
411   get pinned() {
412     throw new Error("Not implemented");
413   }
415   /**
416    * @property {boolean} active
417    *        Returns true if the tab is the currently-selected tab, false
418    *        otherwise.
419    *        @readonly
420    *        @abstract
421    */
422   get active() {
423     throw new Error("Not implemented");
424   }
426   /**
427    * @property {boolean} highlighted
428    *        Returns true if the tab is highlighted.
429    *        @readonly
430    *        @abstract
431    */
432   get highlighted() {
433     throw new Error("Not implemented");
434   }
436   /**
437    * @property {string} status
438    *        Returns the current loading status of the tab. May be either
439    *        "loading" or "complete".
440    *        @readonly
441    *        @abstract
442    */
443   get status() {
444     throw new Error("Not implemented");
445   }
447   /**
448    * @property {integer} height
449    *        Returns the pixel height of the visible area of the tab.
450    *        @readonly
451    *        @abstract
452    */
453   get width() {
454     throw new Error("Not implemented");
455   }
457   /**
458    * @property {DOMWindow} window
459    *        Returns the browser window to which the tab belongs.
460    *        @readonly
461    *        @abstract
462    */
463   get window() {
464     throw new Error("Not implemented");
465   }
467   /**
468    * @property {integer} window
469    *        Returns the numeric ID of the browser window to which the tab belongs.
470    *        @readonly
471    *        @abstract
472    */
473   get windowId() {
474     throw new Error("Not implemented");
475   }
477   /**
478    * @property {boolean} attention
479    *          Returns true if the tab is drawing attention.
480    *          @readonly
481    *          @abstract
482    */
483   get attention() {
484     throw new Error("Not implemented");
485   }
487   /**
488    * @property {boolean} isArticle
489    *        Returns true if the document in the tab can be rendered in reader
490    *        mode.
491    *        @readonly
492    *        @abstract
493    */
494   get isArticle() {
495     throw new Error("Not implemented");
496   }
498   /**
499    * @property {boolean} isInReaderMode
500    *        Returns true if the document in the tab is being rendered in reader
501    *        mode.
502    *        @readonly
503    *        @abstract
504    */
505   get isInReaderMode() {
506     throw new Error("Not implemented");
507   }
509   /**
510    * @property {integer} successorTabId
511    *        @readonly
512    *        @abstract
513    */
514   get successorTabId() {
515     throw new Error("Not implemented");
516   }
518   /**
519    * Returns true if this tab matches the the given query info object. Omitted
520    * or null have no effect on the match.
521    *
522    * @param {object} queryInfo
523    *        The query info against which to match.
524    * @param {boolean} [queryInfo.active]
525    *        Matches against the exact value of the tab's `active` attribute.
526    * @param {boolean} [queryInfo.audible]
527    *        Matches against the exact value of the tab's `audible` attribute.
528    * @param {boolean} [queryInfo.autoDiscardable]
529    *        Matches against the exact value of the tab's `autoDiscardable` attribute.
530    * @param {string} [queryInfo.cookieStoreId]
531    *        Matches against the exact value of the tab's `cookieStoreId` attribute.
532    * @param {boolean} [queryInfo.discarded]
533    *        Matches against the exact value of the tab's `discarded` attribute.
534    * @param {boolean} [queryInfo.hidden]
535    *        Matches against the exact value of the tab's `hidden` attribute.
536    * @param {boolean} [queryInfo.highlighted]
537    *        Matches against the exact value of the tab's `highlighted` attribute.
538    * @param {integer} [queryInfo.index]
539    *        Matches against the exact value of the tab's `index` attribute.
540    * @param {boolean} [queryInfo.muted]
541    *        Matches against the exact value of the tab's `mutedInfo.muted` attribute.
542    * @param {boolean} [queryInfo.pinned]
543    *        Matches against the exact value of the tab's `pinned` attribute.
544    * @param {string} [queryInfo.status]
545    *        Matches against the exact value of the tab's `status` attribute.
546    * @param {string} [queryInfo.title]
547    *        Matches against the exact value of the tab's `title` attribute.
548    * @param {string|boolean } [queryInfo.screen]
549    *        Matches against the exact value of the tab's `sharingState.screen` attribute, or use true to match any screen sharing tab.
550    * @param {boolean} [queryInfo.camera]
551    *        Matches against the exact value of the tab's `sharingState.camera` attribute.
552    * @param {boolean} [queryInfo.microphone]
553    *        Matches against the exact value of the tab's `sharingState.microphone` attribute.
554    *
555    *        Note: Per specification, this should perform a pattern match, rather
556    *        than an exact value match, and will do so in the future.
557    * @param {MatchPattern} [queryInfo.url]
558    *        Requires the tab's URL to match the given MatchPattern object.
559    *
560    * @returns {boolean}
561    *        True if the tab matches the query.
562    */
563   matches(queryInfo) {
564     const PROPS = [
565       "active",
566       "audible",
567       "autoDiscardable",
568       "discarded",
569       "hidden",
570       "highlighted",
571       "index",
572       "openerTabId",
573       "pinned",
574       "status",
575     ];
577     function checkProperty(prop, obj) {
578       return queryInfo[prop] != null && queryInfo[prop] !== obj[prop];
579     }
581     if (PROPS.some(prop => checkProperty(prop, this))) {
582       return false;
583     }
585     if (checkProperty("muted", this.mutedInfo)) {
586       return false;
587     }
589     let state = this.sharingState;
590     if (["camera", "microphone"].some(prop => checkProperty(prop, state))) {
591       return false;
592     }
593     // query for screen can be boolean (ie. any) or string (ie. specific).
594     if (queryInfo.screen !== null) {
595       let match =
596         typeof queryInfo.screen == "boolean"
597           ? queryInfo.screen === !!state.screen
598           : queryInfo.screen === state.screen;
599       if (!match) {
600         return false;
601       }
602     }
604     if (queryInfo.cookieStoreId) {
605       if (!queryInfo.cookieStoreId.includes(this.cookieStoreId)) {
606         return false;
607       }
608     }
610     if (queryInfo.url || queryInfo.title) {
611       if (!this.hasTabPermission) {
612         return false;
613       }
614       // Using _uri and _title instead of url/title to avoid repeated permission checks.
615       if (queryInfo.url && !queryInfo.url.matches(this._uri)) {
616         return false;
617       }
618       if (queryInfo.title && !queryInfo.title.matches(this._title)) {
619         return false;
620       }
621     }
623     return true;
624   }
626   /**
627    * Converts this tab object to a JSON-compatible object containing the values
628    * of its properties which the extension is permitted to access, in the format
629    * required to be returned by WebExtension APIs.
630    *
631    * @param {object} [fallbackTabSize]
632    *        A geometry data if the lazy geometry data for this tab hasn't been
633    *        initialized yet.
634    * @returns {object}
635    */
636   convert(fallbackTabSize = null) {
637     let result = {
638       id: this.id,
639       index: this.index,
640       windowId: this.windowId,
641       highlighted: this.highlighted,
642       active: this.active,
643       attention: this.attention,
644       pinned: this.pinned,
645       status: this.status,
646       hidden: this.hidden,
647       discarded: this.discarded,
648       incognito: this.incognito,
649       width: this.width,
650       height: this.height,
651       lastAccessed: this.lastAccessed,
652       audible: this.audible,
653       autoDiscardable: this.autoDiscardable,
654       mutedInfo: this.mutedInfo,
655       isArticle: this.isArticle,
656       isInReaderMode: this.isInReaderMode,
657       sharingState: this.sharingState,
658       successorTabId: this.successorTabId,
659       cookieStoreId: this.cookieStoreId,
660     };
662     // If the tab has not been fully layed-out yet, fallback to the geometry
663     // from a different tab (usually the currently active tab).
664     if (fallbackTabSize && (!result.width || !result.height)) {
665       result.width = fallbackTabSize.width;
666       result.height = fallbackTabSize.height;
667     }
669     let opener = this.openerTabId;
670     if (opener) {
671       result.openerTabId = opener;
672     }
674     if (this.hasTabPermission) {
675       for (let prop of ["url", "title", "favIconUrl"]) {
676         // We use the underscored variants here to avoid the redundant
677         // permissions checks imposed on the public properties.
678         let val = this[`_${prop}`];
679         if (val) {
680           result[prop] = val;
681         }
682       }
683     }
685     return result;
686   }
688   /**
689    * Query each content process hosting subframes of the tab, return results.
690    *
691    * @param {string} message
692    * @param {object} options
693    *        These options are also sent to the message handler in the
694    *        `ExtensionContentChild`.
695    * @param {number[]} options.frameIds
696    *        When omitted, all frames will be queried.
697    * @param {boolean} options.returnResultsWithFrameIds
698    * @returns {Promise[]}
699    */
700   async queryContent(message, options) {
701     let { frameIds } = options;
703     /** @type {Map<nsIDOMProcessParent, innerWindowId[]>} */
704     let byProcess = new DefaultMap(() => []);
705     // We use this set to know which frame IDs are potentially invalid (as in
706     // not found when visiting the tab's BC tree below) when frameIds is a
707     // non-empty list of frame IDs.
708     let frameIdsSet = new Set(frameIds);
710     // Recursively walk the tab's BC tree, find all frames, group by process.
711     function visit(bc) {
712       let win = bc.currentWindowGlobal;
713       let frameId = bc.parent ? bc.id : 0;
715       if (win?.domProcess && (!frameIds || frameIdsSet.has(frameId))) {
716         byProcess.get(win.domProcess).push(win.innerWindowId);
717         frameIdsSet.delete(frameId);
718       }
720       if (!frameIds || frameIdsSet.size > 0) {
721         bc.children.forEach(visit);
722       }
723     }
724     visit(this.browsingContext);
726     if (frameIdsSet.size > 0) {
727       throw new ExtensionError(
728         `Invalid frame IDs: [${Array.from(frameIdsSet).join(", ")}].`
729       );
730     }
732     let promises = Array.from(byProcess.entries(), ([proc, windows]) =>
733       proc.getActor("ExtensionContent").sendQuery(message, { windows, options })
734     );
736     let results = await Promise.all(promises).catch(err => {
737       if (err.name === "DataCloneError") {
738         let fileName = options.jsPaths.slice(-1)[0] || "<anonymous code>";
739         let message = `Script '${fileName}' result is non-structured-clonable data`;
740         return Promise.reject({ message, fileName });
741       }
742       throw err;
743     });
744     results = results.flat();
746     if (!results.length) {
747       let errorMessage = "Missing host permission for the tab";
748       if (!frameIds || frameIds.length > 1 || frameIds[0] !== 0) {
749         errorMessage += " or frames";
750       }
752       throw new ExtensionError(errorMessage);
753     }
755     if (frameIds && frameIds.length === 1 && results.length > 1) {
756       throw new ExtensionError("Internal error: multiple windows matched");
757     }
759     return results;
760   }
762   /**
763    * Inserts a script or stylesheet in the given tab, and returns a promise
764    * which resolves when the operation has completed.
765    *
766    * @param {BaseContext} context
767    *        The extension context for which to perform the injection.
768    * @param {InjectDetails} details
769    *        The InjectDetails object, specifying what to inject, where, and
770    *        when.
771    * @param {string} kind
772    *        The kind of data being injected. Either "script" or "css".
773    * @param {string} method
774    *        The name of the method which was called to trigger the injection.
775    *        Used to generate appropriate error messages on failure.
776    *
777    * @returns {Promise}
778    *        Resolves to the result of the execution, once it has completed.
779    * @private
780    */
781   _execute(context, details, kind, method) {
782     let options = {
783       jsPaths: [],
784       cssPaths: [],
785       removeCSS: method == "removeCSS",
786       extensionId: context.extension.id,
787     };
789     // We require a `code` or a `file` property, but we can't accept both.
790     if ((details.code === null) == (details.file === null)) {
791       return Promise.reject({
792         message: `${method} requires either a 'code' or a 'file' property, but not both`,
793       });
794     }
796     if (details.frameId !== null && details.allFrames) {
797       return Promise.reject({
798         message: `'frameId' and 'allFrames' are mutually exclusive`,
799       });
800     }
802     options.hasActiveTabPermission = this.hasActiveTabPermission;
803     options.matches = this.extension.allowedOrigins.patterns.map(
804       host => host.pattern
805     );
807     if (details.code !== null) {
808       options[`${kind}Code`] = details.code;
809     }
810     if (details.file !== null) {
811       let url = context.uri.resolve(details.file);
812       if (!this.extension.isExtensionURL(url)) {
813         return Promise.reject({
814           message: "Files to be injected must be within the extension",
815         });
816       }
817       options[`${kind}Paths`].push(url);
818     }
820     if (details.allFrames) {
821       options.allFrames = true;
822     } else if (details.frameId !== null) {
823       options.frameIds = [details.frameId];
824     } else if (!details.allFrames) {
825       options.frameIds = [0];
826     }
828     if (details.matchAboutBlank) {
829       options.matchAboutBlank = details.matchAboutBlank;
830     }
831     if (details.runAt !== null) {
832       options.runAt = details.runAt;
833     } else {
834       options.runAt = "document_idle";
835     }
836     if (details.cssOrigin !== null) {
837       options.cssOrigin = details.cssOrigin;
838     } else {
839       options.cssOrigin = "author";
840     }
842     options.wantReturnValue = true;
844     // The scripting API (defined in `parent/ext-scripting.js`) has its own
845     // `execute()` function that calls `queryContent()` as well. Make sure to
846     // keep both in sync when relevant.
847     return this.queryContent("Execute", options);
848   }
850   /**
851    * Executes a script in the tab's content window, and returns a Promise which
852    * resolves to the result of the evaluation, or rejects to the value of any
853    * error the injection generates.
854    *
855    * @param {BaseContext} context
856    *        The extension context for which to inject the script.
857    * @param {InjectDetails} details
858    *        The InjectDetails object, specifying what to inject, where, and
859    *        when.
860    *
861    * @returns {Promise}
862    *        Resolves to the result of the evaluation of the given script, once
863    *        it has completed, or rejects with any error the evaluation
864    *        generates.
865    */
866   executeScript(context, details) {
867     return this._execute(context, details, "js", "executeScript");
868   }
870   /**
871    * Injects CSS into the tab's content window, and returns a Promise which
872    * resolves when the injection is complete.
873    *
874    * @param {BaseContext} context
875    *        The extension context for which to inject the script.
876    * @param {InjectDetails} details
877    *        The InjectDetails object, specifying what to inject, and where.
878    *
879    * @returns {Promise}
880    *        Resolves when the injection has completed.
881    */
882   insertCSS(context, details) {
883     return this._execute(context, details, "css", "insertCSS").then(() => {});
884   }
886   /**
887    * Removes CSS which was previously into the tab's content window via
888    * `insertCSS`, and returns a Promise which resolves when the operation is
889    * complete.
890    *
891    * @param {BaseContext} context
892    *        The extension context for which to remove the CSS.
893    * @param {InjectDetails} details
894    *        The InjectDetails object, specifying what to remove, and from where.
895    *
896    * @returns {Promise}
897    *        Resolves when the operation has completed.
898    */
899   removeCSS(context, details) {
900     return this._execute(context, details, "css", "removeCSS").then(() => {});
901   }
904 defineLazyGetter(TabBase.prototype, "incognito", function () {
905   return this._incognito;
908 // Note: These must match the values in windows.json.
909 const WINDOW_ID_NONE = -1;
910 const WINDOW_ID_CURRENT = -2;
913  * A platform-independent base class for extension-specific wrappers around
914  * native browser windows
916  * @param {Extension} extension
917  *        The extension object for which this wrapper is being created.
918  * @param {DOMWindow} window
919  *        The browser DOM window which is being wrapped.
920  * @param {integer} id
921  *        The numeric ID of this DOM window object. This ID should be the same for
922  *        every extension, and for the lifetime of the window.
923  */
924 class WindowBase {
925   constructor(extension, window, id) {
926     if (!extension.canAccessWindow(window)) {
927       throw new ExtensionError("extension cannot access window");
928     }
929     this.extension = extension;
930     this.window = window;
931     this.id = id;
932   }
934   /**
935    * @property {nsIAppWindow} appWindow
936    *        The nsIAppWindow object for this browser window.
937    *        @readonly
938    */
939   get appWindow() {
940     return this.window.docShell.treeOwner
941       .QueryInterface(Ci.nsIInterfaceRequestor)
942       .getInterface(Ci.nsIAppWindow);
943   }
945   /**
946    * Returns true if this window is the current window for the given extension
947    * context, false otherwise.
948    *
949    * @param {BaseContext} context
950    *        The extension context for which to perform the check.
951    *
952    * @returns {boolean}
953    */
954   isCurrentFor(context) {
955     if (context && context.currentWindow) {
956       return this.window === context.currentWindow;
957     }
958     return this.isLastFocused;
959   }
961   /**
962    * @property {string} type
963    *        The type of the window, as defined by the WebExtension API. May be
964    *        either "normal" or "popup".
965    *        @readonly
966    */
967   get type() {
968     let { chromeFlags } = this.appWindow;
970     if (chromeFlags & Ci.nsIWebBrowserChrome.CHROME_OPENAS_DIALOG) {
971       return "popup";
972     }
974     return "normal";
975   }
977   /**
978    * Converts this window object to a JSON-compatible object which may be
979    * returned to an extension, in the format required to be returned by
980    * WebExtension APIs.
981    *
982    * @param {object} [getInfo]
983    *        An optional object, the properties of which determine what data is
984    *        available on the result object.
985    * @param {boolean} [getInfo.populate]
986    *        Of true, the result object will contain a `tabs` property,
987    *        containing an array of converted Tab objects, one for each tab in
988    *        the window.
989    *
990    * @returns {object}
991    */
992   convert(getInfo) {
993     let result = {
994       id: this.id,
995       focused: this.focused,
996       top: this.top,
997       left: this.left,
998       width: this.width,
999       height: this.height,
1000       incognito: this.incognito,
1001       type: this.type,
1002       state: this.state,
1003       alwaysOnTop: this.alwaysOnTop,
1004       title: this.title,
1005     };
1007     if (getInfo && getInfo.populate) {
1008       result.tabs = Array.from(this.getTabs(), tab => tab.convert());
1009     }
1011     return result;
1012   }
1014   /**
1015    * Returns true if this window matches the the given query info object. Omitted
1016    * or null have no effect on the match.
1017    *
1018    * @param {object} queryInfo
1019    *        The query info against which to match.
1020    * @param {boolean} [queryInfo.currentWindow]
1021    *        Matches against against the return value of `isCurrentFor()` for the
1022    *        given context.
1023    * @param {boolean} [queryInfo.lastFocusedWindow]
1024    *        Matches against the exact value of the window's `isLastFocused` attribute.
1025    * @param {boolean} [queryInfo.windowId]
1026    *        Matches against the exact value of the window's ID, taking into
1027    *        account the special WINDOW_ID_CURRENT value.
1028    * @param {string} [queryInfo.windowType]
1029    *        Matches against the exact value of the window's `type` attribute.
1030    * @param {BaseContext} context
1031    *        The extension context for which the matching is being performed.
1032    *        Used to determine the current window for relevant properties.
1033    *
1034    * @returns {boolean}
1035    *        True if the window matches the query.
1036    */
1037   matches(queryInfo, context) {
1038     if (
1039       queryInfo.lastFocusedWindow !== null &&
1040       queryInfo.lastFocusedWindow !== this.isLastFocused
1041     ) {
1042       return false;
1043     }
1045     if (queryInfo.windowType !== null && queryInfo.windowType !== this.type) {
1046       return false;
1047     }
1049     if (queryInfo.windowId !== null) {
1050       if (queryInfo.windowId === WINDOW_ID_CURRENT) {
1051         if (!this.isCurrentFor(context)) {
1052           return false;
1053         }
1054       } else if (queryInfo.windowId !== this.id) {
1055         return false;
1056       }
1057     }
1059     if (
1060       queryInfo.currentWindow !== null &&
1061       queryInfo.currentWindow !== this.isCurrentFor(context)
1062     ) {
1063       return false;
1064     }
1066     return true;
1067   }
1069   /**
1070    * @property {boolean} focused
1071    *        Returns true if the browser window is currently focused.
1072    *        @readonly
1073    *        @abstract
1074    */
1075   get focused() {
1076     throw new Error("Not implemented");
1077   }
1079   /**
1080    * @property {integer} top
1081    *        Returns the pixel offset of the top of the window from the top of
1082    *        the screen.
1083    *        @readonly
1084    *        @abstract
1085    */
1086   get top() {
1087     throw new Error("Not implemented");
1088   }
1090   /**
1091    * @property {integer} left
1092    *        Returns the pixel offset of the left of the window from the left of
1093    *        the screen.
1094    *        @readonly
1095    *        @abstract
1096    */
1097   get left() {
1098     throw new Error("Not implemented");
1099   }
1101   /**
1102    * @property {integer} width
1103    *        Returns the pixel width of the window.
1104    *        @readonly
1105    *        @abstract
1106    */
1107   get width() {
1108     throw new Error("Not implemented");
1109   }
1111   /**
1112    * @property {integer} height
1113    *        Returns the pixel height of the window.
1114    *        @readonly
1115    *        @abstract
1116    */
1117   get height() {
1118     throw new Error("Not implemented");
1119   }
1121   /**
1122    * @property {boolean} incognito
1123    *        Returns true if this is a private browsing window, false otherwise.
1124    *        @readonly
1125    *        @abstract
1126    */
1127   get incognito() {
1128     throw new Error("Not implemented");
1129   }
1131   /**
1132    * @property {boolean} alwaysOnTop
1133    *        Returns true if this window is constrained to always remain above
1134    *        other windows.
1135    *        @readonly
1136    *        @abstract
1137    */
1138   get alwaysOnTop() {
1139     throw new Error("Not implemented");
1140   }
1142   /**
1143    * @property {boolean} isLastFocused
1144    *        Returns true if this is the browser window which most recently had
1145    *        focus.
1146    *        @readonly
1147    *        @abstract
1148    */
1149   get isLastFocused() {
1150     throw new Error("Not implemented");
1151   }
1153   /**
1154    * @property {string} state
1155    *        Returns or sets the current state of this window, as determined by
1156    *        `getState()`.
1157    *        @abstract
1158    */
1159   get state() {
1160     throw new Error("Not implemented");
1161   }
1163   set state(state) {
1164     throw new Error("Not implemented");
1165   }
1167   /**
1168    * @property {nsIURI | null} title
1169    *        Returns the current title of this window if the extension has permission
1170    *        to read it, or null otherwise.
1171    *        @readonly
1172    */
1173   get title() {
1174     // activeTab may be null when a new window is adopting an existing tab as its first tab
1175     // (See Bug 1458918 for rationale).
1176     if (this.activeTab && this.activeTab.hasTabPermission) {
1177       return this._title;
1178     }
1179   }
1181   // The JSDoc validator does not support @returns tags in abstract functions or
1182   // star functions without return statements.
1183   /* eslint-disable valid-jsdoc */
1184   /**
1185    * Returns the window state of the given window.
1186    *
1187    * @param {DOMWindow} window
1188    *        The window for which to return a state.
1189    *
1190    * @returns {string}
1191    *        The window's state. One of "normal", "minimized", "maximized",
1192    *        "fullscreen", or "docked".
1193    * @static
1194    * @abstract
1195    */
1196   static getState(window) {
1197     throw new Error("Not implemented");
1198   }
1200   /**
1201    * Returns an iterator of TabBase objects for each tab in this window.
1202    *
1203    * @returns {Iterator<TabBase>}
1204    */
1205   getTabs() {
1206     throw new Error("Not implemented");
1207   }
1209   /**
1210    * Returns an iterator of TabBase objects for each highlighted tab in this window.
1211    *
1212    * @returns {Iterator<TabBase>}
1213    */
1214   getHighlightedTabs() {
1215     throw new Error("Not implemented");
1216   }
1218   /**
1219    * @property {TabBase} The window's currently active tab.
1220    */
1221   get activeTab() {
1222     throw new Error("Not implemented");
1223   }
1225   /**
1226    * Returns the window's tab at the specified index.
1227    *
1228    * @param {integer} index
1229    *        The index of the desired tab.
1230    *
1231    * @returns {TabBase|undefined}
1232    */
1233   getTabAtIndex(index) {
1234     throw new Error("Not implemented");
1235   }
1236   /* eslint-enable valid-jsdoc */
1239 Object.assign(WindowBase, { WINDOW_ID_NONE, WINDOW_ID_CURRENT });
1242  * The parameter type of "tab-attached" events, which are emitted when a
1243  * pre-existing tab is attached to a new window.
1245  * @typedef {object} TabAttachedEvent
1246  * @property {NativeTab} tab
1247  *        The native tab object in the window to which the tab is being
1248  *        attached. This may be a different object than was used to represent
1249  *        the tab in the old window.
1250  * @property {integer} tabId
1251  *        The ID of the tab being attached.
1252  * @property {integer} newWindowId
1253  *        The ID of the window to which the tab is being attached.
1254  * @property {integer} newPosition
1255  *        The position of the tab in the tab list of the new window.
1256  */
1259  * The parameter type of "tab-detached" events, which are emitted when a
1260  * pre-existing tab is detached from a window, in order to be attached to a new
1261  * window.
1263  * @typedef {object} TabDetachedEvent
1264  * @property {NativeTab} tab
1265  *        The native tab object in the window from which the tab is being
1266  *        detached. This may be a different object than will be used to
1267  *        represent the tab in the new window.
1268  * @property {NativeTab} adoptedBy
1269  *        The native tab object in the window to which the tab will be attached,
1270  *        and is adopting the contents of this tab. This may be a different
1271  *        object than the tab in the previous window.
1272  * @property {integer} tabId
1273  *        The ID of the tab being detached.
1274  * @property {integer} oldWindowId
1275  *        The ID of the window from which the tab is being detached.
1276  * @property {integer} oldPosition
1277  *        The position of the tab in the tab list of the window from which it is
1278  *        being detached.
1279  */
1282  * The parameter type of "tab-created" events, which are emitted when a
1283  * new tab is created.
1285  * @typedef {object} TabCreatedEvent
1286  * @property {NativeTab} tab
1287  *        The native tab object for the tab which is being created.
1288  */
1291  * The parameter type of "tab-removed" events, which are emitted when a
1292  * tab is removed and destroyed.
1294  * @typedef {object} TabRemovedEvent
1295  * @property {NativeTab} tab
1296  *        The native tab object for the tab which is being removed.
1297  * @property {integer} tabId
1298  *        The ID of the tab being removed.
1299  * @property {integer} windowId
1300  *        The ID of the window from which the tab is being removed.
1301  * @property {boolean} isWindowClosing
1302  *        True if the tab is being removed because the window is closing.
1303  */
1306  * An object containing basic, extension-independent information about the window
1307  * and tab that a XUL <browser> belongs to.
1309  * @typedef {object} BrowserData
1310  * @property {integer} tabId
1311  *        The numeric ID of the tab that a <browser> belongs to, or -1 if it
1312  *        does not belong to a tab.
1313  * @property {integer} windowId
1314  *        The numeric ID of the browser window that a <browser> belongs to, or -1
1315  *        if it does not belong to a browser window.
1316  */
1319  * A platform-independent base class for the platform-specific TabTracker
1320  * classes, which track the opening and closing of tabs, and manage the mapping
1321  * of them between numeric IDs and native tab objects.
1323  * Instances of this class are EventEmitters which emit the following events,
1324  * each with an argument of the given type:
1326  * - "tab-attached" {@link TabAttacheEvent}
1327  * - "tab-detached" {@link TabDetachedEvent}
1328  * - "tab-created" {@link TabCreatedEvent}
1329  * - "tab-removed" {@link TabRemovedEvent}
1330  */
1331 class TabTrackerBase extends EventEmitter {
1332   on(...args) {
1333     if (!this.initialized) {
1334       this.init();
1335     }
1337     return super.on(...args); // eslint-disable-line mozilla/balanced-listeners
1338   }
1340   /**
1341    * Called to initialize the tab tracking listeners the first time that an
1342    * event listener is added.
1343    *
1344    * @protected
1345    * @abstract
1346    */
1347   init() {
1348     throw new Error("Not implemented");
1349   }
1351   // The JSDoc validator does not support @returns tags in abstract functions or
1352   // star functions without return statements.
1353   /* eslint-disable valid-jsdoc */
1354   /**
1355    * Returns the numeric ID for the given native tab.
1356    *
1357    * @param {NativeTab} nativeTab
1358    *        The native tab for which to return an ID.
1359    *
1360    * @returns {integer}
1361    *        The tab's numeric ID.
1362    * @abstract
1363    */
1364   getId(nativeTab) {
1365     throw new Error("Not implemented");
1366   }
1368   /**
1369    * Returns the native tab with the given numeric ID.
1370    *
1371    * @param {integer} tabId
1372    *        The numeric ID of the tab to return.
1373    * @param {*} default_
1374    *        The value to return if no tab exists with the given ID.
1375    *
1376    * @returns {NativeTab}
1377    * @throws {ExtensionError}
1378    *       If no tab exists with the given ID and a default return value is not
1379    *       provided.
1380    * @abstract
1381    */
1382   getTab(tabId, default_ = undefined) {
1383     throw new Error("Not implemented");
1384   }
1386   /**
1387    * Returns basic information about the tab and window that the given browser
1388    * belongs to.
1389    *
1390    * @param {XULElement} browser
1391    *        The XUL browser element for which to return data.
1392    *
1393    * @returns {BrowserData}
1394    * @abstract
1395    */
1396   /* eslint-enable valid-jsdoc */
1397   getBrowserData(browser) {
1398     throw new Error("Not implemented");
1399   }
1401   /**
1402    * @property {NativeTab} activeTab
1403    *        Returns the native tab object for the active tab in the
1404    *        most-recently focused window, or null if no live tabs currently
1405    *        exist.
1406    *        @abstract
1407    */
1408   get activeTab() {
1409     throw new Error("Not implemented");
1410   }
1414  * A browser progress listener instance which calls a given listener function
1415  * whenever the status of the given browser changes.
1417  * @param {function(object)} listener
1418  *        A function to be called whenever the status of a tab's top-level
1419  *        browser. It is passed an object with a `browser` property pointing to
1420  *        the XUL browser, and a `status` property with a string description of
1421  *        the browser's status.
1422  * @private
1423  */
1424 class StatusListener {
1425   constructor(listener) {
1426     this.listener = listener;
1427   }
1429   onStateChange(browser, webProgress, request, stateFlags, statusCode) {
1430     if (!webProgress.isTopLevel) {
1431       return;
1432     }
1434     let status;
1435     if (stateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW) {
1436       if (stateFlags & Ci.nsIWebProgressListener.STATE_START) {
1437         status = "loading";
1438       } else if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
1439         status = "complete";
1440       }
1441     } else if (
1442       stateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
1443       statusCode == Cr.NS_BINDING_ABORTED
1444     ) {
1445       status = "complete";
1446     }
1448     if (status) {
1449       this.listener({ browser, status });
1450     }
1451   }
1453   onLocationChange(browser, webProgress, request, locationURI, flags) {
1454     if (webProgress.isTopLevel) {
1455       let status = webProgress.isLoadingDocument ? "loading" : "complete";
1456       this.listener({ browser, status, url: locationURI.spec });
1457     }
1458   }
1462  * A platform-independent base class for the platform-specific WindowTracker
1463  * classes, which track the opening and closing of windows, and manage the
1464  * mapping of them between numeric IDs and native tab objects.
1465  */
1466 class WindowTrackerBase extends EventEmitter {
1467   constructor() {
1468     super();
1470     this._handleWindowOpened = this._handleWindowOpened.bind(this);
1472     this._openListeners = new Set();
1473     this._closeListeners = new Set();
1475     this._listeners = new DefaultMap(() => new Set());
1477     this._statusListeners = new DefaultWeakMap(listener => {
1478       return new StatusListener(listener);
1479     });
1481     this._windowIds = new DefaultWeakMap(window => {
1482       return window.docShell.outerWindowID;
1483     });
1484   }
1486   isBrowserWindow(window) {
1487     let { documentElement } = window.document;
1489     return documentElement.getAttribute("windowtype") === "navigator:browser";
1490   }
1492   // The JSDoc validator does not support @returns tags in abstract functions or
1493   // star functions without return statements.
1494   /* eslint-disable valid-jsdoc */
1495   /**
1496    * Returns an iterator for all currently active browser windows.
1497    *
1498    * @param {boolean} [includeInomplete = false]
1499    *        If true, include browser windows which are not yet fully loaded.
1500    *        Otherwise, only include windows which are.
1501    *
1502    * @returns {Iterator<DOMWindow>}
1503    */
1504   /* eslint-enable valid-jsdoc */
1505   *browserWindows(includeIncomplete = false) {
1506     // The window type parameter is only available once the window's document
1507     // element has been created. This means that, when looking for incomplete
1508     // browser windows, we need to ignore the type entirely for windows which
1509     // haven't finished loading, since we would otherwise skip browser windows
1510     // in their early loading stages.
1511     // This is particularly important given that the "domwindowcreated" event
1512     // fires for browser windows when they're in that in-between state, and just
1513     // before we register our own "domwindowcreated" listener.
1515     for (let window of Services.wm.getEnumerator("")) {
1516       let ok = includeIncomplete;
1517       if (window.document.readyState === "complete") {
1518         ok = this.isBrowserWindow(window);
1519       }
1521       if (ok) {
1522         yield window;
1523       }
1524     }
1525   }
1527   /**
1528    * @property {DOMWindow|null} topWindow
1529    *        The currently active, or topmost, browser window, or null if no
1530    *        browser window is currently open.
1531    *        @readonly
1532    */
1533   get topWindow() {
1534     return Services.wm.getMostRecentWindow("navigator:browser");
1535   }
1537   /**
1538    * @property {DOMWindow|null} topWindow
1539    *        The currently active, or topmost, browser window that is not
1540    *        private browsing, or null if no browser window is currently open.
1541    *        @readonly
1542    */
1543   get topNonPBWindow() {
1544     return Services.wm.getMostRecentNonPBWindow("navigator:browser");
1545   }
1547   /**
1548    * Returns the top window accessible by the extension.
1549    *
1550    * @param {BaseContext} context
1551    *        The extension context for which to return the current window.
1552    *
1553    * @returns {DOMWindow|null}
1554    */
1555   getTopWindow(context) {
1556     if (context && !context.privateBrowsingAllowed) {
1557       return this.topNonPBWindow;
1558     }
1559     return this.topWindow;
1560   }
1562   /**
1563    * Returns the numeric ID for the given browser window.
1564    *
1565    * @param {DOMWindow} window
1566    *        The DOM window for which to return an ID.
1567    *
1568    * @returns {integer}
1569    *        The window's numeric ID.
1570    */
1571   getId(window) {
1572     return this._windowIds.get(window);
1573   }
1575   /**
1576    * Returns the browser window to which the given context belongs, or the top
1577    * browser window if the context does not belong to a browser window.
1578    *
1579    * @param {BaseContext} context
1580    *        The extension context for which to return the current window.
1581    *
1582    * @returns {DOMWindow|null}
1583    */
1584   getCurrentWindow(context) {
1585     return (context && context.currentWindow) || this.getTopWindow(context);
1586   }
1588   /**
1589    * Returns the browser window with the given ID.
1590    *
1591    * @param {integer} id
1592    *        The ID of the window to return.
1593    * @param {BaseContext} context
1594    *        The extension context for which the matching is being performed.
1595    *        Used to determine the current window for relevant properties.
1596    * @param {boolean} [strict = true]
1597    *        If false, undefined will be returned instead of throwing an error
1598    *        in case no window exists with the given ID.
1599    *
1600    * @returns {DOMWindow|undefined}
1601    * @throws {ExtensionError}
1602    *        If no window exists with the given ID and `strict` is true.
1603    */
1604   getWindow(id, context, strict = true) {
1605     if (id === WINDOW_ID_CURRENT) {
1606       return this.getCurrentWindow(context);
1607     }
1609     let window = Services.wm.getOuterWindowWithId(id);
1610     if (
1611       window &&
1612       !window.closed &&
1613       (window.document.readyState !== "complete" ||
1614         this.isBrowserWindow(window))
1615     ) {
1616       if (!context || context.canAccessWindow(window)) {
1617         // Tolerate incomplete windows because isBrowserWindow is only reliable
1618         // once the window is fully loaded.
1619         return window;
1620       }
1621     }
1623     if (strict) {
1624       throw new ExtensionError(`Invalid window ID: ${id}`);
1625     }
1626   }
1628   /**
1629    * @property {boolean} _haveListeners
1630    *        Returns true if any window open or close listeners are currently
1631    *        registered.
1632    * @private
1633    */
1634   get _haveListeners() {
1635     return this._openListeners.size > 0 || this._closeListeners.size > 0;
1636   }
1638   /**
1639    * Register the given listener function to be called whenever a new browser
1640    * window is opened.
1641    *
1642    * @param {function(DOMWindow)} listener
1643    *        The listener function to register.
1644    */
1645   addOpenListener(listener) {
1646     if (!this._haveListeners) {
1647       Services.ww.registerNotification(this);
1648     }
1650     this._openListeners.add(listener);
1652     for (let window of this.browserWindows(true)) {
1653       if (window.document.readyState !== "complete") {
1654         window.addEventListener("load", this);
1655       }
1656     }
1657   }
1659   /**
1660    * Unregister a listener function registered in a previous addOpenListener
1661    * call.
1662    *
1663    * @param {function(DOMWindow)} listener
1664    *        The listener function to unregister.
1665    */
1666   removeOpenListener(listener) {
1667     this._openListeners.delete(listener);
1669     if (!this._haveListeners) {
1670       Services.ww.unregisterNotification(this);
1671     }
1672   }
1674   /**
1675    * Register the given listener function to be called whenever a browser
1676    * window is closed.
1677    *
1678    * @param {function(DOMWindow)} listener
1679    *        The listener function to register.
1680    */
1681   addCloseListener(listener) {
1682     if (!this._haveListeners) {
1683       Services.ww.registerNotification(this);
1684     }
1686     this._closeListeners.add(listener);
1687   }
1689   /**
1690    * Unregister a listener function registered in a previous addCloseListener
1691    * call.
1692    *
1693    * @param {function(DOMWindow)} listener
1694    *        The listener function to unregister.
1695    */
1696   removeCloseListener(listener) {
1697     this._closeListeners.delete(listener);
1699     if (!this._haveListeners) {
1700       Services.ww.unregisterNotification(this);
1701     }
1702   }
1704   /**
1705    * Handles load events for recently-opened windows, and adds additional
1706    * listeners which may only be safely added when the window is fully loaded.
1707    *
1708    * @param {Event} event
1709    *        A DOM event to handle.
1710    * @private
1711    */
1712   handleEvent(event) {
1713     if (event.type === "load") {
1714       event.currentTarget.removeEventListener(event.type, this);
1716       let window = event.target.defaultView;
1717       if (!this.isBrowserWindow(window)) {
1718         return;
1719       }
1721       for (let listener of this._openListeners) {
1722         try {
1723           listener(window);
1724         } catch (e) {
1725           Cu.reportError(e);
1726         }
1727       }
1728     }
1729   }
1731   /**
1732    * Observes "domwindowopened" and "domwindowclosed" events, notifies the
1733    * appropriate listeners, and adds necessary additional listeners to the new
1734    * windows.
1735    *
1736    * @param {DOMWindow} window
1737    *        A DOM window.
1738    * @param {string} topic
1739    *        The topic being observed.
1740    * @private
1741    */
1742   observe(window, topic) {
1743     if (topic === "domwindowclosed") {
1744       if (!this.isBrowserWindow(window)) {
1745         return;
1746       }
1748       window.removeEventListener("load", this);
1749       for (let listener of this._closeListeners) {
1750         try {
1751           listener(window);
1752         } catch (e) {
1753           Cu.reportError(e);
1754         }
1755       }
1756     } else if (topic === "domwindowopened") {
1757       window.addEventListener("load", this);
1758     }
1759   }
1761   /**
1762    * Add an event listener to be called whenever the given DOM event is received
1763    * at the top level of any browser window.
1764    *
1765    * @param {string} type
1766    *        The type of event to listen for. May be any valid DOM event name, or
1767    *        one of the following special cases:
1768    *
1769    *        - "progress": Adds a tab progress listener to every browser window.
1770    *        - "status": Adds a StatusListener to every tab of every browser
1771    *           window.
1772    *        - "domwindowopened": Acts as an alias for addOpenListener.
1773    *        - "domwindowclosed": Acts as an alias for addCloseListener.
1774    * @param {Function | object} listener
1775    *        The listener to invoke in response to the given events.
1776    *
1777    * @returns {undefined}
1778    */
1779   addListener(type, listener) {
1780     if (type === "domwindowopened") {
1781       return this.addOpenListener(listener);
1782     } else if (type === "domwindowclosed") {
1783       return this.addCloseListener(listener);
1784     }
1786     if (this._listeners.size === 0) {
1787       this.addOpenListener(this._handleWindowOpened);
1788     }
1790     if (type === "status") {
1791       listener = this._statusListeners.get(listener);
1792       type = "progress";
1793     }
1795     this._listeners.get(type).add(listener);
1797     // Register listener on all existing windows.
1798     for (let window of this.browserWindows()) {
1799       this._addWindowListener(window, type, listener);
1800     }
1801   }
1803   /**
1804    * Removes an event listener previously registered via an addListener call.
1805    *
1806    * @param {string} type
1807    *        The type of event to stop listening for.
1808    * @param {Function | object} listener
1809    *        The listener to remove.
1810    *
1811    * @returns {undefined}
1812    */
1813   removeListener(type, listener) {
1814     if (type === "domwindowopened") {
1815       return this.removeOpenListener(listener);
1816     } else if (type === "domwindowclosed") {
1817       return this.removeCloseListener(listener);
1818     }
1820     if (type === "status") {
1821       listener = this._statusListeners.get(listener);
1822       type = "progress";
1823     }
1825     let listeners = this._listeners.get(type);
1826     listeners.delete(listener);
1828     if (listeners.size === 0) {
1829       this._listeners.delete(type);
1830       if (this._listeners.size === 0) {
1831         this.removeOpenListener(this._handleWindowOpened);
1832       }
1833     }
1835     // Unregister listener from all existing windows.
1836     let useCapture = type === "focus" || type === "blur";
1837     for (let window of this.browserWindows()) {
1838       if (type === "progress") {
1839         this.removeProgressListener(window, listener);
1840       } else {
1841         window.removeEventListener(type, listener, useCapture);
1842       }
1843     }
1844   }
1846   /**
1847    * Adds a listener for the given event to the given window.
1848    *
1849    * @param {DOMWindow} window
1850    *        The browser window to which to add the listener.
1851    * @param {string} eventType
1852    *        The type of DOM event to listen for, or "progress" to add a tab
1853    *        progress listener.
1854    * @param {Function | object} listener
1855    *        The listener to add.
1856    * @private
1857    */
1858   _addWindowListener(window, eventType, listener) {
1859     let useCapture = eventType === "focus" || eventType === "blur";
1861     if (eventType === "progress") {
1862       this.addProgressListener(window, listener);
1863     } else {
1864       window.addEventListener(eventType, listener, useCapture);
1865     }
1866   }
1868   /**
1869    * A private method which is called whenever a new browser window is opened,
1870    * and adds the necessary listeners to it.
1871    *
1872    * @param {DOMWindow} window
1873    *        The window being opened.
1874    * @private
1875    */
1876   _handleWindowOpened(window) {
1877     for (let [eventType, listeners] of this._listeners) {
1878       for (let listener of listeners) {
1879         this._addWindowListener(window, eventType, listener);
1880       }
1881     }
1882   }
1884   /**
1885    * Adds a tab progress listener to the given browser window.
1886    *
1887    * @param {DOMWindow} window
1888    *        The browser window to which to add the listener.
1889    * @param {object} listener
1890    *        The tab progress listener to add.
1891    * @abstract
1892    */
1893   addProgressListener(window, listener) {
1894     throw new Error("Not implemented");
1895   }
1897   /**
1898    * Removes a tab progress listener from the given browser window.
1899    *
1900    * @param {DOMWindow} window
1901    *        The browser window from which to remove the listener.
1902    * @param {object} listener
1903    *        The tab progress listener to remove.
1904    * @abstract
1905    */
1906   removeProgressListener(window, listener) {
1907     throw new Error("Not implemented");
1908   }
1912  * Manages native tabs, their wrappers, and their dynamic permissions for a
1913  * particular extension.
1915  * @param {Extension} extension
1916  *        The extension for which to manage tabs.
1917  */
1918 class TabManagerBase {
1919   constructor(extension) {
1920     this.extension = extension;
1922     this._tabs = new DefaultWeakMap(tab => this.wrapTab(tab));
1923   }
1925   /**
1926    * If the extension has requested activeTab permission, grant it those
1927    * permissions for the current inner window in the given native tab.
1928    *
1929    * @param {NativeTab} nativeTab
1930    *        The native tab for which to grant permissions.
1931    */
1932   addActiveTabPermission(nativeTab) {
1933     let tab = this.getWrapper(nativeTab);
1934     if (
1935       this.extension.hasPermission("activeTab") ||
1936       (this.extension.originControls &&
1937         this.extension.optionalOrigins.matches(tab._uri))
1938     ) {
1939       // Note that, unlike Chrome, we don't currently clear this permission with
1940       // the tab navigates. If the inner window is revived from BFCache before
1941       // we've granted this permission to a new inner window, the extension
1942       // maintains its permissions for it.
1943       tab.activeTabWindowID = tab.innerWindowID;
1944     }
1945   }
1947   /**
1948    * Revoke the extension's activeTab permissions for the current inner window
1949    * of the given native tab.
1950    *
1951    * @param {NativeTab} nativeTab
1952    *        The native tab for which to revoke permissions.
1953    */
1954   revokeActiveTabPermission(nativeTab) {
1955     this.getWrapper(nativeTab).activeTabWindowID = null;
1956   }
1958   /**
1959    * Returns true if the extension has requested activeTab permission, and has
1960    * been granted permissions for the current inner window if this tab.
1961    *
1962    * @param {NativeTab} nativeTab
1963    *        The native tab for which to check permissions.
1964    * @returns {boolean}
1965    *        True if the extension has activeTab permissions for this tab.
1966    */
1967   hasActiveTabPermission(nativeTab) {
1968     return this.getWrapper(nativeTab).hasActiveTabPermission;
1969   }
1971   /**
1972    * Activate MV3 content scripts if the extension has activeTab or an
1973    * (ungranted) host permission.
1974    *
1975    * @param {NativeTab} nativeTab
1976    */
1977   activateScripts(nativeTab) {
1978     let tab = this.getWrapper(nativeTab);
1979     if (
1980       this.extension.originControls &&
1981       !tab.matchesHostPermission &&
1982       (this.extension.optionalOrigins.matches(tab._uri) ||
1983         this.extension.hasPermission("activeTab")) &&
1984       (this.extension.contentScripts.length ||
1985         this.extension.registeredContentScripts.size)
1986     ) {
1987       tab.queryContent("ActivateScripts", { id: this.extension.id });
1988     }
1989   }
1991   /**
1992    * Returns true if the extension has permissions to access restricted
1993    * properties of the given native tab. In practice, this means that it has
1994    * either requested the "tabs" permission or has activeTab permissions for the
1995    * given tab.
1996    *
1997    * NOTE: Never use this method on an object that is not a native tab
1998    * for the current platform: this method implicitly generates a wrapper
1999    * for the passed nativeTab parameter and the platform-specific tabTracker
2000    * instance is likely to store it in a map which is cleared only when the
2001    * tab is closed (and so, if nativeTab is not a real native tab, it will
2002    * never be cleared from the platform-specific tabTracker instance),
2003    * See Bug 1458918 for a rationale.
2004    *
2005    * @param {NativeTab} nativeTab
2006    *        The native tab for which to check permissions.
2007    * @returns {boolean}
2008    *        True if the extension has permissions for this tab.
2009    */
2010   hasTabPermission(nativeTab) {
2011     return this.getWrapper(nativeTab).hasTabPermission;
2012   }
2014   /**
2015    * Returns this extension's TabBase wrapper for the given native tab. This
2016    * method will always return the same wrapper object for any given native tab.
2017    *
2018    * @param {NativeTab} nativeTab
2019    *        The tab for which to return a wrapper.
2020    *
2021    * @returns {TabBase|undefined}
2022    *        The wrapper for this tab.
2023    */
2024   getWrapper(nativeTab) {
2025     if (this.canAccessTab(nativeTab)) {
2026       return this._tabs.get(nativeTab);
2027     }
2028   }
2030   /**
2031    * Determines access using extension context.
2032    *
2033    * @param {NativeTab} nativeTab
2034    *        The tab to check access on.
2035    * @returns {boolean}
2036    *        True if the extension has permissions for this tab.
2037    * @protected
2038    * @abstract
2039    */
2040   canAccessTab(nativeTab) {
2041     throw new Error("Not implemented");
2042   }
2044   /**
2045    * Converts the given native tab to a JSON-compatible object, in the format
2046    * required to be returned by WebExtension APIs, which may be safely passed to
2047    * extension code.
2048    *
2049    * @param {NativeTab} nativeTab
2050    *        The native tab to convert.
2051    * @param {object} [fallbackTabSize]
2052    *        A geometry data if the lazy geometry data for this tab hasn't been
2053    *        initialized yet.
2054    *
2055    * @returns {object}
2056    */
2057   convert(nativeTab, fallbackTabSize = null) {
2058     return this.getWrapper(nativeTab).convert(fallbackTabSize);
2059   }
2061   // The JSDoc validator does not support @returns tags in abstract functions or
2062   // star functions without return statements.
2063   /* eslint-disable valid-jsdoc */
2064   /**
2065    * Returns an iterator of TabBase objects which match the given query info.
2066    *
2067    * @param {object | null} [queryInfo = null]
2068    *        An object containing properties on which to filter. May contain any
2069    *        properties which are recognized by {@link TabBase#matches} or
2070    *        {@link WindowBase#matches}. Unknown properties will be ignored.
2071    * @param {BaseContext|null} [context = null]
2072    *        The extension context for which the matching is being performed.
2073    *        Used to determine the current window for relevant properties.
2074    *
2075    * @returns {Iterator<TabBase>}
2076    */
2077   *query(queryInfo = null, context = null) {
2078     if (queryInfo) {
2079       if (queryInfo.url !== null) {
2080         queryInfo.url = parseMatchPatterns([].concat(queryInfo.url), {
2081           restrictSchemes: false,
2082         });
2083       }
2085       if (queryInfo.cookieStoreId !== null) {
2086         queryInfo.cookieStoreId = [].concat(queryInfo.cookieStoreId);
2087       }
2089       if (queryInfo.title !== null) {
2090         try {
2091           queryInfo.title = new MatchGlob(queryInfo.title);
2092         } catch (e) {
2093           throw new ExtensionError(`Invalid title: ${queryInfo.title}`);
2094         }
2095       }
2096     }
2097     function* candidates(windowWrapper) {
2098       if (queryInfo) {
2099         let { active, highlighted, index } = queryInfo;
2100         if (active === true) {
2101           let { activeTab } = windowWrapper;
2102           if (activeTab) {
2103             yield activeTab;
2104           }
2105           return;
2106         }
2107         if (index != null) {
2108           let tabWrapper = windowWrapper.getTabAtIndex(index);
2109           if (tabWrapper) {
2110             yield tabWrapper;
2111           }
2112           return;
2113         }
2114         if (highlighted === true) {
2115           yield* windowWrapper.getHighlightedTabs();
2116           return;
2117         }
2118       }
2119       yield* windowWrapper.getTabs();
2120     }
2121     let windowWrappers = this.extension.windowManager.query(queryInfo, context);
2122     for (let windowWrapper of windowWrappers) {
2123       for (let tabWrapper of candidates(windowWrapper)) {
2124         if (!queryInfo || tabWrapper.matches(queryInfo)) {
2125           yield tabWrapper;
2126         }
2127       }
2128     }
2129   }
2131   /**
2132    * Returns a TabBase wrapper for the tab with the given ID.
2133    *
2134    * @param {integer} tabId
2135    *        The ID of the tab for which to return a wrapper.
2136    *
2137    * @returns {TabBase}
2138    * @throws {ExtensionError}
2139    *        If no tab exists with the given ID.
2140    * @abstract
2141    */
2142   get(tabId) {
2143     throw new Error("Not implemented");
2144   }
2146   /**
2147    * Returns a new TabBase instance wrapping the given native tab.
2148    *
2149    * @param {NativeTab} nativeTab
2150    *        The native tab for which to return a wrapper.
2151    *
2152    * @returns {TabBase}
2153    * @protected
2154    * @abstract
2155    */
2156   /* eslint-enable valid-jsdoc */
2157   wrapTab(nativeTab) {
2158     throw new Error("Not implemented");
2159   }
2163  * Manages native browser windows and their wrappers for a particular extension.
2165  * @param {Extension} extension
2166  *        The extension for which to manage windows.
2167  */
2168 class WindowManagerBase {
2169   constructor(extension) {
2170     this.extension = extension;
2172     this._windows = new DefaultWeakMap(window => this.wrapWindow(window));
2173   }
2175   /**
2176    * Converts the given browser window to a JSON-compatible object, in the
2177    * format required to be returned by WebExtension APIs, which may be safely
2178    * passed to extension code.
2179    *
2180    * @param {DOMWindow} window
2181    *        The browser window to convert.
2182    * @param {*} args
2183    *        Additional arguments to be passed to {@link WindowBase#convert}.
2184    *
2185    * @returns {object}
2186    */
2187   convert(window, ...args) {
2188     return this.getWrapper(window).convert(...args);
2189   }
2191   /**
2192    * Returns this extension's WindowBase wrapper for the given browser window.
2193    * This method will always return the same wrapper object for any given
2194    * browser window.
2195    *
2196    * @param {DOMWindow} window
2197    *        The browser window for which to return a wrapper.
2198    *
2199    * @returns {WindowBase|undefined}
2200    *        The wrapper for this tab.
2201    */
2202   getWrapper(window) {
2203     if (this.extension.canAccessWindow(window)) {
2204       return this._windows.get(window);
2205     }
2206   }
2208   /**
2209    * Returns whether this window can be accessed by the extension in the given
2210    * context.
2211    *
2212    * @param {DOMWindow} window
2213    *        The browser window that is being tested
2214    * @param {BaseContext|null} context
2215    *        The extension context for which this test is being performed.
2216    * @returns {boolean}
2217    */
2218   canAccessWindow(window, context) {
2219     return (
2220       (context && context.canAccessWindow(window)) ||
2221       this.extension.canAccessWindow(window)
2222     );
2223   }
2225   // The JSDoc validator does not support @returns tags in abstract functions or
2226   // star functions without return statements.
2227   /* eslint-disable valid-jsdoc */
2228   /**
2229    * Returns an iterator of WindowBase objects which match the given query info.
2230    *
2231    * @param {object | null} [queryInfo = null]
2232    *        An object containing properties on which to filter. May contain any
2233    *        properties which are recognized by {@link WindowBase#matches}.
2234    *        Unknown properties will be ignored.
2235    * @param {BaseContext|null} [context = null]
2236    *        The extension context for which the matching is being performed.
2237    *        Used to determine the current window for relevant properties.
2238    *
2239    * @returns {Iterator<WindowBase>}
2240    */
2241   *query(queryInfo = null, context = null) {
2242     function* candidates(windowManager) {
2243       if (queryInfo) {
2244         let { currentWindow, windowId, lastFocusedWindow } = queryInfo;
2245         if (currentWindow === true && windowId == null) {
2246           windowId = WINDOW_ID_CURRENT;
2247         }
2248         if (windowId != null) {
2249           let window = global.windowTracker.getWindow(windowId, context, false);
2250           if (window) {
2251             yield windowManager.getWrapper(window);
2252           }
2253           return;
2254         }
2255         if (lastFocusedWindow === true) {
2256           let window = global.windowTracker.getTopWindow(context);
2257           if (window) {
2258             yield windowManager.getWrapper(window);
2259           }
2260           return;
2261         }
2262       }
2263       yield* windowManager.getAll(context);
2264     }
2265     for (let windowWrapper of candidates(this)) {
2266       if (!queryInfo || windowWrapper.matches(queryInfo, context)) {
2267         yield windowWrapper;
2268       }
2269     }
2270   }
2272   /**
2273    * Returns a WindowBase wrapper for the browser window with the given ID.
2274    *
2275    * @param {integer} windowId
2276    *        The ID of the browser window for which to return a wrapper.
2277    * @param {BaseContext} context
2278    *        The extension context for which the matching is being performed.
2279    *        Used to determine the current window for relevant properties.
2280    *
2281    * @returns {WindowBase}
2282    * @throws {ExtensionError}
2283    *        If no window exists with the given ID.
2284    * @abstract
2285    */
2286   get(windowId, context) {
2287     throw new Error("Not implemented");
2288   }
2290   /**
2291    * Returns an iterator of WindowBase wrappers for each currently existing
2292    * browser window.
2293    *
2294    * @returns {Iterator<WindowBase>}
2295    * @abstract
2296    */
2297   getAll() {
2298     throw new Error("Not implemented");
2299   }
2301   /**
2302    * Returns a new WindowBase instance wrapping the given browser window.
2303    *
2304    * @param {DOMWindow} window
2305    *        The browser window for which to return a wrapper.
2306    *
2307    * @returns {WindowBase}
2308    * @protected
2309    * @abstract
2310    */
2311   wrapWindow(window) {
2312     throw new Error("Not implemented");
2313   }
2314   /* eslint-enable valid-jsdoc */
2317 function getUserContextIdForCookieStoreId(
2318   extension,
2319   cookieStoreId,
2320   isPrivateBrowsing
2321 ) {
2322   if (!extension.hasPermission("cookies")) {
2323     throw new ExtensionError(
2324       `No permission for cookieStoreId: ${cookieStoreId}`
2325     );
2326   }
2328   if (!isValidCookieStoreId(cookieStoreId)) {
2329     throw new ExtensionError(`Illegal cookieStoreId: ${cookieStoreId}`);
2330   }
2332   if (isPrivateBrowsing && !isPrivateCookieStoreId(cookieStoreId)) {
2333     throw new ExtensionError(
2334       `Illegal to set non-private cookieStoreId in a private window`
2335     );
2336   }
2338   if (!isPrivateBrowsing && isPrivateCookieStoreId(cookieStoreId)) {
2339     throw new ExtensionError(
2340       `Illegal to set private cookieStoreId in a non-private window`
2341     );
2342   }
2344   if (isContainerCookieStoreId(cookieStoreId)) {
2345     if (PrivateBrowsingUtils.permanentPrivateBrowsing) {
2346       // Container tabs are not supported in perma-private browsing mode - bug 1320757
2347       throw new ExtensionError(
2348         `Contextual identities are unavailable in permanent private browsing mode`
2349       );
2350     }
2351     if (!containersEnabled) {
2352       throw new ExtensionError(`Contextual identities are currently disabled`);
2353     }
2354     let userContextId = getContainerForCookieStoreId(cookieStoreId);
2355     if (!userContextId) {
2356       throw new ExtensionError(
2357         `No cookie store exists with ID ${cookieStoreId}`
2358       );
2359     }
2360     if (!extension.canAccessContainer(userContextId)) {
2361       throw new ExtensionError(`Cannot access ${cookieStoreId}`);
2362     }
2363     return userContextId;
2364   }
2366   return Services.scriptSecurityManager.DEFAULT_USER_CONTEXT_ID;
2369 Object.assign(global, {
2370   TabTrackerBase,
2371   TabManagerBase,
2372   TabBase,
2373   WindowTrackerBase,
2374   WindowManagerBase,
2375   WindowBase,
2376   getUserContextIdForCookieStoreId,