diff --git a/README.md b/README.md index 84d4c78..9c3d726 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,10 @@ You should do this for **every** Chrome extension you use. Most extensions are c Automatically closes inactive tabs after a configurable timeout (default: 5 minutes). Set excluded hosts to keep important tabs alive. View and re-open recently closed tabs. ### 🍪 Cookie Editor -Full cookie manager for the current site. View, edit, add, and delete cookies. Export cookies as JSON. Expand any cookie to see and modify all fields including domain, path, SameSite, secure, and httpOnly flags. +Full cookie manager for the current site. View, edit, add, and delete cookies. Export cookies as JSON or import from a JSON file (compatible with exports from this tool). Expand any cookie to see and modify all fields including domain, path, SameSite, secure, and httpOnly flags. + +### 🔐 Custom Headers +Add, edit, and remove custom HTTP header rules for debugging and development. Set or override headers on outgoing requests or incoming responses. Scope rules to the current site or apply them globally. Rules are applied via Chrome's `declarativeNetRequest` API and stored locally in `chrome.storage.local`. ### 🔀 Redirect Tracer See every redirect hop your browser took to reach the current page. Shows status codes (301, 302, 307, etc.) with a visual chain. Copy the full redirect chain to clipboard. diff --git a/background.js b/background.js index 3573677..e3b82bf 100644 --- a/background.js +++ b/background.js @@ -211,6 +211,51 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { } }); +// ═══════════════════════════════════ +// Custom Headers (declarativeNetRequest) +// ═══════════════════════════════════ +const ALL_RESOURCE_TYPES = [ + "main_frame", "sub_frame", "stylesheet", "script", "image", + "font", "object", "xmlhttprequest", "ping", "media", "websocket", "other" +]; + +function isValidHeaderRule(r) { + if (!r.name || r.value === undefined) return false; + if (r.scope === "all") return true; + return !!(r.domain || "").replace(/^\./, ""); +} + +async function applyHeaderRules() { + const { headerRules = [] } = await chrome.storage.local.get("headerRules"); + const dnrRules = headerRules + .filter(isValidHeaderRule) + .map((r, i) => { + const action = r.target === "response" + ? { type: "modifyHeaders", responseHeaders: [{ header: r.name, operation: "set", value: r.value }] } + : { type: "modifyHeaders", requestHeaders: [{ header: r.name, operation: "set", value: r.value }] }; + const condition = r.scope === "all" + ? { resourceTypes: ALL_RESOURCE_TYPES } + : { requestDomains: [(r.domain || "").replace(/^\./, "")], resourceTypes: ALL_RESOURCE_TYPES }; + return { id: i + 1, priority: 1, action, condition }; + }); + + try { + const existing = await chrome.declarativeNetRequest.getDynamicRules(); + await chrome.declarativeNetRequest.updateDynamicRules({ + removeRuleIds: existing.map(r => r.id), + addRules: dnrRules, + }); + } catch (err) { + console.error("[superlevels] Failed to apply header rules:", err); + } +} + +applyHeaderRules(); + +chrome.storage.onChanged.addListener((changes) => { + if (changes.headerRules) applyHeaderRules(); +}); + // Set defaults on install chrome.runtime.onInstalled.addListener(() => { chrome.storage.local.get(["enabled", "timeoutMin", "exclusions"], (data) => { @@ -222,4 +267,4 @@ chrome.runtime.onInstalled.addListener(() => { chrome.storage.local.set(defaults); } }); -}); +}); \ No newline at end of file diff --git a/manifest.json b/manifest.json index b54f2cc..3800ece 100644 --- a/manifest.json +++ b/manifest.json @@ -3,7 +3,7 @@ "name": "🚀 SuperLevels", "version": "2.0", "description": "The super Chrome extension by @levelsio — dark mode, GDPR cookie dismisser, YouTube unhook, tab cleaner, cookie editor, redirect tracer, live CSS editor, X/Twitter dim mode, JS toggle, JSON formatter, music recognizer, picture-in-picture, Google Maps links, and view image.", - "permissions": ["tabs", "storage", "alarms", "cookies", "activeTab", "webRequest", "webNavigation", "tabCapture", "contentSettings", "scripting"], + "permissions": ["tabs", "storage", "alarms", "cookies", "activeTab", "webRequest", "webNavigation", "tabCapture", "contentSettings", "scripting", "declarativeNetRequest"], "host_permissions": [""], "background": { "service_worker": "background.js" diff --git a/popup.html b/popup.html index ec790ef..b19fcb9 100644 --- a/popup.html +++ b/popup.html @@ -700,6 +700,24 @@ .toolbar button:hover { color: #fff; background: rgba(255,255,255,0.04); } .toolbar button.danger:hover { color: #e94560; } + /* ── Headers page ── */ + .header-scope { + font-size: 10px; padding: 2px 7px; border-radius: 4px; + font-weight: 600; margin-left: auto; white-space: nowrap; flex-shrink: 0; + } + .header-scope.site { background: #1e3a5f; color: #4fa3e0; } + .header-scope.all { background: #3a1e5f; color: #b07ae0; } + .cookie-field select { + width: 100%; background: #0f0f23; border: 1px solid #2a2a4a; + color: #ccc; padding: 6px 8px; border-radius: 6px; font-size: 13px; + } + .import-toast { + position: fixed; bottom: 60px; left: 50%; transform: translateX(-50%); + background: #1a1a2e; border: 1px solid #2a2a4a; color: #aaa; + padding: 6px 14px; border-radius: 6px; font-size: 12px; + z-index: 200; pointer-events: none; + } + /* ── Add cookie modal ── */ .modal-overlay { display: none; position: fixed; inset: 0; @@ -744,6 +762,7 @@ + @@ -793,11 +812,16 @@

Excluded Hosts

Refresh + + @@ -1100,6 +1124,27 @@

Excluded Hosts

+ +
+
+ +
+
+
+ + +
+
+ + + + diff --git a/popup.js b/popup.js index 9002b20..35157fd 100644 --- a/popup.js +++ b/popup.js @@ -19,6 +19,7 @@ function switchToPage(page) { if (page === "xunhook") loadXUnhook(); if (page === "jsonformat") loadJsonFormat(); if (page === "music") { loadMusicHistory(); loadAcrFields(); } + if (page === "headers") loadHeaders(); chrome.storage.local.set({ last_tab: page }); } @@ -323,6 +324,47 @@ document.getElementById("btnExport").addEventListener("click", () => { URL.revokeObjectURL(url); }); +// Import +document.getElementById("btnImport").addEventListener("click", () => { + document.getElementById("importFileInput").click(); +}); + +document.getElementById("importFileInput").addEventListener("change", async (e) => { + const file = e.target.files[0]; + if (!file) return; + let imported = 0, failed = 0; + try { + const text = await file.text(); + const cookies = JSON.parse(text); + if (!Array.isArray(cookies)) throw new Error("not an array"); + for (const c of cookies) { + try { + const protocol = c.secure ? "https" : "http"; + const domain = (c.domain || "").replace(/^\./, ""); + const path = c.path || "/"; + const url = `${protocol}://${domain}${path}`; + const details = { url, name: c.name, value: c.value || "", path, secure: !!c.secure, httpOnly: !!c.httpOnly }; + if (!c.hostOnly && c.domain) details.domain = c.domain; + if (c.expirationDate) details.expirationDate = c.expirationDate; + if (c.sameSite && c.sameSite !== "unspecified") details.sameSite = c.sameSite; + await chrome.cookies.set(details); + imported++; + } catch { failed++; } + } + } catch { + failed++; + } + e.target.value = ""; + loadCookies(); + const toast = document.createElement("div"); + toast.className = "import-toast"; + toast.textContent = failed + ? `Imported ${imported}, ${failed} failed` + : `Imported ${imported} cookie${imported !== 1 ? "s" : ""}`; + document.body.appendChild(toast); + setTimeout(() => toast.remove(), 3000); +}); + // Add Cookie Modal const addModal = document.getElementById("addModal"); document.getElementById("btnAdd").addEventListener("click", () => { @@ -1125,6 +1167,147 @@ jsonformatToggle.addEventListener("change", async () => { } }); + +// ═══════════════════════════════════ +// Headers Manager +// ═══════════════════════════════════ +let headerRules = []; +let headersCurrentDomain = ""; + +const headersListEl = document.getElementById("headersList"); +const headersDomainEl = document.getElementById("headersDomain"); +const headersCountEl = document.getElementById("headersCount"); + +async function loadHeaders() { + const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); + if (tab && tab.url) { + try { headersCurrentDomain = new URL(tab.url).hostname; } catch { headersCurrentDomain = ""; } + } + const { headerRules: stored = [] } = await chrome.storage.local.get("headerRules"); + headerRules = stored; + headersDomainEl.textContent = headersCurrentDomain || "unknown"; + renderHeaders(); +} + +function renderHeaders() { + headersCountEl.textContent = headerRules.length; + if (!headerRules.length) { + headersListEl.innerHTML = '
No custom header rules
'; + return; + } + headersListEl.innerHTML = headerRules.map((r, i) => ` + + `).join(""); + + headersListEl.querySelectorAll(".cookie-row").forEach((row) => { + row.addEventListener("click", (e) => { + if (e.target.closest(".cookie-del")) return; + row.closest(".cookie-item").classList.toggle("expanded"); + }); + }); + + headersListEl.querySelectorAll("[data-hdelidx]").forEach((btn) => { + btn.addEventListener("click", (e) => { + e.stopPropagation(); + headerRules.splice(parseInt(btn.dataset.hdelidx), 1); + chrome.storage.local.set({ headerRules }); + renderHeaders(); + }); + }); + + headersListEl.querySelectorAll("[data-hsaveidx]").forEach((btn) => { + btn.addEventListener("click", () => saveHeader(parseInt(btn.dataset.hsaveidx))); + }); +} + +function saveHeader(idx) { + const item = headersListEl.querySelector(`.cookie-item[data-hidx="${idx}"]`); + const name = item.querySelector('[data-hfield="name"]').value.trim(); + const value = item.querySelector('[data-hfield="value"]').value; + const scope = item.querySelector('[data-hfield="scope"]').value; + const target = item.querySelector('[data-hfield="target"]').value; + if (!name) return; + headerRules[idx] = { ...headerRules[idx], name, value, scope, target }; + if (scope === "site") headerRules[idx].domain = headersCurrentDomain; + chrome.storage.local.set({ headerRules }); + loadHeaders(); +} + +document.getElementById("btnClearHeaders").addEventListener("click", async () => { + if (!headerRules.length) return; + if (!confirm("Clear all custom header rules?")) return; + headerRules = []; + await chrome.storage.local.set({ headerRules }); + renderHeaders(); +}); + +const addHeaderModal = document.getElementById("addHeaderModal"); + +document.getElementById("btnAddHeader").addEventListener("click", () => { + document.getElementById("newHeaderName").value = ""; + document.getElementById("newHeaderValue").value = ""; + document.getElementById("newHeaderScope").value = "site"; + document.getElementById("newHeaderTarget").value = "request"; + addHeaderModal.classList.add("show"); +}); + +document.getElementById("headerModalCancel").addEventListener("click", () => { + addHeaderModal.classList.remove("show"); +}); + +addHeaderModal.addEventListener("click", (e) => { + if (e.target === addHeaderModal) addHeaderModal.classList.remove("show"); +}); + +document.getElementById("headerModalSave").addEventListener("click", async () => { + const name = document.getElementById("newHeaderName").value.trim(); + if (!name) return; + const value = document.getElementById("newHeaderValue").value; + const scope = document.getElementById("newHeaderScope").value; + const target = document.getElementById("newHeaderTarget").value; + const rule = { id: Date.now(), name, value, scope, target }; + if (scope === "site") rule.domain = headersCurrentDomain; + headerRules.push(rule); + await chrome.storage.local.set({ headerRules }); + addHeaderModal.classList.remove("show"); + renderHeaders(); +}); + // ═══════════════════════════════════ // Helpers // ═══════════════════════════════════