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.

812 lines
26 KiB

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
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 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
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-2018 The uBlock Origin authors
  4. Copyright (C) 2017-present 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. // For background page
  18. 'use strict';
  19. /******************************************************************************/
  20. (function() {
  21. /******************************************************************************/
  22. var vAPI = self.vAPI = self.vAPI || {};
  23. var chrome = self.chrome;
  24. var manifest = chrome.runtime.getManifest();
  25. var noopFunc = function(){};
  26. // https://code.google.com/p/chromium/issues/detail?id=410868#c8
  27. var resetLastError = function() {
  28. void chrome.runtime.lastError;
  29. };
  30. /******************************************************************************/
  31. // https://github.com/gorhill/uMatrix/issues/234
  32. // https://developer.chrome.com/extensions/privacy#property-network
  33. chrome.privacy.network.networkPredictionEnabled.set({ value: false });
  34. /******************************************************************************/
  35. vAPI.app = {
  36. name: manifest.name,
  37. version: manifest.version
  38. };
  39. /******************************************************************************/
  40. vAPI.app.start = function() {
  41. };
  42. /******************************************************************************/
  43. vAPI.app.stop = function() {
  44. };
  45. /******************************************************************************/
  46. vAPI.app.restart = function() {
  47. chrome.runtime.reload();
  48. };
  49. /******************************************************************************/
  50. // chrome.storage.local.get(null, function(bin){ console.debug('%o', bin); });
  51. vAPI.storage = chrome.storage.local;
  52. vAPI.cacheStorage = chrome.storage.local;
  53. /******************************************************************************/
  54. vAPI.tabs = {};
  55. /******************************************************************************/
  56. vAPI.isBehindTheSceneTabId = function(tabId) {
  57. if ( typeof tabId === 'string' ) { debugger; }
  58. return tabId < 0;
  59. };
  60. vAPI.unsetTabId = 0;
  61. vAPI.noTabId = -1; // definitely not any existing tab
  62. vAPI.anyTabId = -2; // one of the existing tab
  63. /******************************************************************************/
  64. vAPI.tabs.registerListeners = function() {
  65. var onNavigationClient = this.onNavigation || noopFunc;
  66. var onUpdatedClient = this.onUpdated || noopFunc;
  67. var onClosedClient = this.onClosed || noopFunc;
  68. // https://developer.chrome.com/extensions/webNavigation
  69. // [onCreatedNavigationTarget ->]
  70. // onBeforeNavigate ->
  71. // onCommitted ->
  72. // onDOMContentLoaded ->
  73. // onCompleted
  74. // The chrome.webRequest.onBeforeRequest() won't be called for everything
  75. // else than `http`/`https`. Thus, in such case, we will bind the tab as
  76. // early as possible in order to increase the likelihood of a context
  77. // properly setup if network requests are fired from within the tab.
  78. // Example: Chromium + case #6 at
  79. // http://raymondhill.net/ublock/popup.html
  80. var reGoodForWebRequestAPI = /^https?:\/\//;
  81. var onCreatedNavigationTarget = function(details) {
  82. //console.debug('onCreatedNavigationTarget: tab id %d = "%s"', details.tabId, details.url);
  83. if ( reGoodForWebRequestAPI.test(details.url) ) { return; }
  84. onNavigationClient(details);
  85. };
  86. var onUpdated = function(tabId, changeInfo, tab) {
  87. onUpdatedClient(tabId, changeInfo, tab);
  88. };
  89. var onCommitted = function(details) {
  90. // Important: do not call client if not top frame.
  91. if ( details.frameId !== 0 ) {
  92. return;
  93. }
  94. onNavigationClient(details);
  95. //console.debug('onCommitted: tab id %d = "%s"', details.tabId, details.url);
  96. };
  97. var onClosed = function(tabId) {
  98. onClosedClient(tabId);
  99. };
  100. chrome.webNavigation.onCreatedNavigationTarget.addListener(onCreatedNavigationTarget);
  101. chrome.webNavigation.onCommitted.addListener(onCommitted);
  102. chrome.tabs.onUpdated.addListener(onUpdated);
  103. chrome.tabs.onRemoved.addListener(onClosed);
  104. };
  105. /******************************************************************************/
  106. // tabId: null, // active tab
  107. vAPI.tabs.get = function(tabId, callback) {
  108. var onTabReady = function(tab) {
  109. resetLastError();
  110. callback(tab);
  111. };
  112. if ( tabId !== null ) {
  113. chrome.tabs.get(tabId, onTabReady);
  114. return;
  115. }
  116. var onTabReceived = function(tabs) {
  117. resetLastError();
  118. var tab = null;
  119. if ( Array.isArray(tabs) && tabs.length !== 0 ) {
  120. tab = tabs[0];
  121. }
  122. callback(tab);
  123. };
  124. chrome.tabs.query({ active: true, currentWindow: true }, onTabReceived);
  125. };
  126. /******************************************************************************/
  127. // https://github.com/uBlockOrigin/uMatrix-issues/issues/9
  128. vAPI.tabs.getAll = function(callback) {
  129. chrome.tabs.query({}, callback);
  130. };
  131. /******************************************************************************/
  132. // properties of the details object:
  133. // url: 'URL', // the address that will be opened
  134. // tabId: 1, // the tab is used if set, instead of creating a new one
  135. // index: -1, // undefined: end of the list, -1: following tab, or after index
  136. // active: false, // opens the tab in background - true and undefined: foreground
  137. // select: true // if a tab is already opened with that url, then select it instead of opening a new one
  138. vAPI.tabs.open = function(details) {
  139. var targetURL = details.url;
  140. if ( typeof targetURL !== 'string' || targetURL === '' ) {
  141. return null;
  142. }
  143. // extension pages
  144. if ( /^[\w-]{2,}:/.test(targetURL) !== true ) {
  145. targetURL = vAPI.getURL(targetURL);
  146. }
  147. // dealing with Chrome's asynchronous API
  148. var wrapper = function() {
  149. if ( details.active === undefined ) {
  150. details.active = true;
  151. }
  152. var subWrapper = function() {
  153. var _details = {
  154. url: targetURL,
  155. active: !!details.active
  156. };
  157. // Opening a tab from incognito window won't focus the window
  158. // in which the tab was opened
  159. var focusWindow = function(tab) {
  160. if ( tab.active ) {
  161. chrome.windows.update(tab.windowId, { focused: true });
  162. }
  163. };
  164. if ( !details.tabId ) {
  165. if ( details.index !== undefined ) {
  166. _details.index = details.index;
  167. }
  168. chrome.tabs.create(_details, focusWindow);
  169. return;
  170. }
  171. // update doesn't accept index, must use move
  172. chrome.tabs.update(details.tabId, _details, function(tab) {
  173. // if the tab doesn't exist
  174. if ( vAPI.lastError() ) {
  175. chrome.tabs.create(_details, focusWindow);
  176. } else if ( details.index !== undefined ) {
  177. chrome.tabs.move(tab.id, {index: details.index});
  178. }
  179. });
  180. };
  181. // Open in a standalone window
  182. if ( details.popup === true ) {
  183. chrome.windows.create({ url: details.url, type: 'popup' });
  184. return;
  185. }
  186. if ( details.index !== -1 ) {
  187. subWrapper();
  188. return;
  189. }
  190. vAPI.tabs.get(null, function(tab) {
  191. if ( tab ) {
  192. details.index = tab.index + 1;
  193. } else {
  194. delete details.index;
  195. }
  196. subWrapper();
  197. });
  198. };
  199. if ( !details.select ) {
  200. wrapper();
  201. return;
  202. }
  203. chrome.tabs.query({ url: targetURL }, function(tabs) {
  204. resetLastError();
  205. var tab = Array.isArray(tabs) && tabs[0];
  206. if ( tab ) {
  207. chrome.tabs.update(tab.id, { active: true }, function(tab) {
  208. chrome.windows.update(tab.windowId, { focused: true });
  209. });
  210. } else {
  211. wrapper();
  212. }
  213. });
  214. };
  215. /******************************************************************************/
  216. // Replace the URL of a tab. Noop if the tab does not exist.
  217. vAPI.tabs.replace = function(tabId, url) {
  218. var targetURL = url;
  219. // extension pages
  220. if ( /^[\w-]{2,}:/.test(targetURL) !== true ) {
  221. targetURL = vAPI.getURL(targetURL);
  222. }
  223. if ( typeof tabId !== 'number' || tabId < 0 ) { return; }
  224. chrome.tabs.update(tabId, { url: targetURL }, resetLastError);
  225. };
  226. /******************************************************************************/
  227. vAPI.tabs.reload = function(tabId, bypassCache) {
  228. if ( typeof tabId !== 'number' || tabId < 0 ) { return; }
  229. chrome.tabs.reload(tabId, { bypassCache: bypassCache === true });
  230. };
  231. /******************************************************************************/
  232. // Must read: https://code.google.com/p/chromium/issues/detail?id=410868#c8
  233. // https://github.com/chrisaljoudi/uBlock/issues/19
  234. // https://github.com/chrisaljoudi/uBlock/issues/207
  235. // Since we may be called asynchronously, the tab id may not exist
  236. // anymore, so this ensures it does still exist.
  237. vAPI.setIcon = (function() {
  238. let onIconReady = function(tabId, badgeDetails) {
  239. if ( vAPI.lastError() ) { return; }
  240. if ( badgeDetails.text !== undefined ) {
  241. chrome.browserAction.setBadgeText({
  242. tabId: tabId,
  243. text: badgeDetails.text
  244. });
  245. }
  246. if ( badgeDetails.color !== undefined ) {
  247. chrome.browserAction.setBadgeBackgroundColor({
  248. tabId: tabId,
  249. color: badgeDetails.color
  250. });
  251. }
  252. };
  253. return function(tabId, iconDetails, badgeDetails) {
  254. if ( typeof tabId !== 'number' || tabId < 0 ) { return; }
  255. chrome.browserAction.setIcon(
  256. { tabId: tabId, path: iconDetails },
  257. function() { onIconReady(tabId, badgeDetails); }
  258. );
  259. };
  260. })();
  261. /******************************************************************************/
  262. /******************************************************************************/
  263. vAPI.messaging = {
  264. ports: new Map(),
  265. listeners: {},
  266. defaultHandler: null,
  267. NOOPFUNC: noopFunc,
  268. UNHANDLED: 'vAPI.messaging.notHandled'
  269. };
  270. /******************************************************************************/
  271. vAPI.messaging.listen = function(listenerName, callback) {
  272. this.listeners[listenerName] = callback;
  273. };
  274. /******************************************************************************/
  275. vAPI.messaging.onPortMessage = (function() {
  276. var messaging = vAPI.messaging;
  277. // Use a wrapper to avoid closure and to allow reuse.
  278. var CallbackWrapper = function(port, request) {
  279. this.callback = this.proxy.bind(this); // bind once
  280. this.init(port, request);
  281. };
  282. CallbackWrapper.prototype = {
  283. init: function(port, request) {
  284. this.port = port;
  285. this.request = request;
  286. return this;
  287. },
  288. proxy: function(response) {
  289. // https://github.com/chrisaljoudi/uBlock/issues/383
  290. if ( messaging.ports.has(this.port.name) ) {
  291. this.port.postMessage({
  292. auxProcessId: this.request.auxProcessId,
  293. channelName: this.request.channelName,
  294. msg: response !== undefined ? response : null
  295. });
  296. }
  297. // Mark for reuse
  298. this.port = this.request = null;
  299. callbackWrapperJunkyard.push(this);
  300. }
  301. };
  302. var callbackWrapperJunkyard = [];
  303. var callbackWrapperFactory = function(port, request) {
  304. var wrapper = callbackWrapperJunkyard.pop();
  305. if ( wrapper ) {
  306. return wrapper.init(port, request);
  307. }
  308. return new CallbackWrapper(port, request);
  309. };
  310. // https://bugzilla.mozilla.org/show_bug.cgi?id=1392067
  311. // Workaround: manually remove ports matching removed tab.
  312. chrome.tabs.onRemoved.addListener(function(tabId) {
  313. for ( var port of messaging.ports.values() ) {
  314. var tab = port.sender && port.sender.tab;
  315. if ( !tab ) { continue; }
  316. if ( tab.id === tabId ) {
  317. vAPI.messaging.onPortDisconnect(port);
  318. }
  319. }
  320. });
  321. return function(request, port) {
  322. // prepare response
  323. var callback = this.NOOPFUNC;
  324. if ( request.auxProcessId !== undefined ) {
  325. callback = callbackWrapperFactory(port, request).callback;
  326. }
  327. // Auxiliary process to main process: specific handler
  328. var r = this.UNHANDLED,
  329. listener = this.listeners[request.channelName];
  330. if ( typeof listener === 'function' ) {
  331. r = listener(request.msg, port.sender, callback);
  332. }
  333. if ( r !== this.UNHANDLED ) { return; }
  334. // Auxiliary process to main process: default handler
  335. r = this.defaultHandler(request.msg, port.sender, callback);
  336. if ( r !== this.UNHANDLED ) { return; }
  337. // Auxiliary process to main process: no handler
  338. console.error(
  339. 'vAPI.messaging.onPortMessage > unhandled request: %o',
  340. request
  341. );
  342. // Need to callback anyways in case caller expected an answer, or
  343. // else there is a memory leak on caller's side
  344. callback();
  345. }.bind(vAPI.messaging);
  346. })();
  347. /******************************************************************************/
  348. vAPI.messaging.onPortDisconnect = function(port) {
  349. port.onDisconnect.removeListener(this.onPortDisconnect);
  350. port.onMessage.removeListener(this.onPortMessage);
  351. this.ports.delete(port.name);
  352. }.bind(vAPI.messaging);
  353. /******************************************************************************/
  354. vAPI.messaging.onPortConnect = function(port) {
  355. port.onDisconnect.addListener(this.onPortDisconnect);
  356. port.onMessage.addListener(this.onPortMessage);
  357. this.ports.set(port.name, port);
  358. }.bind(vAPI.messaging);
  359. /******************************************************************************/
  360. vAPI.messaging.setup = function(defaultHandler) {
  361. if ( this.defaultHandler !== null ) { return; }
  362. if ( typeof defaultHandler !== 'function' ) {
  363. defaultHandler = function(){
  364. return vAPI.messaging.UNHANDLED;
  365. };
  366. }
  367. this.defaultHandler = defaultHandler;
  368. chrome.runtime.onConnect.addListener(this.onPortConnect);
  369. };
  370. /******************************************************************************/
  371. vAPI.messaging.broadcast = function(message) {
  372. var messageWrapper = {
  373. broadcast: true,
  374. msg: message
  375. };
  376. for ( var port of this.ports.values() ) {
  377. port.postMessage(messageWrapper);
  378. }
  379. };
  380. /******************************************************************************/
  381. /******************************************************************************/
  382. vAPI.net = {
  383. listenerMap: new WeakMap(),
  384. // legacy Chromium understands only these network request types.
  385. validTypes: (function() {
  386. let types = new Set([
  387. 'main_frame',
  388. 'sub_frame',
  389. 'stylesheet',
  390. 'script',
  391. 'image',
  392. 'object',
  393. 'xmlhttprequest',
  394. 'other'
  395. ]);
  396. let wrrt = browser.webRequest.ResourceType;
  397. if ( wrrt instanceof Object ) {
  398. for ( let typeKey in wrrt ) {
  399. if ( wrrt.hasOwnProperty(typeKey) ) {
  400. types.add(wrrt[typeKey]);
  401. }
  402. }
  403. }
  404. return types;
  405. })(),
  406. denormalizeFilters: null,
  407. normalizeDetails: null,
  408. addListener: function(which, clientListener, filters, options) {
  409. if ( typeof this.denormalizeFilters === 'function' ) {
  410. filters = this.denormalizeFilters(filters);
  411. }
  412. let actualListener;
  413. if ( typeof this.normalizeDetails === 'function' ) {
  414. actualListener = function(details) {
  415. vAPI.net.normalizeDetails(details);
  416. return clientListener(details);
  417. };
  418. this.listenerMap.set(clientListener, actualListener);
  419. }
  420. browser.webRequest[which].addListener(
  421. actualListener || clientListener,
  422. filters,
  423. options
  424. );
  425. },
  426. removeListener: function(which, clientListener) {
  427. let actualListener = this.listenerMap.get(clientListener);
  428. if ( actualListener !== undefined ) {
  429. this.listenerMap.delete(clientListener);
  430. }
  431. browser.webRequest[which].removeListener(
  432. actualListener || clientListener
  433. );
  434. },
  435. };
  436. /******************************************************************************/
  437. /******************************************************************************/
  438. vAPI.lastError = function() {
  439. return chrome.runtime.lastError;
  440. };
  441. /******************************************************************************/
  442. /******************************************************************************/
  443. vAPI.browserData = {};
  444. /******************************************************************************/
  445. // https://developer.chrome.com/extensions/browsingData
  446. vAPI.browserData.clearCache = function(callback) {
  447. chrome.browsingData.removeCache({ since: 0 }, callback);
  448. };
  449. /******************************************************************************/
  450. /******************************************************************************/
  451. // https://developer.chrome.com/extensions/cookies
  452. vAPI.cookies = {};
  453. /******************************************************************************/
  454. vAPI.cookies.start = function() {
  455. var reallyRemoved = {
  456. 'evicted': true,
  457. 'expired': true,
  458. 'explicit': true
  459. };
  460. var onChanged = function(changeInfo) {
  461. if ( changeInfo.removed ) {
  462. if ( reallyRemoved[changeInfo.cause] && typeof this.onRemoved === 'function' ) {
  463. this.onRemoved(changeInfo.cookie);
  464. }
  465. return;
  466. }
  467. if ( typeof this.onChanged === 'function' ) {
  468. this.onChanged(changeInfo.cookie);
  469. }
  470. };
  471. chrome.cookies.onChanged.addListener(onChanged.bind(this));
  472. };
  473. /******************************************************************************/
  474. vAPI.cookies.getAll = function(callback) {
  475. chrome.cookies.getAll({}, callback);
  476. };
  477. /******************************************************************************/
  478. vAPI.cookies.remove = function(details, callback) {
  479. chrome.cookies.remove(details, callback || noopFunc);
  480. };
  481. /******************************************************************************/
  482. /******************************************************************************/
  483. vAPI.cloud = (function() {
  484. // Not all platforms support `chrome.storage.sync`.
  485. if ( chrome.storage.sync instanceof Object === false ) {
  486. return;
  487. }
  488. var chunkCountPerFetch = 16; // Must be a power of 2
  489. // Mind chrome.storage.sync.MAX_ITEMS (512 at time of writing)
  490. var maxChunkCountPerItem = Math.floor(512 * 0.75) & ~(chunkCountPerFetch - 1);
  491. // Mind chrome.storage.sync.QUOTA_BYTES_PER_ITEM (8192 at time of writing)
  492. // https://github.com/gorhill/uBlock/issues/3006
  493. // For Firefox, we will use a lower ratio to allow for more overhead for
  494. // the infrastructure. Unfortunately this leads to less usable space for
  495. // actual data, but all of this is provided for free by browser vendors,
  496. // so we need to accept and deal with these limitations.
  497. var evalMaxChunkSize = function() {
  498. return Math.floor(
  499. (chrome.storage.sync.QUOTA_BYTES_PER_ITEM || 8192) *
  500. (vAPI.webextFlavor.soup.has('firefox') ? 0.6 : 0.75)
  501. );
  502. };
  503. var maxChunkSize = evalMaxChunkSize();
  504. // The real actual webextFlavor value may not be set in stone, so listen
  505. // for possible future changes.
  506. window.addEventListener('webextFlavor', function() {
  507. maxChunkSize = evalMaxChunkSize();
  508. }, { once: true });
  509. // Mind chrome.storage.sync.QUOTA_BYTES (128 kB at time of writing)
  510. // Firefox:
  511. // https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/storage/sync
  512. // > You can store up to 100KB of data using this API/
  513. var maxStorageSize = chrome.storage.sync.QUOTA_BYTES || 102400;
  514. var options = {
  515. defaultDeviceName: window.navigator.platform,
  516. deviceName: vAPI.localStorage.getItem('deviceName') || ''
  517. };
  518. // This is used to find out a rough count of how many chunks exists:
  519. // We "poll" at specific index in order to get a rough idea of how
  520. // large is the stored string.
  521. // This allows reading a single item with only 2 sync operations -- a
  522. // good thing given chrome.storage.sync.MAX_WRITE_OPERATIONS_PER_MINUTE
  523. // and chrome.storage.sync.MAX_WRITE_OPERATIONS_PER_HOUR.
  524. var getCoarseChunkCount = function(dataKey, callback) {
  525. let bin = {};
  526. for ( let i = 0; i < maxChunkCountPerItem; i += 16 ) {
  527. bin[dataKey + i.toString()] = '';
  528. }
  529. chrome.storage.sync.get(bin, function(bin) {
  530. if ( chrome.runtime.lastError ) {
  531. callback(0, chrome.runtime.lastError.message);
  532. return;
  533. }
  534. var chunkCount = 0;
  535. for ( let i = 0; i < maxChunkCountPerItem; i += 16 ) {
  536. if ( bin[dataKey + i.toString()] === '' ) { break; }
  537. chunkCount = i + 16;
  538. }
  539. callback(chunkCount);
  540. });
  541. };
  542. var deleteChunks = function(dataKey, start) {
  543. var keys = [];
  544. // No point in deleting more than:
  545. // - The max number of chunks per item
  546. // - The max number of chunks per storage limit
  547. var n = Math.min(
  548. maxChunkCountPerItem,
  549. Math.ceil(maxStorageSize / maxChunkSize)
  550. );
  551. for ( var i = start; i < n; i++ ) {
  552. keys.push(dataKey + i.toString());
  553. }
  554. chrome.storage.sync.remove(keys);
  555. };
  556. var start = function(/* dataKeys */) {
  557. };
  558. var push = function(dataKey, data, callback) {
  559. var bin = {
  560. 'source': options.deviceName || options.defaultDeviceName,
  561. 'tstamp': Date.now(),
  562. 'data': data,
  563. 'size': 0
  564. };
  565. bin.size = JSON.stringify(bin).length;
  566. var item = JSON.stringify(bin);
  567. // Chunkify taking into account QUOTA_BYTES_PER_ITEM:
  568. // https://developer.chrome.com/extensions/storage#property-sync
  569. // "The maximum size (in bytes) of each individual item in sync
  570. // "storage, as measured by the JSON stringification of its value
  571. // "plus its key length."
  572. bin = {};
  573. var chunkCount = Math.ceil(item.length / maxChunkSize);
  574. for ( var i = 0; i < chunkCount; i++ ) {
  575. bin[dataKey + i.toString()] = item.substr(i * maxChunkSize, maxChunkSize);
  576. }
  577. bin[dataKey + i.toString()] = ''; // Sentinel
  578. chrome.storage.sync.set(bin, function() {
  579. var errorStr;
  580. if ( chrome.runtime.lastError ) {
  581. errorStr = chrome.runtime.lastError.message;
  582. // https://github.com/gorhill/uBlock/issues/3006#issuecomment-332597677
  583. // - Delete all that was pushed in case of failure.
  584. chunkCount = 0;
  585. }
  586. callback(errorStr);
  587. // Remove potentially unused trailing chunks
  588. deleteChunks(dataKey, chunkCount);
  589. });
  590. };
  591. var pull = function(dataKey, callback) {
  592. var assembleChunks = function(bin) {
  593. if ( chrome.runtime.lastError ) {
  594. callback(null, chrome.runtime.lastError.message);
  595. return;
  596. }
  597. // Assemble chunks into a single string.
  598. let json = [], jsonSlice;
  599. let i = 0;
  600. for (;;) {
  601. jsonSlice = bin[dataKey + i.toString()];
  602. if ( jsonSlice === '' || jsonSlice === undefined ) { break; }
  603. json.push(jsonSlice);
  604. i += 1;
  605. }
  606. let entry = null;
  607. try {
  608. entry = JSON.parse(json.join(''));
  609. } catch(ex) {
  610. }
  611. callback(entry);
  612. };
  613. var fetchChunks = function(coarseCount, errorStr) {
  614. if ( coarseCount === 0 || typeof errorStr === 'string' ) {
  615. callback(null, errorStr);
  616. return;
  617. }
  618. var bin = {};
  619. for ( var i = 0; i < coarseCount; i++ ) {
  620. bin[dataKey + i.toString()] = '';
  621. }
  622. chrome.storage.sync.get(bin, assembleChunks);
  623. };
  624. getCoarseChunkCount(dataKey, fetchChunks);
  625. };
  626. var getOptions = function(callback) {
  627. if ( typeof callback !== 'function' ) {
  628. return;
  629. }
  630. callback(options);
  631. };
  632. var setOptions = function(details, callback) {
  633. if ( typeof details !== 'object' || details === null ) {
  634. return;
  635. }
  636. if ( typeof details.deviceName === 'string' ) {
  637. vAPI.localStorage.setItem('deviceName', details.deviceName);
  638. options.deviceName = details.deviceName;
  639. }
  640. getOptions(callback);
  641. };
  642. return {
  643. start: start,
  644. push: push,
  645. pull: pull,
  646. getOptions: getOptions,
  647. setOptions: setOptions
  648. };
  649. })();
  650. /******************************************************************************/
  651. /******************************************************************************/
  652. })();
  653. /******************************************************************************/