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 申请工具