fix: 修复右键菜单功能的多个逻辑漏洞
1. 修复时区硬编码问题 - 使用用户选择的时区而非固定 Asia/Shanghai 2. 修复 openPopup() 失败后数据残留 - 失败时清除 storage 中的待处理数据 3. 统一数据过期时间常量 - 导出 CONTEXT_MENU_DATA_EXPIRY_MS 并统一使用 4. 修复 featureKey 类型断言不安全 - 使用映射表处理非常规菜单 ID
This commit is contained in:
@@ -41,6 +41,8 @@ export default defineBackground(() => {
|
|||||||
await browser.action.openPopup();
|
await browser.action.openPopup();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[Context Menu] 打开 popup 失败:', err);
|
console.error('[Context Menu] 打开 popup 失败:', err);
|
||||||
|
// 打开失败时清除残留数据,避免下次打开 popup 时误触发
|
||||||
|
await chrome.storage.local.remove('contextMenu/pendingData');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -74,41 +74,44 @@ export function useTimestampConverter(): UseTimestampConverterReturn {
|
|||||||
}, [mode, tsInput, dtInput, unit, zone, t]);
|
}, [mode, tsInput, dtInput, unit, zone, t]);
|
||||||
|
|
||||||
// 处理右键菜单传递的数据
|
// 处理右键菜单传递的数据
|
||||||
const handleContextMenuData = useCallback((payload: string) => {
|
const handleContextMenuData = useCallback(
|
||||||
const trimmed = payload.trim();
|
(payload: string) => {
|
||||||
if (isTimestampLike(trimmed)) {
|
const trimmed = payload.trim();
|
||||||
// 看起来是时间戳,切换到 ts2dt 模式
|
if (isTimestampLike(trimmed)) {
|
||||||
setMode('ts2dt');
|
// 看起来是时间戳,切换到 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 {
|
|
||||||
// 无法识别,作为时间戳处理
|
|
||||||
setMode('ts2dt');
|
setMode('ts2dt');
|
||||||
setTsInput(trimmed);
|
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 });
|
useContextMenuData({ featureKey: 'timestamp', onData: handleContextMenuData });
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
getDefaultPageOrder,
|
getDefaultPageOrder,
|
||||||
getDefaultVisibleFeatureKeys,
|
getDefaultVisibleFeatureKeys,
|
||||||
} from '@/config/features';
|
} 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 (
|
if (
|
||||||
pendingData &&
|
pendingData &&
|
||||||
isValidPage(pendingData.featureKey) &&
|
isValidPage(pendingData.featureKey) &&
|
||||||
Date.now() - pendingData.timestamp < 5000
|
Date.now() - pendingData.timestamp < CONTEXT_MENU_DATA_EXPIRY_MS
|
||||||
) {
|
) {
|
||||||
navigateTo(pendingData.featureKey as PageType);
|
navigateTo(pendingData.featureKey as PageType);
|
||||||
// 不在这里清除数据,让目标页面的 useContextMenuData 来消费和清除
|
// 不在这里清除数据,让目标页面的 useContextMenuData 来消费和清除
|
||||||
@@ -292,7 +292,11 @@ export function RouterProvider({
|
|||||||
if (changes['contextMenu/pendingData']) {
|
if (changes['contextMenu/pendingData']) {
|
||||||
const newData = changes['contextMenu/pendingData']
|
const newData = changes['contextMenu/pendingData']
|
||||||
.newValue as ContextMenuPendingData | null;
|
.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);
|
setCurrentPage(newData.featureKey as PageType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-8
@@ -22,6 +22,19 @@ const PARENT_MENU_ID = 'testing-tools-parent';
|
|||||||
|
|
||||||
export const MAX_PAYLOAD_LENGTH = 10000;
|
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[] = [
|
export const CONTEXT_MENU_CONFIGS: ContextMenuItemConfig[] = [
|
||||||
{
|
{
|
||||||
id: PARENT_MENU_ID,
|
id: PARENT_MENU_ID,
|
||||||
@@ -81,14 +94,7 @@ export function parseContextMenuClick(
|
|||||||
menuItemId: string,
|
menuItemId: string,
|
||||||
info: chrome.contextMenus.OnClickData,
|
info: chrome.contextMenus.OnClickData,
|
||||||
): ParseResult {
|
): ParseResult {
|
||||||
const featureKey = menuItemId as PageType;
|
const featureKey = getMenuPageType(menuItemId);
|
||||||
|
|
||||||
if (menuItemId === 'qrCode-page') {
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
data: { featureKey: 'qrCode', payload: info.pageUrl || '' },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (info.selectionText) {
|
if (info.selectionText) {
|
||||||
const text = info.selectionText;
|
const text = info.selectionText;
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import { storageUtil } from '@/utils/chromeStorage';
|
|||||||
import type { ContextMenuPendingData, PageType } from '@/types/storage';
|
import type { ContextMenuPendingData, PageType } from '@/types/storage';
|
||||||
|
|
||||||
const STORAGE_KEY = 'contextMenu/pendingData' as const;
|
const STORAGE_KEY = 'contextMenu/pendingData' as const;
|
||||||
const DATA_EXPIRY_MS = 5000;
|
|
||||||
|
/** 右键菜单数据过期时间(毫秒) */
|
||||||
|
export const CONTEXT_MENU_DATA_EXPIRY_MS = 5000;
|
||||||
|
|
||||||
export interface UseContextMenuDataOptions {
|
export interface UseContextMenuDataOptions {
|
||||||
/** 当前页面的功能标识 */
|
/** 当前页面的功能标识 */
|
||||||
@@ -29,7 +31,7 @@ export function useContextMenuData({ featureKey, onData }: UseContextMenuDataOpt
|
|||||||
|
|
||||||
if (data.featureKey !== featureKey) return;
|
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);
|
await storageUtil.remove(STORAGE_KEY);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user