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.

1062 lines
33 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
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
9 years ago
9 years ago
9 years ago
9 years ago
10 years ago
10 years ago
  1. /*******************************************************************************
  2. uMatrix - a browser extension to block requests.
  3. Copyright (C) 2014-2017 The uBlock Origin authors
  4. Copyright (C) 2017 Raymond Hill
  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://github.com/gorhill/uMatrix
  16. */
  17. /* global self, µMatrix */
  18. // For background page
  19. 'use strict';
  20. /******************************************************************************/
  21. (function() {
  22. /******************************************************************************/
  23. var vAPI = self.vAPI = self.vAPI || {};
  24. var chrome = self.chrome;
  25. var manifest = chrome.runtime.getManifest();
  26. vAPI.chrome = true;
  27. var noopFunc = function(){};
  28. /******************************************************************************/
  29. // https://github.com/gorhill/uMatrix/issues/234
  30. // https://developer.chrome.com/extensions/privacy#property-network
  31. chrome.privacy.network.networkPredictionEnabled.set({
  32. value: false
  33. });
  34. // rhill 2013-12-07:
  35. // Tell Chromium to allow all javascript: µMatrix will control whether
  36. // javascript execute through `Content-Policy-Directive` and webRequest.
  37. // https://github.com/gorhill/httpswitchboard/issues/74
  38. // Firefox WebExtensions does not support contentSettings API, yet.
  39. if ( chrome.contentSettings instanceof Object ) {
  40. chrome.contentSettings.javascript.set({
  41. primaryPattern: 'https://*/*',
  42. setting: 'allow'
  43. });
  44. chrome.contentSettings.javascript.set({
  45. primaryPattern: 'http://*/*',
  46. setting: 'allow'
  47. });
  48. }
  49. /******************************************************************************/
  50. vAPI.app = {
  51. name: manifest.name,
  52. version: manifest.version
  53. };
  54. /******************************************************************************/
  55. vAPI.app.start = function() {
  56. // rhill 2013-12-07:
  57. // Relinquish control over javascript execution to the user.
  58. // https://github.com/gorhill/httpswitchboard/issues/74
  59. //chrome.contentSettings.javascript.clear({});
  60. };
  61. /******************************************************************************/
  62. vAPI.app.stop = function() {
  63. // Firefox WebExtensions does not support contentSettings API, yet.
  64. if ( chrome.contentSettings instanceof Object ) {
  65. chrome.contentSettings.javascript.clear({});
  66. }
  67. // rhill 2013-12-07:
  68. // Tell Chromium to allow all javascript: µMatrix will control whether
  69. // javascript execute through `Content-Policy-Directive` and webRequest.
  70. // https://github.com/gorhill/httpswitchboard/issues/74
  71. //chrome.contentSettings.javascript.set({
  72. // primaryPattern: 'https://*/*',
  73. // setting: 'allow'
  74. //});
  75. //chrome.contentSettings.javascript.set({
  76. // primaryPattern: 'http://*/*',
  77. // setting: 'allow'
  78. //});
  79. };
  80. /******************************************************************************/
  81. vAPI.app.restart = function() {
  82. chrome.runtime.reload();
  83. };
  84. /******************************************************************************/
  85. // chrome.storage.local.get(null, function(bin){ console.debug('%o', bin); });
  86. vAPI.storage = chrome.storage.local;
  87. vAPI.cacheStorage = chrome.storage.local;
  88. /******************************************************************************/
  89. vAPI.tabs = {};
  90. /******************************************************************************/
  91. vAPI.isBehindTheSceneTabId = function(tabId) {
  92. return tabId.toString() === '-1';
  93. };
  94. vAPI.noTabId = '-1';
  95. /******************************************************************************/
  96. vAPI.tabs.registerListeners = function() {
  97. var onNavigationClient = this.onNavigation || noopFunc;
  98. var onUpdatedClient = this.onUpdated || noopFunc;
  99. var onClosedClient = this.onClosed || noopFunc;
  100. // https://developer.chrome.com/extensions/webNavigation
  101. // [onCreatedNavigationTarget ->]
  102. // onBeforeNavigate ->
  103. // onCommitted ->
  104. // onDOMContentLoaded ->
  105. // onCompleted
  106. // The chrome.webRequest.onBeforeRequest() won't be called for everything
  107. // else than `http`/`https`. Thus, in such case, we will bind the tab as
  108. // early as possible in order to increase the likelihood of a context
  109. // properly setup if network requests are fired from within the tab.
  110. // Example: Chromium + case #6 at
  111. // http://raymondhill.net/ublock/popup.html
  112. var reGoodForWebRequestAPI = /^https?:\/\//;
  113. var onCreatedNavigationTarget = function(details) {
  114. //console.debug('onCreatedNavigationTarget: tab id %d = "%s"', details.tabId, details.url);
  115. if ( reGoodForWebRequestAPI.test(details.url) ) {
  116. return;
  117. }
  118. details.tabId = details.tabId.toString();
  119. onNavigationClient(details);
  120. };
  121. var onUpdated = function(tabId, changeInfo, tab) {
  122. tabId = tabId.toString();
  123. onUpdatedClient(tabId, changeInfo, tab);
  124. };
  125. var onCommitted = function(details) {
  126. // Important: do not call client if not top frame.
  127. if ( details.frameId !== 0 ) {
  128. return;
  129. }
  130. details.tabId = details.tabId.toString();
  131. onNavigationClient(details);
  132. //console.debug('onCommitted: tab id %d = "%s"', details.tabId, details.url);
  133. };
  134. var onClosed = function(tabId) {
  135. onClosedClient(tabId.toString());
  136. };
  137. chrome.webNavigation.onCreatedNavigationTarget.addListener(onCreatedNavigationTarget);
  138. chrome.webNavigation.onCommitted.addListener(onCommitted);
  139. chrome.tabs.onUpdated.addListener(onUpdated);
  140. chrome.tabs.onRemoved.addListener(onClosed);
  141. };
  142. /******************************************************************************/
  143. // tabId: null, // active tab
  144. vAPI.tabs.get = function(tabId, callback) {
  145. var onTabReady = function(tab) {
  146. // https://code.google.com/p/chromium/issues/detail?id=410868#c8
  147. if ( chrome.runtime.lastError ) {
  148. }
  149. if ( tab instanceof Object ) {
  150. tab.id = tab.id.toString();
  151. }
  152. callback(tab);
  153. };
  154. if ( tabId !== null ) {
  155. if ( typeof tabId === 'string' ) {
  156. tabId = parseInt(tabId, 10);
  157. }
  158. chrome.tabs.get(tabId, onTabReady);
  159. return;
  160. }
  161. var onTabReceived = function(tabs) {
  162. // https://code.google.com/p/chromium/issues/detail?id=410868#c8
  163. if ( chrome.runtime.lastError ) {
  164. }
  165. var tab = null;
  166. if ( Array.isArray(tabs) && tabs.length !== 0 ) {
  167. tab = tabs[0];
  168. tab.id = tab.id.toString();
  169. }
  170. callback(tab);
  171. };
  172. chrome.tabs.query({ active: true, currentWindow: true }, onTabReceived);
  173. };
  174. /******************************************************************************/
  175. vAPI.tabs.getAll = function(callback) {
  176. var onTabsReady = function(tabs) {
  177. if ( Array.isArray(tabs) ) {
  178. var i = tabs.length;
  179. while ( i-- ) {
  180. tabs[i].id = tabs[i].id.toString();
  181. }
  182. }
  183. callback(tabs);
  184. };
  185. chrome.tabs.query({ url: '<all_urls>' }, onTabsReady);
  186. };
  187. /******************************************************************************/
  188. // properties of the details object:
  189. // url: 'URL', // the address that will be opened
  190. // tabId: 1, // the tab is used if set, instead of creating a new one
  191. // index: -1, // undefined: end of the list, -1: following tab, or after index
  192. // active: false, // opens the tab in background - true and undefined: foreground
  193. // select: true // if a tab is already opened with that url, then select it instead of opening a new one
  194. vAPI.tabs.open = function(details) {
  195. var targetURL = details.url;
  196. if ( typeof targetURL !== 'string' || targetURL === '' ) {
  197. return null;
  198. }
  199. // extension pages
  200. if ( /^[\w-]{2,}:/.test(targetURL) !== true ) {
  201. targetURL = vAPI.getURL(targetURL);
  202. }
  203. // dealing with Chrome's asynchronous API
  204. var wrapper = function() {
  205. if ( details.active === undefined ) {
  206. details.active = true;
  207. }
  208. var subWrapper = function() {
  209. var _details = {
  210. url: targetURL,
  211. active: !!details.active
  212. };
  213. // Opening a tab from incognito window won't focus the window
  214. // in which the tab was opened
  215. var focusWindow = function(tab) {
  216. if ( tab.active ) {
  217. chrome.windows.update(tab.windowId, { focused: true });
  218. }
  219. };
  220. if ( !details.tabId ) {
  221. if ( details.index !== undefined ) {
  222. _details.index = details.index;
  223. }
  224. chrome.tabs.create(_details, focusWindow);
  225. return;
  226. }
  227. // update doesn't accept index, must use move
  228. chrome.tabs.update(parseInt(details.tabId, 10), _details, function(tab) {
  229. // if the tab doesn't exist
  230. if ( vAPI.lastError() ) {
  231. chrome.tabs.create(_details, focusWindow);
  232. } else if ( details.index !== undefined ) {
  233. chrome.tabs.move(tab.id, {index: details.index});
  234. }
  235. });
  236. };
  237. if ( details.index !== -1 ) {
  238. subWrapper();
  239. return;
  240. }
  241. vAPI.tabs.get(null, function(tab) {
  242. if ( tab ) {
  243. details.index = tab.index + 1;
  244. } else {
  245. delete details.index;
  246. }
  247. subWrapper();
  248. });
  249. };
  250. if ( !details.select ) {
  251. wrapper();
  252. return;
  253. }
  254. chrome.tabs.query({ url: targetURL }, function(tabs) {
  255. var tab = tabs[0];
  256. if ( tab ) {
  257. chrome.tabs.update(tab.id, { active: true }, function(tab) {
  258. chrome.windows.update(tab.windowId, { focused: true });
  259. });
  260. } else {
  261. wrapper();
  262. }
  263. });
  264. };
  265. /******************************************************************************/
  266. // Replace the URL of a tab. Noop if the tab does not exist.
  267. vAPI.tabs.replace = function(tabId, url) {
  268. var targetURL = url;
  269. // extension pages
  270. if ( /^[\w-]{2,}:/.test(targetURL) !== true ) {
  271. targetURL = vAPI.getURL(targetURL);
  272. }
  273. if ( typeof tabId !== 'number' ) {
  274. tabId = parseInt(tabId, 10);
  275. if ( isNaN(tabId) ) {
  276. return;
  277. }
  278. }
  279. chrome.tabs.update(tabId, { url: targetURL }, function() {
  280. // this prevent console error
  281. if ( chrome.runtime.lastError ) {
  282. return;
  283. }
  284. });
  285. };
  286. /******************************************************************************/
  287. vAPI.tabs.remove = function(tabId) {
  288. var onTabRemoved = function() {
  289. if ( vAPI.lastError() ) {
  290. }
  291. };
  292. chrome.tabs.remove(parseInt(tabId, 10), onTabRemoved);
  293. };
  294. /******************************************************************************/
  295. vAPI.tabs.reload = function(tabId /*, flags*/) {
  296. if ( typeof tabId === 'string' ) {
  297. tabId = parseInt(tabId, 10);
  298. }
  299. if ( isNaN(tabId) ) {
  300. return;
  301. }
  302. chrome.tabs.reload(tabId);
  303. };
  304. /******************************************************************************/
  305. vAPI.tabs.injectScript = function(tabId, details, callback) {
  306. var onScriptExecuted = function() {
  307. // https://code.google.com/p/chromium/issues/detail?id=410868#c8
  308. if ( chrome.runtime.lastError ) {
  309. }
  310. if ( typeof callback === 'function' ) {
  311. callback();
  312. }
  313. };
  314. if ( tabId ) {
  315. tabId = parseInt(tabId, 10);
  316. chrome.tabs.executeScript(tabId, details, onScriptExecuted);
  317. } else {
  318. chrome.tabs.executeScript(details, onScriptExecuted);
  319. }
  320. };
  321. /******************************************************************************/
  322. // Must read: https://code.google.com/p/chromium/issues/detail?id=410868#c8
  323. // https://github.com/chrisaljoudi/uBlock/issues/19
  324. // https://github.com/chrisaljoudi/uBlock/issues/207
  325. // Since we may be called asynchronously, the tab id may not exist
  326. // anymore, so this ensures it does still exist.
  327. vAPI.setIcon = function(tabId, iconId, badge) {
  328. tabId = parseInt(tabId, 10);
  329. if ( isNaN(tabId) || tabId <= 0 ) {
  330. return;
  331. }
  332. var onIconReady = function() {
  333. if ( vAPI.lastError() ) {
  334. return;
  335. }
  336. chrome.browserAction.setBadgeText({ tabId: tabId, text: badge });
  337. if ( badge !== '' ) {
  338. chrome.browserAction.setBadgeBackgroundColor({
  339. tabId: tabId,
  340. color: '#000'
  341. });
  342. }
  343. };
  344. var iconSelector = typeof iconId === 'number' ? iconId : 'off';
  345. var iconPaths = {
  346. '19': 'img/browsericons/icon19-' + iconSelector + '.png'/* ,
  347. '38': 'img/browsericons/icon38-' + iconSelector + '.png' */
  348. };
  349. chrome.browserAction.setIcon({ tabId: tabId, path: iconPaths }, onIconReady);
  350. };
  351. /******************************************************************************/
  352. /******************************************************************************/
  353. vAPI.messaging = {
  354. ports: {},
  355. listeners: {},
  356. defaultHandler: null,
  357. NOOPFUNC: noopFunc,
  358. UNHANDLED: 'vAPI.messaging.notHandled'
  359. };
  360. /******************************************************************************/
  361. vAPI.messaging.listen = function(listenerName, callback) {
  362. this.listeners[listenerName] = callback;
  363. };
  364. /******************************************************************************/
  365. vAPI.messaging.onPortMessage = function(request, port) {
  366. var callback = vAPI.messaging.NOOPFUNC;
  367. if ( request.requestId !== undefined ) {
  368. callback = CallbackWrapper.factory(port, request).callback;
  369. }
  370. // Specific handler
  371. var r = vAPI.messaging.UNHANDLED;
  372. var listener = vAPI.messaging.listeners[request.channelName];
  373. if ( typeof listener === 'function' ) {
  374. r = listener(request.msg, port.sender, callback);
  375. }
  376. if ( r !== vAPI.messaging.UNHANDLED ) {
  377. return;
  378. }
  379. // Default handler
  380. r = vAPI.messaging.defaultHandler(request.msg, port.sender, callback);
  381. if ( r !== vAPI.messaging.UNHANDLED ) {
  382. return;
  383. }
  384. console.error('µMatrix> messaging > unknown request: %o', request);
  385. // Unhandled:
  386. // Need to callback anyways in case caller expected an answer, or
  387. // else there is a memory leak on caller's side
  388. callback();
  389. };
  390. /******************************************************************************/
  391. vAPI.messaging.onPortDisconnect = function(port) {
  392. port.onDisconnect.removeListener(vAPI.messaging.onPortDisconnect);
  393. port.onMessage.removeListener(vAPI.messaging.onPortMessage);
  394. delete vAPI.messaging.ports[port.name];
  395. };
  396. /******************************************************************************/
  397. vAPI.messaging.onPortConnect = function(port) {
  398. port.onDisconnect.addListener(vAPI.messaging.onPortDisconnect);
  399. port.onMessage.addListener(vAPI.messaging.onPortMessage);
  400. vAPI.messaging.ports[port.name] = port;
  401. };
  402. /******************************************************************************/
  403. vAPI.messaging.setup = function(defaultHandler) {
  404. // Already setup?
  405. if ( this.defaultHandler !== null ) {
  406. return;
  407. }
  408. if ( typeof defaultHandler !== 'function' ) {
  409. defaultHandler = function(){ return vAPI.messaging.UNHANDLED; };
  410. }
  411. this.defaultHandler = defaultHandler;
  412. chrome.runtime.onConnect.addListener(this.onPortConnect);
  413. };
  414. /******************************************************************************/
  415. vAPI.messaging.broadcast = function(message) {
  416. var messageWrapper = {
  417. broadcast: true,
  418. msg: message
  419. };
  420. for ( var portName in this.ports ) {
  421. if ( this.ports.hasOwnProperty(portName) === false ) {
  422. continue;
  423. }
  424. this.ports[portName].postMessage(messageWrapper);
  425. }
  426. };
  427. /******************************************************************************/
  428. // This allows to avoid creating a closure for every single message which
  429. // expects an answer. Having a closure created each time a message is processed
  430. // has been always bothering me. Another benefit of the implementation here
  431. // is to reuse the callback proxy object, so less memory churning.
  432. //
  433. // https://developers.google.com/speed/articles/optimizing-javascript
  434. // "Creating a closure is significantly slower then creating an inner
  435. // function without a closure, and much slower than reusing a static
  436. // function"
  437. //
  438. // http://hacksoflife.blogspot.ca/2015/01/the-four-horsemen-of-performance.html
  439. // "the dreaded 'uniformly slow code' case where every function takes 1%
  440. // of CPU and you have to make one hundred separate performance optimizations
  441. // to improve performance at all"
  442. //
  443. // http://jsperf.com/closure-no-closure/2
  444. var CallbackWrapper = function(port, request) {
  445. // No need to bind every single time
  446. this.callback = this.proxy.bind(this);
  447. this.messaging = vAPI.messaging;
  448. this.init(port, request);
  449. };
  450. CallbackWrapper.junkyard = [];
  451. CallbackWrapper.factory = function(port, request) {
  452. var wrapper = CallbackWrapper.junkyard.pop();
  453. if ( wrapper ) {
  454. wrapper.init(port, request);
  455. return wrapper;
  456. }
  457. return new CallbackWrapper(port, request);
  458. };
  459. CallbackWrapper.prototype.init = function(port, request) {
  460. this.port = port;
  461. this.request = request;
  462. };
  463. CallbackWrapper.prototype.proxy = function(response) {
  464. // https://github.com/chrisaljoudi/uBlock/issues/383
  465. if ( this.messaging.ports.hasOwnProperty(this.port.name) ) {
  466. this.port.postMessage({
  467. requestId: this.request.requestId,
  468. channelName: this.request.channelName,
  469. msg: response !== undefined ? response : null
  470. });
  471. }
  472. // Mark for reuse
  473. this.port = this.request = null;
  474. CallbackWrapper.junkyard.push(this);
  475. };
  476. /******************************************************************************/
  477. /******************************************************************************/
  478. vAPI.net = {};
  479. /******************************************************************************/
  480. vAPI.net.registerListeners = function() {
  481. var µm = µMatrix,
  482. reNetworkURL = /^(?:https?|wss?):\/\//,
  483. httpRequestHeadersJunkyard = [];
  484. // Abstraction layer to deal with request headers
  485. // >>>>>>>>
  486. var httpRequestHeadersFactory = function(headers) {
  487. var entry = httpRequestHeadersJunkyard.pop();
  488. if ( entry ) {
  489. return entry.init(headers);
  490. }
  491. return new HTTPRequestHeaders(headers);
  492. };
  493. var HTTPRequestHeaders = function(headers) {
  494. this.init(headers);
  495. };
  496. HTTPRequestHeaders.prototype.init = function(headers) {
  497. this.modified = false;
  498. this.headers = headers;
  499. return this;
  500. };
  501. HTTPRequestHeaders.prototype.dispose = function() {
  502. var r = this.modified ? this.headers : null;
  503. this.headers = null;
  504. httpRequestHeadersJunkyard.push(this);
  505. return r;
  506. };
  507. HTTPRequestHeaders.prototype.getHeader = function(target) {
  508. var headers = this.headers;
  509. var header, name;
  510. var i = headers.length;
  511. while ( i-- ) {
  512. header = headers[i];
  513. name = header.name.toLowerCase();
  514. if ( name === target ) {
  515. return header.value;
  516. }
  517. }
  518. return '';
  519. };
  520. HTTPRequestHeaders.prototype.setHeader = function(target, value, create) {
  521. var headers = this.headers;
  522. var header, name;
  523. var i = headers.length;
  524. while ( i-- ) {
  525. header = headers[i];
  526. name = header.name.toLowerCase();
  527. if ( name === target ) {
  528. break;
  529. }
  530. }
  531. if ( i < 0 && !create ) { // Header not found, don't add it
  532. return false;
  533. }
  534. if ( i < 0 ) { // Header not found, add it
  535. headers.push({ name: target, value: value });
  536. } else if ( value === '' ) { // Header found, remove it
  537. headers.splice(i, 1);
  538. } else { // Header found, modify it
  539. header.value = value;
  540. }
  541. this.modified = true;
  542. return true;
  543. };
  544. // <<<<<<<<
  545. // End of: Abstraction layer to deal with request headers
  546. // Normalizing request types
  547. // >>>>>>>>
  548. var extToTypeMap = new Map([
  549. ['eot','font'],['otf','font'],['svg','font'],['ttf','font'],['woff','font'],['woff2','font'],
  550. ['mp3','media'],['mp4','media'],['webm','media'],
  551. ['gif','image'],['ico','image'],['jpeg','image'],['jpg','image'],['png','image'],['webp','image']
  552. ]);
  553. var normalizeRequestDetails = function(details) {
  554. details.tabId = details.tabId.toString();
  555. var type = details.type;
  556. if ( type === 'imageset' ) {
  557. details.type = 'image';
  558. return;
  559. }
  560. // The rest of the function code is to normalize request type
  561. if ( type !== 'other' ) {
  562. return;
  563. }
  564. if ( details.requestHeaders instanceof HTTPRequestHeaders ) {
  565. if ( details.requestHeaders.getHeader('ping-to') !== '' ) {
  566. details.type = 'ping';
  567. return;
  568. }
  569. }
  570. // Try to map known "extension" part of URL to request type.
  571. var path = µm.URI.pathFromURI(details.url),
  572. pos = path.indexOf('.', path.length - 6);
  573. if ( pos !== -1 && (type = extToTypeMap.get(path.slice(pos + 1))) ) {
  574. details.type = type;
  575. }
  576. };
  577. // <<<<<<<<
  578. // End of: Normalizing request types
  579. // Network event handlers
  580. // >>>>>>>>
  581. var onBeforeRequestClient = this.onBeforeRequest.callback;
  582. chrome.webRequest.onBeforeRequest.addListener(
  583. function(details) {
  584. if ( reNetworkURL.test(details.url) ) {
  585. normalizeRequestDetails(details);
  586. return onBeforeRequestClient(details);
  587. }
  588. },
  589. //function(details) {
  590. // quickProfiler.start('onBeforeRequest');
  591. // var r = onBeforeRequest(details);
  592. // quickProfiler.stop();
  593. // return r;
  594. //},
  595. {
  596. 'urls': this.onBeforeRequest.urls || [ '<all_urls>' ],
  597. 'types': this.onBeforeRequest.types || undefined
  598. },
  599. this.onBeforeRequest.extra
  600. );
  601. var onBeforeSendHeadersClient = this.onBeforeSendHeaders.callback;
  602. var onBeforeSendHeaders = function(details) {
  603. details.requestHeaders = httpRequestHeadersFactory(details.requestHeaders);
  604. normalizeRequestDetails(details);
  605. var result = onBeforeSendHeadersClient(details);
  606. if ( typeof result === 'object' ) {
  607. return result;
  608. }
  609. var modifiedHeaders = details.requestHeaders.dispose();
  610. if ( modifiedHeaders !== null ) {
  611. return { requestHeaders: modifiedHeaders };
  612. }
  613. };
  614. chrome.webRequest.onBeforeSendHeaders.addListener(
  615. onBeforeSendHeaders,
  616. {
  617. 'urls': this.onBeforeSendHeaders.urls || [ '<all_urls>' ],
  618. 'types': this.onBeforeSendHeaders.types || undefined
  619. },
  620. this.onBeforeSendHeaders.extra
  621. );
  622. var onHeadersReceivedClient = this.onHeadersReceived.callback;
  623. var onHeadersReceived = function(details) {
  624. normalizeRequestDetails(details);
  625. return onHeadersReceivedClient(details);
  626. };
  627. chrome.webRequest.onHeadersReceived.addListener(
  628. onHeadersReceived,
  629. {
  630. 'urls': this.onHeadersReceived.urls || [ '<all_urls>' ],
  631. 'types': this.onHeadersReceived.types || undefined
  632. },
  633. this.onHeadersReceived.extra
  634. );
  635. // <<<<<<<<
  636. // End of: Network event handlers
  637. };
  638. /******************************************************************************/
  639. /******************************************************************************/
  640. vAPI.contextMenu = {
  641. create: function(details, callback) {
  642. this.menuId = details.id;
  643. this.callback = callback;
  644. chrome.contextMenus.create(details);
  645. chrome.contextMenus.onClicked.addListener(this.callback);
  646. },
  647. remove: function() {
  648. chrome.contextMenus.onClicked.removeListener(this.callback);
  649. chrome.contextMenus.remove(this.menuId);
  650. }
  651. };
  652. /******************************************************************************/
  653. vAPI.lastError = function() {
  654. return chrome.runtime.lastError;
  655. };
  656. /******************************************************************************/
  657. /******************************************************************************/
  658. // This is called only once, when everything has been loaded in memory after
  659. // the extension was launched. It can be used to inject content scripts
  660. // in already opened web pages, to remove whatever nuisance could make it to
  661. // the web pages before uBlock was ready.
  662. vAPI.onLoadAllCompleted = function() {
  663. };
  664. /******************************************************************************/
  665. /******************************************************************************/
  666. vAPI.punycodeHostname = function(hostname) {
  667. return hostname;
  668. };
  669. vAPI.punycodeURL = function(url) {
  670. return url;
  671. };
  672. /******************************************************************************/
  673. /******************************************************************************/
  674. vAPI.browserData = {};
  675. /******************************************************************************/
  676. // https://developer.chrome.com/extensions/browsingData
  677. vAPI.browserData.clearCache = function(callback) {
  678. chrome.browsingData.removeCache({ since: 0 }, callback);
  679. };
  680. /******************************************************************************/
  681. // Not supported on Chromium
  682. vAPI.browserData.clearOrigin = function(domain, callback) {
  683. // unsupported on Chromium
  684. if ( typeof callback === 'function' ) {
  685. callback(undefined);
  686. }
  687. };
  688. /******************************************************************************/
  689. /******************************************************************************/
  690. // https://developer.chrome.com/extensions/cookies
  691. vAPI.cookies = {};
  692. /******************************************************************************/
  693. vAPI.cookies.start = function() {
  694. var reallyRemoved = {
  695. 'evicted': true,
  696. 'expired': true,
  697. 'explicit': true
  698. };
  699. var onChanged = function(changeInfo) {
  700. if ( changeInfo.removed ) {
  701. if ( reallyRemoved[changeInfo.cause] && typeof this.onRemoved === 'function' ) {
  702. this.onRemoved(changeInfo.cookie);
  703. }
  704. return;
  705. }
  706. if ( typeof this.onChanged === 'function' ) {
  707. this.onChanged(changeInfo.cookie);
  708. }
  709. };
  710. chrome.cookies.onChanged.addListener(onChanged.bind(this));
  711. };
  712. /******************************************************************************/
  713. vAPI.cookies.getAll = function(callback) {
  714. chrome.cookies.getAll({}, callback);
  715. };
  716. /******************************************************************************/
  717. vAPI.cookies.remove = function(details, callback) {
  718. chrome.cookies.remove(details, callback || noopFunc);
  719. };
  720. /******************************************************************************/
  721. /******************************************************************************/
  722. vAPI.cloud = (function() {
  723. var chunkCountPerFetch = 16; // Must be a power of 2
  724. // Mind chrome.storage.sync.MAX_ITEMS (512 at time of writing)
  725. var maxChunkCountPerItem = Math.floor(512 * 0.75) & ~(chunkCountPerFetch - 1);
  726. // Mind chrome.storage.sync.QUOTA_BYTES_PER_ITEM (8192 at time of writing)
  727. var maxChunkSize = Math.floor(chrome.storage.sync.QUOTA_BYTES_PER_ITEM * 0.75);
  728. // Mind chrome.storage.sync.QUOTA_BYTES_PER_ITEM (8192 at time of writing)
  729. var maxStorageSize = chrome.storage.sync.QUOTA_BYTES;
  730. var options = {
  731. defaultDeviceName: window.navigator.platform,
  732. deviceName: window.localStorage.getItem('deviceName') || ''
  733. };
  734. // This is used to find out a rough count of how many chunks exists:
  735. // We "poll" at specific index in order to get a rough idea of how
  736. // large is the stored string.
  737. // This allows reading a single item with only 2 sync operations -- a
  738. // good thing given chrome.storage.syncMAX_WRITE_OPERATIONS_PER_MINUTE
  739. // and chrome.storage.syncMAX_WRITE_OPERATIONS_PER_HOUR.
  740. var getCoarseChunkCount = function(dataKey, callback) {
  741. var bin = {};
  742. for ( var i = 0; i < maxChunkCountPerItem; i += 16 ) {
  743. bin[dataKey + i.toString()] = '';
  744. }
  745. chrome.storage.sync.get(bin, function(bin) {
  746. if ( chrome.runtime.lastError ) {
  747. callback(0, chrome.runtime.lastError.message);
  748. return;
  749. }
  750. var chunkCount = 0;
  751. for ( var i = 0; i < maxChunkCountPerItem; i += 16 ) {
  752. if ( bin[dataKey + i.toString()] === '' ) {
  753. break;
  754. }
  755. chunkCount = i + 16;
  756. }
  757. callback(chunkCount);
  758. });
  759. };
  760. var deleteChunks = function(dataKey, start) {
  761. var keys = [];
  762. // No point in deleting more than:
  763. // - The max number of chunks per item
  764. // - The max number of chunks per storage limit
  765. var n = Math.min(
  766. maxChunkCountPerItem,
  767. Math.ceil(maxStorageSize / maxChunkSize)
  768. );
  769. for ( var i = start; i < n; i++ ) {
  770. keys.push(dataKey + i.toString());
  771. }
  772. chrome.storage.sync.remove(keys);
  773. };
  774. var start = function(/* dataKeys */) {
  775. };
  776. var push = function(dataKey, data, callback) {
  777. var bin = {
  778. 'source': options.deviceName || options.defaultDeviceName,
  779. 'tstamp': Date.now(),
  780. 'data': data,
  781. 'size': 0
  782. };
  783. bin.size = JSON.stringify(bin).length;
  784. var item = JSON.stringify(bin);
  785. // Chunkify taking into account QUOTA_BYTES_PER_ITEM:
  786. // https://developer.chrome.com/extensions/storage#property-sync
  787. // "The maximum size (in bytes) of each individual item in sync
  788. // "storage, as measured by the JSON stringification of its value
  789. // "plus its key length."
  790. bin = {};
  791. var chunkCount = Math.ceil(item.length / maxChunkSize);
  792. for ( var i = 0; i < chunkCount; i++ ) {
  793. bin[dataKey + i.toString()] = item.substr(i * maxChunkSize, maxChunkSize);
  794. }
  795. bin[dataKey + i.toString()] = ''; // Sentinel
  796. chrome.storage.sync.set(bin, function() {
  797. var errorStr;
  798. if ( chrome.runtime.lastError ) {
  799. errorStr = chrome.runtime.lastError.message;
  800. }
  801. callback(errorStr);
  802. // Remove potentially unused trailing chunks
  803. deleteChunks(dataKey, chunkCount);
  804. });
  805. };
  806. var pull = function(dataKey, callback) {
  807. var assembleChunks = function(bin) {
  808. if ( chrome.runtime.lastError ) {
  809. callback(null, chrome.runtime.lastError.message);
  810. return;
  811. }
  812. // Assemble chunks into a single string.
  813. var json = [], jsonSlice;
  814. var i = 0;
  815. for (;;) {
  816. jsonSlice = bin[dataKey + i.toString()];
  817. if ( jsonSlice === '' ) {
  818. break;
  819. }
  820. json.push(jsonSlice);
  821. i += 1;
  822. }
  823. var entry = null;
  824. try {
  825. entry = JSON.parse(json.join(''));
  826. } catch(ex) {
  827. }
  828. callback(entry);
  829. };
  830. var fetchChunks = function(coarseCount, errorStr) {
  831. if ( coarseCount === 0 || typeof errorStr === 'string' ) {
  832. callback(null, errorStr);
  833. return;
  834. }
  835. var bin = {};
  836. for ( var i = 0; i < coarseCount; i++ ) {
  837. bin[dataKey + i.toString()] = '';
  838. }
  839. chrome.storage.sync.get(bin, assembleChunks);
  840. };
  841. getCoarseChunkCount(dataKey, fetchChunks);
  842. };
  843. var getOptions = function(callback) {
  844. if ( typeof callback !== 'function' ) {
  845. return;
  846. }
  847. callback(options);
  848. };
  849. var setOptions = function(details, callback) {
  850. if ( typeof details !== 'object' || details === null ) {
  851. return;
  852. }
  853. if ( typeof details.deviceName === 'string' ) {
  854. window.localStorage.setItem('deviceName', details.deviceName);
  855. options.deviceName = details.deviceName;
  856. }
  857. getOptions(callback);
  858. };
  859. return {
  860. start: start,
  861. push: push,
  862. pull: pull,
  863. getOptions: getOptions,
  864. setOptions: setOptions
  865. };
  866. })();
  867. /******************************************************************************/
  868. /******************************************************************************/
  869. })();
  870. /******************************************************************************/