aboutsummaryrefslogtreecommitdiff
path: root/ext
diff options
context:
space:
mode:
authortoasted-nutbread <toasted-nutbread@users.noreply.github.com>2023-12-19 00:33:38 -0500
committerGitHub <noreply@github.com>2023-12-19 05:33:38 +0000
commit1ced9aafc00c10992bab8bd3f1b6b1397f05b7b9 (patch)
tree305bb2b3bfc7fc3b051ee1cd3d1c35f442af0de4 /ext
parent5f96276fda93dcad39f2165fd3c8d890aa5f9be5 (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.js15
-rw-r--r--ext/js/comm/anki-connect.js3
-rw-r--r--ext/js/comm/api.js11
-rw-r--r--ext/js/comm/cross-frame-api.js4
-rw-r--r--ext/js/core.js2
-rw-r--r--ext/js/core/json.js40
-rw-r--r--ext/js/data/options-util.js11
-rw-r--r--ext/js/dom/sandbox/css-style-applier.js7
-rw-r--r--ext/js/input/hotkey-help-controller.js3
-rw-r--r--ext/js/language/deinflector.js2
-rw-r--r--ext/js/language/dictionary-importer.js24
-rw-r--r--ext/js/media/audio-downloader.js6
-rw-r--r--ext/js/pages/settings/backup-controller.js4
-rw-r--r--ext/js/pages/settings/generic-setting-controller.js3
-rw-r--r--ext/js/templates/sandbox/template-renderer-frame-api.js3
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));
}
/**