Skip to content

Commit 66195be

Browse files
committed
fix: harden extension preference storage
1 parent a9b4e2f commit 66195be

3 files changed

Lines changed: 112 additions & 23 deletions

File tree

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
export const SHOW_RESERVED_PLUGINS_STORAGE_KEY = "showReservedPlugins";
2+
export const PLUGIN_LIST_VIEW_MODE_STORAGE_KEY = "pluginListViewMode";
3+
export const PIN_UPDATES_ON_TOP_STORAGE_KEY = "pinUpdatesOnTop";
4+
5+
const resolveStorage = (storage) => {
6+
if (storage !== undefined) {
7+
return storage;
8+
}
9+
if (typeof window === "undefined") {
10+
return null;
11+
}
12+
try {
13+
return window.localStorage ?? null;
14+
} catch {
15+
return null;
16+
}
17+
};
18+
19+
export const readBooleanPreference = (key, fallback, storage) => {
20+
const targetStorage = resolveStorage(storage);
21+
if (!targetStorage) {
22+
return fallback;
23+
}
24+
25+
try {
26+
const saved = targetStorage.getItem(key);
27+
if (saved === "true") {
28+
return true;
29+
}
30+
if (saved === "false") {
31+
return false;
32+
}
33+
return fallback;
34+
} catch {
35+
return fallback;
36+
}
37+
};
38+
39+
export const writeBooleanPreference = (key, value, storage) => {
40+
const targetStorage = resolveStorage(storage);
41+
if (!targetStorage) {
42+
return;
43+
}
44+
45+
try {
46+
targetStorage.setItem(key, String(value));
47+
} catch {
48+
// Ignore restricted storage environments.
49+
}
50+
};

dashboard/src/views/extension/useExtensionPage.js

Lines changed: 13 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@ import {
1414
getValidHashTab,
1515
replaceTabRoute,
1616
} from "@/utils/hashRouteTabs.mjs";
17+
import {
18+
PIN_UPDATES_ON_TOP_STORAGE_KEY,
19+
PLUGIN_LIST_VIEW_MODE_STORAGE_KEY,
20+
SHOW_RESERVED_PLUGINS_STORAGE_KEY,
21+
readBooleanPreference,
22+
writeBooleanPreference,
23+
} from "./extensionPreferenceStorage.mjs";
1724
import { ref, computed, onMounted, onUnmounted, reactive, watch } from "vue";
1825
import { useRoute, useRouter } from "vue-router";
1926
import { useDisplay } from "vuetify";
@@ -124,11 +131,7 @@ export const useExtensionPage = () => {
124131

125132
// 从 localStorage 恢复显示系统插件的状态,默认为 false(隐藏)
126133
const getInitialShowReserved = () => {
127-
if (typeof window !== "undefined" && window.localStorage) {
128-
const saved = localStorage.getItem("showReservedPlugins");
129-
return saved === "true";
130-
}
131-
return false;
134+
return readBooleanPreference(SHOW_RESERVED_PLUGINS_STORAGE_KEY, false);
132135
};
133136
const showReserved = ref(getInitialShowReserved());
134137
const snack_message = ref("");
@@ -178,28 +181,19 @@ export const useExtensionPage = () => {
178181
// 新增变量支持列表视图
179182
// 从 localStorage 恢复显示模式,默认为 false(卡片视图)
180183
const getInitialListViewMode = () => {
181-
if (typeof window !== "undefined" && window.localStorage) {
182-
return localStorage.getItem("pluginListViewMode") === "true";
183-
}
184-
return false;
184+
return readBooleanPreference(PLUGIN_LIST_VIEW_MODE_STORAGE_KEY, false);
185185
};
186186
const isListView = ref(getInitialListViewMode());
187187
const pluginSearch = ref("");
188188
const installedStatusFilter = ref("all");
189189
const installedSortBy = ref("default");
190190
const installedSortOrder = ref("desc");
191191
const getInitialPinUpdatesOnTop = () => {
192-
if (typeof window !== "undefined" && window.localStorage) {
193-
const saved = localStorage.getItem("pinUpdatesOnTop");
194-
return saved !== "false";
195-
}
196-
return true;
192+
return readBooleanPreference(PIN_UPDATES_ON_TOP_STORAGE_KEY, true);
197193
};
198194
const pinUpdatesOnTop = ref(getInitialPinUpdatesOnTop());
199195
watch(pinUpdatesOnTop, (val) => {
200-
if (typeof window !== "undefined" && window.localStorage) {
201-
localStorage.setItem("pinUpdatesOnTop", val.toString());
202-
}
196+
writeBooleanPreference(PIN_UPDATES_ON_TOP_STORAGE_KEY, val);
203197
});
204198
const loading_ = ref(false);
205199

@@ -660,9 +654,7 @@ export const useExtensionPage = () => {
660654
const toggleShowReserved = () => {
661655
showReserved.value = !showReserved.value;
662656
// 保存到 localStorage
663-
if (typeof window !== "undefined" && window.localStorage) {
664-
localStorage.setItem("showReservedPlugins", showReserved.value.toString());
665-
}
657+
writeBooleanPreference(SHOW_RESERVED_PLUGINS_STORAGE_KEY, showReserved.value);
666658
};
667659

668660
const toast = (message, success) => {
@@ -1627,9 +1619,7 @@ export const useExtensionPage = () => {
16271619

16281620
// 监听显示模式变化并保存到 localStorage
16291621
watch(isListView, (newVal) => {
1630-
if (typeof window !== "undefined" && window.localStorage) {
1631-
localStorage.setItem("pluginListViewMode", String(newVal));
1632-
}
1622+
writeBooleanPreference(PLUGIN_LIST_VIEW_MODE_STORAGE_KEY, newVal);
16331623
});
16341624

16351625
watch(
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import test from 'node:test';
2+
import assert from 'node:assert/strict';
3+
4+
import {
5+
PIN_UPDATES_ON_TOP_STORAGE_KEY,
6+
readBooleanPreference,
7+
writeBooleanPreference,
8+
} from '../src/views/extension/extensionPreferenceStorage.mjs';
9+
10+
test("readBooleanPreference returns fallback when storage access throws", () => {
11+
const storage = {
12+
getItem() {
13+
throw new Error("SecurityError");
14+
},
15+
};
16+
17+
assert.equal(
18+
readBooleanPreference(PIN_UPDATES_ON_TOP_STORAGE_KEY, true, storage),
19+
true,
20+
);
21+
});
22+
23+
test("readBooleanPreference parses stored boolean strings", () => {
24+
const storage = {
25+
getItem(key) {
26+
return key === PIN_UPDATES_ON_TOP_STORAGE_KEY ? "false" : null;
27+
},
28+
};
29+
30+
assert.equal(
31+
readBooleanPreference(PIN_UPDATES_ON_TOP_STORAGE_KEY, true, storage),
32+
false,
33+
);
34+
});
35+
36+
test("writeBooleanPreference stores boolean strings and swallows storage errors", () => {
37+
const writes = [];
38+
const storage = {
39+
setItem(key, value) {
40+
writes.push([key, value]);
41+
throw new Error("QuotaExceededError");
42+
},
43+
};
44+
45+
assert.doesNotThrow(() =>
46+
writeBooleanPreference(PIN_UPDATES_ON_TOP_STORAGE_KEY, true, storage),
47+
);
48+
assert.deepEqual(writes, [[PIN_UPDATES_ON_TOP_STORAGE_KEY, "true"]]);
49+
});

0 commit comments

Comments
 (0)