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.

684 lines
21 KiB

10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
  1. /*******************************************************************************
  2. µBlock - a 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 self, µBlock */
  17. // For background page
  18. /******************************************************************************/
  19. (function() {
  20. 'use strict';
  21. /******************************************************************************/
  22. var vAPI = self.vAPI = self.vAPI || {};
  23. var chrome = self.chrome;
  24. var manifest = chrome.runtime.getManifest();
  25. vAPI.chrome = true;
  26. var noopFunc = function(){};
  27. /******************************************************************************/
  28. vAPI.app = {
  29. name: manifest.name,
  30. version: manifest.version
  31. };
  32. /******************************************************************************/
  33. vAPI.app.restart = function() {
  34. chrome.runtime.reload();
  35. };
  36. /******************************************************************************/
  37. vAPI.storage = chrome.storage.local;
  38. /******************************************************************************/
  39. vAPI.tabs = {};
  40. /******************************************************************************/
  41. vAPI.isNoTabId = function(tabId) {
  42. return tabId.toString() === '-1';
  43. };
  44. vAPI.noTabId = '-1';
  45. /******************************************************************************/
  46. vAPI.tabs.registerListeners = function() {
  47. var onNavigationClient = this.onNavigation || noopFunc;
  48. var onPopupClient = this.onPopup || noopFunc;
  49. // https://developer.chrome.com/extensions/webNavigation
  50. // [onCreatedNavigationTarget ->]
  51. // onBeforeNavigate ->
  52. // onCommitted ->
  53. // onDOMContentLoaded ->
  54. // onCompleted
  55. var popupCandidates = Object.create(null);
  56. var PopupCandidate = function(details) {
  57. this.targetTabId = details.tabId;
  58. this.openerTabId = details.sourceTabId;
  59. this.targetURL = details.url;
  60. this.selfDestructionTimer = null;
  61. };
  62. PopupCandidate.prototype.selfDestruct = function() {
  63. if ( this.selfDestructionTimer !== null ) {
  64. clearTimeout(this.selfDestructionTimer);
  65. }
  66. delete popupCandidates[this.targetTabId];
  67. };
  68. PopupCandidate.prototype.launchSelfDestruction = function() {
  69. if ( this.selfDestructionTimer !== null ) {
  70. clearTimeout(this.selfDestructionTimer);
  71. }
  72. this.selfDestructionTimer = setTimeout(this.selfDestruct.bind(this), 1000);
  73. };
  74. var popupCandidateCreate = function(details) {
  75. var popup = popupCandidates[details.tabId];
  76. // This really should not happen...
  77. if ( popup !== undefined ) {
  78. return;
  79. }
  80. return popupCandidates[details.tabId] = new PopupCandidate(details);
  81. };
  82. var popupCandidateTest = function(details) {
  83. var popup = popupCandidates[details.tabId];
  84. if ( popup === undefined ) {
  85. return;
  86. }
  87. popup.targetURL = details.url;
  88. if ( onPopupClient(popup) !== true ) {
  89. return;
  90. }
  91. popup.selfDestruct();
  92. return true;
  93. };
  94. var popupCandidateDestroy = function(details) {
  95. var popup = popupCandidates[details.tabId];
  96. if ( popup instanceof PopupCandidate ) {
  97. popup.launchSelfDestruction();
  98. }
  99. };
  100. var onCreatedNavigationTarget = function(details) {
  101. //console.debug('onCreatedNavigationTarget: popup candidate', details.tabId);
  102. popupCandidateCreate(details);
  103. popupCandidateTest(details);
  104. };
  105. var onBeforeNavigate = function(details) {
  106. if ( details.frameId === 0 ) {
  107. //console.debug('onBeforeNavigate: popup candidate', details.tabId);
  108. popupCandidateTest(details);
  109. }
  110. };
  111. var onCommitted = function(details) {
  112. if ( details.frameId === 0 ) {
  113. //console.debug('onCommitted: popup candidate', details.tabId);
  114. if ( popupCandidateTest(details) === true ) {
  115. return;
  116. }
  117. popupCandidateDestroy(details);
  118. }
  119. onNavigationClient(details);
  120. };
  121. chrome.webNavigation.onCreatedNavigationTarget.addListener(onCreatedNavigationTarget);
  122. chrome.webNavigation.onBeforeNavigate.addListener(onBeforeNavigate);
  123. chrome.webNavigation.onCommitted.addListener(onCommitted);
  124. if ( typeof this.onUpdated === 'function' ) {
  125. chrome.tabs.onUpdated.addListener(this.onUpdated);
  126. }
  127. if ( typeof this.onClosed === 'function' ) {
  128. chrome.tabs.onRemoved.addListener(this.onClosed);
  129. }
  130. };
  131. /******************************************************************************/
  132. vAPI.tabs.get = function(tabId, callback) {
  133. var onTabReady = function(tab) {
  134. // https://code.google.com/p/chromium/issues/detail?id=410868#c8
  135. if ( chrome.runtime.lastError ) {
  136. /* noop */
  137. }
  138. // Caller must be prepared to deal with nil tab value
  139. callback(tab);
  140. };
  141. if ( tabId !== null ) {
  142. if ( typeof tabId === 'string' ) {
  143. tabId = parseInt(tabId, 10);
  144. }
  145. chrome.tabs.get(tabId, onTabReady);
  146. return;
  147. }
  148. var onTabReceived = function(tabs) {
  149. // https://code.google.com/p/chromium/issues/detail?id=410868#c8
  150. if ( chrome.runtime.lastError ) {
  151. /* noop */
  152. }
  153. callback(tabs[0]);
  154. };
  155. chrome.tabs.query({ active: true, currentWindow: true }, onTabReceived);
  156. };
  157. /******************************************************************************/
  158. // properties of the details object:
  159. // url: 'URL', // the address that will be opened
  160. // tabId: 1, // the tab is used if set, instead of creating a new one
  161. // index: -1, // undefined: end of the list, -1: following tab, or after index
  162. // active: false, // opens the tab in background - true and undefined: foreground
  163. // select: true // if a tab is already opened with that url, then select it instead of opening a new one
  164. vAPI.tabs.open = function(details) {
  165. var targetURL = details.url;
  166. if ( typeof targetURL !== 'string' || targetURL === '' ) {
  167. return null;
  168. }
  169. // extension pages
  170. if ( /^[\w-]{2,}:/.test(targetURL) !== true ) {
  171. targetURL = vAPI.getURL(targetURL);
  172. }
  173. // dealing with Chrome's asynchronous API
  174. var wrapper = function() {
  175. if ( details.active === undefined ) {
  176. details.active = true;
  177. }
  178. var subWrapper = function() {
  179. var _details = {
  180. url: targetURL,
  181. active: !!details.active
  182. };
  183. // Opening a tab from incognito window won't focus the window
  184. // in which the tab was opened
  185. var focusWindow = function(tab) {
  186. if ( tab.active ) {
  187. chrome.windows.update(tab.windowId, { focused: true });
  188. }
  189. };
  190. if ( !details.tabId ) {
  191. if ( details.index !== undefined ) {
  192. _details.index = details.index;
  193. }
  194. chrome.tabs.create(_details, focusWindow);
  195. return;
  196. }
  197. // update doesn't accept index, must use move
  198. chrome.tabs.update(parseInt(details.tabId, 10), _details, function(tab) {
  199. // if the tab doesn't exist
  200. if ( vAPI.lastError() ) {
  201. chrome.tabs.create(_details, focusWindow);
  202. } else if ( details.index !== undefined ) {
  203. chrome.tabs.move(tab.id, {index: details.index});
  204. }
  205. });
  206. };
  207. if ( details.index !== -1 ) {
  208. subWrapper();
  209. return;
  210. }
  211. vAPI.tabs.get(null, function(tab) {
  212. if ( tab ) {
  213. details.index = tab.index + 1;
  214. } else {
  215. delete details.index;
  216. }
  217. subWrapper();
  218. });
  219. };
  220. if ( !details.select ) {
  221. wrapper();
  222. return;
  223. }
  224. chrome.tabs.query({ url: targetURL }, function(tabs) {
  225. var tab = tabs[0];
  226. if ( tab ) {
  227. chrome.tabs.update(tab.id, { active: true }, function(tab) {
  228. chrome.windows.update(tab.windowId, { focused: true });
  229. });
  230. } else {
  231. wrapper();
  232. }
  233. });
  234. };
  235. /******************************************************************************/
  236. vAPI.tabs.remove = function(tabId) {
  237. var onTabRemoved = function() {
  238. if ( vAPI.lastError() ) {
  239. }
  240. };
  241. chrome.tabs.remove(parseInt(tabId, 10), onTabRemoved);
  242. };
  243. /******************************************************************************/
  244. vAPI.tabs.reload = function(tabId /*, flags*/) {
  245. if ( typeof tabId === 'string' ) {
  246. tabId = parseInt(tabId, 10);
  247. }
  248. chrome.tabs.reload(tabId);
  249. };
  250. /******************************************************************************/
  251. vAPI.tabs.injectScript = function(tabId, details, callback) {
  252. var onScriptExecuted = function() {
  253. // https://code.google.com/p/chromium/issues/detail?id=410868#c8
  254. if ( chrome.runtime.lastError ) {
  255. }
  256. if ( typeof callback === 'function' ) {
  257. callback();
  258. }
  259. };
  260. if ( tabId ) {
  261. tabId = parseInt(tabId, 10);
  262. chrome.tabs.executeScript(tabId, details, onScriptExecuted);
  263. } else {
  264. chrome.tabs.executeScript(details, onScriptExecuted);
  265. }
  266. };
  267. /******************************************************************************/
  268. // Must read: https://code.google.com/p/chromium/issues/detail?id=410868#c8
  269. // https://github.com/gorhill/uBlock/issues/19
  270. // https://github.com/gorhill/uBlock/issues/207
  271. // Since we may be called asynchronously, the tab id may not exist
  272. // anymore, so this ensures it does still exist.
  273. vAPI.setIcon = function(tabId, iconStatus, badge) {
  274. tabId = parseInt(tabId, 10);
  275. var onIconReady = function() {
  276. if ( vAPI.lastError() ) {
  277. return;
  278. }
  279. chrome.browserAction.setBadgeText({ tabId: tabId, text: badge });
  280. if ( badge !== '' ) {
  281. chrome.browserAction.setBadgeBackgroundColor({
  282. tabId: tabId,
  283. color: '#666'
  284. });
  285. }
  286. };
  287. var iconPaths = iconStatus === 'on' ?
  288. { '19': 'img/browsericons/icon19.png', '38': 'img/browsericons/icon38.png' } :
  289. { '19': 'img/browsericons/icon19-off.png', '38': 'img/browsericons/icon38-off.png' };
  290. chrome.browserAction.setIcon({ tabId: tabId, path: iconPaths }, onIconReady);
  291. };
  292. /******************************************************************************/
  293. vAPI.messaging = {
  294. ports: {},
  295. listeners: {},
  296. defaultHandler: null,
  297. NOOPFUNC: noopFunc,
  298. UNHANDLED: 'vAPI.messaging.notHandled'
  299. };
  300. /******************************************************************************/
  301. vAPI.messaging.listen = function(listenerName, callback) {
  302. this.listeners[listenerName] = callback;
  303. };
  304. /******************************************************************************/
  305. vAPI.messaging.onPortMessage = function(request, port) {
  306. var callback = vAPI.messaging.NOOPFUNC;
  307. if ( request.requestId !== undefined ) {
  308. callback = CallbackWrapper.factory(port, request).callback;
  309. }
  310. // Specific handler
  311. var r = vAPI.messaging.UNHANDLED;
  312. var listener = vAPI.messaging.listeners[request.channelName];
  313. if ( typeof listener === 'function' ) {
  314. r = listener(request.msg, port.sender, callback);
  315. }
  316. if ( r !== vAPI.messaging.UNHANDLED ) {
  317. return;
  318. }
  319. // Default handler
  320. r = vAPI.messaging.defaultHandler(request.msg, port.sender, callback);
  321. if ( r !== vAPI.messaging.UNHANDLED ) {
  322. return;
  323. }
  324. console.error('µBlock> messaging > unknown request: %o', request);
  325. // Unhandled:
  326. // Need to callback anyways in case caller expected an answer, or
  327. // else there is a memory leak on caller's side
  328. callback();
  329. };
  330. /******************************************************************************/
  331. vAPI.messaging.onPortDisconnect = function(port) {
  332. port.onDisconnect.removeListener(vAPI.messaging.onPortDisconnect);
  333. port.onMessage.removeListener(vAPI.messaging.onPortMessage);
  334. delete vAPI.messaging.ports[port.name];
  335. };
  336. /******************************************************************************/
  337. vAPI.messaging.onPortConnect = function(port) {
  338. port.onDisconnect.addListener(vAPI.messaging.onPortDisconnect);
  339. port.onMessage.addListener(vAPI.messaging.onPortMessage);
  340. vAPI.messaging.ports[port.name] = port;
  341. };
  342. /******************************************************************************/
  343. vAPI.messaging.setup = function(defaultHandler) {
  344. // Already setup?
  345. if ( this.defaultHandler !== null ) {
  346. return;
  347. }
  348. if ( typeof defaultHandler !== 'function' ) {
  349. defaultHandler = function(){ return vAPI.messaging.UNHANDLED; };
  350. }
  351. this.defaultHandler = defaultHandler;
  352. chrome.runtime.onConnect.addListener(this.onPortConnect);
  353. };
  354. /******************************************************************************/
  355. vAPI.messaging.broadcast = function(message) {
  356. var messageWrapper = {
  357. broadcast: true,
  358. msg: message
  359. };
  360. for ( var portName in this.ports ) {
  361. if ( this.ports.hasOwnProperty(portName) === false ) {
  362. continue;
  363. }
  364. this.ports[portName].postMessage(messageWrapper);
  365. }
  366. };
  367. /******************************************************************************/
  368. // This allows to avoid creating a closure for every single message which
  369. // expects an answer. Having a closure created each time a message is processed
  370. // has been always bothering me. Another benefit of the implementation here
  371. // is to reuse the callback proxy object, so less memory churning.
  372. //
  373. // https://developers.google.com/speed/articles/optimizing-javascript
  374. // "Creating a closure is significantly slower then creating an inner
  375. // function without a closure, and much slower than reusing a static
  376. // function"
  377. //
  378. // http://hacksoflife.blogspot.ca/2015/01/the-four-horsemen-of-performance.html
  379. // "the dreaded 'uniformly slow code' case where every function takes 1%
  380. // of CPU and you have to make one hundred separate performance optimizations
  381. // to improve performance at all"
  382. //
  383. // http://jsperf.com/closure-no-closure/2
  384. var CallbackWrapper = function(port, request) {
  385. // No need to bind every single time
  386. this.callback = this.proxy.bind(this);
  387. this.messaging = vAPI.messaging;
  388. this.init(port, request);
  389. };
  390. CallbackWrapper.junkyard = [];
  391. CallbackWrapper.factory = function(port, request) {
  392. var wrapper = CallbackWrapper.junkyard.pop();
  393. if ( wrapper ) {
  394. wrapper.init(port, request);
  395. return wrapper;
  396. }
  397. return new CallbackWrapper(port, request);
  398. };
  399. CallbackWrapper.prototype.init = function(port, request) {
  400. this.port = port;
  401. this.request = request;
  402. };
  403. CallbackWrapper.prototype.proxy = function(response) {
  404. // https://github.com/gorhill/uBlock/issues/383
  405. if ( this.messaging.ports.hasOwnProperty(this.port.name) ) {
  406. this.port.postMessage({
  407. requestId: this.request.requestId,
  408. channelName: this.request.channelName,
  409. msg: response !== undefined ? response : null
  410. });
  411. }
  412. // Mark for reuse
  413. this.port = this.request = null;
  414. CallbackWrapper.junkyard.push(this);
  415. };
  416. /******************************************************************************/
  417. vAPI.net = {};
  418. /******************************************************************************/
  419. vAPI.net.registerListeners = function() {
  420. var µb = µBlock;
  421. var µburi = µb.URI;
  422. var normalizeRequestDetails = function(details) {
  423. µburi.set(details.url);
  424. details.tabId = details.tabId.toString();
  425. details.hostname = µburi.hostnameFromURI(details.url);
  426. // The rest of the function code is to normalize type
  427. if ( details.type !== 'other' ) {
  428. return;
  429. }
  430. var tail = µburi.path.slice(-6);
  431. var pos = tail.lastIndexOf('.');
  432. // https://github.com/gorhill/uBlock/issues/862
  433. // If no transposition possible, transpose to `object` as per
  434. // Chromium bug 410382 (see below)
  435. if ( pos === -1 ) {
  436. details.type = 'object';
  437. return;
  438. }
  439. var ext = tail.slice(pos) + '.';
  440. if ( '.eot.ttf.otf.svg.woff.woff2.'.indexOf(ext) !== -1 ) {
  441. details.type = 'font';
  442. return;
  443. }
  444. // Still need this because often behind-the-scene requests are wrongly
  445. // categorized as 'other'
  446. if ( '.ico.png.gif.jpg.jpeg.webp.'.indexOf(ext) !== -1 ) {
  447. details.type = 'image';
  448. return;
  449. }
  450. // https://code.google.com/p/chromium/issues/detail?id=410382
  451. details.type = 'object';
  452. };
  453. var onBeforeRequestClient = this.onBeforeRequest.callback;
  454. var onBeforeRequest = function(details) {
  455. normalizeRequestDetails(details);
  456. return onBeforeRequestClient(details);
  457. };
  458. chrome.webRequest.onBeforeRequest.addListener(
  459. onBeforeRequest,
  460. //function(details) {
  461. // quickProfiler.start('onBeforeRequest');
  462. // var r = onBeforeRequest(details);
  463. // quickProfiler.stop();
  464. // return r;
  465. //},
  466. {
  467. 'urls': this.onBeforeRequest.urls || ['<all_urls>'],
  468. 'types': this.onBeforeRequest.types || []
  469. },
  470. this.onBeforeRequest.extra
  471. );
  472. var onHeadersReceivedClient = this.onHeadersReceived.callback;
  473. var onHeadersReceived = function(details) {
  474. normalizeRequestDetails(details);
  475. return onHeadersReceivedClient(details);
  476. };
  477. chrome.webRequest.onHeadersReceived.addListener(
  478. onHeadersReceived,
  479. {
  480. 'urls': this.onHeadersReceived.urls || ['<all_urls>'],
  481. 'types': this.onHeadersReceived.types || []
  482. },
  483. this.onHeadersReceived.extra
  484. );
  485. };
  486. /******************************************************************************/
  487. vAPI.contextMenu = {
  488. create: function(details, callback) {
  489. this.menuId = details.id;
  490. this.callback = callback;
  491. chrome.contextMenus.create(details);
  492. chrome.contextMenus.onClicked.addListener(this.callback);
  493. },
  494. remove: function() {
  495. chrome.contextMenus.onClicked.removeListener(this.callback);
  496. chrome.contextMenus.remove(this.menuId);
  497. }
  498. };
  499. /******************************************************************************/
  500. vAPI.lastError = function() {
  501. return chrome.runtime.lastError;
  502. };
  503. /******************************************************************************/
  504. // This is called only once, when everything has been loaded in memory after
  505. // the extension was launched. It can be used to inject content scripts
  506. // in already opened web pages, to remove whatever nuisance could make it to
  507. // the web pages before uBlock was ready.
  508. vAPI.onLoadAllCompleted = function() {
  509. // http://code.google.com/p/chromium/issues/detail?id=410868#c11
  510. // Need to be sure to access `vAPI.lastError()` to prevent
  511. // spurious warnings in the console.
  512. var scriptDone = function() {
  513. vAPI.lastError();
  514. };
  515. var scriptEnd = function(tabId) {
  516. if ( vAPI.lastError() ) {
  517. return;
  518. }
  519. vAPI.tabs.injectScript(tabId, {
  520. file: 'js/contentscript-end.js',
  521. allFrames: true,
  522. runAt: 'document_idle'
  523. }, scriptDone);
  524. };
  525. var scriptStart = function(tabId) {
  526. vAPI.tabs.injectScript(tabId, {
  527. file: 'js/vapi-client.js',
  528. allFrames: true,
  529. runAt: 'document_start'
  530. }, function(){ });
  531. vAPI.tabs.injectScript(tabId, {
  532. file: 'js/contentscript-start.js',
  533. allFrames: true,
  534. runAt: 'document_start'
  535. }, function(){ scriptEnd(tabId); });
  536. };
  537. var bindToTabs = function(tabs) {
  538. var µb = µBlock;
  539. var i = tabs.length, tab;
  540. while ( i-- ) {
  541. tab = tabs[i];
  542. µb.bindTabToPageStats(tab.id, tab.url);
  543. // https://github.com/gorhill/uBlock/issues/129
  544. scriptStart(tab.id);
  545. }
  546. };
  547. chrome.tabs.query({ url: 'http://*/*' }, bindToTabs);
  548. chrome.tabs.query({ url: 'https://*/*' }, bindToTabs);
  549. };
  550. /******************************************************************************/
  551. vAPI.punycodeHostname = function(hostname) {
  552. return hostname;
  553. };
  554. vAPI.punycodeURL = function(url) {
  555. return url;
  556. };
  557. /******************************************************************************/
  558. })();
  559. /******************************************************************************/