feat: radical — serve full-page screenshot instead of fragile DOM clone
This commit is contained in:
@@ -104,7 +104,8 @@ async function main() {
|
|||||||
const browser = await chromium.launch({ headless: true });
|
const browser = await chromium.launch({ headless: true });
|
||||||
const ctx = await browser.newContext({
|
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',
|
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([{
|
await ctx.addCookies([{
|
||||||
name: 'MoodleSession',
|
name: 'MoodleSession',
|
||||||
@@ -115,71 +116,42 @@ async function main() {
|
|||||||
sameSite: 'None',
|
sameSite: 'None',
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
const resources = new Map();
|
|
||||||
const page = await ctx.newPage();
|
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 });
|
await page.goto(targetUrl, { waitUntil: 'networkidle', timeout: 90000 });
|
||||||
console.log(`[mirror] networkidle reached. extra settle…`);
|
console.log(`[mirror] networkidle reached. extra settle…`);
|
||||||
await page.waitForTimeout(4000);
|
await page.waitForTimeout(4000);
|
||||||
|
|
||||||
// Re-inline all CSS into <style> so layout never depends on fetched CSS
|
// Pixel-perfect full-page screenshot
|
||||||
// Grab DOM AFTER render
|
fs.mkdirSync(OUT, { recursive: true });
|
||||||
const html = await page.content();
|
const pngPath = path.join(OUT, 'page.png');
|
||||||
console.log(`[mirror] DOM captured: ${html.length} bytes, ${resources.size} resources tracked`);
|
await page.screenshot({ path: pngPath, fullPage: true, type: 'png' });
|
||||||
|
const pngSize = fs.statSync(pngPath).size;
|
||||||
|
console.log(`[mirror] screenshot: ${pngPath} (${pngSize} bytes)`);
|
||||||
|
|
||||||
// Write resources
|
// Compute actual page width for the wrapper (so image scales to its captured size)
|
||||||
fs.mkdirSync(MIRROR, { recursive: true });
|
const pageWidth = await page.evaluate(() => document.documentElement.scrollWidth);
|
||||||
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}`);
|
|
||||||
|
|
||||||
// Rewrite HTML
|
const wrapperHtml = `<!DOCTYPE html>
|
||||||
let finalHtml = html;
|
<html lang="ru">
|
||||||
// 1. Replace absolute same-origin URLs
|
<head>
|
||||||
finalHtml = finalHtml.split(ORIGIN).join('');
|
<meta charset="utf-8">
|
||||||
// 2. Replace each captured path (longest first to avoid substrings)
|
<meta name="viewport" content="width=${pageWidth},initial-scale=1">
|
||||||
const entries = [...mapping.entries()]
|
<title>${await page.title()}</title>
|
||||||
.filter(([k]) => k.startsWith('/'))
|
<style>
|
||||||
.sort((a, b) => b[0].length - a[0].length);
|
html, body { margin:0; padding:0; background:#fff; }
|
||||||
for (const [orig, local] of entries) {
|
body { padding-bottom:55px; }
|
||||||
finalHtml = finalHtml.split(`"${orig}"`).join(`"${local}"`);
|
.q-page { display:block; width:100%; max-width:${pageWidth}px; margin:0 auto; }
|
||||||
finalHtml = finalHtml.split(`'${orig}'`).join(`'${local}'`);
|
.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
|
fs.writeFileSync(path.join(OUT, 'index.html'), wrapperHtml, 'utf8');
|
||||||
finalHtml = finalHtml.replace(/<meta[^>]+http-equiv=["']?(Content-Security-Policy|X-Frame-Options)["']?[^>]*>/gi, '');
|
console.log(`[mirror] wrote ${path.join(OUT, 'index.html')}`);
|
||||||
|
|
||||||
// 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)`);
|
|
||||||
|
|
||||||
await browser.close();
|
await browser.close();
|
||||||
console.log('[mirror] done');
|
console.log('[mirror] done');
|
||||||
|
|||||||
Reference in New Issue
Block a user