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 | |
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')
-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)); } /** |