UNPKG

freshroute-server

Version:

Local development server for FreshRoute extension with API mocking and extended functionality

1,173 lines (1,169 loc) 130 kB
"use strict"; let groups = []; let editingGroupIndex = null; let editingRuleIndex = null; let editingRuleType = null; let environmentVariables = []; let editingEnvironmentVariableIndex = null; window.groups = groups; window.editingGroupIndex = editingGroupIndex; window.editingRuleIndex = editingRuleIndex; window.editingRuleType = editingRuleType; let apiMockManager = null; const PRESET_TEMPLATES = { freshservice: { name: "Freshservice", description: "Complete setup for Freshservice development environment with URL redirection and authentication headers", icon: "icons/freshservice.png", variables: [ { key: "sourceDomain", label: "Source Domain", placeholder: "e.g., localhost.freshservice-dev.com", required: true, type: "text", defaultValue: "localhost.freshservice-dev.com" }, { key: "targetDomain", label: "Target Domain", placeholder: "e.g., infinity-share.freshinfinitysquad.com", required: true, type: "text", defaultValue: "infinity-share.freshinfinitysquad.com" }, { key: "cookieValue", label: "Cookie Value", placeholder: "session=abc123...", required: true, type: "text", hasCookieFetch: true }, { key: "csrfToken", label: "X-CSRF-TOKEN", placeholder: "csrf-token-value", required: false, type: "text", description: "⚠️ Important: If not filled, some API calls related to create/update operations like PATCH, POST, PUT may fail due to CSRF protection." } ], generateRules: function(variables) { const rules = []; const cleanSourceDomain = extractHostname(variables.sourceDomain); const cleanTargetDomain = extractHostname(variables.targetDomain); if (cleanSourceDomain && cleanTargetDomain) { rules.push({ id: "freshservice-api-redirect", name: "Freshservice Main API Redirect", type: "url_rewrite", sourceUrl: `^http://${escapeRegex(cleanSourceDomain)}:3000/(api|support/v1|support/v2|support/employee_offboarding|lookup_choices)(.*)`, targetUrl: `https://${cleanTargetDomain}/$1$2`, enabled: true }); rules.push({ id: "freshservice-microservices-redirect", name: "Freshservice Microservices Redirect", type: "url_rewrite", sourceUrl: `^http://${escapeRegex(cleanSourceDomain)}:(8080|4000|8000)/(.*)$`, targetUrl: `https://${cleanTargetDomain}/$2`, enabled: true }); rules.push({ id: "freshservice-microservices-redirect-extendd", name: "Freshservice Microservices Redirect (Extend)", type: "url_rewrite", sourceUrl: `^http://${escapeRegex(cleanTargetDomain)}:(8080|4000|8000|3000)/(.*)$`, targetUrl: `https://${cleanTargetDomain}/$2`, enabled: true }); } if (cleanTargetDomain && (variables.cookieValue || variables.csrfToken)) { const headers = []; if (variables.cookieValue) { headers.push({ name: "Cookie", value: variables.cookieValue, operation: "set", target: "request" }); } if (variables.csrfToken) { headers.push({ name: "X-CSRF-TOKEN", value: variables.csrfToken, operation: "set", target: "request" }); } headers.push( { name: "X-Extension-Modified", value: "true", operation: "set", target: "response" }, { name: "X-Debug-Info", value: "Freshservice-Extension-Active", operation: "set", target: "response" }, { name: "Cache-Control", value: "no-cache, no-store, must-revalidate", operation: "set", target: "response" } ); if (headers.length > 0) { rules.push({ id: "freshservice-headers", name: "Freshservice Authentication Headers", type: "modify_headers", urlPattern: `^https://${escapeRegex(cleanTargetDomain)}/.*`, headers, enabled: true }); } } return rules; } }, freshdesk: { name: "Freshdesk", description: "Complete setup for Freshdesk development environment with URL redirection and authentication headers", icon: "icons/freshdesk.png", variables: [], disabled: true, generateRules: function(variables) { const rules = []; return rules; } }, freshsales: { name: "Freshsales", description: "Complete setup for Freshsales development environment with URL redirection and authentication headers", icon: "icons/freshsales.png", variables: [], disabled: true, generateRules: function(variables) { const rules = []; return rules; } }, freshmarketer: { name: "Freshmarketer", description: "Complete setup for Freshmarketer development environment with URL redirection and authentication headers", icon: "icons/freshmarketer.png", variables: [], disabled: true, generateRules: function(variables) { const rules = []; return rules; } } // Additional presets can be added here }; function escapeRegex(str) { return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } function extractHostname(urlOrDomain) { if (!urlOrDomain) return ""; let cleanDomain = urlOrDomain.trim(); if (cleanDomain.startsWith("http://") || cleanDomain.startsWith("https://")) { try { const url = new URL(cleanDomain); return url.host; } catch (e) { cleanDomain = cleanDomain.replace(/^https?:\/\//, ""); } } cleanDomain = cleanDomain.split("/")[0]; cleanDomain = cleanDomain.split("?")[0]; cleanDomain = cleanDomain.split("#")[0]; return cleanDomain; } async function fetchAndFillCookies(inputKey, targetDomain, formContainer) { let button = null; let originalText = "🍪 Fetch Cookies"; try { const inputElement = formContainer.querySelector(`[data-key="${inputKey}"]`); if (!inputElement) { throw new Error(`Input with key "${inputKey}" not found`); } button = inputElement.parentElement.querySelector(".fetch-cookies-btn"); if (!button) { throw new Error("Fetch cookies button not found"); } originalText = button.textContent; button.textContent = "🔄 Fetching..."; button.disabled = true; let cleanDomain = targetDomain.replace(/^https?:\/\//, "").split("/")[0]; const cookies = await chrome.cookies.getAll({ domain: cleanDomain }); const subdomainCookies = await chrome.cookies.getAll({ domain: "." + cleanDomain }); const allCookies = [...cookies, ...subdomainCookies]; const uniqueCookies = allCookies.filter( (cookie, index, self) => index === self.findIndex((c) => c.name === cookie.name && c.domain === cookie.domain) ); console.log(`🍪 Found ${uniqueCookies.length} cookies for domain: ${cleanDomain}`); if (uniqueCookies.length === 0) { alert(`No cookies found for domain: ${cleanDomain} Make sure you have visited the target site recently and are logged in.`); return; } const cookieString = uniqueCookies.map((cookie) => `${cookie.name}=${cookie.value}`).join("; "); inputElement.value = cookieString; const cookieCount = uniqueCookies.length; const cookieNames = uniqueCookies.map((c) => c.name).join(", "); const message = `✅ Successfully fetched ${cookieCount} cookies from ${cleanDomain}: Cookie names: ${cookieNames} Total cookie string length: ${cookieString.length} characters The cookies have been filled into the Cookie Value field.`; alert(message); console.log("🍪 Cookie details:", uniqueCookies.map((c) => ({ name: c.name, domain: c.domain, path: c.path, secure: c.secure, httpOnly: c.httpOnly, sameSite: c.sameSite }))); } catch (error) { console.error("❌ Error fetching cookies:", error); let errorMessage = "Failed to fetch cookies. "; if (error.message.includes("chrome.cookies")) { errorMessage += "The extension may not have cookie permissions. Please check the manifest.json file."; } else if (error.message.includes("Invalid domain")) { errorMessage += "Please enter a valid domain name (e.g., example.com)."; } else { errorMessage += `Error: ${error.message}`; } alert(errorMessage); } finally { if (button) { button.textContent = originalText; button.disabled = false; } } } document.addEventListener("DOMContentLoaded", async () => { try { await new Promise((resolve) => setTimeout(resolve, 100)); if (!window.DomainValidator) { console.error("❌ DomainValidator not found! Domain restrictions will not work."); window.DomainValidator = { validateTargetUrl: function(url) { return { allowed: false, reason: "Domain validation system failed to load. Please reload the extension.", suggestions: ["Reload the extension", "Check browser console for errors"] }; }, getAllowedDomains: function() { return []; } }; } else { console.log("✅ DomainValidator loaded successfully"); console.log("Available DomainValidator methods:", Object.keys(window.DomainValidator)); } await loadGroupsAndRules(); await loadEnvironmentVariables(); setupEventListeners(); setupSettingsListeners(); console.log("🔍 Debug - Available functions:"); console.log("typeof renderGroups:", typeof renderGroups); console.log("typeof saveGroups:", typeof saveGroups); console.log("typeof window.renderGroups:", typeof window.renderGroups); console.log("typeof window.saveGroups:", typeof window.saveGroups); setTimeout(() => { try { console.log("🔍 Debug - In setTimeout - Available functions:"); console.log("typeof renderGroups:", typeof renderGroups); console.log("typeof window.renderGroups:", typeof window.renderGroups); if (typeof renderGroups === "function") { renderGroups(); } else { console.error("renderGroups is not a function, type:", typeof renderGroups); if (typeof window.renderGroups === "function") { window.renderGroups(); } else { console.error("window.renderGroups is also not a function"); const allFunctions = Object.getOwnPropertyNames(window).filter((name) => typeof window[name] === "function"); console.log("Available functions in window:", allFunctions.filter((name) => name.includes("render"))); } } } catch (error) { console.error("Error calling renderGroups:", error); } }, 100); renderEnvironmentVariables(); loadSettings(); initializeApiMockManager(); } catch (error) { console.error("Error in DOMContentLoaded:", error); } }); function setupEventListeners() { document.getElementById("closeUrlModal").addEventListener("click", () => closeUrlRuleModal()); document.getElementById("saveUrlRuleBtn").addEventListener("click", () => saveUrlRule()); document.getElementById("cancelUrlRuleBtn").addEventListener("click", () => closeUrlRuleModal()); document.getElementById("closeHeaderModal").addEventListener("click", () => closeHeaderRuleModal()); document.getElementById("saveHeaderRuleBtn").addEventListener("click", () => saveHeaderRule()); document.getElementById("cancelHeaderRuleBtn").addEventListener("click", () => closeHeaderRuleModal()); document.getElementById("addHeaderFieldBtn").addEventListener("click", () => addHeaderField()); document.getElementById("addGroupBtn").addEventListener("click", () => openGroupModal()); document.getElementById("closeGroupModal").addEventListener("click", () => closeGroupModal()); document.getElementById("saveGroupBtn").addEventListener("click", () => saveGroup()); document.getElementById("cancelGroupBtn").addEventListener("click", () => closeGroupModal()); document.getElementById("addPresetGroupBtn").addEventListener("click", () => openPresetModal()); document.getElementById("closePresetModal").addEventListener("click", () => closePresetModal()); document.getElementById("createPresetGroupBtn").addEventListener("click", () => createPresetGroup()); document.getElementById("cancelPresetBtn").addEventListener("click", () => closePresetModal()); document.getElementById("addEnvironmentVariableBtn").addEventListener("click", () => openEnvironmentVariableModal()); document.getElementById("closeEnvironmentVariableModal").addEventListener("click", () => closeEnvironmentVariableModal()); document.getElementById("saveEnvironmentVariableBtn").addEventListener("click", () => saveEnvironmentVariable()); document.getElementById("cancelEnvironmentVariableBtn").addEventListener("click", () => closeEnvironmentVariableModal()); document.getElementById("importBtn").addEventListener("click", () => document.getElementById("importFile").click()); document.getElementById("importFile").addEventListener("change", (e) => importRules(e)); document.getElementById("testSourceUrlBtn").addEventListener("click", () => testRegexPattern("sourceUrl", "sourceUrlTest", "sourceUrlResult", "targetUrl", "targetUrlPreview")); document.getElementById("testHeaderUrlBtn").addEventListener("click", () => testHeaderRegexPattern("headerUrlPattern", "headerUrlTest", "headerUrlResult")); document.getElementById("toggleSourceUrlTest").addEventListener("click", () => toggleTestSection("sourceUrlTestSection", "toggleSourceUrlTest", "targetUrlExamples")); document.getElementById("toggleHeaderUrlTest").addEventListener("click", () => toggleTestSection("headerUrlTestSection", "toggleHeaderUrlTest")); document.getElementById("sourceUrl").addEventListener("input", () => { clearRegexResult("sourceUrlResult"); clearRegexResult("targetUrlPreview"); }); document.getElementById("headerUrlPattern").addEventListener("input", () => { clearRegexResult("headerUrlResult"); validateHeaderUrlPatternInput(); }); document.getElementById("targetUrl").addEventListener("input", () => { const testUrl = document.getElementById("sourceUrlTest").value.trim(); const sourcePattern = document.getElementById("sourceUrl").value.trim(); if (testUrl && sourcePattern) { testRegexPattern("sourceUrl", "sourceUrlTest", "sourceUrlResult", "targetUrl", "targetUrlPreview"); } validateTargetUrlDomainInput(); }); document.getElementById("matchingType").addEventListener("change", () => { updateMatchingTypeUI(); }); document.getElementById("headerMatchingType").addEventListener("change", () => { updateHeaderMatchingTypeUI(); }); document.addEventListener("click", (e) => { if (e.target.classList.contains("btn-example")) { const targetField = e.target.dataset.target; const pattern = e.target.dataset.pattern; insertExamplePattern(targetField, pattern); } }); setupModalCloseHandling(); document.addEventListener("keydown", (event) => { if (event.key === "Escape") { closeUrlRuleModal(); closeHeaderRuleModal(); closeGroupModal(); closePresetModal(); closeEnvironmentVariableModal(); } }); document.addEventListener("click", (event) => { document.querySelectorAll(".add-rule-dropdown-content.show").forEach((dropdown) => { if (!dropdown.closest(".add-rule-dropdown").contains(event.target)) { dropdown.classList.remove("show"); } }); }); document.querySelectorAll(".tab-button").forEach((button) => { button.addEventListener("click", (e) => { const targetTab = e.currentTarget.dataset.tab; switchTab(targetTab); }); }); document.getElementById("rulesTab").addEventListener("click", () => { showRulesView(); updateTabNavigation("rules"); }); document.getElementById("settingsTab").addEventListener("click", () => { showSettingsView(); updateTabNavigation("settings"); }); document.getElementById("backToRulesBtn").addEventListener("click", () => { showRulesView(); }); document.getElementById("dashboardBtn").addEventListener("click", () => { window.location.href = chrome.runtime.getURL("dashboard.html"); }); document.getElementById("networkRecorderBtn").addEventListener("click", () => { window.location.href = chrome.runtime.getURL("network-recorder.html"); }); setupApiMockEventListeners(); } function setupApiMockEventListeners() { const addMockRuleBtn = document.getElementById("addMockRuleBtn"); if (addMockRuleBtn) { addMockRuleBtn.addEventListener("click", () => openApiMockModal()); } const refreshApiMockFilesBtn = document.getElementById("refreshApiMockFilesBtn"); if (refreshApiMockFilesBtn) { refreshApiMockFilesBtn.addEventListener("click", () => refreshApiMockFiles()); } document.addEventListener("click", (e) => { if (e.target.classList.contains("mock-rule-edit")) { const ruleId = e.target.dataset.ruleId; if (ruleId) { editApiMockRule(ruleId); } } if (e.target.classList.contains("mock-rule-delete")) { const ruleId = e.target.dataset.ruleId; if (ruleId) { deleteApiMockRule(ruleId); } } if (e.target.classList.contains("notification-close")) { const notification = e.target.closest(".notification"); if (notification) { notification.remove(); } } }); document.addEventListener("change", (e) => { if (e.target.classList.contains("mock-rule-toggle")) { const ruleId = e.target.dataset.ruleId; const enabled = e.target.checked; if (ruleId) { toggleApiMockRule(ruleId, enabled); } } }); } function setupSettingsListeners() { const notificationsToggle = document.getElementById("notificationsEnabledToggle"); if (notificationsToggle) { notificationsToggle.addEventListener("change", async (e) => { await chrome.storage.sync.set({ notificationsEnabled: e.target.checked }); console.log("Notifications setting updated:", e.target.checked); }); } const compactNotificationsToggle = document.getElementById("compactNotificationsToggle"); if (compactNotificationsToggle) { compactNotificationsToggle.addEventListener("change", async (e) => { await chrome.storage.sync.set({ compactNotifications: e.target.checked }); console.log("Compact notifications setting updated:", e.target.checked); }); } const exportEnvironmentVariablesToggle = document.getElementById("exportEnvironmentVariablesToggle"); if (exportEnvironmentVariablesToggle) { exportEnvironmentVariablesToggle.addEventListener("change", async (e) => { await chrome.storage.sync.set({ exportEnvironmentVariables: e.target.checked }); console.log("Export environment variables setting updated:", e.target.checked); }); } const refreshApiMockFilesBtn = document.getElementById("refreshApiMockFilesBtn"); if (refreshApiMockFilesBtn) { refreshApiMockFilesBtn.addEventListener("click", () => { if (apiMockManager) { showNotification("Refreshing FreshRoute server files...", "info"); apiMockManager.refreshFiles(); } }); } const manageFilesMainBtn = document.getElementById("manageFilesMainBtn"); if (manageFilesMainBtn) { manageFilesMainBtn.addEventListener("click", () => { if (apiMockManager) { apiMockManager.openFileManagementModal(); } }); } const saveMockServerSettingsBtn = document.getElementById("saveMockServerSettingsBtn"); if (saveMockServerSettingsBtn) { saveMockServerSettingsBtn.addEventListener("click", saveMockServerSettings); } } async function loadSettings() { try { const { notificationsEnabled, compactNotifications, exportEnvironmentVariables, mockServerHttpUrl, mockServerWsUrl } = await chrome.storage.sync.get([ "notificationsEnabled", "compactNotifications", "exportEnvironmentVariables", "mockServerHttpUrl", "mockServerWsUrl" ]); const notificationsToggle = document.getElementById("notificationsEnabledToggle"); const compactNotificationsToggle = document.getElementById("compactNotificationsToggle"); const exportEnvVariablesToggle = document.getElementById("exportEnvironmentVariablesToggle"); const mockServerHttpUrlInput = document.getElementById("mockServerHttpUrl"); const mockServerWsUrlInput = document.getElementById("mockServerWsUrl"); if (notificationsToggle) { notificationsToggle.checked = notificationsEnabled !== false; } if (compactNotificationsToggle) { compactNotificationsToggle.checked = compactNotifications !== false; } if (exportEnvVariablesToggle) { exportEnvVariablesToggle.checked = exportEnvironmentVariables === true; } if (mockServerHttpUrlInput) { mockServerHttpUrlInput.value = mockServerHttpUrl || "http://localhost:3001"; } if (mockServerWsUrlInput) { mockServerWsUrlInput.value = mockServerWsUrl || "ws://localhost:3002"; } } catch (error) { console.error("Error loading settings:", error); } } async function saveMockServerSettings() { var _a, _b; try { const mockServerHttpUrlInput = document.getElementById("mockServerHttpUrl"); const mockServerWsUrlInput = document.getElementById("mockServerWsUrl"); const httpUrl = ((_a = mockServerHttpUrlInput == null ? void 0 : mockServerHttpUrlInput.value) == null ? void 0 : _a.trim()) || "http://localhost:3001"; const wsUrl = ((_b = mockServerWsUrlInput == null ? void 0 : mockServerWsUrlInput.value) == null ? void 0 : _b.trim()) || "ws://localhost:3002"; try { new URL(httpUrl); new URL(wsUrl); } catch (error) { showNotification("Please enter valid URLs", "error"); return; } await chrome.storage.sync.set({ mockServerHttpUrl: httpUrl, mockServerWsUrl: wsUrl }); if (apiMockManager) { console.log("🔄 Updating API Mock Manager with new URLs:", { httpUrl, wsUrl }); await apiMockManager.updateServerUrls(httpUrl, wsUrl); } else { console.warn("⚠️ API Mock Manager not available during settings save"); } showNotification("FreshRoute server settings saved successfully", "success"); console.log("FreshRoute server settings saved:", { httpUrl, wsUrl }); } catch (error) { console.error("Error saving FreshRoute server settings:", error); showNotification("Error saving FreshRoute server settings", "error"); } } async function loadGroupsAndRules() { try { let result = await chrome.storage.local.get(["groups", "rules"]); if (!result.groups && !result.rules) { result = await chrome.storage.sync.get(["groups", "rules"]); if (result.groups || result.rules) { console.log("Migrating data from sync to local storage..."); await chrome.storage.local.set(result); await chrome.storage.sync.remove(["groups", "rules"]); } } if (result.groups) { groups = result.groups; window.groups = groups; } else { groups = await migrateFromOldFormat(result.rules || []); window.groups = groups; await saveGroups(); } } catch (error) { console.error("Error loading groups and rules:", error); groups = []; window.groups = groups; } } async function loadEnvironmentVariables() { try { const result = await chrome.storage.sync.get(["environmentVariables"]); environmentVariables = result.environmentVariables || []; } catch (error) { console.error("Error loading environment variables:", error); environmentVariables = []; } } async function saveEnvironmentVariables() { try { await chrome.storage.sync.set({ environmentVariables }); console.log("💾 Environment variables saved to storage"); setTimeout(() => { console.log("🚀 Triggering rule update..."); chrome.runtime.sendMessage({ action: "updateRules" }, (response) => { if (chrome.runtime.lastError) { console.error("❌ Error sending update message:", chrome.runtime.lastError); } else if (response && response.success) { console.log("✅ Rules updated successfully after environment variable change"); } else { console.warn("⚠️ Rule update may have failed:", response == null ? void 0 : response.error); } }); }, 50); } catch (error) { console.error("Error saving environment variables:", error); alert("Error saving environment variables. Please try again."); } } function renderEnvironmentVariables() { const container = document.getElementById("environmentVariables"); if (environmentVariables.length === 0) { container.innerHTML = ` <div class="env-empty-state"> <h3>📋 No Environment Variables</h3> <p>Create reusable variables to avoid repetition in your rules.</p> <p>Variables can be used in URL patterns, target URLs, and header values.</p> <p>Click "Add Variable" to create your first environment variable.</p> </div> `; return; } container.innerHTML = ` <div class="env-variables-container"> ${environmentVariables.map((variable, index) => ` <div class="env-variable-card"> <div class="env-variable-info"> <div class="env-variable-name">${escapeHtml(variable.name)}</div> <div class="env-variable-value">Value: ${escapeHtml(variable.value)}</div> ${variable.description ? `<div class="env-variable-description">${escapeHtml(variable.description)}</div>` : ""} <div class="env-variable-usage">Usage: {{${escapeHtml(variable.name)}}}</div> </div> <div class="env-variable-actions"> <button class="btn btn-secondary btn-small env-variable-edit" data-index="${index}">Edit</button> <button class="btn btn-danger btn-small env-variable-delete" data-index="${index}">Delete</button> </div> </div> `).join("")} </div> `; addEnvironmentVariableListeners(); } function addEnvironmentVariableListeners() { document.querySelectorAll(".env-variable-edit").forEach((button) => { button.addEventListener("click", (e) => { const index = parseInt(e.target.dataset.index); editEnvironmentVariable(index); }); }); document.querySelectorAll(".env-variable-delete").forEach((button) => { button.addEventListener("click", (e) => { const index = parseInt(e.target.dataset.index); deleteEnvironmentVariable(index); }); }); } async function migrateFromOldFormat(oldRules) { if (oldRules.length === 0) return []; const unifiedGroup = { id: "default-mixed-" + Date.now(), name: "Default Rules", expanded: true, enabled: true, rules: oldRules }; return [unifiedGroup]; } async function saveGroups() { try { await chrome.storage.local.set({ groups }); chrome.runtime.sendMessage({ action: "updateRules" }); if (apiMockManager) { apiMockManager.onRulesChanged(); } console.log("Groups saved successfully"); } catch (error) { console.error("Error saving groups:", error); } } function renderGroups() { console.log("🎨 Starting renderGroups with groups:", groups); const container = document.getElementById("allGroups"); if (groups.length === 0) { console.log("🎨 No groups to render, showing empty state"); container.innerHTML = ` <div class="empty-state"> <p>No rule groups configured.</p> <p>Click "Add Group" to create your first group.</p> </div> `; return; } console.log("🎨 Rendering", groups.length, "groups"); container.innerHTML = groups.map((group, groupIndex) => { const enabledRules = group.rules.filter((rule) => rule.enabled).length; const groupEnabled = group.enabled !== false; const urlRules = group.rules.filter((rule) => rule.type === "url_rewrite").length; const headerRules = group.rules.filter((rule) => rule.type === "modify_headers").length; const mockRules = group.rules.filter((rule) => rule.type === "mock_response").length; let typeIndicator = ""; const totalTypes = (urlRules > 0 ? 1 : 0) + (headerRules > 0 ? 1 : 0) + (mockRules > 0 ? 1 : 0); if (totalTypes > 1) { const types = []; if (urlRules > 0) types.push(`${urlRules} URL`); if (headerRules > 0) types.push(`${headerRules} Headers`); if (mockRules > 0) types.push(`${mockRules} Mock`); typeIndicator = `<span class="mixed-group-indicator">Mixed (${types.join(", ")})</span>`; } else if (urlRules > 0) { typeIndicator = `<span class="rule-type-indicator url-rewrite">URL Rewrite</span>`; } else if (headerRules > 0) { typeIndicator = `<span class="rule-type-indicator header-modify">Headers</span>`; } else if (mockRules > 0) { typeIndicator = `<span class="rule-type-indicator mock-response">Mock Response</span>`; } return ` <div class="group-container"> <div class="group-header ${group.expanded === false ? "collapsed" : ""}" data-group-index="${groupIndex}"> <div class="group-left"> <span class="group-expand ${group.expanded === false ? "collapsed" : ""}" data-group-index="${groupIndex}">▼</span> <span class="group-name" title="${escapeHtml(group.name)}">${escapeHtml(group.name)}</span> <span class="group-count">${group.rules.length} rules (${enabledRules} enabled)</span> ${typeIndicator} </div> <div class="group-toggle"> <label class="toggle-switch"> <input type="checkbox" ${groupEnabled ? "checked" : ""} data-group-index="${groupIndex}" class="group-toggle-switch"> <span class="slider"></span> </label> </div> <div class="group-actions"> <div class="add-rule-dropdown"> <button class="btn btn-success btn-small add-rule-toggle" data-group-index="${groupIndex}">Add Rule ▼</button> <div class="add-rule-dropdown-content" data-group-index="${groupIndex}"> <button class="add-rule-option" data-group-index="${groupIndex}" data-rule-type="url_rewrite">🔗 URL Rewrite Rule</button> <button class="add-rule-option" data-group-index="${groupIndex}" data-rule-type="modify_headers">📝 Header Modification Rule</button> <button class="add-rule-option" data-group-index="${groupIndex}" data-rule-type="mock_response">🎭 Mock Response Rule</button> </div> </div> <button class="btn export-btn btn-small group-export" data-group-index="${groupIndex}">Export</button> <button class="btn btn-secondary btn-small group-edit" data-group-index="${groupIndex}">Edit</button> <button class="btn btn-danger btn-small group-delete" data-group-index="${groupIndex}">Delete</button> </div> </div> <div class="group-content" style="display: ${group.expanded === false ? "none" : "block"}"> ${group.rules.length > 0 ? renderRulesInGroup(group.rules, groupIndex) : '<div class="empty-group">No rules in this group. Click "Add Rule" to add the first rule.</div>'} </div> </div> `; }).join(""); addGroupControlListeners(); } function renderRulesInGroup(rules, groupIndex) { console.log("🎨 renderRulesInGroup called for group", groupIndex, "with rules:", rules); return rules.map((rule, ruleIndex) => { console.log("🎨 Processing rule", ruleIndex, "of type", rule.type, ":", rule); let ruleTypeIndicator; if (rule.type === "url_rewrite") { ruleTypeIndicator = '<span class="rule-type-indicator url-rewrite">URL</span>'; } else if (rule.type === "modify_headers") { ruleTypeIndicator = '<span class="rule-type-indicator header-modify">Header</span>'; } else if (rule.type === "mock_response") { ruleTypeIndicator = '<span class="rule-type-indicator mock-response">Mock</span>'; } const isFirstRule = ruleIndex === 0; const isLastRule = ruleIndex === rules.length - 1; if (rule.type === "url_rewrite") { return ` <div class="rule-card ${rule.enabled ? "enabled" : "disabled"}"> <div class="rule-header"> <div class="rule-left"> <div class="rule-title">${escapeHtml(rule.name || "Unnamed Rule")} ${ruleTypeIndicator}</div> </div> <div class="rule-controls"> <label class="toggle-switch"> <input type="checkbox" ${rule.enabled ? "checked" : ""} data-group-index="${groupIndex}" data-rule-index="${ruleIndex}" class="rule-toggle"> <span class="slider"></span> </label> <div class="rule-reorder-controls"> <button class="btn btn-reorder rule-move-up" data-group-index="${groupIndex}" data-rule-index="${ruleIndex}" ${isFirstRule ? "disabled" : ""} title="Move Up">▲</button> <button class="btn btn-reorder rule-move-down" data-group-index="${groupIndex}" data-rule-index="${ruleIndex}" ${isLastRule ? "disabled" : ""} title="Move Down">▼</button> </div> <button class="btn btn-duplicate btn-small rule-duplicate" data-group-index="${groupIndex}" data-rule-index="${ruleIndex}">Duplicate</button> <button class="btn btn-secondary btn-small rule-edit" data-group-index="${groupIndex}" data-rule-index="${ruleIndex}" data-rule-type="url">Edit</button> <button class="btn btn-danger btn-small rule-delete" data-group-index="${groupIndex}" data-rule-index="${ruleIndex}">Delete</button> </div> </div> <div class="rule-details"> <div><strong>Type:</strong> ${getMatchingTypeDisplayName(rule.matchingType || "regex")}</div> <div><strong>Source:</strong> <code>${escapeHtml(rule.sourceUrl)}</code></div> <div><strong>Target:</strong> <code>${escapeHtml(rule.targetUrl)}</code></div> </div> </div> `; } else if (rule.type === "modify_headers") { return ` <div class="rule-card ${rule.enabled ? "enabled" : "disabled"}"> <div class="rule-header"> <div class="rule-left"> <div class="rule-title">${escapeHtml(rule.name || "Unnamed Rule")} ${ruleTypeIndicator}</div> </div> <div class="rule-controls"> <label class="toggle-switch"> <input type="checkbox" ${rule.enabled ? "checked" : ""} data-group-index="${groupIndex}" data-rule-index="${ruleIndex}" class="rule-toggle"> <span class="slider"></span> </label> <div class="rule-reorder-controls"> <button class="btn btn-reorder rule-move-up" data-group-index="${groupIndex}" data-rule-index="${ruleIndex}" ${isFirstRule ? "disabled" : ""} title="Move Up">▲</button> <button class="btn btn-reorder rule-move-down" data-group-index="${groupIndex}" data-rule-index="${ruleIndex}" ${isLastRule ? "disabled" : ""} title="Move Down">▼</button> </div> <button class="btn btn-duplicate btn-small rule-duplicate" data-group-index="${groupIndex}" data-rule-index="${ruleIndex}">Duplicate</button> <button class="btn btn-secondary btn-small rule-edit" data-group-index="${groupIndex}" data-rule-index="${ruleIndex}" data-rule-type="header">Edit</button> <button class="btn btn-danger btn-small rule-delete" data-group-index="${groupIndex}" data-rule-index="${ruleIndex}">Delete</button> </div> </div> <div class="rule-details"> <div><strong>Type:</strong> ${getMatchingTypeDisplayName(rule.matchingType || "regex")}</div> <div><strong>URL Pattern:</strong> <code>${escapeHtml(rule.urlPattern)}</code></div> <div><strong>Headers:</strong></div> <ul> ${rule.headers.map((header) => ` <li><strong>${escapeHtml(header.name)}</strong> (${header.operation}) ${header.target} ${header.value ? `→ ${escapeHtml(header.value)}` : ""}</li> `).join("")} </ul> </div> </div> `; } else if (rule.type === "mock_response") { return ` <div class="rule-card ${rule.enabled ? "enabled" : "disabled"}"> <div class="rule-header"> <div class="rule-left"> <div class="rule-title">${escapeHtml(rule.name || "Unnamed Rule")} ${ruleTypeIndicator}</div> </div> <div class="rule-controls"> <label class="toggle-switch"> <input type="checkbox" ${rule.enabled ? "checked" : ""} data-group-index="${groupIndex}" data-rule-index="${ruleIndex}" class="rule-toggle"> <span class="slider"></span> </label> <div class="rule-reorder-controls"> <button class="btn btn-reorder rule-move-up" data-group-index="${groupIndex}" data-rule-index="${ruleIndex}" ${isFirstRule ? "disabled" : ""} title="Move Up">▲</button> <button class="btn btn-reorder rule-move-down" data-group-index="${groupIndex}" data-rule-index="${ruleIndex}" ${isLastRule ? "disabled" : ""} title="Move Down">▼</button> </div> <button class="btn btn-duplicate btn-small rule-duplicate" data-group-index="${groupIndex}" data-rule-index="${ruleIndex}">Duplicate</button> <button class="btn btn-secondary btn-small rule-edit" data-group-index="${groupIndex}" data-rule-index="${ruleIndex}" data-rule-type="mock">Edit</button> <button class="btn btn-danger btn-small rule-delete" data-group-index="${groupIndex}" data-rule-index="${ruleIndex}">Delete</button> </div> </div> <div class="rule-details"> <div><strong>Method:</strong> <span class="method-badge method-${(rule.method || "any").toLowerCase()}">${rule.method || "ANY"}</span></div> <div><strong>Type:</strong> ${getMatchingTypeDisplayName(rule.matchingType || "regex")}</div> <div><strong>URL Pattern:</strong> <code>${escapeHtml(rule.urlPattern || "")}</code></div> <div><strong>Response:</strong> ${(rule.responseType || "json").toUpperCase()} (${rule.statusCode || 200})</div> ${rule.delay > 0 ? `<div><strong>Delay:</strong> ${rule.delay}ms</div>` : ""} ${rule.description ? `<div><strong>Description:</strong> ${escapeHtml(rule.description)}</div>` : ""} </div> </div> `; } console.warn("⚠️ Unknown rule type:", rule.type, rule); return ` <div class="rule-card ${rule.enabled ? "enabled" : "disabled"}"> <div class="rule-header"> <div class="rule-left"> <div class="rule-title">${escapeHtml(rule.name || "Unknown Rule")} <span class="rule-type-indicator">Unknown</span></div> </div> </div> <div class="rule-details"> <div><strong>Type:</strong> ${rule.type || "Unknown"}</div> <div><strong>Data:</strong> <pre>${JSON.stringify(rule, null, 2)}</pre></div> </div> </div> `; }).join(""); } function addGroupControlListeners() { document.querySelectorAll(".group-expand").forEach((arrow) => { arrow.addEventListener("click", (e) => { e.stopPropagation(); const groupIndex = parseInt(arrow.dataset.groupIndex); toggleGroup(groupIndex); }); }); document.querySelectorAll(".group-header").forEach((header) => { header.addEventListener("click", (e) => { if (e.target === header || e.target.classList.contains("group-name")) { const groupIndex = parseInt(header.dataset.groupIndex); toggleGroup(groupIndex); } }); }); document.querySelectorAll(".group-toggle-switch").forEach((toggle) => { toggle.addEventListener("change", (e) => { e.stopPropagation(); const groupIndex = parseInt(e.target.dataset.groupIndex); toggleGroupEnabled(groupIndex); }); }); document.querySelectorAll(".add-rule-toggle").forEach((button) => { button.addEventListener("click", (e) => { e.stopPropagation(); const groupIndex = parseInt(e.target.dataset.groupIndex); toggleAddRuleDropdown(groupIndex); }); }); document.querySelectorAll(".add-rule-option").forEach((option) => { option.addEventListener("click", (e) => { e.stopPropagation(); const groupIndex = parseInt(e.target.dataset.groupIndex); const ruleType = e.target.dataset.ruleType; addRuleToGroup(groupIndex, ruleType); const dropdown = e.target.closest(".add-rule-dropdown-content"); dropdown.classList.remove("show"); }); }); document.querySelectorAll(".group-export").forEach((button) => { button.addEventListener("click", async (e) => { e.stopPropagation(); const groupIndex = parseInt(e.target.dataset.groupIndex); await exportGroup(groupIndex); }); }); document.querySelectorAll(".group-edit").forEach((button) => { button.addEventListener("click", (e) => { e.stopPropagation(); const groupIndex = parseInt(e.target.dataset.groupIndex); editGroup(groupIndex); }); }); document.querySelectorAll(".group-delete").forEach((button) => { button.addEventListener("click", (e) => { e.stopPropagation(); const groupIndex = parseInt(e.target.dataset.groupIndex); deleteGroup(groupIndex); }); }); document.querySelectorAll(".rule-toggle").forEach((toggle) => { toggle.addEventListener("change", (e) => { const groupIndex = parseInt(e.target.dataset.groupIndex); const ruleIndex = parseInt(e.target.dataset.ruleIndex); toggleRule(groupIndex, ruleIndex); }); }); document.querySelectorAll(".rule-edit").forEach((button) => { button.addEventListener("click", (e) => { const groupIndex = parseInt(e.target.dataset.groupIndex); const ruleIndex = parseInt(e.target.dataset.ruleIndex); const type = e.target.dataset.ruleType; if (type === "url") { editUrlRule(groupIndex, ruleIndex); } else if (type === "header") { editHeaderRule(groupIndex, ruleIndex); } else if (type === "mock") { openMockRuleModal(groupIndex, ruleIndex); } }); }); document.querySelectorAll(".rule-delete").forEach((button) => { button.addEventListener("click", (e) => { const groupIndex = parseInt(e.target.dataset.groupIndex); const ruleIndex = parseInt(e.target.dataset.ruleIndex); deleteRule(groupIndex, ruleIndex); }); }); document.querySelectorAll(".rule-duplicate").forEach((button) => { button.addEventListener("click", (e) => { const groupIndex = parseInt(e.target.dataset.groupIndex); const ruleIndex = parseInt(e.target.dataset.ruleIndex); duplicateRule(groupIndex, ruleIndex); }); }); document.querySelectorAll(".rule-move-up").forEach((button) => { button.addEventListener("click", (e) => { const groupIndex = parseInt(e.target.dataset.groupIndex); const ruleIndex = parseInt(e.target.dataset.ruleIndex); moveRuleUp(groupIndex, ruleIndex); }); }); document.querySelectorAll(".rule-move-down").forEach((button) => { button.addEventListener("click", (e) => { const groupIndex = parseInt(e.target.dataset.groupIndex); const ruleIndex = parseInt(e.target.dataset.ruleIndex); moveRuleDown(groupIndex, ruleIndex); }); }); } function toggleGroup(groupIndex) { groups[groupIndex].expanded = groups[groupIndex].expanded !== false ? false : true; saveGroups(); renderGroups(); } function toggleGroupEnabled(groupIndex) { groups[groupIndex].enabled = !groups[groupIndex].enabled; if (!groups[groupIndex].enabled) { groups[groupIndex].rules.forEach((rule) => { rule.wasEnabled = rule.enabled; rule.enabled = false; }); } else { groups[groupIndex].rules.forEach((rule) => { rule.enabled = rule.wasEnabled !== void 0 ? rule.wasEnabled : true; delete rule.wasEnabled; }); } saveGroups(); renderGroups(); } function toggleAddRuleDropdown(groupIndex) { document.querySelectorAll(".add-rule-dropdown-content.show").forEach((dropdown2) => { dropdown2.classList.remove("show"); }); const dropdown = document.querySelector(`.add-rule-dropdown-content[data-group-index="${groupIndex}"]`); if (dropdown) { dropdown.classList.toggle("show"); } } function addRuleToGroup(groupIndex, ruleType) { groups[groupIndex]; if (ruleType === "url_rewrite") { openUrlRuleModal(groupIndex); } else if (ruleType === "modify_headers") { openHeaderRuleModal(groupIndex); } else if (ruleType === "mock_response") { openMockRuleModal(groupIndex); } } async function exportGroup(groupIndex) { const group = groups[groupIndex]; const { exportEnvironmentVariables } = await chrome.storage.sync.get(["exportEnvironmentVariables"]); const exportData = { version: "1.0", exportDate: (/* @__PURE__ */ new Date()).toISOString(), groups: [group] }; if (exportEnvironmentVariables === true && environmentVariables.length > 0) { exportData.environmentVariables = environmentVariables; } const fileName = `${group.name.replace(/[^a-z0-9]/gi, "_").toLowerCase()}_${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.json`; downloadJSON(exportData, fileName); } async function toggleRule(groupIndex, ruleIndex) { groups[groupIndex].rules[ruleIndex].enabled = !groups[groupIndex].rules[ruleIndex].enabled; await saveGroups(); renderGroups(); } async function deleteRule(groupIndex, ruleIndex) { if (confirm("Are you sure you want to delete this rule?")) { groups[groupIndex].rules.splice(ruleIndex, 1); await saveGroups(); renderGroups(); } } function openGroupModal(groupIndex = null) { editingGroupIndex = groupIndex; const modal = document.getElementById("groupModal"); const nameInput = document.getElementById("groupName"); if (groupIndex !== null) { const group = groups[groupIndex]; nameInput.value = group.name || ""; } else { nameInput.value = ""; } modal.style.display = "block"; nameInput.focus(); } function closeGroupModal() { document.getElementById("groupModal").style.display = "none"; editingGroupIndex = null; } function editGroup(index) { openGroupModal(index); } async function deleteGroup(index) { if (confirm("Are you sure you want to delete this group and all its rules?")) { groups.splice(index, 1); await saveGroups(); renderGroups(); } } async function saveGroup() { const name = document.getElementById("groupName").value.trim(); if (!name) { alert("Please enter a group name."); return; } const group = { id: editingGroupIndex !== null ? groups[editingGroupIndex].id : Date.now().toString(), name, expanded: editingGroupIndex !== null ? groups[editingGroupIndex].expanded : true, enabled: editingGroupIndex !== null ? groups[editingGroupIndex].enabled : true, rules: editingGroupIndex !== null ? groups[editingGroupIndex].rules : [] }; if (editingGroupIndex !== null) { groups[editingGroupIndex] = group; } else { groups.push(group); } try { await saveGroups(); renderGroups(); } catch (error) { console.error("Error saving group:", error); alert("Error saving group: " + error.message); } closeGroupModal(); } function openUrlRuleModal(groupIndex = null, ruleIndex = null) { editingRuleIndex = ruleIndex; editingRuleType = "url_rewrite"; editingGroupIndex = groupIndex; const modal = document.getElementById("urlRuleModal"); const matchingTypeSelect = document.getElementById("matchingType"); const sourceInput = document.getElementById("sourceUrl"); const targetInput = document.getElementById("targetUrl"); if (groupIndex !== null && ruleIndex !== null) { const rule = groups[groupIndex].rules[ruleIndex]; matchingTypeSelect.value = rule.matchingType || "regex"; sourceInput.value = rule.sourceUrl || ""; targetInput.value = rule.targetUrl || ""; if (rule.targetUrl) { setTimeout(() => validateTargetUrlDomainInput(), 100); } } else { matchingTypeSelect.value = "regex"; sourceInput.value = ""; targetInput.value = ""; } resetTestSection("sourceUrlTestSection", "toggleSourceUrlTest"); resetTestSection("targetUrlExamples"); clearRegexResult("sourceUrlResult"); clearRegexResult("targetUrlPreview"); updateMatchingTypeUI(); addVariableSuggestions(sourceInput); addVariableSuggestions(targetInput); modal.style.display = "block"; sourceInput.focus(); } function closeUrlRuleModal() { document.getElementById("urlRuleModal").style.display = "none"; editingRuleIndex = null; editingRuleType = null; editingGroupIndex = null; } function editUrlRule(groupIndex, ruleIndex) { openUrlRuleModal(groupIndex, ruleIndex); } async function saveUrlRule() { const matchingType = document.getElementById("matchingType").value; const sourceUrl = document.getElementById("sourceUrl").value.trim(); const targetUrl = document.getElementById("targetUrl").value.trim(); if (!sourceUrl || !targetUrl)