Parcourir la source

Merge branch 'master' into feat/vue3

tophf il y a 3 ans
Parent
commit
1aa32559d5

+ 4 - 2
src/background/utils/db.js

@@ -316,7 +316,7 @@ async function getScriptEnv(scripts, sizing) {
     ]) {
       list.forEach(key => {
         key = pathMap[key] || key;
-        if (key && !envStart[name].includes(key)) {
+        if (key && !(name === ENV_CACHE_KEYS && envStart[name].includes(key))) {
           env[name].push(key);
           (depsMap[key] || (depsMap[key] = [])).push(id);
         }
@@ -325,13 +325,15 @@ async function getScriptEnv(scripts, sizing) {
     /** @namespace VMInjectedScript */
     env[ENV_SCRIPTS].push(sizing ? script : { ...script, runAt });
   });
+  // Starting to read it before the potentially huge envDelayed
+  const envStartData = await readEnvironmentData(envStart);
   if (envDelayed.ids.length) {
     envDelayed.promise = readEnvironmentData(envDelayed);
   }
   /** @namespace VMScriptByUrlData */
   return {
     ...envStart,
-    ...await readEnvironmentData(envStart),
+    ...envStartData,
     disabledIds,
     envDelayed,
   };

+ 1 - 1
src/injected/content/index.js

@@ -33,7 +33,7 @@ async function init() {
       : await dataPromise
   );
   const { allowCmd } = bridge;
-  createNullObj(bridge, data, [
+  pickIntoNullObj(bridge, data, [
     'ids',
     'injectInto',
   ]);

+ 1 - 0
src/injected/content/inject.js

@@ -291,6 +291,7 @@ function inject(item, iframeCb) {
 }
 
 function injectAll(runAt) {
+  if (process.env.DEBUG) throwIfProtoPresent(realms);
   for (const realm in realms) { /* proto is null */// eslint-disable-line guard-for-in
     const realmData = realms[realm];
     const items = realmData.lists[runAt];

+ 1 - 1
src/injected/content/requests.js

@@ -112,7 +112,7 @@ async function revokeBlobAfterTimeout(url) {
 
 /** ArrayBuffer/Blob in Chrome incognito is transferred in string chunks */
 function receiveAllChunks(req, msg) {
-  createNullObj(req, msg, ['dataSize', 'contentType']);
+  pickIntoNullObj(req, msg, ['dataSize', 'contentType']);
   req.arr = new SafeUint8Array(req.dataSize);
   processChunk(req, msg.data.response, 0);
   return !req.gotChunks

+ 27 - 21
src/injected/safe-globals-injected.js

@@ -23,6 +23,11 @@ export const CALLBACK_ID = '__CBID';
 const { toString: numberToString } = 0;
 const { toString: URLToString } = URL[PROTO];
 
+export const throwIfProtoPresent = process.env.DEBUG && (obj => {
+  if (!obj || obj.__proto__) { // eslint-disable-line no-proto
+    throw 'proto is not null';
+  }
+});
 export const isFunction = val => typeof val === 'function';
 export const isObject = val => val !== null && typeof val === 'object';
 export const isPromise = val => {
@@ -55,36 +60,37 @@ export const setOwnProp = (obj, key, value, mutable = true) => (
 
 export const vmOwnFuncToString = () => '[Violentmonkey property]';
 
+/** `dst` must have a null proto */
+export const pickIntoNullObj = (dst, src, keys) => {
+  if (process.env.DEBUG) throwIfProtoPresent(dst);
+  if (src) {
+    keys::forEach(key => {
+      if (src::hasOwnProperty(key)) {
+        dst[key] = src[key];
+      }
+    });
+  }
+  return dst;
+};
+
 /**
  * Helps avoid interception via `Object.prototype`.
- * @param {Object} [dst] - target object to clear the prototype or to pick into
+ * @param {Object} [base] - source object to copy as a null-proto object
  * @param {Object} [src] - source object to pick from
  * @param {string[]} [keys] - all keys will be picked otherwise
- * @returns {Object} `dst` if it's already without prototype, a new object otherwise
+ * @returns {Object} `base` if it's already without prototype, a new object otherwise
  */
-export const createNullObj = (dst, src, keys) => {
-  const empty = (!dst || dst.__proto__) && { __proto__: null }; // eslint-disable-line no-proto
-  if (!dst) {
-    dst = empty;
-  } else if (empty) {
-    dst = assign(empty, dst);
-  }
-  if (src) {
-    if (keys) {
-      keys::forEach(key => {
-        if (src::hasOwnProperty(key)) {
-          dst[key] = src[key];
-        }
-      });
-    } else {
-      assign(dst, src);
-    }
-  }
-  return dst;
+export const createNullObj = (base, src, keys) => {
+  // eslint-disable-next-line no-proto
+  const res = { __proto__: null };
+  if (base || src && !keys) assign(res, base, src);
+  if (src && keys) pickIntoNullObj(res, src, keys);
+  return res;
 };
 
 // WARNING! `obj` must use __proto__:null
 export const ensureNestedProp = (obj, bucketId, key, defaultValue) => {
+  if (process.env.DEBUG) throwIfProtoPresent(obj);
   const bucket = obj[bucketId] || (
     obj[bucketId] = createNullObj()
   );

+ 1 - 1
src/injected/web/gm-api-wrapper.js

@@ -1,7 +1,7 @@
 import bridge from './bridge';
 import { makeGmApi } from './gm-api';
 import { makeGlobalWrapper } from './gm-global-wrapper';
-import { makeComponentUtils, safeConcat } from './util-web';
+import { makeComponentUtils, safeConcat } from './util';
 
 /** Name in Greasemonkey4 -> name in GM */
 const GM4_ALIAS = {

+ 4 - 3
src/injected/web/gm-api.js

@@ -5,7 +5,7 @@ import { onTabCreate } from './tabs';
 import { onRequestCreate } from './requests';
 import { onNotificationCreate } from './notifications';
 import { decodeValue, dumpValue, loadValues, changeHooks } from './gm-values';
-import { jsonDump } from './util-web';
+import { jsonDump } from './util';
 
 export function makeGmApi() {
   return {
@@ -63,6 +63,7 @@ export function makeGmApi() {
     GM_removeValueChangeListener(listenerId) {
       const keyHooks = changeHooks[this.id];
       if (!keyHooks) return;
+      if (process.env.DEBUG) throwIfProtoPresent(keyHooks);
       for (const key in keyHooks) { /* proto is null */// eslint-disable-line guard-for-in
         const hooks = keyHooks[key];
         if (listenerId in hooks) {
@@ -102,7 +103,7 @@ export function makeGmApi() {
       } else if (arg1) {
         name = arg1.name;
         onload = arg1.onload;
-        createNullObj(opts, arg1, [
+        pickIntoNullObj(opts, arg1, [
           'url',
           'headers',
           'timeout',
@@ -124,7 +125,7 @@ export function makeGmApi() {
       return onRequestCreate(opts, this, name);
     },
     GM_xmlhttpRequest(opts) {
-      return onRequestCreate(opts, this);
+      return onRequestCreate(createNullObj(opts), this);
     },
     /**
      * Bypasses site's CSP for inline `style`, `link`, and `script`.

+ 3 - 1
src/injected/web/gm-global-wrapper.js

@@ -1,6 +1,6 @@
 import { INJECT_CONTENT } from '../util';
 import bridge from './bridge';
-import { FastLookup, safeConcat } from './util-web';
+import { FastLookup, safeConcat } from './util';
 
 /** The index strings that look exactly like integers can't be forged
  * but for example '011' doesn't look like 11 so it's allowed */
@@ -135,6 +135,7 @@ const boundMethods = {
   webkitResolveLocalFileSystemURL: MAYBE,
 };
 
+if (process.env.DEBUG) throwIfProtoPresent(unforgeables);
 for (const name in unforgeables) { /* proto is null */// eslint-disable-line guard-for-in
   let thisObj;
   let info = (
@@ -241,6 +242,7 @@ export function makeGlobalWrapper(local) {
       delete desc.set;
       desc.value = wrapper;
     }
+    if (process.env.DEBUG) throwIfProtoPresent(desc);
     /* proto is already null */// eslint-disable-next-line no-restricted-syntax
     defineProperty(local, name, desc);
   }

+ 2 - 1
src/injected/web/requests.js

@@ -9,8 +9,9 @@ bridge.addHandlers({
   },
 });
 
+/** `opts` must already have a null proto */
 export function onRequestCreate(opts, context, fileName) {
-  opts = createNullObj(opts);
+  if (process.env.DEBUG) throwIfProtoPresent(opts);
   let { url } = opts;
   if (url && !isString(url)) { // USVString in XMLHttpRequest spec calls ToString
     try {

+ 1 - 0
src/injected/web/util-web.js → src/injected/web/util.js

@@ -66,6 +66,7 @@ export const FastLookup = (hubs = createNullObj()) => {
     },
     clone() {
       const clone = createNullObj();
+      if (process.env.DEBUG) throwIfProtoPresent(clone);
       for (const group in hubs) { /* proto is null */// eslint-disable-line guard-for-in
         clone[group] = createNullObj(hubs[group]);
       }

+ 5 - 1
src/options/views/edit/index.vue

@@ -307,11 +307,15 @@ export default {
           isNew: !id,
           message: '',
         });
+        const newId = res?.where?.id;
         savedSettings = deepCopy(settings);
         codeComponent.cm.markClean();
         this.codeDirty = false; // triggers onChange which sets canSave
         this.canSave = false; // ...and set it explicitly in case codeDirty was false
-        if (res?.where?.id) this.script = res.update;
+        if (newId) {
+          this.script = res.update;
+          window.history.replaceState(null, this.scriptName, `#scripts/${newId}`);
+        }
       } catch (err) {
         showConfirmation(`${err.message || err}`, {
           cancel: false,

+ 1 - 1
test/injected/helpers.test.js

@@ -1,4 +1,4 @@
-import { jsonDump } from '@/injected/web/util-web';
+import { jsonDump } from '@/injected/web/util';
 
 test('jsonDump', () => {
   const sameChildObj = { foo: 1 };