diff --git a/src/js/background.js b/src/js/background.js index 1ca5804..e05e108 100644 --- a/src/js/background.js +++ b/src/js/background.js @@ -1,526 +1,810 @@ -// Cross-browser compatibility shim -const browserAPI = typeof browser !== "undefined" ? browser : chrome; - -// URLs and Constants -const filterListURLUnsafe = - "https://raw.githubusercontent.com/fmhy/FMHYFilterlist/refs/heads/main/sitelist.txt"; -const filterListURLPotentiallyUnsafe = - "https://raw.githubusercontent.com/fmhy/FMHYFilterlist/refs/heads/main/sitelist-plus.txt"; -const safeListURLs = [ - "https://raw.githubusercontent.com/fmhy/edit/refs/heads/main/docs/adblockvpnguide.md", - "https://raw.githubusercontent.com/fmhy/edit/refs/heads/main/docs/ai.md", - "https://raw.githubusercontent.com/fmhy/edit/refs/heads/main/docs/android-iosguide.md", - "https://raw.githubusercontent.com/fmhy/edit/refs/heads/main/docs/audiopiracyguide.md", - "https://raw.githubusercontent.com/fmhy/edit/refs/heads/main/docs/devtools.md", - "https://raw.githubusercontent.com/fmhy/edit/refs/heads/main/docs/downloadpiracyguide.md", - "https://raw.githubusercontent.com/fmhy/edit/refs/heads/main/docs/edupiracyguide.md", - "https://raw.githubusercontent.com/fmhy/edit/refs/heads/main/docs/file-tools.md", - "https://raw.githubusercontent.com/fmhy/edit/refs/heads/main/docs/gaming-tools.md", - "https://raw.githubusercontent.com/fmhy/edit/refs/heads/main/docs/gamingpiracyguide.md", - "https://raw.githubusercontent.com/fmhy/edit/refs/heads/main/docs/img-tools.md", - "https://raw.githubusercontent.com/fmhy/edit/refs/heads/main/docs/internet-tools.md", - "https://raw.githubusercontent.com/fmhy/edit/refs/heads/main/docs/linuxguide.md", - "https://raw.githubusercontent.com/fmhy/edit/refs/heads/main/docs/miscguide.md", - "https://raw.githubusercontent.com/fmhy/edit/refs/heads/main/docs/non-english.md", - "https://raw.githubusercontent.com/fmhy/edit/refs/heads/main/docs/nsfwpiracy.md", - "https://raw.githubusercontent.com/fmhy/edit/refs/heads/main/docs/readingpiracyguide.md", - "https://raw.githubusercontent.com/fmhy/edit/refs/heads/main/docs/social-media-tools.md", - "https://raw.githubusercontent.com/fmhy/edit/refs/heads/main/docs/storage.md", - "https://raw.githubusercontent.com/fmhy/edit/refs/heads/main/docs/system-tools.md", - "https://raw.githubusercontent.com/fmhy/edit/refs/heads/main/docs/text-tools.md", - "https://raw.githubusercontent.com/fmhy/edit/refs/heads/main/docs/torrentpiracyguide.md", - "https://raw.githubusercontent.com/fmhy/edit/refs/heads/main/docs/video-tools.md", - "https://raw.githubusercontent.com/fmhy/edit/refs/heads/main/docs/videopiracyguide.md", -]; -const starredListURL = - "https://raw.githubusercontent.com/fmhy/bookmarks/refs/heads/main/fmhy_in_bookmarks_starred_only.html"; -const fmhyFilterListURL = - "https://raw.githubusercontent.com/fmhy/FMHY-SafeGuard/refs/heads/main/fmhy-filterlist.txt"; - -// State Variables -let unsafeSitesRegex = null; -let potentiallyUnsafeSitesRegex = null; -let fmhySitesRegex = null; -let safeSites = []; -let starredSites = []; -const approvedUrls = new Map(); // Map to store approved URLs per tab - -// Helper Functions -function extractUrlsFromMarkdown(markdown) { - const urlRegex = /https?:\/\/[^\s)]+/g; - return markdown.match(urlRegex) || []; -} - -function extractUrlsFromBookmarks(html) { - const urlRegex = / - domain.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") - ); - return new RegExp(`(${escapedList.join("|")})`, "i"); -} - -function extractUrlsFromFilterList(text) { - return text - .split("\n") - .map((line) => line.trim()) - .filter((line) => line && !line.startsWith("!")) - .map((line) => normalizeUrl(line)) - .filter((url) => url !== null); -} - -// Fetch and Update Functions -async function fetchFilterLists() { - console.log("Fetching filter lists..."); - try { - const [unsafeResponse, potentiallyUnsafeResponse, fmhyResponse] = - await Promise.all([ - fetch(filterListURLUnsafe), - fetch(filterListURLPotentiallyUnsafe), - fetch(fmhyFilterListURL), - ]); - - let unsafeSites = []; - let potentiallyUnsafeSites = []; - let fmhySites = []; - - if (unsafeResponse.ok) { - const unsafeText = await unsafeResponse.text(); - unsafeSites = extractUrlsFromFilterList(unsafeText); - unsafeSitesRegex = generateRegexFromList(unsafeSites); - } - - if (potentiallyUnsafeResponse.ok) { - const potentiallyUnsafeText = await potentiallyUnsafeResponse.text(); - potentiallyUnsafeSites = extractUrlsFromFilterList(potentiallyUnsafeText); - potentiallyUnsafeSitesRegex = generateRegexFromList( - potentiallyUnsafeSites - ); - } - - if (fmhyResponse.ok) { - const fmhyText = await fmhyResponse.text(); - fmhySites = extractUrlsFromFilterList(fmhyText); - fmhySitesRegex = generateRegexFromList(fmhySites); - } - - await browserAPI.storage.local.set({ - unsafeSites, - potentiallyUnsafeSites, - fmhySites, - unsafeFilterCount: unsafeSites.length, - potentiallyUnsafeFilterCount: potentiallyUnsafeSites.length, - fmhyFilterCount: fmhySites.length, - lastUpdated: new Date().toISOString(), - }); - - console.log("Filter lists fetched and stored successfully."); - - notifySettingsPage(); - } catch (error) { - console.error("Error fetching filter lists:", error); - } -} - -async function fetchSafeSites() { - console.log("Fetching safe sites from multiple URLs..."); - try { - const fetchPromises = safeListURLs.map((url) => fetch(url)); - const responses = await Promise.all(fetchPromises); - - // Extract URLs from each markdown document - let allUrls = []; - for (const response of responses) { - if (response.ok) { - const markdown = await response.text(); - const urls = extractUrlsFromMarkdown(markdown); - allUrls = allUrls.concat(urls); - } else { - console.warn(`Failed to fetch from ${response.url}`); - } - } - - // Normalize URLs and remove duplicates - safeSites = [...new Set(allUrls.map((url) => normalizeUrl(url.trim())))]; - - // Store safe site count for use in the extension's storage - await browserAPI.storage.local.set({ - safeSiteCount: safeSites.length, - }); - - console.log("Stored safe site count:", safeSites.length); - } catch (error) { - console.error("Error fetching safe sites:", error); - } -} - -async function fetchStarredSites() { - console.log("Fetching starred sites..."); - try { - const response = await fetch(starredListURL); - if (response.ok) { - const html = await response.text(); - const urls = extractUrlsFromBookmarks(html); - starredSites = [...new Set([...urls.map(normalizeUrl), ...starredSites])]; - } - } catch (error) { - console.error("Error fetching starred sites:", error); - } -} - -// UI Update Functions -function updatePageAction(status, tabId) { - const icons = { - safe: { - 19: "../res/icons/safe_19.png", - 38: "../res/icons/safe_38.png", - }, - unsafe: { - 19: "../res/icons/unsafe_19.png", - 38: "../res/icons/unsafe_38.png", - }, - potentially_unsafe: { - 19: "../res/icons/potentially_unsafe_19.png", - 38: "../res/icons/potentially_unsafe_38.png", - }, - starred: { - 19: "../res/icons/starred_19.png", - 38: "../res/icons/starred_38.png", - }, - fmhy: { - 19: "../res/icons/fmhy_19.png", - 38: "../res/icons/fmhy_38.png", - }, - extension_page: { - 19: "../res/ext_icon_144.png", - 38: "../res/ext_icon_144.png", - }, - default: { - 19: "../res/icons/default_19.png", - 38: "../res/icons/default_38.png", - }, - }; - - const icon = icons[status] || icons["default"]; - - browserAPI.action.setIcon({ - tabId: tabId, - path: icon, - }); -} - -async function notifySettingsPage() { - const tabs = await browserAPI.tabs.query({}); - for (const tab of tabs) { - try { - await browserAPI.tabs.sendMessage(tab.id, { type: "filterlistUpdated" }); - } catch (e) { - // Ignore errors for tabs that can't receive messages - } - } -} - -// Site Status Checking -function checkSiteAndUpdatePageAction(tabId, url) { - console.log( - `checkSiteAndUpdatePageAction: Checking status for ${url} on tab ${tabId}` - ); - - if (!url) { - updatePageAction("default", tabId); - return; - } - - const normalizedUrl = normalizeUrl(url.trim()); - const rootUrl = extractRootUrl(normalizedUrl); - - // Detect if the URL is an internal extension page - const warningPageUrl = browserAPI.runtime.getURL("pub/warning-page.html"); - if (url.startsWith(warningPageUrl)) { - // Skip if already on the warning page to avoid looping - updatePageAction("extension_page", tabId); - return; - } - - // Check if the full URL is starred or has a specific status - let status = getStatusFromLists(normalizedUrl); - let matchedUrl = normalizedUrl; - - // If no specific match for the full URL, check the root URL - if (status === "no_data") { - status = getStatusFromLists(rootUrl); - matchedUrl = rootUrl; - } - - // Apply the correct icon status to the tab - updatePageAction(status, tabId); - - // Handle unsafe sites that need warning page redirection if not approved - if (status === "unsafe" && !approvedUrls.get(tabId)?.includes(rootUrl)) { - openWarningPage(tabId, rootUrl); - } -} - -// Update Schedule Management -async function shouldUpdate() { - try { - const { lastUpdated } = await browserAPI.storage.local.get("lastUpdated"); - const { updateFrequency = "daily" } = await browserAPI.storage.sync.get({ - updateFrequency: "daily", - }); - - if (!lastUpdated) return true; - - const lastUpdate = new Date(lastUpdated); - const now = new Date(); - const diffHours = (now - lastUpdate) / (1000 * 60 * 60); - - if (updateFrequency === "daily") { - return diffHours >= 24; - } else if (updateFrequency === "weekly") { - return diffHours >= 168; - } else if (updateFrequency === "monthly") { - return diffHours >= 720; - } - return false; - } catch (error) { - console.error("Error checking update schedule:", error); - return false; - } -} - -async function setupUpdateSchedule() { - await browserAPI.alarms.clearAll(); - - // Get the user's preferred update frequency from storage - const { updateFrequency } = await browserAPI.storage.sync.get({ - updateFrequency: "daily", - }); - - // Determine period in minutes based on selected frequency - let periodInMinutes; - switch (updateFrequency) { - case "weekly": - periodInMinutes = 10080; // 7 days in minutes - break; - case "monthly": - periodInMinutes = 43200; // 30 days in minutes - break; - default: - periodInMinutes = 1440; // 24 hours in minutes for daily updates - } - - // Create the alarm based on calculated period - browserAPI.alarms.create("checkUpdate", { - periodInMinutes: periodInMinutes, - }); -} - -// Event Listeners -browserAPI.runtime.onMessage.addListener((message, sender, sendResponse) => { - if (message.action === "checkSiteStatus") { - const { url, rootUrl } = message; - - // Attempt to match with the full URL first (for specific paths) - let status = getStatusFromLists(url); - let matchedUrl = url; - - // If no specific match, try the root URL - if (status === "no_data") { - status = getStatusFromLists(rootUrl); - matchedUrl = rootUrl; - } - - sendResponse({ status, matchedUrl }); - return true; - } -}); - -function getStatusFromLists(url) { - if (unsafeSitesRegex?.test(url)) return "unsafe"; - if (potentiallyUnsafeSitesRegex?.test(url)) return "potentially_unsafe"; - if (fmhySitesRegex?.test(url)) return "fmhy"; - if (starredSites.includes(url)) return "starred"; - if (safeSites.includes(url)) return "safe"; - return "no_data"; -} - -async function openWarningPage(tabId, unsafeUrl) { - const normalizedUrl = normalizeUrl(unsafeUrl); - const tabApprovedUrls = approvedUrls.get(tabId) || []; - - // Check if URL has already been approved for this tab to avoid loop - if (tabApprovedUrls.includes(normalizedUrl)) { - console.log(`URL ${unsafeUrl} was already approved for tab ${tabId}`); - return; - } - - // Fetch the warning page setting - const { warningPage } = await browserAPI.storage.sync.get({ - warningPage: true, - }); - - if (!warningPage) { - console.log("Warning page is disabled by the user settings."); - return; - } - - // Add temporary approval to avoid repeated redirection - tabApprovedUrls.push(normalizedUrl); - approvedUrls.set(tabId, tabApprovedUrls); - - // Redirect to the warning page if it is enabled in settings - const warningPageUrl = browserAPI.runtime.getURL( - `../pub/warning-page.html?url=${encodeURIComponent(unsafeUrl)}` - ); - browserAPI.tabs.update(tabId, { url: warningPageUrl }); -} - -// Add listener for approval from the warning page -browserAPI.runtime.onMessage.addListener((message, sender, sendResponse) => { - if (message.action === "approveSite") { - const { tabId, url } = message; - const rootUrl = extractRootUrl(url); - - // Fetch existing approved URLs from storage - browserAPI.storage.local.get("approvedUrls", (result) => { - const approvedUrls = result.approvedUrls || []; - - // Add the root URL if not already approved - if (!approvedUrls.includes(rootUrl)) { - approvedUrls.push(rootUrl); - browserAPI.storage.local.set({ approvedUrls }); - console.log(`approveSite: ${rootUrl} approved globally.`); - } - - // Set the toolbar icon to "unsafe" immediately - updatePageAction("unsafe", tabId); - sendResponse({ status: "approved" }); - }); - } - return true; -}); - -// Listen for settings updates from the settings page -browserAPI.runtime.onMessage.addListener((message, sender, sendResponse) => { - if (message.type === "settingsUpdated") { - setupUpdateSchedule(); // Adjust update schedule based on new settings - sendResponse({ status: "Settings updated successfully" }); - return true; // Indicates asynchronous response handling - } -}); - -browserAPI.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { - if (changeInfo.status === "complete" && tab.url) { - checkSiteAndUpdatePageAction(tabId, tab.url); - } -}); - -browserAPI.tabs.onActivated.addListener(async (activeInfo) => { - const tab = await browserAPI.tabs.get(activeInfo.tabId); - if (tab.url) { - checkSiteAndUpdatePageAction(tab.id, tab.url); - } -}); - -browserAPI.alarms.onAlarm.addListener(async (alarm) => { - if (alarm.name === "checkUpdate") { - const needsUpdate = await shouldUpdate(); - if (needsUpdate) { - await fetchFilterLists(); - } - } -}); - -browserAPI.tabs.onRemoved.addListener((tabId) => { - approvedUrls.delete(tabId); - browserAPI.storage.local.remove(`proceedTab_${tabId}`); -}); - -// Initialize extension -async function initializeExtension() { - try { - const { - unsafeFilterCount, - potentiallyUnsafeFilterCount, - fmhyFilterCount, - unsafeSites, - potentiallyUnsafeSites, - fmhySites, - } = await browserAPI.storage.local.get([ - "unsafeFilterCount", - "potentiallyUnsafeFilterCount", - "fmhyFilterCount", - "unsafeSites", - "potentiallyUnsafeSites", - "fmhySites", - ]); - - // Check if data is available in storage and load it into memory - if (unsafeSites && potentiallyUnsafeSites && fmhySites) { - unsafeSitesRegex = generateRegexFromList(unsafeSites); - potentiallyUnsafeSitesRegex = generateRegexFromList( - potentiallyUnsafeSites - ); - fmhySitesRegex = generateRegexFromList(fmhySites); - console.log("Loaded filter lists from storage."); - } else { - // If data isn't in storage, fetch it - await fetchFilterLists(); - } - - // Fetch safe and starred sites, and set up the update schedule - await fetchSafeSites(); - await fetchStarredSites(); - await setupUpdateSchedule(); - - console.log("Extension initialized successfully."); - } catch (error) { - console.error("Error during extension initialization:", error); - } -} - -initializeExtension(); +// Cross-browser compatibility shim +const browserAPI = typeof browser !== "undefined" ? browser : chrome; + +// URLs and Constants +const filterListURLUnsafe = + "https://raw.githubusercontent.com/fmhy/FMHYFilterlist/refs/heads/main/sitelist.txt"; +const filterListURLPotentiallyUnsafe = + "https://raw.githubusercontent.com/fmhy/FMHYFilterlist/refs/heads/main/sitelist-plus.txt"; +const safeListURLs = [ + "https://raw.githubusercontent.com/fmhy/edit/refs/heads/main/docs/adblockvpnguide.md", + "https://raw.githubusercontent.com/fmhy/edit/refs/heads/main/docs/ai.md", + "https://raw.githubusercontent.com/fmhy/edit/refs/heads/main/docs/android-iosguide.md", + "https://raw.githubusercontent.com/fmhy/edit/refs/heads/main/docs/audiopiracyguide.md", + "https://raw.githubusercontent.com/fmhy/edit/refs/heads/main/docs/devtools.md", + "https://raw.githubusercontent.com/fmhy/edit/refs/heads/main/docs/downloadpiracyguide.md", + "https://raw.githubusercontent.com/fmhy/edit/refs/heads/main/docs/edupiracyguide.md", + "https://raw.githubusercontent.com/fmhy/edit/refs/heads/main/docs/file-tools.md", + "https://raw.githubusercontent.com/fmhy/edit/refs/heads/main/docs/gaming-tools.md", + "https://raw.githubusercontent.com/fmhy/edit/refs/heads/main/docs/gamingpiracyguide.md", + "https://raw.githubusercontent.com/fmhy/edit/refs/heads/main/docs/img-tools.md", + "https://raw.githubusercontent.com/fmhy/edit/refs/heads/main/docs/internet-tools.md", + "https://raw.githubusercontent.com/fmhy/edit/refs/heads/main/docs/linuxguide.md", + "https://raw.githubusercontent.com/fmhy/edit/refs/heads/main/docs/miscguide.md", + "https://raw.githubusercontent.com/fmhy/edit/refs/heads/main/docs/non-english.md", + "https://raw.githubusercontent.com/fmhy/edit/refs/heads/main/docs/nsfwpiracy.md", + "https://raw.githubusercontent.com/fmhy/edit/refs/heads/main/docs/readingpiracyguide.md", + "https://raw.githubusercontent.com/fmhy/edit/refs/heads/main/docs/social-media-tools.md", + "https://raw.githubusercontent.com/fmhy/edit/refs/heads/main/docs/storage.md", + "https://raw.githubusercontent.com/fmhy/edit/refs/heads/main/docs/system-tools.md", + "https://raw.githubusercontent.com/fmhy/edit/refs/heads/main/docs/text-tools.md", + "https://raw.githubusercontent.com/fmhy/edit/refs/heads/main/docs/torrentpiracyguide.md", + "https://raw.githubusercontent.com/fmhy/edit/refs/heads/main/docs/video-tools.md", + "https://raw.githubusercontent.com/fmhy/edit/refs/heads/main/docs/videopiracyguide.md", +]; +const starredListURL = + "https://raw.githubusercontent.com/fmhy/bookmarks/refs/heads/main/fmhy_in_bookmarks_starred_only.html"; +const fmhyFilterListURL = + "https://raw.githubusercontent.com/fmhy/FMHY-SafeGuard/refs/heads/main/fmhy-filterlist.txt"; + +// State Variables +let unsafeSitesRegex = null; +let potentiallyUnsafeSitesRegex = null; +let fmhySitesRegex = null; +let safeSites = []; +let starredSites = []; +const approvedUrls = new Map(); // Map to store approved URLs per tab + +// Helper Functions +function extractUrlsFromMarkdown(markdown) { + const urlRegex = /https?:\/\/[^\s)]+/g; + return markdown.match(urlRegex) || []; +} + +function extractUrlsFromBookmarks(html) { + console.log("Extracting URLs from bookmarks HTML..."); + + // Try multiple regex patterns to handle different bookmark formats + const patterns = [ + /]*HREF="(https?:\/\/[^\s"]+)"[^>]*>([^<]+)/gi, // Full bookmark format + ]; + + const allUrls = []; + + // Try each pattern + for (const pattern of patterns) { + let matches; + while ((matches = pattern.exec(html)) !== null) { + if (matches[1]) { + allUrls.push(matches[1]); + } + } + } + + console.log(`Extracted ${allUrls.length} URLs from bookmarks HTML`); + return allUrls; +} + +function normalizeUrl(url) { + if (!url) { + console.warn("Received null or undefined URL."); + return null; + } + + try { + if (!/^https?:\/\//i.test(url)) { + url = `https://${url}`; + } + + const urlObj = new URL(url); + + // Remove 'www.' prefix consistently + if (urlObj.hostname.startsWith("www.")) { + urlObj.hostname = urlObj.hostname.substring(4); + } + + // Clear search parameters and hash + urlObj.search = ""; + urlObj.hash = ""; + + // Remove trailing slash consistently + let normalized = urlObj.href.replace(/\/+$/, ""); + + return normalized; + } catch (error) { + console.warn(`Invalid URL skipped: ${url} - ${error.message}`); + return null; + } +} + +function extractRootUrl(url) { + if (!url) { + console.warn("Received null or undefined URL for root extraction."); + return null; + } + + try { + const urlObj = new URL(url); + return `${urlObj.protocol}//${urlObj.hostname}`; + } catch (error) { + console.warn(`Failed to extract root URL from: ${url}`); + return null; + } +} + +function generateRegexFromList(list) { + const escapedList = list.map((domain) => + domain.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + ); + return new RegExp(`(${escapedList.join("|")})`, "i"); +} + +function extractUrlsFromFilterList(text) { + return text + .split("\n") + .map((line) => line.trim()) + .filter((line) => line && !line.startsWith("!")) + .map((line) => normalizeUrl(line)) + .filter((url) => url !== null); +} + +// Fetch and Update Functions +async function fetchFilterLists() { + console.log("Fetching filter lists..."); + try { + const [unsafeResponse, potentiallyUnsafeResponse, fmhyResponse] = + await Promise.all([ + fetch(filterListURLUnsafe), + fetch(filterListURLPotentiallyUnsafe), + fetch(fmhyFilterListURL), + ]); + + let unsafeSites = []; + let potentiallyUnsafeSites = []; + let fmhySites = []; + + if (unsafeResponse.ok) { + const unsafeText = await unsafeResponse.text(); + unsafeSites = extractUrlsFromFilterList(unsafeText); + unsafeSitesRegex = generateRegexFromList(unsafeSites); + } + + if (potentiallyUnsafeResponse.ok) { + const potentiallyUnsafeText = await potentiallyUnsafeResponse.text(); + potentiallyUnsafeSites = extractUrlsFromFilterList(potentiallyUnsafeText); + potentiallyUnsafeSitesRegex = generateRegexFromList( + potentiallyUnsafeSites + ); + } + + if (fmhyResponse.ok) { + const fmhyText = await fmhyResponse.text(); + fmhySites = extractUrlsFromFilterList(fmhyText); + fmhySitesRegex = generateRegexFromList(fmhySites); + } + + await browserAPI.storage.local.set({ + unsafeSites, + potentiallyUnsafeSites, + fmhySites, + unsafeFilterCount: unsafeSites.length, + potentiallyUnsafeFilterCount: potentiallyUnsafeSites.length, + fmhyFilterCount: fmhySites.length, + lastUpdated: new Date().toISOString(), + }); + + console.log("Filter lists fetched and stored successfully."); + + notifySettingsPage(); + } catch (error) { + console.error("Error fetching filter lists:", error); + } +} + +async function fetchSafeSites() { + console.log("Fetching safe sites from multiple URLs..."); + try { + const fetchPromises = safeListURLs.map((url) => fetch(url)); + const responses = await Promise.all(fetchPromises); + + // Extract URLs from each markdown document + let allUrls = []; + for (const response of responses) { + if (response.ok) { + const markdown = await response.text(); + const urls = extractUrlsFromMarkdown(markdown); + allUrls = allUrls.concat(urls); + } else { + console.warn(`Failed to fetch from ${response.url}`); + } + } + + // Normalize URLs and remove duplicates + safeSites = [...new Set(allUrls.map((url) => normalizeUrl(url.trim())))]; + + // Store safe sites for content script use + await browserAPI.storage.local.set({ + safeSiteCount: safeSites.length, + safeSiteList: safeSites, + }); + + console.log("Stored safe site count:", safeSites.length); + } catch (error) { + console.error("Error fetching safe sites:", error); + } +} + +async function fetchStarredSites() { + console.log("Fetching starred sites..."); + try { + const response = await fetch(starredListURL); + if (response.ok) { + const html = await response.text(); + const urls = extractUrlsFromBookmarks(html); + starredSites = [...new Set([...urls.map(normalizeUrl), ...starredSites])]; + + // Store starred sites in storage for persistence + await browserAPI.storage.local.set({ + starredSites: starredSites, + starredSiteCount: starredSites.length, + }); + + console.log(`Stored ${starredSites.length} starred sites`); + } + } catch (error) { + console.error("Error fetching starred sites:", error); + } +} + +// UI Update Functions +function updatePageAction(status, tabId) { + const icons = { + safe: { + 19: "../res/icons/safe_19.png", + 38: "../res/icons/safe_38.png", + }, + unsafe: { + 19: "../res/icons/unsafe_19.png", + 38: "../res/icons/unsafe_38.png", + }, + potentially_unsafe: { + 19: "../res/icons/potentially_unsafe_19.png", + 38: "../res/icons/potentially_unsafe_38.png", + }, + starred: { + 19: "../res/icons/starred_19.png", + 38: "../res/icons/starred_38.png", + }, + fmhy: { + 19: "../res/icons/fmhy_19.png", + 38: "../res/icons/fmhy_38.png", + }, + extension_page: { + 19: "../res/ext_icon_144.png", + 38: "../res/ext_icon_144.png", + }, + default: { + 19: "../res/icons/default_19.png", + 38: "../res/icons/default_38.png", + }, + }; + + const icon = icons[status] || icons["default"]; + + browserAPI.action.setIcon({ + tabId: tabId, + path: icon, + }); +} + +async function notifySettingsPage() { + const tabs = await browserAPI.tabs.query({}); + for (const tab of tabs) { + try { + await browserAPI.tabs.sendMessage(tab.id, { type: "filterlistUpdated" }); + } catch (e) { + // Ignore errors for tabs that can't receive messages + } + } +} + +// Site Status Checking +function checkSiteAndUpdatePageAction(tabId, url) { + console.log( + `checkSiteAndUpdatePageAction: Checking status for ${url} on tab ${tabId}` + ); + + if (!url) { + updatePageAction("default", tabId); + return; + } + + const normalizedUrl = normalizeUrl(url.trim()); + const rootUrl = extractRootUrl(normalizedUrl); + + // Detect if the URL is an internal extension page (settings page or warning page) + const extUrlBase = browserAPI.runtime.getURL(""); + if (url.startsWith(extUrlBase)) { + console.log("Detected extension page: " + url); + updatePageAction("extension_page", tabId); + return; + } + + // Create variations of the URL to check + // Some URLs might be stored with or without trailing slashes or www + let status = "no_data"; + let matchedUrl = normalizedUrl; + + // First check the full URL + status = getStatusFromLists(normalizedUrl); + + // If not found, try with trailing slash + if (status === "no_data" && !normalizedUrl.endsWith("/")) { + status = getStatusFromLists(normalizedUrl + "/"); + if (status !== "no_data") matchedUrl = normalizedUrl + "/"; + } + + // If not found, try without trailing slash + if (status === "no_data" && normalizedUrl.endsWith("/")) { + status = getStatusFromLists(normalizedUrl.slice(0, -1)); + if (status !== "no_data") matchedUrl = normalizedUrl.slice(0, -1); + } + + // If still no match, check the root URL + if (status === "no_data") { + status = getStatusFromLists(rootUrl); + if (status !== "no_data") matchedUrl = rootUrl; + + // Try root URL with trailing slash + if (status === "no_data" && !rootUrl.endsWith("/")) { + status = getStatusFromLists(rootUrl + "/"); + if (status !== "no_data") matchedUrl = rootUrl + "/"; + } + } + + // Apply the correct icon status to the tab + updatePageAction(status, tabId); + + // Handle unsafe sites that need warning page redirection if not approved + if (status === "unsafe" && !approvedUrls.get(tabId)?.includes(rootUrl)) { + openWarningPage(tabId, rootUrl); + } +} + +// Update Schedule Management +async function shouldUpdate() { + try { + const { lastUpdated } = await browserAPI.storage.local.get("lastUpdated"); + const { updateFrequency = "daily" } = await browserAPI.storage.sync.get({ + updateFrequency: "daily", + }); + + if (!lastUpdated) return true; + + const lastUpdate = new Date(lastUpdated); + const now = new Date(); + const diffHours = (now - lastUpdate) / (1000 * 60 * 60); + + if (updateFrequency === "daily") { + return diffHours >= 24; + } else if (updateFrequency === "weekly") { + return diffHours >= 168; + } else if (updateFrequency === "monthly") { + return diffHours >= 720; + } + return false; + } catch (error) { + console.error("Error checking update schedule:", error); + return false; + } +} + +async function setupUpdateSchedule() { + await browserAPI.alarms.clearAll(); + + // Get the user's preferred update frequency from storage + const { updateFrequency } = await browserAPI.storage.sync.get({ + updateFrequency: "daily", + }); + + // Determine period in minutes based on selected frequency + let periodInMinutes; + switch (updateFrequency) { + case "weekly": + periodInMinutes = 10080; // 7 days in minutes + break; + case "monthly": + periodInMinutes = 43200; // 30 days in minutes + break; + default: + periodInMinutes = 1440; // 24 hours in minutes for daily updates + } + + // Create the alarm based on calculated period + browserAPI.alarms.create("checkUpdate", { + periodInMinutes: periodInMinutes, + }); +} + +// Event Listeners +browserAPI.runtime.onMessage.addListener((message, sender, sendResponse) => { + if (message.action === "checkSiteStatus") { + const { url, rootUrl } = message; + + // Attempt to match with the full URL first (for specific paths) + let status = getStatusFromLists(url); + let matchedUrl = url; + + // If no specific match, try the root URL + if (status === "no_data") { + status = getStatusFromLists(rootUrl); + matchedUrl = rootUrl; + } + + sendResponse({ status, matchedUrl }); + return true; + } +}); + +function getStatusFromLists(url) { + // Skip null, empty or non-string URLs + if (!url || typeof url !== "string") { + console.warn(`getStatusFromLists: Invalid URL provided: ${url}`); + return "no_data"; + } + + // Create URL variations to check consistently across all lists + const originalUrl = url; + const urlWithSlash = url.endsWith("/") ? url : url + "/"; + const urlWithoutSlash = url.endsWith("/") ? url.slice(0, -1) : url; + const urlVariations = [originalUrl, urlWithSlash, urlWithoutSlash]; + + // Special handling for repository hosting sites + const urlObj = new URL(url); + const isRepoSite = ["github.com", "gitlab.com", "sourceforge.net"].some( + (domain) => + urlObj.hostname === domain || urlObj.hostname.endsWith("." + domain) + ); + + // For repository sites, we need to check the full path, not just the domain + if (isRepoSite) { + // Check unsafe and potentially unsafe lists first using regex + if (unsafeSitesRegex?.test(url)) return "unsafe"; + if (potentiallyUnsafeSitesRegex?.test(url)) return "potentially_unsafe"; + if (fmhySitesRegex?.test(url)) return "fmhy"; + + // For repo sites, check for exact matches in starred and safe lists + // Skip domain-only matching which we do later in the function + for (const variant of urlVariations) { + if (starredSites.includes(variant)) return "starred"; + } + + for (const variant of urlVariations) { + if (safeSites.includes(variant)) return "safe"; + } + + // If no match found for the specific repository, return no_data + // We skip the domain-level matching for repository hosting sites + return "no_data"; + } + + // For non-repository sites, continue with standard checks + if (unsafeSitesRegex?.test(url)) return "unsafe"; + if (potentiallyUnsafeSitesRegex?.test(url)) return "potentially_unsafe"; + if (fmhySitesRegex?.test(url)) return "fmhy"; + + // Check for starred status with all URL variations - highest priority after unsafe/fmhy + for (const variant of urlVariations) { + if (starredSites.includes(variant)) { + return "starred"; + } + } + + // Check for safe status with all URL variations - lower priority than starred + for (const variant of urlVariations) { + if (safeSites.includes(variant)) { + return "safe"; + } + } + + // Try matching the domain part only for safe sites + try { + const domain = urlObj.hostname; + + // First check if domain matches any starred site (priority) + for (const starredUrl of starredSites) { + try { + const starredUrlObj = new URL(starredUrl); + if (starredUrlObj.hostname === domain) { + return "starred"; + } + } catch (e) { + // Skip invalid URLs in starredSites + continue; + } + } + + // Then check for safe site domain matches + for (const safeUrl of safeSites) { + try { + const safeUrlObj = new URL(safeUrl); + if (safeUrlObj.hostname === domain) { + return "safe"; + } + } catch (e) { + // Skip invalid URLs in safeSites + continue; + } + } + } catch (e) { + // If URL parsing fails, skip domain matching + } + + return "no_data"; +} + +async function openWarningPage(tabId, unsafeUrl) { + const normalizedUrl = normalizeUrl(unsafeUrl); + const tabApprovedUrls = approvedUrls.get(tabId) || []; + + // Check if URL has already been approved for this tab to avoid loop + if (tabApprovedUrls.includes(normalizedUrl)) { + console.log(`URL ${unsafeUrl} was already approved for tab ${tabId}`); + return; + } + + // Fetch the warning page setting + const { warningPage } = await browserAPI.storage.sync.get({ + warningPage: true, + }); + + if (!warningPage) { + console.log("Warning page is disabled by the user settings."); + return; + } + + // Add temporary approval to avoid repeated redirection + tabApprovedUrls.push(normalizedUrl); + approvedUrls.set(tabId, tabApprovedUrls); + + // Redirect to the warning page if it is enabled in settings + const warningPageUrl = browserAPI.runtime.getURL( + `../pub/warning-page.html?url=${encodeURIComponent(unsafeUrl)}` + ); + browserAPI.tabs.update(tabId, { url: warningPageUrl }); +} + +// Add listener for approval from the warning page +browserAPI.runtime.onMessage.addListener((message, sender, sendResponse) => { + if (message.action === "approveSite") { + const { tabId, url } = message; + const rootUrl = extractRootUrl(url); + + // Fetch existing approved URLs from storage + browserAPI.storage.local.get("approvedUrls", (result) => { + const approvedUrls = result.approvedUrls || []; + + // Add the root URL if not already approved + if (!approvedUrls.includes(rootUrl)) { + approvedUrls.push(rootUrl); + browserAPI.storage.local.set({ approvedUrls }); + console.log(`approveSite: ${rootUrl} approved globally.`); + } + + // Set the toolbar icon to "unsafe" immediately + updatePageAction("unsafe", tabId); + sendResponse({ status: "approved" }); + }); + } + return true; +}); + +// Listen for settings updates from the settings page +browserAPI.runtime.onMessage.addListener((message, sender, sendResponse) => { + if (message.type === "settingsUpdated") { + setupUpdateSchedule(); // Adjust update schedule based on new settings + sendResponse({ status: "Settings updated successfully" }); + return true; // Indicates asynchronous response handling + } +}); + +browserAPI.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { + if (changeInfo.status === "complete" && tab.url) { + checkSiteAndUpdatePageAction(tabId, tab.url); + } +}); + +browserAPI.tabs.onActivated.addListener(async (activeInfo) => { + const tab = await browserAPI.tabs.get(activeInfo.tabId); + if (tab.url) { + checkSiteAndUpdatePageAction(tab.id, tab.url); + } +}); + +browserAPI.alarms.onAlarm.addListener(async (alarm) => { + if (alarm.name === "checkUpdate") { + const needsUpdate = await shouldUpdate(); + if (needsUpdate) { + await fetchFilterLists(); + } + } +}); + +browserAPI.tabs.onRemoved.addListener((tabId) => { + approvedUrls.delete(tabId); + browserAPI.storage.local.remove(`proceedTab_${tabId}`); +}); + +// Initialize settings with defaults if needed +async function initializeSettings() { + const defaultSettings = { + theme: "system", + showWarning: true, + updateFrequency: "daily", + highlightTrusted: true, + highlightUntrusted: true, + showWarningBanners: true, + trustedColor: "#32cd32", + untrustedColor: "#ff4444", + userTrustedDomains: [], + userUntrustedDomains: [], + }; + + // Check for existing settings + const existingSettings = await browserAPI.storage.local.get( + Object.keys(defaultSettings) + ); + + // Merge with defaults for any missing settings + const mergedSettings = { ...defaultSettings, ...existingSettings }; + + // Save the merged settings + await browserAPI.storage.local.set(mergedSettings); + + console.log("Settings initialized:", mergedSettings); +} + +// Add well-known safe sites manually as a fallback +function addKnownSafeSites() { + // Known safe sites from FMHY that should be recognized + const knownSafeSites = [ + // Common gaming sites + "https://fitgirl-repacks.site", + "https://pcgamestorrents.com", + "https://steamunlocked.net", + "https://gog-games.com", + + // Common tools/software sites + // GitHub URLs should be evaluated per repository, not by domain + "https://gitlab.com", + "https://sourceforge.net", + + // Media streaming/download sites + "https://archive.org", + "https://nyaa.si", + "https://rutracker.org", + "https://1337x.to", + + // Known safe GitHub repositories + "https://github.com/hydralauncher/hydra", + + // Add more known safe sites here as needed + ]; + + // Process and add to safeSites + const normalizedSites = knownSafeSites + .map((site) => normalizeUrl(site)) + .filter((site) => site); + + // Add to safeSites if not already present + for (const site of normalizedSites) { + if (!safeSites.includes(site)) { + console.log(`Adding known safe site: ${site}`); + safeSites.push(site); + } + } + + console.log(`Added ${normalizedSites.length} known safe sites as fallback`); +} + +// Extension initialization +async function initializeExtension() { + console.log("Initializing extension..."); + + try { + await initializeSettings(); + + // Check if we need to update + if (await shouldUpdate()) { + await fetchFilterLists(); + await fetchSafeSites(); + await fetchStarredSites(); + } else { + // Load data from storage + try { + const storedData = await browserAPI.storage.local.get([ + "unsafeSites", + "potentiallyUnsafeSites", + "fmhySites", + "starredSites", + "safeSiteList", + ]); + + if (storedData.unsafeSites && storedData.unsafeSites.length > 0) { + unsafeSitesRegex = generateRegexFromList(storedData.unsafeSites); + } + + if ( + storedData.potentiallyUnsafeSites && + storedData.potentiallyUnsafeSites.length > 0 + ) { + potentiallyUnsafeSitesRegex = generateRegexFromList( + storedData.potentiallyUnsafeSites + ); + } + + if (storedData.fmhySites && storedData.fmhySites.length > 0) { + fmhySitesRegex = generateRegexFromList(storedData.fmhySites); + } + + // Load starred sites from storage + if (storedData.starredSites && storedData.starredSites.length > 0) { + starredSites = storedData.starredSites; + console.log( + `Loaded ${starredSites.length} starred sites from storage` + ); + } else { + // If no starred sites in storage, fetch them now + await fetchStarredSites(); + } + + // Load safe sites from storage + if (storedData.safeSiteList && storedData.safeSiteList.length > 0) { + safeSites = storedData.safeSiteList; + console.log(`Loaded ${safeSites.length} safe sites from storage`); + } else { + // If no safe sites in storage, fetch them now + await fetchSafeSites(); + } + } catch (error) { + console.error("Error loading from storage:", error); + } + } + + // Add fallback known sites - only for safe sites, not for starred + addKnownSafeSites(); + + // Set up the update schedule + await setupUpdateSchedule(); + + console.log("Extension initialized successfully."); + } catch (error) { + console.error("Error during initialization:", error); + } +} + +// Extension message handling +browserAPI.runtime.onMessage.addListener( + async (message, sender, sendResponse) => { + if (message.action === "updateAlarm") { + await setupUpdateSchedule(); + return true; + } + + if (message.action === "refreshAllTabs") { + // Get all tabs + const tabs = await browserAPI.tabs.query({}); + + // Send refresh message to all tabs + for (const tab of tabs) { + try { + await browserAPI.tabs.sendMessage(tab.id, { + action: "refreshSettings", + }); + } catch (error) { + // Content script might not be loaded in some tabs, ignore errors + console.log(`Could not refresh tab ${tab.id}: ${error.message}`); + } + } + + return true; + } + + return false; + } +); + +initializeExtension(); diff --git a/src/js/content.js b/src/js/content.js new file mode 100644 index 0000000..6deb0de --- /dev/null +++ b/src/js/content.js @@ -0,0 +1,388 @@ +// FMHY SafeLink Guard - Content Script +// Implements visual marking of safe/unsafe links similar to the userscript + +"use strict"; + +// Cross-browser compatibility shim +const browserAPI = typeof browser !== "undefined" ? browser : chrome; + +// Track processed elements to avoid reprocessing +const processedLinks = new WeakSet(); +const processedDomains = new Set(); +const highlightCountTrusted = new Map(); +const highlightCountUntrusted = new Map(); + +// Default settings +let settings = { + highlightTrusted: true, + highlightUntrusted: true, + showWarningBanners: true, + trustedColor: "#32cd32", + untrustedColor: "#ff4444", +}; + +// Domain lists +let unsafeDomains = new Set(); +let safeDomains = new Set(); +let userTrusted = new Set(); +let userUntrusted = new Set(); + +// Search engines where highlighting should be applied +const searchEngines = [ + "google.com", + "google.", // Covers all Google country domains like google.co.uk, google.fr, etc. + "bing.com", + "duckduckgo.com", + "librey.org", + "4get.ca", + "mojeek.com", + "qwant.com", + "swisscows.com", + "yacy.net", + "startpage.com", + "search.brave.com", + "ekoru.org", + "gibiru.com", + "searx.org", + "searx.", // Covers all SearX instances + "searxng.", // Covers all SearXNG instances + "whoogle.", // Covers all Whoogle instances + "metager.org", + "ecosia.org", + "yandex.com", + "yandex.", // Covers all Yandex country domains + "yahoo.com", + "yahoo.", // Covers all Yahoo country domains + "baidu.com", + "naver.com", + "seznam.cz", +]; + +// FMHY domains to exclude +const fmhyDomains = [ + "fmhy.net", + "fmhy.pages.dev", + "fmhy.lol", + "fmhy.vercel.app", + "fmhy.xyz", +]; + +// CSS for warning banners +const warningStyle = ` + background-color: #ff0000; + color: #fff; + padding: 2px 6px; + font-weight: bold; + border-radius: 4px; + font-size: 12px; + margin-left: 6px; + z-index: 9999; +`; + +// Main initialization +function init() { + loadSettings() + .then(() => loadDomainLists()) + .then(() => { + processPage(); + setupObserver(); + }) + .catch((err) => + console.error("[FMHY SafeGuard] Error initializing content script:", err) + ); +} + +// Check if current site is a search engine where we should apply highlighting +function isSupportedSite(domain) { + // Don't highlight on FMHY sites + if (fmhyDomains.some((fmhyDomain) => domain.endsWith(fmhyDomain))) { + console.log( + `[FMHY SafeGuard] Skipping highlighting on FMHY domain: ${domain}` + ); + return false; + } + + // Only highlight on search engines + return searchEngines.some((searchDomain) => domain.includes(searchDomain)); +} + +// Load user settings from storage +async function loadSettings() { + try { + const data = await browserAPI.storage.local.get([ + "highlightTrusted", + "highlightUntrusted", + "showWarningBanners", + "trustedColor", + "untrustedColor", + "userTrustedDomains", + "userUntrustedDomains", + ]); + + // Apply stored settings or use defaults + settings.highlightTrusted = + data.highlightTrusted !== undefined + ? data.highlightTrusted + : settings.highlightTrusted; + settings.highlightUntrusted = + data.highlightUntrusted !== undefined + ? data.highlightUntrusted + : settings.highlightUntrusted; + settings.showWarningBanners = + data.showWarningBanners !== undefined + ? data.showWarningBanners + : settings.showWarningBanners; + + if (data.trustedColor) settings.trustedColor = data.trustedColor; + if (data.untrustedColor) settings.untrustedColor = data.untrustedColor; + + // Load user trusted/untrusted domains + if (data.userTrustedDomains) { + userTrusted = new Set(data.userTrustedDomains); + } + + if (data.userUntrustedDomains) { + userUntrusted = new Set(data.userUntrustedDomains); + } + + console.log("[FMHY SafeGuard] Settings loaded:", settings); + } catch (error) { + console.error("[FMHY SafeGuard] Error loading settings:", error); + } +} + +// Load domain lists from extension storage +async function loadDomainLists() { + try { + const { unsafeSites, safeSiteList } = await browserAPI.storage.local.get([ + "unsafeSites", + "safeSiteList", + ]); + + if (unsafeSites && Array.isArray(unsafeSites)) { + unsafeDomains = new Set( + unsafeSites.map((site) => normalizeDomain(new URL(site).hostname)) + ); + } + + if (safeSiteList && Array.isArray(safeSiteList)) { + safeDomains = new Set( + safeSiteList.map((site) => normalizeDomain(new URL(site).hostname)) + ); + } + + // Apply user overrides + applyUserOverrides(); + + console.log( + `[FMHY SafeGuard] Loaded ${unsafeDomains.size} unsafe domains and ${safeDomains.size} safe domains` + ); + } catch (error) { + console.error("[FMHY SafeGuard] Error loading domain lists:", error); + } +} + +// Apply user trusted/untrusted overrides +function applyUserOverrides() { + userTrusted.forEach((domain) => { + safeDomains.add(domain); + unsafeDomains.delete(domain); + }); + + userUntrusted.forEach((domain) => { + unsafeDomains.add(domain); + safeDomains.delete(domain); + }); +} + +// Process all links in the page +function processPage() { + const currentDomain = normalizeDomain(window.location.hostname); + + // Only process links on search engines and not on FMHY sites + if (!isSupportedSite(currentDomain)) { + console.log( + `[FMHY SafeGuard] Skipping highlighting on non-search engine: ${currentDomain}` + ); + return; + } + + console.log( + `[FMHY SafeGuard] Processing links on search engine: ${currentDomain}` + ); + document + .querySelectorAll("a[href]") + .forEach((link) => processLink(link, currentDomain)); +} + +// Set up mutation observer to handle dynamically added content +function setupObserver() { + const currentDomain = normalizeDomain(window.location.hostname); + + // Skip setting up observer if not on a supported site + if (!isSupportedSite(currentDomain)) { + return; + } + + const observer = new MutationObserver((mutations) => { + for (const { addedNodes } of mutations) { + for (const node of addedNodes) { + if (node.nodeType === Node.ELEMENT_NODE) { + // If it's a link itself + if (node.tagName === "A" && node.href) { + processLink(node, currentDomain); + } + + // Process any links inside the added node + node + .querySelectorAll("a[href]") + .forEach((link) => processLink(link, currentDomain)); + } + } + } + }); + + observer.observe(document.body, { childList: true, subtree: true }); +} + +// Process a single link +function processLink(link, currentDomain) { + // Skip if already processed + if (processedLinks.has(link)) return; + processedLinks.add(link); + + try { + // Skip links without proper URLs + if ( + !link.href || + link.href.startsWith("javascript:") || + link.href.startsWith("#") + ) { + return; + } + + const linkDomain = normalizeDomain(new URL(link.href).hostname); + + // Skip if the current site is safe AND the link is internal + if ( + (safeDomains.has(currentDomain) || userTrusted.has(currentDomain)) && + linkDomain === currentDomain + ) { + return; + } + + // Handle untrusted links + if ( + userUntrusted.has(linkDomain) || + (!userTrusted.has(linkDomain) && unsafeDomains.has(linkDomain)) + ) { + if ( + settings.highlightUntrusted && + getHighlightCount(highlightCountUntrusted, linkDomain) < 2 + ) { + highlightLink(link, "untrusted"); + incrementHighlightCount(highlightCountUntrusted, linkDomain); + } + + if (settings.showWarningBanners && !processedDomains.has(linkDomain)) { + addWarningBanner(link); + processedDomains.add(linkDomain); + } + } + // Handle trusted links + else if (userTrusted.has(linkDomain) || safeDomains.has(linkDomain)) { + if ( + settings.highlightTrusted && + getHighlightCount(highlightCountTrusted, linkDomain) < 2 + ) { + highlightLink(link, "trusted"); + incrementHighlightCount(highlightCountTrusted, linkDomain); + } + } + } catch (error) { + console.warn("[FMHY SafeGuard] Error processing link:", error); + } +} + +// Highlight a link based on its trustworthiness +function highlightLink(link, type) { + const color = + type === "trusted" ? settings.trustedColor : settings.untrustedColor; + link.style.textShadow = `0 0 4px ${color}`; + link.style.fontWeight = "bold"; +} + +// Add a warning banner after an unsafe link +function addWarningBanner(link) { + const warning = document.createElement("span"); + warning.textContent = "⚠️ FMHY Unsafe Site"; + warning.style = warningStyle; + link.after(warning); +} + +// Helper functions +function normalizeDomain(hostname) { + return hostname.replace(/^www\./, "").toLowerCase(); +} + +function getHighlightCount(map, domain) { + return map.get(domain) || 0; +} + +function incrementHighlightCount(map, domain) { + if (map.size > 1000) map.clear(); // Reset if too large + map.set(domain, getHighlightCount(map, domain) + 1); +} + +// Listen for settings changes +browserAPI.storage.onChanged.addListener((changes, area) => { + if (area === "local") { + let settingsChanged = false; + + for (let key in changes) { + if ( + key === "highlightTrusted" || + key === "highlightUntrusted" || + key === "showWarningBanners" || + key === "trustedColor" || + key === "untrustedColor" || + key === "userTrustedDomains" || + key === "userUntrustedDomains" + ) { + settingsChanged = true; + } + } + + if (settingsChanged) { + refreshPage(); + } + } +}); + +// Handle messages from background script +browserAPI.runtime.onMessage.addListener((message, sender, sendResponse) => { + if (message.action === "refreshSettings") { + refreshPage(); + sendResponse({ status: "success" }); + return true; + } + return false; +}); + +// Function to refresh page highlighting +function refreshPage() { + // Reload settings and reprocess the page + loadSettings() + .then(() => loadDomainLists()) + .then(() => { + // Clear processed state and reprocess + processedLinks.clear(); + processedDomains.clear(); + highlightCountTrusted.clear(); + highlightCountUntrusted.clear(); + processPage(); + }); +} + +// Start the script +init(); diff --git a/src/pub/index.js b/src/pub/index.js index 7555d2d..81398c0 100644 --- a/src/pub/index.js +++ b/src/pub/index.js @@ -1,152 +1,152 @@ -document.addEventListener("DOMContentLoaded", async () => { - console.log("Popup loaded, preparing to check site status..."); - - const statusIcon = document.getElementById("status-icon"); - const statusMessage = document.getElementById("status-message"); - const errorMessage = document.getElementById("error-message"); - - const browserAPI = typeof browser !== "undefined" ? browser : chrome; - - const warningPageUrl = browserAPI.runtime.getURL("pub/warning-page.html"); - const settingsPageUrl = browserAPI.runtime.getURL("pub/settings-page.html"); - const welcomePageUrl = browserAPI.runtime.getURL("pub/welcome-page.html"); - - async function applyTheme() { - try { - const { theme } = await browserAPI.storage.sync.get("theme"); - const prefersDark = window.matchMedia( - "(prefers-color-scheme: dark)" - ).matches; - document.body.setAttribute( - "data-theme", - theme || (prefersDark ? "dark" : "light") - ); - } catch (error) { - console.error("Error applying theme:", error); - } - } - await applyTheme(); - - try { - const [activeTab] = await browserAPI.tabs.query({ - active: true, - currentWindow: true, - }); - - if (!activeTab || !activeTab.url) { - throw new Error("No active tab found or URL is unavailable."); - } - - const currentUrl = activeTab.url; - const rootUrl = extractRootUrl(currentUrl); - - if ( - currentUrl.startsWith(warningPageUrl) || - currentUrl === settingsPageUrl || - currentUrl === welcomePageUrl - ) { - handleStatusUpdate("extension_page", currentUrl); - return; - } - - // Send both the full URL and root URL to the background for status checking - const response = await browserAPI.runtime.sendMessage({ - action: "checkSiteStatus", - url: currentUrl, // full path URL - rootUrl: rootUrl, // root domain URL - }); - - if (!response || !response.status) { - throw new Error( - "Failed to retrieve site status from the background script." - ); - } - - // Display the appropriate URL in the popup - const displayUrl = response.matchedUrl || rootUrl; - handleStatusUpdate(response.status, displayUrl); - } catch (error) { - console.error("Error while checking site status:", error); - errorMessage.textContent = `Error: ${error.message}`; - updateUI("error", "An error occurred while retrieving the site status."); - } - - function handleStatusUpdate(status, displayUrl) { - let message; - - switch (status) { - case "unsafe": - message = `${displayUrl} is flagged as unsafe. Be cautious when interacting with this site.`; - break; - case "potentially_unsafe": - message = `${displayUrl} is potentially unsafe. Proceed with caution.`; - break; - case "fmhy": - message = `${displayUrl} is an FMHY related site. Proceed confidently.`; - break; - case "safe": - message = `${displayUrl} is safe to browse.`; - break; - case "starred": - message = `${displayUrl} is a starred site.`; - break; - case "extension_page": - if (displayUrl.startsWith(warningPageUrl)) { - message = - "You are on the Warning Page. This page warns you about potentially unsafe sites."; - } else if (displayUrl === settingsPageUrl) { - message = - "This is the Settings Page of the extension. Customize your preferences here."; - } else if (displayUrl === welcomePageUrl) { - message = - "Welcome to FMHY SafeGuard! Explore the extension's features and get started."; - } else { - message = "This is an extension page."; - } - break; - case "no_data": - message = `No data available for ${displayUrl}.`; - break; - default: - message = `An unknown status was received for ${displayUrl}.`; - } - - updateUI(status, message); - } - - function updateUI(status, message) { - const icons = { - unsafe: "../res/icons/unsafe.png", - potentially_unsafe: "../res/icons/potentially_unsafe.png", - fmhy: "../res/icons/fmhy.png", - safe: "../res/icons/safe.png", - starred: "../res/icons/starred.png", - extension_page: "../res/ext_icon_144.png", - no_data: "../res/ext_icon_144.png", - error: "../res/icons/error.png", - unknown: "../res/ext_icon_144.png", - }; - - statusIcon.src = icons[status] || icons["unknown"]; - statusMessage.innerHTML = message || "An unknown error occurred."; - - statusIcon.classList.add("active"); - setTimeout(() => statusIcon.classList.remove("active"), 300); - - console.log(`UI updated: ${message}`); - } - - document.getElementById("settingsButton").addEventListener("click", () => { - browserAPI.runtime.openOptionsPage(); - }); - - function extractRootUrl(url) { - try { - const urlObj = new URL(url); - return `${urlObj.protocol}//${urlObj.hostname}`; - } catch (error) { - console.warn(`Failed to extract root URL from: ${url}`); - return url; - } - } -}); +document.addEventListener("DOMContentLoaded", async () => { + console.log("Popup loaded, preparing to check site status..."); + + const statusIcon = document.getElementById("status-icon"); + const statusMessage = document.getElementById("status-message"); + const errorMessage = document.getElementById("error-message"); + + const browserAPI = typeof browser !== "undefined" ? browser : chrome; + + const warningPageUrl = browserAPI.runtime.getURL("pub/warning-page.html"); + const settingsPageUrl = browserAPI.runtime.getURL("pub/settings-page.html"); + const welcomePageUrl = browserAPI.runtime.getURL("pub/welcome-page.html"); + + async function applyTheme() { + try { + const { theme } = await browserAPI.storage.sync.get("theme"); + const prefersDark = window.matchMedia( + "(prefers-color-scheme: dark)" + ).matches; + document.body.setAttribute( + "data-theme", + theme || (prefersDark ? "dark" : "light") + ); + } catch (error) { + console.error("Error applying theme:", error); + } + } + await applyTheme(); + + try { + const [activeTab] = await browserAPI.tabs.query({ + active: true, + currentWindow: true, + }); + + if (!activeTab || !activeTab.url) { + throw new Error("No active tab found or URL is unavailable."); + } + + const currentUrl = activeTab.url; + const rootUrl = extractRootUrl(currentUrl); + + if ( + currentUrl.startsWith(warningPageUrl) || + currentUrl === settingsPageUrl || + currentUrl === welcomePageUrl + ) { + handleStatusUpdate("extension_page", currentUrl); + return; + } + + // Send both the full URL and root URL to the background for status checking + const response = await browserAPI.runtime.sendMessage({ + action: "checkSiteStatus", + url: currentUrl, // full path URL + rootUrl: rootUrl, // root domain URL + }); + + if (!response || !response.status) { + throw new Error( + "Failed to retrieve site status from the background script." + ); + } + + // Display the appropriate URL in the popup + const displayUrl = response.matchedUrl || rootUrl; + handleStatusUpdate(response.status, displayUrl); + } catch (error) { + console.error("Error while checking site status:", error); + errorMessage.textContent = `Error: ${error.message}`; + updateUI("error", "An error occurred while retrieving the site status."); + } + + function handleStatusUpdate(status, displayUrl) { + let message; + + switch (status) { + case "unsafe": + message = `${displayUrl} is flagged as unsafe. Be cautious when interacting with this site.`; + break; + case "potentially_unsafe": + message = `${displayUrl} is potentially unsafe. Proceed with caution.`; + break; + case "fmhy": + message = `${displayUrl} is an FMHY related site. Proceed confidently.`; + break; + case "safe": + message = `${displayUrl} is safe to browse.`; + break; + case "starred": + message = `${displayUrl} is a starred site.`; + break; + case "extension_page": + if (displayUrl.startsWith(warningPageUrl)) { + message = + "You are on the Warning Page. This page warns you about potentially unsafe sites."; + } else if (displayUrl === settingsPageUrl) { + message = + "This is the Settings Page of the extension. Customize your preferences here."; + } else if (displayUrl === welcomePageUrl) { + message = + "Welcome to FMHY SafeGuard! Explore the extension's features and get started."; + } else { + message = "This is an extension page."; + } + break; + case "no_data": + message = `No data available for ${displayUrl}.`; + break; + default: + message = `An unknown status was received for ${displayUrl}.`; + } + + updateUI(status, message); + } + + function updateUI(status, message) { + const icons = { + unsafe: "../res/icons/unsafe.png", + potentially_unsafe: "../res/icons/potentially_unsafe.png", + fmhy: "../res/icons/fmhy.png", + safe: "../res/icons/safe.png", + starred: "../res/icons/starred.png", + extension_page: "../res/ext_icon_144.png", + no_data: "../res/ext_icon_144.png", + error: "../res/icons/error.png", + unknown: "../res/ext_icon_144.png", + }; + + statusIcon.src = icons[status] || icons["unknown"]; + statusMessage.innerHTML = message || "An unknown error occurred."; + + statusIcon.classList.add("active"); + setTimeout(() => statusIcon.classList.remove("active"), 300); + + console.log(`UI updated: ${message}`); + } + + document.getElementById("settingsButton").addEventListener("click", () => { + browserAPI.runtime.openOptionsPage(); + }); + + function extractRootUrl(url) { + try { + const urlObj = new URL(url); + return `${urlObj.protocol}//${urlObj.hostname}`; + } catch (error) { + console.warn(`Failed to extract root URL from: ${url}`); + return url; + } + } +}); diff --git a/src/pub/settings-page.html b/src/pub/settings-page.html index b58fcc0..bda5e1f 100644 --- a/src/pub/settings-page.html +++ b/src/pub/settings-page.html @@ -520,6 +520,9 @@
+ + + Next update scheduled for: Checking...
@@ -559,6 +562,68 @@ + +
+

Link Highlighting

+

+ Visually highlight safe and unsafe links in web pages +

+ +
+
+

+ Highlight safe links (green) +

+
+ +
+ +
+
+

+ Highlight unsafe links (red) +

+
+ +
+ +
+
+

+ Show warning banners next to unsafe links +

+
+ +
+ +
+
+

+ Safe link color +

+
+ +
+ +
+
+

+ Unsafe link color +

+
+ +
+
+
@@ -577,13 +642,48 @@
+ + +
+

Domain Management

+

+ Manually add domains to safe or unsafe lists +

+ +
+
+

+ Safe Domains (one per line) +

+ +
+ +
+

+ Unsafe Domains (one per line) +

+ +
+
+
- -
- + +
+
+ +
- -
- Settings saved successfully! -
- diff --git a/src/pub/settings-page.js b/src/pub/settings-page.js index 6ffcf83..5d3d904 100644 --- a/src/pub/settings-page.js +++ b/src/pub/settings-page.js @@ -1,273 +1,423 @@ -document.addEventListener("DOMContentLoaded", () => { - // Cross-browser compatibility shim - const browserAPI = typeof browser !== "undefined" ? browser : chrome; - // Load and display the extension version from manifest.json - const manifest = browserAPI.runtime.getManifest(); - document.getElementById("versionNumber").textContent = manifest.version; - - // Get all DOM elements - const themeSelect = document.getElementById("themeSelect"); - const warningToggle = document.getElementById("warningToggle"); - const updateFrequency = document.getElementById("updateFrequency"); - const saveButton = document.getElementById("saveSettings"); - const notification = document.getElementById("notification"); - const lastUpdated = document.getElementById("lastUpdated"); - const updateStatus = document.getElementById("updateStatus"); - - // Theme application function - function applyTheme(theme) { - if (theme === "system") { - const prefersDark = window.matchMedia( - "(prefers-color-scheme: dark)" - ).matches; - document.body.setAttribute("data-theme", prefersDark ? "dark" : "light"); - } else { - document.body.setAttribute("data-theme", theme); - } - } - - // Format date function - function formatDate(date) { - if (!date) return "Never"; - const d = new Date(date); - const now = new Date(); - const diffTime = Math.abs(now - d); - const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24)); - - if (diffDays === 0) { - return ( - "Today at " + - d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) - ); - } else if (diffDays === 1) { - return ( - "Yesterday at " + - d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) - ); - } else { - return ( - d.toLocaleDateString() + - " " + - d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) - ); - } - } - - // Calculate next update time - function calculateNextUpdate(lastUpdate, frequency) { - if (!lastUpdate) return "Not scheduled"; - const lastUpdateDate = new Date(lastUpdate); - let nextUpdate = new Date(lastUpdateDate); - - switch (frequency) { - case "daily": - nextUpdate.setDate(nextUpdate.getDate() + 1); - nextUpdate.setHours(0, 0, 0, 0); - break; - case "weekly": - nextUpdate.setDate(nextUpdate.getDate() + 7); - nextUpdate.setHours(0, 0, 0, 0); - break; - case "monthly": - nextUpdate.setMonth(nextUpdate.getMonth() + 1); - nextUpdate.setHours(0, 0, 0, 0); - break; - default: - return "Not scheduled"; - } - - const now = new Date(); - if (nextUpdate < now) { - return "Update pending..."; - } - - const timeUntil = nextUpdate - now; - const hoursUntil = Math.floor(timeUntil / (1000 * 60 * 60)); - const minutesUntil = Math.floor( - (timeUntil % (1000 * 60 * 60)) / (1000 * 60) - ); - - if (hoursUntil < 24) { - if (hoursUntil === 0) { - return `in ${minutesUntil} minutes`; - } else { - return `in ${hoursUntil}h ${minutesUntil}m`; - } - } else { - const days = Math.floor(hoursUntil / 24); - if (days === 1) { - return "tomorrow"; - } else { - return `in ${days} days`; - } - } - } - - // Update the UI with next update time - async function updateNextUpdateStatus() { - try { - const stats = await browserAPI.storage.local.get({ - lastUpdated: null, - }); - const settings = await browserAPI.storage.sync.get({ - updateFrequency: "daily", - }); - - const nextUpdateText = calculateNextUpdate( - stats.lastUpdated, - settings.updateFrequency - ); - - if (updateStatus) { - updateStatus.innerHTML = ` - - - - Next update ${nextUpdateText} - `; - } - } catch (error) { - console.error("Error updating next update status:", error); - if (updateStatus) { - updateStatus.textContent = "Unable to check next update time"; - } - } - } - - // Load filterlist stats - async function loadFilterlistStats() { - try { - const stats = await browserAPI.storage.local.get({ - unsafeFilterCount: 0, - potentiallyUnsafeFilterCount: 0, - safeSiteCount: 0, - lastUpdated: null, - }); - - console.log("Fetched stats:", stats); - - document.getElementById("unsafeFilterCount").textContent = - stats.unsafeFilterCount; - document.getElementById("potentiallyUnsafeFilterCount").textContent = - stats.potentiallyUnsafeFilterCount; - document.getElementById("safeSiteCount").textContent = - stats.safeSiteCount; - document.getElementById("lastUpdated").textContent = formatDate( - stats.lastUpdated - ); - - await updateNextUpdateStatus(); - } catch (error) { - console.error("Error loading filterlist stats:", error); - document.getElementById("unsafeFilterCount").textContent = "Error"; - document.getElementById("potentiallyUnsafeFilterCount").textContent = - "Error"; - document.getElementById("safeSiteCount").textContent = "Error"; - document.getElementById("lastUpdated").textContent = "Error"; - } - } - - // Load settings function - async function loadSettings() { - try { - const savedSettings = await browserAPI.storage.sync.get({ - theme: "system", - warningPage: true, - updateFrequency: "daily", - }); - - if (themeSelect) themeSelect.value = savedSettings.theme; - if (warningToggle) warningToggle.checked = savedSettings.warningPage; - if (updateFrequency) - updateFrequency.value = savedSettings.updateFrequency; - - applyTheme(savedSettings.theme); - await loadFilterlistStats(); - } catch (error) { - console.error("Error loading settings:", error); - showNotification("Error loading settings", true); - } - } - - // Show notification function - function showNotification(message, isError = false) { - if (notification) { - notification.textContent = message; - if (isError) { - notification.style.background = - "linear-gradient(120deg, #ff6b6b, #ff8787)"; - } else { - notification.style.background = - "linear-gradient(120deg, var(--accent-purple), var(--accent-blue))"; - } - notification.classList.add("show"); - setTimeout(() => { - notification.classList.remove("show"); - }, 3000); - } - } - - // Save settings function - async function saveSettings() { - try { - const settings = { - theme: themeSelect.value, - warningPage: warningToggle.checked, - updateFrequency: updateFrequency.value, - }; - - console.log("Saving settings:", settings); - - // Save settings to storage - await browserAPI.storage.sync.set(settings); - showNotification("Settings saved successfully!"); - applyTheme(settings.theme); - await updateNextUpdateStatus(); - - console.log( - "Settings saved to storage, sending message to background script..." - ); - - // Send updated settings to background script - await browserAPI.runtime.sendMessage({ - type: "settingsUpdated", - settings: settings, - }); - - console.log( - "Settings update message sent to background script successfully." - ); - } catch (error) { - console.error("Error occurred during saveSettings:", error); - showNotification("Error saving settings", true); - } - } - - // Event Listeners - if (saveButton) { - saveButton.addEventListener("click", saveSettings); - } - - if (themeSelect) { - themeSelect.addEventListener("change", (e) => { - applyTheme(e.target.value); - }); - } - - window - .matchMedia("(prefers-color-scheme: dark)") - .addEventListener("change", (e) => { - if (themeSelect && themeSelect.value === "system") { - applyTheme("system"); - } - }); - - browserAPI.runtime.onMessage.addListener((message) => { - if (message.type === "filterlistUpdated") { - loadFilterlistStats(); - } - }); - - setInterval(updateNextUpdateStatus, 60000); - loadSettings(); -}); +document.addEventListener("DOMContentLoaded", () => { + // Cross-browser compatibility shim + const browserAPI = typeof browser !== "undefined" ? browser : chrome; + // Load and display the extension version from manifest.json + const manifest = browserAPI.runtime.getManifest(); + document.getElementById("versionNumber").textContent = manifest.version; + + // Get all DOM elements + const themeSelect = document.getElementById("themeSelect"); + const warningToggle = document.getElementById("warningToggle"); + const updateFrequency = document.getElementById("updateFrequency"); + const saveButton = document.getElementById("saveSettings"); + const notification = document.getElementById("notification"); + const lastUpdated = document.getElementById("lastUpdated"); + const updateStatus = document.getElementById("updateStatus"); + const forceRefreshButton = document.getElementById("forceRefresh"); + + // Get link highlighting elements + const highlightTrustedToggle = document.getElementById( + "highlightTrustedToggle" + ); + const highlightUntrustedToggle = document.getElementById( + "highlightUntrustedToggle" + ); + const showWarningBannersToggle = document.getElementById( + "showWarningBannersToggle" + ); + const trustedColor = document.getElementById("trustedColor"); + const untrustedColor = document.getElementById("untrustedColor"); + + // Get domain management elements + const trustedDomains = document.getElementById("trustedDomains"); + const untrustedDomains = document.getElementById("untrustedDomains"); + + // Theme application function + function applyTheme(theme) { + if (theme === "system") { + const prefersDark = window.matchMedia( + "(prefers-color-scheme: dark)" + ).matches; + document.body.setAttribute("data-theme", prefersDark ? "dark" : "light"); + } else { + document.body.setAttribute("data-theme", theme); + } + } + + // Format date function + function formatDate(date) { + if (!date) return "Never"; + const d = new Date(date); + const now = new Date(); + const diffTime = Math.abs(now - d); + const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24)); + + if (diffDays === 0) { + return ( + "Today at " + + d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) + ); + } else if (diffDays === 1) { + return ( + "Yesterday at " + + d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) + ); + } else { + return ( + d.toLocaleDateString() + + " " + + d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) + ); + } + } + + // Calculate next update time + function calculateNextUpdate(lastUpdate, frequency) { + if (!lastUpdate) return "Not scheduled"; + const lastUpdateDate = new Date(lastUpdate); + let nextUpdate = new Date(lastUpdateDate); + + switch (frequency) { + case "daily": + nextUpdate.setDate(nextUpdate.getDate() + 1); + nextUpdate.setHours(0, 0, 0, 0); + break; + case "weekly": + nextUpdate.setDate(nextUpdate.getDate() + 7); + nextUpdate.setHours(0, 0, 0, 0); + break; + case "monthly": + nextUpdate.setMonth(nextUpdate.getMonth() + 1); + nextUpdate.setHours(0, 0, 0, 0); + break; + default: + return "Not scheduled"; + } + + const now = new Date(); + if (nextUpdate < now) { + return "Update pending..."; + } + + const timeUntil = nextUpdate - now; + const hoursUntil = Math.floor(timeUntil / (1000 * 60 * 60)); + const minutesUntil = Math.floor( + (timeUntil % (1000 * 60 * 60)) / (1000 * 60) + ); + + if (hoursUntil < 24) { + if (hoursUntil === 0) { + return `in ${minutesUntil} minutes`; + } else { + return `in ${hoursUntil}h ${minutesUntil}m`; + } + } else { + const days = Math.floor(hoursUntil / 24); + if (days === 1) { + return "tomorrow"; + } else { + return `in ${days} days`; + } + } + } + + // Update the UI with next update time + async function updateNextUpdateStatus() { + try { + // Get both lastUpdated and updateFrequency in a single storage call + const data = await browserAPI.storage.local.get({ + lastUpdated: null, + updateFrequency: "daily", + }); + + // Use the stored frequency setting directly + const nextUpdateText = calculateNextUpdate( + data.lastUpdated, + data.updateFrequency + ); + + if (updateStatus) { + // Clear current content + while (updateStatus.firstChild) { + updateStatus.removeChild(updateStatus.firstChild); + } + + // Add spinning icon + const svgIcon = document.createElementNS( + "http://www.w3.org/2000/svg", + "svg" + ); + svgIcon.setAttribute("class", "update-icon"); + svgIcon.setAttribute("viewBox", "0 0 24 24"); + svgIcon.setAttribute("width", "16"); + svgIcon.setAttribute("height", "16"); + + const path = document.createElementNS( + "http://www.w3.org/2000/svg", + "path" + ); + path.setAttribute("fill", "currentColor"); + path.setAttribute( + "d", + "M12 4V2C6.477 2 2 6.477 2 12C2 17.523 6.477 22 12 22C17.523 22 22 17.523 22 12H20C20 16.418 16.418 20 12 20C7.582 20 4 16.418 4 12C4 7.582 7.582 4 12 4Z" + ); + + svgIcon.appendChild(path); + updateStatus.appendChild(svgIcon); + + // Add text + updateStatus.appendChild( + document.createTextNode(`Next update ${nextUpdateText}`) + ); + } + } catch (error) { + console.error("Error updating next update status:", error); + if (updateStatus) { + updateStatus.textContent = "Unable to check next update time"; + } + } + } + + // Load filterlist stats + async function loadFilterlistStats() { + try { + const stats = await browserAPI.storage.local.get({ + unsafeFilterCount: 0, + potentiallyUnsafeFilterCount: 0, + safeSiteCount: 0, + lastUpdated: null, + }); + + console.log("Fetched stats:", stats); + + document.getElementById("unsafeFilterCount").textContent = + stats.unsafeFilterCount; + document.getElementById("potentiallyUnsafeFilterCount").textContent = + stats.potentiallyUnsafeFilterCount; + document.getElementById("safeSiteCount").textContent = + stats.safeSiteCount; + document.getElementById("lastUpdated").textContent = formatDate( + stats.lastUpdated + ); + + await updateNextUpdateStatus(); + } catch (error) { + console.error("Error loading filterlist stats:", error); + document.getElementById("unsafeFilterCount").textContent = "Error"; + document.getElementById("potentiallyUnsafeFilterCount").textContent = + "Error"; + document.getElementById("safeSiteCount").textContent = "Error"; + document.getElementById("lastUpdated").textContent = "Error"; + } + } + + // Load settings function + async function loadSettings() { + try { + const settings = await browserAPI.storage.local.get([ + "theme", + "showWarning", + "updateFrequency", + "lastUpdated", + "nextUpdate", + "highlightTrusted", + "highlightUntrusted", + "showWarningBanners", + "trustedColor", + "untrustedColor", + "userTrustedDomains", + "userUntrustedDomains", + ]); + + console.log("Loaded settings:", settings); + + themeSelect.value = settings.theme || "system"; + applyTheme(settings.theme || "system"); + + warningToggle.checked = settings.showWarning !== false; + + // Set updateFrequency with fallback to "daily" + updateFrequency.value = settings.updateFrequency || "daily"; + console.log("Set updateFrequency to:", updateFrequency.value); + + // Set link highlighting settings + highlightTrustedToggle.checked = settings.highlightTrusted !== false; + highlightUntrustedToggle.checked = settings.highlightUntrusted !== false; + showWarningBannersToggle.checked = settings.showWarningBanners !== false; + + if (settings.trustedColor) { + trustedColor.value = settings.trustedColor; + } + + if (settings.untrustedColor) { + untrustedColor.value = settings.untrustedColor; + } + + // Set domain lists + if ( + settings.userTrustedDomains && + Array.isArray(settings.userTrustedDomains) + ) { + trustedDomains.value = settings.userTrustedDomains.join("\n"); + } + + if ( + settings.userUntrustedDomains && + Array.isArray(settings.userUntrustedDomains) + ) { + untrustedDomains.value = settings.userUntrustedDomains.join("\n"); + } + + if (settings.lastUpdated) { + lastUpdated.textContent = formatDate(settings.lastUpdated); + } + + if (settings.nextUpdate) { + updateNextUpdateStatus(); + } + } catch (error) { + console.error("Error loading settings:", error); + showNotification("Error loading settings", true); + } + } + + // Show notification function + function showNotification(message, isError = false) { + if (notification) { + notification.textContent = message; + if (isError) { + notification.style.background = + "linear-gradient(120deg, #ff6b6b, #ff8787)"; + } else { + notification.style.background = + "linear-gradient(120deg, var(--accent-purple), var(--accent-blue))"; + } + notification.classList.add("show"); + setTimeout(() => { + notification.classList.remove("show"); + }, 3000); + } + } + + /** + * Parse domains from textarea + * @param {string} text - Textarea content + * @returns {string[]} - Array of normalized domains + */ + function parseDomainList(text) { + if (!text) return []; + + return text + .split("\n") + .map((line) => line.trim().toLowerCase()) + .filter((line) => line && !line.startsWith("#") && !line.startsWith("//")) + .map((domain) => { + // Remove protocols and paths + if (domain.includes("://")) { + try { + return new URL(domain).hostname.replace(/^www\./, ""); + } catch (e) { + return domain; + } + } + // Just remove www prefix if no protocol + return domain.replace(/^www\./, ""); + }); + } + + // Save settings function + async function saveSettings() { + try { + const newSettings = { + theme: themeSelect.value, + showWarning: warningToggle.checked, + updateFrequency: updateFrequency.value, + highlightTrusted: highlightTrustedToggle.checked, + highlightUntrusted: highlightUntrustedToggle.checked, + showWarningBanners: showWarningBannersToggle.checked, + trustedColor: trustedColor.value, + untrustedColor: untrustedColor.value, + userTrustedDomains: parseDomainList(trustedDomains.value), + userUntrustedDomains: parseDomainList(untrustedDomains.value), + }; + + // Save all settings explicitly to local storage + await browserAPI.storage.local.set(newSettings); + + // Calculate the next update time based on the new frequency + const nextUpdate = calculateNextUpdate( + new Date().toISOString(), + newSettings.updateFrequency + ); + + // Update lastUpdated to now and store the next update time + await browserAPI.storage.local.set({ + nextUpdate, + lastUpdated: new Date().toISOString(), + }); + + // Tell the background script to update its alarm + await browserAPI.runtime.sendMessage({ action: "updateAlarm" }); + + // Apply settings to all open tabs + await browserAPI.runtime.sendMessage({ action: "refreshAllTabs" }); + + // Show notification and update status + showNotification("Settings saved and applied to all tabs!"); + await updateNextUpdateStatus(); + } catch (error) { + console.error("Error saving settings:", error); + showNotification("Error saving settings. Please try again.", true); + } + } + + // For debugging - can be called from the browser console + window.checkCurrentSettings = async function () { + try { + const data = await browserAPI.storage.local.get(null); // Get all storage + console.log("All stored settings:", data); + return data; + } catch (error) { + console.error("Error retrieving settings:", error); + return null; + } + }; + + // Event listeners + document.addEventListener("DOMContentLoaded", async () => { + // First load settings to ensure we have the correct values + await loadSettings(); + // Then load filter stats + await loadFilterlistStats(); + // Explicitly update the update status to ensure it's not stuck + await updateNextUpdateStatus(); + + // Log current settings for debugging + console.log("Current update frequency:", updateFrequency.value); + await window.checkCurrentSettings(); + }); + + saveButton.addEventListener("click", saveSettings); + + if (themeSelect) { + themeSelect.addEventListener("change", (e) => { + applyTheme(e.target.value); + }); + } + + window + .matchMedia("(prefers-color-scheme: dark)") + .addEventListener("change", (e) => { + if (themeSelect && themeSelect.value === "system") { + applyTheme("system"); + } + }); + + browserAPI.runtime.onMessage.addListener((message) => { + if (message.type === "filterlistUpdated") { + loadFilterlistStats(); + } + }); + + setInterval(updateNextUpdateStatus, 60000); +});