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.

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