From 43915ee69a5557dba11316fd7728d7cb9878a1c0 Mon Sep 17 00:00:00 2001 From: Kenneth Hendricks <50819541+kenhendricks00@users.noreply.github.com> Date: Wed, 19 Mar 2025 13:37:49 -0400 Subject: [PATCH] Add files via upload --- platform/chromium/manifest.json | 62 +++++- platform/firefox/manifest.json | 62 +++++- src/js/background.js | 323 +++++++++++++++++++++++--------- src/js/content.js | 9 +- src/pub/index.js | 171 ++++++++++++----- 5 files changed, 475 insertions(+), 152 deletions(-) diff --git a/platform/chromium/manifest.json b/platform/chromium/manifest.json index bb005ac..672b2e9 100644 --- a/platform/chromium/manifest.json +++ b/platform/chromium/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "FMHY SafeGuard", "author": "Kenneth Hendricks & contributors", - "version": "1.1.9", + "version": "1.2.0", "icons": { "128": "res/ext_icon_144.png" }, @@ -18,16 +18,68 @@ "tabs", "alarms" ], - "background": { - "service_worker": "js/background.js" - }, "content_scripts": [ { - "matches": [""], + "matches": [ + "*://*.google.com/*", + "*://*.google.co.uk/*", + "*://*.google.de/*", + "*://*.google.fr/*", + "*://*.google.it/*", + "*://*.google.es/*", + "*://*.google.nl/*", + "*://*.google.ca/*", + "*://*.google.com.au/*", + "*://*.google.com.br/*", + "*://*.google.co.jp/*", + "*://*.google.co.kr/*", + "*://*.google.co.in/*", + "*://*.google.ru/*", + "*://*.google.cn/*", + "*://*.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/*", + "*://*.metager.org/*", + "*://*.ecosia.org/*", + "*://*.yandex.com/*", + "*://*.yandex.ru/*", + "*://*.yandex.ua/*", + "*://*.yandex.kz/*", + "*://*.yandex.by/*", + "*://*.yandex.tr/*", + "*://*.yahoo.com/*", + "*://*.yahoo.co.jp/*", + "*://*.yahoo.co.uk/*", + "*://*.yahoo.de/*", + "*://*.yahoo.fr/*", + "*://*.yahoo.it/*", + "*://*.yahoo.es/*", + "*://*.yahoo.ca/*", + "*://*.yahoo.com.au/*", + "*://*.yahoo.com.br/*", + "*://*.yahoo.co.kr/*", + "*://*.yahoo.co.in/*", + "*://*.baidu.com/*", + "*://*.naver.com/*", + "*://*.seznam.cz/*" + ], "js": ["js/content.js"], "run_at": "document_idle" } ], + "background": { + "service_worker": "js/background.js" + }, "description": "An extension that detects starred, safe, unsafe or potentially unsafe sites using the FMHY Filterlist.", "minimum_chrome_version": "100" } diff --git a/platform/firefox/manifest.json b/platform/firefox/manifest.json index 8eb69b0..2e0c3ff 100644 --- a/platform/firefox/manifest.json +++ b/platform/firefox/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "FMHY SafeGuard", "author": "Kenneth Hendricks & contributors", - "version": "1.1.9", + "version": "1.2.0", "icons": { "128": "res/ext_icon_144.png" }, @@ -18,16 +18,68 @@ "tabs", "alarms" ], - "background": { - "scripts": ["js/background.js"] - }, "content_scripts": [ { - "matches": [""], + "matches": [ + "*://*.google.com/*", + "*://*.google.co.uk/*", + "*://*.google.de/*", + "*://*.google.fr/*", + "*://*.google.it/*", + "*://*.google.es/*", + "*://*.google.nl/*", + "*://*.google.ca/*", + "*://*.google.com.au/*", + "*://*.google.com.br/*", + "*://*.google.co.jp/*", + "*://*.google.co.kr/*", + "*://*.google.co.in/*", + "*://*.google.ru/*", + "*://*.google.cn/*", + "*://*.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/*", + "*://*.metager.org/*", + "*://*.ecosia.org/*", + "*://*.yandex.com/*", + "*://*.yandex.ru/*", + "*://*.yandex.ua/*", + "*://*.yandex.kz/*", + "*://*.yandex.by/*", + "*://*.yandex.tr/*", + "*://*.yahoo.com/*", + "*://*.yahoo.co.jp/*", + "*://*.yahoo.co.uk/*", + "*://*.yahoo.de/*", + "*://*.yahoo.fr/*", + "*://*.yahoo.it/*", + "*://*.yahoo.es/*", + "*://*.yahoo.ca/*", + "*://*.yahoo.com.au/*", + "*://*.yahoo.com.br/*", + "*://*.yahoo.co.kr/*", + "*://*.yahoo.co.in/*", + "*://*.baidu.com/*", + "*://*.naver.com/*", + "*://*.seznam.cz/*" + ], "js": ["js/content.js"], "run_at": "document_idle" } ], + "background": { + "scripts": ["js/background.js"] + }, "browser_specific_settings": { "gecko": { "id": "{5d554479-7cc4-487f-bd25-d8fc67a42602}" diff --git a/src/js/background.js b/src/js/background.js index e05e108..f923f4f 100644 --- a/src/js/background.js +++ b/src/js/background.js @@ -45,6 +45,37 @@ let safeSites = []; let starredSites = []; const approvedUrls = new Map(); // Map to store approved URLs per tab +// List of search engines to check against +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", +]; + // Helper Functions function extractUrlsFromMarkdown(markdown) { const urlRegex = /https?:\/\/[^\s)]+/g; @@ -141,6 +172,20 @@ function extractUrlsFromFilterList(text) { .filter((url) => url !== null); } +// Function to check if a URL is a search engine +function isSearchEngine(url) { + try { + const urlObj = new URL(url); + return searchEngines.some( + (domain) => + urlObj.hostname === domain || urlObj.hostname.endsWith("." + domain) + ); + } catch (error) { + console.error("Error checking search engine:", error); + return false; + } +} + // Fetch and Update Functions async function fetchFilterLists() { console.log("Fetching filter lists..."); @@ -437,6 +482,132 @@ browserAPI.runtime.onMessage.addListener((message, sender, sendResponse) => { sendResponse({ status, matchedUrl }); return true; } + + if (message.action === "getSiteStatus") { + try { + // Get the URL from the message + const url = message.url; + if (!url) { + sendResponse({ status: "no_data", matchedUrl: null }); + return true; + } + + console.log(`getSiteStatus: checking status for ${url}`); + + // Normalize the URL + const normalizedUrl = normalizeUrl(url); + if (!normalizedUrl) { + sendResponse({ status: "no_data", matchedUrl: null }); + return true; + } + + // Extract domain for domain-level checking + const urlObj = new URL(normalizedUrl); + const domain = urlObj.hostname; + + // First check if it's an extension page + if (url.startsWith(browserAPI.runtime.getURL(""))) { + sendResponse({ status: "extension_page", matchedUrl: url }); + return true; + } + + // Special handling for repository sites + const isRepoSite = ["github.com", "gitlab.com", "sourceforge.net"].some( + (domain) => + urlObj.hostname === domain || urlObj.hostname.endsWith("." + domain) + ); + + // Variables to track status and matched URL + let status = "no_data"; + let matchedUrl = null; + + // Check full URL first + if (unsafeSitesRegex?.test(normalizedUrl)) { + status = "unsafe"; + matchedUrl = normalizedUrl; + } else if (potentiallyUnsafeSitesRegex?.test(normalizedUrl)) { + status = "potentially_unsafe"; + matchedUrl = normalizedUrl; + } else if (fmhySitesRegex?.test(normalizedUrl)) { + status = "fmhy"; + matchedUrl = normalizedUrl; + } else if (starredSites.includes(normalizedUrl)) { + status = "starred"; + matchedUrl = normalizedUrl; + } else if (safeSites.includes(normalizedUrl)) { + status = "safe"; + matchedUrl = normalizedUrl; + } + + // If no match for full URL and it's a repository site, don't try domain matching + if (status === "no_data" && isRepoSite) { + console.log(`No match for repository URL: ${normalizedUrl}`); + sendResponse({ status: "no_data", matchedUrl: normalizedUrl }); + return true; + } + + // If no match for full URL and it's a regular site, try domain-level matching + if (status === "no_data" && !isRepoSite) { + console.log(`No match for full URL, trying domain: ${domain}`); + + // Check domain against regex patterns + if (unsafeSitesRegex?.test(domain)) { + status = "unsafe"; + matchedUrl = `https://${domain}`; + } else if (potentiallyUnsafeSitesRegex?.test(domain)) { + status = "potentially_unsafe"; + matchedUrl = `https://${domain}`; + } else if (fmhySitesRegex?.test(domain)) { + status = "fmhy"; + matchedUrl = `https://${domain}`; + } + + // Check domain against starred and safe lists + if (status === "no_data") { + for (const starredUrl of starredSites) { + try { + const starredUrlObj = new URL(starredUrl); + if (starredUrlObj.hostname === domain) { + status = "starred"; + matchedUrl = starredUrl; + break; + } + } catch (e) { + continue; + } + } + } + + if (status === "no_data") { + for (const safeUrl of safeSites) { + try { + const safeUrlObj = new URL(safeUrl); + if (safeUrlObj.hostname === domain) { + status = "safe"; + matchedUrl = safeUrl; + break; + } + } catch (e) { + continue; + } + } + } + } + + console.log( + `getSiteStatus result for ${url}: ${status}, matched: ${matchedUrl}` + ); + sendResponse({ status: status, matchedUrl: matchedUrl }); + } catch (error) { + console.error("Error in getSiteStatus handler:", error); + sendResponse({ + status: "no_data", + matchedUrl: null, + error: error.message, + }); + } + return true; // Keep the message channel open for async response + } }); function getStatusFromLists(url) { @@ -446,94 +617,62 @@ function getStatusFromLists(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]; + try { + // Special handling for repository hosting sites + const urlObj = new URL(url); + const domain = urlObj.hostname; + const isRepoSite = ["github.com", "gitlab.com", "sourceforge.net"].some( + (domain) => + urlObj.hostname === domain || urlObj.hostname.endsWith("." + domain) + ); - // 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, need exact path matching + if (isRepoSite) { + // Check unsafe and potentially unsafe first + 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"; // No domain-only matches for repos + } - // 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 + // For normal sites, direct checks first (full 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"; - // 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"; - } + // Then check domain-level + if (unsafeSitesRegex?.test(domain)) return "unsafe"; + if (potentiallyUnsafeSitesRegex?.test(domain)) return "potentially_unsafe"; + if (fmhySitesRegex?.test(domain)) return "fmhy"; - 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) + // Try domain-level checks for starred and safe for (const starredUrl of starredSites) { try { const starredUrlObj = new URL(starredUrl); - if (starredUrlObj.hostname === domain) { - return "starred"; - } + 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"; - } + 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"; + return "no_data"; + } catch (e) { + console.warn(`Error in getStatusFromLists: ${e.message}`); + return "no_data"; + } } async function openWarningPage(tabId, unsafeUrl) { @@ -567,31 +706,6 @@ async function openWarningPage(tabId, 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") { @@ -601,8 +715,10 @@ browserAPI.runtime.onMessage.addListener((message, sender, sendResponse) => { } }); -browserAPI.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { +// Listen for tab updates +browserAPI.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => { if (changeInfo.status === "complete" && tab.url) { + // Always check the site status checkSiteAndUpdatePageAction(tabId, tab.url); } }); @@ -807,4 +923,29 @@ browserAPI.runtime.onMessage.addListener( } ); +// 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; + } +}); + initializeExtension(); diff --git a/src/js/content.js b/src/js/content.js index 6deb0de..3ad9bce 100644 --- a/src/js/content.js +++ b/src/js/content.js @@ -359,14 +359,11 @@ browserAPI.storage.onChanged.addListener((changes, area) => { } }); -// Handle messages from background script +// Listen for messages from background script browserAPI.runtime.onMessage.addListener((message, sender, sendResponse) => { - if (message.action === "refreshSettings") { - refreshPage(); - sendResponse({ status: "success" }); - return true; + if (message.action === "processPage") { + processPage(); } - return false; }); // Function to refresh page highlighting diff --git a/src/pub/index.js b/src/pub/index.js index 81398c0..d92e3eb 100644 --- a/src/pub/index.js +++ b/src/pub/index.js @@ -11,6 +11,17 @@ document.addEventListener("DOMContentLoaded", async () => { const settingsPageUrl = browserAPI.runtime.getURL("pub/settings-page.html"); const welcomePageUrl = browserAPI.runtime.getURL("pub/welcome-page.html"); + // Apply theme + await applyTheme(); + + // Check site status immediately + await checkSiteStatus(); + + // Add settings button listener + document.getElementById("settingsButton").addEventListener("click", () => { + browserAPI.runtime.openOptionsPage(); + }); + async function applyTheme() { try { const { theme } = await browserAPI.storage.sync.get("theme"); @@ -25,50 +36,124 @@ document.addEventListener("DOMContentLoaded", async () => { console.error("Error applying theme:", error); } } - await applyTheme(); - try { - const [activeTab] = await browserAPI.tabs.query({ - active: true, - currentWindow: true, - }); + async function checkSiteStatus() { + console.log("Checking site status from popup"); + 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."); + if (!activeTab || !activeTab.url) { + throw new Error("No active tab found or URL is unavailable."); + } + + const currentUrl = activeTab.url; + const rootUrl = extractRootUrl(currentUrl); + console.log("Current URL:", currentUrl); + console.log("Root URL:", rootUrl); + + // Handle extension pages + if ( + currentUrl.startsWith(warningPageUrl) || + currentUrl === settingsPageUrl || + currentUrl === welcomePageUrl || + currentUrl.startsWith(browserAPI.runtime.getURL("")) + ) { + handleStatusUpdate("extension_page", currentUrl); + return; + } + + // Get the status from the background script + const response = await browserAPI.runtime.sendMessage({ + action: "getSiteStatus", + url: currentUrl, + }); + + console.log("Status response:", response); + if (!response || !response.status) { + throw new Error("Failed to get site status"); + } + + // Determine what URL to display + let displayUrl; + + // If we have a matched URL from the background, format it appropriately + if (response.matchedUrl) { + try { + const matchedUrlObj = new URL(response.matchedUrl); + const currentUrlObj = new URL(currentUrl); + const isRepoSite = [ + "github.com", + "gitlab.com", + "sourceforge.net", + ].some( + (domain) => + matchedUrlObj.hostname === domain || + matchedUrlObj.hostname.endsWith("." + domain) + ); + + if (isRepoSite) { + // For repo sites, extract the domain and path parts that were matched + const pathParts = matchedUrlObj.pathname + .split("/") + .filter((p) => p); + if (pathParts.length >= 2) { + displayUrl = `${matchedUrlObj.hostname}/${pathParts[0]}/${pathParts[1]}`; + } else { + // If the matched URL doesn't have enough path parts but current URL does, + // use the current URL's path for better display (but only for no_data status) + if (response.status === "no_data") { + const currentPathParts = currentUrlObj.pathname + .split("/") + .filter((p) => p); + if (currentPathParts.length >= 2) { + displayUrl = `${currentUrlObj.hostname}/${currentPathParts[0]}/${currentPathParts[1]}`; + } else { + displayUrl = matchedUrlObj.hostname + matchedUrlObj.pathname; + } + } else { + displayUrl = matchedUrlObj.hostname + matchedUrlObj.pathname; + } + } + } else { + // For regular sites, just show the hostname from the matched URL + displayUrl = matchedUrlObj.hostname; + } + } catch (e) { + console.error("Error formatting matched URL:", e); + displayUrl = response.matchedUrl; + } + } else { + // Fallback to current URL if no match + const urlObj = new URL(currentUrl); + const isRepoSite = ["github.com", "gitlab.com", "sourceforge.net"].some( + (domain) => + urlObj.hostname === domain || urlObj.hostname.endsWith("." + domain) + ); + + if (isRepoSite) { + // For repo sites, extract the domain and first two path segments (user/repo) + const pathParts = urlObj.pathname.split("/").filter((p) => p); + if (pathParts.length >= 2) { + displayUrl = `${urlObj.hostname}/${pathParts[0]}/${pathParts[1]}`; + } else { + displayUrl = urlObj.hostname + urlObj.pathname; + } + } else { + // For regular sites, just show the hostname + displayUrl = urlObj.hostname; + } + } + + // Update the popup with the result + handleStatusUpdate(response.status, displayUrl); + } catch (error) { + console.error("Error checking site status:", error); + errorMessage.textContent = `Error: ${error.message}`; + updateUI("error", "An error occurred while checking the site status"); } - - 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) { @@ -136,10 +221,6 @@ document.addEventListener("DOMContentLoaded", async () => { console.log(`UI updated: ${message}`); } - document.getElementById("settingsButton").addEventListener("click", () => { - browserAPI.runtime.openOptionsPage(); - }); - function extractRootUrl(url) { try { const urlObj = new URL(url);