feat: radical — serve full-page screenshot instead of fragile DOM clone

This commit is contained in:
aedes
2026-04-22 02:46:00 +03:00
parent 4a5c9c2d80
commit a070cfcc1f

View File

@@ -104,7 +104,8 @@ async function main() {
const browser = await chromium.launch({ headless: true });
const ctx = await browser.newContext({
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
viewport: { width: 1440, height: 900 },
viewport: { width: 1600, height: 900 },
deviceScaleFactor: 2,
});
await ctx.addCookies([{
name: 'MoodleSession',
@@ -115,71 +116,42 @@ async function main() {
sameSite: 'None',
}]);
const resources = new Map();
const page = await ctx.newPage();
page.on('response', async (resp) => {
const rurl = resp.url();
if (!rurl.startsWith('http')) return;
try {
const buf = await resp.body();
const ct = resp.headers()['content-type'] || '';
if (buf && buf.length > 0) resources.set(rurl, { buf, ct });
} catch (e) { /* redirect/204/etc */ }
});
await page.goto(targetUrl, { waitUntil: 'networkidle', timeout: 90000 });
console.log(`[mirror] networkidle reached. extra settle…`);
await page.waitForTimeout(4000);
// Re-inline all CSS into <style> so layout never depends on fetched CSS
// Grab DOM AFTER render
const html = await page.content();
console.log(`[mirror] DOM captured: ${html.length} bytes, ${resources.size} resources tracked`);
// Pixel-perfect full-page screenshot
fs.mkdirSync(OUT, { recursive: true });
const pngPath = path.join(OUT, 'page.png');
await page.screenshot({ path: pngPath, fullPage: true, type: 'png' });
const pngSize = fs.statSync(pngPath).size;
console.log(`[mirror] screenshot: ${pngPath} (${pngSize} bytes)`);
// Write resources
fs.mkdirSync(MIRROR, { recursive: true });
const mapping = new Map(); // absUrl → /q/mirror/<safepath>
for (const [rurl, { buf, ct }] of resources) {
const u = new URL(rurl);
if (u.hostname !== HOST) continue; // skip third-party (cdnjs etc) — let browser load them
const safe = urlToSafePath(rurl, ct);
const local = path.join(MIRROR, safe);
fs.mkdirSync(path.dirname(local), { recursive: true });
fs.writeFileSync(local, buf);
mapping.set(rurl, '/q/mirror/' + safe);
// Also map the path-only version (no origin)
mapping.set(u.pathname + (u.search || ''), '/q/mirror/' + safe);
}
console.log(`[mirror] wrote ${mapping.size / 2} files to ${MIRROR}`);
// Compute actual page width for the wrapper (so image scales to its captured size)
const pageWidth = await page.evaluate(() => document.documentElement.scrollWidth);
// Rewrite HTML
let finalHtml = html;
// 1. Replace absolute same-origin URLs
finalHtml = finalHtml.split(ORIGIN).join('');
// 2. Replace each captured path (longest first to avoid substrings)
const entries = [...mapping.entries()]
.filter(([k]) => k.startsWith('/'))
.sort((a, b) => b[0].length - a[0].length);
for (const [orig, local] of entries) {
finalHtml = finalHtml.split(`"${orig}"`).join(`"${local}"`);
finalHtml = finalHtml.split(`'${orig}'`).join(`'${local}'`);
}
const wrapperHtml = `<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=${pageWidth},initial-scale=1">
<title>${await page.title()}</title>
<style>
html, body { margin:0; padding:0; background:#fff; }
body { padding-bottom:55px; }
.q-page { display:block; width:100%; max-width:${pageWidth}px; margin:0 auto; }
.q-page img { display:block; width:100%; height:auto; }
</style>
</head>
<body>
<div class="q-page"><img src="/q/page.png" alt=""></div>
${CHAT_OVERLAY}
</body>
</html>`;
// 3. Strip CSP / XFO meta
finalHtml = finalHtml.replace(/<meta[^>]+http-equiv=["']?(Content-Security-Policy|X-Frame-Options)["']?[^>]*>/gi, '');
// 4. Strip ALL scripts — static snapshot, no dynamic code that'd break on our domain
finalHtml = finalHtml.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
// 5. Neutralize <base>
finalHtml = finalHtml.replace(/<base[^>]*>/gi, '');
// 6. Inject chat overlay (AFTER script strip so overlay scripts survive)
finalHtml = finalHtml.replace(/<\/body>/i, CHAT_OVERLAY + '</body>');
fs.writeFileSync(path.join(OUT, 'index.html'), finalHtml, 'utf8');
console.log(`[mirror] wrote ${path.join(OUT, 'index.html')} (${finalHtml.length} bytes)`);
fs.writeFileSync(path.join(OUT, 'index.html'), wrapperHtml, 'utf8');
console.log(`[mirror] wrote ${path.join(OUT, 'index.html')}`);
await browser.close();
console.log('[mirror] done');