From 877a423c3bcf3bc4199038eb5d195fa7be3e490e Mon Sep 17 00:00:00 2001 From: Lars Date: Thu, 17 Nov 2022 14:54:48 +0100 Subject: [PATCH] Restructured code, added blacklist/whitelist functionality, improved performance, fixed some bugs --- inject.js | 222 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 171 insertions(+), 51 deletions(-) diff --git a/inject.js b/inject.js index a97c8e7..aab5296 100644 --- a/inject.js +++ b/inject.js @@ -1,50 +1,103 @@ // prep +var buttonPaths = ["div.account__header button.logo-button","div.public-account-header a.logo-button"]; +var domainRegex = /^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/; +var handleRegex = /^(?:https?:\/\/(www\.)?.*\..*?\/)(?@\w+(?:@\w+\.\w+)?)(?:\/?.*|\z)$/; +var target = "_blank"; +var enableConsoleLog = true; +var logPrepend = "[FediFollow]"; +var maxElementWaitFactor = 200; // x 100ms for total time + +var lastUrl = window.location.href; var instance; var showAlert; -var lastUrl = window.location.href; -var buttonPaths = ["div.account__header button.logo-button","div.public-account-header a.logo-button"]; +var mode; +var whitelist; +var blacklist; -// function to wait for given elements to appear - first found element gets returned -var waitForEl = function(selectors, callback) { +// wrapper to prepend to log messages +function log(text) { + if (enableConsoleLog) { + console.log(logPrepend + ' ' + text) + } +} + +// function to wait for given elements to appear - first found element gets returned (but as of now the selectors are for different layouts anyways) +var waitForEl = function(counter, selectors, callback) { var match = false; + // check all of the selectors for (const selector of selectors) { + // if found if ($(selector).length) { + // set match = true to prevent repetition hand over the found element match = true; callback(selector); } } - if (!match) { + // repeat if no match was found and we did not exceed the wait factor yet + if (!match && counter < maxElementWaitFactor) { setTimeout(function() { - waitForEl(selectors, callback); - }, 100); + // increase counter + waitForEl(counter + 1, selectors, callback); + }, 100); } }; -// regex match to extract @handle from any (as always, the regex was awful to build) -function extractHandle(url) { - if (!url) return null; +// extract handle from any mastodon url +var extractHandle = function(url, callback) { // regex with named match group - const match = url.match(/^(?:https?:\/\/(www\.)?.*\..*?\/)(?@\w+(?:@\w+\.\w+)?)(?:\/?.*|\z)$/); - // return the named match group - return match.groups.handle -} + var match = url.match(handleRegex); + match = match.groups.handle + // check if match is valid + ats = (match.match(/@/g) || []).length; + if (!(match == null) && ats >= 1 && ats <= 2) { + // return the named match group + callback(match); + } +} + +// test if the current site should be processed or not +// this will also be the function for whitelist/blacklist feature +function checkSite() { + // is this site on our home instance? + if (document.domain == instance) { + log("Current site is your home instance."); + return false; + } + if (mode == "whitelist") { + if ($.inArray(document.domain, whitelist) < 0) { + log("Current site is not in whitelist."); + return false; + } + } else { + if ($.inArray(document.domain, blacklist) > -1) { + log("Current site is in blacklist."); + return false; + } + } + // check if the current site looks like Mastodon + $(document).ready(function() { + if (!($("head").text().includes("mastodon") || $("head").text().includes("Mastodon") || $("div#mastodon").length)) { + log("Could not find a reference that this is a Mastodon site.") + return false; + } + }); + return true; +} // main function to listen for the follow button pressed and open a new tab with the home instance -function processButton() { - // is this site on our home instance? - if (!(document.domain == instance)) { - // wait for the DOM... - $(document).ready(function() { - // check if we are on a mastodon site with a handle in url - if (($("head").text().includes("mastodon") || $("head").text().includes("Mastodon") || $("div#mastodon").length) && window.location.href.includes("@")) { - // wait until follow button appears - waitForEl(buttonPaths, function(found) { - // grab the user handle - handle = extractHandle(window.location.href); - // if we got one... - if (!(handle == null) && (handle.match(/@/g) || []).length > 0) { +function processSite() { + // check if we have a handle in the url + if (window.location.href.includes("@")) { + // grab the user handle + extractHandle(window.location.href, function(handle) { + // if we got one... + if (handle) { + // wait until follow button appears (document is already ready, but most content is loaded afterwards) + waitForEl(0, buttonPaths, function(found) { + if (found) { // setup the button click listener $(found).click(function(e) { + // prevent default action and other handlers e.preventDefault(); e.stopImmediatePropagation(); // check the alert setting and show it if set @@ -57,7 +110,7 @@ function processButton() { $(found).text("Redirecting..."); // timeout 1000ms to make it possible to notice the redirection indication setTimeout(function() { - // if more than 1 @, we have a domain + // if more than 1 @, we have a domain in the handle if ((handle.match(/@/g) || []).length > 1) { // but if its our own... if (handle.includes(instance)) { @@ -67,26 +120,33 @@ function processButton() { // request string var request = 'https://'+instance+'/'+handle; } else { - // with only 1 @, we have a local handle and need to append to domain + // with only 1 @, we have a local handle and need to append the domain var request = 'https://'+instance+'/'+handle+'@'+document.domain; } // open the window - var win = window.open(request, '_blank'); + var win = window.open(request, target); + log("Redirected to " + request) // focus the new tab if open was successfull if (win) { win.focus(); } else { // otherwise notify user... - console.log('Please allow popups for this website'); + log('Could not open new window. Please allow popups for this website.'); } // restore original button text - $(found).text(originaltext) + $(found).text(originaltext); }, 1000); }); + } else { + log("Could not find any follow button."); } }); + } else { + log("Could not find a handle."); } }); + } else { + log("No handle in this URL."); } } @@ -98,31 +158,91 @@ function urlChangeLoop() { if (!(lastUrl == window.location.href)) { // update lastUrl and run main script lastUrl = window.location.href; - processButton(); + processSite(); } // repeat urlChangeLoop(); }, 300); } -// get the extension setting for the users' Mastadon home instance -chrome.storage.local.get(['mastodonhomeinstance'], function(fetchedData) { - instance = fetchedData.mastodonhomeinstance; - // and alert setting - chrome.storage.local.get(['mastodonalert'], function(fetchedData) { - showAlert = fetchedData.mastodonalert; - // if the value is empty/null/undefined... - if (instance == null || !instance) { - console.log("Mastodon home instance is not set."); +function processDomainList(newLineList) { + // split by new line + var arrayFromList = newLineList.split(/\r?\n/); + // array to put checked domains into + var cleanedArray = []; + for (var domain of arrayFromList) { + // remove whitespace + domain = domain.trim(); + if (domainRegex.test(domain)) { + cleanedArray.push(domain) } else { - // if the value looks like a domain... - if (/\w+\.\w+/.test(instance)) { - // ... run the actual script (once for the start and then in a loop depending on url changes) - processButton(); - urlChangeLoop(); - } else { - console.log("Instance setting is not a valid domain name."); - } + log("Removed invalid domain " + domain + " from blacklist/whitelist.") } + } + // return newly created set (remvoes duplicates) + return [...new Set(cleanedArray)];; +} + +function checkSettings() { + // if the home instance is undefined/null/empty + if (instance == null || !instance) { + log("Mastodon home instance is not set."); + return false; + } + // if the value looks like a domain... + if (!(domainRegex.test(instance))) { + log("Instance setting is not a valid domain name."); + return false; + } + // set default if no value + if ($.inArray(mode, ["blacklist","whitelist"]) < 0) { + mode = "blacklist"; + } + if (mode == "whitelist") { + // if in whitelist mode and the cleaned whitelist is empty, return false + whitelist = processDomainList(whitelist); + if (whitelist.length < 1) { + log("Whitelist is empty or invalid.") + return false; + } + } else { + // also process the blacklist if in blacklist mode, but an empty blacklist is OK so we do not return false + blacklist = processDomainList(blacklist); + } + return true; +} + +function run() { + // get the extension setting for the users' Mastadon home instance + chrome.storage.local.get(['fedifollow_homeinstance'], function(fetchedData) { + instance = fetchedData.fedifollow_homeinstance; + // and alert setting + chrome.storage.local.get(['fedifollow_alert'], function(fetchedData) { + showAlert = fetchedData.fedifollow_alert; + // mode + chrome.storage.local.get(['fedifollow_mode'], function(fetchedData) { + mode = fetchedData.fedifollow_mode; + // whitelist + chrome.storage.local.get(['fedifollow_whitelist'], function(fetchedData) { + whitelist = fetchedData.fedifollow_whitelist; + // blacklist + chrome.storage.local.get(['fedifollow_blacklist'], function(fetchedData) { + blacklist = fetchedData.fedifollow_blacklist; + if (checkSettings()) { + // check if the current URL should be processed + if (checkSite()) { + // ... run the actual script (once for the start and then in a loop depending on url changes) + processSite(); + urlChangeLoop(); + } else { + log("Will not process this URL.") + } + } + }); + }); + }); + }); }); -}); +} + +run();