Browse Source

Merge remote-tracking branch 'LOCAL-main/master' into vue3

tophf 3 years ago
parent
commit
a29c60481b

+ 66 - 52
src/background/utils/db.js

@@ -1,5 +1,5 @@
 import {
-  compareVersion, dataUri2text, i18n, getScriptHome, makeDataUri,
+  compareVersion, dataUri2text, i18n, getScriptHome, isDataUri, makeDataUri,
   getFullUrl, getScriptName, getScriptUpdateUrl, isRemote, sendCmd, trueJoin,
 } from '@/common';
 import { INJECT_PAGE, INJECT_AUTO, TIMEOUT_WEEK } from '@/common/consts';
@@ -100,9 +100,14 @@ preInitialize.push(async () => {
   const { scripts, storeInfo, scriptMap } = store;
   const uriMap = {};
   const mods = [];
+  const toRemove = [];
   const resUrls = new Set();
   /** @this VMScriptCustom.pathMap */
-  const rememberUrl = function _(url) { resUrls.add(this[url] || url); };
+  const rememberUrl = function _(url) {
+    if (url && !isDataUri(url)) {
+      resUrls.add(this[url] || url);
+    }
+  };
   data::forEachEntry(([key, script]) => {
     let id = +storage.script.toId(key);
     if (id) {
@@ -146,8 +151,11 @@ preInitialize.push(async () => {
       getScriptUpdateUrl(script, true)?.forEach(rememberUrl, pathMap);
     } else if ((id = storage.mod.toId(key))) {
       mods.push(id);
+    } else if (/^\w+:data:/.test(key)) { // VM before 2.13.2 stored them unnecessarily
+      toRemove.push(key);
     }
   });
+  storage.base.remove(toRemove);
   storage.mod.remove(mods.filter(url => !resUrls.has(url)));
   // Switch defaultInjectInto from `page` to `auto` when upgrading VM2.12.7 or older
   if (version !== lastVersion
@@ -233,6 +241,19 @@ export const ENV_SCRIPTS = 'scripts';
 export const ENV_VALUE_IDS = 'valueIds';
 const GMVALUES_RE = /^GM[_.](listValues|([gs]et|delete)Value)$/;
 const RUN_AT_RE = /^document-(start|body|end|idle)$/;
+const S_REQUIRE = storage.require.name;
+const S_CACHE = storage.cache.name;
+const S_VALUE = storage.value.name;
+const S_CODE = storage.code.name;
+const STORAGE_ROUTES = {
+  [S_CACHE]: ENV_CACHE_KEYS,
+  [S_CODE]: 'ids',
+  [S_REQUIRE]: ENV_REQ_KEYS,
+  [S_VALUE]: ENV_VALUE_IDS,
+};
+const STORAGE_ROUTES_ENTRIES = Object.entries(STORAGE_ROUTES);
+const retriedStorageKeys = {};
+
 /**
  * @desc Get scripts to be injected to page with specific URL.
  */
@@ -250,20 +271,18 @@ export function getScriptsByURL(url, isTop) {
 /**
  * @param {VMScript[]} scripts
  * @param {boolean} [sizing]
- * @return {VMScriptByUrlData}
  */
 function getScriptEnv(scripts, sizing) {
   const disabledIds = [];
-  /** @namespace VMScriptByUrlData */
   const [envStart, envDelayed] = [0, 1].map(() => ({
-    ids: [],
     depsMap: {},
     sizing,
-    [ENV_CACHE_KEYS]: [],
-    [ENV_REQ_KEYS]: [],
     [ENV_SCRIPTS]: [],
-    [ENV_VALUE_IDS]: [],
   }));
+  for (const [areaName, listName] of STORAGE_ROUTES_ENTRIES) {
+    envStart[areaName] = {}; envDelayed[areaName] = {};
+    envStart[listName] = []; envDelayed[listName] = [];
+  }
   scripts.forEach((script) => {
     const { id } = script.props;
     if (!sizing && !script.config.enabled) {
@@ -279,17 +298,25 @@ function getScriptEnv(scripts, sizing) {
     if (meta.grant.some(GMVALUES_RE.test, GMVALUES_RE)) {
       env[ENV_VALUE_IDS].push(id);
     }
-    for (const [list, name] of [
-      [meta.require, ENV_REQ_KEYS],
-      [Object.values(meta.resources), ENV_CACHE_KEYS],
+    for (const [list, name, dataUriDecoder] of [
+      [meta.require, S_REQUIRE, dataUri2text],
+      [Object.values(meta.resources), S_CACHE],
     ]) {
-      list.forEach(key => {
-        key = pathMap[key] || key;
-        if (key && !(name === ENV_CACHE_KEYS && envStart[name].includes(key))) {
-          env[name].push(key);
-          (depsMap[key] || (depsMap[key] = [])).push(id);
+      const listName = STORAGE_ROUTES[name];
+      const envCheck = name === S_CACHE ? envStart : env; // envStart cache is reused in injected
+      for (let url of list) {
+        url = pathMap[url] || url;
+        if (url) {
+          if (isDataUri(url)) {
+            if (dataUriDecoder) {
+              env[name][url] = dataUriDecoder(url);
+            }
+          } else if (!envCheck[listName].includes(url)) {
+            env[listName].push(url);
+            (depsMap[url] || (depsMap[url] = [])).push(id);
+          }
         }
-      });
+      }
     }
     /** @namespace VMInjectedScript */
     env[ENV_SCRIPTS].push(sizing ? script : { ...script, runAt });
@@ -301,37 +328,21 @@ function getScriptEnv(scripts, sizing) {
   return Object.assign(envStart, { disabledIds, envDelayed });
 }
 
-/**
- * Object keys == areas in `storage` module.
- * @namespace VMScriptByUrlData
- */
-const STORAGE_ROUTES = Object.entries({
-  cache: ENV_CACHE_KEYS,
-  code: 'ids',
-  require: ENV_REQ_KEYS,
-  value: ENV_VALUE_IDS,
-});
-const retriedStorageKeys = {};
-
 async function readEnvironmentData(env, isRetry) {
   const keys = [];
-  STORAGE_ROUTES.forEach(([area, srcIds]) => {
-    env[srcIds].forEach(id => {
-      if (!/^data:/.test(id)) {
-        keys.push(storage[area].toKey(id));
-      }
-    });
-  });
+  for (const [area, listName] of STORAGE_ROUTES_ENTRIES) {
+    for (const id of env[listName]) {
+      keys.push(storage[area].toKey(id));
+    }
+  }
   const data = await storage.base.getMulti(keys);
   const badScripts = new Set();
-  for (const [area, srcIds] of STORAGE_ROUTES) {
-    env[area] = {};
-    for (const id of env[srcIds]) {
-      const val = /^data:/.test(id)
-        ? area !== 'require' && id || dataUri2text(id)
-        : data[storage[area].toKey(id)];
+  for (const [area, listName] of STORAGE_ROUTES_ENTRIES) {
+    for (const id of env[listName]) {
+      let val = data[storage[area].toKey(id)];
+      if (!val && area === S_VALUE) val = {};
       env[area][id] = val;
-      if (val == null && area !== 'value' && !env.sizing && retriedStorageKeys[area + id] !== 2) {
+      if (val == null && !env.sizing && retriedStorageKeys[area + id] !== 2) {
         retriedStorageKeys[area + id] = isRetry ? 2 : 1;
         if (!isRetry) {
           console.warn(`The "${area}" storage is missing "${id}"! Vacuuming...`);
@@ -339,7 +350,7 @@ async function readEnvironmentData(env, isRetry) {
             return readEnvironmentData(env, true);
           }
         }
-        if (area === 'code') {
+        if (area === S_CODE) {
           badScripts.add(id);
         } else {
           env.depsMap[id]?.forEach(scriptId => badScripts.add(scriptId));
@@ -389,7 +400,9 @@ function getIconCache(scripts) {
 
 export async function getSizes(ids) {
   const scripts = ids ? ids.map(getScriptById) : store.scripts;
-  const { cache, code, value, require } = await getScriptEnv(scripts, true).promise;
+  const {
+    [S_CACHE]: cache, [S_CODE]: code, [S_VALUE]: value, [S_REQUIRE]: require,
+  } = await getScriptEnv(scripts, true).promise;
   return scripts.map(({
     meta,
     custom: { pathMap = {} },
@@ -566,6 +579,7 @@ function buildPathMap(script, base) {
 export async function fetchResources(script, resourceCache, reqOptions) {
   const { custom: { pathMap }, meta } = script;
   const snatch = (url, type, validator) => {
+    if (!url || isDataUri(url)) return;
     url = pathMap[url] || url;
     const contents = resourceCache?.[type]?.[url];
     return contents != null && !validator
@@ -573,9 +587,9 @@ export async function fetchResources(script, resourceCache, reqOptions) {
       : storage[type].fetch(url, reqOptions, validator).catch(err => err);
   };
   const errors = await Promise.all([
-    ...meta.require.map(url => url && snatch(url, 'require')),
-    ...Object.values(meta.resources).map(url => url && snatch(url, 'cache')),
-    isRemote(meta.icon) && snatch(meta.icon, 'cache', validateImage),
+    ...meta.require.map(url => snatch(url, S_REQUIRE)),
+    ...Object.values(meta.resources).map(url => snatch(url, S_CACHE)),
+    isRemote(meta.icon) && snatch(meta.icon, S_CACHE, validateImage),
   ]);
   if (!resourceCache?.ignoreDepsErrors) {
     const error = errors.map(formatHttpError)::trueJoin('\n');
@@ -630,10 +644,10 @@ export async function vacuum(data) {
   const requireKeys = {};
   const codeKeys = {};
   const mappings = [
-    [storage.value, valueKeys],
-    [storage.cache, cacheKeys],
-    [storage.require, requireKeys],
-    [storage.code, codeKeys],
+    [storage[S_VALUE], valueKeys],
+    [storage[S_CACHE], cacheKeys],
+    [storage[S_REQUIRE], requireKeys],
+    [storage[S_CODE], codeKeys],
   ];
   if (!data) data = await storage.base.getMulti();
   data::forEachKey((key) => {

+ 1 - 1
src/background/utils/preinject.js

@@ -316,7 +316,7 @@ function prepareScript(script) {
     // code will be `true` if the desired realm is PAGE which is not injectable
     code: isContent ? '' : forceContent || injectedCode,
     metaStr: code.match(METABLOCK_RE)[1] || '',
-    values: id in value ? value[id] || {} : null,
+    values: value[id] || null,
   });
   return isContent && [
     dataKey,

+ 8 - 6
src/background/utils/storage-fetch.js

@@ -1,4 +1,4 @@
-import { makeRaw, request } from '@/common';
+import { isDataUri, makeRaw, request } from '@/common';
 import storage from '@/common/storage';
 
 /** @type { function(url, options, check): Promise<void> } or throws on error */
@@ -25,21 +25,20 @@ storage.require.fetch = cacheOrFetch({
 function cacheOrFetch(handlers = {}) {
   const requests = {};
   const { init, transform } = handlers;
-  /** @this VMStorageBase */
+  /** @this StorageArea */
   return function cacheOrFetchHandler(...args) {
     const [url] = args;
     const promise = requests[url] || (requests[url] = this::doFetch(...args));
     return promise;
   };
-  /** @this VMStorageBase */
+  /** @this StorageArea */
   async function doFetch(...args) {
     const [url, options] = args;
     try {
-      const res = !url.startsWith('data:')
-        && await requestNewer(url, init ? init(options) : options);
+      const res = await requestNewer(url, init ? init(options) : options);
       if (res) {
         const result = transform ? await transform(res, ...args) : res.data;
-        await this.set(url, result);
+        await this.setOne(url, result);
       }
     } finally {
       delete requests[url];
@@ -48,6 +47,9 @@ function cacheOrFetch(handlers = {}) {
 }
 
 export async function requestNewer(url, opts) {
+  if (isDataUri(url)) {
+    return;
+  }
   const modOld = await storage.mod.getOne(url);
   for (const get of [0, 1]) {
     if (modOld || get) {

+ 0 - 1
src/background/utils/values.js

@@ -69,7 +69,6 @@ export function addValueOpener(tabId, frameId, injectedScripts) {
   });
 }
 
-/** Caution: may delete keys in `data` */
 function commit(data) {
   storage.value.set(data);
   chain = chain.catch(console.warn).then(broadcast);

+ 2 - 2
src/common/index.js

@@ -2,7 +2,7 @@
 
 import { browser } from '@/common/consts';
 import { deepCopy } from './object';
-import { blob2base64, i18n, noop } from './util';
+import { blob2base64, i18n, isDataUri, noop } from './util';
 
 export { normalizeKeys } from './object';
 export * from './util';
@@ -224,7 +224,7 @@ export function trueJoin(separator) {
  * @returns {?string}
  */
 export function makeDataUri(raw, url) {
-  if (url.startsWith('data:')) return url;
+  if (isDataUri(url)) return url;
   if (/^(i,|image\/)/.test(raw)) { // workaround for bugs in old VM, see 2e135cf7
     const i = raw.lastIndexOf(',');
     const type = raw.startsWith('image/') ? raw.slice(0, i) : 'image/png';

+ 2 - 2
src/common/load-script-icon.js

@@ -1,4 +1,4 @@
-import { sendCmdDirectly } from '@/common/index';
+import { isDataUri, sendCmdDirectly } from '@/common/index';
 
 const KEY = 'safeIcon';
 
@@ -15,7 +15,7 @@ export async function loadScriptIcon(script, cache = {}) {
     script[KEY] = null;
     if (url) {
       script[KEY] = cache[url]
-        || url.startsWith('data:') && url
+        || isDataUri(url) && url
         || await sendCmdDirectly('GetImageData', url)
         || null;
     }

+ 16 - 10
src/common/storage.js

@@ -3,9 +3,9 @@ import { ensureArray } from './util';
 
 let api = browser.storage.local;
 
-/** @namespace VMStorageBase */
-class Area {
+class StorageArea {
   constructor(prefix) {
+    this.name = '';
     this.prefix = prefix;
   }
 
@@ -63,15 +63,21 @@ class Area {
   }
 }
 
-export default {
+const storage = {
   get api() { return api; },
   set api(val) { api = val; },
-  base: new Area(''),
-  cache: new Area('cac:'),
-  code: new Area('code:'),
+  base: new StorageArea(''),
+  cache: new StorageArea('cac:'),
+  code: new StorageArea('code:'),
   /** last-modified HTTP header value per URL */
-  mod: new Area('mod:'),
-  require: new Area('req:'),
-  script: new Area('scr:'),
-  value: new Area('val:'),
+  mod: new StorageArea('mod:'),
+  require: new StorageArea('req:'),
+  script: new StorageArea('scr:'),
+  value: new StorageArea('val:'),
 };
+storage::mapEntry((val, name) => {
+  if (val instanceof StorageArea) {
+    val.name = name;
+  }
+});
+export default storage;

+ 1 - 0
src/common/util.js

@@ -263,6 +263,7 @@ const FORCED_ACCEPT = {
   'greasyfork.org': 'application/javascript, text/plain, text/css',
 };
 
+export const isDataUri = url => /^data:/.test(url);
 export const isRemote = url => url
   && !(/^(file:\/\/|data:|https?:\/\/([^@/]*@)?(localhost|127\.0\.0\.1|(192\.168|172\.16|10\.0)\.[0-9]+\.[0-9]+|\[(::1|(fe80|fc00)::[.:0-9a-f]+)\]|.+\.(test|example|invalid|localhost))(:[0-9]+|\/|$))/i.test(url));
 

+ 2 - 3
src/injected/content/gm-api-content.js

@@ -36,9 +36,8 @@ bridge.addHandlers({
     bridge.post('Callback', { id: cbId, data: res }, realm, el);
   },
 
-  GetResource({ id, isBlob, key }) {
-    const path = bridge.pathMaps[id]?.[key] || key;
-    const raw = bridge.cache[path];
+  GetResource({ id, isBlob, key, raw }) {
+    if (!raw) raw = bridge.cache[bridge.pathMaps[id]?.[key] || key];
     return raw ? decodeResource(raw, isBlob) : true;
   },
 

+ 8 - 1
src/injected/web/bridge.js

@@ -29,15 +29,22 @@ const bridge = {
   call: postWithCallback,
 };
 
+let callbackResult;
+
 function postWithCallback(cmd, data, context, node, cb, customCallbackId) {
   const id = safeGetUniqId();
-  callbacks[id] = cb;
+  callbacks[id] = cb || defaultCallback;
   if (customCallbackId) {
     setOwnProp(data, customCallbackId, id);
   } else {
     data = { [CALLBACK_ID]: id, data };
   }
   bridge.post(cmd, data, context, node);
+  if (!cb) return callbackResult;
+}
+
+function defaultCallback(val) {
+  callbackResult = val;
 }
 
 export default bridge;

+ 10 - 9
src/injected/web/gm-api.js

@@ -81,8 +81,8 @@ export function makeGmApi() {
     GM_getResourceText(name) {
       return getResource(this, name);
     },
-    GM_getResourceURL(name, isBlobUrl = true) {
-      return getResource(this, name, !!isBlobUrl);
+    GM_getResourceURL(name, isBlobUrl) {
+      return getResource(this, name, !!isBlobUrl, isBlobUrl === undefined);
     },
     GM_registerMenuCommand(cap, func) {
       const { id } = this;
@@ -205,22 +205,23 @@ function webAddElement(parent, tag, attrs, context) {
   ));
 }
 
-function getResource(context, name, isBlob) {
+function getResource(context, name, isBlob, isBlobAuto) {
+  let res;
   const { id, resCache, resources } = context;
   const key = resources[name];
-  const bucketKey = isBlob == null ? 0 : 1 + isBlob;
   if (key) {
-    let res = ensureNestedProp(resCache, bucketKey, key, false);
+    // data URIs aren't cached in bridge, so we'll send them
+    const isData = key::slice(0, 5) === 'data:';
+    const bucketKey = isBlob == null ? 0 : 1 + (isBlob = isBlobAuto ? !isData : isBlob);
+    res = isData && isBlob === false || ensureNestedProp(resCache, bucketKey, key, false);
     if (!res) {
-      bridge.call('GetResource', { id, isBlob, key }, context, null, response => {
-        res = response;
-      });
+      res = bridge.call('GetResource', { id, isBlob, key, raw: isData && key }, context);
       if (res !== true && isBlob) {
         // Creating Blob URL in page context to make it accessible for page userscripts
         res = createObjectURL(res);
       }
       ensureNestedProp(resCache, bucketKey, key, res);
     }
-    return resolveOrReturn(context, res === true ? key : res);
   }
+  return resolveOrReturn(context, res === true ? key : res);
 }

+ 14 - 20
src/options/index.js

@@ -42,7 +42,7 @@ async function initScript(script) {
   ].filter(Boolean).join('\n');
   const name = script.custom.name || localeName;
   const lowerName = name.toLowerCase();
-  script.$cache = { search, name, lowerName };
+  script.$cache = { search, name, lowerName, size: '', sizes: '', sizeNum: 0 };
   if (!await loadScriptIcon(script, store.cache)) {
     script.safeIcon = `/public/images/icon${
       store.HiDPI ? 128 : script.config.removed && 32 || 38
@@ -78,27 +78,21 @@ async function initScriptAndSize(script) {
   return res;
 }
 
-export async function loadData() {
-  const id = store.route.paths[1];
-  const params = id ? [+id].filter(Boolean) : null;
-  const [
-    { cache, scripts, sync },
-    sizes,
-  ] = await requestData(params);
-  store.cache = cache;
-  scripts.forEach(initScript);
-  sizes.forEach((sz, i) => initSize(sz, scripts[i]));
-  store.scripts = scripts;
-  store.sync = sync;
-  store.loading = false;
+export function loadData() {
+  const id = +store.route.paths[1];
+  return requestData(id ? [id] : null)
+  .catch(id ? (() => requestData()) : console.error);
 }
 
-function requestData(ids) {
-  return Promise.all([
-    sendCmdDirectly('GetData', ids, { retry: true }),
-    sendCmdDirectly('GetSizes', ids, { retry: true }),
-    options.ready,
-  ]).catch(ids ? (() => requestData()) : console.error);
+async function requestData(ids) {
+  const getDataP = sendCmdDirectly('GetData', ids, { retry: true });
+  const getSizesP = sendCmdDirectly('GetSizes', ids, { retry: true });
+  const [data] = await Promise.all([getDataP, options.ready]);
+  const { scripts } = data;
+  scripts.forEach(initScript);
+  getSizesP.then(sizes => sizes.forEach((sz, i) => initSize(sz, scripts[i])));
+  Object.assign(store, data);
+  store.loading = false;
 }
 
 function initMain() {