db.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735
  1. import {
  2. compareVersion, dataUri2text, i18n, getScriptHome, isDataUri, makeDataUri,
  3. getFullUrl, getScriptName, getScriptUpdateUrl, isRemote, sendCmd, trueJoin,
  4. makePause,
  5. } from '@/common';
  6. import { ICON_PREFIX, INJECT_PAGE, INJECT_AUTO, TIMEOUT_WEEK } from '@/common/consts';
  7. import { deepSize, forEachEntry, forEachKey, forEachValue } from '@/common/object';
  8. import pluginEvents from '../plugin/events';
  9. import { getNameURI, parseMeta, newScript, getDefaultCustom } from './script';
  10. import { testScript, testBlacklist, testerBatch } from './tester';
  11. import { preInitialize } from './init';
  12. import { commands } from './message';
  13. import patchDB from './patch-db';
  14. import { setOption } from './options';
  15. import storage, {
  16. S_CACHE, S_CODE, S_REQUIRE, S_VALUE,
  17. S_CACHE_PRE, S_CODE_PRE, S_MOD_PRE, S_REQUIRE_PRE, S_SCRIPT_PRE, S_VALUE_PRE,
  18. } from './storage';
  19. export const store = {
  20. /** @type {VMScript[]} */
  21. scripts: [],
  22. /** @type {Object<string,VMScript[]>} */
  23. scriptMap: {},
  24. /** @type {{ [url:string]: number }} */
  25. sizes: {},
  26. /** Same order as in SIZE_TITLES and getSizes */
  27. sizesPrefixRe: RegExp(`^(${S_CODE_PRE}|${S_SCRIPT_PRE}|${S_VALUE_PRE}|${S_REQUIRE_PRE}|${S_CACHE_PRE}${S_MOD_PRE})`),
  28. storeInfo: {
  29. id: 0,
  30. position: 0,
  31. },
  32. };
  33. Object.assign(commands, {
  34. CheckPosition: sortScripts,
  35. CheckRemove: checkRemove,
  36. /** @return {VMScript} */
  37. GetScript: getScript,
  38. /** @return {Promise<{ items: VMScript[], values? }>} */
  39. async ExportZip({ values }) {
  40. const scripts = getScripts();
  41. const ids = scripts.map(getPropsId);
  42. const codeMap = await storage.code.getMulti(ids);
  43. return {
  44. items: scripts.map(script => ({ script, code: codeMap[script.props.id] })),
  45. values: values ? await storage.value.getMulti(ids) : undefined,
  46. };
  47. },
  48. /** @return {Promise<string>} */
  49. GetScriptCode(id) {
  50. return storage.code[Array.isArray(id) ? 'getMulti' : 'getOne'](id);
  51. },
  52. GetScriptVer(opts) {
  53. const script = getScript(opts);
  54. return script && !script.config.removed
  55. ? script.meta.version
  56. : null;
  57. },
  58. /** @return {Promise<void>} */
  59. MarkRemoved({ id, removed }) {
  60. return updateScriptInfo(id, {
  61. config: { removed: removed ? 1 : 0 },
  62. props: { lastModified: Date.now() },
  63. });
  64. },
  65. /** @return {Promise<number>} */
  66. Move({ id, offset }) {
  67. const script = getScriptById(id);
  68. const index = store.scripts.indexOf(script);
  69. store.scripts.splice(index, 1);
  70. store.scripts.splice(index + offset, 0, script);
  71. return normalizePosition();
  72. },
  73. /** @return {Promise<void>} */
  74. async RemoveScript(id) {
  75. const i = store.scripts.indexOf(getScriptById(id));
  76. if (i >= 0) {
  77. store.scripts.splice(i, 1);
  78. await storage.base.remove([
  79. storage.script.toKey(id),
  80. storage.code.toKey(id),
  81. storage.value.toKey(id),
  82. ]);
  83. }
  84. return sendCmd('RemoveScript', id);
  85. },
  86. ParseMeta: parseMetaWithErrors,
  87. ParseScript: parseScript,
  88. /** @return {Promise<void>} */
  89. UpdateScriptInfo({ id, config, custom }) {
  90. return updateScriptInfo(id, {
  91. config,
  92. custom,
  93. props: { lastModified: Date.now() },
  94. });
  95. },
  96. /** @return {Promise<number>} */
  97. Vacuum: vacuum,
  98. });
  99. preInitialize.push(async () => {
  100. const lastVersion = await storage.base.getOne('version');
  101. const version = process.env.VM_VER;
  102. if (!lastVersion) await patchDB();
  103. if (version !== lastVersion) storage.base.set({ version });
  104. const data = await storage.base.getMulti();
  105. const { scripts, storeInfo, scriptMap } = store;
  106. const uriMap = {};
  107. data::forEachEntry(([key, script]) => {
  108. const id = +storage.script.toId(key);
  109. if (id && script) {
  110. if (scriptMap[id] && scriptMap[id] !== script) {
  111. // ID conflicts!
  112. // Should not happen, discard duplicates.
  113. return;
  114. }
  115. const uri = getNameURI(script);
  116. if (uriMap[uri]) {
  117. // Namespace conflicts!
  118. // Should not happen, discard duplicates.
  119. return;
  120. }
  121. uriMap[uri] = script;
  122. script.props = {
  123. ...script.props,
  124. id,
  125. uri,
  126. };
  127. script.custom = {
  128. ...getDefaultCustom(),
  129. ...script.custom,
  130. };
  131. storeInfo.id = Math.max(storeInfo.id, id);
  132. storeInfo.position = Math.max(storeInfo.position, getInt(script.props.position));
  133. scripts.push(script);
  134. // listing all known resource urls in order to remove unused mod keys
  135. const {
  136. meta = script.meta = {},
  137. } = script;
  138. if (!meta.require) meta.require = [];
  139. if (!meta.resources) meta.resources = {};
  140. meta.grant = [...new Set(meta.grant || [])]; // deduplicate
  141. }
  142. });
  143. // Switch defaultInjectInto from `page` to `auto` when upgrading VM2.12.7 or older
  144. if (version !== lastVersion
  145. && IS_FIREFOX
  146. && data.options?.defaultInjectInto === INJECT_PAGE
  147. && compareVersion(lastVersion, '2.12.7') <= 0) {
  148. setOption('defaultInjectInto', INJECT_AUTO);
  149. }
  150. if (process.env.DEBUG) {
  151. console.log('store:', store); // eslint-disable-line no-console
  152. }
  153. sortScripts();
  154. vacuum(data);
  155. });
  156. /** @return {number} */
  157. function getInt(val) {
  158. return +val || 0;
  159. }
  160. /** @return {?number} */
  161. function getPropsId(script) {
  162. return script?.props.id;
  163. }
  164. /** @return {void} */
  165. function updateLastModified() {
  166. setOption('lastModified', Date.now());
  167. }
  168. /** @return {Promise<boolean>} */
  169. export async function normalizePosition() {
  170. const updates = store.scripts.reduce((res, script, index) => {
  171. const { props } = script;
  172. const position = index + 1;
  173. if (props.position !== position) {
  174. props.position = position;
  175. (res || (res = {}))[props.id] = script;
  176. }
  177. return res;
  178. }, null);
  179. store.storeInfo.position = store.scripts.length;
  180. if (updates) {
  181. await storage.script.set(updates);
  182. updateLastModified();
  183. }
  184. return !!updates;
  185. }
  186. /** @return {Promise<number>} */
  187. export async function sortScripts() {
  188. store.scripts.sort((a, b) => getInt(a.props.position) - getInt(b.props.position));
  189. const changed = await normalizePosition();
  190. sendCmd('ScriptsUpdated', null);
  191. return changed;
  192. }
  193. /** @return {?VMScript} */
  194. export function getScriptById(id) {
  195. return store.scriptMap[id];
  196. }
  197. /** @return {?VMScript} */
  198. export function getScript({ id, uri, meta }) {
  199. let script;
  200. if (id) {
  201. script = getScriptById(id);
  202. } else {
  203. if (!uri) uri = getNameURI({ meta, id: '@@should-have-name' });
  204. script = store.scripts.find(({ props }) => uri === props.uri);
  205. }
  206. return script;
  207. }
  208. /** @return {VMScript[]} */
  209. export function getScripts() {
  210. return store.scripts.filter(script => !script.config.removed);
  211. }
  212. export const ENV_CACHE_KEYS = 'cacheKeys';
  213. export const ENV_REQ_KEYS = 'reqKeys';
  214. export const ENV_SCRIPTS = 'scripts';
  215. export const ENV_VALUE_IDS = 'valueIds';
  216. const GMVALUES_RE = /^GM[_.](listValues|([gs]et|delete)Value)$/;
  217. const RUN_AT_RE = /^document-(start|body|end|idle)$/;
  218. const STORAGE_ROUTES = {
  219. [S_CACHE]: ENV_CACHE_KEYS,
  220. [S_CODE]: 'ids',
  221. [S_REQUIRE]: ENV_REQ_KEYS,
  222. [S_VALUE]: ENV_VALUE_IDS,
  223. };
  224. const STORAGE_ROUTES_ENTRIES = Object.entries(STORAGE_ROUTES);
  225. const retriedStorageKeys = {};
  226. /**
  227. * @desc Get scripts to be injected to page with specific URL.
  228. */
  229. export function getScriptsByURL(url, isTop, errors) {
  230. testerBatch(errors || true);
  231. const allScripts = testBlacklist(url)
  232. ? []
  233. : store.scripts.filter(script => (
  234. !script.config.removed
  235. && (isTop || !(script.custom.noframes ?? script.meta.noframes))
  236. && testScript(url, script)
  237. ));
  238. testerBatch();
  239. return getScriptEnv(allScripts);
  240. }
  241. /**
  242. * @param {VMScript[]} scripts
  243. * @return {Promise<VMInjection.Env>}
  244. */
  245. async function getScriptEnv(scripts) {
  246. const disabledIds = [];
  247. const [envStart, envDelayed] = [0, 1].map(() => ({
  248. depsMap: {},
  249. [ENV_SCRIPTS]: [],
  250. }));
  251. for (const [areaName, listName] of STORAGE_ROUTES_ENTRIES) {
  252. envStart[areaName] = {}; envDelayed[areaName] = {};
  253. envStart[listName] = []; envDelayed[listName] = [];
  254. }
  255. scripts.forEach((script) => {
  256. const { id } = script.props;
  257. if (!script.config.enabled) {
  258. disabledIds.push(id);
  259. return;
  260. }
  261. const { meta, custom } = script;
  262. const { pathMap = buildPathMap(script) } = custom;
  263. const runAt = `${custom.runAt || meta.runAt || ''}`.match(RUN_AT_RE)?.[1] || 'end';
  264. /** @type {VMInjection.Env} */
  265. const env = runAt === 'start' || runAt === 'body' ? envStart : envDelayed;
  266. const { depsMap } = env;
  267. env.ids.push(id);
  268. if (meta.grant.some(GMVALUES_RE.test, GMVALUES_RE)) {
  269. env[ENV_VALUE_IDS].push(id);
  270. }
  271. for (const [list, name, dataUriDecoder] of [
  272. [meta.require, S_REQUIRE, dataUri2text],
  273. [Object.values(meta.resources), S_CACHE],
  274. ]) {
  275. const listName = STORAGE_ROUTES[name];
  276. const envCheck = name === S_CACHE ? envStart : env; // envStart cache is reused in injected
  277. for (let url of list) {
  278. url = pathMap[url] || url;
  279. if (url) {
  280. if (isDataUri(url)) {
  281. if (dataUriDecoder) {
  282. env[name][url] = dataUriDecoder(url);
  283. }
  284. } else if (!envCheck[listName].includes(url)) {
  285. env[listName].push(url);
  286. (depsMap[url] || (depsMap[url] = [])).push(id);
  287. }
  288. }
  289. }
  290. }
  291. env[ENV_SCRIPTS].push({ ...script, runAt });
  292. });
  293. if (envStart.ids.length) {
  294. Object.assign(envStart, await readEnvironmentData(envStart));
  295. }
  296. if (envDelayed.ids.length) {
  297. envDelayed.promise = makePause().then(() => readEnvironmentData(envDelayed));
  298. }
  299. return Object.assign(envStart, { disabledIds, envDelayed });
  300. }
  301. async function readEnvironmentData(env, isRetry) {
  302. const keys = [];
  303. for (const [area, listName] of STORAGE_ROUTES_ENTRIES) {
  304. for (const id of env[listName]) {
  305. keys.push(storage[area].toKey(id));
  306. }
  307. }
  308. const data = await storage.base.getMulti(keys);
  309. const badScripts = new Set();
  310. for (const [area, listName] of STORAGE_ROUTES_ENTRIES) {
  311. for (const id of env[listName]) {
  312. let val = data[storage[area].toKey(id)];
  313. if (!val && area === S_VALUE) val = {};
  314. env[area][id] = val;
  315. if (val == null && retriedStorageKeys[area + id] !== 2) {
  316. retriedStorageKeys[area + id] = isRetry ? 2 : 1;
  317. if (!isRetry) {
  318. console.warn(`The "${area}" storage is missing "${id}"! Vacuuming...`);
  319. if ((await vacuum()).fixes) {
  320. return readEnvironmentData(env, true);
  321. }
  322. }
  323. if (area === S_CODE) {
  324. badScripts.add(id);
  325. } else {
  326. env.depsMap[id]?.forEach(scriptId => badScripts.add(scriptId));
  327. }
  328. }
  329. }
  330. }
  331. if (badScripts.size) {
  332. const title = i18n('msgMissingResources');
  333. const text = i18n('msgReinstallScripts')
  334. + [...badScripts].map(id => `\n#${id}: ${getScriptName(getScriptById(id))}`).join('');
  335. console.error(`${title} ${text}`);
  336. await commands.Notification({ title, text }, undefined, {
  337. onClick() {
  338. badScripts.forEach(id => commands.OpenEditor(id));
  339. },
  340. });
  341. }
  342. return env;
  343. }
  344. /**
  345. * @desc Get data for dashboard.
  346. * @return {Promise<{ scripts: VMScript[], cache: Object }>}
  347. */
  348. export async function getData({ ids, sizes }) {
  349. const scripts = ids ? ids.map(getScriptById) : store.scripts;
  350. return {
  351. scripts,
  352. cache: await getIconCache(scripts),
  353. sizes: sizes && getSizes(ids),
  354. };
  355. }
  356. /**
  357. * @param {VMScript[]} scripts
  358. * @return {Promise<{}>}
  359. */
  360. async function getIconCache(scripts) {
  361. const urls = [];
  362. for (const { custom, meta: { icon } } of scripts) {
  363. if (isRemote(icon)) {
  364. urls.push(custom.pathMap[icon] || icon);
  365. }
  366. }
  367. // Getting a data uri for own icon to load it instantly in Chrome when there are many images
  368. const ownPath = `${ICON_PREFIX}38.png`;
  369. const [res, ownUri] = await Promise.all([
  370. storage.cache.getMulti(urls, makeDataUri),
  371. commands.GetImageData(ownPath),
  372. ]);
  373. res[ownPath] = ownUri;
  374. return res;
  375. }
  376. /**
  377. * @param {number[]} [ids]
  378. * @return {number[][]}
  379. */
  380. export function getSizes(ids) {
  381. const scripts = ids ? ids.map(getScriptById) : store.scripts;
  382. return scripts.map(({
  383. meta,
  384. custom: { pathMap = {} },
  385. props: { id },
  386. }, i) => [
  387. // Same order as SIZE_TITLES and sizesPrefixRe
  388. store.sizes[S_CODE_PRE + id] || 0,
  389. deepSize(scripts[i]),
  390. store.sizes[S_VALUE_PRE + id] || 0,
  391. meta.require.reduce(getSizeForRequires, { len: 0, pathMap }).len,
  392. Object.values(meta.resources).reduce(getSizeForResources, { len: 0, pathMap }).len,
  393. ]);
  394. }
  395. function getSizeForRequires(accum, url) {
  396. accum.len += (store.sizes[S_REQUIRE_PRE + (accum.pathMap[url] || url)] || 0) + url.length;
  397. return accum;
  398. }
  399. function getSizeForResources(accum, url) {
  400. accum.len += (store.sizes[S_CACHE_PRE + (accum.pathMap[url] || url)] || 0) + url.length;
  401. return accum;
  402. }
  403. /** @return {?Promise<void>} only if something was removed, otherwise undefined */
  404. export function checkRemove({ force } = {}) {
  405. const now = Date.now();
  406. const toKeep = [];
  407. const toRemove = [];
  408. store.scripts.forEach(script => {
  409. const { id, lastModified } = script.props;
  410. if (script.config.removed && (force || now - getInt(lastModified) > TIMEOUT_WEEK)) {
  411. toRemove.push(storage.code.toKey(id),
  412. storage.script.toKey(id),
  413. storage.value.toKey(id));
  414. } else {
  415. toKeep.push(script);
  416. }
  417. });
  418. if (toRemove.length) {
  419. store.scripts = toKeep;
  420. return storage.base.remove(toRemove);
  421. }
  422. }
  423. /** @return {string} */
  424. function getUUID() {
  425. const rnd = new Uint16Array(8);
  426. window.crypto.getRandomValues(rnd);
  427. // xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx
  428. // We're using UUIDv4 variant 1 so N=4 and M=8
  429. // See format_uuid_v3or5 in https://tools.ietf.org/rfc/rfc4122.txt
  430. rnd[3] = rnd[3] & 0x0FFF | 0x4000; // eslint-disable-line no-bitwise
  431. rnd[4] = rnd[4] & 0x3FFF | 0x8000; // eslint-disable-line no-bitwise
  432. return '01-2-3-4-567'.replace(/\d/g, i => (rnd[i] + 0x1_0000).toString(16).slice(-4));
  433. }
  434. /**
  435. * @param {VMScript} script
  436. * @param {string} code
  437. * @return {Promise<VMScript[]>}
  438. */
  439. async function saveScript(script, code) {
  440. const config = script.config || {};
  441. config.enabled = getInt(config.enabled);
  442. config.shouldUpdate = getInt(config.shouldUpdate);
  443. const props = script.props || {};
  444. let oldScript;
  445. if (!props.id) {
  446. store.storeInfo.id += 1;
  447. props.id = store.storeInfo.id;
  448. } else {
  449. oldScript = store.scriptMap[props.id];
  450. }
  451. props.uri = getNameURI(script);
  452. props.uuid = props.uuid || crypto.randomUUID?.() || getUUID();
  453. // Do not allow script with same name and namespace
  454. if (store.scripts.some(({ props: { id, uri } = {} }) => props.id !== id && props.uri === uri)) {
  455. throw i18n('msgNamespaceConflict');
  456. }
  457. if (oldScript) {
  458. script.config = { ...oldScript.config, ...config };
  459. script.props = { ...oldScript.props, ...props };
  460. const index = store.scripts.indexOf(oldScript);
  461. store.scripts[index] = script;
  462. } else {
  463. if (!props.position) {
  464. store.storeInfo.position += 1;
  465. props.position = store.storeInfo.position;
  466. } else if (store.storeInfo.position < props.position) {
  467. store.storeInfo.position = props.position;
  468. }
  469. script.config = config;
  470. script.props = props;
  471. store.scripts.push(script);
  472. }
  473. return storage.base.set({
  474. [storage.script.toKey(props.id)]: script,
  475. [storage.code.toKey(props.id)]: code,
  476. });
  477. }
  478. /** @return {Promise<void>} */
  479. export async function updateScriptInfo(id, data) {
  480. const script = store.scriptMap[id];
  481. if (!script) throw null;
  482. script.props = { ...script.props, ...data.props };
  483. script.config = { ...script.config, ...data.config };
  484. script.custom = { ...script.custom, ...data.custom };
  485. await storage.script.setOne(id, script);
  486. return sendCmd('UpdateScript', { where: { id }, update: script });
  487. }
  488. /**
  489. * @param {string | {code:string, custom:VMScript.Custom}} src
  490. * @return {{ meta: VMScript.Meta, errors: string[] }}
  491. */
  492. function parseMetaWithErrors(src) {
  493. const isObject = typeof src === 'object';
  494. const custom = isObject && src.custom || getDefaultCustom();
  495. const meta = parseMeta(isObject ? src.code : src);
  496. const errors = [];
  497. testerBatch(errors);
  498. testScript('', { meta, custom });
  499. testerBatch();
  500. return {
  501. meta,
  502. errors: errors.length ? errors : null,
  503. };
  504. }
  505. /** @return {Promise<{ isNew?, update, where }>} */
  506. export async function parseScript(src) {
  507. const { meta, errors } = parseMetaWithErrors(src);
  508. if (!meta.name) throw `${i18n('msgInvalidScript')}\n${i18n('labelNoName')}`;
  509. const result = {
  510. errors,
  511. update: {
  512. message: src.message == null ? i18n('msgUpdated') : src.message || '',
  513. },
  514. };
  515. let script;
  516. const oldScript = await getScript({ id: src.id, meta });
  517. if (oldScript) {
  518. if (src.isNew) throw i18n('msgNamespaceConflict');
  519. script = { ...oldScript };
  520. } else {
  521. ({ script } = newScript());
  522. result.isNew = true;
  523. result.update.message = i18n('msgInstalled');
  524. }
  525. script.config = {
  526. ...script.config,
  527. ...src.config,
  528. removed: 0, // force reset `removed` since this is an installation
  529. };
  530. script.custom = {
  531. ...script.custom,
  532. ...src.custom,
  533. };
  534. script.props = {
  535. ...script.props,
  536. lastModified: Date.now(),
  537. lastUpdated: Date.now(),
  538. ...src.props,
  539. };
  540. script.meta = meta;
  541. if (!getScriptHome(script) && isRemote(src.from)) {
  542. script.custom.homepageURL = src.from;
  543. }
  544. if (isRemote(src.url)) script.custom.lastInstallURL = src.url;
  545. if (src.position) script.props.position = +src.position;
  546. buildPathMap(script, src.url);
  547. await saveScript(script, src.code);
  548. fetchResources(script, src);
  549. Object.assign(result.update, script, src.update);
  550. result.where = { id: script.props.id };
  551. sendCmd('UpdateScript', result);
  552. pluginEvents.emit('scriptChanged', result);
  553. return result;
  554. }
  555. /** @return {Object} */
  556. function buildPathMap(script, base) {
  557. const { meta } = script;
  558. const baseUrl = base || script.custom.lastInstallURL;
  559. const pathMap = baseUrl ? [
  560. ...meta.require,
  561. ...Object.values(meta.resources),
  562. meta.icon,
  563. ].reduce((map, key) => {
  564. if (key) {
  565. const fullUrl = getFullUrl(key, baseUrl);
  566. if (fullUrl !== key) map[key] = fullUrl;
  567. }
  568. return map;
  569. }, {}) : {};
  570. script.custom.pathMap = pathMap;
  571. return pathMap;
  572. }
  573. /** @return {Promise<?string>} resolves to error text if `resourceCache` is absent */
  574. export async function fetchResources(script, resourceCache, reqOptions) {
  575. const { custom: { pathMap }, meta } = script;
  576. const snatch = (url, type, validator) => {
  577. if (!url || isDataUri(url)) return;
  578. url = pathMap[url] || url;
  579. const contents = resourceCache?.[type]?.[url];
  580. return contents != null && !validator
  581. ? storage[type].setOne(url, contents) && null
  582. : storage[type].fetch(url, reqOptions, validator).catch(err => err);
  583. };
  584. const errors = await Promise.all([
  585. ...meta.require.map(url => snatch(url, S_REQUIRE)),
  586. ...Object.values(meta.resources).map(url => snatch(url, S_CACHE)),
  587. isRemote(meta.icon) && snatch(meta.icon, S_CACHE, validateImage),
  588. ]);
  589. if (!resourceCache?.ignoreDepsErrors) {
  590. const error = errors.map(formatHttpError)::trueJoin('\n');
  591. if (error) {
  592. const message = i18n('msgErrorFetchingResource');
  593. sendCmd('UpdateScript', {
  594. update: { error, message },
  595. where: { id: script.props.id },
  596. });
  597. return `${message}\n${error}`;
  598. }
  599. }
  600. }
  601. /** @return {Promise<void>} resolves on success, rejects on error */
  602. function validateImage(url, buf, type) {
  603. return new Promise((resolve, reject) => {
  604. const blobUrl = URL.createObjectURL(new Blob([buf], { type }));
  605. const onDone = (e) => {
  606. URL.revokeObjectURL(blobUrl);
  607. if (e.type === 'load') resolve();
  608. else reject(`IMAGE_ERROR: ${url}`);
  609. };
  610. const image = new Image();
  611. image.onload = onDone;
  612. image.onerror = onDone;
  613. image.src = blobUrl;
  614. });
  615. }
  616. function formatHttpError(e) {
  617. return e && [e.status && `HTTP${e.status}`, e.url]::trueJoin(' ') || e;
  618. }
  619. let _vacuuming;
  620. /**
  621. * @param {Object} [data]
  622. * @return {Promise<{errors:string[], fixes:number}>}
  623. */
  624. export async function vacuum(data) {
  625. if (_vacuuming) return _vacuuming;
  626. let resolveSelf;
  627. _vacuuming = new Promise(r => { resolveSelf = r; });
  628. const sizes = {};
  629. const result = {};
  630. const toFetch = [];
  631. const keysToRemove = [];
  632. /** -1=untouched, 1=touched, 2(+scriptId)=missing */
  633. const status = {};
  634. const prefixRe = RegExp(`^(${S_VALUE_PRE}|${S_CACHE_PRE}|${S_REQUIRE_PRE}|${S_CODE_PRE}${S_MOD_PRE})`);
  635. const downloadUrls = {};
  636. const touch = (prefix, id, scriptId, pathMap) => {
  637. if (!id || pathMap && isDataUri(id)) {
  638. return 0;
  639. }
  640. const key = prefix + (pathMap?.[id] || id);
  641. const val = status[key];
  642. if (val < 0) {
  643. status[key] = 1;
  644. if (id !== scriptId) {
  645. status[S_MOD_PRE + id] = 1;
  646. }
  647. if (prefix === S_CACHE_PRE || prefix === S_REQUIRE_PRE || prefix === S_VALUE_PRE) {
  648. sizes[key] = deepSize(data[key]) + (prefix === S_VALUE_PRE ? 0 : key.length);
  649. }
  650. } else if (!val) {
  651. status[key] = 2 + scriptId;
  652. }
  653. };
  654. if (!data) data = await storage.base.getMulti();
  655. data::forEachKey((key) => {
  656. if (prefixRe.test(key)) {
  657. status[key] = -1;
  658. }
  659. });
  660. store.sizes = sizes;
  661. store.scripts.forEach((script) => {
  662. const { meta, props } = script;
  663. const { icon } = meta;
  664. const { id } = props;
  665. const pathMap = script.custom.pathMap || buildPathMap(script);
  666. const updUrls = getScriptUpdateUrl(script, true);
  667. if (updUrls) {
  668. updUrls.forEach(url => touch(S_MOD_PRE, url, id));
  669. downloadUrls[id] = updUrls[0];
  670. }
  671. touch(S_CODE_PRE, id, id);
  672. touch(S_VALUE_PRE, id, id);
  673. meta.require.forEach(url => touch(S_REQUIRE_PRE, url, id, pathMap));
  674. meta.resources::forEachValue(url => touch(S_CACHE_PRE, url, id, pathMap));
  675. if (isRemote(icon)) touch(S_CACHE_PRE, icon, id, pathMap);
  676. });
  677. status::forEachEntry(([key, value]) => {
  678. if (value < 0) {
  679. // Removing redundant value
  680. keysToRemove.push(key);
  681. } else if (value >= 2) {
  682. // Downloading the missing code or resource
  683. const area = storage.forKey(key);
  684. const id = area.toId(key);
  685. const url = area.name === S_CODE ? downloadUrls[id] : id;
  686. if (url && area.fetch) {
  687. keysToRemove.push(S_MOD_PRE + url);
  688. toFetch.push(area.fetch(url).catch(err => `${
  689. getScriptName(getScriptById(+id || value - 2))
  690. }: ${
  691. formatHttpError(err)
  692. }`));
  693. }
  694. }
  695. });
  696. if (keysToRemove.length) {
  697. await storage.base.remove(keysToRemove); // Removing `mod` before fetching
  698. result.errors = (await Promise.all(toFetch)).filter(Boolean);
  699. }
  700. _vacuuming = null;
  701. result.fixes = toFetch.length + keysToRemove.length;
  702. resolveSelf(result);
  703. return result;
  704. }