fix: 修复右键菜单功能的多个逻辑漏洞

1. 修复时区硬编码问题 - 使用用户选择的时区而非固定 Asia/Shanghai
2. 修复 openPopup() 失败后数据残留 - 失败时清除 storage 中的待处理数据
3. 统一数据过期时间常量 - 导出 CONTEXT_MENU_DATA_EXPIRY_MS 并统一使用
4. 修复 featureKey 类型断言不安全 - 使用映射表处理非常规菜单 ID
This commit is contained in:
雨霖铃
2026-05-20 21:39:33 +08:00
parent e9a6fa3dd8
commit 0427f16cc9
5 changed files with 62 additions and 45 deletions
+2
View File
@@ -41,6 +41,8 @@ export default defineBackground(() => {
await browser.action.openPopup();
} catch (err) {
console.error('[Context Menu] 打开 popup 失败:', err);
// 打开失败时清除残留数据,避免下次打开 popup 时误触发
await chrome.storage.local.remove('contextMenu/pendingData');
}
});
+35 -32
View File
@@ -74,41 +74,44 @@ export function useTimestampConverter(): UseTimestampConverterReturn {
}, [mode, tsInput, dtInput, unit, zone, t]);
// 处理右键菜单传递的数据
const handleContextMenuData = useCallback((payload: string) => {
const trimmed = payload.trim();
if (isTimestampLike(trimmed)) {
// 看起来是时间戳,切换到 ts2dt 模式
setMode('ts2dt');
setTsInput(trimmed);
// 如果是 13 位毫秒级时间戳,自动选择 ms 单位
const detectedUnit: UnitType = trimmed.length >= 13 ? 'ms' : 's';
setUnit(detectedUnit);
// 直接执行转换
const num = Number(trimmed);
if (!isNaN(num)) {
const d = detectedUnit === 'ms' ? dayjs(num) : dayjs.unix(num);
if (d.isValid()) {
setError('');
setResult(d.tz('Asia/Shanghai').format(DATE_FORMAT));
}
}
} else {
// 尝试作为日期时间解析
const d = dayjs(trimmed);
if (d.isValid()) {
setMode('dt2ts');
setDtInput(d.format(DATE_FORMAT));
// 直接执行转换
const ms = d.valueOf();
setError('');
setResult(String(ms));
} else {
// 无法识别,作为时间戳处理
const handleContextMenuData = useCallback(
(payload: string) => {
const trimmed = payload.trim();
if (isTimestampLike(trimmed)) {
// 看起来是时间戳,切换到 ts2dt 模式
setMode('ts2dt');
setTsInput(trimmed);
// 如果是 13 位毫秒级时间戳,自动选择 ms 单位
const detectedUnit: UnitType = trimmed.length >= 13 ? 'ms' : 's';
setUnit(detectedUnit);
// 直接执行转换
const num = Number(trimmed);
if (!isNaN(num)) {
const d = detectedUnit === 'ms' ? dayjs(num) : dayjs.unix(num);
if (d.isValid()) {
setError('');
setResult(d.tz(zone).format(DATE_FORMAT));
}
}
} else {
// 尝试作为日期时间解析
const d = dayjs(trimmed);
if (d.isValid()) {
setMode('dt2ts');
setDtInput(d.format(DATE_FORMAT));
// 直接执行转换
const ms = d.valueOf();
setError('');
setResult(String(ms));
} else {
// 无法识别,作为时间戳处理
setMode('ts2dt');
setTsInput(trimmed);
}
}
}
}, []);
},
[zone],
);
useContextMenuData({ featureKey: 'timestamp', onData: handleContextMenuData });
+7 -3
View File
@@ -14,7 +14,7 @@ import {
getDefaultPageOrder,
getDefaultVisibleFeatureKeys,
} from '@/config/features';
import { saveContextMenuData } from '@/utils/useContextMenuData';
import { saveContextMenuData, CONTEXT_MENU_DATA_EXPIRY_MS } from '@/utils/useContextMenuData';
/**
* 校验是否为合法的页面类型
@@ -215,7 +215,7 @@ export function RouterProvider({
if (
pendingData &&
isValidPage(pendingData.featureKey) &&
Date.now() - pendingData.timestamp < 5000
Date.now() - pendingData.timestamp < CONTEXT_MENU_DATA_EXPIRY_MS
) {
navigateTo(pendingData.featureKey as PageType);
// 不在这里清除数据,让目标页面的 useContextMenuData 来消费和清除
@@ -292,7 +292,11 @@ export function RouterProvider({
if (changes['contextMenu/pendingData']) {
const newData = changes['contextMenu/pendingData']
.newValue as ContextMenuPendingData | null;
if (newData && isValidPage(newData.featureKey) && Date.now() - newData.timestamp < 5000) {
if (
newData &&
isValidPage(newData.featureKey) &&
Date.now() - newData.timestamp < CONTEXT_MENU_DATA_EXPIRY_MS
) {
setCurrentPage(newData.featureKey as PageType);
}
}
+14 -8
View File
@@ -22,6 +22,19 @@ const PARENT_MENU_ID = 'testing-tools-parent';
export const MAX_PAYLOAD_LENGTH = 10000;
/** 菜单项 ID 到 PageType 的映射(仅处理非常规映射) */
const MENU_ID_TO_PAGE_TYPE: Record<string, PageType> = {
'qrCode-page': 'qrCode',
};
/**
* 将菜单项 ID 转换为 PageType
* 如果存在显式映射则使用映射,否则直接使用 menuItemId
*/
function getMenuPageType(menuItemId: string): PageType {
return MENU_ID_TO_PAGE_TYPE[menuItemId] ?? (menuItemId as PageType);
}
export const CONTEXT_MENU_CONFIGS: ContextMenuItemConfig[] = [
{
id: PARENT_MENU_ID,
@@ -81,14 +94,7 @@ export function parseContextMenuClick(
menuItemId: string,
info: chrome.contextMenus.OnClickData,
): ParseResult {
const featureKey = menuItemId as PageType;
if (menuItemId === 'qrCode-page') {
return {
success: true,
data: { featureKey: 'qrCode', payload: info.pageUrl || '' },
};
}
const featureKey = getMenuPageType(menuItemId);
if (info.selectionText) {
const text = info.selectionText;
+4 -2
View File
@@ -3,7 +3,9 @@ import { storageUtil } from '@/utils/chromeStorage';
import type { ContextMenuPendingData, PageType } from '@/types/storage';
const STORAGE_KEY = 'contextMenu/pendingData' as const;
const DATA_EXPIRY_MS = 5000;
/** 右键菜单数据过期时间(毫秒) */
export const CONTEXT_MENU_DATA_EXPIRY_MS = 5000;
export interface UseContextMenuDataOptions {
/** 当前页面的功能标识 */
@@ -29,7 +31,7 @@ export function useContextMenuData({ featureKey, onData }: UseContextMenuDataOpt
if (data.featureKey !== featureKey) return;
if (Date.now() - data.timestamp > DATA_EXPIRY_MS) {
if (Date.now() - data.timestamp > CONTEXT_MENU_DATA_EXPIRY_MS) {
await storageUtil.remove(STORAGE_KEY);
return;
}