Runner: Analyse-Einsetzen ignoriert Kommentare/Skripte (Fix Runde 1)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
18d37d1e01
commit
9fa05e23d1
4 files changed
+79
-16
No files matched your search
@@ -17,20 +17,47 @@ export function scriptTag(analytics, baseUrl) {
|
||||
return `<script data-wk-analytics src="${baseUrl}w.js" data-site="${analytics.siteKey}" data-color="${analytics.color}" defer></script>`;
|
||||
}
|
||||
|
||||
// Spannen, in denen Marker nichts zu suchen haben: Kommentare, script, style, textarea, template (ohne Verschachtelungs-Logik).
|
||||
function findSpans(s) {
|
||||
const spans = [];
|
||||
const re = /<!--|<(script|style|textarea|template)\b/gi;
|
||||
let m;
|
||||
while ((m = re.exec(s))) {
|
||||
let end;
|
||||
let kind = 'raw';
|
||||
if (m[0] === '<!--') {
|
||||
const e = s.indexOf('-->', m.index + 4);
|
||||
end = e < 0 ? s.length : e + 3;
|
||||
kind = 'comment';
|
||||
} else {
|
||||
const close = new RegExp(`</${m[1]}\\s*>`, 'ig');
|
||||
close.lastIndex = m.index + m[0].length;
|
||||
const c = close.exec(s);
|
||||
end = c ? c.index + c[0].length : s.length;
|
||||
if (m[1].toLowerCase() === 'script') kind = 'script';
|
||||
}
|
||||
spans.push({ start: m.index, end, kind });
|
||||
re.lastIndex = end;
|
||||
}
|
||||
return spans;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
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)));
|
||||
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');
|
||||
}
|
||||
let masked = s;
|
||||
for (const x of spans) masked = masked.slice(0, x.start) + ' '.repeat(x.end - x.start) + masked.slice(x.end);
|
||||
const head = masked.search(/<\/head\s*>/i);
|
||||
if (head >= 0) s = s.slice(0, head) + tag + s.slice(head);
|
||||
else {
|
||||
const m = /<body\b[^>]*>/i.exec(masked);
|
||||
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');
|
||||
}
|
||||
|
||||
+1
-1
@@ -256,7 +256,7 @@ export function createJobs({ config, docker, fs: fsOver = {}, git, mem = readMem
|
||||
await fs.copyTree(fs, srcDir, fresh, { skipGit, skipNodeModules });
|
||||
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 || 'https://project.webklar.com/a/', onLine, fs });
|
||||
if (job.analytics) await injectAnalytics(fresh, job.analytics, { baseUrl: config.analyticsPublicBase, 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}`));
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
// Strikte Eingabeprüfung für Runner-Jobs.
|
||||
import { RE_SITE_KEY, RE_COLOR } from './analytics-inject.js';
|
||||
const RE_SUB = /^[a-z0-9-]{1,63}$/;
|
||||
const RE_REPO = /^[\w.-]+\/[\w.-]+$/;
|
||||
const RE_SHA = /^[0-9a-f]{7,40}$/;
|
||||
const RE_BRANCH = /^[\w./-]{1,100}$/;
|
||||
const RE_ENV = /^[A-Z_][A-Z0-9_]*$/;
|
||||
const RE_SITE_KEY = /^[a-z0-9]{16,40}$/;
|
||||
const RE_COLOR = /^#[0-9a-f]{6}$/;
|
||||
const MODES = ['auto', 'build', 'dev'];
|
||||
|
||||
export const isValidRunId = (s) => typeof s === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s);
|
||||
@@ -58,10 +57,10 @@ export function validateJob(body) {
|
||||
const privacy = {};
|
||||
for (const [k, max] of [['company', 200], ['address', 300], ['email', 200]]) {
|
||||
const v = pr[k];
|
||||
if (k === 'address' && (v === undefined || (typeof v === 'string' && !v.trim()))) { privacy[k] = ''; continue; }
|
||||
// eslint-disable-next-line no-control-regex
|
||||
if (k === 'address' && (v === undefined || v === '')) { privacy[k] = ''; continue; }
|
||||
if (typeof v !== 'string' || !v.trim() || v.length > max || /[\x00-\x1f\x7f]/.test(v)) return { ok: false, error: `Ungültige Analyse-Datenschutzangabe: ${k}` };
|
||||
privacy[k] = v;
|
||||
privacy[k] = v.trim();
|
||||
}
|
||||
analytics = { siteKey: a.siteKey, color: a.color, privacy };
|
||||
}
|
||||
|
||||
@@ -185,3 +185,40 @@ test('leere Adresse: keine leere Zeile, kein Platzhalterrest', async () => {
|
||||
assert.ok(p.includes('Muster GmbH<br>E-Mail:'));
|
||||
assert.ok(!p.includes('{{') && !p.includes('<br><br>'));
|
||||
});
|
||||
|
||||
test('Marker in script-String, Kommentar, template: Tag landet an der echten Stelle', async () => {
|
||||
const d = dir();
|
||||
put(d, 'a.html', '<head><script>var t="</head>";</script></head><body></body>');
|
||||
put(d, 'b.html', '<!-- </head> --><head></head>');
|
||||
put(d, 'c.html', '<template><body></template><p>x</p>');
|
||||
put(d, 'd.html', '<style>/* </head> */</style><head></head>');
|
||||
await injectAnalytics(d, A(), { baseUrl: BASE });
|
||||
assert.equal(get(d, 'a.html'), `<head><script>var t="</head>";</script>${TAG}</head><body></body>`);
|
||||
assert.equal(get(d, 'b.html'), `<!-- </head> --><head>${TAG}</head>`);
|
||||
assert.equal(get(d, 'c.html'), `${TAG}<template><body></template><p>x</p>`);
|
||||
assert.equal(get(d, 'd.html'), `<style>/* </head> */</style><head>${TAG}</head>`);
|
||||
});
|
||||
|
||||
test('alter Tag im Kommentar wird nicht ersetzt; echter wird genau einmal eingesetzt', async () => {
|
||||
const d = dir();
|
||||
const old = '<script data-wk-analytics src="https://alt/w.js"></script>';
|
||||
put(d, 'a.html', `<head><!-- ${old} --></head>`);
|
||||
await injectAnalytics(d, A(), { baseUrl: BASE });
|
||||
assert.equal(get(d, 'a.html'), `<head><!-- ${old} -->${TAG}</head>`);
|
||||
});
|
||||
|
||||
test('5-MB-Grenze: genau 5 MB wird bearbeitet', async () => {
|
||||
const d = dir();
|
||||
const b = Buffer.alloc(5 * 1024 * 1024, 0x20); b.write('<head></head>', 0);
|
||||
writeFileSync(join(d, 'g.html'), b);
|
||||
await injectAnalytics(d, A(), { baseUrl: BASE });
|
||||
assert.ok(get(d, 'g.html').includes('data-wk-analytics'));
|
||||
});
|
||||
|
||||
test('validateJob: Adresse nur Leerraum ⇒ leer, sonst getrimmt', () => {
|
||||
const base = { sub: 'demo', repo: 'JUSN/demo', sha: 'abcdef1', branch: 'main' };
|
||||
const r = validateJob({ ...base, analytics: A({ privacy: { company: 'F', email: 'e@x.de', address: ' ' } }) });
|
||||
assert.equal(r.job.analytics.privacy.address, '');
|
||||
const r2 = validateJob({ ...base, analytics: A({ privacy: { company: 'F', email: 'e@x.de', address: ' Weg 1 ' } }) });
|
||||
assert.equal(r2.job.analytics.privacy.address, 'Weg 1');
|
||||
});
|
||||
Reference in new issue
Block a user