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.

910 lines
28 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
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
  1. /*******************************************************************************
  2. µMatrix - a browser extension to black/white list requests.
  3. Copyright (C) 2013-2015 Raymond Hill
  4. This program is free software: you can redistribute it and/or modify
  5. it under the terms of the GNU General Public License as published by
  6. the Free Software Foundation, either version 3 of the License, or
  7. (at your option) any later version.
  8. This program is distributed in the hope that it will be useful,
  9. but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. GNU General Public License for more details.
  12. You should have received a copy of the GNU General Public License
  13. along with this program. If not, see {http://www.gnu.org/licenses/}.
  14. Home: https://github.com/gorhill/uMatrix
  15. */
  16. 'use strict';
  17. /******************************************************************************/
  18. µMatrix.assets = (function() {
  19. /******************************************************************************/
  20. var reIsExternalPath = /^(?:[a-z-]+):\/\//,
  21. errorCantConnectTo = vAPI.i18n('errorCantConnectTo'),
  22. noopfunc = function(){};
  23. var api = {
  24. };
  25. /******************************************************************************/
  26. var observers = [];
  27. api.addObserver = function(observer) {
  28. if ( observers.indexOf(observer) === -1 ) {
  29. observers.push(observer);
  30. }
  31. };
  32. api.removeObserver = function(observer) {
  33. var pos;
  34. while ( (pos = observers.indexOf(observer)) !== -1 ) {
  35. observers.splice(pos, 1);
  36. }
  37. };
  38. var fireNotification = function(topic, details) {
  39. var result;
  40. for ( var i = 0; i < observers.length; i++ ) {
  41. if ( observers[i](topic, details) === false ) {
  42. result = false;
  43. }
  44. }
  45. return result;
  46. };
  47. /******************************************************************************/
  48. api.fetchText = function(url, onLoad, onError) {
  49. var actualUrl = reIsExternalPath.test(url) ? url : vAPI.getURL(url);
  50. if ( typeof onError !== 'function' ) {
  51. onError = onLoad;
  52. }
  53. // https://github.com/gorhill/uMatrix/issues/15
  54. var onResponseReceived = function() {
  55. this.onload = this.onerror = this.ontimeout = null;
  56. // xhr for local files gives status 0, but actually succeeds
  57. var details = {
  58. url: url,
  59. content: '',
  60. statusCode: this.status || 200,
  61. statusText: this.statusText || ''
  62. };
  63. if ( details.statusCode < 200 || details.statusCode >= 300 ) {
  64. return onError.call(null, details);
  65. }
  66. // consider an empty result to be an error
  67. if ( stringIsNotEmpty(this.responseText) === false ) {
  68. return onError.call(null, details);
  69. }
  70. // we never download anything else than plain text: discard if response
  71. // appears to be a HTML document: could happen when server serves
  72. // some kind of error page I suppose
  73. var text = this.responseText.trim();
  74. if ( text.startsWith('<') && text.endsWith('>') ) {
  75. return onError.call(null, details);
  76. }
  77. details.content = this.responseText;
  78. return onLoad.call(null, details);
  79. };
  80. var onErrorReceived = function() {
  81. this.onload = this.onerror = this.ontimeout = null;
  82. µMatrix.logger.writeOne('', 'error', errorCantConnectTo.replace('{{msg}}', actualUrl));
  83. onError.call(null, { url: url, content: '' });
  84. };
  85. // Be ready for thrown exceptions:
  86. // I am pretty sure it used to work, but now using a URL such as
  87. // `file:///` on Chromium 40 results in an exception being thrown.
  88. var xhr = new XMLHttpRequest();
  89. try {
  90. xhr.open('get', actualUrl, true);
  91. xhr.timeout = 30000;
  92. xhr.onload = onResponseReceived;
  93. xhr.onerror = onErrorReceived;
  94. xhr.ontimeout = onErrorReceived;
  95. xhr.responseType = 'text';
  96. xhr.send();
  97. } catch (e) {
  98. onErrorReceived.call(xhr);
  99. }
  100. };
  101. /*******************************************************************************
  102. TODO(seamless migration):
  103. This block of code will be removed when I am confident all users have
  104. moved to a version of uBO which does not require the old way of caching
  105. assets.
  106. api.listKeyAliases: a map of old asset keys to new asset keys.
  107. migrate(): to seamlessly migrate the old cache manager to the new one:
  108. - attempt to preserve and move content of cached assets to new locations;
  109. - removes all traces of now obsolete cache manager entries in cacheStorage.
  110. This code will typically execute only once, when the newer version of uBO
  111. is first installed and executed.
  112. **/
  113. api.listKeyAliases = {
  114. "assets/thirdparties/publicsuffix.org/list/effective_tld_names.dat": "public_suffix_list.dat",
  115. "assets/thirdparties/hosts-file.net/ad-servers": "hphosts",
  116. "assets/thirdparties/www.malwaredomainlist.com/hostslist/hosts.txt": "malware-0",
  117. "assets/thirdparties/mirror1.malwaredomains.com/files/justdomains": "malware-1",
  118. "assets/thirdparties/pgl.yoyo.org/as/serverlist": "plowe-0",
  119. "assets/thirdparties/someonewhocares.org/hosts/hosts": "dpollock-0",
  120. "assets/thirdparties/winhelp2002.mvps.org/hosts.txt": "mvps-0"
  121. };
  122. var migrate = function(callback) {
  123. var entries,
  124. moveCount = 0,
  125. toRemove = [];
  126. var countdown = function(change) {
  127. moveCount -= (change || 0);
  128. if ( moveCount !== 0 ) { return; }
  129. vAPI.cacheStorage.remove(toRemove);
  130. saveAssetCacheRegistry();
  131. callback();
  132. };
  133. var onContentRead = function(oldKey, newKey, bin) {
  134. var content = bin && bin['cached_asset_content://' + oldKey] || undefined;
  135. if ( content ) {
  136. assetCacheRegistry[newKey] = {
  137. readTime: Date.now(),
  138. writeTime: entries[oldKey]
  139. };
  140. if ( reIsExternalPath.test(oldKey) ) {
  141. assetCacheRegistry[newKey].remoteURL = oldKey;
  142. }
  143. bin = {};
  144. bin['cache/' + newKey] = content;
  145. vAPI.cacheStorage.set(bin);
  146. }
  147. countdown(1);
  148. };
  149. var onEntries = function(bin) {
  150. entries = bin && bin.cached_asset_entries;
  151. if ( !entries ) { return callback(); }
  152. if ( bin && bin.assetCacheRegistry ) {
  153. assetCacheRegistry = bin.assetCacheRegistry;
  154. }
  155. var aliases = api.listKeyAliases;
  156. for ( var oldKey in entries ) {
  157. var newKey = aliases[oldKey];
  158. if ( !newKey && /^https?:\/\//.test(oldKey) ) {
  159. newKey = oldKey;
  160. }
  161. if ( newKey ) {
  162. vAPI.cacheStorage.get(
  163. 'cached_asset_content://' + oldKey,
  164. onContentRead.bind(null, oldKey, newKey)
  165. );
  166. moveCount += 1;
  167. }
  168. toRemove.push('cached_asset_content://' + oldKey);
  169. }
  170. toRemove.push('cached_asset_entries', 'extensionLastVersion');
  171. countdown();
  172. };
  173. vAPI.cacheStorage.get(
  174. [ 'cached_asset_entries', 'assetCacheRegistry' ],
  175. onEntries
  176. );
  177. };
  178. /*******************************************************************************
  179. The purpose of the asset source registry is to keep key detail information
  180. about an asset:
  181. - Where to load it from: this may consist of one or more URLs, either local
  182. or remote.
  183. - After how many days an asset should be deemed obsolete -- i.e. in need of
  184. an update.
  185. - The origin and type of an asset.
  186. - The last time an asset was registered.
  187. **/
  188. var assetSourceRegistryStatus,
  189. assetSourceRegistry = Object.create(null);
  190. var registerAssetSource = function(assetKey, dict) {
  191. var entry = assetSourceRegistry[assetKey] || {};
  192. for ( var prop in dict ) {
  193. if ( dict.hasOwnProperty(prop) === false ) { continue; }
  194. if ( dict[prop] === undefined ) {
  195. delete entry[prop];
  196. } else {
  197. entry[prop] = dict[prop];
  198. }
  199. }
  200. var contentURL = dict.contentURL;
  201. if ( contentURL !== undefined ) {
  202. if ( typeof contentURL === 'string' ) {
  203. contentURL = entry.contentURL = [ contentURL ];
  204. } else if ( Array.isArray(contentURL) === false ) {
  205. contentURL = entry.contentURL = [];
  206. }
  207. var remoteURLCount = 0;
  208. for ( var i = 0; i < contentURL.length; i++ ) {
  209. if ( reIsExternalPath.test(contentURL[i]) ) {
  210. remoteURLCount += 1;
  211. }
  212. }
  213. entry.hasLocalURL = remoteURLCount !== contentURL.length;
  214. entry.hasRemoteURL = remoteURLCount !== 0;
  215. } else if ( entry.contentURL === undefined ) {
  216. entry.contentURL = [];
  217. }
  218. if ( typeof entry.updateAfter !== 'number' ) {
  219. entry.updateAfter = 13;
  220. }
  221. if ( entry.submitter ) {
  222. entry.submitTime = Date.now(); // To detect stale entries
  223. }
  224. assetSourceRegistry[assetKey] = entry;
  225. };
  226. var unregisterAssetSource = function(assetKey) {
  227. assetCacheRemove(assetKey);
  228. delete assetSourceRegistry[assetKey];
  229. };
  230. var saveAssetSourceRegistry = (function() {
  231. var timer;
  232. var save = function() {
  233. timer = undefined;
  234. vAPI.cacheStorage.set({ assetSourceRegistry: assetSourceRegistry });
  235. };
  236. return function(lazily) {
  237. if ( timer !== undefined ) {
  238. clearTimeout(timer);
  239. }
  240. if ( lazily ) {
  241. timer = vAPI.setTimeout(save, 500);
  242. } else {
  243. save();
  244. }
  245. };
  246. })();
  247. var updateAssetSourceRegistry = function(json, silent) {
  248. var newDict;
  249. try {
  250. newDict = JSON.parse(json);
  251. } catch (ex) {
  252. }
  253. if ( newDict instanceof Object === false ) { return; }
  254. var oldDict = assetSourceRegistry,
  255. assetKey;
  256. // Remove obsolete entries (only those which were built-in).
  257. for ( assetKey in oldDict ) {
  258. if (
  259. newDict[assetKey] === undefined &&
  260. oldDict[assetKey].submitter === undefined
  261. ) {
  262. unregisterAssetSource(assetKey);
  263. }
  264. }
  265. // Add/update existing entries. Notify of new asset sources.
  266. for ( assetKey in newDict ) {
  267. if ( oldDict[assetKey] === undefined && !silent ) {
  268. fireNotification(
  269. 'builtin-asset-source-added',
  270. { assetKey: assetKey, entry: newDict[assetKey] }
  271. );
  272. }
  273. registerAssetSource(assetKey, newDict[assetKey]);
  274. }
  275. saveAssetSourceRegistry();
  276. };
  277. var getAssetSourceRegistry = function(callback) {
  278. // Already loaded.
  279. if ( assetSourceRegistryStatus === 'ready' ) {
  280. callback(assetSourceRegistry);
  281. return;
  282. }
  283. // Being loaded.
  284. if ( Array.isArray(assetSourceRegistryStatus) ) {
  285. assetSourceRegistryStatus.push(callback);
  286. return;
  287. }
  288. // Not loaded: load it.
  289. assetSourceRegistryStatus = [ callback ];
  290. var registryReady = function() {
  291. var callers = assetSourceRegistryStatus;
  292. assetSourceRegistryStatus = 'ready';
  293. var fn;
  294. while ( (fn = callers.shift()) ) {
  295. fn(assetSourceRegistry);
  296. }
  297. };
  298. // First-install case.
  299. var createRegistry = function() {
  300. api.fetchText(
  301. µMatrix.assetsBootstrapLocation || 'assets/assets.json',
  302. function(details) {
  303. updateAssetSourceRegistry(details.content, true);
  304. registryReady();
  305. }
  306. );
  307. };
  308. vAPI.cacheStorage.get('assetSourceRegistry', function(bin) {
  309. if ( !bin || !bin.assetSourceRegistry ) {
  310. createRegistry();
  311. return;
  312. }
  313. assetSourceRegistry = bin.assetSourceRegistry;
  314. registryReady();
  315. });
  316. };
  317. api.registerAssetSource = function(assetKey, details) {
  318. getAssetSourceRegistry(function() {
  319. registerAssetSource(assetKey, details);
  320. saveAssetSourceRegistry(true);
  321. });
  322. };
  323. api.unregisterAssetSource = function(assetKey) {
  324. getAssetSourceRegistry(function() {
  325. unregisterAssetSource(assetKey);
  326. saveAssetSourceRegistry(true);
  327. });
  328. };
  329. /*******************************************************************************
  330. The purpose of the asset cache registry is to keep track of all assets
  331. which have been persisted into the local cache.
  332. **/
  333. var assetCacheRegistryStatus,
  334. assetCacheRegistryStartTime = Date.now(),
  335. assetCacheRegistry = {};
  336. var getAssetCacheRegistry = function(callback) {
  337. // Already loaded.
  338. if ( assetCacheRegistryStatus === 'ready' ) {
  339. callback(assetCacheRegistry);
  340. return;
  341. }
  342. // Being loaded.
  343. if ( Array.isArray(assetCacheRegistryStatus) ) {
  344. assetCacheRegistryStatus.push(callback);
  345. return;
  346. }
  347. // Not loaded: load it.
  348. assetCacheRegistryStatus = [ callback ];
  349. var registryReady = function() {
  350. var callers = assetCacheRegistryStatus;
  351. assetCacheRegistryStatus = 'ready';
  352. var fn;
  353. while ( (fn = callers.shift()) ) {
  354. fn(assetCacheRegistry);
  355. }
  356. };
  357. var migrationDone = function() {
  358. vAPI.cacheStorage.get('assetCacheRegistry', function(bin) {
  359. if ( bin && bin.assetCacheRegistry ) {
  360. assetCacheRegistry = bin.assetCacheRegistry;
  361. }
  362. registryReady();
  363. });
  364. };
  365. migrate(migrationDone);
  366. };
  367. var saveAssetCacheRegistry = (function() {
  368. var timer;
  369. var save = function() {
  370. timer = undefined;
  371. vAPI.cacheStorage.set({ assetCacheRegistry: assetCacheRegistry });
  372. };
  373. return function(lazily) {
  374. if ( timer !== undefined ) { clearTimeout(timer); }
  375. if ( lazily ) {
  376. timer = vAPI.setTimeout(save, 500);
  377. } else {
  378. save();
  379. }
  380. };
  381. })();
  382. var assetCacheRead = function(assetKey, callback) {
  383. var internalKey = 'cache/' + assetKey;
  384. var reportBack = function(content, err) {
  385. var details = { assetKey: assetKey, content: content };
  386. if ( err ) { details.error = err; }
  387. callback(details);
  388. };
  389. var onAssetRead = function(bin) {
  390. if ( !bin || !bin[internalKey] ) {
  391. return reportBack('', 'E_NOTFOUND');
  392. }
  393. var entry = assetCacheRegistry[assetKey];
  394. if ( entry === undefined ) {
  395. return reportBack('', 'E_NOTFOUND');
  396. }
  397. entry.readTime = Date.now();
  398. saveAssetCacheRegistry(true);
  399. reportBack(bin[internalKey]);
  400. };
  401. var onReady = function() {
  402. vAPI.cacheStorage.get(internalKey, onAssetRead);
  403. };
  404. getAssetCacheRegistry(onReady);
  405. };
  406. var assetCacheWrite = function(assetKey, details, callback) {
  407. var internalKey = 'cache/' + assetKey;
  408. var content = '';
  409. if ( typeof details === 'string' ) {
  410. content = details;
  411. } else if ( details instanceof Object ) {
  412. content = details.content || '';
  413. }
  414. if ( content === '' ) {
  415. return assetCacheRemove(assetKey, callback);
  416. }
  417. var reportBack = function(content) {
  418. var details = { assetKey: assetKey, content: content };
  419. if ( typeof callback === 'function' ) {
  420. callback(details);
  421. }
  422. fireNotification('after-asset-updated', details);
  423. };
  424. var onReady = function() {
  425. var entry = assetCacheRegistry[assetKey];
  426. if ( entry === undefined ) {
  427. entry = assetCacheRegistry[assetKey] = {};
  428. }
  429. entry.writeTime = entry.readTime = Date.now();
  430. if ( details instanceof Object && typeof details.url === 'string' ) {
  431. entry.remoteURL = details.url;
  432. }
  433. var bin = { assetCacheRegistry: assetCacheRegistry };
  434. bin[internalKey] = content;
  435. vAPI.cacheStorage.set(bin);
  436. reportBack(content);
  437. };
  438. getAssetCacheRegistry(onReady);
  439. };
  440. var assetCacheRemove = function(pattern, callback) {
  441. var onReady = function() {
  442. var cacheDict = assetCacheRegistry,
  443. removedEntries = [],
  444. removedContent = [];
  445. for ( var assetKey in cacheDict ) {
  446. if ( pattern instanceof RegExp && !pattern.test(assetKey) ) {
  447. continue;
  448. }
  449. if ( typeof pattern === 'string' && assetKey !== pattern ) {
  450. continue;
  451. }
  452. removedEntries.push(assetKey);
  453. removedContent.push('cache/' + assetKey);
  454. delete cacheDict[assetKey];
  455. }
  456. if ( removedContent.length !== 0 ) {
  457. vAPI.cacheStorage.remove(removedContent);
  458. var bin = { assetCacheRegistry: assetCacheRegistry };
  459. vAPI.cacheStorage.set(bin);
  460. }
  461. if ( typeof callback === 'function' ) {
  462. callback();
  463. }
  464. for ( var i = 0; i < removedEntries.length; i++ ) {
  465. fireNotification('after-asset-updated', { assetKey: removedEntries[i] });
  466. }
  467. };
  468. getAssetCacheRegistry(onReady);
  469. };
  470. var assetCacheMarkAsDirty = function(pattern, exclude, callback) {
  471. var onReady = function() {
  472. var cacheDict = assetCacheRegistry,
  473. cacheEntry,
  474. mustSave = false;
  475. for ( var assetKey in cacheDict ) {
  476. if ( pattern instanceof RegExp ) {
  477. if ( pattern.test(assetKey) === false ) { continue; }
  478. } else if ( typeof pattern === 'string' ) {
  479. if ( assetKey !== pattern ) { continue; }
  480. } else if ( Array.isArray(pattern) ) {
  481. if ( pattern.indexOf(assetKey) === -1 ) { continue; }
  482. }
  483. if ( exclude instanceof RegExp ) {
  484. if ( exclude.test(assetKey) ) { continue; }
  485. } else if ( typeof exclude === 'string' ) {
  486. if ( assetKey === exclude ) { continue; }
  487. } else if ( Array.isArray(exclude) ) {
  488. if ( exclude.indexOf(assetKey) !== -1 ) { continue; }
  489. }
  490. cacheEntry = cacheDict[assetKey];
  491. if ( !cacheEntry.writeTime ) { continue; }
  492. cacheDict[assetKey].writeTime = 0;
  493. mustSave = true;
  494. }
  495. if ( mustSave ) {
  496. var bin = { assetCacheRegistry: assetCacheRegistry };
  497. vAPI.cacheStorage.set(bin);
  498. }
  499. if ( typeof callback === 'function' ) {
  500. callback();
  501. }
  502. };
  503. if ( typeof exclude === 'function' ) {
  504. callback = exclude;
  505. exclude = undefined;
  506. }
  507. getAssetCacheRegistry(onReady);
  508. };
  509. /******************************************************************************/
  510. var stringIsNotEmpty = function(s) {
  511. return typeof s === 'string' && s !== '';
  512. };
  513. /******************************************************************************/
  514. api.get = function(assetKey, options, callback) {
  515. if ( typeof options === 'function' ) {
  516. callback = options;
  517. options = {};
  518. } else if ( typeof callback !== 'function' ) {
  519. callback = noopfunc;
  520. }
  521. var assetDetails = {},
  522. contentURLs,
  523. contentURL;
  524. var reportBack = function(content, err) {
  525. var details = { assetKey: assetKey, content: content };
  526. if ( err ) {
  527. details.error = assetDetails.lastError = err;
  528. } else {
  529. assetDetails.lastError = undefined;
  530. }
  531. callback(details);
  532. };
  533. var onContentNotLoaded = function() {
  534. var isExternal;
  535. while ( (contentURL = contentURLs.shift()) ) {
  536. isExternal = reIsExternalPath.test(contentURL);
  537. if ( isExternal === false || assetDetails.hasLocalURL !== true ) {
  538. break;
  539. }
  540. }
  541. if ( !contentURL ) {
  542. return reportBack('', 'E_NOTFOUND');
  543. }
  544. api.fetchText(contentURL, onContentLoaded, onContentNotLoaded);
  545. };
  546. var onContentLoaded = function(details) {
  547. if ( stringIsNotEmpty(details.content) === false ) {
  548. onContentNotLoaded();
  549. return;
  550. }
  551. if ( reIsExternalPath.test(contentURL) && options.dontCache !== true ) {
  552. assetCacheWrite(assetKey, {
  553. content: details.content,
  554. url: contentURL
  555. });
  556. }
  557. reportBack(details.content);
  558. };
  559. var onCachedContentLoaded = function(details) {
  560. if ( details.content !== '' ) {
  561. return reportBack(details.content);
  562. }
  563. getAssetSourceRegistry(function(registry) {
  564. assetDetails = registry[assetKey] || {};
  565. if ( typeof assetDetails.contentURL === 'string' ) {
  566. contentURLs = [ assetDetails.contentURL ];
  567. } else if ( Array.isArray(assetDetails.contentURL) ) {
  568. contentURLs = assetDetails.contentURL.slice(0);
  569. } else {
  570. contentURLs = [];
  571. }
  572. onContentNotLoaded();
  573. });
  574. };
  575. assetCacheRead(assetKey, onCachedContentLoaded);
  576. };
  577. /******************************************************************************/
  578. var getRemote = function(assetKey, callback) {
  579. var assetDetails = {},
  580. contentURLs,
  581. contentURL;
  582. var reportBack = function(content, err) {
  583. var details = { assetKey: assetKey, content: content };
  584. if ( err ) {
  585. details.error = assetDetails.lastError = err;
  586. } else {
  587. assetDetails.lastError = undefined;
  588. }
  589. callback(details);
  590. };
  591. var onRemoteContentLoaded = function(details) {
  592. if ( stringIsNotEmpty(details.content) === false ) {
  593. registerAssetSource(assetKey, { error: { time: Date.now(), error: 'No content' } });
  594. tryLoading();
  595. return;
  596. }
  597. assetCacheWrite(assetKey, {
  598. content: details.content,
  599. url: contentURL
  600. });
  601. registerAssetSource(assetKey, { error: undefined });
  602. reportBack(details.content);
  603. };
  604. var onRemoteContentError = function(details) {
  605. var text = details.statusText;
  606. if ( details.statusCode === 0 ) {
  607. text = 'network error';
  608. }
  609. registerAssetSource(assetKey, { error: { time: Date.now(), error: text } });
  610. tryLoading();
  611. };
  612. var tryLoading = function() {
  613. while ( (contentURL = contentURLs.shift()) ) {
  614. if ( reIsExternalPath.test(contentURL) ) { break; }
  615. }
  616. if ( !contentURL ) {
  617. return reportBack('', 'E_NOTFOUND');
  618. }
  619. api.fetchText(contentURL, onRemoteContentLoaded, onRemoteContentError);
  620. };
  621. getAssetSourceRegistry(function(registry) {
  622. assetDetails = registry[assetKey] || {};
  623. if ( typeof assetDetails.contentURL === 'string' ) {
  624. contentURLs = [ assetDetails.contentURL ];
  625. } else if ( Array.isArray(assetDetails.contentURL) ) {
  626. contentURLs = assetDetails.contentURL.slice(0);
  627. } else {
  628. contentURLs = [];
  629. }
  630. tryLoading();
  631. });
  632. };
  633. /******************************************************************************/
  634. api.put = function(assetKey, content, callback) {
  635. assetCacheWrite(assetKey, content, callback);
  636. };
  637. /******************************************************************************/
  638. api.metadata = function(callback) {
  639. var assetRegistryReady = false,
  640. cacheRegistryReady = false;
  641. var onReady = function() {
  642. var assetDict = JSON.parse(JSON.stringify(assetSourceRegistry)),
  643. cacheDict = assetCacheRegistry,
  644. assetEntry, cacheEntry,
  645. now = Date.now(), obsoleteAfter;
  646. for ( var assetKey in assetDict ) {
  647. assetEntry = assetDict[assetKey];
  648. cacheEntry = cacheDict[assetKey];
  649. if ( cacheEntry ) {
  650. assetEntry.cached = true;
  651. assetEntry.writeTime = cacheEntry.writeTime;
  652. obsoleteAfter = cacheEntry.writeTime + assetEntry.updateAfter * 86400000;
  653. assetEntry.obsolete = obsoleteAfter < now;
  654. assetEntry.remoteURL = cacheEntry.remoteURL;
  655. } else {
  656. assetEntry.writeTime = 0;
  657. obsoleteAfter = 0;
  658. assetEntry.obsolete = true;
  659. }
  660. }
  661. callback(assetDict);
  662. };
  663. getAssetSourceRegistry(function() {
  664. assetRegistryReady = true;
  665. if ( cacheRegistryReady ) { onReady(); }
  666. });
  667. getAssetCacheRegistry(function() {
  668. cacheRegistryReady = true;
  669. if ( assetRegistryReady ) { onReady(); }
  670. });
  671. };
  672. /******************************************************************************/
  673. api.purge = assetCacheMarkAsDirty;
  674. api.remove = function(pattern, callback) {
  675. assetCacheRemove(pattern, callback);
  676. };
  677. api.rmrf = function() {
  678. assetCacheRemove(/./);
  679. };
  680. /******************************************************************************/
  681. // Asset updater area.
  682. var updaterStatus,
  683. updaterTimer,
  684. updaterAssetDelayDefault = 120000,
  685. updaterAssetDelay = updaterAssetDelayDefault,
  686. updaterUpdated = [],
  687. updaterFetched = new Set();
  688. var updateFirst = function() {
  689. updaterStatus = 'updating';
  690. updaterFetched.clear();
  691. updaterUpdated = [];
  692. fireNotification('before-assets-updated');
  693. updateNext();
  694. };
  695. var updateNext = function() {
  696. var assetDict, cacheDict;
  697. // This will remove a cached asset when it's no longer in use.
  698. var garbageCollectOne = function(assetKey) {
  699. var cacheEntry = cacheDict[assetKey];
  700. if ( cacheEntry && cacheEntry.readTime < assetCacheRegistryStartTime ) {
  701. assetCacheRemove(assetKey);
  702. }
  703. };
  704. var findOne = function() {
  705. var now = Date.now(),
  706. assetEntry, cacheEntry;
  707. for ( var assetKey in assetDict ) {
  708. assetEntry = assetDict[assetKey];
  709. if ( assetEntry.hasRemoteURL !== true ) { continue; }
  710. if ( updaterFetched.has(assetKey) ) { continue; }
  711. cacheEntry = cacheDict[assetKey];
  712. if ( cacheEntry && (cacheEntry.writeTime + assetEntry.updateAfter * 86400000) > now ) {
  713. continue;
  714. }
  715. if ( fireNotification('before-asset-updated', { assetKey: assetKey }) !== false ) {
  716. return assetKey;
  717. }
  718. garbageCollectOne(assetKey);
  719. }
  720. };
  721. var updatedOne = function(details) {
  722. if ( details.content !== '' ) {
  723. updaterUpdated.push(details.assetKey);
  724. if ( details.assetKey === 'assets.json' ) {
  725. updateAssetSourceRegistry(details.content);
  726. }
  727. } else {
  728. fireNotification('asset-update-failed', { assetKey: details.assetKey });
  729. }
  730. if ( findOne() !== undefined ) {
  731. vAPI.setTimeout(updateNext, updaterAssetDelay);
  732. } else {
  733. updateDone();
  734. }
  735. };
  736. var updateOne = function() {
  737. var assetKey = findOne();
  738. if ( assetKey === undefined ) {
  739. return updateDone();
  740. }
  741. updaterFetched.add(assetKey);
  742. getRemote(assetKey, updatedOne);
  743. };
  744. getAssetSourceRegistry(function(dict) {
  745. assetDict = dict;
  746. if ( !cacheDict ) { return; }
  747. updateOne();
  748. });
  749. getAssetCacheRegistry(function(dict) {
  750. cacheDict = dict;
  751. if ( !assetDict ) { return; }
  752. updateOne();
  753. });
  754. };
  755. var updateDone = function() {
  756. var assetKeys = updaterUpdated.slice(0);
  757. updaterFetched.clear();
  758. updaterUpdated = [];
  759. updaterStatus = undefined;
  760. updaterAssetDelay = updaterAssetDelayDefault;
  761. fireNotification('after-assets-updated', { assetKeys: assetKeys });
  762. };
  763. api.updateStart = function(details) {
  764. var oldUpdateDelay = updaterAssetDelay,
  765. newUpdateDelay = details.delay || updaterAssetDelayDefault;
  766. updaterAssetDelay = Math.min(oldUpdateDelay, newUpdateDelay);
  767. if ( updaterStatus !== undefined ) {
  768. if ( newUpdateDelay < oldUpdateDelay ) {
  769. clearTimeout(updaterTimer);
  770. updaterTimer = vAPI.setTimeout(updateNext, updaterAssetDelay);
  771. }
  772. return;
  773. }
  774. updateFirst();
  775. };
  776. api.updateStop = function() {
  777. if ( updaterTimer ) {
  778. clearTimeout(updaterTimer);
  779. updaterTimer = undefined;
  780. }
  781. if ( updaterStatus !== undefined ) {
  782. updateDone();
  783. }
  784. };
  785. /******************************************************************************/
  786. return api;
  787. /******************************************************************************/
  788. })();
  789. /******************************************************************************/