aboutsummaryrefslogtreecommitdiffstats
path: root/js/vapi-tabs.js
diff options
context:
space:
mode:
authorAlessio Vanni <vannilla@firemail.cc>2019-06-21 22:03:03 +0200
committerAlessio Vanni <vannilla@firemail.cc>2019-06-21 22:03:03 +0200
commitbdc82e65c4d30f541b5e0efcd3c0723103435c4e (patch)
tree43bbac3804e4fd006e75779b2537164ea1b9f91c /js/vapi-tabs.js
parent23f2978b8a41649c93f8b46c0f967f8b226928a4 (diff)
downloadematrix-bdc82e65c4d30f541b5e0efcd3c0723103435c4e.tar.lz
ematrix-bdc82e65c4d30f541b5e0efcd3c0723103435c4e.tar.xz
ematrix-bdc82e65c4d30f541b5e0efcd3c0723103435c4e.zip
Split tab handling from vapi-background
That file is too large, let's split it up.
Diffstat (limited to 'js/vapi-tabs.js')
-rw-r--r--js/vapi-tabs.js735
1 files changed, 735 insertions, 0 deletions
diff --git a/js/vapi-tabs.js b/js/vapi-tabs.js
new file mode 100644
index 0000000..4c9caf5
--- /dev/null
+++ b/js/vapi-tabs.js
@@ -0,0 +1,735 @@
+/*******************************************************************************
+
+ ηMatrix - a browser extension to black/white list requests.
+ Copyright (C) 2014-2019 The uMatrix/uBlock Origin authors
+ Copyright (C) 2019 Alessio Vanni
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://gitlab.com/vannilla/ematrix
+ uMatrix Home: https://github.com/gorhill/uMatrix
+*/
+
+/* global self, Components */
+
+// For background page (tabs management)
+
+'use strict';
+
+/******************************************************************************/
+
+(function () {
+ const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+ const {Services} = Cu.import('resource://gre/modules/Services.jsm', null);
+
+ let vAPI = self.vAPI; // Guaranteed to be initialized by vapi-background.js
+
+ vAPI.noTabId = '-1';
+
+ vAPI.tabs = {};
+
+ vAPI.tabs.registerListeners = function() {
+ tabWatcher.start();
+ };
+
+ vAPI.tabs.get = function (tabId, callback) {
+ // eMatrix: the following might be obsoleted (though probably
+ // still relevant at least for Pale Moon.)
+ //
+ // Firefox:
+ //
+ // browser -> ownerDocument -> defaultView -> gBrowser -> browsers --+
+ // ^ |
+ // | |
+ // +--------------------------------------------------------------+
+ //
+ // browser (browser)
+ // contentTitle
+ // currentURI
+ // ownerDocument (XULDocument)
+ // defaultView (ChromeWindow)
+ // gBrowser (tabbrowser OR browser)
+ // browsers (browser)
+ // selectedBrowser
+ // selectedTab
+ // tabs (tab.tabbrowser-tab)
+ //
+ // Fennec: (what I figured so far)
+ //
+ // tab -> browser windows -> window -> BrowserApp -> tabs --+
+ // ^ window |
+ // | |
+ // +-----------------------------------------------------------+
+ //
+ // tab
+ // browser
+ // [manual search to go back to tab from list of windows]
+ let browser;
+
+ if (tabId === null) {
+ browser = tabWatcher.currentBrowser();
+ tabId = tabWatcher.tabIdFromTarget(browser);
+ } else {
+ browser = tabWatcher.browserFromTabId(tabId);
+ }
+
+ // For internal use
+ if (typeof callback !== 'function') {
+ return browser;
+ }
+
+ if (!browser || !browser.currentURI) {
+ callback();
+ return;
+ }
+
+ let win = getOwnerWindow(browser);
+ let tabBrowser = getTabBrowser(win);
+
+ callback({
+ id: tabId,
+ windowId: winWatcher.idFromWindow(win),
+ active: tabBrowser !== null
+ && browser === tabBrowser.selectedBrowser,
+ url: browser.currentURI.asciiSpec,
+ title: browser.contentTitle
+ });
+ };
+
+ vAPI.tabs.getAllSync = function (window) {
+ let win;
+ let tabs = [];
+
+ for (let win of winWatcher.getWindows()) {
+ if (window && window !== win) {
+ continue;
+ }
+
+ let tabBrowser = getTabBrowser(win);
+ if (tabBrowser === null) {
+ continue;
+ }
+
+ // This can happens if a tab-less window is currently opened.
+ // Example of a tab-less window: one opened from clicking
+ // "View Page Source".
+ if (!tabBrowser.tabs) {
+ continue;
+ }
+
+ for (let tab of tabBrowser.tabs) {
+ tabs.push(tab);
+ }
+ }
+
+ return tabs;
+ };
+
+ vAPI.tabs.getAll = function (callback) {
+ let tabs = [];
+
+ for (let browser of tabWatcher.browsers()) {
+ let tab = tabWatcher.tabFromBrowser(browser);
+
+ if (tab === null) {
+ continue;
+ }
+
+ if (tab.hasAttribute('pending')) {
+ continue;
+ }
+
+ tabs.push({
+ id: tabWatcher.tabIdFromTarget(browser),
+ url: browser.currentURI.asciiSpec
+ });
+ }
+
+ callback(tabs);
+ };
+
+ vAPI.tabs.open = function (details) {
+ // properties of the details object:
+ // + url - the address that will be opened
+ // + tabId:- the tab is used if set, instead of creating a new one
+ // + index: - undefined: end of the list, -1: following tab, or
+ // after index
+ // + active: - opens the tab in background - true and undefined:
+ // foreground
+ // + select: - if a tab is already opened with that url, then
+ // select it instead of opening a new one
+ if (!details.url) {
+ return null;
+ }
+
+ // extension pages
+ if (/^[\w-]{2,}:/.test(details.url) === false) {
+ details.url = vAPI.getURL(details.url);
+ }
+
+ if (details.select) {
+ let URI = Services.io.newURI(details.url, null, null);
+
+ for (let tab of this.getAllSync()) {
+ let browser = tabWatcher.browserFromTarget(tab);
+
+ // https://github.com/gorhill/uBlock/issues/2558
+ if (browser === null) {
+ continue;
+ }
+
+ // Or simply .equals if we care about the fragment
+ if (URI.equalsExceptRef(browser.currentURI) === false) {
+ continue;
+ }
+
+ this.select(tab);
+
+ // Update URL if fragment is different
+ if (URI.equals(browser.currentURI) === false) {
+ browser.loadURI(URI.asciiSpec);
+ }
+
+ return;
+ }
+ }
+
+ if (details.active === undefined) {
+ details.active = true;
+ }
+
+ if (details.tabId) {
+ let tab = tabWatcher.browserFromTabId(details.tabId);
+
+ if (tab) {
+ tabWatcher.browserFromTarget(tab).loadURI(details.url);
+ return;
+ }
+ }
+
+ // Open in a standalone window
+ if (details.popup === true) {
+ Services.ww.openWindow(self,
+ details.url,
+ null,
+ 'location=1,menubar=1,personalbar=1,'
+ +'resizable=1,toolbar=1',
+ null);
+ return;
+ }
+
+ let win = winWatcher.getCurrentWindow();
+ let tabBrowser = getTabBrowser(win);
+
+ if (tabBrowser === null) {
+ return;
+ }
+
+ if (details.index === -1) {
+ details.index =
+ tabBrowser.browsers.indexOf(tabBrowser.selectedBrowser) + 1;
+ }
+
+ let tab = tabBrowser.loadOneTab(details.url, {
+ inBackground: !details.active
+ });
+
+ if (details.index !== undefined) {
+ tabBrowser.moveTabTo(tab, details.index);
+ }
+ };
+
+ vAPI.tabs.replace = function (tabId, url) {
+ // Replace the URL of a tab. Noop if the tab does not exist.
+ let targetURL = url;
+
+ // extension pages
+ if (/^[\w-]{2,}:/.test(targetURL) !== true) {
+ targetURL = vAPI.getURL(targetURL);
+ }
+
+ let browser = tabWatcher.browserFromTabId(tabId);
+ if (browser) {
+ browser.loadURI(targetURL);
+ }
+ };
+
+ function removeInternal(tab, tabBrowser) {
+ if (tabBrowser) {
+ tabBrowser.removeTab(tab);
+ }
+ }
+
+ vAPI.tabs.remove = function (tabId) {
+ let browser = tabWatcher.browserFromTabId(tabId);
+ if (!browser) {
+ return;
+ }
+
+ let tab = tabWatcher.tabFromBrowser(browser);
+ if (!tab) {
+ return;
+ }
+
+ removeInternal(tab, getTabBrowser(getOwnerWindow(browser)));
+ };
+
+ vAPI.tabs.reload = function (tabId) {
+ let browser = tabWatcher.browserFromTabId(tabId);
+ if (!browser) {
+ return;
+ }
+
+ browser.webNavigation.reload(Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE);
+ };
+
+ vAPI.tabs.select = function (tab) {
+ if (typeof tab !== 'object') {
+ tab = tabWatcher.tabFromBrowser(tabWatcher.browserFromTabId(tab));
+ }
+ if (!tab) {
+ return;
+ }
+
+ // https://github.com/gorhill/uBlock/issues/470
+ let win = getOwnerWindow(tab);
+ win.focus();
+
+ let tabBrowser = getTabBrowser(win);
+ if (tabBrowser) {
+ tabBrowser.selectedTab = tab;
+ }
+ };
+
+ vAPI.tabs.injectScript = function (tabId, details, callback) {
+ let browser = tabWatcher.browserFromTabId(tabId);
+ if (!browser) {
+ return;
+ }
+
+ if (typeof details.file !== 'string') {
+ return;
+ }
+
+ details.file = vAPI.getURL(details.file);
+ browser.messageManager.sendAsyncMessage(location.host + ':broadcast',
+ JSON.stringify({
+ broadcast: true,
+ channelName: 'vAPI',
+ msg: {
+ cmd: 'injectScript',
+ details: details
+ }
+ }));
+
+ if (typeof callback === 'function') {
+ vAPI.setTimeout(callback, 13);
+ }
+ };
+
+ let tabWatcher = (function () {
+ // TODO: find out whether we need a janitor to take care of stale entries.
+
+ // https://github.com/gorhill/uMatrix/issues/540
+ // Use only weak references to hold onto browser references.
+ let browserToTabIdMap = new WeakMap();
+ let tabIdToBrowserMap = new Map();
+ let tabIdGenerator = 1;
+
+ let indexFromBrowser = function (browser) {
+ if (!browser) {
+ return -1;
+ }
+
+ let win = getOwnerWindow(browser);
+ if (!win) {
+ return -1;
+ }
+
+ let tabBrowser = getTabBrowser(win);
+ if (tabBrowser === null) {
+ return -1;
+ }
+
+ // This can happen, for example, the `view-source:`
+ // window, there is no tabbrowser object, the browser
+ // object sits directly in the window.
+ if (tabBrowser === browser) {
+ return 0;
+ }
+
+ return tabBrowser.browsers.indexOf(browser);
+ };
+
+ let indexFromTarget = function (target) {
+ return indexFromBrowser(browserFromTarget(target));
+ };
+
+ let tabFromBrowser = function (browser) {
+ let i = indexFromBrowser(browser);
+ if (i === -1) {
+ return null;
+ }
+
+ let win = getOwnerWindow(browser);
+ if (!win) {
+ return null;
+ }
+
+ let tabBrowser = getTabBrowser(win);
+ if (tabBrowser === null) {
+ return null;
+ }
+
+ if (!tabBrowser.tabs || i >= tabBrowser.tabs.length) {
+ return null;
+ }
+
+ return tabBrowser.tabs[i];
+ };
+
+ let browserFromTarget = function (target) {
+ if (!target) {
+ return null;
+ }
+
+ if (target.linkedPanel) {
+ // target is a tab
+ target = target.linkedBrowser;
+ }
+
+ if (target.localName !== 'browser') {
+ return null;
+ }
+
+ return target;
+ };
+
+ let tabIdFromTarget = function (target) {
+ let browser = browserFromTarget(target);
+ if (browser === null) {
+ return vAPI.noTabId;
+ }
+
+ let tabId = browserToTabIdMap.get(browser);
+ if (tabId === undefined) {
+ tabId = '' + tabIdGenerator++;
+ browserToTabIdMap.set(browser, tabId);
+ tabIdToBrowserMap.set(tabId, Cu.getWeakReference(browser));
+ }
+
+ return tabId;
+ };
+
+ let browserFromTabId = function (tabId) {
+ let weakref = tabIdToBrowserMap.get(tabId);
+ let browser = weakref && weakref.get();
+
+ return browser || null;
+ };
+
+ let currentBrowser = function () {
+ let win = winWatcher.getCurrentWindow();
+
+ // https://github.com/gorhill/uBlock/issues/399
+ // getTabBrowser() can return null at browser launch time.
+ let tabBrowser = getTabBrowser(win);
+ if (tabBrowser === null) {
+ return null;
+ }
+
+ return browserFromTarget(tabBrowser.selectedTab);
+ };
+
+ let removeBrowserEntry = function (tabId, browser) {
+ if (tabId && tabId !== vAPI.noTabId) {
+ vAPI.tabs.onClosed(tabId);
+ delete vAPI.toolbarButton.tabs[tabId];
+ tabIdToBrowserMap.delete(tabId);
+ }
+
+ if (browser) {
+ browserToTabIdMap.delete(browser);
+ }
+ };
+
+ let removeTarget = function (target) {
+ onClose({
+ target: target
+ });
+ };
+
+ let getAllBrowsers = function () {
+ let browsers = [];
+
+ for (let [tabId, weakref] of tabIdToBrowserMap) {
+ let browser = weakref.get();
+
+ // TODO: Maybe call removeBrowserEntry() if the
+ // browser no longer exists?
+ if (browser) {
+ browsers.push(browser);
+ }
+ }
+
+ return browsers;
+ };
+
+ // var onOpen = function (target) {
+ // var tabId = tabIdFromTarget(target);
+ // var browser = browserFromTabId(tabId);
+ // vAPI.tabs.onNavigation({
+ // frameId: 0,
+ // tabId: tabId,
+ // url: browser.currentURI.asciiSpec,
+ // });
+ // };
+
+ var onShow = function ({target}) {
+ tabIdFromTarget(target);
+ };
+
+ var onClose = function ({target}) {
+ // target is tab in Firefox, browser in Fennec
+ let browser = browserFromTarget(target);
+ let tabId = browserToTabIdMap.get(browser);
+ removeBrowserEntry(tabId, browser);
+ };
+
+ var onSelect = function ({target}) {
+ // This is an entry point: when creating a new tab, it is
+ // not always reported through onLocationChanged...
+ // Sigh. It is "reported" here however.
+ let browser = browserFromTarget(target);
+ let tabId = browserToTabIdMap.get(browser);
+
+ if (tabId === undefined) {
+ tabId = tabIdFromTarget(target);
+ vAPI.tabs.onNavigation({
+ frameId: 0,
+ tabId: tabId,
+ url: browser.currentURI.asciiSpec
+ });
+ }
+
+ vAPI.setIcon(tabId, getOwnerWindow(target));
+ };
+
+ let locationChangedMessageName = location.host + ':locationChanged';
+
+ let onLocationChanged = function (e) {
+ let details = e.data;
+
+ // Ignore notifications related to our popup
+ if (details.url.lastIndexOf(vAPI.getURL('popup.html'), 0) === 0) {
+ return;
+ }
+
+ let browser = e.target;
+ let tabId = tabIdFromTarget(browser);
+ if (tabId === vAPI.noTabId) {
+ return;
+ }
+
+ // LOCATION_CHANGE_SAME_DOCUMENT = "did not load a new document"
+ if (details.flags
+ & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) {
+ vAPI.tabs.onUpdated(tabId, {url: details.url}, {
+ frameId: 0,
+ tabId: tabId,
+ url: browser.currentURI.asciiSpec
+ });
+ return;
+ }
+
+ // https://github.com/chrisaljoudi/uBlock/issues/105
+ // Allow any kind of pages
+ vAPI.tabs.onNavigation({
+ frameId: 0,
+ tabId: tabId,
+ url: details.url
+ });
+ };
+
+ let attachToTabBrowser = function (window) {
+ if (typeof vAPI.toolbarButton.attachToNewWindow === 'function') {
+ vAPI.toolbarButton.attachToNewWindow(window);
+ }
+
+ let tabBrowser = getTabBrowser(window);
+ if (tabBrowser === null) {
+ return;
+ }
+
+ let tabContainer;
+ if (tabBrowser.deck) {
+ // Fennec
+ tabContainer = tabBrowser.deck;
+ } else if (tabBrowser.tabContainer) {
+ // Firefox
+ tabContainer = tabBrowser.tabContainer;
+ vAPI.contextMenu.register(document);
+ }
+
+ // https://github.com/gorhill/uBlock/issues/697
+ // Ignore `TabShow` events: unfortunately the `pending`
+ // attribute is not set when a tab is opened as a result
+ // of session restore -- it is set *after* the event is
+ // fired in such case.
+ if (tabContainer) {
+ tabContainer.addEventListener('TabShow', onShow);
+ tabContainer.addEventListener('TabClose', onClose);
+ // when new window is opened TabSelect doesn't run on
+ // the selected tab?
+ tabContainer.addEventListener('TabSelect', onSelect);
+ }
+ };
+
+ var canAttachToTabBrowser = function (window) {
+ // https://github.com/gorhill/uBlock/issues/906
+ // Ensure the environment is ready before trying to attaching.
+ let document = window && window.document;
+ if (!document || document.readyState !== 'complete') {
+ return false;
+ }
+
+ // On some platforms, the tab browser isn't immediately
+ // available, try waiting a bit if this
+ // https://github.com/gorhill/uBlock/issues/763
+ // Not getting a tab browser should not prevent from
+ // attaching ourself to the window.
+ let tabBrowser = getTabBrowser(window);
+ if (tabBrowser === null) {
+ return false;
+ }
+
+ return winWatcher.toBrowserWindow(window) !== null;
+ };
+
+ let onWindowLoad = function (win) {
+ deferUntil(canAttachToTabBrowser.bind(null, win),
+ attachToTabBrowser.bind(null, win));
+ };
+
+ let onWindowUnload = function (win) {
+ vAPI.contextMenu.unregister(win.document);
+
+ let tabBrowser = getTabBrowser(win);
+ if (tabBrowser === null) {
+ return;
+ }
+
+ let tabContainer = tabBrowser.tabContainer;
+ if (tabContainer) {
+ tabContainer.removeEventListener('TabShow', onShow);
+ tabContainer.removeEventListener('TabClose', onClose);
+ tabContainer.removeEventListener('TabSelect', onSelect);
+ }
+
+ // https://github.com/gorhill/uBlock/issues/574
+ // To keep in mind: not all windows are tab containers,
+ // sometimes the window IS the tab.
+ let tabs;
+ if (tabBrowser.tabs) {
+ tabs = tabBrowser.tabs;
+ } else if (tabBrowser.localName === 'browser') {
+ tabs = [tabBrowser];
+ } else {
+ tabs = [];
+ }
+
+ let browser;
+ let URI;
+ let tabId;
+ for (let i=tabs.length-1; i>=0; --i) {
+ let tab = tabs[i];
+ browser = browserFromTarget(tab);
+ if (browser === null) {
+ continue;
+ }
+
+ URI = browser.currentURI;
+ // Close extension tabs
+ if (URI.schemeIs('chrome') && URI.host === location.host) {
+ removeInternal(tab, getTabBrowser(win));
+ }
+
+ tabId = browserToTabIdMap.get(browser);
+ if (tabId !== undefined) {
+ removeBrowserEntry(tabId, browser);
+ tabIdToBrowserMap.delete(tabId);
+ }
+ browserToTabIdMap.delete(browser);
+ }
+ };
+
+ var start = function () {
+ // Initialize map with existing active tabs
+ let tabBrowser;
+ let tabs;
+
+ for (let win of winWatcher.getWindows()) {
+ onWindowLoad(win);
+
+ tabBrowser = getTabBrowser(win);
+ if (tabBrowser === null) {
+ continue;
+ }
+
+ for (let tab of tabBrowser.tabs) {
+ if (!tab.hasAttribute('pending')) {
+ tabIdFromTarget(tab);
+ }
+ }
+ }
+
+ winWatcher.onOpenWindow = onWindowLoad;
+ winWatcher.onCloseWindow = onWindowUnload;
+
+ vAPI.messaging.globalMessageManager
+ .addMessageListener(locationChangedMessageName,
+ onLocationChanged);
+ };
+
+ let stop = function () {
+ winWatcher.onOpenWindow = null;
+ winWatcher.onCloseWindow = null;
+
+ vAPI.messaging.globalMessageManager
+ .removeMessageListener(locationChangedMessageName,
+ onLocationChanged);
+
+ for (let win of winWatcher.getWindows()) {
+ onWindowUnload(win);
+ }
+
+ browserToTabIdMap = new WeakMap();
+ tabIdToBrowserMap.clear();
+ };
+
+ vAPI.addCleanUpTask(stop);
+
+ return {
+ browsers: getAllBrowsers,
+ browserFromTabId: browserFromTabId,
+ browserFromTarget: browserFromTarget,
+ currentBrowser: currentBrowser,
+ indexFromTarget: indexFromTarget,
+ removeTarget: removeTarget,
+ start: start,
+ tabFromBrowser: tabFromBrowser,
+ tabIdFromTarget: tabIdFromTarget
+ };
+ })();
+})();