fune/mobile/android/components/extensions/ext-utils.js
Victor Porof 9e9be26a9b Bug 1561435 - Fix linting errors for mobile/, r=standard8
# ignore-this-changeset

Differential Revision: https://phabricator.services.mozilla.com/D35915

--HG--
extra : source : 2072cc33ae8e2b64a761ece5ab32ce711d547c97
extra : intermediate-source : afb89fcad8e41f4f03ea5d16a7528002820b8305
2019-06-20 11:33:11 -07:00

794 lines
19 KiB
JavaScript

/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
ChromeUtils.defineModuleGetter(
this,
"PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm"
);
/* globals EventDispatcher */
var { EventDispatcher } = ChromeUtils.import(
"resource://gre/modules/Messaging.jsm"
);
var { ExtensionCommon } = ChromeUtils.import(
"resource://gre/modules/ExtensionCommon.jsm"
);
var { ExtensionUtils } = ChromeUtils.import(
"resource://gre/modules/ExtensionUtils.jsm"
);
var { DefaultWeakMap, ExtensionError } = ExtensionUtils;
var { defineLazyGetter } = ExtensionCommon;
global.GlobalEventDispatcher = EventDispatcher.instance;
const BrowserStatusFilter = Components.Constructor(
"@mozilla.org/appshell/component/browser-status-filter;1",
"nsIWebProgress",
"addProgressListener"
);
let tabTracker;
let windowTracker;
/**
* A nsIWebProgressListener for a specific XUL browser, which delegates the
* events that it receives to a tab progress listener, and prepends the browser
* to their arguments list.
*
* @param {XULElement} browser
* A XUL browser element.
* @param {object} listener
* A tab progress listener object.
* @param {integer} flags
* The web progress notification flags with which to filter events.
*/
class BrowserProgressListener {
constructor(browser, listener, flags) {
this.listener = listener;
this.browser = browser;
this.filter = new BrowserStatusFilter(this, flags);
this.browser.addProgressListener(this.filter, flags);
}
/**
* Destroy the listener, and perform any necessary cleanup.
*/
destroy() {
this.browser.removeProgressListener(this.filter);
this.filter.removeProgressListener(this);
}
/**
* Calls the appropriate listener in the wrapped tab progress listener, with
* the wrapped XUL browser object as its first argument, and the additional
* arguments in `args`.
*
* @param {string} method
* The name of the nsIWebProgressListener method which is being
* delegated.
* @param {*} args
* The arguments to pass to the delegated listener.
* @private
*/
delegate(method, ...args) {
if (this.listener[method]) {
this.listener[method](this.browser, ...args);
}
}
onLocationChange(webProgress, request, locationURI, flags) {
this.delegate("onLocationChange", webProgress, request, locationURI, flags);
}
onStateChange(webProgress, request, stateFlags, status) {
this.delegate("onStateChange", webProgress, request, stateFlags, status);
}
}
/**
* Handles wrapping a tab progress listener in browser-specific
* BrowserProgressListener instances, an attaching them to each tab in a given
* browser window.
*
* @param {DOMWindow} window
* The browser window to which to attach the listeners.
* @param {object} listener
* The tab progress listener to wrap.
*/
class ProgressListenerWrapper {
constructor(window, listener) {
this.window = window;
this.listener = listener;
this.listeners = new WeakMap();
this.flags =
Ci.nsIWebProgress.NOTIFY_STATE_ALL | Ci.nsIWebProgress.NOTIFY_LOCATION;
for (let nativeTab of this.window.BrowserApp.tabs) {
this.addBrowserProgressListener(nativeTab.browser);
}
this.window.BrowserApp.deck.addEventListener("TabOpen", this);
}
/**
* Destroy the wrapper, removing any remaining listeners it has added.
*/
destroy() {
this.window.BrowserApp.deck.removeEventListener("TabOpen", this);
for (let nativeTab of this.window.BrowserApp.tabs) {
this.removeProgressListener(nativeTab.browser);
}
}
/**
* Adds a progress listener to the given XUL browser element.
*
* @param {XULElement} browser
* The XUL browser to add the listener to.
* @private
*/
addBrowserProgressListener(browser) {
this.removeProgressListener(browser);
let listener = new BrowserProgressListener(
browser,
this.listener,
this.flags
);
this.listeners.set(browser, listener);
}
/**
* Removes a progress listener from the given XUL browser element.
*
* @param {XULElement} browser
* The XUL browser to remove the listener from.
* @private
*/
removeProgressListener(browser) {
let listener = this.listeners.get(browser);
if (listener) {
listener.destroy();
this.listeners.delete(browser);
}
}
/**
* Handles tab open events, and adds the necessary progress listeners to the
* new tabs.
*
* @param {Event} event
* The DOM event to handle.
* @private
*/
handleEvent(event) {
if (event.type === "TabOpen") {
this.addBrowserProgressListener(event.originalTarget);
}
}
}
class WindowTracker extends WindowTrackerBase {
constructor(...args) {
super(...args);
this.progressListeners = new DefaultWeakMap(() => new WeakMap());
}
get topWindow() {
return (
Services.wm.getMostRecentWindow("navigator:browser") ||
Services.wm.getMostRecentWindow("navigator:geckoview")
);
}
addProgressListener(window, listener) {
let listeners = this.progressListeners.get(window);
if (!listeners.has(listener)) {
let wrapper = new ProgressListenerWrapper(window, listener);
listeners.set(listener, wrapper);
}
}
removeProgressListener(window, listener) {
let listeners = this.progressListeners.get(window);
let wrapper = listeners.get(listener);
if (wrapper) {
wrapper.destroy();
listeners.delete(listener);
}
}
}
/**
* Helper to create an event manager which listens for an event in the Android
* global EventDispatcher, and calls the given listener function whenever the
* event is received. That listener function receives a `fire` object,
* which it can use to dispatch events to the extension, and an object
* detailing the EventDispatcher event that was received.
*
* @param {BaseContext} context
* The extension context which the event manager belongs to.
* @param {string} name
* The API name of the event manager, e.g.,"runtime.onMessage".
* @param {string} event
* The name of the EventDispatcher event to listen for.
* @param {function} listener
* The listener function to call when an EventDispatcher event is
* recieved.
*
* @returns {object} An injectable api for the new event.
*/
global.makeGlobalEvent = function makeGlobalEvent(
context,
name,
event,
listener
) {
return new EventManager({
context,
name,
register: fire => {
let listener2 = {
onEvent(event, data, callback) {
listener(fire, data);
},
};
GlobalEventDispatcher.registerListener(listener2, [event]);
return () => {
GlobalEventDispatcher.unregisterListener(listener2, [event]);
};
},
}).api();
};
class TabTracker extends TabTrackerBase {
constructor() {
super();
// Keep track of the extension popup tab.
this._extensionPopupTabWeak = null;
// Keep track of the selected tabId
this._selectedTabId = null;
}
init() {
if (this.initialized) {
return;
}
this.initialized = true;
windowTracker.addListener("TabClose", this);
windowTracker.addListener("TabOpen", this);
// Register a listener for the Tab:Selected global event,
// so that we can close the popup when a popup tab has been
// unselected.
GlobalEventDispatcher.registerListener(this, ["Tab:Selected"]);
}
/**
* Returns the currently opened popup tab if any
*/
get extensionPopupTab() {
if (this._extensionPopupTabWeak) {
const tab = this._extensionPopupTabWeak.get();
// Return the native tab only if the tab has not been removed in the meantime.
if (tab.browser) {
return tab;
}
// Clear the tracked popup tab if it has been closed in the meantime.
this._extensionPopupTabWeak = null;
}
return undefined;
}
/**
* Open a pageAction/browserAction popup url in a tab and keep track of
* its weak reference (to be able to customize the activedTab using the tab parentId,
* to skip it in the tabs.query and to set the parent tab as active when the popup
* tab is currently selected).
*
* @param {string} popup
* The popup url to open in a tab.
*/
openExtensionPopupTab(popup) {
let win = windowTracker.topWindow;
if (!win) {
throw new ExtensionError(
`Unable to open a popup without an active window`
);
}
if (this.extensionPopupTab) {
win.BrowserApp.closeTab(this.extensionPopupTab);
}
this.init();
let { browser, id } = win.BrowserApp.selectedTab;
let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(browser);
this._extensionPopupTabWeak = Cu.getWeakReference(
win.BrowserApp.addTab(popup, {
selected: true,
parentId: id,
isPrivate,
})
);
}
getId(nativeTab) {
return nativeTab.id;
}
getTab(id, default_ = undefined) {
let win = windowTracker.topWindow;
if (win) {
let nativeTab = win.BrowserApp.getTabForId(id);
if (nativeTab) {
return nativeTab;
}
}
if (default_ !== undefined) {
return default_;
}
throw new ExtensionError(`Invalid tab ID: ${id}`);
}
/**
* Handles tab open and close events, and emits the appropriate internal
* events for them.
*
* @param {Event} event
* A DOM event to handle.
* @private
*/
handleEvent(event) {
const { BrowserApp } = event.target.ownerGlobal;
const nativeTab = BrowserApp.getTabForBrowser(event.target);
switch (event.type) {
case "TabOpen":
this.emitCreated(nativeTab);
break;
case "TabClose":
this.emitRemoved(nativeTab, false);
break;
}
}
/**
* Required by the GlobalEventDispatcher module. This event will get
* called whenever one of the registered listeners fires.
* @param {string} event The event which fired.
* @param {object} data Information about the event which fired.
*/
onEvent(event, data) {
const { BrowserApp } = windowTracker.topWindow;
switch (event) {
case "Tab:Selected": {
this._selectedTabId = data.id;
// If a new tab has been selected while an extension popup tab is still open,
// close it immediately.
const nativeTab = BrowserApp.getTabForId(data.id);
const popupTab = tabTracker.extensionPopupTab;
if (popupTab && popupTab !== nativeTab) {
BrowserApp.closeTab(popupTab);
}
break;
}
}
}
/**
* Emits a "tab-created" event for the given tab element.
*
* @param {NativeTab} nativeTab
* The tab element which is being created.
* @private
*/
emitCreated(nativeTab) {
this.emit("tab-created", { nativeTab });
}
/**
* Emits a "tab-removed" event for the given tab element.
*
* @param {NativeTab} nativeTab
* The tab element which is being removed.
* @param {boolean} isWindowClosing
* True if the tab is being removed because the browser window is
* closing.
* @private
*/
emitRemoved(nativeTab, isWindowClosing) {
let windowId = windowTracker.getId(nativeTab.browser.ownerGlobal);
let tabId = this.getId(nativeTab);
if (this.extensionPopupTab && this.extensionPopupTab === nativeTab) {
this._extensionPopupTabWeak = null;
// Do not switch to the parent tab of the extension popup tab
// if the popup tab is not the selected tab.
if (this._selectedTabId !== tabId) {
return;
}
// Select the parent tab when the closed tab was an extension popup tab.
const { BrowserApp } = windowTracker.topWindow;
const popupParentTab = BrowserApp.getTabForId(nativeTab.parentId);
if (popupParentTab) {
BrowserApp.selectTab(popupParentTab);
}
}
Services.tm.dispatchToMainThread(() => {
this.emit("tab-removed", { nativeTab, tabId, windowId, isWindowClosing });
});
}
getBrowserData(browser) {
let result = {
tabId: -1,
windowId: -1,
};
let { BrowserApp } = browser.ownerGlobal;
if (BrowserApp) {
result.windowId = windowTracker.getId(browser.ownerGlobal);
let nativeTab = BrowserApp.getTabForBrowser(browser);
if (nativeTab) {
result.tabId = this.getId(nativeTab);
}
}
return result;
}
get activeTab() {
let win = windowTracker.topWindow;
if (win && win.BrowserApp) {
const selectedTab = win.BrowserApp.selectedTab;
// If the current tab is an extension popup tab, we use the parentId to retrieve
// and return the tab that was selected when the popup tab has been opened.
if (selectedTab === this.extensionPopupTab) {
return win.BrowserApp.getTabForId(selectedTab.parentId);
}
return selectedTab;
}
return null;
}
}
windowTracker = new WindowTracker();
tabTracker = new TabTracker();
Object.assign(global, { tabTracker, windowTracker });
class Tab extends TabBase {
get _favIconUrl() {
return undefined;
}
get attention() {
return false;
}
get audible() {
return this.nativeTab.playingAudio;
}
get browser() {
return this.nativeTab.browser;
}
get discarded() {
return this.browser.getAttribute("pending") === "true";
}
get cookieStoreId() {
return getCookieStoreIdForTab(this, this.nativeTab);
}
get height() {
return this.browser.clientHeight;
}
get incognito() {
return PrivateBrowsingUtils.isBrowserPrivate(this.browser);
}
get index() {
return this.window.BrowserApp.tabs.indexOf(this.nativeTab);
}
get mutedInfo() {
return { muted: false };
}
get lastAccessed() {
return this.nativeTab.lastTouchedAt;
}
get pinned() {
return false;
}
get active() {
// If there is an extension popup tab and it is active,
// then the parent tab of the extension popup tab is active
// (while the extension popup tab will not be included in the
// tabs.query results).
if (tabTracker.extensionPopupTab) {
if (
tabTracker.extensionPopupTab.getActive() &&
this.nativeTab.id === tabTracker.extensionPopupTab.parentId
) {
return true;
}
// Never return true for an active extension popup, e.g. so that
// the popup tab will not be part of the results of querying
// all the active tabs.
if (tabTracker.extensionPopupTab === this.nativeTab) {
return false;
}
}
return this.nativeTab.getActive();
}
get highlighted() {
return this.active;
}
get selected() {
return this.nativeTab.getActive();
}
get status() {
if (this.browser.webProgress.isLoadingDocument) {
return "loading";
}
return "complete";
}
get successorTabId() {
return -1;
}
get width() {
return this.browser.clientWidth;
}
get window() {
return this.browser.ownerGlobal;
}
get windowId() {
return windowTracker.getId(this.window);
}
// TODO: Just return false for these until properly implemented on Android.
// https://bugzilla.mozilla.org/show_bug.cgi?id=1402924
get isArticle() {
return false;
}
get isInReaderMode() {
return false;
}
get hidden() {
return false;
}
get sharingState() {
return {
screen: undefined,
microphone: false,
camera: false,
};
}
}
// Manages tab-specific context data and dispatches tab select and close events.
class TabContext extends EventEmitter {
constructor(getDefaultPrototype) {
super();
this.getDefaultPrototype = getDefaultPrototype;
this.tabData = new Map();
GlobalEventDispatcher.registerListener(this, [
"Tab:Selected",
"Tab:Closed",
]);
}
get(tabId) {
if (!this.tabData.has(tabId)) {
let data = Object.create(this.getDefaultPrototype(tabId));
this.tabData.set(tabId, data);
}
return this.tabData.get(tabId);
}
clear(tabId) {
this.tabData.delete(tabId);
}
/**
* Required by the GlobalEventDispatcher module. This event will get
* called whenever one of the registered listeners fires.
* @param {string} event The event which fired.
* @param {object} data Information about the event which fired.
*/
onEvent(event, data) {
switch (event) {
case "Tab:Selected":
this.emit("tab-selected", data.id);
break;
case "Tab:Closed":
this.emit("tab-closed", data.tabId);
break;
}
}
shutdown() {
GlobalEventDispatcher.unregisterListener(this, [
"Tab:Selected",
"Tab:Closed",
]);
}
}
class Window extends WindowBase {
get focused() {
return this.window.document.hasFocus();
}
get top() {
return this.window.screenY;
}
get left() {
return this.window.screenX;
}
get width() {
return this.window.outerWidth;
}
get height() {
return this.window.outerHeight;
}
get incognito() {
return PrivateBrowsingUtils.isWindowPrivate(this.window);
}
get alwaysOnTop() {
return false;
}
get isLastFocused() {
return this.window === windowTracker.topWindow;
}
get state() {
return "fullscreen";
}
*getTabs() {
let { tabManager } = this.extension;
for (let nativeTab of this.window.BrowserApp.tabs) {
yield tabManager.getWrapper(nativeTab);
}
}
*getHighlightedTabs() {
yield this.activeTab;
}
get activeTab() {
let { BrowserApp } = this.window;
let { selectedTab } = BrowserApp;
// If the current tab is an extension popup tab, we use the parentId to retrieve
// and return the tab that was selected when the popup tab has been opened.
if (selectedTab === tabTracker.extensionPopupTab) {
selectedTab = BrowserApp.getTabForId(selectedTab.parentId);
}
let { tabManager } = this.extension;
return tabManager.getWrapper(selectedTab);
}
getTabAtIndex(index) {
let nativeTab = this.window.BrowserApp.tabs[index];
if (nativeTab) {
return this.extension.tabManager.getWrapper(nativeTab);
}
}
}
Object.assign(global, { Tab, TabContext, Window });
class TabManager extends TabManagerBase {
get(tabId, default_ = undefined) {
let nativeTab = tabTracker.getTab(tabId, default_);
if (nativeTab) {
return this.getWrapper(nativeTab);
}
return default_;
}
addActiveTabPermission(nativeTab = tabTracker.activeTab) {
return super.addActiveTabPermission(nativeTab);
}
revokeActiveTabPermission(nativeTab = tabTracker.activeTab) {
return super.revokeActiveTabPermission(nativeTab);
}
canAccessTab(nativeTab) {
return (
this.extension.privateBrowsingAllowed ||
!PrivateBrowsingUtils.isBrowserPrivate(nativeTab.browser)
);
}
wrapTab(nativeTab) {
return new Tab(this.extension, nativeTab, nativeTab.id);
}
}
class WindowManager extends WindowManagerBase {
get(windowId, context) {
let window = windowTracker.getWindow(windowId, context);
return this.getWrapper(window);
}
*getAll() {
for (let window of windowTracker.browserWindows()) {
yield this.getWrapper(window);
}
}
wrapWindow(window) {
return new Window(this.extension, window, windowTracker.getId(window));
}
}
// eslint-disable-next-line mozilla/balanced-listeners
extensions.on("startup", (type, extension) => {
defineLazyGetter(extension, "tabManager", () => new TabManager(extension));
defineLazyGetter(
extension,
"windowManager",
() => new WindowManager(extension)
);
});