From fab643dfbe6b3607b787a88359b204f52ec48698 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Fri, 13 Feb 2026 23:00:47 +0900 Subject: [PATCH] fix #1235 --- src/app/service/service_worker/script.ts | 10 +- src/locales/ach-UG/translation.json | 6 +- src/locales/de-DE/translation.json | 2 + src/locales/en-US/translation.json | 6 +- src/locales/ja-JP/translation.json | 2 + src/locales/ru-RU/translation.json | 2 + src/locales/vi-VN/translation.json | 2 + src/locales/zh-CN/translation.json | 2 + src/locales/zh-TW/translation.json | 4 +- src/pages/install/App.tsx | 148 ++++++++++++++++++----- 10 files changed, 147 insertions(+), 37 deletions(-) diff --git a/src/app/service/service_worker/script.ts b/src/app/service/service_worker/script.ts index 227be28d1..2d610bcfd 100644 --- a/src/app/service/service_worker/script.ts +++ b/src/app/service/service_worker/script.ts @@ -266,7 +266,15 @@ export class ScriptService { action: { type: "redirect" as chrome.declarativeNetRequest.RuleActionType, redirect: { - regexSubstitution: `${installPageURL}?url=\\1`, + /** + * 核心设计: + * 使用 `<,\1,>` 作为特征锚点注入到重定向 URL 中。 + * 1. 引导格式化:利用 \1 提取正则捕获组内容。 + * 2. 编码探测:通过包裹特殊的定界符(尖括号和逗号),在目标页面解析时, + * 可以通过检测这些字符是否被转义(如变为 %3C, %2C)来精准判定 + * 浏览器底层触发的是哪种 URL 编码策略(Raw / encodeURI / encodeURIComponent)。 + */ + regexSubstitution: `${installPageURL}?url=<,\\1,>`, }, }, condition: condition, diff --git a/src/locales/ach-UG/translation.json b/src/locales/ach-UG/translation.json index cf254e349..7b984f867 100644 --- a/src/locales/ach-UG/translation.json +++ b/src/locales/ach-UG/translation.json @@ -325,8 +325,10 @@ "header_other_update": "crwdns12784:0crwdne12784:0" }, "downloading_status_text": "Downloading. Received {{bytes}}.", - "install_page_loading": "Installation page loading", - "invalid_page": "Invalid page", + "install_page_please_wait": "Please wait", + "install_page_loading": "Loading Installation Page", + "install_page_load_failed": "Failed to Load Installation Page", + "invalid_page": "Invalid Page", "background_script_tag": "crwdns8460:0crwdne8460:0", "scheduled_script_tag": "crwdns8462:0crwdne8462:0", "background_script": "crwdns8464:0crwdne8464:0", diff --git a/src/locales/de-DE/translation.json b/src/locales/de-DE/translation.json index 6ea643df8..dc389e7ea 100644 --- a/src/locales/de-DE/translation.json +++ b/src/locales/de-DE/translation.json @@ -334,7 +334,9 @@ "header_other_update": "Andere verfügbare Updates" }, "downloading_status_text": "Wird heruntergeladen. {{bytes}} empfangen.", + "install_page_please_wait": "Bitte warten Sie", "install_page_loading": "Installationsseite wird geladen", + "install_page_load_failed": "Installationsseite konnte nicht geladen werden", "invalid_page": "Ungültige Seite", "background_script_tag": "Dies ist ein Hintergrundskript", "scheduled_script_tag": "Dies ist ein geplantes Skript", diff --git a/src/locales/en-US/translation.json b/src/locales/en-US/translation.json index 1091314dd..006a3af2d 100644 --- a/src/locales/en-US/translation.json +++ b/src/locales/en-US/translation.json @@ -334,8 +334,10 @@ "header_other_update": "Other available updates" }, "downloading_status_text": "Downloading. Received {{bytes}}.", - "install_page_loading": "Installation page loading", - "invalid_page": "Invalid page", + "install_page_please_wait": "Please wait", + "install_page_loading": "Loading Installation Page", + "install_page_load_failed": "Failed to Load Installation Page", + "invalid_page": "Invalid Page", "background_script_tag": "This is a Background Script", "scheduled_script_tag": "This is a Scheduled Script", "background_script": "Background Script", diff --git a/src/locales/ja-JP/translation.json b/src/locales/ja-JP/translation.json index 346ac1524..68a3db516 100644 --- a/src/locales/ja-JP/translation.json +++ b/src/locales/ja-JP/translation.json @@ -334,7 +334,9 @@ "header_other_update": "その他の利用可能な更新" }, "downloading_status_text": "ダウンロード中。{{bytes}} を受信しました。", + "install_page_please_wait": "しばらくお待ちください", "install_page_loading": "インストールページを読み込み中", + "install_page_load_failed": "インストールページの読み込みに失敗しました", "invalid_page": "無効なページ", "background_script_tag": "これはバックグラウンドスクリプトです", "scheduled_script_tag": "これはスケジュールスクリプトです", diff --git a/src/locales/ru-RU/translation.json b/src/locales/ru-RU/translation.json index cc80fe49e..f3da00dc2 100644 --- a/src/locales/ru-RU/translation.json +++ b/src/locales/ru-RU/translation.json @@ -334,7 +334,9 @@ "header_other_update": "Другие доступные обновления" }, "downloading_status_text": "Загрузка. Получено {{bytes}}.", + "install_page_please_wait": "Пожалуйста, подождите", "install_page_loading": "Загрузка страницы установки", + "install_page_load_failed": "Не удалось загрузить страницу установки", "invalid_page": "Недействительная страница", "background_script_tag": "Это фоновый скрипт", "scheduled_script_tag": "Это запланированный скрипт", diff --git a/src/locales/vi-VN/translation.json b/src/locales/vi-VN/translation.json index f16febc99..ed482f3ed 100644 --- a/src/locales/vi-VN/translation.json +++ b/src/locales/vi-VN/translation.json @@ -334,7 +334,9 @@ "header_other_update": "Các bản cập nhật khác" }, "downloading_status_text": "Đang tải xuống. Đã nhận {{bytes}}.", + "install_page_please_wait": "Vui lòng chờ", "install_page_loading": "Đang tải trang cài đặt", + "install_page_load_failed": "Tải trang cài đặt thất bại", "invalid_page": "Trang không hợp lệ", "background_script_tag": "Đây là một script nền", "scheduled_script_tag": "Đây là một script hẹn giờ", diff --git a/src/locales/zh-CN/translation.json b/src/locales/zh-CN/translation.json index 508c9ea3d..80be7c663 100644 --- a/src/locales/zh-CN/translation.json +++ b/src/locales/zh-CN/translation.json @@ -334,7 +334,9 @@ "header_other_update": "其他可用更新" }, "downloading_status_text": "正在下载。已接收 {{bytes}}。", + "install_page_please_wait": "请稍等", "install_page_loading": "安装页面加载中", + "install_page_load_failed": "安装页面加载失败", "invalid_page": "无效页面", "background_script_tag": "这是一个后台脚本", "scheduled_script_tag": "这是一个定时脚本", diff --git a/src/locales/zh-TW/translation.json b/src/locales/zh-TW/translation.json index abb5c289d..c384e2ab4 100644 --- a/src/locales/zh-TW/translation.json +++ b/src/locales/zh-TW/translation.json @@ -334,7 +334,9 @@ "header_other_update": "其他可用更新" }, "downloading_status_text": "正在下載。已接收 {{bytes}}。", - "install_page_loading": "安裝頁載入中", + "install_page_please_wait": "請稍等", + "install_page_loading": "安裝頁面載入中", + "install_page_load_failed": "安裝頁面載入失敗", "invalid_page": "無效頁面", "background_script_tag": "這是一個背景腳本", "scheduled_script_tag": "這是一個排程腳本", diff --git a/src/pages/install/App.tsx b/src/pages/install/App.tsx index d27481a9c..8ce047257 100644 --- a/src/pages/install/App.tsx +++ b/src/pages/install/App.tsx @@ -648,49 +648,125 @@ function App() { }, [memoWatchFile]); // 检查是否有 uuid 或 file - const hasUUIDorFile = useMemo(() => { - return !!(searchParams.get("uuid") || searchParams.get("file")); - }, [searchParams]); + const hasValidSourceParam = !!(searchParams.get("uuid") || searchParams.get("file")); const urlHref = useMemo(() => { - try { - if (!hasUUIDorFile) { - const url = searchParams.get("url"); - if (url) { + if (!hasValidSourceParam) { + /** + * 逻辑说明: + * 在 chrome.declarativeNetRequest 规则中,我们使用 `<,\1,>` 作为占位符引导 API 进行参数填充。 + * 由于不同浏览器版本或配置对 URL 参数的自动编码(Auto-encoding)策略不一致, + * 我们通过检测该占位符的“被编码状态”来逆推浏览器采用了哪种编码方式。 + */ + let m; + let url; + try { + // 场景 1:URL 完全未编码。直接匹配原始特征符号 "<", ">" 和 "," + if ((m = /\burl=(<,.+,>)(&|$)/.exec(location.search)?.[1])) { + url = m; // 未被编码,取原始值。 + } + // 场景 2:URL 经过了部分编码(类似 encodeURI)。逗号 "," 未被编码,但尖括号被转义为 %3C, %3E + else if ((m = /\burl=(%3C,.+,%3E)(&|$)/.exec(location.search)?.[1])) { + url = decodeURI(m); + } + // 场景 3:URL 经过了完全编码(类似 encodeURIComponent)。逗号也被转义为 %2C + else if ((m = /\burl=(%3C%2C.+%2C%3E)(&|$)/.exec(location.search)?.[1])) { + url = decodeURIComponent(m); + } + } catch { + // ignored + } + // 如果正则匹配/标准解码失败,回退到标准的 searchParams 获取方式 (浏览器会自行理解和解码不规范的编码) + if (!url) url = searchParams.get("url") || ""; // fallback + // 移除人工注入的特征锚点 <, ,>,提取真实的 URL 内容 + url = url.replace(/^<,(.+),>$/, "$1"); // 去掉 <, ,> + if (url) { + try { const urlObject = new URL(url); + // 验证解析后的 URL 是否具备核心要素,确保安全性与合法性 if (urlObject.protocol && urlObject.hostname && urlObject.pathname) { - return urlObject.href; + return url; } + } catch { + // ignored } } - } catch { - // ignored } return ""; - }, [hasUUIDorFile, searchParams]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [hasValidSourceParam, searchParams.get("url")]); const [fetchingState, setFetchingState] = useState({ loadingStatus: "", errorStatus: "", }); - const loadURLAsync = async (urlHref: string) => { + const getCandidateUrls = (targetUrlHref: string) => { + const inputU = new URL(targetUrlHref); + const extraCandidateUrls = new Set(); + extraCandidateUrls.add(inputU.href); + + const isGreasyForkOrSleazyFork = /[.-](greasyfork|sleazyfork)\.org$/.test(inputU.hostname); + + if (isGreasyForkOrSleazyFork) { + // example: + // CASE 1 + // raw 'https://update.greasyfork.org/scripts/550295/100%解锁CSDN文库vip文章阅读限制.user.js' + // encoded 'https://update.greasyfork.org/scripts/550295/100%25%E8%A7%A3%E9%94%81CSDN%E6%96%87%E5%BA%93vip%E6%96%87%E7%AB%A0%E9%98%85%E8%AF%BB%E9%99%90%E5%88%B6.user.js' + // correct 'https://update.greasyfork.org/scripts/550295/100%25%E8%A7%A3%E9%94%81CSDN%E6%96%87%E5%BA%93vip%E6%96%87%E7%AB%A0%E9%98%85%E8%AF%BB%E9%99%90%E5%88%B6.user.js' + // CASE 2 + // raw 'https://update.greasyfork.org/scripts/519037/Nexus No Wait ++.user.js' + // encoded 'https://update.greasyfork.org/scripts/519037/Nexus%20No%20Wait%20++.user.js' + // correct 'https://update.greasyfork.org/scripts/519037/Nexus%20No%20Wait%20%2B%2B.user.js' + try { + const url = targetUrlHref.replace(/([^/]+\.js)/, encodeURIComponent); + extraCandidateUrls.add(new URL(url).href); + } catch (e) { + // can skip if it cannot be converted using decodeURI + console.warn(e); // just a warning for debug purpose. + } + } + + return [...extraCandidateUrls]; + }; + + const loadURLAsync = async (candidateUrls: string[]) => { + // 1. 定义获取单个脚本的内部逻辑,负责处理进度条与单次错误 + const fetchValidScript = async () => { + let firstError: unknown; + for (const url of candidateUrls) { + try { + const result = await fetchScriptBody(url, { + onProgress: (info: { receivedLength: number }) => { + setFetchingState((prev) => ({ + ...prev, + loadingStatusText: t("downloading_status_text", { bytes: formatBytes(info.receivedLength) }), + })); + }, + }); + if (result.code && result.metadata) { + return { result, url }; // 找到有效的立即返回 + } + } catch (e) { + if (!firstError) firstError = e; + } + } + // 如果循环结束都没成功,抛出第一个捕获到的错误或预设错误 + throw firstError || new Error(t("install_page_load_failed")); + }; + try { - const { code, metadata } = await fetchScriptBody(urlHref, { - onProgress: (info: { receivedLength: number }) => { - setFetchingState((prev) => ({ - ...prev, - loadingStatus: t("downloading_status_text", { bytes: `${formatBytes(info.receivedLength)}` }), - })); - }, - }); - const update = false; + // 2. 执行获取 + const { result, url } = await fetchValidScript(); + const { code, metadata } = result; + + // 3. 处理数据与缓存 const uuid = uuidv4(); - const url = urlHref; - const upsertBy = "user"; + const scriptData = [false, createScriptInfo(uuid, code, url, "user", metadata)]; + + await cacheInstance.set(`${CACHE_KEY_SCRIPT_INFO}${uuid}`, scriptData); - const si = [update, createScriptInfo(uuid, code, url, upsertBy, metadata)]; - await cacheInstance.set(`${CACHE_KEY_SCRIPT_INFO}${uuid}`, si); + // 4. 更新导向 setSearchParams( (prev) => { prev.delete("url"); @@ -700,21 +776,31 @@ function App() { { replace: true } ); } catch (err: any) { - const errMessage = `${err.message || err}`; + // 5. 统一错误处理 setFetchingState((prev) => ({ ...prev, - loadingStatus: "", - errorStatus: errMessage, + loadingStatusText: "", + errorStatusText: String(err?.message || err), })); } }; + const handleUrlChangeAndFetch = (targetUrlHref: string) => { + setFetchingState((prev) => ({ + ...prev, + loadingStatusText: t("install_page_please_wait"), + })); + const candidateUrls = getCandidateUrls(targetUrlHref); + loadURLAsync(candidateUrls); + }; + + // 有 url 的话下载内容 useEffect(() => { - if (!urlHref) return; - loadURLAsync(urlHref); + if (urlHref) handleUrlChangeAndFetch(urlHref); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [urlHref]); - if (!hasUUIDorFile) { + if (!hasValidSourceParam) { return urlHref ? (