Add files via upload

This commit is contained in:
Kenneth Hendricks 2025-03-19 13:37:49 -04:00 committed by GitHub
parent 8171528bfc
commit 43915ee69a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 475 additions and 152 deletions

View file

@ -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": ["<all_urls>"],
"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"
}

View file

@ -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": ["<all_urls>"],
"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}"

View file

@ -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();

View file

@ -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

View file

@ -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);