diff options
author | Alessio Vanni <vannilla@firemail.cc> | 2019-06-21 22:03:03 +0200 |
---|---|---|
committer | Alessio Vanni <vannilla@firemail.cc> | 2019-06-21 22:03:03 +0200 |
commit | bdc82e65c4d30f541b5e0efcd3c0723103435c4e (patch) | |
tree | 43bbac3804e4fd006e75779b2537164ea1b9f91c /js/vapi-tabs.js | |
parent | 23f2978b8a41649c93f8b46c0f967f8b226928a4 (diff) | |
download | ematrix-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.js | 735 |
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 + }; + })(); +})(); |