You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

656 lines
18 KiB

  1. /*******************************************************************************
  2. µBlock - a Chromium browser extension to block requests.
  3. Copyright (C) 2014 The µBlock authors
  4. This program is free software: you can redistribute it and/or modify
  5. it under the terms of the GNU General Public License as published by
  6. the Free Software Foundation, either version 3 of the License, or
  7. (at your option) any later version.
  8. This program is distributed in the hope that it will be useful,
  9. but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. GNU General Public License for more details.
  12. You should have received a copy of the GNU General Public License
  13. along with this program. If not, see {http://www.gnu.org/licenses/}.
  14. Home: https://github.com/gorhill/uBlock
  15. */
  16. /* global Services */
  17. // For background page
  18. /******************************************************************************/
  19. (function() {
  20. 'use strict';
  21. /******************************************************************************/
  22. const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
  23. Cu['import']('resource://gre/modules/Services.jsm');
  24. /******************************************************************************/
  25. self.vAPI = self.vAPI || {};
  26. vAPI.firefox = true;
  27. /******************************************************************************/
  28. vAPI.app = {
  29. name: 'µBlock',
  30. version: '0.7.2.0'
  31. };
  32. /******************************************************************************/
  33. var SQLite = {
  34. open: function() {
  35. var path = Services.dirsvc.get('ProfD', Ci.nsIFile);
  36. path.append('extension-data');
  37. if (!path.exists()) {
  38. path.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt('0774', 8));
  39. }
  40. if (!path.isDirectory()) {
  41. throw Error('Should be a directory...');
  42. }
  43. path.append('uBlock.sqlite');
  44. this.db = Services.storage.openDatabase(path);
  45. this.db.executeSimpleSQL(
  46. 'CREATE TABLE IF NOT EXISTS settings' +
  47. '(name TEXT PRIMARY KEY NOT NULL, value TEXT);'
  48. );
  49. },
  50. close: function() {
  51. this.run('VACUUM');
  52. this.db.asyncClose();
  53. },
  54. run: function(query, values, callback) {
  55. if (!this.db) {
  56. this.open();
  57. }
  58. var result = {};
  59. query = this.db.createAsyncStatement(query);
  60. if (Array.isArray(values) && values.length) {
  61. var i = values.length;
  62. while (i--) {
  63. query.bindByIndex(i, values[i]);
  64. }
  65. }
  66. query.executeAsync({
  67. handleResult: function(rows) {
  68. if (!rows || typeof callback !== 'function') {
  69. return;
  70. }
  71. var row;
  72. while (row = rows.getNextRow()) {
  73. // we assume that there will be two columns, since we're
  74. // using it only for preferences
  75. result[row.getResultByIndex(0)] = row.getResultByIndex(1);
  76. }
  77. },
  78. handleCompletion: function(reason) {
  79. if (typeof callback === 'function' && reason === 0) {
  80. callback(result);
  81. }
  82. },
  83. handleError: function(error) {
  84. console.error('SQLite error ', error.result, error.message);
  85. }
  86. });
  87. }
  88. };
  89. /******************************************************************************/
  90. vAPI.storage = {
  91. QUOTA_BYTES: 100 * 1024 * 1024,
  92. sqlWhere: function(col, params) {
  93. if (params > 0) {
  94. params = Array(params + 1).join('?, ').slice(0, -2);
  95. return ' WHERE ' + col + ' IN (' + params + ')';
  96. }
  97. return '';
  98. },
  99. get: function(details, callback) {
  100. if (typeof callback !== 'function') {
  101. return;
  102. }
  103. var values = [], defaults = false;
  104. if (details !== null) {
  105. if (Array.isArray(details)) {
  106. values = details;
  107. }
  108. else if (typeof details === 'object') {
  109. defaults = true;
  110. values = Object.keys(details);
  111. }
  112. else {
  113. values = [details.toString()];
  114. }
  115. }
  116. SQLite.run(
  117. 'SELECT * FROM settings' + this.sqlWhere('name', values.length),
  118. values,
  119. function(result) {
  120. var key;
  121. for (key in result) {
  122. result[key] = JSON.parse(result[key]);
  123. }
  124. if (defaults) {
  125. for (key in details) {
  126. if (!result[key]) {
  127. result[key] = details[key];
  128. }
  129. }
  130. }
  131. callback(result);
  132. }
  133. );
  134. },
  135. set: function(details, callback) {
  136. var key, values = [], placeholders = [];
  137. for (key in details) {
  138. values.push(key);
  139. values.push(JSON.stringify(details[key]));
  140. placeholders.push('?, ?');
  141. }
  142. if (!values.length) {
  143. return;
  144. }
  145. SQLite.run(
  146. 'INSERT OR REPLACE INTO settings (name, value) SELECT ' +
  147. placeholders.join(' UNION SELECT '),
  148. values,
  149. callback
  150. );
  151. },
  152. remove: function(keys, callback) {
  153. if (typeof keys === 'string') {
  154. keys = [keys];
  155. }
  156. SQLite.run(
  157. 'DELETE FROM settings' + this.sqlWhere('name', keys.length),
  158. keys,
  159. callback
  160. );
  161. },
  162. clear: function(callback) {
  163. SQLite.run('DELETE FROM settings', null, callback);
  164. SQLite.run('VACUUM');
  165. },
  166. getBytesInUse: function(keys, callback) {
  167. if (typeof callback !== 'function') {
  168. return;
  169. }
  170. SQLite.run(
  171. 'SELECT "size" AS size, SUM(LENGTH(value)) FROM settings' +
  172. this.sqlWhere('name', Array.isArray(keys) ? keys.length : 0),
  173. keys,
  174. function(result) {
  175. callback(result.size);
  176. }
  177. );
  178. }
  179. };
  180. /******************************************************************************/
  181. var windowWatcher = {
  182. onTabClose: function(e) {
  183. vAPI.tabs.onClosed(vAPI.tabs.getTabId(e.target));
  184. },
  185. onTabSelect: function(e) {
  186. console.log(vAPI.tabs.getTabId(e.target));
  187. // vAPI.setIcon();
  188. },
  189. onLoad: function(e) {
  190. if (e) {
  191. this.removeEventListener('load', windowWatcher.onLoad);
  192. }
  193. var docElement = this.document.documentElement;
  194. if (docElement.getAttribute('windowtype') !== 'navigator:browser') {
  195. return;
  196. }
  197. if (!this.gBrowser || this.gBrowser.tabContainer) {
  198. return;
  199. }
  200. var tC = this.gBrowser.tabContainer;
  201. this.gBrowser.addTabsProgressListener(tabsProgressListener);
  202. tC.addEventListener('TabClose', windowWatcher.onTabClose);
  203. tC.addEventListener('TabSelect', windowWatcher.onTabSelect);
  204. // when new window is opened TabSelect doesn't run on the selected tab?
  205. },
  206. unregister: function() {
  207. Services.ww.unregisterNotification(this);
  208. for (var win of vAPI.tabs.getWindows()) {
  209. win.removeEventListener('load', this.onLoad);
  210. win.gBrowser.removeTabsProgressListener(tabsProgressListener);
  211. var tC = win.gBrowser.tabContainer;
  212. tC.removeEventListener('TabClose', this.onTabClose);
  213. tC.removeEventListener('TabSelect', this.onTabSelect);
  214. }
  215. },
  216. observe: function(win, topic) {
  217. if (topic === 'domwindowopened') {
  218. win.addEventListener('load', this.onLoad);
  219. }
  220. }
  221. };
  222. /******************************************************************************/
  223. var tabsProgressListener = {
  224. onLocationChange: function(browser, webProgress, request, location, flags) {
  225. if (!webProgress.isTopLevel) {
  226. return;
  227. }
  228. var tabId = vAPI.tabs.getTabId(browser);
  229. if (flags & 1) {
  230. vAPI.tabs.onUpdated(tabId, {url: location.spec}, {
  231. frameId: 0,
  232. tabId: tabId,
  233. url: browser.currentURI.spec
  234. });
  235. }
  236. else {
  237. vAPI.tabs.onNavigation({
  238. frameId: 0,
  239. tabId: tabId,
  240. url: location.spec
  241. });
  242. }
  243. }
  244. };
  245. /******************************************************************************/
  246. vAPI.tabs = {};
  247. /******************************************************************************/
  248. vAPI.tabs.registerListeners = function() {
  249. // onNavigation and onUpdated handled with tabsProgressListener
  250. // onClosed - handled in windowWatcher.onTabClose
  251. // onPopup ?
  252. Services.ww.registerNotification(windowWatcher);
  253. // already opened windows
  254. for (var win of this.getWindows()) {
  255. windowWatcher.onLoad.call(win);
  256. }
  257. };
  258. /******************************************************************************/
  259. vAPI.tabs.getTabId = function(target) {
  260. if (target.linkedPanel) {
  261. return target.linkedPanel.slice(6);
  262. }
  263. var gBrowser = target.ownerDocument.defaultView.gBrowser;
  264. var i = gBrowser.browsers.indexOf(target);
  265. if (i !== -1) {
  266. i = this.getTabId(gBrowser.tabs[i]);
  267. }
  268. return i;
  269. };
  270. /******************************************************************************/
  271. vAPI.tabs.get = function(tabId, callback) {
  272. var tab, windows;
  273. if (tabId === null) {
  274. tab = Services.wm.getMostRecentWindow('navigator:browser').gBrowser.selectedTab;
  275. tabId = vAPI.tabs.getTabId(tab);
  276. }
  277. else {
  278. windows = this.getWindows();
  279. for (var win of windows) {
  280. tab = win.gBrowser.tabContainer.querySelector(
  281. 'tab[linkedpanel="panel-' + tabId + '"]'
  282. );
  283. if (tab) {
  284. break;
  285. }
  286. }
  287. }
  288. if (!tab) {
  289. callback();
  290. return;
  291. }
  292. var browser = tab.linkedBrowser;
  293. var gBrowser = browser.ownerDocument.defaultView.gBrowser;
  294. if (!windows) {
  295. windows = this.getWindows();
  296. }
  297. callback({
  298. id: tabId,
  299. index: gBrowser.browsers.indexOf(browser),
  300. windowId: windows.indexOf(browser.ownerDocument.defaultView),
  301. active: tab === gBrowser.selectedTab,
  302. url: browser.currentURI.spec,
  303. title: tab.label
  304. });
  305. };
  306. /******************************************************************************/
  307. vAPI.tabs.getAll = function(window) {
  308. var tabs = [];
  309. for (var win of this.getWindows()) {
  310. if (window && window !== win) {
  311. continue;
  312. }
  313. for (var tab of win.gBrowser.tabs) {
  314. tabs.push(tab);
  315. }
  316. }
  317. return tabs;
  318. };
  319. /******************************************************************************/
  320. vAPI.tabs.getWindows = function() {
  321. var winumerator = Services.wm.getEnumerator('navigator:browser');
  322. var windows = [];
  323. while (winumerator.hasMoreElements()) {
  324. var win = winumerator.getNext();
  325. if (!win.closed) {
  326. windows.push(win);
  327. }
  328. }
  329. return windows;
  330. };
  331. /******************************************************************************/
  332. // properties of the details object:
  333. // url: 'URL', // the address that will be opened
  334. // tabId: 1, // the tab is used if set, instead of creating a new one
  335. // index: -1, // undefined: end of the list, -1: following tab, or after index
  336. // active: false, // opens the tab in background - true and undefined: foreground
  337. // select: true // if a tab is already opened with that url, then select it instead of opening a new one
  338. vAPI.tabs.open = function(details) {
  339. if (!details.url) {
  340. return null;
  341. }
  342. // extension pages
  343. if (!/^[\w-]{2,}:/.test(details.url)) {
  344. details.url = vAPI.getURL(details.url);
  345. }
  346. var tab, tabs;
  347. if (details.select) {
  348. var rgxHash = /#.*/;
  349. // this is questionable
  350. var url = details.url.replace(rgxHash, '');
  351. tabs = this.getAll();
  352. for (tab of tabs) {
  353. var browser = tab.linkedBrowser;
  354. if (browser.currentURI.spec.replace(rgxHash, '') === url) {
  355. browser.ownerDocument.defaultView.gBrowser.selectedTab = tab;
  356. return;
  357. }
  358. }
  359. }
  360. if (details.active === undefined) {
  361. details.active = true;
  362. }
  363. var gBrowser = Services.wm.getMostRecentWindow('navigator:browser').gBrowser;
  364. if (details.index === -1) {
  365. details.index = gBrowser.browsers.indexOf(gBrowser.selectedBrowser) + 1;
  366. }
  367. if (details.tabId) {
  368. tabs = tabs || this.getAll();
  369. for (tab of tabs) {
  370. if (vAPI.tabs.getTabId(tab) === details.tabId) {
  371. tab.linkedBrowser.loadURI(details.url);
  372. return;
  373. }
  374. }
  375. }
  376. tab = gBrowser.loadOneTab(details.url, {inBackground: !details.active});
  377. if (details.index !== undefined) {
  378. gBrowser.moveTabTo(tab, details.index);
  379. }
  380. };
  381. /******************************************************************************/
  382. vAPI.tabs.close = function(tabIds) {
  383. if (!Array.isArray(tabIds)) {
  384. tabIds = [tabIds];
  385. }
  386. tabIds = tabIds.map(function(tabId) {
  387. return 'tab[linkedpanel="panel-' + tabId + '"]';
  388. }).join(',');
  389. for (var win of this.getWindows()) {
  390. var tabs = win.gBrowser.tabContainer.querySelectorAll(tabIds);
  391. if (!tabs) {
  392. continue;
  393. }
  394. for (var tab of tabs) {
  395. win.gBrowser.removeTab(tab);
  396. }
  397. }
  398. };
  399. /******************************************************************************/
  400. /*vAPI.tabs.injectScript = function(tabId, details, callback) {
  401. };*/
  402. /******************************************************************************/
  403. vAPI.messaging = {
  404. gmm: Cc['@mozilla.org/globalmessagemanager;1'].getService(Ci.nsIMessageListenerManager),
  405. frameScript: 'chrome://ublock/content/frameScript.js',
  406. listeners: {},
  407. defaultHandler: null,
  408. NOOPFUNC: function(){},
  409. UNHANDLED: 'vAPI.messaging.notHandled'
  410. };
  411. /******************************************************************************/
  412. vAPI.messaging.gmm.loadFrameScript(vAPI.messaging.frameScript, true);
  413. /******************************************************************************/
  414. vAPI.messaging.listen = function(listenerName, callback) {
  415. this.listeners[listenerName] = callback;
  416. };
  417. /******************************************************************************/
  418. vAPI.messaging.onMessage = function(request) {
  419. var messageManager = request.target.messageManager;
  420. var listenerId = request.data.portName.split('|');
  421. var requestId = request.data.requestId;
  422. var portName = listenerId[1];
  423. listenerId = listenerId[0];
  424. var callback = vAPI.messaging.NOOPFUNC;
  425. if ( requestId !== undefined ) {
  426. callback = function(response) {
  427. messageManager.sendAsyncMessage(
  428. listenerId,
  429. JSON.stringify({
  430. requestId: requestId,
  431. portName: portName,
  432. msg: response !== undefined ? response : null
  433. })
  434. );
  435. };
  436. }
  437. var sender = {
  438. tab: {
  439. id: vAPI.tabs.getTabId(request.target)
  440. }
  441. };
  442. // Specific handler
  443. var r = vAPI.messaging.UNHANDLED;
  444. var listener = vAPI.messaging.listeners[portName];
  445. if ( typeof listener === 'function' ) {
  446. r = listener(request.data.msg, sender, callback);
  447. }
  448. if ( r !== vAPI.messaging.UNHANDLED ) {
  449. return;
  450. }
  451. // Default handler
  452. r = vAPI.messaging.defaultHandler(request.data.msg, sender, callback);
  453. if ( r !== vAPI.messaging.UNHANDLED ) {
  454. return;
  455. }
  456. console.error('µBlock> messaging > unknown request: %o', request.data);
  457. // Unhandled:
  458. // Need to callback anyways in case caller expected an answer, or
  459. // else there is a memory leak on caller's side
  460. callback();
  461. };
  462. /******************************************************************************/
  463. vAPI.messaging.setup = function(defaultHandler) {
  464. // Already setup?
  465. if ( this.defaultHandler !== null ) {
  466. return;
  467. }
  468. if ( typeof defaultHandler !== 'function' ) {
  469. defaultHandler = function(){ return vAPI.messaging.UNHANDLED; };
  470. }
  471. this.defaultHandler = defaultHandler;
  472. this.gmm.addMessageListener(vAPI.app.name + ':background', this.onMessage);
  473. };
  474. /******************************************************************************/
  475. vAPI.messaging.broadcast = function(message) {
  476. this.gmm.broadcastAsyncMessage(
  477. vAPI.app.name + ':broadcast',
  478. JSON.stringify({broadcast: true, msg: message})
  479. );
  480. };
  481. /******************************************************************************/
  482. vAPI.messaging.unload = function() {
  483. this.gmm.removeMessageListener(
  484. vAPI.app.name + ':background',
  485. this.onMessage
  486. );
  487. this.gmm.removeDelayedFrameScript(this.frameScript);
  488. };
  489. /******************************************************************************/
  490. vAPI.lastError = function() {
  491. return null;
  492. };
  493. /******************************************************************************/
  494. // clean up when the extension is disabled
  495. window.addEventListener('unload', function() {
  496. SQLite.close();
  497. windowWatcher.unregister();
  498. vAPI.messaging.unload();
  499. // close extension tabs
  500. var extURI, win, tab, host = 'ublock';
  501. for (win of vAPI.tabs.getWindows()) {
  502. for (tab of win.gBrowser.tabs) {
  503. extURI = tab.linkedBrowser.currentURI;
  504. if (extURI.scheme === 'chrome' && extURI.host === host) {
  505. win.gBrowser.removeTab(tab);
  506. }
  507. }
  508. }
  509. });
  510. /******************************************************************************/
  511. })();
  512. /******************************************************************************/