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.

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