User Tools

Site Tools


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