gecko-dev/toolkit/components/extensions/ext-tabs-base.js
Kris Maglione ea86cfbbc8 Bug 1398630: Part 4 - Use getWinUtils everywhere we use DOMWindowUtils. r=zombie
MozReview-Commit-ID: FroMQF9Tiz1

--HG--
extra : rebase_source : 16bc023dc5ca08358097592b04d6da1a8282b5f1
2017-09-10 15:33:54 -07:00

1876 lines
54 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";
/* globals EventEmitter */
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");
var {
DefaultMap,
DefaultWeakMap,
ExtensionError,
defineLazyGetter,
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 {integer} lastAccessed
* Returns the last time the tab was accessed as the number of
* milliseconds since epoch.
* @readonly
* @abstract
*/
get lastAccessed() {
throw new Error("Not implemented");
}
/**
* @property {boolean} audible
* Returns true if the tab is currently playing audio, false otherwise.
* @readonly
* @abstract
*/
get audible() {
throw new Error("Not implemented");
}
/**
* @property {XULElement} browser
* Returns the XUL browser for the given tab.
* @readonly
* @abstract
*/
get browser() {
throw new Error("Not implemented");
}
/**
* @property {nsIFrameLoader} browser
* Returns the frameloader for the given tab.
* @readonly
*/
get frameLoader() {
return this.browser.frameLoader;
}
/**
* @property {string} cookieStoreId
* Returns the cookie store identifier for the given tab.
* @readonly
* @abstract
*/
get cookieStoreId() {
throw new Error("Not implemented");
}
/**
* @property {integer} openerTabId
* Returns the ID of the tab which opened this one.
* @readonly
*/
get openerTabId() {
return null;
}
/**
* @property {integer} 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", "openerTabId", "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.
*
* @param {Tab} [fallbackTab]
* A tab to retrieve geometry data from if the lazy geometry data for
* this tab hasn't been initialized yet.
* @returns {object}
*/
convert(fallbackTab = null) {
let result = {
id: this.id,
index: this.index,
windowId: this.windowId,
highlighted: this.selected,
active: this.selected,
pinned: this.pinned,
status: this.status,
discarded: this.discarded,
incognito: this.incognito,
width: this.width,
height: this.height,
lastAccessed: this.lastAccessed,
audible: this.audible,
mutedInfo: this.mutedInfo,
};
// If the tab has not been fully layed-out yet, fallback to the geometry
// from a different tab (usually the currently active tab).
if (fallbackTab && (!result.width || !result.height)) {
result.width = fallbackTab.width;
result.height = fallbackTab.height;
}
let opener = this.openerTabId;
if (opener) {
result.openerTabId = opener;
}
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.matches = ["<all_urls>"];
} else {
options.matches = this.extension.whiteListedHosts.patterns.map(host => host.pattern);
}
if (details.code !== null) {
options[`${kind}Code`] = details.code;
}
if (details.file !== null) {
let url = context.uri.resolve(details.file);
if (!this.extension.isExtensionURL(url)) {
return Promise.reject({message: "Files to be injected must be within the extension"});
}
options[kind].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(() => {});
}
}
defineLazyGetter(TabBase.prototype, "incognito", function() { return this._incognito; });
// Note: These must match the values in windows.json.
const WINDOW_ID_NONE = -1;
const WINDOW_ID_CURRENT = -2;
/**
* A platform-independent base class for extension-specific wrappers around
* native browser windows
*
* @param {Extension} extension
* The extension object for which this wrapper is being created.
* @param {DOMWindow} window
* The browser DOM window which is being wrapped.
* @param {integer} id
* The numeric ID of this DOM window object. This ID should be the same for
* every extension, and for the lifetime of the window.
*/
class WindowBase {
constructor(extension, window, id) {
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.document.docShell.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,
title: this.title,
};
if (getInfo && getInfo.populate) {
result.tabs = Array.from(this.getTabs(), tab => tab.convert());
}
return result;
}
/**
* Returns true if this window matches the the given query info object. Omitted
* or null have no effect on the match.
*
* @param {object} queryInfo
* The query info against which to match.
* @param {boolean} [queryInfo.currentWindow]
* Matches against against the return value of `isCurrentFor()` for the
* given context.
* @param {boolean} [queryInfo.lastFocusedWindow]
* Matches against the exact value of the window's `isLastFocused` attribute.
* @param {boolean} [queryInfo.windowId]
* Matches against the exact value of the window's ID, taking into
* account the special WINDOW_ID_CURRENT value.
* @param {string} [queryInfo.windowType]
* Matches against the exact value of the window's `type` attribute.
* @param {BaseContext} context
* The extension context for which the matching is being performed.
* Used to determine the current window for relevant properties.
*
* @returns {boolean}
* True if the window matches the query.
*/
matches(queryInfo, context) {
if (queryInfo.lastFocusedWindow !== null && queryInfo.lastFocusedWindow !== this.isLastFocused) {
return false;
}
if (queryInfo.windowType !== null && queryInfo.windowType !== this.type) {
return false;
}
if (queryInfo.windowId !== null) {
if (queryInfo.windowId === WINDOW_ID_CURRENT) {
if (!this.isCurrentFor(context)) {
return false;
}
} else if (queryInfo.windowId !== this.id) {
return false;
}
}
if (queryInfo.currentWindow !== null && queryInfo.currentWindow !== this.isCurrentFor(context)) {
return false;
}
return true;
}
/**
* @property {boolean} focused
* Returns true if the browser window is currently focused.
* @readonly
* @abstract
*/
get focused() {
throw new Error("Not implemented");
}
/**
* @property {integer} top
* Returns the pixel offset of the top of the window from the top of
* the screen.
* @readonly
* @abstract
*/
get top() {
throw new Error("Not implemented");
}
/**
* @property {integer} left
* Returns the pixel offset of the left of the window from the left of
* the screen.
* @readonly
* @abstract
*/
get left() {
throw new Error("Not implemented");
}
/**
* @property {integer} width
* Returns the pixel width of the window.
* @readonly
* @abstract
*/
get width() {
throw new Error("Not implemented");
}
/**
* @property {integer} height
* Returns the pixel height of the window.
* @readonly
* @abstract
*/
get height() {
throw new Error("Not implemented");
}
/**
* @property {boolean} incognito
* Returns true if this is a private browsing window, false otherwise.
* @readonly
* @abstract
*/
get incognito() {
throw new Error("Not implemented");
}
/**
* @property {boolean} alwaysOnTop
* Returns true if this window is constrained to always remain above
* other windows.
* @readonly
* @abstract
*/
get alwaysOnTop() {
throw new Error("Not implemented");
}
/**
* @property {boolean} isLastFocused
* Returns true if this is the browser window which most recently had
* focus.
* @readonly
* @abstract
*/
get isLastFocused() {
throw new Error("Not implemented");
}
/**
* @property {string} state
* Returns or sets the current state of this window, as determined by
* `getState()`.
* @abstract
*/
get state() {
throw new Error("Not implemented");
}
set state(state) {
throw new Error("Not implemented");
}
/**
* @property {nsIURI | null} title
* Returns the current title of this window if the extension has permission
* to read it, or null otherwise.
* @readonly
*/
get title() {
if (this.activeTab.hasTabPermission) {
return this._title;
}
}
// 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");
}
/**
* @property {TabBase} The window's currently active tab.
*/
get activeTab() {
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 => {
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.
* @param {NativeTab} [fallbackTab]
* A tab to retrieve geometry data from if the lazy geometry data for
* this tab hasn't been initialized yet.
*
* @returns {Object}
*/
convert(nativeTab, fallbackTab = null) {
return this.getWrapper(nativeTab)
.convert(fallbackTab && this.getWrapper(fallbackTab));
}
// 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 */
}
Object.assign(global, {TabTrackerBase, TabManagerBase, TabBase, WindowTrackerBase, WindowManagerBase, WindowBase});