Runner: Analyse-Skript und Datenschutz-Zusatzseite einsetzen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
add7fa4add
commit
7de9f1aaa3
6 files changed
+309
No files matched your search
@@ -0,0 +1,66 @@
|
||||
// Website-Analyse: Skript-Tag in veröffentlichte HTML-Dateien einsetzen und Datenschutz-Zusatzseite schreiben.
|
||||
import * as nodeFs from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
|
||||
export const RE_SITE_KEY = /^[a-z0-9]{16,40}$/;
|
||||
export const RE_COLOR = /^#[0-9a-f]{6}$/;
|
||||
const MAX_HTML_BYTES = 5 * 1024 * 1024;
|
||||
const PAGE = 'datenschutz-analyse.html';
|
||||
const TEMPLATE_URL = new URL('./templates/datenschutz-analyse.html', import.meta.url);
|
||||
|
||||
const esc = (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
||||
|
||||
export const isValidBase = (b) => typeof b === 'string' && /^https:\/\/[^\s"'<>]+\/$/.test(b);
|
||||
|
||||
export function scriptTag(analytics, baseUrl) {
|
||||
// Attributwerte nur aus geprüften Eingaben (Regex), daher ohne Maskierung sicher.
|
||||
return `<script data-wk-analytics src="${baseUrl}w.js" data-site="${analytics.siteKey}" data-color="${analytics.color}" defer></script>`;
|
||||
}
|
||||
|
||||
// Arbeitet auf latin1-Zeichenketten (1 Byte = 1 Zeichen), damit beliebige Kodierungen byte-genau erhalten bleiben.
|
||||
export function injectIntoHtml(buf, tag) {
|
||||
let s = buf.toString('latin1');
|
||||
const old = /<script\b[^>]*\bdata-wk-analytics\b[^>]*>\s*<\/script\s*>/gi;
|
||||
if (old.test(s)) {
|
||||
let first = true;
|
||||
s = s.replace(old, () => (first ? ((first = false), tag) : ''));
|
||||
} else {
|
||||
const head = s.search(/<\/head\s*>/i);
|
||||
if (head >= 0) s = s.slice(0, head) + tag + s.slice(head);
|
||||
else {
|
||||
const m = /<body\b[^>]*>/i.exec(s);
|
||||
if (m) { const at = m.index + m[0].length; s = s.slice(0, at) + tag + s.slice(at); } else s = tag + s;
|
||||
}
|
||||
}
|
||||
return Buffer.from(s, 'latin1');
|
||||
}
|
||||
|
||||
async function* walk(fs, dir) {
|
||||
for (const e of await fs.readdir(dir, { withFileTypes: true })) {
|
||||
if (e.name.startsWith('.') || e.name === 'node_modules') continue;
|
||||
const p = join(dir, e.name);
|
||||
if (e.isDirectory()) yield* walk(fs, p);
|
||||
else if (e.isFile() && /\.html?$/i.test(e.name)) yield p;
|
||||
}
|
||||
}
|
||||
|
||||
export async function injectAnalytics(dir, analytics, { baseUrl, onLine, fs = nodeFs } = {}) {
|
||||
if (!analytics) return;
|
||||
if (!RE_SITE_KEY.test(analytics.siteKey || '')) throw new Error('Analyse: ungültiger siteKey');
|
||||
if (!RE_COLOR.test(analytics.color || '')) throw new Error('Analyse: ungültige Farbe');
|
||||
if (!isValidBase(baseUrl)) throw new Error('Analyse: ungültige öffentliche Basis-URL');
|
||||
const tag = scriptTag(analytics, baseUrl);
|
||||
const p = analytics.privacy || {};
|
||||
const template = await fs.readFile(TEMPLATE_URL, 'utf8');
|
||||
const page = template.replace(/\{\{(company|address|email)\}\}/g, (_, k) => esc(p[k] ?? ''));
|
||||
await fs.writeFile(join(dir, PAGE), page, 'utf8');
|
||||
await fs.chmod(join(dir, PAGE), 0o644);
|
||||
let n = 0;
|
||||
for await (const file of walk(fs, dir)) {
|
||||
const rel = file.slice(dir.length + 1);
|
||||
if ((await fs.stat(file)).size > MAX_HTML_BYTES) { onLine?.(`Analyse: ${rel} übersprungen (größer als 5 MB)`); continue; }
|
||||
await fs.writeFile(file, injectIntoHtml(await fs.readFile(file), tag));
|
||||
n++;
|
||||
}
|
||||
onLine?.(`Analyse-Skript in ${n} HTML-Datei(en) eingesetzt`);
|
||||
}
|
||||
Reference in new issue
Block a user