From 7bdcbfcaeed5e26649ca926b67df71ec8a0a055b Mon Sep 17 00:00:00 2001 From: Abner <22141172+Silentely@users.noreply.github.com> Date: Sat, 1 Nov 2025 19:38:07 +0800 Subject: [PATCH] =?UTF-8?q?feat(security):=20=E6=9B=B4=E6=96=B0=20Content-?= =?UTF-8?q?Security-Policy=20=E7=AD=96=E7=95=A5=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增对 Google Analytics、Google Tag Manager 和 Cloudflare Turnstile 的支持, 同时调整 frame-src 允许加载特定域名内容。此变更应用于多个页面的 CSP 设置, 包括 index.html、giffgaff 和 simyo 相关页面,提升安全性与功能兼容性。 fix(turnstile): 改进 Cloudflare Turnstile 初始化逻辑 增强 widget 加载稳定性并优化 token 刷新机制,增加错误处理和状态管理, 防止重复执行并适配浏览器可见性变化。提取独立脚本确保 CSP 兼容性, 避免内联代码触发安全策略限制。 feat(i18n): 实现页脚组件国际化支持 为 copyright 文本、GitHub 链接及容器添加多语言切换能力, 引入翻译键值绑定与动态更新机制,并完善中英文对照文本定义。 refactor(footer): 抽离页脚国际化相关逻辑 将原有静态文本生成改为参数化解析方式,分离品牌名称与年份范围计算逻辑, 便于后续扩展更多本地化字段与样式控制选项。 chore(scripts): 创建首页专用 i18n 初始化模块 消除主页中的内联 JavaScript 脚本片段,将其封装成外部可引用的 ES Module, 解决潜在的内容安全策略冲突问题,提高代码整洁度和维护性。 --- index.html | 7 +- src/giffgaff/giffgaff_complete_esim.html | 25 +++++- src/giffgaff/giffgaff_modular.html | 25 +++++- src/js/init-home-i18n.js | 4 + src/js/main.js | 96 ++++++++++++++++++++---- src/js/modules/footer.js | 81 +++++++++++++++----- src/js/modules/i18n-data.js | 6 ++ src/simyo/simyo_complete_esim.html | 2 +- src/simyo/simyo_modular.html | 2 +- 9 files changed, 199 insertions(+), 49 deletions(-) create mode 100644 src/js/init-home-i18n.js diff --git a/index.html b/index.html index d38ec4d..41bcce7 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ - + eSIM Tools - Giffgaff & Simyo @@ -357,10 +357,7 @@ - + diff --git a/src/giffgaff/giffgaff_complete_esim.html b/src/giffgaff/giffgaff_complete_esim.html index 3ccafca..0b8868c 100644 --- a/src/giffgaff/giffgaff_complete_esim.html +++ b/src/giffgaff/giffgaff_complete_esim.html @@ -11,7 +11,7 @@ - + Giffgaff eSIM 申请工具 @@ -3648,15 +3648,32 @@ try { if (!window.turnstile || !siteKey) { setTimeout(boot, 300); return; } var c = document.createElement('div'); + c.style.display = 'none'; + c.setAttribute('aria-hidden', 'true'); document.body.appendChild(c); + var isExecuting = false; + var hasExecuted = false; + var markIdle = function() { isExecuting = false; }; + var exec = function() { + if (!window.turnstile || isExecuting) { return; } + isExecuting = true; + if (hasExecuted) { try { window.turnstile.reset(id); } catch (_) {} } + hasExecuted = true; + try { window.turnstile.execute(id); } catch (e) { markIdle(); } + }; var id = window.turnstile.render(c, { sitekey: siteKey, size: 'invisible', - callback: function (token) { window.__cfTurnstileToken = token; } + callback: function (token) { window.__cfTurnstileToken = token; markIdle(); }, + 'error-callback': markIdle, + 'timeout-callback': markIdle, + 'expired-callback': function () { markIdle(); exec(); } }); - var exec = function() { try { window.turnstile.execute(id); } catch (e) {} }; exec(); - setInterval(exec, 110000); + setInterval(function () { + if (document.visibilityState === 'hidden') { return; } + exec(); + }, 110000); } catch (e) { setTimeout(boot, 500); } } if (document.readyState === 'loading') { diff --git a/src/giffgaff/giffgaff_modular.html b/src/giffgaff/giffgaff_modular.html index 5b3004e..9267669 100644 --- a/src/giffgaff/giffgaff_modular.html +++ b/src/giffgaff/giffgaff_modular.html @@ -15,7 +15,7 @@ - + Giffgaff eSIM 申请工具 @@ -764,15 +764,32 @@ try { if (!window.turnstile || !siteKey) { setTimeout(boot, 300); return; } var c = document.createElement('div'); + c.style.display = 'none'; + c.setAttribute('aria-hidden', 'true'); document.body.appendChild(c); + var isExecuting = false; + var hasExecuted = false; + var markIdle = function() { isExecuting = false; }; + var exec = function() { + if (!window.turnstile || isExecuting) { return; } + isExecuting = true; + if (hasExecuted) { try { window.turnstile.reset(id); } catch (_) {} } + hasExecuted = true; + try { window.turnstile.execute(id); } catch (e) { markIdle(); } + }; var id = window.turnstile.render(c, { sitekey: siteKey, size: 'invisible', - callback: function (token) { window.__cfTurnstileToken = token; } + callback: function (token) { window.__cfTurnstileToken = token; markIdle(); }, + 'error-callback': markIdle, + 'timeout-callback': markIdle, + 'expired-callback': function () { markIdle(); exec(); } }); - var exec = function() { try { window.turnstile.execute(id); } catch (e) {} }; exec(); - setInterval(exec, 110000); + setInterval(function () { + if (document.visibilityState === 'hidden') { return; } + exec(); + }, 110000); } catch (e) { setTimeout(boot, 500); } } if (document.readyState === 'loading') { diff --git a/src/js/init-home-i18n.js b/src/js/init-home-i18n.js new file mode 100644 index 0000000..8083343 --- /dev/null +++ b/src/js/init-home-i18n.js @@ -0,0 +1,4 @@ +// 首页国际化入口脚本:避免内联脚本触发 CSP 限制 +import { initI18n } from './modules/i18n.js'; + +initI18n('home'); diff --git a/src/js/main.js b/src/js/main.js index bb937f9..a8c87be 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -18,43 +18,107 @@ window.TURNSTILE_SITE_KEY = (typeof process !== 'undefined' && process.env && pr ? process.env.TURNSTILE_SITE_KEY : '0x4AAAAAABqjeVscZAiqB11B'; + if (window.TURNSTILE_SITE_KEY) { - const s = document.createElement('script'); - s.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js'; - s.async = true; - s.defer = true; - document.head.appendChild(s); + const ensureLoaderScript = () => { + if (window.turnstile) { + return true; + } + const selector = 'script[src="https://challenges.cloudflare.com/turnstile/v0/api.js"]'; + const existing = document.querySelector(selector); + if (existing) { + if (!existing.dataset.turnstileLoader) { + existing.dataset.turnstileLoader = 'true'; + } + return false; + } + const script = document.createElement('script'); + script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js'; + script.async = true; + script.defer = true; + script.dataset.turnstileLoader = 'true'; + document.head.appendChild(script); + return false; + }; // 定义回调,保存 token 供 /bff/* 请求使用 window.onTurnstileDone = (token) => { window.__cfTurnstileToken = token; }; - // 初始化不可见 Turnstile 并立即执行获取 token + let widgetId = null; + let hasExecuted = false; + let executing = false; + + const markIdle = () => { executing = false; }; + + const handleToken = (token) => { + window.__cfTurnstileToken = token; + markIdle(); + }; + + const runExecute = () => { + if (!window.turnstile || widgetId == null || executing) { + return; + } + executing = true; + if (hasExecuted) { + try { window.turnstile.reset(widgetId); } catch (_) {} + } + hasExecuted = true; + try { + window.turnstile.execute(widgetId); + } catch (_) { + markIdle(); + } + }; + const initTurnstile = () => { if (!window.turnstile) { setTimeout(initTurnstile, 300); return; } const container = document.createElement('div'); + container.style.display = 'none'; + container.setAttribute('aria-hidden', 'true'); document.body.appendChild(container); - const widgetId = window.turnstile.render(container, { + widgetId = window.turnstile.render(container, { sitekey: window.TURNSTILE_SITE_KEY, size: 'invisible', - callback: (token) => { - window.__cfTurnstileToken = token; + callback: handleToken, + 'error-callback': markIdle, + 'timeout-callback': markIdle, + 'expired-callback': () => { + markIdle(); + runExecute(); } }); - // 主动执行以获取 token(并定期刷新) - const exec = () => { try { window.turnstile.execute(widgetId); } catch (_) {} }; - exec(); - setInterval(exec, 110000); // 约每110秒刷新一次 + runExecute(); + setInterval(() => { + if (document.visibilityState === 'hidden') return; + runExecute(); + }, 110000); }; - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', initTurnstile); + const boot = () => { + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initTurnstile, { once: true }); + } else { + initTurnstile(); + } + }; + + if (ensureLoaderScript()) { + boot(); } else { - initTurnstile(); + const waitForScript = () => { + if (window.turnstile) { + boot(); + return; + } + setTimeout(waitForScript, 150); + }; + waitForScript(); } } diff --git a/src/js/modules/footer.js b/src/js/modules/footer.js index 4d49b60..5547461 100644 --- a/src/js/modules/footer.js +++ b/src/js/modules/footer.js @@ -1,20 +1,65 @@ // 统一页脚组件:自动注入到页面底部 import './config.js'; +import { t, onLocaleChange } from './i18n.js'; -function getCopyrightText() { +function resolveFooterParams() { + const currentYear = new Date().getFullYear(); try { const cfg = window.__APP_CONFIG__ || {}; const brand = cfg.brand || 'eSIM Tools'; - const since = Number(cfg.since) || new Date().getFullYear(); - const current = new Date().getFullYear(); - const yearText = since && since < current ? `${since}–${current}` : `${current}`; - return `© ${yearText} ${brand}. 保留所有权利。`; + const since = Number(cfg.since) || currentYear; + const yearRange = since && since < currentYear ? `${since}–${currentYear}` : `${currentYear}`; + return { brand, yearRange }; } catch (_) { - const current = new Date().getFullYear(); - return `© ${current} eSIM Tools. 保留所有权利。`; + return { brand: 'eSIM Tools', yearRange: `${currentYear}` }; } } +function translateOrFallback(key, fallback, params) { + try { + const value = params ? t(key, params) : t(key); + if (typeof value === 'string' && value !== key) { + return value; + } + } catch (_) {} + return fallback; +} + +function bindGithubLink(link) { + const apply = () => { + const aria = translateOrFallback('footer.github.ariaLabel', '在 GitHub 上查看源码'); + link.setAttribute('aria-label', aria); + }; + apply(); + onLocaleChange(apply); +} + +function bindContainerAria(container) { + const apply = () => { + const aria = translateOrFallback('footer.container.ariaLabel', '版权与声明'); + container.setAttribute('aria-label', aria); + }; + apply(); + onLocaleChange(apply); +} + +function bindCopyright(span, options = {}) { + const custom = typeof options.text === 'string' ? options.text.trim() : ''; + if (custom) { + span.textContent = custom; + return; + } + + const apply = () => { + const params = resolveFooterParams(); + const fallback = `© ${params.yearRange} ${params.brand}. 保留所有权利。`; + span.textContent = translateOrFallback('footer.copyright', fallback, params); + }; + + apply(); + onLocaleChange(apply); +} + export function injectFooter(options = {}) { if (document.querySelector('footer[data-component="app-footer"]')) return; @@ -30,38 +75,38 @@ export function injectFooter(options = {}) { div.style.justifyContent = 'center'; div.style.gap = '12px'; div.style.marginTop = '8px'; - + const span = document.createElement('span'); - span.textContent = options.text || getCopyrightText(); - + bindCopyright(span, options); + const githubLink = document.createElement('a'); githubLink.href = 'https://github.com/Silentely/eSIM-Tools'; githubLink.target = '_blank'; githubLink.rel = 'noopener noreferrer'; - githubLink.setAttribute('aria-label', '在 GitHub 上查看源码'); githubLink.style.display = 'flex'; githubLink.style.alignItems = 'center'; githubLink.style.justifyContent = 'center'; githubLink.style.color = '#6b7280'; githubLink.style.textDecoration = 'none'; githubLink.style.transition = 'color 0.2s ease'; - + const githubIcon = document.createElement('i'); githubIcon.className = 'fab fa-github'; githubIcon.style.fontSize = '18px'; - + githubLink.addEventListener('mouseenter', () => { githubLink.style.color = '#1f2937'; }); - + githubLink.addEventListener('mouseleave', () => { githubLink.style.color = '#6b7280'; }); - + githubLink.appendChild(githubIcon); + bindGithubLink(githubLink); div.appendChild(span); div.appendChild(githubLink); - + return div; }; @@ -75,7 +120,6 @@ export function injectFooter(options = {}) { // 情形 2 / 3:新建组件化 footer const container = document.createElement('footer'); container.setAttribute('role', 'contentinfo'); - container.setAttribute('aria-label', '版权与声明'); container.setAttribute('data-component', 'app-footer'); container.style.cssText = [ 'margin-top:40px', @@ -86,6 +130,7 @@ export function injectFooter(options = {}) { 'font-size:14px' ].join(';'); container.appendChild(createCopyrightNode()); + bindContainerAria(container); const pickTarget = () => { return ( @@ -108,4 +153,4 @@ export function autoInjectFooter() { } else { run(); } -} \ No newline at end of file +} diff --git a/src/js/modules/i18n-data.js b/src/js/modules/i18n-data.js index 624159e..1d25356 100644 --- a/src/js/modules/i18n-data.js +++ b/src/js/modules/i18n-data.js @@ -232,6 +232,9 @@ export const TRANSLATIONS = { 'home.simyo.features.confirm': '安装确认功能', 'home.simyo.cta': '使用 Simyo 工具', 'home.footer.html': ' 本工具仅供个人使用,请遵守相关服务条款。
所有数据处理均在本地进行,保护您的隐私安全。', + 'footer.container.ariaLabel': '版权与声明', + 'footer.github.ariaLabel': '在 GitHub 上查看源码', + 'footer.copyright': '© {yearRange} {brand}。保留所有权利。', 'giffgaff.meta.title': 'Giffgaff eSIM 申请工具', 'giffgaff.meta.description': 'Giffgaff eSIM 工具,支持 OAuth 登录、MFA 验证、二维码生成与设备更换。', @@ -454,6 +457,9 @@ export const TRANSLATIONS = { 'home.simyo.features.confirm': 'Installation confirmation', 'home.simyo.cta': 'Launch Simyo tool', 'home.footer.html': ' For personal use only. Please follow the related terms of service.
All data processing happens locally to keep your privacy safe.', + 'footer.container.ariaLabel': 'Copyright and disclaimer', + 'footer.github.ariaLabel': 'View source on GitHub', + 'footer.copyright': '© {yearRange} {brand}. All rights reserved.', 'giffgaff.meta.title': 'Giffgaff eSIM Application Tool', 'giffgaff.meta.description': 'Giffgaff eSIM toolkit covering OAuth login, MFA verification, QR generation, and device change workflows.', diff --git a/src/simyo/simyo_complete_esim.html b/src/simyo/simyo_complete_esim.html index 3d35f76..8224d60 100644 --- a/src/simyo/simyo_complete_esim.html +++ b/src/simyo/simyo_complete_esim.html @@ -5,7 +5,7 @@ - + Simyo NL eSIM 申请工具 diff --git a/src/simyo/simyo_modular.html b/src/simyo/simyo_modular.html index 63017e2..a0cfd3b 100644 --- a/src/simyo/simyo_modular.html +++ b/src/simyo/simyo_modular.html @@ -13,7 +13,7 @@ - + Simyo NL eSIM 申请工具