Prechádzať zdrojové kódy

fix #2108: handle invalid/empty script template

tophf 1 rok pred
rodič
commit
17bd8f8ef6

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

@@ -604,7 +604,7 @@ function parseMetaWithErrors(src) {
   const isObj = isObject(src);
   const custom = isObj && src.custom || getDefaultCustom();
   const errors = [];
-  const meta = parseMeta(isObj ? src.code : src, false, errors);
+  const meta = parseMeta(isObj ? src.code : src, { errors });
   if (meta) {
     testerBatch(errors);
     testScript('', { meta, custom });

+ 4 - 5
src/background/utils/options.js

@@ -1,6 +1,6 @@
 import { debounce, initHooks, normalizeKeys, sendCmd } from '@/common';
 import { deepCopy, deepEqual, objectGet, objectSet } from '@/common/object';
-import defaults from '@/common/options-defaults';
+import defaults, { kScriptTemplate } from '@/common/options-defaults';
 import { addOwnCommands, init } from './init';
 import storage from './storage';
 
@@ -24,7 +24,6 @@ addOwnCommands({
 const options = {};
 export const kOptions = 'options';
 export const kVersion = 'version';
-const TPL_KEY = 'scriptTemplate';
 const TPL_OLD_VAL = `\
 // ==UserScript==
 // @name New Script
@@ -48,11 +47,11 @@ export function initOptions(data) {
   if (!options[kVersion]) {
     setOption(kVersion, 1);
   }
-  if (options[TPL_KEY] === TPL_OLD_VAL) {
-    options[TPL_KEY] = defaults[TPL_KEY]; // will be detected by omitDefaultValue below
+  if (options[kScriptTemplate] === TPL_OLD_VAL) {
+    options[kScriptTemplate] = defaults[kScriptTemplate]; // will be detected by omitDefaultValue below
   }
   if (Object.keys(options).map(omitDefaultValue).some(Boolean)) {
-    delete options[`${TPL_KEY}Edited`]; // TODO: remove this in 2023
+    delete options[`${kScriptTemplate}Edited`]; // TODO: remove this in 2023
     writeOptionsLater();
   }
 }

+ 27 - 7
src/background/utils/script.js

@@ -1,13 +1,14 @@
 import {
-  encodeFilename, getFullUrl, getScriptHome, getScriptSupportUrl, noop,
+  encodeFilename, getFullUrl, getScriptHome, getScriptSupportUrl, i18n, noop,
 } from '@/common';
 import {
   __CODE, HOMEPAGE_URL, INFERRED, METABLOCK_RE, SUPPORT_URL, TL_AWAIT, UNWRAP,
 } from '@/common/consts';
 import { formatDate } from '@/common/date';
 import { mapEntry } from '@/common/object';
+import defaults, { kScriptTemplate } from '@/common/options-defaults';
 import { addOwnCommands, commands } from './init';
-import { getOption } from './options';
+import { getOption, hookOptionsInit } from './options';
 import { injectableRe } from './tabs';
 
 addOwnCommands({
@@ -22,6 +23,16 @@ addOwnCommands({
   },
 });
 
+hookOptionsInit((changes, firstRun) => {
+  if (!firstRun && kScriptTemplate in changes) {
+    const errors = [];
+    const tpl = changes[kScriptTemplate];
+    const meta = !tpl /*empty = default*/ || parseMeta(tpl, { errors });
+    if (!meta) errors.unshift(i18n('msgInvalidScript'));
+    if (errors.length) throw errors;
+  }
+});
+
 /** @return {boolean|?RegExpExecArray} */
 export const matchUserScript = text => !/^\s*</.test(text) /*HTML*/ && METABLOCK_RE.exec(text);
 
@@ -69,11 +80,20 @@ const META_ITEM_RE = /(?:^|\n)(.*?)\/\/([\x20\t]*)(@\S+)(.*)/g;
 export const ERR_META_SPACE_BEFORE = 'Unexpected text before "//" in ';
 export const ERR_META_SPACE_INSIDE = 'Expected a single space after "//" in ';
 
-export function parseMeta(code, includeMatchedString, errors) {
+/**
+ * @param {string} code
+ * @param {object} [opts]
+ * @param {Array} [opts.errors] - to collect errors
+ * @param {boolean} [opts.retDefault] - returns the default empty meta if no meta is found
+ * @param {boolean} [opts.retMetaStr] - adds the matched part as [__CODE] prop in result
+ * @return {VMScript.Meta | false}
+ */
+export function parseMeta(code, { errors, retDefault, retMetaStr } = {}) {
   // initialize meta
   const meta = metaTypes::mapEntry(value => value.default());
   const match = matchUserScript(code);
-  if (!match) return false; // TODO: `return;` + null check in all callers?
+  if (!match) return retDefault ? meta : false;
+  // TODO: use `null` instead of `false` + null check in all callers?
   if (errors) checkMetaItemErrors(match, 1, errors);
   let parts;
   while ((parts = META_ITEM_RE.exec(match[4]))) {
@@ -90,7 +110,7 @@ export function parseMeta(code, includeMatchedString, errors) {
   if (errors) checkMetaItemErrors(match, 5, errors);
   meta.resources = meta.resource;
   delete meta.resource;
-  if (includeMatchedString) meta[__CODE] = match[0];
+  if (retMetaStr) meta[__CODE] = match[0];
   return meta;
 }
 
@@ -124,7 +144,7 @@ export function newScript(data) {
     name: '',
     ...data,
   };
-  const code = getOption('scriptTemplate')
+  const code = (getOption(kScriptTemplate) || defaults[kScriptTemplate])
   .replace(/{{(\w+)(?::(.+?))?}}/g, (str, name, format) => state[name] ?? (
     name !== 'date' ? str
       : format ? formatDate(format)
@@ -136,7 +156,7 @@ export function newScript(data) {
       enabled: 1,
       shouldUpdate: 1,
     },
-    meta: parseMeta(code),
+    meta: parseMeta(code, { retDefault: true }),
     props: {},
   };
   return { script, code };

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

@@ -107,7 +107,7 @@ async function downloadUpdate(script, urls, opts) {
   announce(i18n('msgCheckingForUpdate'));
   try {
     const { data } = await requestNewer(updateURL, { ...FAST_CHECK, ...opts }) || {};
-    const { version, [__CODE]: metaStr } = data ? parseMeta(data, true) : {};
+    const { version, [__CODE]: metaStr } = data ? parseMeta(data, { retMetaStr: true }) : {};
     if (compareVersion(meta.version, version) >= 0) {
       announce(i18n('msgNoUpdate'), { [kChecking]: false });
     } else if (!downloadURL) {

+ 2 - 1
src/common/options-defaults.js

@@ -5,6 +5,7 @@ export const kFiltersPopup = 'filtersPopup';
 export const kKillTrailingSpaceOnSave = 'killTrailingSpaceOnSave';
 export const kPopupWidth = 'popupWidth';
 export const kShowTrailingSpace = 'showTrailingSpace';
+export const kScriptTemplate = 'scriptTemplate';
 export const kUpdateEnabledScriptsOnly = 'updateEnabledScriptsOnly';
 const defaultsValueEditor = {
   [kAutocompleteOnTyping]: 100,
@@ -84,7 +85,7 @@ export default {
   editorWindow: false, // whether popup opens editor in a new window
   editorWindowPos: {}, // { left, top, width, height }
   editorWindowSimple: true, // whether to open a simplified popup or a normal browser window
-  scriptTemplate: `\
+  [kScriptTemplate]: `\
 // ==UserScript==
 // @name        New script {{name}}
 // @namespace   ${VIOLENTMONKEY} Scripts

+ 17 - 5
src/common/ui/setting-text.vue

@@ -17,7 +17,10 @@
             :disabled="disabled || !canReset"/>
     <!-- DANGER! Keep the error tag in one line to keep the space which ensures the first word
          is selected correctly without the preceding button's text on double-click. -->
-    <slot/> <span class="error text-red sep" v-text="error" v-if="json"/>
+    <slot/> <template v-if="error">
+      <span v-if="typeof error === 'string'" class="error text-red sep" v-text="error"/>
+      <ol v-else class="text-red"><li v-for="e in error" :key="e" v-text="e"/></ol>
+    </template>
   </div>
 </template>
 
@@ -31,7 +34,7 @@ const handleJSON = val => JSON.stringify(val, null, '  ');
 </script>
 
 <script setup>
-import { onBeforeUnmount, ref, watch } from 'vue';
+import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
 import { i18n } from '@/common';
 import { getUnloadSentry } from '@/common/router';
 import { deepEqual, objectGet } from '../object';
@@ -46,6 +49,7 @@ const props = defineProps({
   name: String,
   json: Boolean,
   disabled: Boolean,
+  getErrors: Function,
   hasSave: {
     type: Boolean,
     default: true,
@@ -53,7 +57,7 @@ const props = defineProps({
   hasReset: Boolean,
   rows: Number,
 });
-const emit = defineEmits(['bg-error', 'save']);
+const emit = defineEmits(['save']);
 const $text = ref();
 const canSave = ref();
 const canReset = ref();
@@ -106,6 +110,9 @@ watch(text, str => {
   canSave.value = _canSave = _isDirty && !err;
   if (_canSave && !props.hasSave) onSave(); // Auto save if there is no `Save` button
 });
+onMounted(async () => {
+  error.value = await props.getErrors?.();
+});
 onBeforeUnmount(() => {
   revoke();
   toggleUnloadSentry(false);
@@ -134,8 +141,13 @@ function onReset() {
     }
   }
 }
-function bgError(err) {
-  emit('bg-error', err && JSON.parse(err.message));
+function bgError({ message: m } = {}) {
+  if (m) try { m = JSON.parse(m); } catch {/**/}
+  error.value = m && (
+    m.length <= 1 && Array.isArray(m)
+      ? m[0]
+      : m
+  );
 }
 </script>
 

+ 2 - 2
src/options/views/tab-settings/index.vue

@@ -94,7 +94,7 @@
                      :key="i" :is="i % 2 ? 'code' : 'span'"
           /> <vm-date-info/><!--DANGER! Using the same line to preserve the space-->
         </p>
-        <setting-text name="scriptTemplate" has-reset/>
+        <setting-text :name="kScriptTemplate" has-reset/>
       </section>
 
       <vm-blacklist />
@@ -112,7 +112,7 @@
 import { i18n } from '@/common';
 import { KNOWN_INJECT_INTO, VM_HOME } from '@/common/consts';
 import options from '@/common/options';
-import { kUpdateEnabledScriptsOnly } from '@/common/options-defaults';
+import { kScriptTemplate, kUpdateEnabledScriptsOnly } from '@/common/options-defaults';
 import { keyboardService } from '@/common/keyboard';
 import { EXTERNAL_LINK_PROPS, focusMe, getActiveElement } from '@/common/ui';
 import { hookSettingsForUI } from '@/common/ui/util';

+ 3 - 11
src/options/views/tab-settings/vm-blacklist-body.vue

@@ -1,10 +1,7 @@
 <template>
-  <p v-text="props.desc" class="mt-1"/>
+  <p v-text="desc" class="mt-1"/>
   <div class="flex flex-wrap">
-    <setting-text :name="props.name" class="flex-1" @bgError="errors = $event"/>
-    <ol v-if="errors" class="text-red">
-      <li v-for="e in errors" :key="e" v-text="e"/>
-    </ol>
+    <setting-text :name="name" class="flex-1" :get-errors="getErrors"/>
   </div>
 </template>
 
@@ -15,15 +12,10 @@ import { ERRORS } from '@/common/consts';
 
 <script setup>
 import SettingText from '@/common/ui/setting-text';
-import { onMounted, ref } from 'vue';
 
-const errors = ref();
 const props = defineProps({
   name: String,
   desc: String,
 });
-
-onMounted(async () => {
-  errors.value = await sendCmdDirectly('Storage', ['base', 'getOne', props.name + ERRORS]);
-});
+const getErrors = () => sendCmdDirectly('Storage', ['base', 'getOne', props.name + ERRORS]);
 </script>

+ 1 - 1
test/background/script.test.js

@@ -50,7 +50,7 @@ test('parseMetaIrregularities', () => {
   };
   const parseWeirdMeta = code => {
     const errors = [];
-    const res = parseMeta(code, false, errors);
+    const res = parseMeta(code, { errors });
     return errors.length ? [res, ...errors] : res;
   };
   expect(parseWeirdMeta(`\