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.

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