--- a/browser/components/extensions/ext-utils.js
+++ b/browser/components/extensions/ext-utils.js
@@ -197,16 +197,21 @@ class TabTracker extends TabTrackerBase
return tab;
}
if (default_ !== undefined) {
return default_;
}
throw new ExtensionError(`Invalid tab ID: ${tabId}`);
}
+ /**
+ * @param {Event} event
+ * The DOM Event to handle.
+ * @private
+ */
handleEvent(event) {
let tab = event.target;
switch (event.type) {
case "TabOpen":
let {adoptedTab} = event.detail;
if (adoptedTab) {
this.adoptedTabs.set(adoptedTab, event.target);
@@ -243,16 +248,24 @@ class TabTracker extends TabTrackerBase
this.emitDetached(tab, adoptedBy);
} else {
this.emitRemoved(tab, false);
}
break;
}
}
+ /**
+ * A private method which is called whenever a new browser window is opened,
+ * and dispatches the necessary events for it.
+ *
+ * @param {DOMWindow} window
+ * The window being opened.
+ * @private
+ */
_handleWindowOpen(window) {
if (window.arguments && window.arguments[0] instanceof window.XULElement) {
// If the first window argument is a XUL element, it means the
// window is about to adopt a tab from another window to replace its
// initial tab.
//
// Note that this event handler depends on running before the
// delayed startup code in browser.js, which is currently triggered
@@ -280,44 +293,86 @@ class TabTracker extends TabTrackerBase
this.on("tab-detached", listener);
} else {
for (let tab of window.gBrowser.tabs) {
this.emitCreated(tab);
}
}
}
+ /**
+ * A private method which is called whenever a browser window is closed,
+ * and dispatches the necessary events for it.
+ *
+ * @param {DOMWindow} window
+ * The window being closed.
+ * @private
+ */
_handleWindowClose(window) {
for (let tab of window.gBrowser.tabs) {
if (this.adoptedTabs.has(tab)) {
this.emitDetached(tab, this.adoptedTabs.get(tab));
} else {
this.emitRemoved(tab, true);
}
}
}
+ /**
+ * Emits a "tab-attached" event for the given tab element.
+ *
+ * @param {NativeTab} tab
+ * The tab element in the window to which the tab is being attached.
+ * @private
+ */
emitAttached(tab) {
let newWindowId = windowTracker.getId(tab.ownerGlobal);
let tabId = this.getId(tab);
this.emit("tab-attached", {tab, tabId, newWindowId, newPosition: tab._tPos});
}
+ /**
+ * Emits a "tab-detached" event for the given tab element.
+ *
+ * @param {NativeTab} tab
+ * The tab element in the window from which the tab is being detached.
+ * @param {NativeTab} adoptedBy
+ * The tab element in the window to which detached tab is being moved,
+ * and will adopt this tab's contents.
+ * @private
+ */
emitDetached(tab, adoptedBy) {
let oldWindowId = windowTracker.getId(tab.ownerGlobal);
let tabId = this.getId(tab);
this.emit("tab-detached", {tab, adoptedBy, tabId, oldWindowId, oldPosition: tab._tPos});
}
+ /**
+ * Emits a "tab-created" event for the given tab element.
+ *
+ * @param {NativeTab} tab
+ * The tab element which is being created.
+ * @private
+ */
emitCreated(tab) {
this.emit("tab-created", {tab});
}
+ /**
+ * Emits a "tab-removed" event for the given tab element.
+ *
+ * @param {NativeTab} tab
+ * The tab element which is being removed.
+ * @param {boolean} isWindowClosing
+ * True if the tab is being removed because the browser window is
+ * closing.
+ * @private
+ */
emitRemoved(tab, isWindowClosing) {
let windowId = windowTracker.getId(tab.ownerGlobal);
let tabId = this.getId(tab);
// When addons run in-process, `window.close()` is synchronous. Most other
// addon-invoked calls are asynchronous since they go through a proxy
// context via the message manager. This includes event registrations such
// as `tabs.onRemoved.addListener`.
@@ -394,20 +449,16 @@ class Tab extends TabBase {
get height() {
return this.browser.clientHeight;
}
get index() {
return this.tab._tPos;
}
- get innerWindowID() {
- return this.browser.innerWindowID;
- }
-
get mutedInfo() {
let tab = this.tab;
let mutedInfo = {muted: tab.muted};
if (tab.muteReason === null) {
mutedInfo.reason = "user";
} else if (tab.muteReason) {
mutedInfo.reason = "extension";
@@ -443,42 +494,74 @@ class Tab extends TabBase {
get window() {
return this.tab.ownerGlobal;
}
get windowId() {
return windowTracker.getId(this.window);
}
- static convertFromSessionStoreClosedData(extension, tab, window = null) {
+ /**
+ * Converts session store data to an object compatible with the return value
+ * of the convert() method, representing that data.
+ *
+ * @param {Extension} extension
+ * The extension for which to convert the data.
+ * @param {Object} tabData
+ * Session store data for a closed tab, as returned by
+ * `SessionStore.getClosedTabData()`.
+ * @param {DOMWindow} [window = null]
+ * The browser window which the tab belonged to before it was closed.
+ * May be null if the window the tab belonged to no longer exists.
+ *
+ * @returns {Object}
+ * @static
+ */
+ static convertFromSessionStoreClosedData(extension, tabData, window = null) {
let result = {
- sessionId: String(tab.closedId),
- index: tab.pos ? tab.pos : 0,
+ sessionId: String(tabData.closedId),
+ index: tabData.pos ? tabData.pos : 0,
windowId: window && windowTracker.getId(window),
selected: false,
highlighted: false,
active: false,
pinned: false,
- incognito: Boolean(tab.state && tab.state.isPrivate),
+ incognito: Boolean(tabData.state && tabData.state.isPrivate),
};
- if (extension.tabManager.hasTabPermission(tab)) {
- let entries = tab.state ? tab.state.entries : tab.entries;
+ if (extension.tabManager.hasTabPermission(tabData)) {
+ let entries = tabData.state ? tabData.state.entries : tabData.entries;
result.url = entries[0].url;
result.title = entries[0].title;
- if (tab.image) {
- result.favIconUrl = tab.image;
+ if (tabData.image) {
+ result.favIconUrl = tabData.image;
}
}
return result;
}
}
class Window extends WindowBase {
+ /**
+ * Update the geometry of the browser window.
+ *
+ * @param {Object} options
+ * An object containing new values for the window's geometry.
+ * @param {integer} [options.left]
+ * The new pixel distance of the left side of the browser window from
+ * the left of the screen.
+ * @param {integer} [options.top]
+ * The new pixel distance of the top side of the browser window from
+ * the top of the screen.
+ * @param {integer} [options.width]
+ * The new pixel width of the window.
+ * @param {integer} [options.height]
+ * The new pixel height of the window.
+ */
updateGeometry(options) {
let {window} = this;
if (options.left !== null || options.top !== null) {
let left = options.left !== null ? options.left : window.screenX;
let top = options.top !== null ? options.top : window.screenY;
window.moveTo(left, top);
}
@@ -582,29 +665,42 @@ class Window extends WindowBase {
* getTabs() {
let {tabManager} = this.extension;
for (let tab of this.window.gBrowser.tabs) {
yield tabManager.getWrapper(tab);
}
}
- static convertFromSessionStoreClosedData(extension, window) {
+ /**
+ * Converts session store data to an object compatible with the return value
+ * of the convert() method, representing that data.
+ *
+ * @param {Extension} extension
+ * The extension for which to convert the data.
+ * @param {Object} windowData
+ * Session store data for a closed window, as returned by
+ * `SessionStore.getClosedWindowData()`.
+ *
+ * @returns {Object}
+ * @static
+ */
+ static convertFromSessionStoreClosedData(extension, windowData) {
let result = {
- sessionId: String(window.closedId),
+ sessionId: String(windowData.closedId),
focused: false,
incognito: false,
type: "normal", // this is always "normal" for a closed window
// Surely this does not actually work?
- state: this.getState(window),
+ state: this.getState(windowData),
alwaysOnTop: false,
};
- if (window.tabs.length) {
- result.tabs = window.tabs.map(tab => {
+ if (windowData.tabs.length) {
+ result.tabs = windowData.tabs.map(tab => {
return Tab.convertFromSessionStoreClosedData(extension, tab);
});
}
return result;
}
}
--- a/toolkit/components/extensions/ExtensionTabs.jsm
+++ b/toolkit/components/extensions/ExtensionTabs.jsm
@@ -22,16 +22,50 @@ Cu.import("resource://gre/modules/Extens
const {
DefaultMap,
DefaultWeakMap,
EventEmitter,
ExtensionError,
} = ExtensionUtils;
+/**
+ * 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} tab
+ * 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, tab, id) {
this.extension = extension;
this.tabManager = extension.tabManager;
this.id = id;
this.tab = tab;
this.activeTabWindowID = null;
}
@@ -95,67 +129,310 @@ class TabBase {
options,
width: this.width,
height: this.height,
};
return this.sendMessage(context, "Extension:Capture", message);
}
+ /**
+ * @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;
}
+ /**
+ * @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.hasPermission("activeTab") &&
this.activeTabWindowID != null &&
this.activeTabWindowID === this.innerWindowID);
}
+ /**
+ * @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 | null} url
+ * Returns the current URL of this tab if the extension has permission
+ * to read it, or null otherwise.
+ * @readonly
+ */
get url() {
if (this.hasTabPermission) {
return this._url;
}
}
+ /**
+ * @property {nsIURI | null} uri
+ * Returns the current URI of this tab if the extension has permission
+ * to read it, or null otherwise.
+ * @readonly
+ */
get uri() {
if (this.hasTabPermission) {
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.tab.label;
}
+ /**
+ * @property {nsIURI | null} title
+ * Returns the current title of this tab if the extension has permission
+ * to read it, or null otherwise.
+ * @readonly
+ */
get title() {
if (this.hasTabPermission) {
return this._title;
}
}
+ /**
+ * @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 | null} faviconUrl
+ * Returns the current faviron URL of this tab if the extension has permission
+ * to read it, or null otherwise.
+ * @readonly
+ */
get favIconUrl() {
if (this.hasTabPermission) {
return this._favIconUrl;
}
}
+ /**
+ * @property {boolean} audible
+ * Returns true if the tab is currently playing audio, false otherwise.
+ * @readonly
+ * @abstract
+ */
+ get audible() {
+ 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 {string} cookieStoreId
+ * Returns the cookie store identifier for the given tab.
+ * @readonly
+ * @abstract
+ */
+ get cookieStoreId() {
+ 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} 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 {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} selected
+ * An alias for `active`.
+ * @readonly
+ * @abstract
+ */
+ get selected() {
+ 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");
+ }
+
+ /**
+ * 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 {string} [queryInfo.cookieStoreId]
+ * Matches against the exact value of the tab's `cookieStoreId` 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.
+ *
+ * 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", "cookieStoreId", "highlighted", "index", "pinned", "status", "title"];
if (PROPS.some(prop => queryInfo[prop] !== null && queryInfo[prop] !== this[prop])) {
return false;
}
if (queryInfo.muted !== null) {
@@ -166,16 +443,23 @@ class TabBase {
if (queryInfo.url && !queryInfo.url.matches(this.uri)) {
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
+ * requried to be returned by WebExtension APIs.
+ *
+ * @returns {object}
+ */
convert() {
let result = {
id: this.id,
index: this.index,
windowId: this.windowId,
selected: this.selected,
highlighted: this.selected,
active: this.selected,
@@ -201,16 +485,35 @@ class TabBase {
result[prop] = val;
}
}
}
return result;
}
+ /**
+ * 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 = {
js: [],
css: [],
remove_css: method == "removeCSS",
};
// We require a `code` or a `file` property, but we can't accept both.
@@ -258,64 +561,153 @@ class TabBase {
options.css_origin = details.cssOrigin;
} else {
options.css_origin = "author";
}
return this.sendMessage(context, "Extension: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(() => {});
}
}
// 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) {
this.extension = extension;
this.window = window;
this.id = id;
}
+ /**
+ * @property {nsIXULWindow} xulWindow
+ * The nsIXULWindow object for this browser window.
+ * @readonly
+ */
get xulWindow() {
return this.window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDocShell)
.treeOwner.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIXULWindow);
}
+ /**
+ * 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.xulWindow;
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 requried 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,
@@ -327,16 +719,39 @@ class WindowBase {
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;
}
@@ -352,30 +767,331 @@ class WindowBase {
}
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");
+ }
+
+ // 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");
+ }
+ /* 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 containg 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} tab
+ * The native tab for which to return an ID.
+ *
+ * @returns {integer}
+ * The tab's numeric ID.
+ * @abstract
+ */
+ getId(tab) {
+ 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_ = undefined) {
+ 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(browser) {
+ 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)} 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;
@@ -401,16 +1117,21 @@ class StatusListener {
onLocationChange(browser, webProgress, request, locationURI, flags) {
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();
@@ -429,18 +1150,29 @@ class WindowTrackerBase extends EventEmi
}
isBrowserWindow(window) {
let {documentElement} = window.document;
return documentElement.getAttribute("windowtype") === "navigator:browser";
}
- // Returns an iterator for all browser windows. Unless |includeIncomplete| is
- // true, only fully-loaded windows are returned.
+ // 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
@@ -456,87 +1188,162 @@ class WindowTrackerBase extends EventEmi
}
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");
}
+ /**
+ * 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) {
- let {xulWindow} = context;
- if (xulWindow && context.viewType !== "background") {
- return xulWindow;
- }
- return this.topWindow;
+ return context.currentWindow || this.topWindow;
}
+ /**
+ * 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.
+ *
+ * @returns {DOMWindow}
+ * @throws {ExtensionError}
+ * If no window exists with the given ID.
+ */
getWindow(id, context) {
if (id === WINDOW_ID_CURRENT) {
return this.getCurrentWindow(context);
}
for (let window of this.browserWindows(true)) {
if (this.getId(window) === id) {
return window;
}
}
throw new ExtensionError(`Invalid window ID: ${id}`);
}
- get haveListeners() {
+ /**
+ * @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)} listener
+ * The listener function to register.
+ */
addOpenListener(listener) {
- if (!this.haveListeners) {
+ 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)} listener
+ * The listener function to unregister.
+ */
removeOpenListener(listener) {
this._openListeners.delete(listener);
- if (!this.haveListeners) {
+ if (!this._haveListeners) {
Services.ww.unregisterNotification(this);
}
}
+ /**
+ * Register the given listener function to be called whenever a browser
+ * window is closed.
+ *
+ * @param {function(DOMWindow)} listener
+ * The listener function to register.
+ */
addCloseListener(listener) {
- if (!this.haveListeners) {
+ if (!this._haveListeners) {
Services.ww.registerNotification(this);
}
this._closeListeners.add(listener);
}
+ /**
+ * Unregister a listener function registered in a previous addCloseListener
+ * call.
+ *
+ * @param {function(DOMWindow)} listener
+ * The listener function to unregister.
+ */
removeCloseListener(listener) {
this._closeListeners.delete(listener);
- if (!this.haveListeners) {
+ 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;
}
@@ -546,17 +1353,28 @@ class WindowTrackerBase extends EventEmi
listener(window);
} catch (e) {
Cu.reportError(e);
}
}
}
}
- observe(window, topic, data) {
+ /**
+ * 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 {
@@ -565,19 +1383,34 @@ class WindowTrackerBase extends EventEmi
Cu.reportError(e);
}
}
} else if (topic === "domwindowopened") {
window.addEventListener("load", this);
}
}
- // If |type| is a normal event type, invoke |listener| each time
- // that event fires in any open window. If |type| is "progress", add
- // a web progress listener that covers all open windows.
+ /**
+ * Add an event listener to be called whenever the given DOM event is recieved
+ * 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) {
@@ -592,140 +1425,384 @@ class WindowTrackerBase extends EventEmi
this._listeners.get(type).add(listener);
// Register listener on all existing windows.
for (let window of this.browserWindows()) {
this._addWindowListener(window, type, listener);
}
}
- removeListener(eventType, listener) {
- if (eventType === "domwindowopened") {
+ /**
+ * 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 (eventType === "domwindowclosed") {
+ } else if (type === "domwindowclosed") {
return this.removeCloseListener(listener);
}
- if (eventType === "status") {
+ if (type === "status") {
listener = this._statusListeners.get(listener);
- eventType = "progress";
+ type = "progress";
}
- let listeners = this._listeners.get(eventType);
+ let listeners = this._listeners.get(type);
listeners.delete(listener);
if (listeners.size === 0) {
- this._listeners.delete(eventType);
+ this._listeners.delete(type);
if (this._listeners.size === 0) {
this.removeOpenListener(this._handleWindowOpened);
}
}
// Unregister listener from all existing windows.
- let useCapture = eventType === "focus" || eventType === "blur";
+ let useCapture = type === "focus" || type === "blur";
for (let window of this.browserWindows()) {
- if (eventType === "progress") {
+ if (type === "progress") {
this.removeProgressListener(window, listener);
} else {
- window.removeEventListener(eventType, listener, useCapture);
+ 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} tab
+ * The native tab for which to grant permissions.
+ */
addActiveTabPermission(tab) {
if (this.extension.hasPermission("activeTab")) {
// 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 = this.getWrapper(tab);
tab.activeTabWindowID = tab.innerWindowID;
}
}
+ /**
+ * Revoke the extension's activeTab permissions for the current inner window
+ * of the given native tab.
+ *
+ * @param {NativeTab} tab
+ * The native tab for which to revoke permissions.
+ */
revokeActiveTabPermission(tab) {
this.getWrapper(tab).activeTabWindowID = null;
}
- // Returns true if the extension has the "activeTab" permission for this tab.
- // This is somewhat more permissive than the generic "tabs" permission, as
- // checked by |hasTabPermission|, in that it also allows programmatic script
- // injection without an explicit host permission.
+ /**
+ * Returns true if the extension has requested activeTab permission, and has
+ * been granted permissions for the current inner window if this tab.
+ *
+ * @param {NativeTab} tab
+ * The native tab for which to check permissions.
+ * @returns {boolean}
+ * True if the extension has activeTab permissions for this tab.
+ */
hasActiveTabPermission(tab) {
return this.getWrapper(tab).hasActiveTabPermission;
}
+ /**
+ * 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.
+ *
+ * @param {NativeTab} tab
+ * The native tab for which to check permissions.
+ * @returns {boolean}
+ * True if the extension has permissions for this tab.
+ */
hasTabPermission(tab) {
return this.getWrapper(tab).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} tab
+ * The tab for which to return a wrapper.
+ *
+ * @returns {TabBase}
+ * The wrapper for this tab.
+ */
getWrapper(tab) {
return this._tabs.get(tab);
}
+ /**
+ * Converts the given native tab to a JSON-compatible object, in the format
+ * requried to be returned by WebExtension APIs, which may be safely passed to
+ * extension code.
+ *
+ * @param {NativeTab} tab
+ * The native tab to convert.
+ *
+ * @returns {Object}
+ */
+ convert(tab) {
+ return this.getWrapper(tab).convert();
+ }
+
+ // 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) {
for (let window of this.extension.windowManager.query(queryInfo, context)) {
for (let tab of window.getTabs()) {
if (!queryInfo || tab.matches(queryInfo)) {
yield tab;
}
}
}
}
- convert(tab) {
- return this.getWrapper(tab).convert();
+ /**
+ * Returns a TabBase wrapper for the tab with the given ID.
+ *
+ * @param {integer} id
+ * 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} tab
+ * The native tab for which to return a wrapper.
+ *
+ * @returns {TabBase}
+ * @protected
+ * @abstract
+ */
+ /* eslint-enable valid-jsdoc */
wrapTab(tab) {
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 requried 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);
}
- getWrapper(tab) {
- return this._windows.get(tab);
+ /**
+ * 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}
+ * The wrapper for this tab.
+ */
+ getWrapper(window) {
+ return this._windows.get(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) {
for (let window of this.getAll()) {
if (!queryInfo || window.matches(queryInfo, context)) {
yield window;
}
}
}
+
+ /**
+ * Returns a WindowBase wrapper for the browser window with the given ID.
+ *
+ * @param {integer} id
+ * 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 */
}