vapi-tabs.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725
  1. /*******************************************************************************
  2. ηMatrix - a browser extension to black/white list requests.
  3. Copyright (C) 2014-2019 The uMatrix/uBlock Origin authors
  4. Copyright (C) 2019-2022 Alessio Vanni
  5. This program is free software: you can redistribute it and/or modify
  6. it under the terms of the GNU General Public License as published by
  7. the Free Software Foundation, either version 3 of the License, or
  8. (at your option) any later version.
  9. This program is distributed in the hope that it will be useful,
  10. but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. GNU General Public License for more details.
  13. You should have received a copy of the GNU General Public License
  14. along with this program. If not, see {http://www.gnu.org/licenses/}.
  15. Home: https://gitlab.com/vannilla/ematrix
  16. uMatrix Home: https://github.com/gorhill/uMatrix
  17. */
  18. 'use strict';
  19. /******************************************************************************/
  20. (function () {
  21. vAPI.tabs = {};
  22. vAPI.tabs.registerListeners = function() {
  23. vAPI.tabs.manager.start();
  24. };
  25. vAPI.tabs.get = function (tabId, callback) {
  26. // eMatrix: the following might be obsoleted (though probably
  27. // still relevant at least for Pale Moon.)
  28. //
  29. // Firefox:
  30. //
  31. // browser -> ownerDocument -> defaultView -> gBrowser -> browsers --+
  32. // ^ |
  33. // | |
  34. // +--------------------------------------------------------------+
  35. //
  36. // browser (browser)
  37. // contentTitle
  38. // currentURI
  39. // ownerDocument (XULDocument)
  40. // defaultView (ChromeWindow)
  41. // gBrowser (tabbrowser OR browser)
  42. // browsers (browser)
  43. // selectedBrowser
  44. // selectedTab
  45. // tabs (tab.tabbrowser-tab)
  46. //
  47. // Fennec: (what I figured so far)
  48. //
  49. // tab -> browser windows -> window -> BrowserApp -> tabs --+
  50. // ^ window |
  51. // | |
  52. // +-----------------------------------------------------------+
  53. //
  54. // tab
  55. // browser
  56. // [manual search to go back to tab from list of windows]
  57. let browser;
  58. if (tabId === null) {
  59. browser = vAPI.tabs.manager.currentBrowser();
  60. tabId = vAPI.tabs.manager.tabIdFromTarget(browser);
  61. } else {
  62. browser = vAPI.tabs.manager.browserFromTabId(tabId);
  63. }
  64. // For internal use
  65. if (typeof callback !== 'function') {
  66. return browser;
  67. }
  68. if (!browser || !browser.currentURI) {
  69. callback();
  70. return;
  71. }
  72. let win = vAPI.browser.getOwnerWindow(browser);
  73. let tabBrowser = vAPI.browser.getTabBrowser(win);
  74. callback({
  75. id: tabId,
  76. windowId: vAPI.window.idFromWindow(win),
  77. active: tabBrowser !== null
  78. && browser === tabBrowser.selectedBrowser,
  79. url: browser.currentURI.asciiSpec,
  80. title: browser.contentTitle
  81. });
  82. };
  83. vAPI.tabs.getAllSync = function (window) {
  84. let win;
  85. let tabs = [];
  86. for (let win of vAPI.window.getWindows()) {
  87. if (window && window !== win) {
  88. continue;
  89. }
  90. let tabBrowser = vAPI.browser.getTabBrowser(win);
  91. if (tabBrowser === null) {
  92. continue;
  93. }
  94. // This can happens if a tab-less window is currently opened.
  95. // Example of a tab-less window: one opened from clicking
  96. // "View Page Source".
  97. if (!tabBrowser.tabs) {
  98. continue;
  99. }
  100. for (let tab of tabBrowser.tabs) {
  101. tabs.push(tab);
  102. }
  103. }
  104. return tabs;
  105. };
  106. vAPI.tabs.getAll = function (callback) {
  107. let tabs = [];
  108. for (let browser of vAPI.tabs.manager.browsers()) {
  109. let tab = vAPI.tabs.manager.tabFromBrowser(browser);
  110. if (tab === null) {
  111. continue;
  112. }
  113. if (tab.hasAttribute('pending')) {
  114. continue;
  115. }
  116. tabs.push({
  117. id: vAPI.tabs.manager.tabIdFromTarget(browser),
  118. url: browser.currentURI.asciiSpec
  119. });
  120. }
  121. callback(tabs);
  122. };
  123. vAPI.tabs.open = function (details) {
  124. // properties of the details object:
  125. // + url - the address that will be opened
  126. // + tabId:- the tab is used if set, instead of creating a new one
  127. // + index: - undefined: end of the list, -1: following tab, or
  128. // after index
  129. // + active: - opens the tab in background - true and undefined:
  130. // foreground
  131. // + select: - if a tab is already opened with that url, then
  132. // select it instead of opening a new one
  133. if (!details.url) {
  134. return null;
  135. }
  136. // extension pages
  137. if (/^[\w-]{2,}:/.test(details.url) === false) {
  138. details.url = vAPI.getURL(details.url);
  139. }
  140. if (details.select) {
  141. let URI = Services.io.newURI(details.url, null, null);
  142. for (let tab of this.getAllSync()) {
  143. let browser = vAPI.tabs.manager.browserFromTarget(tab);
  144. // https://github.com/gorhill/uBlock/issues/2558
  145. if (browser === null) {
  146. continue;
  147. }
  148. // Or simply .equals if we care about the fragment
  149. if (URI.equalsExceptRef(browser.currentURI) === false) {
  150. continue;
  151. }
  152. this.select(tab);
  153. // Update URL if fragment is different
  154. if (URI.equals(browser.currentURI) === false) {
  155. browser.loadURI(URI.asciiSpec);
  156. }
  157. return;
  158. }
  159. }
  160. if (details.active === undefined) {
  161. details.active = true;
  162. }
  163. if (details.tabId) {
  164. let tab = vAPI.tabs.manager.browserFromTabId(details.tabId);
  165. if (tab) {
  166. vAPI.tabs.manager.browserFromTarget(tab).loadURI(details.url);
  167. return;
  168. }
  169. }
  170. // Open in a standalone window
  171. if (details.popup === true) {
  172. Services.ww.openWindow(self,
  173. details.url,
  174. null,
  175. 'location=1,menubar=1,personalbar=1,'
  176. +'resizable=1,toolbar=1',
  177. null);
  178. return;
  179. }
  180. let win = vAPI.window.getCurrentWindow();
  181. let tabBrowser = vAPI.browser.getTabBrowser(win);
  182. if (tabBrowser === null) {
  183. return;
  184. }
  185. if (details.index === -1) {
  186. details.index =
  187. tabBrowser.browsers.indexOf(tabBrowser.selectedBrowser) + 1;
  188. }
  189. let tab = tabBrowser.loadOneTab(details.url, {
  190. inBackground: !details.active
  191. });
  192. if (details.index !== undefined) {
  193. tabBrowser.moveTabTo(tab, details.index);
  194. }
  195. };
  196. vAPI.tabs.replace = function (tabId, url) {
  197. // Replace the URL of a tab. Noop if the tab does not exist.
  198. let targetURL = url;
  199. // extension pages
  200. if (/^[\w-]{2,}:/.test(targetURL) !== true) {
  201. targetURL = vAPI.getURL(targetURL);
  202. }
  203. let browser = vAPI.tabs.manager.browserFromTabId(tabId);
  204. if (browser) {
  205. browser.loadURI(targetURL);
  206. }
  207. };
  208. function removeInternal(tab, tabBrowser) {
  209. if (tabBrowser) {
  210. tabBrowser.removeTab(tab);
  211. }
  212. }
  213. vAPI.tabs.remove = function (tabId) {
  214. let browser = vAPI.tabs.manager.browserFromTabId(tabId);
  215. if (!browser) {
  216. return;
  217. }
  218. let tab = vAPI.tabs.manager.tabFromBrowser(browser);
  219. if (!tab) {
  220. return;
  221. }
  222. removeInternal(tab,
  223. vAPI.browser.getTabBrowser
  224. (vAPI.browser.getOwnerWindow(browser)));
  225. };
  226. vAPI.tabs.reload = function (tabId) {
  227. let browser = vAPI.tabs.manager.browserFromTabId(tabId);
  228. if (!browser) {
  229. return;
  230. }
  231. browser.webNavigation.reload(Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE);
  232. };
  233. vAPI.tabs.select = function (tab) {
  234. if (typeof tab !== 'object') {
  235. tab = vAPI.tabs.manager
  236. .tabFromBrowser(vAPI.tabs.manager.browserFromTabId(tab));
  237. }
  238. if (!tab) {
  239. return;
  240. }
  241. // https://github.com/gorhill/uBlock/issues/470
  242. let win = vAPI.browser.getOwnerWindow(tab);
  243. win.focus();
  244. let tabBrowser = vAPI.browser.getTabBrowser(win);
  245. if (tabBrowser) {
  246. tabBrowser.selectedTab = tab;
  247. }
  248. };
  249. vAPI.tabs.injectScript = function (tabId, details, callback) {
  250. let browser = vAPI.tabs.manager.browserFromTabId(tabId);
  251. if (!browser) {
  252. return;
  253. }
  254. if (typeof details.file !== 'string') {
  255. return;
  256. }
  257. details.file = vAPI.getURL(details.file);
  258. browser.messageManager.sendAsyncMessage(location.host + ':broadcast',
  259. JSON.stringify({
  260. broadcast: true,
  261. channelName: 'vAPI',
  262. msg: {
  263. cmd: 'injectScript',
  264. details: details
  265. }
  266. }));
  267. if (typeof callback === 'function') {
  268. vAPI.setTimeout(callback, 13);
  269. }
  270. };
  271. vAPI.tabs.manager = (function () {
  272. // TODO: find out whether we need a janitor to take care of stale entries.
  273. // https://github.com/gorhill/uMatrix/issues/540
  274. // Use only weak references to hold onto browser references.
  275. let browserToTabIdMap = new WeakMap();
  276. let tabIdToBrowserMap = new Map();
  277. let tabIdGenerator = 1;
  278. let indexFromBrowser = function (browser) {
  279. if (!browser) {
  280. return -1;
  281. }
  282. let win = vAPI.browser.getOwnerWindow(browser);
  283. if (!win) {
  284. return -1;
  285. }
  286. let tabBrowser = vAPI.browser.getTabBrowser(win);
  287. if (tabBrowser === null) {
  288. return -1;
  289. }
  290. // This can happen, for example, the `view-source:`
  291. // window, there is no tabbrowser object, the browser
  292. // object sits directly in the window.
  293. if (tabBrowser === browser) {
  294. return 0;
  295. }
  296. return tabBrowser.browsers.indexOf(browser);
  297. };
  298. let indexFromTarget = function (target) {
  299. return indexFromBrowser(browserFromTarget(target));
  300. };
  301. let tabFromBrowser = function (browser) {
  302. let i = indexFromBrowser(browser);
  303. if (i === -1) {
  304. return null;
  305. }
  306. let win = vAPI.browser.getOwnerWindow(browser);
  307. if (!win) {
  308. return null;
  309. }
  310. let tabBrowser = vAPI.browser.getTabBrowser(win);
  311. if (tabBrowser === null) {
  312. return null;
  313. }
  314. if (!tabBrowser.tabs || i >= tabBrowser.tabs.length) {
  315. return null;
  316. }
  317. return tabBrowser.tabs[i];
  318. };
  319. let browserFromTarget = function (target) {
  320. if (!target) {
  321. return null;
  322. }
  323. if (target.linkedPanel) {
  324. // target is a tab
  325. target = target.linkedBrowser;
  326. }
  327. if (target.localName !== 'browser') {
  328. return null;
  329. }
  330. return target;
  331. };
  332. let tabIdFromTarget = function (target) {
  333. let browser = browserFromTarget(target);
  334. if (browser === null) {
  335. return vAPI.noTabId;
  336. }
  337. let tabId = browserToTabIdMap.get(browser);
  338. if (tabId === undefined) {
  339. tabId = '' + tabIdGenerator++;
  340. browserToTabIdMap.set(browser, tabId);
  341. tabIdToBrowserMap.set(tabId, Cu.getWeakReference(browser));
  342. }
  343. return tabId;
  344. };
  345. let browserFromTabId = function (tabId) {
  346. let weakref = tabIdToBrowserMap.get(tabId);
  347. let browser = weakref && weakref.get();
  348. return browser || null;
  349. };
  350. let currentBrowser = function () {
  351. let win = vAPI.window.getCurrentWindow();
  352. // https://github.com/gorhill/uBlock/issues/399
  353. // getTabBrowser() can return null at browser launch time.
  354. let tabBrowser = vAPI.browser.getTabBrowser(win);
  355. if (tabBrowser === null) {
  356. return null;
  357. }
  358. return browserFromTarget(tabBrowser.selectedTab);
  359. };
  360. let removeBrowserEntry = function (tabId, browser) {
  361. if (tabId && tabId !== vAPI.noTabId) {
  362. vAPI.tabs.onClosed(tabId);
  363. delete vAPI.toolbarButton.tabs[tabId];
  364. tabIdToBrowserMap.delete(tabId);
  365. }
  366. if (browser) {
  367. browserToTabIdMap.delete(browser);
  368. }
  369. };
  370. let removeTarget = function (target) {
  371. onClose({
  372. target: target
  373. });
  374. };
  375. let getAllBrowsers = function () {
  376. let browsers = [];
  377. for (let [tabId, weakref] of tabIdToBrowserMap) {
  378. let browser = weakref.get();
  379. // TODO: Maybe call removeBrowserEntry() if the
  380. // browser no longer exists?
  381. if (browser) {
  382. browsers.push(browser);
  383. }
  384. }
  385. return browsers;
  386. };
  387. // var onOpen = function (target) {
  388. // var tabId = tabIdFromTarget(target);
  389. // var browser = browserFromTabId(tabId);
  390. // vAPI.tabs.onNavigation({
  391. // frameId: 0,
  392. // tabId: tabId,
  393. // url: browser.currentURI.asciiSpec,
  394. // });
  395. // };
  396. var onShow = function ({target}) {
  397. tabIdFromTarget(target);
  398. };
  399. var onClose = function ({target}) {
  400. // target is tab in Firefox, browser in Fennec
  401. let browser = browserFromTarget(target);
  402. let tabId = browserToTabIdMap.get(browser);
  403. removeBrowserEntry(tabId, browser);
  404. };
  405. var onSelect = function ({target}) {
  406. // This is an entry point: when creating a new tab, it is
  407. // not always reported through onLocationChanged...
  408. // Sigh. It is "reported" here however.
  409. let browser = browserFromTarget(target);
  410. let tabId = browserToTabIdMap.get(browser);
  411. if (tabId === undefined) {
  412. tabId = tabIdFromTarget(target);
  413. vAPI.tabs.onNavigation({
  414. frameId: 0,
  415. tabId: tabId,
  416. url: browser.currentURI.asciiSpec
  417. });
  418. }
  419. vAPI.setIcon(tabId, vAPI.browser.getOwnerWindow(target));
  420. };
  421. let locationChangedMessageName = location.host + ':locationChanged';
  422. let onLocationChanged = function (e) {
  423. let details = e.data;
  424. // Ignore notifications related to our popup
  425. if (details.url.lastIndexOf(vAPI.getURL('popup.html'), 0) === 0) {
  426. return;
  427. }
  428. let browser = e.target;
  429. let tabId = tabIdFromTarget(browser);
  430. if (tabId === vAPI.noTabId) {
  431. return;
  432. }
  433. // LOCATION_CHANGE_SAME_DOCUMENT = "did not load a new document"
  434. if (details.flags
  435. & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) {
  436. vAPI.tabs.onUpdated(tabId, {url: details.url}, {
  437. frameId: 0,
  438. tabId: tabId,
  439. url: browser.currentURI.asciiSpec
  440. });
  441. return;
  442. }
  443. // https://github.com/chrisaljoudi/uBlock/issues/105
  444. // Allow any kind of pages
  445. vAPI.tabs.onNavigation({
  446. frameId: 0,
  447. tabId: tabId,
  448. url: details.url
  449. });
  450. };
  451. let attachToTabBrowser = function (window) {
  452. if (typeof vAPI.toolbarButton.attachToNewWindow === 'function') {
  453. vAPI.toolbarButton.attachToNewWindow(window);
  454. }
  455. let tabBrowser = vAPI.browser.getTabBrowser(window);
  456. if (tabBrowser === null) {
  457. return;
  458. }
  459. let tabContainer;
  460. if (tabBrowser.deck) {
  461. // Fennec
  462. tabContainer = tabBrowser.deck;
  463. } else if (tabBrowser.tabContainer) {
  464. // Firefox
  465. tabContainer = tabBrowser.tabContainer;
  466. }
  467. // https://github.com/gorhill/uBlock/issues/697
  468. // Ignore `TabShow` events: unfortunately the `pending`
  469. // attribute is not set when a tab is opened as a result
  470. // of session restore -- it is set *after* the event is
  471. // fired in such case.
  472. if (tabContainer) {
  473. tabContainer.addEventListener('TabShow', onShow);
  474. tabContainer.addEventListener('TabClose', onClose);
  475. // when new window is opened TabSelect doesn't run on
  476. // the selected tab?
  477. tabContainer.addEventListener('TabSelect', onSelect);
  478. }
  479. };
  480. var canAttachToTabBrowser = function (window) {
  481. // https://github.com/gorhill/uBlock/issues/906
  482. // Ensure the environment is ready before trying to attaching.
  483. let document = window && window.document;
  484. if (!document || document.readyState !== 'complete') {
  485. return false;
  486. }
  487. // On some platforms, the tab browser isn't immediately
  488. // available, try waiting a bit if this
  489. // https://github.com/gorhill/uBlock/issues/763
  490. // Not getting a tab browser should not prevent from
  491. // attaching ourself to the window.
  492. let tabBrowser = vAPI.browser.getTabBrowser(window);
  493. if (tabBrowser === null) {
  494. return false;
  495. }
  496. return vAPI.window.toBrowserWindow(window) !== null;
  497. };
  498. let onWindowLoad = function (win) {
  499. vAPI.deferUntil(canAttachToTabBrowser.bind(null, win),
  500. attachToTabBrowser.bind(null, win));
  501. };
  502. let onWindowUnload = function (win) {
  503. let tabBrowser = vAPI.browser.getTabBrowser(win);
  504. if (tabBrowser === null) {
  505. return;
  506. }
  507. let tabContainer = tabBrowser.tabContainer;
  508. if (tabContainer) {
  509. tabContainer.removeEventListener('TabShow', onShow);
  510. tabContainer.removeEventListener('TabClose', onClose);
  511. tabContainer.removeEventListener('TabSelect', onSelect);
  512. }
  513. // https://github.com/gorhill/uBlock/issues/574
  514. // To keep in mind: not all windows are tab containers,
  515. // sometimes the window IS the tab.
  516. let tabs;
  517. if (tabBrowser.tabs) {
  518. tabs = tabBrowser.tabs;
  519. } else if (tabBrowser.localName === 'browser') {
  520. tabs = [tabBrowser];
  521. } else {
  522. tabs = [];
  523. }
  524. let browser;
  525. let URI;
  526. let tabId;
  527. for (let i=tabs.length-1; i>=0; --i) {
  528. let tab = tabs[i];
  529. browser = browserFromTarget(tab);
  530. if (browser === null) {
  531. continue;
  532. }
  533. URI = browser.currentURI;
  534. // Close extension tabs
  535. if (URI.schemeIs('chrome') && URI.host === location.host) {
  536. removeInternal(tab, vAPI.browser.getTabBrowser(win));
  537. }
  538. tabId = browserToTabIdMap.get(browser);
  539. if (tabId !== undefined) {
  540. removeBrowserEntry(tabId, browser);
  541. tabIdToBrowserMap.delete(tabId);
  542. }
  543. browserToTabIdMap.delete(browser);
  544. }
  545. };
  546. var start = function () {
  547. // Initialize map with existing active tabs
  548. let tabBrowser;
  549. let tabs;
  550. for (let win of vAPI.window.getWindows()) {
  551. onWindowLoad(win);
  552. tabBrowser = vAPI.browser.getTabBrowser(win);
  553. if (tabBrowser === null) {
  554. continue;
  555. }
  556. for (let tab of tabBrowser.tabs) {
  557. if (!tab.hasAttribute('pending')) {
  558. tabIdFromTarget(tab);
  559. }
  560. }
  561. }
  562. vAPI.window.onOpenWindow = onWindowLoad;
  563. vAPI.window.onCloseWindow = onWindowUnload;
  564. vAPI.messaging.globalMessageManager
  565. .addMessageListener(locationChangedMessageName,
  566. onLocationChanged);
  567. };
  568. let stop = function () {
  569. vAPI.window.onOpenWindow = null;
  570. vAPI.window.onCloseWindow = null;
  571. vAPI.messaging.globalMessageManager
  572. .removeMessageListener(locationChangedMessageName,
  573. onLocationChanged);
  574. for (let win of vAPI.window.getWindows()) {
  575. onWindowUnload(win);
  576. }
  577. browserToTabIdMap = new WeakMap();
  578. tabIdToBrowserMap.clear();
  579. };
  580. vAPI.addCleanUpTask(stop);
  581. return {
  582. browsers: getAllBrowsers,
  583. browserFromTabId: browserFromTabId,
  584. browserFromTarget: browserFromTarget,
  585. currentBrowser: currentBrowser,
  586. indexFromTarget: indexFromTarget,
  587. removeTarget: removeTarget,
  588. start: start,
  589. tabFromBrowser: tabFromBrowser,
  590. tabIdFromTarget: tabIdFromTarget
  591. };
  592. })();
  593. })();