db.js 22 KB

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