зеркало из https://github.com/mozilla/gecko-dev.git
1812 строки
53 KiB
JavaScript
1812 строки
53 KiB
JavaScript
/* -*- 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";
|
|
|
|
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
|
|
|
|
/* exported TabTrackerBase, TabManagerBase, TabBase, WindowTrackerBase, WindowManagerBase, WindowBase */
|
|
|
|
var EXPORTED_SYMBOLS = ["TabTrackerBase", "TabManagerBase", "TabBase", "WindowTrackerBase", "WindowManagerBase", "WindowBase"];
|
|
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
|
|
"resource://gre/modules/PrivateBrowsingUtils.jsm");
|
|
XPCOMUtils.defineLazyModuleGetter(this, "Services",
|
|
"resource://gre/modules/Services.jsm");
|
|
|
|
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
|
|
|
|
const {
|
|
DefaultMap,
|
|
DefaultWeakMap,
|
|
EventEmitter,
|
|
ExtensionError,
|
|
getWinUtils,
|
|
} = 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} 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;
|
|
}
|
|
|
|
/**
|
|
* Sends a message, via the given context, to the ExtensionContent running in
|
|
* this tab. The tab's current innerWindowID is automatically added to the
|
|
* recipient filter for the message, and is used to ensure that the message is
|
|
* not processed if the content process navigates to a different content page
|
|
* before the message is received.
|
|
*
|
|
* @param {BaseContext} context
|
|
* The context through which to send the message.
|
|
* @param {string} messageName
|
|
* The name of the messge to send.
|
|
* @param {object} [data = {}]
|
|
* Arbitrary, structured-clonable message data to send.
|
|
* @param {object} [options]
|
|
* An options object, as accepted by BaseContext.sendMessage.
|
|
*
|
|
* @returns {Promise}
|
|
*/
|
|
sendMessage(context, messageName, data = {}, options = null) {
|
|
let {browser, innerWindowID} = this;
|
|
|
|
options = Object.assign({}, options);
|
|
options.recipient = Object.assign({innerWindowID}, options.recipient);
|
|
|
|
return context.sendMessage(browser.messageManager, messageName,
|
|
data, options);
|
|
}
|
|
|
|
/**
|
|
* Capture the visible area of this tab, and return the result as a data: URL.
|
|
*
|
|
* @param {BaseContext} context
|
|
* The extension context for which to perform the capture.
|
|
* @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.
|
|
*
|
|
* @returns {Promise<string>}
|
|
*/
|
|
capture(context, options = null) {
|
|
if (!options) {
|
|
options = {};
|
|
}
|
|
if (options.format == null) {
|
|
options.format = "png";
|
|
}
|
|
if (options.quality == null) {
|
|
options.quality = 92;
|
|
}
|
|
|
|
let message = {
|
|
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.nativeTab.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) {
|
|
if (queryInfo.muted !== this.mutedInfo.muted) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
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,
|
|
pinned: this.pinned,
|
|
status: this.status,
|
|
incognito: this.incognito,
|
|
width: this.width,
|
|
height: this.height,
|
|
audible: this.audible,
|
|
mutedInfo: this.mutedInfo,
|
|
};
|
|
|
|
if (this.extension.hasPermission("cookies")) {
|
|
result.cookieStoreId = this.cookieStoreId;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
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`});
|
|
}
|
|
|
|
if (this.hasActiveTabPermission) {
|
|
// If we have the "activeTab" permission for this tab, ignore
|
|
// the host whitelist.
|
|
options.matchesHost = ["<all_urls>"];
|
|
} else {
|
|
options.matchesHost = this.extension.whiteListedHosts.serialize();
|
|
}
|
|
|
|
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].push(url);
|
|
}
|
|
if (details.allFrames) {
|
|
options.all_frames = details.allFrames;
|
|
}
|
|
if (details.frameId !== null) {
|
|
options.frame_id = details.frameId;
|
|
}
|
|
if (details.matchAboutBlank) {
|
|
options.match_about_blank = details.matchAboutBlank;
|
|
}
|
|
if (details.runAt !== null) {
|
|
options.run_at = details.runAt;
|
|
} else {
|
|
options.run_at = "document_idle";
|
|
}
|
|
if (details.cssOrigin !== null) {
|
|
options.css_origin = details.cssOrigin;
|
|
} else {
|
|
options.css_origin = "author";
|
|
}
|
|
|
|
options.wantReturnValue = true;
|
|
|
|
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,
|
|
incognito: this.incognito,
|
|
type: this.type,
|
|
state: this.state,
|
|
alwaysOnTop: this.alwaysOnTop,
|
|
};
|
|
|
|
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");
|
|
}
|
|
|
|
// 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} 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_ = 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;
|
|
}
|
|
|
|
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, 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();
|
|
|
|
this._listeners = new DefaultMap(() => new Set());
|
|
|
|
this._statusListeners = new DefaultWeakMap(listener => {
|
|
return new StatusListener(listener);
|
|
});
|
|
|
|
this._windowIds = new DefaultWeakMap(window => {
|
|
window.QueryInterface(Ci.nsIInterfaceRequestor);
|
|
|
|
return getWinUtils(window).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.
|
|
|
|
let e = Services.wm.getEnumerator("");
|
|
while (e.hasMoreElements()) {
|
|
let window = e.getNext();
|
|
|
|
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");
|
|
}
|
|
|
|
/**
|
|
* 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.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}`);
|
|
}
|
|
|
|
/**
|
|
* @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) {
|
|
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) {
|
|
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) {
|
|
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) {
|
|
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 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) {
|
|
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) {
|
|
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.
|
|
let tab = this.getWrapper(nativeTab);
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* 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} 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}
|
|
* The wrapper for this tab.
|
|
*/
|
|
getWrapper(nativeTab) {
|
|
return this._tabs.get(nativeTab);
|
|
}
|
|
|
|
/**
|
|
* 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} nativeTab
|
|
* The native tab to convert.
|
|
*
|
|
* @returns {Object}
|
|
*/
|
|
convert(nativeTab) {
|
|
return this.getWrapper(nativeTab).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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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} nativeTab
|
|
* The native tab for which to return a wrapper.
|
|
*
|
|
* @returns {TabBase}
|
|
* @protected
|
|
* @abstract
|
|
*/
|
|
/* eslint-enable valid-jsdoc */
|
|
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 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);
|
|
}
|
|
|
|
/**
|
|
* 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 */
|
|
}
|