/******************************************************************************* η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://libregit.org/heckyel/ematrix uMatrix Home: https://github.com/gorhill/uMatrix */ 'use strict'; /******************************************************************************/ (function () { vAPI.tabs = {}; vAPI.tabs.registerListeners = function() { vAPI.tabs.manager.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 = vAPI.tabs.manager.currentBrowser(); tabId = vAPI.tabs.manager.tabIdFromTarget(browser); } else { browser = vAPI.tabs.manager.browserFromTabId(tabId); } // For internal use if (typeof callback !== 'function') { return browser; } if (!browser || !browser.currentURI) { callback(); return; } let win = vAPI.browser.getOwnerWindow(browser); let tabBrowser = vAPI.browser.getTabBrowser(win); callback({ id: tabId, windowId: vAPI.window.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 vAPI.window.getWindows()) { if (window && window !== win) { continue; } let tabBrowser = vAPI.browser.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 vAPI.tabs.manager.browsers()) { let tab = vAPI.tabs.manager.tabFromBrowser(browser); if (tab === null) { continue; } if (tab.hasAttribute('pending')) { continue; } tabs.push({ id: vAPI.tabs.manager.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 = vAPI.tabs.manager.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 = vAPI.tabs.manager.browserFromTabId(details.tabId); if (tab) { vAPI.tabs.manager.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 = vAPI.window.getCurrentWindow(); let tabBrowser = vAPI.browser.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 = vAPI.tabs.manager.browserFromTabId(tabId); if (browser) { browser.loadURI(targetURL); } }; function removeInternal(tab, tabBrowser) { if (tabBrowser) { tabBrowser.removeTab(tab); } } vAPI.tabs.remove = function (tabId) { let browser = vAPI.tabs.manager.browserFromTabId(tabId); if (!browser) { return; } let tab = vAPI.tabs.manager.tabFromBrowser(browser); if (!tab) { return; } removeInternal(tab, vAPI.browser.getTabBrowser (vAPI.browser.getOwnerWindow(browser))); }; vAPI.tabs.reload = function (tabId) { let browser = vAPI.tabs.manager.browserFromTabId(tabId); if (!browser) { return; } browser.webNavigation.reload(Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE); }; vAPI.tabs.select = function (tab) { if (typeof tab !== 'object') { tab = vAPI.tabs.manager .tabFromBrowser(vAPI.tabs.manager.browserFromTabId(tab)); } if (!tab) { return; } // https://github.com/gorhill/uBlock/issues/470 let win = vAPI.browser.getOwnerWindow(tab); win.focus(); let tabBrowser = vAPI.browser.getTabBrowser(win); if (tabBrowser) { tabBrowser.selectedTab = tab; } }; vAPI.tabs.injectScript = function (tabId, details, callback) { let browser = vAPI.tabs.manager.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); } }; vAPI.tabs.manager = (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 = vAPI.browser.getOwnerWindow(browser); if (!win) { return -1; } let tabBrowser = vAPI.browser.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 = vAPI.browser.getOwnerWindow(browser); if (!win) { return null; } let tabBrowser = vAPI.browser.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 = vAPI.window.getCurrentWindow(); // https://github.com/gorhill/uBlock/issues/399 // getTabBrowser() can return null at browser launch time. let tabBrowser = vAPI.browser.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, vAPI.browser.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 = vAPI.browser.getTabBrowser(window); if (tabBrowser === null) { return; } let tabContainer; if (tabBrowser.deck) { // Fennec tabContainer = tabBrowser.deck; } else if (tabBrowser.tabContainer) { // Firefox tabContainer = tabBrowser.tabContainer; } // 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 = vAPI.browser.getTabBrowser(window); if (tabBrowser === null) { return false; } return vAPI.window.toBrowserWindow(window) !== null; }; let onWindowLoad = function (win) { vAPI.deferUntil(canAttachToTabBrowser.bind(null, win), attachToTabBrowser.bind(null, win)); }; let onWindowUnload = function (win) { let tabBrowser = vAPI.browser.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, vAPI.browser.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 vAPI.window.getWindows()) { onWindowLoad(win); tabBrowser = vAPI.browser.getTabBrowser(win); if (tabBrowser === null) { continue; } for (let tab of tabBrowser.tabs) { if (!tab.hasAttribute('pending')) { tabIdFromTarget(tab); } } } vAPI.window.onOpenWindow = onWindowLoad; vAPI.window.onCloseWindow = onWindowUnload; vAPI.messaging.globalMessageManager .addMessageListener(locationChangedMessageName, onLocationChanged); }; let stop = function () { vAPI.window.onOpenWindow = null; vAPI.window.onCloseWindow = null; vAPI.messaging.globalMessageManager .removeMessageListener(locationChangedMessageName, onLocationChanged); for (let win of vAPI.window.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 }; })(); })();