diff options
| author | toasted-nutbread <toasted-nutbread@users.noreply.github.com> | 2023-12-19 00:33:38 -0500 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-12-19 05:33:38 +0000 | 
| commit | 1ced9aafc00c10992bab8bd3f1b6b1397f05b7b9 (patch) | |
| tree | 305bb2b3bfc7fc3b051ee1cd3d1c35f442af0de4 /ext/js | |
| parent | 5f96276fda93dcad39f2165fd3c8d890aa5f9be5 (diff) | |
Make JSON.parse usage safer (#373)
* Make JSON.parse usage safer
* Fix any type
* Add readResponseJson
* Use readResponseJson
* Additional updates
* Rename files
* Add types
Diffstat (limited to 'ext/js')
| -rw-r--r-- | ext/js/background/backend.js | 15 | ||||
| -rw-r--r-- | ext/js/comm/anki-connect.js | 3 | ||||
| -rw-r--r-- | ext/js/comm/api.js | 11 | ||||
| -rw-r--r-- | ext/js/comm/cross-frame-api.js | 4 | ||||
| -rw-r--r-- | ext/js/core.js | 2 | ||||
| -rw-r--r-- | ext/js/core/json.js | 40 | ||||
| -rw-r--r-- | ext/js/data/options-util.js | 11 | ||||
| -rw-r--r-- | ext/js/dom/sandbox/css-style-applier.js | 7 | ||||
| -rw-r--r-- | ext/js/input/hotkey-help-controller.js | 3 | ||||
| -rw-r--r-- | ext/js/language/deinflector.js | 2 | ||||
| -rw-r--r-- | ext/js/language/dictionary-importer.js | 24 | ||||
| -rw-r--r-- | ext/js/media/audio-downloader.js | 6 | ||||
| -rw-r--r-- | ext/js/pages/settings/backup-controller.js | 4 | ||||
| -rw-r--r-- | ext/js/pages/settings/generic-setting-controller.js | 3 | ||||
| -rw-r--r-- | ext/js/templates/sandbox/template-renderer-frame-api.js | 3 | 
15 files changed, 94 insertions, 44 deletions
| diff --git a/ext/js/background/backend.js b/ext/js/background/backend.js index 20c7a189..09edbd6e 100644 --- a/ext/js/background/backend.js +++ b/ext/js/background/backend.js @@ -24,6 +24,7 @@ import {ClipboardReader} from '../comm/clipboard-reader.js';  import {Mecab} from '../comm/mecab.js';  import {clone, deferPromise, generateId, invokeMessageHandler, isObject, log, promiseTimeout} from '../core.js';  import {ExtensionError} from '../core/extension-error.js'; +import {parseJson, readResponseJson} from '../core/json.js';  import {AnkiUtil} from '../data/anki-util.js';  import {OptionsUtil} from '../data/options-util.js';  import {PermissionsUtil} from '../data/permissions-util.js'; @@ -291,7 +292,8 @@ export class Backend {                  log.error(e);              } -            const deinflectionReasons = /** @type {import('deinflector').ReasonsRaw} */ (await this._fetchJson('/data/deinflect.json')); +            /** @type {import('deinflector').ReasonsRaw} */ +            const deinflectionReasons = await this._fetchJson('/data/deinflect.json');              this._translator.prepare(deinflectionReasons);              await this._optionsUtil.prepare(); @@ -764,6 +766,7 @@ export class Backend {          const frameId = sender.frameId;          const id = generateId(16); +        /** @type {import('cross-frame-api').ActionPortDetails} */          const details = {              name: 'action-port',              id @@ -908,11 +911,13 @@ export class Backend {              throw new Error('Port does not have an associated frame ID');          } +        /** @type {import('cross-frame-api').CrossFrameCommunicationPortDetails} */          const sourceDetails = {              name: 'cross-frame-communication-port',              otherTabId: targetTabId,              otherFrameId: targetFrameId          }; +        /** @type {import('cross-frame-api').CrossFrameCommunicationPortDetails} */          const targetDetails = {              name: 'cross-frame-communication-port',              otherTabId: sourceTabId, @@ -1530,7 +1535,8 @@ export class Backend {                              hasStarted = true;                              port.onMessage.removeListener(onMessage); -                            const messageData = JSON.parse(messageString); +                            /** @type {{action: string, params?: import('core').SerializableObject}} */ +                            const messageData = parseJson(messageString);                              messageString = null;                              onMessageComplete(messageData);                          } @@ -2062,12 +2068,13 @@ export class Backend {      }      /** +     * @template [T=unknown]       * @param {string} url -     * @returns {Promise<unknown>} +     * @returns {Promise<T>}       */      async _fetchJson(url) {          const response = await this._fetchAsset(url); -        return await response.json(); +        return await readResponseJson(response);      }      /** diff --git a/ext/js/comm/anki-connect.js b/ext/js/comm/anki-connect.js index bd9a69a2..fa5543d5 100644 --- a/ext/js/comm/anki-connect.js +++ b/ext/js/comm/anki-connect.js @@ -17,6 +17,7 @@   */  import {ExtensionError} from '../core/extension-error.js'; +import {parseJson} from '../core/json.js';  import {AnkiUtil} from '../data/anki-util.js';  /** @@ -419,7 +420,7 @@ export class AnkiConnect {          let result;          try {              responseText = await response.text(); -            result = JSON.parse(responseText); +            result = parseJson(responseText);          } catch (e) {              const error = new ExtensionError('Invalid Anki response');              error.data = {action, params, status: response.status, responseText, originalError: e}; diff --git a/ext/js/comm/api.js b/ext/js/comm/api.js index 35a66d9e..43f707e2 100644 --- a/ext/js/comm/api.js +++ b/ext/js/comm/api.js @@ -18,6 +18,7 @@  import {deferPromise} from '../core.js';  import {ExtensionError} from '../core/extension-error.js'; +import {parseJson} from '../core/json.js';  export class API {      /** @@ -433,6 +434,7 @@ export class API {          return new Promise((resolve, reject) => {              /** @type {?import('core').Timeout} */              let timer = null; +            /** @type {import('core').DeferredPromiseDetails<import('api').CreateActionPortResult>} */              const portDetails = deferPromise();              /** @@ -441,8 +443,9 @@ export class API {              const onConnect = async (port) => {                  try {                      const {name: expectedName, id: expectedId} = await portDetails.promise; -                    const {name, id} = JSON.parse(port.name); -                    if (name !== expectedName || id !== expectedId || timer === null) { return; } +                    /** @type {import('cross-frame-api').PortDetails} */ +                    const portDetails2 = parseJson(port.name); +                    if (portDetails2.name !== expectedName || portDetails2.id !== expectedId || timer === null) { return; }                  } catch (e) {                      return;                  } @@ -470,7 +473,9 @@ export class API {              timer = setTimeout(() => onError(new Error('Timeout')), timeout);              chrome.runtime.onConnect.addListener(onConnect); -            this._invoke('createActionPort').then(portDetails.resolve, onError); +            /** @type {Promise<import('api').CreateActionPortResult>} */ +            const createActionPortResult = this._invoke('createActionPort'); +            createActionPortResult.then(portDetails.resolve, onError);          });      } diff --git a/ext/js/comm/cross-frame-api.js b/ext/js/comm/cross-frame-api.js index 3ac38cf2..0d3f3275 100644 --- a/ext/js/comm/cross-frame-api.js +++ b/ext/js/comm/cross-frame-api.js @@ -18,6 +18,7 @@  import {EventDispatcher, EventListenerCollection, invokeMessageHandler, log} from '../core.js';  import {ExtensionError} from '../core/extension-error.js'; +import {parseJson} from '../core/json.js';  import {yomitan} from '../yomitan.js';  /** @@ -377,9 +378,10 @@ export class CrossFrameAPI {       */      _onConnect(port) {          try { +            /** @type {import('cross-frame-api').PortDetails} */              let details;              try { -                details = JSON.parse(port.name); +                details = parseJson(port.name);              } catch (e) {                  return;              } diff --git a/ext/js/core.js b/ext/js/core.js index c95eae01..d16a2099 100644 --- a/ext/js/core.js +++ b/ext/js/core.js @@ -47,7 +47,7 @@ export function stringReverse(string) {  }  /** - * Creates a deep clone of an object or value. This is similar to `JSON.parse(JSON.stringify(value))`. + * Creates a deep clone of an object or value. This is similar to `parseJson(JSON.stringify(value))`.   * @template T   * @param {T} value The value to clone.   * @returns {T} A new clone of the value. diff --git a/ext/js/core/json.js b/ext/js/core/json.js new file mode 100644 index 00000000..a031f84e --- /dev/null +++ b/ext/js/core/json.js @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2023  Yomitan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program.  If not, see <https://www.gnu.org/licenses/>. + */ + +/** + * This function is used to ensure more safe usage of `JSON.parse`. + * By default, `JSON.parse` returns a value with type `any`, which is easy to misuse. + * By changing the default to `unknown` and allowing it to be templatized, + * this improves how the return type is used. + * @template [T=unknown] + * @param {string} value + * @returns {T} + */ +export function parseJson(value) { +    return JSON.parse(value); +} + +/** + * This function is used to ensure more safe usage of `Response.json`, + * which returns the `any` type. + * @template [T=unknown] + * @param {Response} response + * @returns {Promise<T>} + */ +export async function readResponseJson(response) { +    return await response.json(); +} diff --git a/ext/js/data/options-util.js b/ext/js/data/options-util.js index c3c0e685..a17763e9 100644 --- a/ext/js/data/options-util.js +++ b/ext/js/data/options-util.js @@ -17,6 +17,7 @@   */  import {escapeRegExp, isObject} from '../core.js'; +import {parseJson, readResponseJson} from '../core/json.js';  import {TemplatePatcher} from '../templates/template-patcher.js';  import {JsonSchema} from './json-schema.js'; @@ -30,7 +31,8 @@ export class OptionsUtil {      /** */      async prepare() { -        const schema = /** @type {import('json-schema').Schema} */ (await this._fetchJson('/data/schemas/options-schema.json')); +        /** @type {import('json-schema').Schema} */ +        const schema = await this._fetchJson('/data/schemas/options-schema.json');          this._optionsSchema = new JsonSchema(schema);      } @@ -115,7 +117,7 @@ export class OptionsUtil {                      }                  });              }); -            options = JSON.parse(optionsStr); +            options = parseJson(optionsStr);          } catch (e) {              // NOP          } @@ -477,12 +479,13 @@ export class OptionsUtil {      }      /** +     * @template [T=unknown]       * @param {string} url -     * @returns {Promise<unknown>} +     * @returns {Promise<T>}       */      async _fetchJson(url) {          const response = await this._fetchGeneric(url); -        return await response.json(); +        return await readResponseJson(response);      }      /** diff --git a/ext/js/dom/sandbox/css-style-applier.js b/ext/js/dom/sandbox/css-style-applier.js index 200fd05f..6925f263 100644 --- a/ext/js/dom/sandbox/css-style-applier.js +++ b/ext/js/dom/sandbox/css-style-applier.js @@ -16,6 +16,8 @@   * along with this program.  If not, see <https://www.gnu.org/licenses/>.   */ +import {readResponseJson} from '../../core/json.js'; +  /**   * This class is used to apply CSS styles to elements using a consistent method   * that is the same across different browsers. @@ -99,8 +101,9 @@ export class CssStyleApplier {      /**       * Fetches and parses a JSON file. +     * @template [T=unknown]       * @param {string} url The URL to the file. -     * @returns {Promise<*>} A JSON object. +     * @returns {Promise<T>} A JSON object.       * @throws {Error} An error is thrown if the fetch fails.       */      async _fetchJsonAsset(url) { @@ -115,7 +118,7 @@ export class CssStyleApplier {          if (!response.ok) {              throw new Error(`Failed to fetch ${url}: ${response.status}`);          } -        return await response.json(); +        return await readResponseJson(response);      }      /** diff --git a/ext/js/input/hotkey-help-controller.js b/ext/js/input/hotkey-help-controller.js index 4a3c0264..14063d9a 100644 --- a/ext/js/input/hotkey-help-controller.js +++ b/ext/js/input/hotkey-help-controller.js @@ -17,6 +17,7 @@   */  import {isObject} from '../core.js'; +import {parseJson} from '../core/json.js';  import {yomitan} from '../yomitan.js';  import {HotkeyUtil} from './hotkey-util.js'; @@ -149,7 +150,7 @@ export class HotkeyHelpController {      _getNodeInfo(node) {          const {hotkey} = node.dataset;          if (typeof hotkey !== 'string') { return null; } -        const data = /** @type {unknown} */ (JSON.parse(hotkey)); +        const data = /** @type {unknown} */ (parseJson(hotkey));          if (!Array.isArray(data)) { return null; }          const [action, attributes, values] = data;          if (typeof action !== 'string') { return null; } diff --git a/ext/js/language/deinflector.js b/ext/js/language/deinflector.js index 676f45a1..26cc6b18 100644 --- a/ext/js/language/deinflector.js +++ b/ext/js/language/deinflector.js @@ -20,7 +20,7 @@ export class Deinflector {      /**       * @param {import('deinflector').ReasonsRaw} reasons       * @example -     * const deinflectionReasons = JSON.parse( +     * const deinflectionReasons = parseJson(       *   readFileSync(path.join('ext/data/deinflect.json')).toString(),       * );       * const deinflector = new Deinflector(deinflectionReasons); diff --git a/ext/js/language/dictionary-importer.js b/ext/js/language/dictionary-importer.js index dfbd9590..df9c48f1 100644 --- a/ext/js/language/dictionary-importer.js +++ b/ext/js/language/dictionary-importer.js @@ -26,6 +26,7 @@ import {  } from '../../lib/zip.js';  import {stringReverse} from '../core.js';  import {ExtensionError} from '../core/extension-error.js'; +import {parseJson} from '../core/json.js';  import {MediaUtil} from '../media/media-util.js';  const ajvSchemas = /** @type {import('dictionary-importer').CompiledSchemaValidators} */ (/** @type {unknown} */ (ajvSchemas0)); @@ -89,7 +90,7 @@ export class DictionaryImporter {          const indexFile2 = /** @type {import('@zip.js/zip.js').Entry} */ (indexFile);          const indexContent = await this._getData(indexFile2, new TextWriter()); -        const index = /** @type {import('dictionary-data').Index} */ (JSON.parse(indexContent)); +        const index = /** @type {import('dictionary-data').Index} */ (parseJson(indexContent));          if (!ajvSchemas.dictionaryIndex(index)) {              throw this._formatAjvSchemaError(ajvSchemas.dictionaryIndex, indexFileName); @@ -589,25 +590,6 @@ export class DictionaryImporter {      }      /** -     * @param {string} url -     * @returns {Promise<unknown>} -     */ -    async _fetchJsonAsset(url) { -        const response = await fetch(url, { -            method: 'GET', -            mode: 'no-cors', -            cache: 'default', -            credentials: 'omit', -            redirect: 'follow', -            referrerPolicy: 'no-referrer' -        }); -        if (!response.ok) { -            throw new Error(`Failed to fetch ${url}: ${response.status}`); -        } -        return await response.json(); -    } - -    /**       * @param {import('dictionary-data').TermV1} entry       * @param {string} dictionary       * @returns {import('dictionary-database').DatabaseTermEntry} @@ -730,7 +712,7 @@ export class DictionaryImporter {          const results = [];          for (const file of files) {              const content = await this._getData(file, new TextWriter()); -            const entries = /** @type {unknown} */ (JSON.parse(content)); +            const entries = /** @type {unknown} */ (parseJson(content));              startIndex = progressData.index;              this._progress(); diff --git a/ext/js/media/audio-downloader.js b/ext/js/media/audio-downloader.js index e041cc67..a9b2133b 100644 --- a/ext/js/media/audio-downloader.js +++ b/ext/js/media/audio-downloader.js @@ -18,6 +18,7 @@  import {RequestBuilder} from '../background/request-builder.js';  import {ExtensionError} from '../core/extension-error.js'; +import {readResponseJson} from '../core/json.js';  import {JsonSchema} from '../data/json-schema.js';  import {ArrayBufferUtil} from '../data/sandbox/array-buffer-util.js';  import {NativeSimpleDOMParser} from '../dom/native-simple-dom-parser.js'; @@ -272,7 +273,8 @@ export class AudioDownloader {              throw new Error(`Invalid response: ${response.status}`);          } -        const responseJson = await response.json(); +        /** @type {import('audio-downloader').CustomAudioList} */ +        const responseJson = await readResponseJson(response);          if (this._customAudioListSchema === null) {              const schema = await this._getCustomAudioListSchema(); @@ -425,6 +427,6 @@ export class AudioDownloader {              redirect: 'follow',              referrerPolicy: 'no-referrer'          }); -        return await response.json(); +        return await readResponseJson(response);      }  } diff --git a/ext/js/pages/settings/backup-controller.js b/ext/js/pages/settings/backup-controller.js index 85803077..c539bdfe 100644 --- a/ext/js/pages/settings/backup-controller.js +++ b/ext/js/pages/settings/backup-controller.js @@ -18,6 +18,7 @@  import {Dexie} from '../../../lib/dexie.js';  import {isObject, log} from '../../core.js'; +import {parseJson} from '../../core/json.js';  import {OptionsUtil} from '../../data/options-util.js';  import {ArrayBufferUtil} from '../../data/sandbox/array-buffer-util.js';  import {querySelectorNotNull} from '../../dom/query-selector.js'; @@ -427,7 +428,8 @@ export class BackupController {          if (this._optionsUtil === null) { throw new Error('OptionsUtil invalid'); }          const dataString = ArrayBufferUtil.arrayBufferUtf8Decode(await this._readFileArrayBuffer(file)); -        const data = JSON.parse(dataString); +        /** @type {import('backup-controller').BackupData} */ +        const data = parseJson(dataString);          // Type check          if (!isObject(data)) { diff --git a/ext/js/pages/settings/generic-setting-controller.js b/ext/js/pages/settings/generic-setting-controller.js index 8666614b..8268f563 100644 --- a/ext/js/pages/settings/generic-setting-controller.js +++ b/ext/js/pages/settings/generic-setting-controller.js @@ -17,6 +17,7 @@   */  import {ExtensionError} from '../../core/extension-error.js'; +import {parseJson} from '../../core/json.js';  import {DocumentUtil} from '../../dom/document-util.js';  import {DOMDataBinder} from '../../dom/dom-data-binder.js'; @@ -265,7 +266,7 @@ export class GenericSettingController {       */      _getTransformDataArray(transformRaw) {          if (typeof transformRaw === 'string') { -            const transforms = JSON.parse(transformRaw); +            const transforms = parseJson(transformRaw);              return Array.isArray(transforms) ? transforms : [transforms];          }          return []; diff --git a/ext/js/templates/sandbox/template-renderer-frame-api.js b/ext/js/templates/sandbox/template-renderer-frame-api.js index 94ebf7fe..388401f2 100644 --- a/ext/js/templates/sandbox/template-renderer-frame-api.js +++ b/ext/js/templates/sandbox/template-renderer-frame-api.js @@ -17,6 +17,7 @@   */  import {ExtensionError} from '../../core/extension-error.js'; +import {parseJson} from '../../core/json.js';  export class TemplateRendererFrameApi {      /** @@ -108,7 +109,7 @@ export class TemplateRendererFrameApi {       * @returns {T}       */      _clone(value) { -        return JSON.parse(JSON.stringify(value)); +        return parseJson(JSON.stringify(value));      }      /** |