Source code

Revision control

Copy as Markdown

Other Tools

/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
/* globals EventEmitter */
ChromeUtils.defineESModuleGetters(this, {
});
XPCOMUtils.defineLazyPreferenceGetter(
this,
"containersEnabled",
"privacy.userContext.enabled"
);
var { DefaultMap, DefaultWeakMap, ExtensionError, parseMatchPatterns } =
ExtensionUtils;
var { defineLazyGetter } = ExtensionCommon;
/**
* The platform-specific type of native tab objects, which are wrapped by
* TabBase instances.
*
* @typedef {object | XULElement} NativeTab
*/
/**
* @typedef {object} MutedInfo
* @property {boolean} muted
* True if the tab is currently muted, false otherwise.
* @property {string} [reason]
* The reason the tab is muted. Either "user", if the tab was muted by a
* user, or "extension", if it was muted by an extension.
* @property {string} [extensionId]
* If the tab was muted by an extension, contains the internal ID of that
* extension.
*/
/**
* A platform-independent base class for extension-specific wrappers around
* native tab objects.
*
* @param {Extension} extension
* The extension object for which this wrapper is being created. Used to
* determine permissions for access to certain properties and
* functionality.
* @param {NativeTab} nativeTab
* The native tab object which is being wrapped. The type of this object
* varies by platform.
* @param {integer} id
* The numeric ID of this tab object. This ID should be the same for
* every extension, and for the lifetime of the tab.
*/
class TabBase {
constructor(extension, nativeTab, id) {
this.extension = extension;
this.tabManager = extension.tabManager;
this.id = id;
this.nativeTab = nativeTab;
this.activeTabWindowID = null;
if (!extension.privateBrowsingAllowed && this._incognito) {
throw new ExtensionError(`Invalid tab ID: ${id}`);
}
}
/**
* Capture the visible area of this tab, and return the result as a data: URI.
*
* @param {BaseContext} context
* The extension context for which to perform the capture.
* @param {number} zoom
* The current zoom for the page.
* @param {object} [options]
* The options with which to perform the capture.
* @param {string} [options.format = "png"]
* The image format in which to encode the captured data. May be one of
* "png" or "jpeg".
* @param {integer} [options.quality = 92]
* The quality at which to encode the captured image data, ranging from
* 0 to 100. Has no effect for the "png" format.
* @param {DOMRectInit} [options.rect]
* Area of the document to render, in CSS pixels, relative to the page.
* If null, the currently visible viewport is rendered.
* @param {number} [options.scale]
* The scale to render at, defaults to devicePixelRatio.
* @returns {Promise<string>}
*/
async capture(context, zoom, options) {
let win = this.browser.ownerGlobal;
let scale = options?.scale || win.devicePixelRatio;
let rect = options?.rect && win.DOMRect.fromRect(options.rect);
// We only allow mozilla addons to use the resetScrollPosition option,
// since it's not standardized.
let resetScrollPosition = false;
if (!context.extension.restrictSchemes) {
resetScrollPosition = !!options?.resetScrollPosition;
}
let wgp = this.browsingContext.currentWindowGlobal;
let image = await wgp.drawSnapshot(
rect,
scale * zoom,
"white",
resetScrollPosition
);
let doc = Services.appShell.hiddenDOMWindow.document;
let canvas = doc.createElement("canvas");
canvas.width = image.width;
canvas.height = image.height;
let ctx = canvas.getContext("2d", { alpha: false });
ctx.drawImage(image, 0, 0);
image.close();
return canvas.toDataURL(`image/${options?.format}`, options?.quality / 100);
}
/**
* @property {integer | null} innerWindowID
* The last known innerWindowID loaded into this tab's docShell. This
* property must remain in sync with the last known values of
* properties such as `url` and `title`. Any operations on the content
* of an out-of-process tab will automatically fail if the
* innerWindowID of the tab when the message is received does not match
* the value of this property when the message was sent.
* @readonly
*/
get innerWindowID() {
return this.browser.innerWindowID;
}
/**
* @property {boolean} hasTabPermission
* Returns true if the extension has permission to access restricted
* properties of this tab, such as `url`, `title`, and `favIconUrl`.
* @readonly
*/
get hasTabPermission() {
return (
this.extension.hasPermission("tabs") ||
this.hasActiveTabPermission ||
this.matchesHostPermission
);
}
/**
* @property {boolean} hasActiveTabPermission
* Returns true if the extension has the "activeTab" permission, and
* has been granted access to this tab due to a user executing an
* extension action.
*
* If true, the extension may load scripts and CSS into this tab, and
* access restricted properties, such as its `url`.
* @readonly
*/
get hasActiveTabPermission() {
return (
(this.extension.originControls ||
this.extension.hasPermission("activeTab")) &&
this.activeTabWindowID != null &&
this.activeTabWindowID === this.innerWindowID
);
}
/**
* @property {boolean} matchesHostPermission
* Returns true if the extensions host permissions match the current tab url.
* @readonly
*/
get matchesHostPermission() {
return this.extension.allowedOrigins.matches(this._uri);
}
/**
* @property {boolean} incognito
* Returns true if this is a private browsing tab, false otherwise.
* @readonly
*/
get _incognito() {
return PrivateBrowsingUtils.isBrowserPrivate(this.browser);
}
/**
* @property {string} _url
* Returns the current URL of this tab. Does not do any permission
* checks.
* @readonly
*/
get _url() {
return this.browser.currentURI.spec;
}
/**
* @property {string | undefined} url
* Returns the current URL of this tab if the extension has permission
* to read it, or undefined otherwise.
* @readonly
*/
get url() {
if (this.hasTabPermission) {
return this._url;
}
return undefined;
}
/**
* @property {nsIURI} _uri
* Returns the current URI of this tab.
* @readonly
*/
get _uri() {
return this.browser.currentURI;
}
/**
* @property {string} _title
* Returns the current title of this tab. Does not do any permission
* checks.
* @readonly
*/
get _title() {
return this.browser.contentTitle || this.nativeTab.label;
}
/**
* @property {nsIURI | undefined} title
* Returns the current title of this tab if the extension has permission
* to read it, or undefined otherwise.
* @readonly
*/
get title() {
if (this.hasTabPermission) {
return this._title;
}
return undefined;
}
/**
* @property {string} _favIconUrl
* Returns the current favicon URL of this tab. Does not do any permission
* checks.
* @readonly
* @abstract
*/
get _favIconUrl() {
throw new Error("Not implemented");
}
/**
* @property {nsIURI | undefined} faviconUrl
* Returns the current faviron URL of this tab if the extension has permission
* to read it, or undefined otherwise.
* @readonly
*/
get favIconUrl() {
if (this.hasTabPermission) {
return this._favIconUrl;
}
return undefined;
}
/**
* @property {integer} lastAccessed
* Returns the last time the tab was accessed as the number of
* milliseconds since epoch.
* @readonly
* @abstract
*/
get lastAccessed() {
throw new Error("Not implemented");
}
/**
* @property {boolean} audible
* Returns true if the tab is currently playing audio, false otherwise.
* @readonly
* @abstract
*/
get audible() {
throw new Error("Not implemented");
}
/**
* @property {boolean} autoDiscardable
* Returns true if the tab can be discarded on memory pressure, false otherwise.
* @readonly
* @abstract
*/
get autoDiscardable() {
throw new Error("Not implemented");
}
/**
* @property {XULElement} browser
* Returns the XUL browser for the given tab.
* @readonly
* @abstract
*/
get browser() {
throw new Error("Not implemented");
}
/**
* @property {BrowsingContext} browsingContext
* Returns the BrowsingContext for the given tab.
* @readonly
*/
get browsingContext() {
return this.browser?.browsingContext;
}
/**
* @property {FrameLoader} frameLoader
* Returns the frameloader for the given tab.
* @readonly
*/
get frameLoader() {
return this.browser && this.browser.frameLoader;
}
/**
* @property {string} cookieStoreId
* Returns the cookie store identifier for the given tab.
* @readonly
* @abstract
*/
get cookieStoreId() {
throw new Error("Not implemented");
}
/**
* @property {integer} openerTabId
* Returns the ID of the tab which opened this one.
* @readonly
*/
get openerTabId() {
return null;
}
/**
* @property {integer} discarded
* Returns true if the tab is discarded.
* @readonly
* @abstract
*/
get discarded() {
throw new Error("Not implemented");
}
/**
* @property {integer} height
* Returns the pixel height of the visible area of the tab.
* @readonly
* @abstract
*/
get height() {
throw new Error("Not implemented");
}
/**
* @property {integer} hidden
* Returns true if the tab is hidden.
* @readonly
* @abstract
*/
get hidden() {
throw new Error("Not implemented");
}
/**
* @property {integer} index
* Returns the index of the tab in its window's tab list.
* @readonly
* @abstract
*/
get index() {
throw new Error("Not implemented");
}
/**
* @property {MutedInfo} mutedInfo
* Returns information about the tab's current audio muting status.
* @readonly
* @abstract
*/
get mutedInfo() {
throw new Error("Not implemented");
}
/**
* @property {SharingState} sharingState
* Returns object with tab sharingState.
* @readonly
* @abstract
*/
get sharingState() {
throw new Error("Not implemented");
}
/**
* @property {boolean} pinned
* Returns true if the tab is pinned, false otherwise.
* @readonly
* @abstract
*/
get pinned() {
throw new Error("Not implemented");
}
/**
* @property {boolean} active
* Returns true if the tab is the currently-selected tab, false
* otherwise.
* @readonly
* @abstract
*/
get active() {
throw new Error("Not implemented");
}
/**
* @property {boolean} highlighted
* Returns true if the tab is highlighted.
* @readonly
* @abstract
*/
get highlighted() {
throw new Error("Not implemented");
}
/**
* @property {string} status
* Returns the current loading status of the tab. May be either
* "loading" or "complete".
* @readonly
* @abstract
*/
get status() {
throw new Error("Not implemented");
}
/**
* @property {integer} height
* Returns the pixel height of the visible area of the tab.
* @readonly
* @abstract
*/
get width() {
throw new Error("Not implemented");
}
/**
* @property {DOMWindow} window
* Returns the browser window to which the tab belongs.
* @readonly
* @abstract
*/
get window() {
throw new Error("Not implemented");
}
/**
* @property {integer} window
* Returns the numeric ID of the browser window to which the tab belongs.
* @readonly
* @abstract
*/
get windowId() {
throw new Error("Not implemented");
}
/**
* @property {boolean} attention
* Returns true if the tab is drawing attention.
* @readonly
* @abstract
*/
get attention() {
throw new Error("Not implemented");
}
/**
* @property {boolean} isArticle
* Returns true if the document in the tab can be rendered in reader
* mode.
* @readonly
* @abstract
*/
get isArticle() {
throw new Error("Not implemented");
}
/**
* @property {boolean} isInReaderMode
* Returns true if the document in the tab is being rendered in reader
* mode.
* @readonly
* @abstract
*/
get isInReaderMode() {
throw new Error("Not implemented");
}
/**
* @property {integer} successorTabId
* @readonly
* @abstract
*/
get successorTabId() {
throw new Error("Not implemented");
}
/**
* Returns true if this tab matches the the given query info object. Omitted
* or null have no effect on the match.
*
* @param {object} queryInfo
* The query info against which to match.
* @param {boolean} [queryInfo.active]
* Matches against the exact value of the tab's `active` attribute.
* @param {boolean} [queryInfo.audible]
* Matches against the exact value of the tab's `audible` attribute.
* @param {boolean} [queryInfo.autoDiscardable]
* Matches against the exact value of the tab's `autoDiscardable` attribute.
* @param {string} [queryInfo.cookieStoreId]
* Matches against the exact value of the tab's `cookieStoreId` attribute.
* @param {boolean} [queryInfo.discarded]
* Matches against the exact value of the tab's `discarded` attribute.
* @param {boolean} [queryInfo.hidden]
* Matches against the exact value of the tab's `hidden` attribute.
* @param {boolean} [queryInfo.highlighted]
* Matches against the exact value of the tab's `highlighted` attribute.
* @param {integer} [queryInfo.index]
* Matches against the exact value of the tab's `index` attribute.
* @param {boolean} [queryInfo.muted]
* Matches against the exact value of the tab's `mutedInfo.muted` attribute.
* @param {boolean} [queryInfo.pinned]
* Matches against the exact value of the tab's `pinned` attribute.
* @param {string} [queryInfo.status]
* Matches against the exact value of the tab's `status` attribute.
* @param {string} [queryInfo.title]
* Matches against the exact value of the tab's `title` attribute.
* @param {string|boolean } [queryInfo.screen]
* Matches against the exact value of the tab's `sharingState.screen` attribute, or use true to match any screen sharing tab.
* @param {boolean} [queryInfo.camera]
* Matches against the exact value of the tab's `sharingState.camera` attribute.
* @param {boolean} [queryInfo.microphone]
* Matches against the exact value of the tab's `sharingState.microphone` attribute.
*
* Note: Per specification, this should perform a pattern match, rather
* than an exact value match, and will do so in the future.
* @param {MatchPattern} [queryInfo.url]
* Requires the tab's URL to match the given MatchPattern object.
*
* @returns {boolean}
* True if the tab matches the query.
*/
matches(queryInfo) {
const PROPS = [
"active",
"audible",
"autoDiscardable",
"discarded",
"hidden",
"highlighted",
"index",
"openerTabId",
"pinned",
"status",
];
function checkProperty(prop, obj) {
return queryInfo[prop] != null && queryInfo[prop] !== obj[prop];
}
if (PROPS.some(prop => checkProperty(prop, this))) {
return false;
}
if (checkProperty("muted", this.mutedInfo)) {
return false;
}
let state = this.sharingState;
if (["camera", "microphone"].some(prop => checkProperty(prop, state))) {
return false;
}
// query for screen can be boolean (ie. any) or string (ie. specific).
if (queryInfo.screen !== null) {
let match =
typeof queryInfo.screen == "boolean"
? queryInfo.screen === !!state.screen
: queryInfo.screen === state.screen;
if (!match) {
return false;
}
}
if (queryInfo.cookieStoreId) {
if (!queryInfo.cookieStoreId.includes(this.cookieStoreId)) {
return false;
}
}
if (queryInfo.url || queryInfo.title) {
if (!this.hasTabPermission) {
return false;
}
// Using _uri and _title instead of url/title to avoid repeated permission checks.
if (queryInfo.url && !queryInfo.url.matches(this._uri)) {
return false;
}
if (queryInfo.title && !queryInfo.title.matches(this._title)) {
return false;
}
}
return true;
}
/**
* Converts this tab object to a JSON-compatible object containing the values
* of its properties which the extension is permitted to access, in the format
* required to be returned by WebExtension APIs.
*
* @param {object} [fallbackTabSize]
* A geometry data if the lazy geometry data for this tab hasn't been
* initialized yet.
* @returns {object}
*/
convert(fallbackTabSize = null) {
let result = {
id: this.id,
index: this.index,
windowId: this.windowId,
highlighted: this.highlighted,
active: this.active,
attention: this.attention,
pinned: this.pinned,
status: this.status,
hidden: this.hidden,
discarded: this.discarded,
incognito: this.incognito,
width: this.width,
height: this.height,
lastAccessed: this.lastAccessed,
audible: this.audible,
autoDiscardable: this.autoDiscardable,
mutedInfo: this.mutedInfo,
isArticle: this.isArticle,
isInReaderMode: this.isInReaderMode,
sharingState: this.sharingState,
successorTabId: this.successorTabId,
cookieStoreId: this.cookieStoreId,
};
// If the tab has not been fully layed-out yet, fallback to the geometry
// from a different tab (usually the currently active tab).
if (fallbackTabSize && (!result.width || !result.height)) {
result.width = fallbackTabSize.width;
result.height = fallbackTabSize.height;
}
let opener = this.openerTabId;
if (opener) {
result.openerTabId = opener;
}
if (this.hasTabPermission) {
for (let prop of ["url", "title", "favIconUrl"]) {
// We use the underscored variants here to avoid the redundant
// permissions checks imposed on the public properties.
let val = this[`_${prop}`];
if (val) {
result[prop] = val;
}
}
}
return result;
}
/**
* Query each content process hosting subframes of the tab, return results.
*
* @param {string} message
* @param {object} options
* These options are also sent to the message handler in the
* `ExtensionContentChild`.
* @param {number[]} options.frameIds
* When omitted, all frames will be queried.
* @param {boolean} options.returnResultsWithFrameIds
* @returns {Promise[]}
*/
async queryContent(message, options) {
let { frameIds } = options;
/** @type {Map<nsIDOMProcessParent, innerWindowId[]>} */
let byProcess = new DefaultMap(() => []);
// We use this set to know which frame IDs are potentially invalid (as in
// not found when visiting the tab's BC tree below) when frameIds is a
// non-empty list of frame IDs.
let frameIdsSet = new Set(frameIds);
// Recursively walk the tab's BC tree, find all frames, group by process.
function visit(bc) {
let win = bc.currentWindowGlobal;
let frameId = bc.parent ? bc.id : 0;
if (win?.domProcess && (!frameIds || frameIdsSet.has(frameId))) {
byProcess.get(win.domProcess).push(win.innerWindowId);
frameIdsSet.delete(frameId);
}
if (!frameIds || frameIdsSet.size > 0) {
bc.children.forEach(visit);
}
}
visit(this.browsingContext);
if (frameIdsSet.size > 0) {
throw new ExtensionError(
`Invalid frame IDs: [${Array.from(frameIdsSet).join(", ")}].`
);
}
let promises = Array.from(byProcess.entries(), ([proc, windows]) =>
proc.getActor("ExtensionContent").sendQuery(message, { windows, options })
);
let results = await Promise.all(promises).catch(err => {
if (err.name === "DataCloneError") {
let fileName = options.jsPaths.slice(-1)[0] || "<anonymous code>";
let message = `Script '${fileName}' result is non-structured-clonable data`;
return Promise.reject({ message, fileName });
}
throw err;
});
results = results.flat();
if (!results.length) {
let errorMessage = "Missing host permission for the tab";
if (!frameIds || frameIds.length > 1 || frameIds[0] !== 0) {
errorMessage += " or frames";
}
throw new ExtensionError(errorMessage);
}
if (frameIds && frameIds.length === 1 && results.length > 1) {
throw new ExtensionError("Internal error: multiple windows matched");
}
return results;
}
/**
* Inserts a script or stylesheet in the given tab, and returns a promise
* which resolves when the operation has completed.
*
* @param {BaseContext} context
* The extension context for which to perform the injection.
* @param {InjectDetails} details
* The InjectDetails object, specifying what to inject, where, and
* when.
* @param {string} kind
* The kind of data being injected. Either "script" or "css".
* @param {string} method
* The name of the method which was called to trigger the injection.
* Used to generate appropriate error messages on failure.
*
* @returns {Promise}
* Resolves to the result of the execution, once it has completed.
* @private
*/
_execute(context, details, kind, method) {
let options = {
jsPaths: [],
cssPaths: [],
removeCSS: method == "removeCSS",
extensionId: context.extension.id,
};
// We require a `code` or a `file` property, but we can't accept both.
if ((details.code === null) == (details.file === null)) {
return Promise.reject({
message: `${method} requires either a 'code' or a 'file' property, but not both`,
});
}
if (details.frameId !== null && details.allFrames) {
return Promise.reject({
message: `'frameId' and 'allFrames' are mutually exclusive`,
});
}
options.hasActiveTabPermission = this.hasActiveTabPermission;
options.matches = this.extension.allowedOrigins.patterns.map(
host => host.pattern
);
if (details.code !== null) {
options[`${kind}Code`] = details.code;
}
if (details.file !== null) {
let url = context.uri.resolve(details.file);
if (!this.extension.isExtensionURL(url)) {
return Promise.reject({
message: "Files to be injected must be within the extension",
});
}
options[`${kind}Paths`].push(url);
}
if (details.allFrames) {
options.allFrames = true;
} else if (details.frameId !== null) {
options.frameIds = [details.frameId];
} else if (!details.allFrames) {
options.frameIds = [0];
}
if (details.matchAboutBlank) {
options.matchAboutBlank = details.matchAboutBlank;
}
if (details.runAt !== null) {
options.runAt = details.runAt;
} else {
options.runAt = "document_idle";
}
if (details.cssOrigin !== null) {
options.cssOrigin = details.cssOrigin;
} else {
options.cssOrigin = "author";
}
options.wantReturnValue = true;
// The scripting API (defined in `parent/ext-scripting.js`) has its own
// `execute()` function that calls `queryContent()` as well. Make sure to
// keep both in sync when relevant.
return this.queryContent("Execute", options);
}
/**
* Executes a script in the tab's content window, and returns a Promise which
* resolves to the result of the evaluation, or rejects to the value of any
* error the injection generates.
*
* @param {BaseContext} context
* The extension context for which to inject the script.
* @param {InjectDetails} details
* The InjectDetails object, specifying what to inject, where, and
* when.
*
* @returns {Promise}
* Resolves to the result of the evaluation of the given script, once
* it has completed, or rejects with any error the evaluation
* generates.
*/
executeScript(context, details) {
return this._execute(context, details, "js", "executeScript");
}
/**
* Injects CSS into the tab's content window, and returns a Promise which
* resolves when the injection is complete.
*
* @param {BaseContext} context
* The extension context for which to inject the script.
* @param {InjectDetails} details
* The InjectDetails object, specifying what to inject, and where.
*
* @returns {Promise}
* Resolves when the injection has completed.
*/
insertCSS(context, details) {
return this._execute(context, details, "css", "insertCSS").then(() => {});
}
/**
* Removes CSS which was previously into the tab's content window via
* `insertCSS`, and returns a Promise which resolves when the operation is
* complete.
*
* @param {BaseContext} context
* The extension context for which to remove the CSS.
* @param {InjectDetails} details
* The InjectDetails object, specifying what to remove, and from where.
*
* @returns {Promise}
* Resolves when the operation has completed.
*/
removeCSS(context, details) {
return this._execute(context, details, "css", "removeCSS").then(() => {});
}
}
defineLazyGetter(TabBase.prototype, "incognito", function () {
return this._incognito;
});
// Note: These must match the values in windows.json.
const WINDOW_ID_NONE = -1;
const WINDOW_ID_CURRENT = -2;
/**
* A platform-independent base class for extension-specific wrappers around
* native browser windows
*
* @param {Extension} extension
* The extension object for which this wrapper is being created.
* @param {DOMWindow} window
* The browser DOM window which is being wrapped.
* @param {integer} id
* The numeric ID of this DOM window object. This ID should be the same for
* every extension, and for the lifetime of the window.
*/
class WindowBase {
constructor(extension, window, id) {
if (!extension.canAccessWindow(window)) {
throw new ExtensionError("extension cannot access window");
}
this.extension = extension;
this.window = window;
this.id = id;
}
/**
* @property {nsIAppWindow} appWindow
* The nsIAppWindow object for this browser window.
* @readonly
*/
get appWindow() {
return this.window.docShell.treeOwner
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIAppWindow);
}
/**
* Returns true if this window is the current window for the given extension
* context, false otherwise.
*
* @param {BaseContext} context
* The extension context for which to perform the check.
*
* @returns {boolean}
*/
isCurrentFor(context) {
if (context && context.currentWindow) {
return this.window === context.currentWindow;
}
return this.isLastFocused;
}
/**
* @property {string} type
* The type of the window, as defined by the WebExtension API. May be
* either "normal" or "popup".
* @readonly
*/
get type() {
let { chromeFlags } = this.appWindow;
if (chromeFlags & Ci.nsIWebBrowserChrome.CHROME_OPENAS_DIALOG) {
return "popup";
}
return "normal";
}
/**
* Converts this window object to a JSON-compatible object which may be
* returned to an extension, in the format required to be returned by
* WebExtension APIs.
*
* @param {object} [getInfo]
* An optional object, the properties of which determine what data is
* available on the result object.
* @param {boolean} [getInfo.populate]
* Of true, the result object will contain a `tabs` property,
* containing an array of converted Tab objects, one for each tab in
* the window.
*
* @returns {object}
*/
convert(getInfo) {
let result = {
id: this.id,
focused: this.focused,
top: this.top,
left: this.left,
width: this.width,
height: this.height,
incognito: this.incognito,
type: this.type,
state: this.state,
alwaysOnTop: this.alwaysOnTop,
title: this.title,
};
if (getInfo && getInfo.populate) {
result.tabs = Array.from(this.getTabs(), tab => tab.convert());
}
return result;
}
/**
* Returns true if this window matches the the given query info object. Omitted
* or null have no effect on the match.
*
* @param {object} queryInfo
* The query info against which to match.
* @param {boolean} [queryInfo.currentWindow]
* Matches against against the return value of `isCurrentFor()` for the
* given context.
* @param {boolean} [queryInfo.lastFocusedWindow]
* Matches against the exact value of the window's `isLastFocused` attribute.
* @param {boolean} [queryInfo.windowId]
* Matches against the exact value of the window's ID, taking into
* account the special WINDOW_ID_CURRENT value.
* @param {string} [queryInfo.windowType]
* Matches against the exact value of the window's `type` attribute.
* @param {BaseContext} context
* The extension context for which the matching is being performed.
* Used to determine the current window for relevant properties.
*
* @returns {boolean}
* True if the window matches the query.
*/
matches(queryInfo, context) {
if (
queryInfo.lastFocusedWindow !== null &&
queryInfo.lastFocusedWindow !== this.isLastFocused
) {
return false;
}
if (queryInfo.windowType !== null && queryInfo.windowType !== this.type) {
return false;
}
if (queryInfo.windowId !== null) {
if (queryInfo.windowId === WINDOW_ID_CURRENT) {
if (!this.isCurrentFor(context)) {
return false;
}
} else if (queryInfo.windowId !== this.id) {
return false;
}
}
if (
queryInfo.currentWindow !== null &&
queryInfo.currentWindow !== this.isCurrentFor(context)
) {
return false;
}
return true;
}
/**
* @property {boolean} focused
* Returns true if the browser window is currently focused.
* @readonly
* @abstract
*/
get focused() {
throw new Error("Not implemented");
}
/**
* @property {integer} top
* Returns the pixel offset of the top of the window from the top of
* the screen.
* @readonly
* @abstract
*/
get top() {
throw new Error("Not implemented");
}
/**
* @property {integer} left
* Returns the pixel offset of the left of the window from the left of
* the screen.
* @readonly
* @abstract
*/
get left() {
throw new Error("Not implemented");
}
/**
* @property {integer} width
* Returns the pixel width of the window.
* @readonly
* @abstract
*/
get width() {
throw new Error("Not implemented");
}
/**
* @property {integer} height
* Returns the pixel height of the window.
* @readonly
* @abstract
*/
get height() {
throw new Error("Not implemented");
}
/**
* @property {boolean} incognito
* Returns true if this is a private browsing window, false otherwise.
* @readonly
* @abstract
*/
get incognito() {
throw new Error("Not implemented");
}
/**
* @property {boolean} alwaysOnTop
* Returns true if this window is constrained to always remain above
* other windows.
* @readonly
* @abstract
*/
get alwaysOnTop() {
throw new Error("Not implemented");
}
/**
* @property {boolean} isLastFocused
* Returns true if this is the browser window which most recently had
* focus.
* @readonly
* @abstract
*/
get isLastFocused() {
throw new Error("Not implemented");
}
/**
* @property {string} state
* Returns or sets the current state of this window, as determined by
* `getState()`.
* @abstract
*/
get state() {
throw new Error("Not implemented");
}
set state(state) {
throw new Error("Not implemented");
}
/**
* @property {nsIURI | undefined} title
* Returns the current title of this window if the extension has permission
* to read it, or undefined otherwise.
* @readonly
*/
get title() {
// activeTab may be null when a new window is adopting an existing tab as its first tab
// (See Bug 1458918 for rationale).
if (this.activeTab && this.activeTab.hasTabPermission) {
return this._title;
}
return undefined;
}
// The JSDoc validator does not support @returns tags in abstract functions or
// star functions without return statements.
/* eslint-disable valid-jsdoc */
/**
* Returns the window state of the given window.
*
* @param {DOMWindow} _window
* The window for which to return a state.
*
* @returns {string}
* The window's state. One of "normal", "minimized", "maximized",
* "fullscreen", or "docked".
* @static
* @abstract
*/
static getState(_window) {
throw new Error("Not implemented");
}
/**
* Returns an iterator of TabBase objects for each tab in this window.
*
* @returns {Iterator<TabBase>}
*/
getTabs() {
throw new Error("Not implemented");
}
/**
* Returns an iterator of TabBase objects for each highlighted tab in this window.
*
* @returns {Iterator<TabBase>}
*/
getHighlightedTabs() {
throw new Error("Not implemented");
}
/**
* @property {TabBase} The window's currently active tab.
*/
get activeTab() {
throw new Error("Not implemented");
}
/**
* Returns the window's tab at the specified index.
*
* @param {integer} _index
* The index of the desired tab.
*
* @returns {TabBase|undefined}
*/
getTabAtIndex(_index) {
throw new Error("Not implemented");
}
/* eslint-enable valid-jsdoc */
}
Object.assign(WindowBase, { WINDOW_ID_NONE, WINDOW_ID_CURRENT });
/**
* The parameter type of "tab-attached" events, which are emitted when a
* pre-existing tab is attached to a new window.
*
* @typedef {object} TabAttachedEvent
* @property {NativeTab} tab
* The native tab object in the window to which the tab is being
* attached. This may be a different object than was used to represent
* the tab in the old window.
* @property {integer} tabId
* The ID of the tab being attached.
* @property {integer} newWindowId
* The ID of the window to which the tab is being attached.
* @property {integer} newPosition
* The position of the tab in the tab list of the new window.
*/
/**
* The parameter type of "tab-detached" events, which are emitted when a
* pre-existing tab is detached from a window, in order to be attached to a new
* window.
*
* @typedef {object} TabDetachedEvent
* @property {NativeTab} tab
* The native tab object in the window from which the tab is being
* detached. This may be a different object than will be used to
* represent the tab in the new window.
* @property {NativeTab} adoptedBy
* The native tab object in the window to which the tab will be attached,
* and is adopting the contents of this tab. This may be a different
* object than the tab in the previous window.
* @property {integer} tabId
* The ID of the tab being detached.
* @property {integer} oldWindowId
* The ID of the window from which the tab is being detached.
* @property {integer} oldPosition
* The position of the tab in the tab list of the window from which it is
* being detached.
*/
/**
* The parameter type of "tab-created" events, which are emitted when a
* new tab is created.
*
* @typedef {object} TabCreatedEvent
* @property {NativeTab} tab
* The native tab object for the tab which is being created.
*/
/**
* The parameter type of "tab-removed" events, which are emitted when a
* tab is removed and destroyed.
*
* @typedef {object} TabRemovedEvent
* @property {NativeTab} tab
* The native tab object for the tab which is being removed.
* @property {integer} tabId
* The ID of the tab being removed.
* @property {integer} windowId
* The ID of the window from which the tab is being removed.
* @property {boolean} isWindowClosing
* True if the tab is being removed because the window is closing.
*/
/**
* An object containing basic, extension-independent information about the window
* and tab that a XUL <browser> belongs to.
*
* @typedef {object} BrowserData
* @property {integer} tabId
* The numeric ID of the tab that a <browser> belongs to, or -1 if it
* does not belong to a tab.
* @property {integer} windowId
* The numeric ID of the browser window that a <browser> belongs to, or -1
* if it does not belong to a browser window.
*/
/**
* A platform-independent base class for the platform-specific TabTracker
* classes, which track the opening and closing of tabs, and manage the mapping
* of them between numeric IDs and native tab objects.
*
* Instances of this class are EventEmitters which emit the following events,
* each with an argument of the given type:
*
* - "tab-attached" {@link TabAttacheEvent}
* - "tab-detached" {@link TabDetachedEvent}
* - "tab-created" {@link TabCreatedEvent}
* - "tab-removed" {@link TabRemovedEvent}
*/
class TabTrackerBase extends EventEmitter {
on(...args) {
if (!this.initialized) {
this.init();
}
return super.on(...args); // eslint-disable-line mozilla/balanced-listeners
}
/**
* Called to initialize the tab tracking listeners the first time that an
* event listener is added.
*
* @protected
* @abstract
*/
init() {
throw new Error("Not implemented");
}
// The JSDoc validator does not support @returns tags in abstract functions or
// star functions without return statements.
/* eslint-disable valid-jsdoc */
/**
* Returns the numeric ID for the given native tab.
*
* @param {NativeTab} _nativeTab
* The native tab for which to return an ID.
*
* @returns {integer}
* The tab's numeric ID.
* @abstract
*/
getId(_nativeTab) {
throw new Error("Not implemented");
}
/**
* Returns the native tab with the given numeric ID.
*
* @param {integer} _tabId
* The numeric ID of the tab to return.
* @param {*} _default
* The value to return if no tab exists with the given ID.
*
* @returns {NativeTab}
* @throws {ExtensionError}
* If no tab exists with the given ID and a default return value is not
* provided.
* @abstract
*/
getTab(_tabId, _default) {
throw new Error("Not implemented");
}
/**
* Returns basic information about the tab and window that the given browser
* belongs to.
*
* @param {XULElement} browser
* The XUL browser element for which to return data.
*
* @returns {BrowserData}
* @abstract
*/
/* eslint-enable valid-jsdoc */
getBrowserData() {
throw new Error("Not implemented");
}
/**
* @property {NativeTab} activeTab
* Returns the native tab object for the active tab in the
* most-recently focused window, or null if no live tabs currently
* exist.
* @abstract
*/
get activeTab() {
throw new Error("Not implemented");
}
}
/**
* A browser progress listener instance which calls a given listener function
* whenever the status of the given browser changes.
*
* @param {function(object): void} listener
* A function to be called whenever the status of a tab's top-level
* browser. It is passed an object with a `browser` property pointing to
* the XUL browser, and a `status` property with a string description of
* the browser's status.
* @private
*/
class StatusListener {
constructor(listener) {
this.listener = listener;
}
onStateChange(browser, webProgress, request, stateFlags, statusCode) {
if (!webProgress.isTopLevel) {
return;
}
let status;
if (stateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW) {
if (stateFlags & Ci.nsIWebProgressListener.STATE_START) {
status = "loading";
} else if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
status = "complete";
}
} else if (
stateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
statusCode == Cr.NS_BINDING_ABORTED
) {
status = "complete";
}
if (status) {
this.listener({ browser, status });
}
}
onLocationChange(browser, webProgress, request, locationURI) {
if (webProgress.isTopLevel) {
let status = webProgress.isLoadingDocument ? "loading" : "complete";
this.listener({ browser, status, url: locationURI.spec });
}
}
}
/**
* A platform-independent base class for the platform-specific WindowTracker
* classes, which track the opening and closing of windows, and manage the
* mapping of them between numeric IDs and native tab objects.
*/
class WindowTrackerBase extends EventEmitter {
constructor() {
super();
this._handleWindowOpened = this._handleWindowOpened.bind(this);
this._openListeners = new Set();
this._closeListeners = new Set();
this._listeners = new DefaultMap(() => new Set());
this._statusListeners = new DefaultWeakMap(listener => {
return new StatusListener(listener);
});
this._windowIds = new DefaultWeakMap(window => {
return window.docShell.outerWindowID;
});
}
isBrowserWindow(window) {
let { documentElement } = window.document;
return documentElement.getAttribute("windowtype") === "navigator:browser";
}
// The JSDoc validator does not support @returns tags in abstract functions or
// star functions without return statements.
/* eslint-disable valid-jsdoc */
/**
* Returns an iterator for all currently active browser windows.
*
* @param {boolean} [includeInomplete = false]
* If true, include browser windows which are not yet fully loaded.
* Otherwise, only include windows which are.
*
* @returns {Iterator<DOMWindow>}
*/
/* eslint-enable valid-jsdoc */
*browserWindows(includeIncomplete = false) {
// The window type parameter is only available once the window's document
// element has been created. This means that, when looking for incomplete
// browser windows, we need to ignore the type entirely for windows which
// haven't finished loading, since we would otherwise skip browser windows
// in their early loading stages.
// This is particularly important given that the "domwindowcreated" event
// fires for browser windows when they're in that in-between state, and just
// before we register our own "domwindowcreated" listener.
for (let window of Services.wm.getEnumerator("")) {
let ok = includeIncomplete;
if (window.document.readyState === "complete") {
ok = this.isBrowserWindow(window);
}
if (ok) {
yield window;
}
}
}
/**
* @property {DOMWindow|null} topWindow
* The currently active, or topmost, browser window, or null if no
* browser window is currently open.
* @readonly
*/
get topWindow() {
return Services.wm.getMostRecentWindow("navigator:browser");
}
/**
* @property {DOMWindow|null} topWindow
* The currently active, or topmost, browser window that is not
* private browsing, or null if no browser window is currently open.
* @readonly
*/
get topNonPBWindow() {
return Services.wm.getMostRecentNonPBWindow("navigator:browser");
}
/**
* Returns the top window accessible by the extension.
*
* @param {BaseContext} context
* The extension context for which to return the current window.
*
* @returns {DOMWindow|null}
*/
getTopWindow(context) {
if (context && !context.privateBrowsingAllowed) {
return this.topNonPBWindow;
}
return this.topWindow;
}
/**
* Returns the numeric ID for the given browser window.
*
* @param {DOMWindow} window
* The DOM window for which to return an ID.
*
* @returns {integer}
* The window's numeric ID.
*/
getId(window) {
return this._windowIds.get(window);
}
/**
* Returns the browser window to which the given context belongs, or the top
* browser window if the context does not belong to a browser window.
*
* @param {BaseContext} context
* The extension context for which to return the current window.
*
* @returns {DOMWindow|null}
*/
getCurrentWindow(context) {
return (context && context.currentWindow) || this.getTopWindow(context);
}
/**
* Returns the browser window with the given ID.
*
* @param {integer} id
* The ID of the window to return.
* @param {BaseContext} context
* The extension context for which the matching is being performed.
* Used to determine the current window for relevant properties.
* @param {boolean} [strict = true]
* If false, undefined will be returned instead of throwing an error
* in case no window exists with the given ID.
*
* @returns {DOMWindow|undefined}
* @throws {ExtensionError}
* If no window exists with the given ID and `strict` is true.
*/
getWindow(id, context, strict = true) {
if (id === WINDOW_ID_CURRENT) {
return this.getCurrentWindow(context);
}
let window = Services.wm.getOuterWindowWithId(id);
if (
window &&
!window.closed &&
(window.document.readyState !== "complete" ||
this.isBrowserWindow(window))
) {
if (!context || context.canAccessWindow(window)) {
// Tolerate incomplete windows because isBrowserWindow is only reliable
// once the window is fully loaded.
return window;
}
}
if (strict) {
throw new ExtensionError(`Invalid window ID: ${id}`);
}
}
/**
* @property {boolean} _haveListeners
* Returns true if any window open or close listeners are currently
* registered.
* @private
*/
get _haveListeners() {
return this._openListeners.size > 0 || this._closeListeners.size > 0;
}
/**
* Register the given listener function to be called whenever a new browser
* window is opened.
*
* @param {function(DOMWindow): void} listener
* The listener function to register.
*/
addOpenListener(listener) {
if (!this._haveListeners) {
Services.ww.registerNotification(this);
}
this._openListeners.add(listener);
for (let window of this.browserWindows(true)) {
if (window.document.readyState !== "complete") {
window.addEventListener("load", this);
}
}
}
/**
* Unregister a listener function registered in a previous addOpenListener
* call.
*
* @param {function(DOMWindow): void} listener
* The listener function to unregister.
*/
removeOpenListener(listener) {
this._openListeners.delete(listener);
if (!this._haveListeners) {
Services.ww.unregisterNotification(this);
}
}
/**
* Register the given listener function to be called whenever a browser
* window is closed.
*
* @param {function(DOMWindow): void} listener
* The listener function to register.
*/
addCloseListener(listener) {
if (!this._haveListeners) {
Services.ww.registerNotification(this);
}
this._closeListeners.add(listener);
}
/**
* Unregister a listener function registered in a previous addCloseListener
* call.
*
* @param {function(DOMWindow): void} listener
* The listener function to unregister.
*/
removeCloseListener(listener) {
this._closeListeners.delete(listener);
if (!this._haveListeners) {
Services.ww.unregisterNotification(this);
}
}
/**
* Handles load events for recently-opened windows, and adds additional
* listeners which may only be safely added when the window is fully loaded.
*
* @param {Event} event
* A DOM event to handle.
* @private
*/
handleEvent(event) {
if (event.type === "load") {
event.currentTarget.removeEventListener(event.type, this);
let window = event.target.defaultView;
if (!this.isBrowserWindow(window)) {
return;
}
for (let listener of this._openListeners) {
try {
listener(window);
} catch (e) {
Cu.reportError(e);
}
}
}
}
/**
* Observes "domwindowopened" and "domwindowclosed" events, notifies the
* appropriate listeners, and adds necessary additional listeners to the new
* windows.
*
* @param {DOMWindow} window
* A DOM window.
* @param {string} topic
* The topic being observed.
* @private
*/
observe(window, topic) {
if (topic === "domwindowclosed") {
if (!this.isBrowserWindow(window)) {
return;
}
window.removeEventListener("load", this);
for (let listener of this._closeListeners) {
try {
listener(window);
} catch (e) {
Cu.reportError(e);
}
}
} else if (topic === "domwindowopened") {
window.addEventListener("load", this);
}
}
/**
* Add an event listener to be called whenever the given DOM event is received
* at the top level of any browser window.
*
* @param {string} type
* The type of event to listen for. May be any valid DOM event name, or
* one of the following special cases:
*
* - "progress": Adds a tab progress listener to every browser window.
* - "status": Adds a StatusListener to every tab of every browser
* window.
* - "domwindowopened": Acts as an alias for addOpenListener.
* - "domwindowclosed": Acts as an alias for addCloseListener.
* @param {Function | object} listener
* The listener to invoke in response to the given events.
*
* @returns {undefined}
*/
addListener(type, listener) {
if (type === "domwindowopened") {
return this.addOpenListener(listener);
} else if (type === "domwindowclosed") {
return this.addCloseListener(listener);
}
if (this._listeners.size === 0) {
this.addOpenListener(this._handleWindowOpened);
}
if (type === "status") {
listener = this._statusListeners.get(listener);
type = "progress";
}
this._listeners.get(type).add(listener);
// Register listener on all existing windows.
for (let window of this.browserWindows()) {
this._addWindowListener(window, type, listener);
}
}
/**
* Removes an event listener previously registered via an addListener call.
*
* @param {string} type
* The type of event to stop listening for.
* @param {Function | object} listener
* The listener to remove.
*
* @returns {undefined}
*/
removeListener(type, listener) {
if (type === "domwindowopened") {
return this.removeOpenListener(listener);
} else if (type === "domwindowclosed") {
return this.removeCloseListener(listener);
}
if (type === "status") {
listener = this._statusListeners.get(listener);
type = "progress";
}
let listeners = this._listeners.get(type);
listeners.delete(listener);
if (listeners.size === 0) {
this._listeners.delete(type);
if (this._listeners.size === 0) {
this.removeOpenListener(this._handleWindowOpened);
}
}
// Unregister listener from all existing windows.
let useCapture = type === "focus" || type === "blur";
for (let window of this.browserWindows()) {
if (type === "progress") {
this.removeProgressListener(window, listener);
} else {
window.removeEventListener(type, listener, useCapture);
}
}
}
/**
* Adds a listener for the given event to the given window.
*
* @param {DOMWindow} window
* The browser window to which to add the listener.
* @param {string} eventType
* The type of DOM event to listen for, or "progress" to add a tab
* progress listener.
* @param {Function | object} listener
* The listener to add.
* @private
*/
_addWindowListener(window, eventType, listener) {
let useCapture = eventType === "focus" || eventType === "blur";
if (eventType === "progress") {
this.addProgressListener(window, listener);
} else {
window.addEventListener(eventType, listener, useCapture);
}
}
/**
* A private method which is called whenever a new browser window is opened,
* and adds the necessary listeners to it.
*
* @param {DOMWindow} window
* The window being opened.
* @private
*/
_handleWindowOpened(window) {
for (let [eventType, listeners] of this._listeners) {
for (let listener of listeners) {
this._addWindowListener(window, eventType, listener);
}
}
}
/**
* Adds a tab progress listener to the given browser window.
*
* @param {DOMWindow} _window
* The browser window to which to add the listener.
* @param {object} _listener
* The tab progress listener to add.
* @abstract
*/
addProgressListener(_window, _listener) {
throw new Error("Not implemented");
}
/**
* Removes a tab progress listener from the given browser window.
*
* @param {DOMWindow} _window
* The browser window from which to remove the listener.
* @param {object} _listener
* The tab progress listener to remove.
* @abstract
*/
removeProgressListener(_window, _listener) {
throw new Error("Not implemented");
}
}
/**
* Manages native tabs, their wrappers, and their dynamic permissions for a
* particular extension.
*
* @param {Extension} extension
* The extension for which to manage tabs.
*/
class TabManagerBase {
constructor(extension) {
this.extension = extension;
this._tabs = new DefaultWeakMap(tab => this.wrapTab(tab));
}
/**
* If the extension has requested activeTab permission, grant it those
* permissions for the current inner window in the given native tab.
*
* @param {NativeTab} nativeTab
* The native tab for which to grant permissions.
*/
addActiveTabPermission(nativeTab) {
let tab = this.getWrapper(nativeTab);
if (
this.extension.hasPermission("activeTab") ||
(this.extension.originControls &&
this.extension.optionalOrigins.matches(tab._uri))
) {
// Note that, unlike Chrome, we don't currently clear this permission with
// the tab navigates. If the inner window is revived from BFCache before
// we've granted this permission to a new inner window, the extension
// maintains its permissions for it.
tab.activeTabWindowID = tab.innerWindowID;
}
}
/**
* Revoke the extension's activeTab permissions for the current inner window
* of the given native tab.
*
* @param {NativeTab} nativeTab
* The native tab for which to revoke permissions.
*/
revokeActiveTabPermission(nativeTab) {
this.getWrapper(nativeTab).activeTabWindowID = null;
}
/**
* Returns true if the extension has requested activeTab permission, and has
* been granted permissions for the current inner window if this tab.
*
* @param {NativeTab} nativeTab
* The native tab for which to check permissions.
* @returns {boolean}
* True if the extension has activeTab permissions for this tab.
*/
hasActiveTabPermission(nativeTab) {
return this.getWrapper(nativeTab).hasActiveTabPermission;
}
/**
* Activate MV3 content scripts if the extension has activeTab or an
* (ungranted) host permission.
*
* @param {NativeTab} nativeTab
*/
activateScripts(nativeTab) {
let tab = this.getWrapper(nativeTab);
if (
this.extension.originControls &&
!tab.matchesHostPermission &&
(this.extension.optionalOrigins.matches(tab._uri) ||
this.extension.hasPermission("activeTab")) &&
(this.extension.contentScripts.length ||
this.extension.registeredContentScripts.size)
) {
tab.queryContent("ActivateScripts", { id: this.extension.id });
}
}
/**
* Returns true if the extension has permissions to access restricted
* properties of the given native tab. In practice, this means that it has
* either requested the "tabs" permission or has activeTab permissions for the
* given tab.
*
* NOTE: Never use this method on an object that is not a native tab
* for the current platform: this method implicitly generates a wrapper
* for the passed nativeTab parameter and the platform-specific tabTracker
* instance is likely to store it in a map which is cleared only when the
* tab is closed (and so, if nativeTab is not a real native tab, it will
* never be cleared from the platform-specific tabTracker instance),
* See Bug 1458918 for a rationale.
*
* @param {NativeTab} nativeTab
* The native tab for which to check permissions.
* @returns {boolean}
* True if the extension has permissions for this tab.
*/
hasTabPermission(nativeTab) {
return this.getWrapper(nativeTab).hasTabPermission;
}
/**
* Returns this extension's TabBase wrapper for the given native tab. This
* method will always return the same wrapper object for any given native tab.
*
* @param {NativeTab} nativeTab
* The tab for which to return a wrapper.
*
* @returns {TabBase|undefined}
* The wrapper for this tab.
*/
getWrapper(nativeTab) {
if (this.canAccessTab(nativeTab)) {
return this._tabs.get(nativeTab);
}
}
/**
* Determines access using extension context.
*
* @param {NativeTab} _nativeTab
* The tab to check access on.
* @returns {boolean}
* True if the extension has permissions for this tab.
* @protected
* @abstract
*/
canAccessTab(_nativeTab) {
throw new Error("Not implemented");
}
/**
* Converts the given native tab to a JSON-compatible object, in the format
* required to be returned by WebExtension APIs, which may be safely passed to
* extension code.
*
* @param {NativeTab} nativeTab
* The native tab to convert.
* @param {object} [fallbackTabSize]
* A geometry data if the lazy geometry data for this tab hasn't been
* initialized yet.
*
* @returns {object}
*/
convert(nativeTab, fallbackTabSize = null) {
return this.getWrapper(nativeTab).convert(fallbackTabSize);
}
// The JSDoc validator does not support @returns tags in abstract functions or
// star functions without return statements.
/* eslint-disable valid-jsdoc */
/**
* Returns an iterator of TabBase objects which match the given query info.
*
* @param {object | null} [queryInfo = null]
* An object containing properties on which to filter. May contain any
* properties which are recognized by {@link TabBase#matches} or
* {@link WindowBase#matches}. Unknown properties will be ignored.
* @param {BaseContext|null} [context = null]
* The extension context for which the matching is being performed.
* Used to determine the current window for relevant properties.
*
* @returns {Iterator<TabBase>}
*/
*query(queryInfo = null, context = null) {
if (queryInfo) {
if (queryInfo.url !== null) {
queryInfo.url = parseMatchPatterns([].concat(queryInfo.url), {
restrictSchemes: false,
});
}
if (queryInfo.cookieStoreId !== null) {
queryInfo.cookieStoreId = [].concat(queryInfo.cookieStoreId);
}
if (queryInfo.title !== null) {
try {
queryInfo.title = new MatchGlob(queryInfo.title);
} catch (e) {
throw new ExtensionError(`Invalid title: ${queryInfo.title}`);
}
}
}
function* candidates(windowWrapper) {
if (queryInfo) {
let { active, highlighted, index } = queryInfo;
if (active === true) {
let { activeTab } = windowWrapper;
if (activeTab) {
yield activeTab;
}
return;
}
if (index != null) {
let tabWrapper = windowWrapper.getTabAtIndex(index);
if (tabWrapper) {
yield tabWrapper;
}
return;
}
if (highlighted === true) {
yield* windowWrapper.getHighlightedTabs();
return;
}
}
yield* windowWrapper.getTabs();
}
let windowWrappers = this.extension.windowManager.query(queryInfo, context);
for (let windowWrapper of windowWrappers) {
for (let tabWrapper of candidates(windowWrapper)) {
if (!queryInfo || tabWrapper.matches(queryInfo)) {
yield tabWrapper;
}
}
}
}
/**
* Returns a TabBase wrapper for the tab with the given ID.
*
* @param {integer} _tabId
* The ID of the tab for which to return a wrapper.
*
* @returns {TabBase}
* @throws {ExtensionError}
* If no tab exists with the given ID.
* @abstract
*/
get(_tabId) {
throw new Error("Not implemented");
}
/**
* Returns a new TabBase instance wrapping the given native tab.
*
* @param {NativeTab} _nativeTab
* The native tab for which to return a wrapper.
*
* @returns {TabBase}
* @protected
* @abstract
*/
wrapTab(_nativeTab) {
throw new Error("Not implemented");
}
}
/**
* Manages native browser windows and their wrappers for a particular extension.
*
* @param {Extension} extension
* The extension for which to manage windows.
*/
class WindowManagerBase {
constructor(extension) {
this.extension = extension;
this._windows = new DefaultWeakMap(window => this.wrapWindow(window));
}
/**
* Converts the given browser window to a JSON-compatible object, in the
* format required to be returned by WebExtension APIs, which may be safely
* passed to extension code.
*
* @param {DOMWindow} window
* The browser window to convert.
* @param {*} args
* Additional arguments to be passed to {@link WindowBase#convert}.
*
* @returns {object}
*/
convert(window, ...args) {
return this.getWrapper(window).convert(...args);
}
/**
* Returns this extension's WindowBase wrapper for the given browser window.
* This method will always return the same wrapper object for any given
* browser window.
*
* @param {DOMWindow} window
* The browser window for which to return a wrapper.
*
* @returns {WindowBase|undefined}
* The wrapper for this tab.
*/
getWrapper(window) {
if (this.extension.canAccessWindow(window)) {
return this._windows.get(window);
}
}
/**
* Returns whether this window can be accessed by the extension in the given
* context.
*
* @param {DOMWindow} window
* The browser window that is being tested
* @param {BaseContext|null} context
* The extension context for which this test is being performed.
* @returns {boolean}
*/
canAccessWindow(window, context) {
return (
(context && context.canAccessWindow(window)) ||
this.extension.canAccessWindow(window)
);
}
// The JSDoc validator does not support @returns tags in abstract functions or
// star functions without return statements.
/* eslint-disable valid-jsdoc */
/**
* Returns an iterator of WindowBase objects which match the given query info.
*
* @param {object | null} [queryInfo = null]
* An object containing properties on which to filter. May contain any
* properties which are recognized by {@link WindowBase#matches}.
* Unknown properties will be ignored.
* @param {BaseContext|null} [context = null]
* The extension context for which the matching is being performed.
* Used to determine the current window for relevant properties.
*
* @returns {Iterator<WindowBase>}
*/
*query(queryInfo = null, context = null) {
function* candidates(windowManager) {
if (queryInfo) {
let { currentWindow, windowId, lastFocusedWindow } = queryInfo;
if (currentWindow === true && windowId == null) {
windowId = WINDOW_ID_CURRENT;
}
if (windowId != null) {
let window = global.windowTracker.getWindow(windowId, context, false);
if (window) {
yield windowManager.getWrapper(window);
}
return;
}
if (lastFocusedWindow === true) {
let window = global.windowTracker.getTopWindow(context);
if (window) {
yield windowManager.getWrapper(window);
}
return;
}
}
yield* windowManager.getAll(context);
}
for (let windowWrapper of candidates(this)) {
if (!queryInfo || windowWrapper.matches(queryInfo, context)) {
yield windowWrapper;
}
}
}
/**
* Returns a WindowBase wrapper for the browser window with the given ID.
*
* @param {integer} _windowId
* The ID of the browser window for which to return a wrapper.
* @param {BaseContext} _context
* The extension context for which the matching is being performed.
* Used to determine the current window for relevant properties.
*
* @returns {WindowBase}
* @throws {ExtensionError}
* If no window exists with the given ID.
* @abstract
*/
get(_windowId, _context) {
throw new Error("Not implemented");
}
/**
* Returns an iterator of WindowBase wrappers for each currently existing
* browser window.
*
* @returns {Iterator<WindowBase>}
* @abstract
*/
getAll() {
throw new Error("Not implemented");
}
/**
* Returns a new WindowBase instance wrapping the given browser window.
*
* @param {DOMWindow} _window
* The browser window for which to return a wrapper.
*
* @returns {WindowBase}
* @protected
* @abstract
*/
wrapWindow(_window) {
throw new Error("Not implemented");
}
/* eslint-enable valid-jsdoc */
}
function getUserContextIdForCookieStoreId(
extension,
cookieStoreId,
isPrivateBrowsing
) {
if (!extension.hasPermission("cookies")) {
throw new ExtensionError(
`No permission for cookieStoreId: ${cookieStoreId}`
);
}
if (!isValidCookieStoreId(cookieStoreId)) {
throw new ExtensionError(`Illegal cookieStoreId: ${cookieStoreId}`);
}
if (isPrivateBrowsing && !isPrivateCookieStoreId(cookieStoreId)) {
throw new ExtensionError(
`Illegal to set non-private cookieStoreId in a private window`
);
}
if (!isPrivateBrowsing && isPrivateCookieStoreId(cookieStoreId)) {
throw new ExtensionError(
`Illegal to set private cookieStoreId in a non-private window`
);
}
if (isContainerCookieStoreId(cookieStoreId)) {
if (PrivateBrowsingUtils.permanentPrivateBrowsing) {
// Container tabs are not supported in perma-private browsing mode - bug 1320757
throw new ExtensionError(
`Contextual identities are unavailable in permanent private browsing mode`
);
}
if (!containersEnabled) {
throw new ExtensionError(`Contextual identities are currently disabled`);
}
let userContextId = getContainerForCookieStoreId(cookieStoreId);
if (!userContextId) {
throw new ExtensionError(
`No cookie store exists with ID ${cookieStoreId}`
);
}
if (!extension.canAccessContainer(userContextId)) {
throw new ExtensionError(`Cannot access ${cookieStoreId}`);
}
return userContextId;
}
return Services.scriptSecurityManager.DEFAULT_USER_CONTEXT_ID;
}
Object.assign(global, {
TabTrackerBase,
TabManagerBase,
TabBase,
WindowTrackerBase,
WindowManagerBase,
WindowBase,
getUserContextIdForCookieStoreId,
});