feat(security): 更新 Content-Security-Policy 策略配置

新增对 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,
解决潜在的内容安全策略冲突问题,提高代码整洁度和维护性。
This commit is contained in:
Abner
2025-11-01 19:38:07 +08:00
parent 91efda93bc
commit 7bdcbfcaee
9 changed files with 199 additions and 49 deletions

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="eSIM 工具集,支持 Giffgaff 与 Simyo 的 eSIM 申请与管理、二维码生成及验证码处理。">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://www.googletagmanager.com; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; font-src 'self' data:; connect-src 'self' https://qrcode.show https://api.qrserver.com https://appapi.simyo.nl https://api.giffgaff.com https://id.giffgaff.com https://publicapi.giffgaff.com; img-src 'self' data: https:; frame-src 'none';">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://www.googletagmanager.com; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; font-src 'self' data:; connect-src 'self' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://www.google-analytics.com https://www.googletagmanager.com https://qrcode.show https://api.qrserver.com https://appapi.simyo.nl https://api.giffgaff.com https://id.giffgaff.com https://publicapi.giffgaff.com https://challenges.cloudflare.com; img-src 'self' data: https:; frame-src 'self' https://challenges.cloudflare.com;">
<title>eSIM Tools - Giffgaff & Simyo</title>
<link rel="manifest" href="/manifest.webmanifest">
<meta name="theme-color" content="#6366f1">
@@ -357,10 +357,7 @@
<script type="module" src="/src/js/bootstrap-footer.js"></script>
<!-- 国际化初始化 -->
<script type="module">
import { initI18n } from '/src/js/modules/i18n.js';
initI18n('home');
</script>
<script type="module" src="/src/js/init-home-i18n.js"></script>
<!-- 性能优化脚本 -->

View File

@@ -11,7 +11,7 @@
<link rel="preconnect" href="https://api.giffgaff.com" crossorigin>
<link rel="preconnect" href="https://id.giffgaff.com" crossorigin>
<link rel="preconnect" href="https://publicapi.giffgaff.com" crossorigin>
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; font-src 'self' data: https://cdnjs.cloudflare.com; connect-src 'self' https://qrcode.show https://api.qrserver.com https://appapi.simyo.nl https://api.giffgaff.com https://id.giffgaff.com https://publicapi.giffgaff.com; img-src 'self' data: https:; frame-src 'none';">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://challenges.cloudflare.com; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; font-src 'self' data: https://cdnjs.cloudflare.com; connect-src 'self' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://www.google-analytics.com https://www.googletagmanager.com https://qrcode.show https://api.qrserver.com https://appapi.simyo.nl https://api.giffgaff.com https://id.giffgaff.com https://publicapi.giffgaff.com https://challenges.cloudflare.com; img-src 'self' data: https:; frame-src 'self' https://challenges.cloudflare.com;">
<title>Giffgaff eSIM 申请工具</title>
<link rel="manifest" href="/manifest.webmanifest">
<meta name="theme-color" content="#ffcc00">
@@ -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') {

View File

@@ -15,7 +15,7 @@
<link rel="preconnect" href="https://publicapi.giffgaff.com" crossorigin>
<!-- CSP策略 -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://challenges.cloudflare.com; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; font-src 'self' data: https://cdnjs.cloudflare.com; connect-src 'self' https://qrcode.show https://api.qrserver.com https://appapi.simyo.nl https://api.giffgaff.com https://id.giffgaff.com https://publicapi.giffgaff.com https://challenges.cloudflare.com; img-src 'self' data: https:; frame-src 'none';">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://challenges.cloudflare.com; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; font-src 'self' data: https://cdnjs.cloudflare.com; connect-src 'self' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://www.google-analytics.com https://www.googletagmanager.com https://qrcode.show https://api.qrserver.com https://appapi.simyo.nl https://api.giffgaff.com https://id.giffgaff.com https://publicapi.giffgaff.com https://challenges.cloudflare.com; img-src 'self' data: https:; frame-src 'self' https://challenges.cloudflare.com;">
<title>Giffgaff eSIM 申请工具</title>
<link rel="manifest" href="/manifest.webmanifest">
@@ -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') {

4
src/js/init-home-i18n.js Normal file
View File

@@ -0,0 +1,4 @@
// 首页国际化入口脚本:避免内联脚本触发 CSP 限制
import { initI18n } from './modules/i18n.js';
initI18n('home');

View File

@@ -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();
}
}

View File

@@ -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();
}
}
}

View File

@@ -232,6 +232,9 @@ export const TRANSLATIONS = {
'home.simyo.features.confirm': '安装确认功能',
'home.simyo.cta': '使用 Simyo 工具',
'home.footer.html': '<i class=\"fas fa-info-circle me-2\" aria-hidden=\"true\"></i> 本工具仅供个人使用,请遵守相关服务条款。<br><i class=\"fas fa-shield-alt me-2\" aria-hidden=\"true\"></i> 所有数据处理均在本地进行,保护您的隐私安全。',
'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': '<i class=\"fas fa-info-circle me-2\" aria-hidden=\"true\"></i> For personal use only. Please follow the related terms of service.<br><i class=\"fas fa-shield-alt me-2\" aria-hidden=\"true\"></i> 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.',

View File

@@ -5,7 +5,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="color-scheme" content="light dark" />
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; font-src 'self' data: https://cdnjs.cloudflare.com; connect-src 'self' https://qrcode.show https://api.qrserver.com https://appapi.simyo.nl https://api.giffgaff.com https://id.giffgaff.com https://publicapi.giffgaff.com; img-src 'self' data: https:; frame-src 'none';">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; font-src 'self' data: https://cdnjs.cloudflare.com; connect-src 'self' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://www.google-analytics.com https://www.googletagmanager.com https://qrcode.show https://api.qrserver.com https://appapi.simyo.nl https://api.giffgaff.com https://id.giffgaff.com https://publicapi.giffgaff.com https://challenges.cloudflare.com; img-src 'self' data: https:; frame-src 'self' https://challenges.cloudflare.com;">
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin>
<title>Simyo NL eSIM 申请工具</title>

View File

@@ -13,7 +13,7 @@
<link rel="preconnect" href="https://appapi.simyo.nl" crossorigin>
<!-- CSP策略 -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; font-src 'self' data: https://cdnjs.cloudflare.com; connect-src 'self' https://qrcode.show https://api.qrserver.com https://appapi.simyo.nl; img-src 'self' data: https:; frame-src 'none';">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; font-src 'self' data: https://cdnjs.cloudflare.com; connect-src 'self' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://www.google-analytics.com https://www.googletagmanager.com https://qrcode.show https://api.qrserver.com https://appapi.simyo.nl https://challenges.cloudflare.com; img-src 'self' data: https:; frame-src 'self' https://challenges.cloudflare.com;">
<title>Simyo NL eSIM 申请工具</title>
<link rel="manifest" href="/manifest.webmanifest">