Cookie Exporter | Netscape, JSON, Header

Export cookies in Netscape, JSON, or header format for curl, wget, yt-dlp — replaces Get cookies.txt LOCALLY

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Cookie Exporter | Netscape, JSON, Header
// @namespace    https://greasyfork.org/en/users/1462137-piknockyou
// @version      3.0.9
// @author       Piknockyou
// @license      AGPL-3.0
// @description  Export cookies in Netscape, JSON, or header format for curl, wget, yt-dlp — replaces Get cookies.txt LOCALLY
// @icon         https://www.google.com/s2/favicons?sz=64&domain=github.com
// @grant        GM_cookie
// @grant        GM.cookie
// @grant        GM_registerMenuCommand
// @grant        GM_getValue
// @grant        GM_setValue
// @match        *://*/*
// @noframes
// @run-at       document-idle
// ==/UserScript==

(() => {
	// ═══════════════════════════════════════════════════════════════
	// COOKIE API DETECTION
	// ═══════════════════════════════════════════════════════════════

	function hasCookieApi() {
		return (
			(typeof GM_cookie === "object" && GM_cookie?.list) ||
			typeof GM_cookie === "function"
		);
	}

	// ═══════════════════════════════════════════════════════════════
	// FETCH COOKIES
	// ═══════════════════════════════════════════════════════════════

	function gmCookieList(details) {
		return new Promise((resolve, reject) => {
			if (typeof GM_cookie === "object" && GM_cookie?.list) {
				GM_cookie.list(details, (cookies, err) => {
					if (err) reject(err);
					else resolve(cookies || []);
				});
			} else if (typeof GM_cookie === "function") {
				GM_cookie("list", details, (cookies, err) => {
					if (err) reject(err);
					else resolve(cookies || []);
				});
			} else {
				reject(new Error("No callback-style GM_cookie API"));
			}
		});
	}

	async function fetchCookies(hostname) {
		for (const details of [
			{ url: `https://${hostname}/` },
			{ domain: hostname },
			{},
		]) {
			try {
				const cookies = await gmCookieList(details);
				console.log(
					"[Cookie Exporter] OK:",
					cookies.length,
					JSON.stringify(details),
				);
				return cookies;
			} catch (err) {
				console.error(
					"[Cookie Exporter] attempt",
					JSON.stringify(details),
					err,
				);
			}
		}
		throw new Error("All cookie queries failed");
	}

	async function fetchAllCookies() {
		try {
			const cookies = await gmCookieList({});
			console.log("[Cookie Exporter] All cookies:", cookies.length);
			return cookies;
		} catch (err) {
			console.error("[Cookie Exporter] All cookies failed:", err);
			throw new Error("Failed to fetch all cookies");
		}
	}

	// ═══════════════════════════════════════════════════════════════
	// FORMATS
	// ═══════════════════════════════════════════════════════════════

	const FORMATS = {
		netscape: {
			ext: "txt",
			mime: "text/plain",
			serialize(cookies) {
				const lines = cookies.map((c) => {
					const domain = c.domain || "";
					const flag = domain.startsWith(".") ? "TRUE" : "FALSE";
					const path = c.path || "/";
					const secure = c.secure ? "TRUE" : "FALSE";
					const expiry = c.expirationDate ? Math.floor(c.expirationDate) : 0;
					return `${domain}\t${flag}\t${path}\t${secure}\t${expiry}\t${c.name}\t${c.value}`;
				});
				return [
					"# Netscape HTTP Cookie File",
					"# https://curl.haxx.se/rfc/cookie_spec.html",
					"# This is a generated file! Do not edit.",
					"",
					...lines,
					"",
				].join("\n");
			},
		},
		json: {
			ext: "json",
			mime: "application/json",
			serialize(cookies) {
				return JSON.stringify(cookies, null, 2);
			},
		},
		header: {
			ext: "txt",
			mime: "text/plain",
			serialize(cookies) {
				return cookies.map(({ name, value }) => `${name}=${value}`).join(";\n");
			},
		},
	};

	const FORMAT_KEYS = Object.keys(FORMATS);

	// ═══════════════════════════════════════════════════════════════
	// FORMAT PERSISTENCE
	// ═══════════════════════════════════════════════════════════════

	function getSelectedFormat() {
		return (
			(typeof GM_getValue === "function" && GM_getValue("exportFormat")) ||
			"netscape"
		);
	}

	function setSelectedFormat(key) {
		if (typeof GM_setValue === "function") {
			GM_setValue("exportFormat", key);
		}
	}

	// ═══════════════════════════════════════════════════════════════
	// DOWNLOAD
	// ═══════════════════════════════════════════════════════════════

	function domDownload(content, filename, mime) {
		const blob = new Blob([content], { type: mime });
		const url = URL.createObjectURL(blob);

		const a = document.createElement("a");
		a.href = url;
		a.download = filename;
		a.style.display = "none";
		a.rel = "noopener";

		(document.body || document.documentElement).appendChild(a);

		a.addEventListener(
			"click",
			(e) => {
				e.stopPropagation();
				if (typeof e.stopImmediatePropagation === "function") {
					e.stopImmediatePropagation();
				}
			},
			{ capture: true, once: true },
		);

		a.click();
		a.remove();

		setTimeout(() => URL.revokeObjectURL(url), 10000);
	}

	function downloadFile(content, filename, mime = "text/plain") {
		return new Promise((resolve) => {
			domDownload(content, filename, mime);
			resolve();
		});
	}

	// ═══════════════════════════════════════════════════════════════
	// NOTIFICATION
	// ═══════════════════════════════════════════════════════════════

	function notify(msg, color = "#4CAF50") {
		const el = document.createElement("div");
		Object.assign(el.style, {
			position: "fixed",
			bottom: "20px",
			right: "20px",
			padding: "10px 18px",
			background: color,
			color: "#fff",
			borderRadius: "6px",
			fontSize: "13px",
			fontWeight: "500",
			fontFamily: "sans-serif",
			zIndex: "2147483647",
			boxShadow: "0 3px 12px rgba(0,0,0,0.3)",
			transition: "opacity 0.3s",
		});
		el.textContent = msg;
		document.body.appendChild(el);
		setTimeout(() => {
			el.style.opacity = "0";
			setTimeout(() => el.remove(), 400);
		}, 2500);
	}

	// ═══════════════════════════════════════════════════════════════
	// BANNER (when API unavailable)
	// ═══════════════════════════════════════════════════════════════

	function showBanner() {
		const host = document.createElement("div");
		Object.assign(host.style, {
			position: "fixed",
			top: "0",
			left: "0",
			width: "100vw",
			height: "100vh",
			zIndex: "2147483647",
		});
		const shadow = host.attachShadow({ mode: "closed" });

		const style = document.createElement("style");
		style.textContent = `
			:host { all: initial; }
			.overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.7); display: flex; align-items: center; justify-content: center; padding: 20px; font-family: sans-serif; }
			.box { background: #1e1e1e; border: 1px solid #444; border-radius: 12px; max-width: 520px; width: 100%; padding: 28px 32px; color: #e0e0e0; }
			.title { font-size: 18px; font-weight: 700; text-align: center; margin-bottom: 14px; color: #f44336; }
			.desc { font-size: 13px; line-height: 1.6; color: #ccc; margin-bottom: 14px; }
			.sec { background: #2a2a2a; border: 1px solid #3a3a3a; border-radius: 8px; padding: 12px 14px; margin: 10px 0; }
			.sec-title { font-size: 13px; font-weight: 600; color: #4CAF50; margin-bottom: 6px; }
			.steps { margin: 0; padding-left: 18px; font-size: 12px; line-height: 1.7; color: #ccc; }
			.btn { display: block; width: 100%; padding: 12px; background: #f44336; color: #fff; border: none; border-radius: 6px; font-size: 14px; font-weight: 600; cursor: pointer; margin-top: 14px; }
		`;
		shadow.appendChild(style);

		const overlay = document.createElement("div");
		overlay.className = "overlay";
		const box = document.createElement("div");
		box.className = "box";

		box.innerHTML = `
			<div class="title">Cookie API Not Available</div>
			<div class="desc">Your userscript manager doesn't expose the GM_cookie API.</div>
			<div class="sec"><div class="sec-title">Violentmonkey</div>
				<ol class="steps"><li>Violentmonkey → Settings → Advanced → Script settings</li><li>Enable "Allow GM_cookie to access HTTP-only cookies"</li><li>Also check it in this script's Settings tab</li></ol></div>
			<div class="sec"><div class="sec-title">Tampermonkey</div>
				<ol class="steps"><li>Tampermonkey → Settings → Config mode: Advanced</li><li>Security → "Allow scripts to access cookies" → All</li></ol></div>
			<button class="btn">Got it</button>
		`;

		box.querySelector(".btn").onclick = () => host.remove();
		overlay.onclick = (e) => {
			if (e.target === overlay) host.remove();
		};
		overlay.appendChild(box);
		shadow.appendChild(overlay);
		document.body.appendChild(host);
	}

	// ═══════════════════════════════════════════════════════════════
	// MAIN EXPORT
	// ═══════════════════════════════════════════════════════════════

	async function exportCookies(scope = "current") {
		if (!hasCookieApi()) {
			console.log("[Cookie Exporter] no API");
			showBanner();
			return;
		}

		const formatKey = getSelectedFormat();
		const fmt = FORMATS[formatKey];
		if (!fmt) {
			notify(`Unknown format: ${formatKey}`, "#f44336");
			return;
		}

		try {
			const cookies =
				scope === "all"
					? await fetchAllCookies()
					: await fetchCookies(window.location.hostname);

			if (!cookies || cookies.length === 0) {
				notify(
					`No cookies${scope === "all" ? "" : ` for ${window.location.hostname}`}`,
					"#FF9800",
				);
				return;
			}

			const text = fmt.serialize(cookies);
			const name =
				scope === "all" ? "cookies" : `${window.location.hostname}_cookies`;
			await downloadFile(text, `${name}.${fmt.ext}`, fmt.mime);
			notify(`Exported ${cookies.length} cookies as ${formatKey}`);
		} catch (e) {
			console.error("[Cookie Exporter] FAILED:", e);
			notify(`${e.message}`, "#f44336");
		}
	}

	// ═══════════════════════════════════════════════════════════════
	// MENU COMMANDS
	// ═══════════════════════════════════════════════════════════════

	if (typeof GM_registerMenuCommand === "function") {
		GM_registerMenuCommand("Export Cookies", () => exportCookies("current"));
		GM_registerMenuCommand("Export All Cookies", () => exportCookies("all"));

		let formatMenuId = null;
		const updateFormatMenu = () => {
			const key = getSelectedFormat();
			formatMenuId = GM_registerMenuCommand(
				`Format: ${key} (click to change)`,
				() => {
					const next =
						FORMAT_KEYS[(FORMAT_KEYS.indexOf(key) + 1) % FORMAT_KEYS.length];
					setSelectedFormat(next);
					notify(`Format: ${next}`);
					updateFormatMenu();
				},
				{ id: formatMenuId, autoClose: false },
			);
		};
		updateFormatMenu();
	}
})();