diff --git a/scripts/content.js b/scripts/content.js new file mode 100644 index 0000000..c997d5c --- /dev/null +++ b/scripts/content.js @@ -0,0 +1,245 @@ +/** + * @var attempts + * @description Number of attempts + * @type {number} + */ + +let attempts = 1; + +/** + * @var enabled + * @description Is extension enabled? + * @type {boolean} + */ + +let enabled = true; + +/** + * @var selectors + * @description Array of selectors + * @type {string[]} + */ + +let selectors = []; + +/** + * @constant url + * @description Database link + * @type {string} + */ + +const url = chrome.runtime.getURL("data/elements.txt"); +/*const url = + "https://raw.githubusercontent.com/wanhose/do-not-consent/master/data/elements.txt";*/ + +/** + * @function commit + * @description Commits selector to cache + * @param {string} selector + */ + +const commit = (selector) => { + chrome.storage.local.get(null, (cache) => { + const current = cache[document.location.hostname]; + + chrome.storage.local.set({ + [document.location.hostname]: { + ...current, + matches: [...new Set([...cache.matches, selector])], + }, + }); + }); +}; + +/** + * @function fix + * @description Fix scroll issues + */ + +const fix = () => { + const html = document.documentElement; + const body = document.body; + + html.style.setProperty("overflow-y", "unset", "important"); + body.style.setProperty("overflow-y", "unset", "important"); +}; + +/** + * @function search + * @description Retrieves HTML element if selector exists + * + * @param {string} selector + * @returns {HTMLElement | null} An HTML element or null + */ + +const search = (selector) => { + if (!selector.includes("[") && !selector.includes(">")) { + if (selector.startsWith(".")) { + return document.getElementsByClassName(selector.slice(1))[0]; + } + + if (selector.startsWith("#")) { + return document.getElementById(selector.slice(1)); + } + } else { + return document.querySelector(selector); + } + + return null; +}; + +/** + * @async + * @function check + * @description Checks if extension is enabled + * @returns {Promise} + */ + +const check = () => + new Promise((resolve) => { + chrome.storage.local.get(null, (store) => { + try { + const cache = store[document.location.hostname]; + + resolve(cache.enabled); + } catch { + chrome.storage.local.set( + { + [document.location.hostname]: { + enabled: true, + matches: [], + }, + }, + () => resolve(true) + ); + } + }); + }); + +/** + * @function removeFromCache + * @description Removes matched elements from cache results + */ + +const removeFromCache = () => { + chrome.storage.local.get(null, (store) => { + const cache = store[document.location.hostname]; + const matches = cache.matches; + + if (!!matches.length) { + for (let i = matches.length; i--; ) { + const selector = selectors[i]; + const element = search(selector); + + if (element) { + const tagName = element.tagName.toUpperCase(); + + if (!["BODY", "HTML"].includes(tagName)) { + element.remove(); + } + } + } + } + }); +}; + +/** + * @function removeFromNetwork + * @description Removes matched elements from network results + */ + +const removeFromNetwork = () => { + for (let i = selectors.length; i--; ) { + const selector = selectors[i]; + const element = search(selector); + + if (element) { + const tagName = element.tagName.toUpperCase(); + + if (!["BODY", "HTML"].includes(tagName)) { + element.remove(); + commit(selector); + } + } + } +}; + +/** + * @async + * @function query + * @description Retrieves selectors list + * + * @returns {Promise} A selectors list + */ + +const query = async () => { + try { + const response = await fetch(url); + const data = await response.text(); + + if (response.status !== 200) throw new Error(); + + return data.split("\n"); + } catch { + return []; + } +}; + +/** + * @constant observer + * @description Observer instance + * @type {MutationObserver} + */ + +const observer = new MutationObserver((_, instance) => { + instance.disconnect(); + fix(); + removeFromCache(); + if (attempts <= 5) removeFromNetwork(); + attempts += 1; + observe(); +}); + +/** + * @function observe + * @description Starts observing document.body element + */ + +const observe = () => { + observer.observe(document.body, { + attributes: true, + childList: true, + }); +}; + +/** + * @async + * @function handleContentLoaded + * @description Cleans, fixes scroll issues and observes document.body element + */ + +const handleContentLoaded = async () => { + chrome.runtime.sendMessage({ type: "ENABLE_POPUP" }); + enabled = await check(); + + if (enabled) { + chrome.runtime.sendMessage({ type: "ENABLE_ICON" }); + selectors = await query(); + + if (selectors.length > 0) { + fix(); + removeFromCache(); + removeFromNetwork(); + observe(); + } + } +}; + +/** + * @description Listen to document ready + * + * @type {Document} + * @listens document#ready + */ + +document.addEventListener("DOMContentLoaded", handleContentLoaded); diff --git a/scripts/popup.js b/scripts/popup.js new file mode 100644 index 0000000..5417349 --- /dev/null +++ b/scripts/popup.js @@ -0,0 +1,151 @@ +/** + * @constant chromeUrl + * @description Chrome Web Store link + * @type {string} + */ + +const chromeUrl = + "https://chrome.google.com/webstore/detail/do-not-consent/djcbfpkdhdkaflcigibkbpboflaplabg"; + +/** + * @constant firefoxUrl + * @description Firefox Add-ons link + * @type {string} + */ + +const firefoxUrl = + "https://addons.mozilla.org/es/firefox/addon/do-not-consent/"; + +/** + * @constant isChromium + * @description Is current browser an instance of Chromium? + * @type {boolean} + */ + +const isChromium = chrome.runtime.getURL("").startsWith("chrome-extension://"); + +/** + * @async + * @function currentTab + * @description Returns current tab state + * + * @returns {Promise<{ id: string, location: URL }>} + */ + +const currentTab = () => + new Promise((resolve) => { + chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { + resolve({ + id: tabs[0].id, + location: new URL(tabs[0].url), + }); + }); + }); + +/** + * @async + * @function currentState + * @description Returns current extension state + * + * @returns {Promise>}>} + */ + +const currentState = async () => { + const tab = await currentTab(); + + return new Promise((resolve) => { + chrome.storage.local.get(null, (store) => { + resolve(store[tab.location.hostname]); + }); + }); +}; + +/** + * @function handleButtonClick + * @description Disables or enables extension + * + * @param {MouseEvent} event + */ + +const handleStateButtonClick = async () => { + const state = await currentState(); + const tab = await currentTab(); + + chrome.storage.local.set( + { + [tab.location.hostname]: { + ...state, + enabled: !state.enabled, + }, + }, + () => { + const stateButton = document.getElementById("state-button"); + + stateButton.innerHTML = state.enabled + ? "Enable extension" + : "Disable extension"; + chrome.runtime.sendMessage({ + type: state.enabled ? "DISABLE_ICON" : "ENABLE:ICON", + }); + chrome.tabs.reload(tab.id, { bypassCache: true }); + } + ); +}; + +/** + * @function handleStarClick + * @description Hides stars and shows negative or positive messages + * + * @param {MouseEvent} event + */ + +const handleStarClick = (event) => { + const negative = document.getElementById("negative"); + const positive = document.getElementById("positive"); + const { score } = event.currentTarget.dataset; + const stars = document.getElementById("stars"); + + switch (score) { + case "1": + case "2": + case "3": + stars.setAttribute("hidden", "true"); + negative.removeAttribute("hidden"); + break; + case "4": + case "5": + stars.setAttribute("hidden", "true"); + positive.removeAttribute("hidden"); + break; + default: + break; + } +}; + +/** + * @function handleContentLoaded + * @description Setup stars handlers and result message links + */ + +const handleContentLoaded = async () => { + const stars = Array.from(document.getElementsByClassName("star")); + const state = await currentState(); + const stateButton = document.getElementById("state-button"); + const storeLink = document.getElementById("store-link"); + + stars.forEach((star) => star.addEventListener("click", handleStarClick)); + stateButton.innerHTML = state.enabled + ? "Disable extension" + : "Enable extension"; + stateButton.addEventListener("click", handleStateButtonClick); + storeLink.setAttribute("href", isChromium ? chromeUrl : firefoxUrl); +}; + +/** + * @description Listen to document ready + * + * @type {Document} + * @listens document#ready + */ + +document.addEventListener("DOMContentLoaded", handleContentLoaded);