diff --git a/tests/test_storage_unification.mjs b/tests/test_storage_unification.mjs new file mode 100644 index 0000000..5dba367 --- /dev/null +++ b/tests/test_storage_unification.mjs @@ -0,0 +1,17 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import test from 'node:test'; + +const STORAGE_MODULES = [ + 'web/resolver/model_resolver.js', + 'web/resolver/actions/queue_storage_methods.js', + 'web/resolver/actions/queue_methods.js', +]; + +test('resolver storage modules use safeStorage instead of direct localStorage access', async () => { + for (const relativePath of STORAGE_MODULES) { + const source = await readFile(new URL(`../${relativePath}`, import.meta.url), 'utf8'); + assert.match(source, /safeStorage\.(?:getItem|setItem|removeItem)/, relativePath); + assert.doesNotMatch(source, /\blocalStorage\s*\./, relativePath); + } +}); diff --git a/web/resolver/actions/queue_methods.js b/web/resolver/actions/queue_methods.js index 91b929d..3ec4dc8 100644 --- a/web/resolver/actions/queue_methods.js +++ b/web/resolver/actions/queue_methods.js @@ -4,6 +4,7 @@ import { getSvgIcon } from "../../utils/icon_utils.js"; import { startSplitterDrag } from "../utils/splitter_drag.js"; import { syncElementAttributes } from "../utils/dom_patch_utils.js"; import { bindDownloadActionHandlers } from "../utils/download_action_handlers.js"; +import { safeStorage } from "../utils/html_utils.js"; const FOOTER_VERSION_REFRESH_INTERVAL_MS = 6 * 60 * 60 * 1000; const FOOTER_VERSION_FAILURE_REFRESH_INTERVAL_MS = 5 * 60 * 1000; @@ -36,18 +37,16 @@ export const queueMethods = { this.observeQueueSplitContainer(body); // Restore saved queue width and wire splitter - try { - const savedSplit = localStorage.getItem('model_resolver_split_w'); - if (savedSplit) { - const w = parseInt(savedSplit, 10); - if (!isNaN(w) && w > 0) { - this.queueElement.style.width = `${w}px`; - this.queueElement.style.flexBasis = `${w}px`; - this.queueElement.style.flexGrow = '0'; - this.queueElement.style.flexShrink = '0'; - } + const savedSplit = safeStorage.getItem('model_resolver_split_w'); + if (savedSplit) { + const w = parseInt(savedSplit, 10); + if (!isNaN(w) && w > 0) { + this.queueElement.style.width = `${w}px`; + this.queueElement.style.flexBasis = `${w}px`; + this.queueElement.style.flexGrow = '0'; + this.queueElement.style.flexShrink = '0'; } - } catch (_e) { } + } try { const onSplitMouseDown = (e) => this.startSplitDrag(e); @@ -72,10 +71,8 @@ export const queueMethods = { } catch (_e) { } // Restore queue collapsed state - try { - const col = localStorage.getItem('model_resolver_queue_collapsed'); - if (col === '1') this.setQueueCollapsed(true, { persist: false, updatePanel: false }); - } catch (_e) { } + const col = safeStorage.getItem('model_resolver_queue_collapsed'); + if (col === '1') this.setQueueCollapsed(true, { persist: false, updatePanel: false }); return body; }, diff --git a/web/resolver/actions/queue_storage_methods.js b/web/resolver/actions/queue_storage_methods.js index af80369..00ef3ac 100644 --- a/web/resolver/actions/queue_storage_methods.js +++ b/web/resolver/actions/queue_storage_methods.js @@ -1,3 +1,5 @@ +import { safeStorage } from '../utils/html_utils.js'; + export const queueStorageMethods = { getActiveDownloadRecoveryStorageKey() { return this.activeDownloadsStorageKey || 'model_resolver_active_downloads'; @@ -10,7 +12,7 @@ export const queueStorageMethods = { this._activeDownloadRecoveryLoaded = true; try { - const raw = localStorage.getItem(this.getActiveDownloadRecoveryStorageKey()); + const raw = safeStorage.getItem(this.getActiveDownloadRecoveryStorageKey()); const parsed = raw ? JSON.parse(raw) : {}; this._activeDownloadRecovery = parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed @@ -24,14 +26,10 @@ export const queueStorageMethods = { saveActiveDownloadRecovery() { const recovery = this.loadActiveDownloadRecovery(); - try { - localStorage.setItem( - this.getActiveDownloadRecoveryStorageKey(), - JSON.stringify(recovery) - ); - } catch (error) { - console.warn('Model Resolver: failed to save active download recovery', error); - } + safeStorage.setItem( + this.getActiveDownloadRecoveryStorageKey(), + JSON.stringify(recovery) + ); }, persistActiveDownloadRecovery(downloadId, info = {}) { @@ -88,9 +86,7 @@ export const queueStorageMethods = { } const writeState = () => { this._queueCollapsedPersistTimer = null; - try { - localStorage.setItem('model_resolver_queue_collapsed', collapsed ? '1' : '0'); - } catch (_e) { } + safeStorage.setItem('model_resolver_queue_collapsed', collapsed ? '1' : '0'); }; const delay = Math.max(0, Number(delayMs) || 0); if (delay > 0) { @@ -109,9 +105,7 @@ export const queueStorageMethods = { } const writeWidth = () => { this._queueSplitPersistTimer = null; - try { - localStorage.setItem('model_resolver_split_w', String(nextWidth)); - } catch (_e) { } + safeStorage.setItem('model_resolver_split_w', String(nextWidth)); }; const delay = Math.max(0, Number(delayMs) || 0); if (delay > 0) { @@ -125,7 +119,7 @@ export const queueStorageMethods = { if (this._downloadHistoryLoaded) return this.downloadHistory || []; this._downloadHistoryLoaded = true; try { - const raw = localStorage.getItem(this.downloadHistoryStorageKey || 'model_resolver_download_history'); + const raw = safeStorage.getItem(this.downloadHistoryStorageKey || 'model_resolver_download_history'); const parsed = raw ? JSON.parse(raw) : []; this.downloadHistory = Array.isArray(parsed) ? parsed.filter(item => item && typeof item === 'object') @@ -147,14 +141,10 @@ export const queueStorageMethods = { saveDownloadHistory() { const history = this.getDownloadHistory().slice(0, this.downloadHistoryLimit || 200); this.downloadHistory = history; - try { - localStorage.setItem( - this.downloadHistoryStorageKey || 'model_resolver_download_history', - JSON.stringify(history) - ); - } catch (error) { - console.warn('Model Resolver: failed to save download history', error); - } + safeStorage.setItem( + this.downloadHistoryStorageKey || 'model_resolver_download_history', + JSON.stringify(history) + ); }, getDownloadHistoryIdentity(entry = {}) { diff --git a/web/resolver/model_resolver.js b/web/resolver/model_resolver.js index 86e4857..5ef530f 100644 --- a/web/resolver/model_resolver.js +++ b/web/resolver/model_resolver.js @@ -8,6 +8,7 @@ import { isSidebarButtonActive } from "./utils/dom_patch_utils.js"; import { fetchJson as fetchJsonRequest } from "./utils/api_client.js"; import { ResolverManagerDialog } from "./resolver_dialog.js"; import { showNotification } from "../utils/notification_utils.js"; +import { safeStorage } from "./utils/html_utils.js"; import { serializeKeybindingCombo, keybindingsEqual, @@ -60,11 +61,11 @@ function getKeybindingSetting(id) { } function applyStoredFrontendLoggingPreference() { - const stored = localStorage.getItem('ModelResolver.frontendLogsEnabled'); + const stored = safeStorage.getItem('ModelResolver.frontendLogsEnabled'); if (stored !== null) { frontendLogger.setEnabled(stored !== 'false'); } - const storedLevel = localStorage.getItem('ModelResolver.frontendLogLevel'); + const storedLevel = safeStorage.getItem('ModelResolver.frontendLogLevel'); if (storedLevel) { frontendLogger.setGlobalAndModuleLevel(frontendLogger.normalizeLevel(storedLevel)); } @@ -716,11 +717,7 @@ export class ModelResolver { this.dialog.showResolvedModels = true; this.dialog.missingModelsTypeFilter = 'all'; this.dialog.missingModelsTypeFilterMenuOpen = false; - try { - localStorage.setItem(this.dialog.showResolvedModelsStorageKey, '1'); - } catch (error) { - log.debug('Model Resolver: failed to persist resolved-model visibility.', error); - } + safeStorage.setItem(this.dialog.showResolvedModelsStorageKey, '1'); const selectionRequest = this.dialog.queueWorkflowModelReferenceSelection?.(reference); if (!wasVisible) { @@ -961,11 +958,11 @@ export class ModelResolver { } isWorkflowHashMetadataEnabled() { - return localStorage.getItem('ModelResolver.workflowHashMetadataEnabled') !== 'false'; + return safeStorage.getItem('ModelResolver.workflowHashMetadataEnabled') !== 'false'; } isWorkflowDependencyMarkerEnabled() { - return localStorage.getItem('ModelResolver.workflowDependencyMarkerEnabled') === 'true'; + return safeStorage.getItem('ModelResolver.workflowDependencyMarkerEnabled') === 'true'; } setupWorkflowHashMetadataInjection() { @@ -1605,7 +1602,7 @@ export class ModelResolver { * Check if auto-open preference is enabled */ isAutoOpenEnabled() { - return localStorage.getItem('ModelResolver.autoOpenOnMissing') === 'true'; + return safeStorage.getItem('ModelResolver.autoOpenOnMissing') === 'true'; } openResolverForDetectedMissingModels() {