Runner: Zeiten-Skript beim Veröffentlichen einsetzen
injectIntoHtml nimmt eine Marke je Tag (geprüft gegen data-wk-[a-z]+), injectZeiten setzt den data-wk-zeiten-Tag ohne Datenschutzseite ein, validateJob lässt nur den siteKey durch, ZEITEN_PUBLIC_BASE in der Konfiguration. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a198c472bd
commit
a2d11ad6c4
5 files changed
+297
-10
No files matched your search
@@ -20,6 +20,11 @@ export function scriptTag(analytics, baseUrl) {
|
||||
return `<script data-wk-analytics src="${baseUrl}w.js?s=${analytics.siteKey}" data-site="${analytics.siteKey}" data-color="${analytics.color}" defer></script>`;
|
||||
}
|
||||
|
||||
// Öffnungszeiten live: siteKey und Basis-URL werden vor dem Aufruf per Regex geprüft (injectZeiten), daher ohne Maskierung sicher.
|
||||
export function zeitenScriptTag(zeiten, baseUrl) {
|
||||
return `<script data-wk-zeiten src="${baseUrl}zeiten.js" data-site="${zeiten.siteKey}" defer></script>`;
|
||||
}
|
||||
|
||||
// Spannen, in denen Marker nichts zu suchen haben: Kommentare, script, style, textarea, template (ohne Verschachtelungs-Logik).
|
||||
function findSpans(s) {
|
||||
const spans = [];
|
||||
@@ -45,11 +50,16 @@ function findSpans(s) {
|
||||
return spans;
|
||||
}
|
||||
|
||||
const RE_MARKER = /^data-wk-[a-z]+$/;
|
||||
|
||||
// Arbeitet auf latin1-Zeichenketten (1 Byte = 1 Zeichen), damit beliebige Kodierungen byte-genau erhalten bleiben.
|
||||
export function injectIntoHtml(buf, tag) {
|
||||
// Die Marke (Attribut des Tags, z. B. data-wk-zeiten) erkennt einen früher eingesetzten Tag; sie wird geprüft, weil sie in einen regulären Ausdruck eingeht.
|
||||
export function injectIntoHtml(buf, tag, marker = 'data-wk-analytics') {
|
||||
if (typeof marker !== 'string' || !RE_MARKER.test(marker)) throw new Error('Ungültiger Marker');
|
||||
const reOld = new RegExp(`\\b${marker}\\b`, 'i');
|
||||
let s = buf.toString('latin1');
|
||||
const spans = findSpans(s);
|
||||
const olds = spans.filter((x) => x.kind === 'script' && /\bdata-wk-analytics\b/i.test(s.slice(x.start, s.indexOf('>', x.start) + 1)));
|
||||
const olds = spans.filter((x) => x.kind === 'script' && reOld.test(s.slice(x.start, s.indexOf('>', x.start) + 1)));
|
||||
if (olds.length) {
|
||||
for (let i = olds.length - 1; i >= 0; i--) s = s.slice(0, olds[i].start) + (i === 0 ? tag : '') + s.slice(olds[i].end);
|
||||
return Buffer.from(s, 'latin1');
|
||||
@@ -74,6 +84,18 @@ async function* walk(fs, dir) {
|
||||
}
|
||||
}
|
||||
|
||||
// Setzt den Tag in alle HTML-Dateien unterhalb von dir ein (Dateien über 5 MB bleiben unberührt) und liefert die Anzahl.
|
||||
async function injectTagIntoDir(fs, dir, tag, marker, label, onLine) {
|
||||
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?.(`${label}: ${rel} übersprungen (größer als 5 MB)`); continue; }
|
||||
await fs.writeFile(file, injectIntoHtml(await fs.readFile(file), tag, marker));
|
||||
n++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
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');
|
||||
@@ -86,12 +108,15 @@ export async function injectAnalytics(dir, analytics, { baseUrl, onLine, fs = no
|
||||
const page = withAddr.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++;
|
||||
}
|
||||
const n = await injectTagIntoDir(fs, dir, tag, 'data-wk-analytics', 'Analyse', onLine);
|
||||
onLine?.(`Analyse-Skript in ${n} HTML-Datei(en) eingesetzt`);
|
||||
}
|
||||
|
||||
// Öffnungszeiten live: nur der Skript-Tag, keine Datenschutzseite.
|
||||
export async function injectZeiten(dir, zeiten, { baseUrl, onLine, fs = nodeFs } = {}) {
|
||||
if (!zeiten) return;
|
||||
if (typeof zeiten.siteKey !== 'string' || !RE_SITE_KEY.test(zeiten.siteKey)) throw new Error('Zeiten: ungültiger siteKey');
|
||||
if (!isValidBase(baseUrl)) throw new Error('Zeiten: ungültige öffentliche Basis-URL');
|
||||
const n = await injectTagIntoDir(fs, dir, zeitenScriptTag(zeiten, baseUrl), 'data-wk-zeiten', 'Zeiten', onLine);
|
||||
onLine?.(`Zeiten-Skript in ${n} HTML-Datei(en) eingesetzt`);
|
||||
}
|
||||
@@ -2,8 +2,11 @@
|
||||
export function loadConfig(env = process.env) {
|
||||
const analyticsPublicBase = env.ANALYTICS_PUBLIC_BASE || 'https://project.webklar.com/a/';
|
||||
if (!/^https:\/\/[^\s"'<>]+\/$/.test(analyticsPublicBase)) throw new Error('ANALYTICS_PUBLIC_BASE muss mit https:// beginnen und mit / enden');
|
||||
const zeitenPublicBase = env.ZEITEN_PUBLIC_BASE || 'https://project.webklar.com/embed/';
|
||||
if (!/^https:\/\/[^\s"'<>]+\/$/.test(zeitenPublicBase)) throw new Error('ZEITEN_PUBLIC_BASE muss mit https:// beginnen und mit / enden');
|
||||
return {
|
||||
analyticsPublicBase,
|
||||
zeitenPublicBase,
|
||||
token: env.PREVIEW_RUNNER_TOKEN || '',
|
||||
giteaUrl: env.GITEA_URL || '',
|
||||
giteaToken: env.GITEA_TOKEN || '',
|
||||
|
||||
+3
-1
@@ -7,7 +7,7 @@ import { join, dirname, sep } from 'node:path';
|
||||
import { buildRunArgs } from './docker.js';
|
||||
import { detectFromDir } from './detect.js';
|
||||
import { readMemAvailable } from './server.js';
|
||||
import { injectAnalytics } from './analytics-inject.js';
|
||||
import { injectAnalytics, injectZeiten } from './analytics-inject.js';
|
||||
|
||||
const LOG_LINES = 300;
|
||||
const MAX_LINE = 2000;
|
||||
@@ -257,6 +257,8 @@ export function createJobs({ config, docker, fs: fsOver = {}, git, mem = readMem
|
||||
await ensureIndexHtml(fs, fresh, job.displayName || job.sub, onLine);
|
||||
// Analyse (optional): auf dem .new-Ordner vor dem Tausch; Fehler brechen die Veröffentlichung ab, die alte Preview bleibt (V4).
|
||||
if (job.analytics) await injectAnalytics(fresh, job.analytics, { baseUrl: config.analyticsPublicBase, onLine, fs });
|
||||
// Öffnungszeiten live: gleiche Runde, gleiche Fehlerfolge (Job failed, alte Preview bleibt). Nie im Dev-Pfad.
|
||||
if (job.zeiten) await injectZeiten(fresh, job.zeiten, { baseUrl: config.zeitenPublicBase, onLine, fs });
|
||||
try { await fs.rename(target, old); } catch (e) { if (e.code !== 'ENOENT') throw e; hadOld = false; }
|
||||
try { await fs.rename(fresh, target); } catch (e) {
|
||||
if (hadOld) await fs.rename(old, target).catch((e2) => onLine?.(`Rollback fehlgeschlagen: ${e2.message}`));
|
||||
|
||||
@@ -64,10 +64,19 @@ export function validateJob(body) {
|
||||
}
|
||||
analytics = { siteKey: a.siteKey, color: a.color, privacy };
|
||||
}
|
||||
// Öffnungszeiten live: nur der siteKey, Zusatzfelder werden verworfen.
|
||||
let zeiten;
|
||||
if (body.zeiten !== undefined && body.zeiten !== null) {
|
||||
const z = body.zeiten;
|
||||
if (!z || typeof z !== 'object' || Array.isArray(z)) return { ok: false, error: 'Ungültige Zeiten-Angaben' };
|
||||
if (typeof z.siteKey !== 'string' || !RE_SITE_KEY.test(z.siteKey)) return { ok: false, error: 'Ungültiger Zeiten-Schlüssel' };
|
||||
zeiten = { siteKey: z.siteKey };
|
||||
}
|
||||
// Whitelist: keine ungeprüften Client-Felder im Job
|
||||
const job = { sub, repo, sha, branch, mode, env };
|
||||
if (index !== undefined) job.index = index;
|
||||
if (displayName !== undefined) job.displayName = displayName;
|
||||
if (analytics !== undefined) job.analytics = analytics;
|
||||
if (zeiten !== undefined) job.zeiten = zeiten;
|
||||
return { ok: true, job };
|
||||
}
|
||||
Reference in new issue
Block a user