diff --git a/package-lock.json b/package-lock.json index fc85203ce..3e8ea3c81 100644 --- a/package-lock.json +++ b/package-lock.json @@ -153,7 +153,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1979,7 +1978,6 @@ "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.0.tgz", "integrity": "sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==", "license": "MIT", - "peer": true, "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", @@ -1992,7 +1990,6 @@ "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.2.tgz", "integrity": "sha512-vvX1fsih9HledO1c9zdotZYUZnE4xV0m6i3m25s5DIfXofuprk6cRcLUZvSk3CASUbwjQX21tOGbkY2BH8TpnQ==", "license": "MIT", - "peer": true, "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.4.0", @@ -2029,7 +2026,6 @@ "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz", "integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==", "license": "MIT", - "peer": true, "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.0.0", @@ -2056,7 +2052,6 @@ "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.11.tgz", "integrity": "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==", "license": "MIT", - "peer": true, "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/lang-css": "^6.0.0", @@ -2284,7 +2279,6 @@ "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.1.tgz", "integrity": "sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ==", "license": "MIT", - "peer": true, "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", @@ -2377,7 +2371,6 @@ "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.4.tgz", "integrity": "sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==", "license": "MIT", - "peer": true, "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } @@ -2399,7 +2392,6 @@ "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.15.tgz", "integrity": "sha512-aCWjgweIIXLBHh7bY6cACvXuyrZ0xGafjQ2VInjp4RM4gMfscK5uESiNdrH0pE+e1lZr2B4ONGsjchl2KsKZzg==", "license": "MIT", - "peer": true, "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", @@ -4212,7 +4204,6 @@ "integrity": "sha512-Iax6UhrfZqJajA778c1d5DBFbSIqPOSrI34kpNIiNpWd8Jq7mFIa+Z60SQb5ZQDZuUxcCZikjz5BxinFjTkg7Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@module-federation/runtime-tools": "0.22.0", "@rspack/binding": "1.7.6", @@ -4502,7 +4493,8 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/markdown-it": { "version": "14.1.2", @@ -4519,7 +4511,8 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/mime": { "version": "1.3.5", @@ -4936,8 +4929,7 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", @@ -5014,7 +5006,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5063,7 +5054,6 @@ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -5580,7 +5570,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -5743,7 +5732,6 @@ "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@chevrotain/cst-dts-gen": "11.0.3", "@chevrotain/gast": "11.0.3", @@ -8977,7 +8965,6 @@ "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", "license": "MIT", - "peer": true, "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", @@ -10076,7 +10063,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -10197,7 +10183,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -10341,7 +10326,6 @@ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -10612,7 +10596,6 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -12039,8 +12022,7 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/tuf-js": { "version": "4.1.0", @@ -12108,7 +12090,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12411,7 +12392,6 @@ "integrity": "sha512-dRXm0a2qcHPUBEzVk8uph0xWSjV/xZxenQQbLwnwP7caQCYpqG1qddwlyEkIDkYn0K8tvmcrZ+bOrzoQ3HxCDw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", diff --git a/src/cm/lsp/workspace.ts b/src/cm/lsp/workspace.ts index d6218843b..01a071dee 100644 --- a/src/cm/lsp/workspace.ts +++ b/src/cm/lsp/workspace.ts @@ -109,7 +109,12 @@ export default class AcodeWorkspace extends Workspace { if (mode?.name) { return String(mode.name).toLowerCase(); } - } catch (_) {} + } catch (error) { + console.warn( + `[LSP:Workspace] Failed to resolve language id for ${uri}`, + error, + ); + } return "plaintext"; } diff --git a/src/lib/editorFile.js b/src/lib/editorFile.js index e5d8113bd..2863027e5 100644 --- a/src/lib/editorFile.js +++ b/src/lib/editorFile.js @@ -1000,7 +1000,12 @@ export default class EditorFile { EditorState.readOnly.of(!!value), ), }); - } catch (_) {} + } catch (error) { + console.warn( + `Failed to update read-only state for ${this.filename || this.uri}`, + error, + ); + } // Sync internal flags and header this.readOnly = !!value; @@ -1072,7 +1077,9 @@ export default class EditorFile { // Ensure any native DOM selection is cleared on blur to avoid sticky selection handles try { document.getSelection()?.removeAllRanges(); - } catch (_) {} + } catch (error) { + console.warn("Failed to clear native text selection.", error); + } } } else { editorManager.container.style.display = "none"; @@ -1322,7 +1329,9 @@ export default class EditorFile { if (activeFile?.id === this.id) { emit("file-loaded", this); } - } catch (_) {} + } catch (error) { + console.warn("Failed to emit interim file-loaded event.", error); + } try { const cacheFs = fsOperation(this.cacheFile); diff --git a/src/lib/editorManager.js b/src/lib/editorManager.js index 2e7b91e03..fb57388d4 100644 --- a/src/lib/editorManager.js +++ b/src/lib/editorManager.js @@ -106,6 +106,15 @@ async function EditorManager($header, $body) { let touchSelectionController = null; let touchSelectionSyncRaf = 0; let nativeContextMenuDisabled = null; + const recoverableWarningKeys = new Set(); + + function warnRecoverable(message, error, key) { + if (key) { + if (recoverableWarningKeys.has(key)) return; + recoverableWarningKeys.add(key); + } + console.warn(message, error); + } const setNativeContextMenuDisabled = (disabled) => { const value = !!disabled; @@ -626,7 +635,13 @@ async function EditorManager($header, $body) { try { const guess = getModeForPath(file.filename || file.name || ""); if (guess?.name) return String(guess.name).toLowerCase(); - } catch (_) {} + } catch (error) { + warnRecoverable( + `Failed to resolve language id for ${file.filename || file.name || "untitled file"}`, + error, + "language-id-resolution", + ); + } return "plaintext"; } @@ -634,11 +649,9 @@ async function EditorManager($header, $body) { const uri = context.uri || context.file?.uri; if (!uri) return null; for (const folder of addedFolder) { - try { - const base = folder?.url; - if (!base) continue; - if (uri.startsWith(base)) return base; - } catch (_) {} + const base = typeof folder?.url === "string" ? folder.url : ""; + if (!base) continue; + if (uri.startsWith(base)) return base; } return uri; } @@ -1153,7 +1166,13 @@ async function EditorManager($header, $body) { editor.dispatch({ effects: languageCompartment.reconfigure(ext || []), }); - } catch (_) {} + } catch (error) { + warnRecoverable( + "Failed to apply async language extensions.", + error, + "async-language-reconfigure", + ); + } }) .catch(() => { // ignore load errors; remain in plain text @@ -1208,7 +1227,13 @@ async function EditorManager($header, $body) { const mainIndex = sel.mainIndex ?? 0; restoreSelection(editor, { ranges, mainIndex }); } - } catch (_) {} + } catch (error) { + warnRecoverable( + "Failed to restore selection from previous session state.", + error, + "restore-selection", + ); + } // Restore folds from previous state if available try { @@ -1216,7 +1241,13 @@ async function EditorManager($header, $body) { if (folds && folds.length) { restoreFolds(editor, folds); } - } catch (_) {} + } catch (error) { + warnRecoverable( + "Failed to restore folded regions from previous session state.", + error, + "restore-folds", + ); + } // Restore last known scroll position if present if ( @@ -1360,9 +1391,7 @@ async function EditorManager($header, $body) { const listener = () => { const active = manager.activeFile; if (active?.type === "editor") { - try { - active.session = editor.state; - } catch (_) {} + active.session = editor.state; } toggleProblemButton(); }; @@ -1424,7 +1453,13 @@ async function EditorManager($header, $body) { try { const mode = getModeForPath(uri); if (mode?.name) return String(mode.name).toLowerCase(); - } catch (_) {} + } catch (error) { + warnRecoverable( + `Failed to resolve language id for URI: ${uri}`, + error, + "lsp-language-id-resolution", + ); + } return "plaintext"; }, clientExtensions: [diagnosticsClientExt], @@ -1440,7 +1475,14 @@ async function EditorManager($header, $body) { try { const desired = appSettings?.value?.editorTheme || "one_dark"; editor.setTheme(desired); - } catch (_) {} + } catch (error) { + warnRecoverable( + "Failed to apply configured editor theme. Falling back to one_dark.", + error, + "initial-editor-theme", + ); + editor.setTheme("one_dark"); + } // Ensure initial options reflect settings applyOptions(); @@ -1608,9 +1650,7 @@ async function EditorManager($header, $body) { if (!update.docChanged) return; // Mirror latest state only on doc changes to avoid clobbering async loads - try { - file.session = update.state; - } catch (_) {} + file.session = update.state; // Debounced change handling (unsaved flag, cache, autosave) if (checkTimeout) clearTimeout(checkTimeout); @@ -1621,7 +1661,13 @@ async function EditorManager($header, $body) { file.isUnsaved = changed; try { await file.writeToCache(); - } catch (_) {} + } catch (error) { + warnRecoverable( + `Failed to write cache for ${file.filename || file.uri}`, + error, + `cache-write-${file.id}`, + ); + } events.emit("file-content-changed", file); manager.onupdate("file-changed"); @@ -1657,7 +1703,12 @@ async function EditorManager($header, $body) { effects: readOnlyCompartment.reconfigure(EditorState.readOnly.of(ro)), }); touchSelectionController?.onStateChanged(); - } catch (_) { + } catch (error) { + warnRecoverable( + "Failed to apply read-only compartment update. Recreating editor state.", + error, + "readonly-reconfigure", + ); // Fallback: full re-apply applyFileToEditor(file); } @@ -1682,7 +1733,13 @@ async function EditorManager($header, $body) { editor.dispatch({ effects: StateEffect.appendConfig.of(getDocSyncListener()), }); - } catch (_) {} + } catch (error) { + warnRecoverable( + "Failed to attach document sync listener to editor.", + error, + "doc-sync-listener", + ); + } return manager; @@ -2043,14 +2100,26 @@ async function EditorManager($header, $body) { try { const annotations = session.getAnnotations() || []; if (annotations.length) return true; - } catch (_) {} + } catch (error) { + warnRecoverable( + "Failed to read editor annotations while checking problems.", + error, + "read-annotations", + ); + } } if (typeof state.field !== "function") return false; try { const diagnostics = getLspDiagnostics(state); return diagnostics.length > 0; - } catch (_) {} + } catch (error) { + warnRecoverable( + "Failed to read LSP diagnostics while checking problems.", + error, + "read-lsp-diagnostics", + ); + } return false; } @@ -2135,13 +2204,9 @@ async function EditorManager($header, $body) { // Persist the previous editor's state before switching away const prev = manager.activeFile; if (prev?.type === "editor") { - try { - prev.session = editor.state; - } catch (_) {} - try { - prev.lastScrollTop = editor.scrollDOM?.scrollTop || 0; - prev.lastScrollLeft = editor.scrollDOM?.scrollLeft || 0; - } catch (_) {} + prev.session = editor.state; + prev.lastScrollTop = editor.scrollDOM?.scrollTop || 0; + prev.lastScrollLeft = editor.scrollDOM?.scrollLeft || 0; } manager.activeFile = file; diff --git a/src/lib/notificationManager.js b/src/lib/notificationManager.js index 52518402d..2ff308981 100644 --- a/src/lib/notificationManager.js +++ b/src/lib/notificationManager.js @@ -1,4 +1,5 @@ import sidebarApps from "sidebarApps"; +import DOMPurify from "dompurify"; // Singleton instance let instance = null; @@ -107,21 +108,24 @@ export default class NotificationManager { data-id={notification.id} > ); + const safeIcon = this.sanitizeIcon(this.parseIcon(notification.icon)); + const safeTitle = this.sanitizeText(notification.title); + const safeMessage = this.sanitizeText(notification.message); element.innerHTML = ` -
- ${this.parseIcon(notification.icon)} -
-
-
- ${notification.title} - ${this.formatTime(notification.time)} +
+ ${safeIcon}
-
${notification.message}
-
-
Dismiss
+
+
+ ${safeTitle} + ${this.formatTime(notification.time)} +
+
${safeMessage}
+
+
Dismiss
+
-
- `; + `; if (notification.action) { element.addEventListener("click", (e) => { if (e.target.closest(".action-button")) { @@ -140,16 +144,27 @@ export default class NotificationManager { data-id={notification.id} >
); + const safeIcon = this.sanitizeIcon(this.parseIcon(notification.icon)); + const safeTitle = this.sanitizeText(notification.title); + const safeMessage = this.sanitizeText(notification.message); element.innerHTML = ` -
${this.parseIcon(notification.icon)}
-
-
- ${notification.title} +
${safeIcon}
+
+
+ ${safeTitle} +
+
${safeMessage}
-
${notification.message}
-
- ${notification.autoClose ? "" : ``} - `; + ${notification.autoClose ? "" : ``} + `; + + const closeIcon = element.querySelector(".close-icon"); + if (closeIcon) { + closeIcon.addEventListener("click", (event) => { + event.stopPropagation(); + element.remove(); + }); + } if (notification.action) { element.addEventListener("click", () => notification.action(notification), @@ -202,13 +217,27 @@ export default class NotificationManager { } parseIcon(icon) { - if (!icon) return this.DEFAULT_ICON; + if (typeof icon !== "string" || !icon) return this.DEFAULT_ICON; if (icon.startsWith("`; return ``; } + sanitizeText(text) { + return DOMPurify.sanitize(String(text ?? ""), { + ALLOWED_TAGS: [], + ALLOWED_ATTR: [], + }); + } + + sanitizeIcon(iconMarkup) { + return DOMPurify.sanitize(iconMarkup, { + USE_PROFILES: { html: true, svg: true }, + ALLOW_DATA_ATTR: false, + }); + } + formatTime(date) { const now = new Date(); const diff = Math.floor((now - date) / 1000); diff --git a/src/lib/openFile.js b/src/lib/openFile.js index 9de895323..1d716ed88 100644 --- a/src/lib/openFile.js +++ b/src/lib/openFile.js @@ -75,7 +75,12 @@ export default async function openFile(file, options = {}) { editor.gotoLine(cursorPos.row, cursorPos.column); } } - } catch (_) {} + } catch (error) { + console.warn( + `Failed to move cursor for ${existingFile.filename || existingFile.uri}`, + error, + ); + } if (encoding && existingFile.encoding !== encoding) { reopenWithNewEncoding(encoding); diff --git a/src/lib/prettierFormatter.js b/src/lib/prettierFormatter.js index 659898230..4b93fa0f6 100644 --- a/src/lib/prettierFormatter.js +++ b/src/lib/prettierFormatter.js @@ -1,4 +1,5 @@ import fsOperation from "fileSystem"; +import { parse } from "acorn"; import toast from "components/toast"; import appSettings from "lib/settings"; import prettierPluginBabel from "prettier/plugins/babel"; @@ -319,7 +320,7 @@ function parseJsonLike(text) { const parsed = helpers.parseJSON(trimmed); if (parsed) return parsed; try { - return new Function(`return (${trimmed});`)(); + return parseSafeExpression(trimmed); } catch (_) { return null; } @@ -329,26 +330,180 @@ function parseJsConfig(directory, source, absolutePath) { if (!source) return null; void directory; void absolutePath; - let transformed = source; - if (/export\s+default/.test(transformed)) { - transformed = transformed.replace(/export\s+default/, "module.exports ="); - } - const module = { exports: {} }; - const exports = module.exports; - function requireStub(request) { - throw new Error( - `require(\"${request}\") is not supported in Prettier configs inside Acode`, - ); - } try { - const fn = new Function("module", "exports", "require", transformed); - fn(module, exports, requireStub); - return module.exports ?? exports; + return extractConfigFromProgram(source); } catch (_) { return null; } } +function parseProgram(source) { + try { + return parse(source, { + ecmaVersion: "latest", + sourceType: "module", + allowHashBang: true, + }); + } catch (_) { + return parse(source, { + ecmaVersion: "latest", + sourceType: "script", + allowHashBang: true, + }); + } +} + +function extractConfigFromProgram(source) { + const ast = parseProgram(source); + const scope = new Map(); + + for (const statement of ast.body) { + const declared = readVariableDeclaration(statement, scope); + if (declared) { + for (const [name, value] of declared) { + scope.set(name, value); + } + continue; + } + + const exported = readCommonJsExport(statement, scope); + if (exported !== undefined) return exported; + + const esmExported = readEsmExport(statement, scope); + if (esmExported !== undefined) return esmExported; + } + + return null; +} + +function parseSafeExpression(text) { + const wrapped = `(${text})`; + const ast = parse(wrapped, { + ecmaVersion: "latest", + sourceType: "module", + allowHashBang: true, + }); + const statement = ast.body[0]; + if (statement?.type !== "ExpressionStatement") return null; + return evaluateNode(statement.expression, new Map()); +} + +function readVariableDeclaration(statement, scope) { + if (statement?.type !== "VariableDeclaration") return null; + const values = new Map(); + const lookupScope = new Map(scope); + + for (const decl of statement.declarations || []) { + if (!decl || decl.type !== "VariableDeclarator") continue; + if (decl.id?.type !== "Identifier") continue; + if (!decl.init) continue; + try { + const value = evaluateNode(decl.init, lookupScope); + values.set(decl.id.name, value); + lookupScope.set(decl.id.name, value); + } catch (_) { + // Ignore unsupported declarations + } + } + + return values.size ? values : null; +} + +function readCommonJsExport(statement, scope) { + if (statement?.type !== "ExpressionStatement") return undefined; + const expr = statement.expression; + if (expr?.type !== "AssignmentExpression" || expr.operator !== "=") { + return undefined; + } + + if (!isModuleExports(expr.left)) return undefined; + return evaluateNode(expr.right, scope); +} + +function readEsmExport(statement, scope) { + if (statement?.type !== "ExportDefaultDeclaration") return undefined; + return evaluateNode(statement.declaration, scope); +} + +function isModuleExports(node) { + return ( + node?.type === "MemberExpression" && + !node.computed && + node.object?.type === "Identifier" && + node.object.name === "module" && + node.property?.type === "Identifier" && + node.property.name === "exports" + ); +} + +function evaluateNode(node, scope) { + if (!node) return null; + + switch (node.type) { + case "ObjectExpression": + return evaluateObjectExpression(node, scope); + case "ArrayExpression": + return node.elements.map((entry) => evaluateNode(entry, scope)); + case "Literal": + return node.value; + case "TemplateLiteral": + if (node.expressions.length) { + throw new Error("Template expressions are not supported"); + } + return node.quasis.map((part) => part.value.cooked ?? "").join(""); + case "Identifier": + if (scope.has(node.name)) return scope.get(node.name); + if (node.name === "undefined") return undefined; + throw new Error(`Unsupported identifier: ${node.name}`); + case "UnaryExpression": + return evaluateUnaryExpression(node, scope); + default: + throw new Error(`Unsupported node type: ${node.type}`); + } +} + +function evaluateObjectExpression(node, scope) { + const output = {}; + for (const property of node.properties || []) { + if (!property || property.type !== "Property") { + throw new Error("Unsupported object property"); + } + if (property.kind !== "init" || property.method || property.shorthand) { + throw new Error("Unsupported object property kind"); + } + const key = property.computed + ? evaluateNode(property.key, scope) + : getPropertyKey(property.key); + const normalizedKey = + typeof key === "string" || typeof key === "number" ? String(key) : null; + if (!normalizedKey) { + throw new Error("Unsupported object key"); + } + output[normalizedKey] = evaluateNode(property.value, scope); + } + return output; +} + +function getPropertyKey(node) { + if (node?.type === "Identifier") return node.name; + if (node?.type === "Literal") return node.value; + throw new Error("Unsupported property key"); +} + +function evaluateUnaryExpression(node, scope) { + const value = evaluateNode(node.argument, scope); + switch (node.operator) { + case "+": + return +value; + case "-": + return -value; + case "!": + return !value; + default: + throw new Error(`Unsupported unary operator: ${node.operator}`); + } +} + function normalizePath(path) { let result = String(path || "").replace(/\\/g, "/"); while (result.length > 1 && result.endsWith("/")) { diff --git a/src/lib/recents.js b/src/lib/recents.js index 9eb3bad84..c3d18436f 100644 --- a/src/lib/recents.js +++ b/src/lib/recents.js @@ -8,13 +8,15 @@ const recents = { * @returns {Array} */ get files() { - return JSON.parse(localStorage.recentFiles || "[]"); + const files = helpers.parseJSON(localStorage.recentFiles); + return Array.isArray(files) ? files : []; }, /** * @returns {{url: String, opts: Map}[]} */ get folders() { - return JSON.parse(localStorage.recentFolders || "[]"); + const folders = helpers.parseJSON(localStorage.recentFolders); + return Array.isArray(folders) ? folders : []; }, set files(list) { if (Array.isArray(list)) localStorage.recentFiles = JSON.stringify(list); diff --git a/src/lib/remoteStorage.js b/src/lib/remoteStorage.js index 96dee270a..883b8b88d 100644 --- a/src/lib/remoteStorage.js +++ b/src/lib/remoteStorage.js @@ -434,5 +434,7 @@ async function loadAd() { toast(strings.loading); await window.iad.load(); } - } catch (error) {} + } catch (error) { + console.warn("Failed to load interstitial ad.", error); + } } diff --git a/src/pages/changelog/changelog.js b/src/pages/changelog/changelog.js index 20dc31bf6..de68e7f30 100644 --- a/src/pages/changelog/changelog.js +++ b/src/pages/changelog/changelog.js @@ -3,6 +3,7 @@ import fsOperation from "fileSystem"; import Contextmenu from "components/contextmenu"; import Page from "components/page"; import toast from "components/toast"; +import DOMPurify from "dompurify"; import Ref from "html-tag-js/ref"; import actionStack from "lib/actionStack"; import markdownIt from "markdown-it"; @@ -164,7 +165,8 @@ export default async function Changelog() { md.use(markdownItTaskLists); md.use(markdownItFootnote); - body.innerHTML = md.render(processedText); + const renderedHtml = md.render(processedText); + body.innerHTML = DOMPurify.sanitize(renderedHtml); } function updateVersionSelector() { diff --git a/src/pages/customTheme/customTheme.js b/src/pages/customTheme/customTheme.js index a2bdd54ee..25b96366d 100644 --- a/src/pages/customTheme/customTheme.js +++ b/src/pages/customTheme/customTheme.js @@ -55,7 +55,9 @@ export default function CustomThemeInclude() { ["dark", strings["dark"]], ]); applyTheme(); - } catch (error) {} + } catch (error) { + console.warn("Unable to update custom theme type.", error); + } return; } diff --git a/src/pages/fileBrowser/fileBrowser.js b/src/pages/fileBrowser/fileBrowser.js index f034f1c22..a4e9ef95b 100644 --- a/src/pages/fileBrowser/fileBrowser.js +++ b/src/pages/fileBrowser/fileBrowser.js @@ -62,7 +62,8 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { const state = []; /**@type {Array} */ const allStorages = []; - let storageList = JSON.parse(localStorage.storageList || "[]"); + let storageList = helpers.parseJSON(localStorage.storageList); + if (!Array.isArray(storageList)) storageList = []; let isSelectionMode = false; let selectedItems = new Set(); @@ -1088,7 +1089,9 @@ function FileBrowserInclude(mode, info, doesOpenLast = true) { storageType: "sd", }); }); - } catch (err) {} + } catch (err) { + console.warn("Unable to list external storages.", err); + } storageList.forEach((storage) => { let url = storage.url || /**@deprecated */ storage["uri"]; diff --git a/src/pages/plugin/plugin.js b/src/pages/plugin/plugin.js index 8407423ea..caa5b80c8 100644 --- a/src/pages/plugin/plugin.js +++ b/src/pages/plugin/plugin.js @@ -481,7 +481,9 @@ export default async function PluginInclude( await window.iad.load(); el.textContent = oldText; } - } catch (error) {} + } catch (error) { + console.warn("Failed to load plugin page ad.", error); + } } async function getPurchase(sku) { diff --git a/src/pages/problems/problems.js b/src/pages/problems/problems.js index e79e6289d..73d56bd40 100644 --- a/src/pages/problems/problems.js +++ b/src/pages/problems/problems.js @@ -129,13 +129,9 @@ export default function Problems() { return diagnostics .map((diagnostic) => { const start = clampPosition(diagnostic.from, doc.length); - let row = 0; - let column = 0; - try { - const line = doc.lineAt(start); - row = Math.max(0, line.number - 1); - column = Math.max(0, start - line.from); - } catch (_) {} + const line = doc.lineAt(start); + const row = Math.max(0, line.number - 1); + const column = Math.max(0, start - line.from); let message = diagnostic.message || ""; if (diagnostic.source) { diff --git a/src/pages/sponsors/sponsors.js b/src/pages/sponsors/sponsors.js index b9e12f55c..a965d8ed0 100644 --- a/src/pages/sponsors/sponsors.js +++ b/src/pages/sponsors/sponsors.js @@ -128,7 +128,10 @@ export default function Sponsors() { if (!sponsors.length && "cached_sponsors" in localStorage) { try { - sponsors = JSON.parse(localStorage.getItem("cached_sponsors")) || []; + const cachedSponsors = helpers.parseJSON( + localStorage.getItem("cached_sponsors"), + ); + sponsors = Array.isArray(cachedSponsors) ? cachedSponsors : []; } catch (error) { console.error("Failed to parse cached sponsors", error); } diff --git a/src/pages/themeSetting/themeSetting.js b/src/pages/themeSetting/themeSetting.js index 836a3bf5b..9d10bd503 100644 --- a/src/pages/themeSetting/themeSetting.js +++ b/src/pages/themeSetting/themeSetting.js @@ -30,11 +30,20 @@ export default function () { const list = new Ref(); let cmPreview = null; const previewDoc = `// Acode is awesome!\nconst message = "Welcome to Acode";\nconsole.log(message);`; - function createPreview(themeId) { - if (cmPreview) { + + function destroyPreview(context) { + if (!cmPreview) return; + try { cmPreview.destroy(); + } catch (error) { + console.warn(`Failed to destroy theme preview (${context}).`, error); + } finally { cmPreview = null; } + } + + function createPreview(themeId) { + destroyPreview("create"); const theme = getThemeExtensions(themeId, [oneDark]); const fixedHeightTheme = EditorView.theme({ "&": { height: "100%", flex: "1 1 auto" }, @@ -51,9 +60,7 @@ export default function () { actionStack.push({ id: "appTheme", action: () => { - try { - cmPreview?.destroy(); - } catch (_) {} + destroyPreview("close"); $page.hide(); $page.removeEventListener("click", clickHandler); }, @@ -87,9 +94,7 @@ export default function () { function renderAppThemes() { // Remove and destroy CodeMirror preview when showing app themes - try { - cmPreview?.destroy(); - } catch (_) {} + destroyPreview("switch-tab"); $themePreview.remove(); const content = []; diff --git a/src/palettes/changeMode/index.js b/src/palettes/changeMode/index.js index cd299fa61..47c783804 100644 --- a/src/palettes/changeMode/index.js +++ b/src/palettes/changeMode/index.js @@ -1,5 +1,6 @@ import { getModes } from "cm/modelist"; import palette from "components/palette"; +import helpers from "utils/helpers"; import Path from "utils/Path"; export default function changeMode() { @@ -25,7 +26,7 @@ function onselect(mode) { let modeAssociated; try { - modeAssociated = JSON.parse(localStorage.modeassoc || "{}"); + modeAssociated = helpers.parseJSON(localStorage.modeassoc) || {}; } catch (error) { modeAssociated = {}; } diff --git a/src/palettes/changeTheme/index.js b/src/palettes/changeTheme/index.js index d42294c4e..7827f7054 100644 --- a/src/palettes/changeTheme/index.js +++ b/src/palettes/changeTheme/index.js @@ -43,7 +43,9 @@ function generateHints(type) { let previousDark = isDeviceDarkTheme(); const updateTimeMs = 2000; -let intervalId = setInterval(async () => { +let intervalId = null; + +function syncSystemTheme() { if (appSettings.value.appTheme.toLowerCase() === "system") { const isDark = isDeviceDarkTheme(); if (isDark !== previousDark) { @@ -51,33 +53,37 @@ let intervalId = setInterval(async () => { updateSystemTheme(isDark); } } -}, updateTimeMs); +} + +function startSystemThemeWatcher() { + if (intervalId) return; + intervalId = setInterval(syncSystemTheme, updateTimeMs); +} + +function stopSystemThemeWatcher() { + if (!intervalId) return; + clearInterval(intervalId); + intervalId = null; +} + +function updateSystemThemeWatcher(theme) { + if (String(theme).toLowerCase() === "system") { + startSystemThemeWatcher(); + syncSystemTheme(); + return; + } + stopSystemThemeWatcher(); +} + +updateSystemThemeWatcher(appSettings.value.appTheme); +appSettings.on("update:appTheme", updateSystemThemeWatcher); function onselect(value) { if (!value) return; const selection = JSON.parse(value); - if (selection.theme === "system") { - // Start interval if not already started - if (!intervalId) { - intervalId = setInterval(async () => { - if (appSettings.value.appTheme.toLowerCase() === "system") { - const isDark = isDeviceDarkTheme(); - if (isDark !== previousDark) { - previousDark = isDark; - updateSystemTheme(isDark); - } - } - }, updateTimeMs); - } - } else { - // Cancel interval if it's running - if (intervalId) { - clearInterval(intervalId); - intervalId = null; - } - } + updateSystemThemeWatcher(selection.theme); if (selection.type === "editor") { editorManager.editor.setTheme(selection.theme); diff --git a/src/plugins/browser/android/com/foxdebug/browser/BrowserActivity.java b/src/plugins/browser/android/com/foxdebug/browser/BrowserActivity.java index eaec867f1..c422e4001 100644 --- a/src/plugins/browser/android/com/foxdebug/browser/BrowserActivity.java +++ b/src/plugins/browser/android/com/foxdebug/browser/BrowserActivity.java @@ -85,8 +85,14 @@ private void setSystemTheme(int systemBarColor) { controller.setSystemBarsAppearance(0, appearance); } } - } catch (IllegalArgumentException ignore) {} catch (Exception ignore) {} - } catch (Exception e) {} + } catch (IllegalArgumentException error) { + Log.w("BrowserActivity", "Invalid system bar color or appearance input.", error); + } catch (Exception error) { + Log.w("BrowserActivity", "Failed applying system bar theme values.", error); + } + } catch (Exception e) { + Log.e("BrowserActivity", "Failed to apply system theme.", e); + } } private void setStatusBarStyle(final Window window) { diff --git a/src/plugins/server/src/android/com/foxdebug/server/NanoHTTPDWebserver.java b/src/plugins/server/src/android/com/foxdebug/server/NanoHTTPDWebserver.java index 05887c4a6..c6f1a0fa0 100644 --- a/src/plugins/server/src/android/com/foxdebug/server/NanoHTTPDWebserver.java +++ b/src/plugins/server/src/android/com/foxdebug/server/NanoHTTPDWebserver.java @@ -116,17 +116,19 @@ Response serveFile( long endAt = -1; String range = header.get("range"); if (range != null) { - if (range.startsWith("bytes=")) { - range = range.substring("bytes=".length()); - int minus = range.indexOf('-'); - try { - if (minus > 0) { - startFrom = Long.parseLong(range.substring(0, minus)); - endAt = Long.parseLong(range.substring(minus + 1)); - } - } catch (NumberFormatException ignored) {} - } - } + if (range.startsWith("bytes=")) { + range = range.substring("bytes=".length()); + int minus = range.indexOf('-'); + try { + if (minus > 0) { + startFrom = Long.parseLong(range.substring(0, minus)); + endAt = Long.parseLong(range.substring(minus + 1)); + } + } catch (NumberFormatException error) { + Log.w("NanoHTTPDWebserver", "Invalid range header: " + range, error); + } + } + } // get if-range header. If present, it must match etag or else we // should ignore the range request @@ -334,11 +336,13 @@ private InputStream getInputStream(DocumentFile file) return contentResolver.openInputStream(uri); } - private JSONObject getJSONObject(JSONObject ob, String key) { - JSONObject jsonObject = null; - try { - jsonObject = ob.getJSONObject(key); - } catch (JSONException e) {} - return jsonObject; - } -} + private JSONObject getJSONObject(JSONObject ob, String key) { + JSONObject jsonObject = null; + try { + jsonObject = ob.getJSONObject(key); + } catch (JSONException e) { + Log.w("NanoHTTPDWebserver", "Missing or invalid JSON object for key: " + key, e); + } + return jsonObject; + } +} diff --git a/src/plugins/system/android/com/foxdebug/system/System.java b/src/plugins/system/android/com/foxdebug/system/System.java index 19897a472..37259c9ea 100644 --- a/src/plugins/system/android/com/foxdebug/system/System.java +++ b/src/plugins/system/android/com/foxdebug/system/System.java @@ -109,6 +109,7 @@ public class System extends CordovaPlugin { + private static final String TAG = "SystemPlugin"; private CallbackContext requestPermissionCallback; private Activity activity; @@ -765,7 +766,9 @@ private void compareFileText( if (inputStream != null) { try { inputStream.close(); - } catch (IOException ignored) {} + } catch (IOException closeError) { + Log.w(TAG, "Failed to close input stream while reading file.", closeError); + } } } } else { @@ -1013,13 +1016,15 @@ private String[] checkPermissions(JSONArray arr) throws Exception { for (int i = 0; i < arr.length(); i++) { try { String permission = arr.getString(i); - if (permission != null || !permission.equals("")) { + if (permission == null || permission.equals("")) { throw new Exception("Permission cannot be null or empty"); } if (!cordova.hasPermission(permission)) { list.add(permission); } - } catch (JSONException e) {} + } catch (JSONException e) { + Log.w(TAG, "Invalid permission entry at index " + i, e); + } } String[] res = new String[list.size()]; @@ -1990,7 +1995,9 @@ private String getFileProviderAuthority() { } } } - } catch (PackageManager.NameNotFoundException ignored) {} + } catch (PackageManager.NameNotFoundException error) { + Log.w(TAG, "Unable to inspect package providers for FileProvider authority.", error); + } if (fileProviderAuthority == null || fileProviderAuthority.isEmpty()) { fileProviderAuthority = context.getPackageName() + ".provider"; diff --git a/src/plugins/system/www/plugin.js b/src/plugins/system/www/plugin.js index 08b24d2a6..c6d6f49ce 100644 --- a/src/plugins/system/www/plugin.js +++ b/src/plugins/system/www/plugin.js @@ -129,19 +129,32 @@ module.exports = { onError: null, }; - cordova.exec(function (data) { - try { - var dataTag = data.split(':')[0]; - var dataUrl = data.split(':')[1]; - if (dataTag === 'onOpenExternalBrowser') { - myInAppBrowser.onOpenExternalBrowser(dataUrl); - } - } catch (error) { } - }, function (err) { - try { - onError(err); - } catch (error) { } - }, 'System', 'in-app-browser', [url, title, !!showButtons, disableCache]); + cordova.exec(function (data) { + if (typeof data !== 'string') { + console.warn('System.inAppBrowser: invalid callback payload', data); + return; + } + var separatorIndex = data.indexOf(':'); + if (separatorIndex < 0) { + console.warn('System.inAppBrowser: malformed callback payload', data); + return; + } + var dataTag = data.slice(0, separatorIndex); + var dataUrl = data.slice(separatorIndex + 1); + if (dataTag === 'onOpenExternalBrowser') { + if (typeof myInAppBrowser.onOpenExternalBrowser === 'function') { + myInAppBrowser.onOpenExternalBrowser(dataUrl); + } else { + console.warn('System.inAppBrowser: onOpenExternalBrowser handler is not set'); + } + } + }, function (err) { + if (typeof myInAppBrowser.onError === 'function') { + myInAppBrowser.onError(err); + return; + } + console.warn('System.inAppBrowser error callback not handled', err); + }, 'System', 'in-app-browser', [url, title, !!showButtons, disableCache]); return myInAppBrowser; }, setUiTheme: function (systemBarColor, theme, onSuccess, onFail) { diff --git a/src/plugins/terminal/src/android/ProcessUtils.java b/src/plugins/terminal/src/android/ProcessUtils.java index 10ccc4034..eb3a8f579 100644 --- a/src/plugins/terminal/src/android/ProcessUtils.java +++ b/src/plugins/terminal/src/android/ProcessUtils.java @@ -1,6 +1,7 @@ package com.foxdebug.acode.rk.exec.terminal; import java.lang.reflect.Field; +import android.util.Log; import com.foxdebug.acode.rk.exec.terminal.*; public class ProcessUtils { @@ -39,7 +40,9 @@ public static void killProcessTree(Process process) { if (pid > 0) { Runtime.getRuntime().exec("kill -9 -" + pid); } - } catch (Exception ignored) {} + } catch (Exception error) { + Log.w("ProcessUtils", "Failed to kill process tree.", error); + } process.destroy(); } -} \ No newline at end of file +} diff --git a/src/sidebarApps/searchInFiles/index.js b/src/sidebarApps/searchInFiles/index.js index 3014700a8..bfda17e25 100644 --- a/src/sidebarApps/searchInFiles/index.js +++ b/src/sidebarApps/searchInFiles/index.js @@ -741,7 +741,9 @@ async function onCursorChange(line) { selection: { anchor: from, head: to }, effects: EditorView.scrollIntoView(from, { y: "center" }), }); - } catch (_) {} + } catch (error) { + console.warn(`Failed to focus search result at line ${line}.`, error); + } } /** diff --git a/src/utils/Uri.js b/src/utils/Uri.js index 195ab1762..5ce203c79 100644 --- a/src/utils/Uri.js +++ b/src/utils/Uri.js @@ -1,6 +1,15 @@ import escapeStringRegexp from "escape-string-regexp"; import path from "./Path"; +function parseStorageList() { + try { + const storageList = JSON.parse(localStorage.storageList || "[]"); + return Array.isArray(storageList) ? storageList : []; + } catch (_) { + return []; + } +} + export default { /** * Parse content uri to rootUri and docID @@ -81,7 +90,7 @@ export default { */ getVirtualAddress(url) { try { - const storageList = JSON.parse(localStorage.storageList || "[]"); + const storageList = parseStorageList(); const matches = []; for (let storage of storageList) { diff --git a/src/utils/helpers.js b/src/utils/helpers.js index 0a9c69434..bd49b1e8e 100644 --- a/src/utils/helpers.js +++ b/src/utils/helpers.js @@ -242,7 +242,8 @@ export default { } /**@type {string[]} */ - const storageList = JSON.parse(localStorage.storageList || "[]"); + const storageList = this.parseJSON(localStorage.storageList); + if (!Array.isArray(storageList)) return url; const storageListLen = storageList.length; for (let i = 0; i < storageListLen; ++i) {