655 lines
16 KiB
JavaScript
655 lines
16 KiB
JavaScript
/* 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";
|
|
|
|
/**
|
|
* NOTE: If you change the globals in this file, you must check if the globals
|
|
* list in mobile/android/.eslintrc.js also needs updating.
|
|
*/
|
|
|
|
ChromeUtils.defineESModuleGetters(this, {
|
|
GeckoViewTabBridge: "resource://gre/modules/GeckoViewTab.sys.mjs",
|
|
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
|
|
mobileWindowTracker: "resource://gre/modules/GeckoViewWebExtension.sys.mjs",
|
|
});
|
|
|
|
var { EventDispatcher } = ChromeUtils.importESModule(
|
|
"resource://gre/modules/Messaging.sys.mjs"
|
|
);
|
|
|
|
var { ExtensionCommon } = ChromeUtils.importESModule(
|
|
"resource://gre/modules/ExtensionCommon.sys.mjs"
|
|
);
|
|
var { ExtensionUtils } = ChromeUtils.importESModule(
|
|
"resource://gre/modules/ExtensionUtils.sys.mjs"
|
|
);
|
|
|
|
var { DefaultWeakMap, ExtensionError } = ExtensionUtils;
|
|
|
|
var { defineLazyGetter } = ExtensionCommon;
|
|
|
|
const BrowserStatusFilter = Components.Constructor(
|
|
"@mozilla.org/appshell/component/browser-status-filter;1",
|
|
"nsIWebProgress",
|
|
"addProgressListener"
|
|
);
|
|
|
|
const WINDOW_TYPE = "navigator:geckoview";
|
|
|
|
// We need let to break cyclic dependency
|
|
/* eslint-disable-next-line prefer-const */
|
|
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) {
|
|
const window = this.browser.ownerGlobal;
|
|
// GeckoView windows can become popups at any moment, so we need to check
|
|
// here
|
|
if (!windowTracker.isBrowserWindow(window)) {
|
|
return;
|
|
}
|
|
|
|
this.delegate("onLocationChange", webProgress, request, locationURI, flags);
|
|
}
|
|
onStateChange(webProgress, request, stateFlags, status) {
|
|
this.delegate("onStateChange", webProgress, request, stateFlags, status);
|
|
}
|
|
}
|
|
|
|
const PROGRESS_LISTENER_FLAGS =
|
|
Ci.nsIWebProgress.NOTIFY_STATE_ALL | Ci.nsIWebProgress.NOTIFY_LOCATION;
|
|
|
|
class ProgressListenerWrapper {
|
|
constructor(window, listener) {
|
|
this.listener = new BrowserProgressListener(
|
|
window.browser,
|
|
listener,
|
|
PROGRESS_LISTENER_FLAGS
|
|
);
|
|
}
|
|
|
|
destroy() {
|
|
this.listener.destroy();
|
|
}
|
|
}
|
|
|
|
class WindowTracker extends WindowTrackerBase {
|
|
constructor(...args) {
|
|
super(...args);
|
|
|
|
this.progressListeners = new DefaultWeakMap(() => new WeakMap());
|
|
}
|
|
|
|
getCurrentWindow(context) {
|
|
// In GeckoView the popup is on a separate window so getCurrentWindow for
|
|
// the popup should return whatever is the topWindow.
|
|
if (context?.viewType === "popup") {
|
|
return this.topWindow;
|
|
}
|
|
return super.getCurrentWindow(context);
|
|
}
|
|
|
|
get topWindow() {
|
|
return mobileWindowTracker.topWindow;
|
|
}
|
|
|
|
get topNonPBWindow() {
|
|
return mobileWindowTracker.topNonPBWindow;
|
|
}
|
|
|
|
isBrowserWindow(window) {
|
|
const { documentElement } = window.document;
|
|
return documentElement.getAttribute("windowtype") === WINDOW_TYPE;
|
|
}
|
|
|
|
addProgressListener(window, listener) {
|
|
const listeners = this.progressListeners.get(window);
|
|
if (!listeners.has(listener)) {
|
|
const wrapper = new ProgressListenerWrapper(window, listener);
|
|
listeners.set(listener, wrapper);
|
|
}
|
|
}
|
|
|
|
removeProgressListener(window, listener) {
|
|
const listeners = this.progressListeners.get(window);
|
|
const 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 => {
|
|
const listener2 = {
|
|
onEvent(event, data) {
|
|
listener(fire, data);
|
|
},
|
|
};
|
|
|
|
EventDispatcher.instance.registerListener(listener2, [event]);
|
|
return () => {
|
|
EventDispatcher.instance.unregisterListener(listener2, [event]);
|
|
};
|
|
},
|
|
}).api();
|
|
};
|
|
|
|
class TabTracker extends TabTrackerBase {
|
|
init() {
|
|
if (this.initialized) {
|
|
return;
|
|
}
|
|
this.initialized = true;
|
|
|
|
windowTracker.addOpenListener(window => {
|
|
const nativeTab = window.tab;
|
|
this.emit("tab-created", { nativeTab });
|
|
});
|
|
|
|
windowTracker.addCloseListener(window => {
|
|
const { tab: nativeTab, browser } = window;
|
|
const { windowId, tabId } = this.getBrowserData(browser);
|
|
this.emit("tab-removed", {
|
|
nativeTab,
|
|
tabId,
|
|
windowId,
|
|
// In GeckoView, it is not meaningful to speak of "window closed", because a tab is a window.
|
|
// Until we have a meaningful way to group tabs (and close multiple tabs at once),
|
|
// let's use isWindowClosing: false
|
|
isWindowClosing: false,
|
|
});
|
|
});
|
|
}
|
|
|
|
getId(nativeTab) {
|
|
return nativeTab.id;
|
|
}
|
|
|
|
getTab(id, default_ = undefined) {
|
|
const windowId = GeckoViewTabBridge.tabIdToWindowId(id);
|
|
const window = windowTracker.getWindow(windowId, null, false);
|
|
|
|
if (window) {
|
|
const { tab } = window;
|
|
if (tab) {
|
|
return tab;
|
|
}
|
|
}
|
|
|
|
if (default_ !== undefined) {
|
|
return default_;
|
|
}
|
|
throw new ExtensionError(`Invalid tab ID: ${id}`);
|
|
}
|
|
|
|
getBrowserData(browser) {
|
|
const window = browser.ownerGlobal;
|
|
const tab = window?.tab;
|
|
if (!tab) {
|
|
return {
|
|
tabId: -1,
|
|
windowId: -1,
|
|
};
|
|
}
|
|
|
|
const windowId = windowTracker.getId(window);
|
|
|
|
if (!windowTracker.isBrowserWindow(window)) {
|
|
return {
|
|
windowId,
|
|
tabId: -1,
|
|
};
|
|
}
|
|
|
|
return {
|
|
windowId,
|
|
tabId: this.getId(tab),
|
|
};
|
|
}
|
|
|
|
getBrowserDataForContext(context) {
|
|
if (["tab", "background"].includes(context.viewType)) {
|
|
return this.getBrowserData(context.xulBrowser);
|
|
} else if (context.viewType === "popup") {
|
|
const chromeWindow = windowTracker.getCurrentWindow(context);
|
|
const windowId = chromeWindow ? windowTracker.getId(chromeWindow) : -1;
|
|
return { tabId: -1, windowId };
|
|
}
|
|
|
|
return { tabId: -1, windowId: -1 };
|
|
}
|
|
|
|
get activeTab() {
|
|
const window = windowTracker.topWindow;
|
|
if (window) {
|
|
return window.tab;
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
windowTracker = new WindowTracker();
|
|
const 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 0;
|
|
}
|
|
|
|
get mutedInfo() {
|
|
return { muted: false };
|
|
}
|
|
|
|
get lastAccessed() {
|
|
return this.nativeTab.lastTouchedAt;
|
|
}
|
|
|
|
get pinned() {
|
|
return false;
|
|
}
|
|
|
|
get active() {
|
|
return this.nativeTab.getActive();
|
|
}
|
|
|
|
get highlighted() {
|
|
return this.active;
|
|
}
|
|
|
|
get status() {
|
|
if (this.browser.webProgress.isLoadingDocument) {
|
|
return "loading";
|
|
}
|
|
return "complete";
|
|
}
|
|
|
|
get successorTabId() {
|
|
return -1;
|
|
}
|
|
|
|
get groupId() {
|
|
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 autoDiscardable() {
|
|
// This property reflects whether the browser is allowed to auto-discard.
|
|
// Since extensions cannot do so on Android, we return true here.
|
|
return true;
|
|
}
|
|
|
|
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();
|
|
|
|
windowTracker.addListener("progress", this);
|
|
|
|
this.getDefaultPrototype = getDefaultPrototype;
|
|
this.tabData = new Map();
|
|
}
|
|
|
|
onLocationChange(browser, webProgress, request, locationURI, flags) {
|
|
if (!webProgress.isTopLevel) {
|
|
// Only pageAction and browserAction are consuming the "location-change" event
|
|
// to update their per-tab status, and they should only do so in response of
|
|
// location changes related to the top level frame (See Bug 1493470 for a rationale).
|
|
return;
|
|
}
|
|
const { tab } = browser.ownerGlobal;
|
|
// fromBrowse will be false in case of e.g. a hash change or history.pushState
|
|
const fromBrowse = !(
|
|
flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT
|
|
);
|
|
this.emit(
|
|
"location-change",
|
|
{
|
|
id: tab.id,
|
|
linkedBrowser: browser,
|
|
// TODO: we don't support selected so we just alway say we are
|
|
selected: true,
|
|
},
|
|
fromBrowse
|
|
);
|
|
}
|
|
|
|
get(tabId) {
|
|
if (!this.tabData.has(tabId)) {
|
|
const data = Object.create(this.getDefaultPrototype(tabId));
|
|
this.tabData.set(tabId, data);
|
|
}
|
|
|
|
return this.tabData.get(tabId);
|
|
}
|
|
|
|
clear(tabId) {
|
|
this.tabData.delete(tabId);
|
|
}
|
|
|
|
shutdown() {
|
|
windowTracker.removeListener("progress", this);
|
|
}
|
|
}
|
|
|
|
class Window extends WindowBase {
|
|
get focused() {
|
|
return this.window.document.hasFocus();
|
|
}
|
|
|
|
isCurrentFor(context) {
|
|
// In GeckoView the popup is on a separate window so the current window for
|
|
// the popup is whatever is the topWindow.
|
|
if (context?.viewType === "popup") {
|
|
return mobileWindowTracker.topWindow == this.window;
|
|
}
|
|
return super.isCurrentFor(context);
|
|
}
|
|
|
|
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() {
|
|
yield this.activeTab;
|
|
}
|
|
|
|
*getHighlightedTabs() {
|
|
yield this.activeTab;
|
|
}
|
|
|
|
get activeTab() {
|
|
const { tabManager } = this.extension;
|
|
return tabManager.getWrapper(this.window.tab);
|
|
}
|
|
|
|
getTabAtIndex(index) {
|
|
if (index == 0) {
|
|
return this.activeTab;
|
|
}
|
|
}
|
|
}
|
|
|
|
Object.assign(global, { Tab, TabContext, Window });
|
|
|
|
class TabManager extends TabManagerBase {
|
|
get(tabId, default_ = undefined) {
|
|
const 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) {
|
|
const window = windowTracker.getWindow(windowId, context);
|
|
|
|
return this.getWrapper(window);
|
|
}
|
|
|
|
*getAll(context) {
|
|
for (const window of windowTracker.browserWindows()) {
|
|
if (!this.canAccessWindow(window, context)) {
|
|
continue;
|
|
}
|
|
const wrapped = this.getWrapper(window);
|
|
if (wrapped) {
|
|
yield wrapped;
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
);
|
|
});
|
|
|
|
/* eslint-disable mozilla/balanced-listeners */
|
|
extensions.on("page-shutdown", (type, context) => {
|
|
if (context.viewType == "tab") {
|
|
const window = context.xulBrowser.ownerGlobal;
|
|
if (!windowTracker.isBrowserWindow(window)) {
|
|
// Content in non-browser window, e.g. ContentPage in xpcshell uses
|
|
// chrome://extensions/content/dummy.xhtml as the window.
|
|
return;
|
|
}
|
|
GeckoViewTabBridge.closeTab({
|
|
window,
|
|
extensionId: context.extension.id,
|
|
});
|
|
}
|
|
});
|
|
/* eslint-enable mozilla/balanced-listeners */
|
|
|
|
global.openOptionsPage = async extension => {
|
|
const { optionsPageProperties } = extension;
|
|
const extensionId = extension.id;
|
|
|
|
if (optionsPageProperties.open_in_tab) {
|
|
// Delegate new tab creation and open the options page in the new tab.
|
|
const tab = await GeckoViewTabBridge.createNewTab({
|
|
extensionId,
|
|
createProperties: {
|
|
url: optionsPageProperties.page,
|
|
active: true,
|
|
},
|
|
});
|
|
|
|
const { browser } = tab;
|
|
const loadFlags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
|
|
|
|
browser.fixupAndLoadURIString(optionsPageProperties.page, {
|
|
loadFlags,
|
|
triggeringPrincipal: extension.principal,
|
|
});
|
|
|
|
const newWindow = browser.ownerGlobal;
|
|
mobileWindowTracker.setTabActive(newWindow, true);
|
|
return;
|
|
}
|
|
|
|
// Delegate option page handling to the app.
|
|
return GeckoViewTabBridge.openOptionsPage(extensionId);
|
|
};
|