test1:subject6
This is an old revision of the document!
(async () => {
const origin = location.origin;
const pageUrl = new URL(location.href);
const decoder = new TextDecoder();
const encoder = new TextEncoder();
const collected = new Map();
const fileMap = new Map();
const failed = [];
const normalizeUrl = (u, base = location.href) => {
try {
const x = new URL(u, base);
if (!/^https?:$/.test(x.protocol)) return null;
if (x.origin !== origin) return null;
x.hash = "";
return x;
} catch {
return null;
}
};
const hasExt = (name) => /\.[a-zA-Z0-9]{1,10}$/.test(name || "");
const toLocalPath = (urlObj, isHtml = false) => {
let path = decodeURIComponent(urlObj.pathname || "/");
if (!path || path === "/") return "index.html";
const parts = path.split("/").filter(Boolean);
const last = parts[parts.length - 1] || "";
if (isHtml) {
if (!hasExt(last)) parts.push("index.html");
return parts.join("/");
}
if (!hasExt(last)) parts.push("index.html");
return parts.join("/");
};
const dirname = (p) => {
const i = p.lastIndexOf("/");
return i >= 0 ? p.slice(0, i) : "";
};
const relativePath = (fromFile, toFile) => {
const fromDir = dirname(fromFile);
const fromParts = fromDir ? fromDir.split("/") : [];
const toParts = toFile.split("/");
let i = 0;
while (i < fromParts.length && i < toParts.length && fromParts[i] === toParts[i]) {
i++;
}
const up = new Array(fromParts.length - i).fill("..");
const down = toParts.slice(i);
const out = [...up, ...down].join("/");
return out || ".";
};
const addUrl = (u, type = "asset", base = location.href) => {
const nu = normalizeUrl(u, base);
if (!nu) return null;
if (!collected.has(nu.href)) {
collected.set(nu.href, { url: nu, type });
}
return nu.href;
};
const cssUrlRegex = /url\((['"]?)(.*?)\1\)/g;
const extractCssUrls = (cssText, baseUrl) => {
const out = [];
let m;
while ((m = cssUrlRegex.exec(cssText))) {
const raw = (m[2] || "").trim();
if (!raw || raw.startsWith("data:") || raw.startsWith("blob:") || raw.startsWith("#")) continue;
const nu = normalizeUrl(raw, baseUrl);
if (nu) out.push(nu.href);
}
return out;
};
addUrl(location.href, "html");
document.querySelectorAll("script[src]").forEach(el => addUrl(el.getAttribute("src"), "asset"));
document.querySelectorAll("link[href]").forEach(el => addUrl(el.getAttribute("href"), "asset"));
document.querySelectorAll("img[src]").forEach(el => addUrl(el.getAttribute("src"), "asset"));
document.querySelectorAll("[style]").forEach(el => {
const style = el.getAttribute("style") || "";
let m;
while ((m = cssUrlRegex.exec(style))) {
addUrl(m[2], "asset");
}
});
performance.getEntriesByType("resource").forEach(r => addUrl(r.name, "asset"));
const fetchBinary = async (url) => {
const res = await fetch(url, { credentials: "include" });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return new Uint8Array(await res.arrayBuffer());
};
const fetchText = async (url) => {
const res = await fetch(url, { credentials: "include" });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.text();
};
const pageLocalPath = toLocalPath(pageUrl, true);
// 1) fetch all current collected files
for (const { url, type } of [...collected.values()]) {
const href = url.href;
const localPath = toLocalPath(url, type === "html");
try {
if (type === "html" && href === pageUrl.href) {
continue;
}
const bytes = await fetchBinary(href);
fileMap.set(href, {
url: href,
localPath,
bytes,
contentType: "",
text: null
});
console.log("saved:", localPath);
} catch (e) {
failed.push({ url: href, error: String(e) });
console.warn("failed:", href, e);
}
}
// 2) bring CSS files as text, collect nested url(...) assets
const cssCandidates = [...fileMap.values()].filter(f => /\.css(\?|$)/i.test(f.url) || /\.css$/i.test(f.localPath));
for (const f of cssCandidates) {
try {
const txt = decoder.decode(f.bytes);
f.text = txt;
const nested = extractCssUrls(txt, f.url);
for (const u of nested) {
addUrl(u, "asset", f.url);
}
} catch (e) {
console.warn("css decode failed:", f.url, e);
}
}
// 3) fetch newly found nested CSS assets
for (const { url, type } of [...collected.values()]) {
const href = url.href;
if (href === pageUrl.href) continue;
if (fileMap.has(href)) continue;
const localPath = toLocalPath(url, type === "html");
try {
const bytes = await fetchBinary(href);
fileMap.set(href, {
url: href,
localPath,
bytes,
contentType: "",
text: null
});
console.log("saved nested:", localPath);
} catch (e) {
failed.push({ url: href, error: String(e) });
console.warn("failed nested:", href, e);
}
}
// 4) rewrite CSS url(...) to local relative paths
for (const f of [...fileMap.values()]) {
if (!(/\.css(\?|$)/i.test(f.url) || /\.css$/i.test(f.localPath))) continue;
try {
let txt = f.text != null ? f.text : decoder.decode(f.bytes);
txt = txt.replace(cssUrlRegex, (full, q, raw) => {
const val = (raw || "").trim();
if (!val || val.startsWith("data:") || val.startsWith("blob:") || val.startsWith("#")) return full;
const nu = normalizeUrl(val, f.url);
if (!nu) return full;
const target = fileMap.get(nu.href);
if (!target) return full;
const rel = relativePath(f.localPath, target.localPath);
return `url("${rel}")`;
});
f.bytes = encoder.encode(txt);
f.text = txt;
} catch (e) {
console.warn("css rewrite failed:", f.url, e);
}
}
// 5) rewrite HTML to local relative paths
const htmlDoc = document.documentElement.cloneNode(true);
const rewriteAttr = (selector, attr) => {
htmlDoc.querySelectorAll(selector).forEach(el => {
const raw = el.getAttribute(attr);
if (!raw) return;
if (raw.startsWith("data:") || raw.startsWith("blob:") || raw.startsWith("#")) return;
const nu = normalizeUrl(raw, location.href);
if (!nu) return;
const target = fileMap.get(nu.href);
if (!target) return;
el.setAttribute(attr, relativePath(pageLocalPath, target.localPath));
});
};
rewriteAttr("script[src]", "src");
rewriteAttr("link[href]", "href");
rewriteAttr("img[src]", "src");
rewriteAttr("source[src]", "src");
rewriteAttr("video[src]", "src");
rewriteAttr("audio[src]", "src");
rewriteAttr("iframe[src]", "src");
rewriteAttr("a[href]", "href");
htmlDoc.querySelectorAll("[style]").forEach(el => {
const style = el.getAttribute("style") || "";
const replaced = style.replace(cssUrlRegex, (full, q, raw) => {
const val = (raw || "").trim();
if (!val || val.startsWith("data:") || val.startsWith("blob:") || val.startsWith("#")) return full;
const nu = normalizeUrl(val, location.href);
if (!nu) return full;
const target = fileMap.get(nu.href);
if (!target) return full;
return `url("${relativePath(pageLocalPath, target.localPath)}")`;
});
el.setAttribute("style", replaced);
});
const htmlText = "<!DOCTYPE html>\n" + htmlDoc.outerHTML;
fileMap.set(pageUrl.href, {
url: pageUrl.href,
localPath: pageLocalPath,
bytes: encoder.encode(htmlText),
contentType: "text/html",
text: htmlText
});
const reportText = JSON.stringify({
page: location.href,
savedPage: pageLocalPath,
savedCount: fileMap.size,
failedCount: failed.length,
failed
}, null, 2);
fileMap.set("__REPORT__", {
url: "__REPORT__",
localPath: "_download_report.json",
bytes: encoder.encode(reportText),
contentType: "application/json",
text: reportText
});
// 6) TAR builder
const padOctal = (num, width) => {
const s = num.toString(8);
return s.padStart(width - 1, "0") + "\0";
};
const writeString = (buf, offset, str, length) => {
const bytes = encoder.encode(str);
buf.set(bytes.slice(0, length), offset);
};
const writeOctal = (buf, offset, num, length) => {
writeString(buf, offset, padOctal(num, length), length);
};
const tarHeader = (name, size, mtime = Math.floor(Date.now() / 1000), mode = 0o644) => {
const buf = new Uint8Array(512);
writeString(buf, 0, name, 100);
writeOctal(buf, 100, mode, 8);
writeOctal(buf, 108, 0, 8);
writeOctal(buf, 116, 0, 8);
writeOctal(buf, 124, size, 12);
writeOctal(buf, 136, mtime, 12);
for (let i = 148; i < 156; i++) buf[i] = 32;
buf[156] = "0".charCodeAt(0);
writeString(buf, 257, "ustar", 6);
writeString(buf, 263, "00", 2);
let sum = 0;
for (let i = 0; i < 512; i++) sum += buf[i];
const chk = sum.toString(8).padStart(6, "0");
writeString(buf, 148, chk, 6);
buf[154] = 0;
buf[155] = 32;
return buf;
};
const tarParts = [];
let totalBytes = 0;
for (const f of [...fileMap.values()]) {
const name = f.localPath;
const body = f.bytes instanceof Uint8Array ? f.bytes : new Uint8Array(f.bytes);
const header = tarHeader(name, body.length);
tarParts.push(header);
tarParts.push(body);
totalBytes += 512 + body.length;
const pad = (512 - (body.length % 512)) % 512;
if (pad) {
tarParts.push(new Uint8Array(pad));
totalBytes += pad;
}
}
tarParts.push(new Uint8Array(1024));
totalBytes += 1024;
const tarBlob = new Blob(tarParts, { type: "application/x-tar" });
const a = document.createElement("a");
a.href = URL.createObjectURL(tarBlob);
const safeName = (document.title || "page").replace(/[\\/:*?"<>|]+/g, "_").slice(0, 80) || "page";
a.download = `${safeName}.tar`;
a.click();
setTimeout(() => URL.revokeObjectURL(a.href), 5000);
console.log("DONE");
console.log("page:", pageLocalPath);
console.log("saved:", fileMap.size);
console.log("failed:", failed.length);
})();
test1/subject6.1775961924.txt.gz · Last modified: by admin · Currently locked by: 127.0.0.1,192.168.0.3
