diff --git a/packages/browser-extension/src/manifest.json b/packages/browser-extension/src/manifest.json index c0ad2ba..b9ae3e1 100644 --- a/packages/browser-extension/src/manifest.json +++ b/packages/browser-extension/src/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 2, "name": "Cookie Dialog Monster", - "version": "5.4.6", + "version": "5.5.0", "default_locale": "en", "description": "__MSG_appDesc__", "icons": { @@ -31,12 +31,6 @@ "run_at": "document_start" } ], - "permissions": [ - "contextMenus", - "http://*/*", - "https://*/*", - "storage", - "tabs" - ], + "permissions": ["contextMenus", "http://*/*", "https://*/*", "storage", "tabs"], "web_accessible_resources": ["assets/fonts/*", "scripts/popup.js", "styles/*"] } diff --git a/packages/browser-extension/src/scripts/background.js b/packages/browser-extension/src/scripts/background.js index 0937bf3..11a67bd 100644 --- a/packages/browser-extension/src/scripts/background.js +++ b/packages/browser-extension/src/scripts/background.js @@ -1,5 +1,13 @@ +/** + * @description Base data URL + * @type {string} + */ + +const baseDataUrl = 'https://raw.githubusercontent.com/wanhose/cookie-dialog-monster/main/data'; + /** * @description Context menu identifier + * @type {string} */ const contextMenuId = 'CDM_MENU'; @@ -9,150 +17,63 @@ const contextMenuId = 'CDM_MENU'; * @type {{ enabled: boolean }} */ -const initial = { - enabled: true, -}; - -/** - * @description Check cache validity - * @param {object} [cache] - */ - -const check = (cache) => typeof cache.enabled === 'boolean'; +const initial = { enabled: true }; /** * @description Disables icon - * @param {string} [tabId] + * @param {string} tabId */ -const disableIcon = (tabId) => { - chrome.browserAction.setIcon({ - path: 'assets/icons/disabled.png', - tabId, - }); -}; - -/** - * @description Disables popup - * @param {string} [tabId] - */ - -const disablePopup = (tabId) => { - chrome.browserAction.setPopup({ - popup: '', - tabId, - }); -}; +const disableIcon = (tabId) => + chrome.browserAction.setIcon({ path: 'assets/icons/disabled.png', tabId }); /** * @description Enables icon - * @param {string} [tabId] + * @param {string} tabId */ -const enableIcon = (tabId) => { - chrome.browserAction.setIcon({ - path: 'assets/icons/enabled.png', - tabId, - }); -}; +const enableIcon = (tabId) => + chrome.browserAction.setIcon({ path: 'assets/icons/enabled.png', tabId }); /** * @description Enables popup - * @param {string} [tabId] + * @param {string} tabId */ -const enablePopup = (tabId) => { - chrome.browserAction.setPopup({ - popup: 'popup.html', - tabId, - }); -}; +const enablePopup = (tabId) => chrome.browserAction.setPopup({ popup: 'popup.html', tabId }); /** * @description Retrieves cache state - * @param {string} [hostname] - * @param {void} [callback] + * @param {string} hostname + * @param {void} callback * @returns {Promise<{ enabled: boolean }>} */ const getCache = (hostname, callback) => { chrome.storage.local.get(null, (store) => { - try { - const cache = store[hostname]; - - if (!check(cache)) throw new Error(); - - callback(cache); - } catch { - chrome.storage.local.set({ [hostname]: initial }); - callback(initial); - } + callback(store[hostname] ?? initial); }); }; /** * @async - * @description Retrieves a selectors list - * @param {void} [callback] - * @returns {Promise<{ classes: string[] }>} + * @description Retrieves data from GitHub + * @param {string} key + * @param {void} callback + * @returns {Promise<{ any: string[] }>} */ -const getClasses = async (callback) => { +const query = async (key, callback) => { try { - const url = - 'https://raw.githubusercontent.com/wanhose/cookie-dialog-monster/master/data/classes.txt'; + const url = `${baseDataUrl}/${key}.txt`; const response = await fetch(url); const data = await response.text(); if (response.status !== 200) throw new Error(); - callback({ classes: data.split('\n') }); + callback({ [key]: data.split('\n') }); } catch { - callback({ classes: [] }); - } -}; - -/** - * @async - * @description Retrieves a selectors list - * @param {void} [callback] - * @returns {Promise<{ classes: string[] }>} - */ - -const getFixes = async (callback) => { - try { - const url = - 'https://raw.githubusercontent.com/wanhose/cookie-dialog-monster/master/data/fixes.txt'; - const response = await fetch(url); - const data = await response.text(); - - if (response.status !== 200) throw new Error(); - - callback({ fixes: data.split('\n') }); - } catch { - callback({ fixes: [] }); - } -}; - -/** - * @async - * @description Retrieves a selectors list - * @param {void} [callback] - * @returns {Promise<{ selectors: string }>} - */ - -const getSelectors = async (callback) => { - try { - const url = - 'https://raw.githubusercontent.com/wanhose/cookie-dialog-monster/master/data/elements.txt'; - const response = await fetch(url); - const data = await response.text(); - - if (response.status !== 200) throw new Error(); - - callback({ selectors: data.split('\n') }); - } catch { - callback({ selectors: [] }); + callback({ [key]: [] }); } }; @@ -162,7 +83,7 @@ const getSelectors = async (callback) => { * @returns {Promise<{ id: string, location: string }>} */ -const getTab = (callback) => { +const queryTab = (callback) => { chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { callback({ id: tabs[0].id, @@ -209,10 +130,7 @@ const updateCache = (hostname, state) => { chrome.storage.local.set({ [hostname]: { - enabled: - typeof state.enabled === 'undefined' - ? current.enabled - : state.enabled, + enabled: typeof state.enabled === 'undefined' ? current.enabled : state.enabled, }, }); }); @@ -223,39 +141,40 @@ const updateCache = (hostname, state) => { */ chrome.runtime.onMessage.addListener((request, sender, callback) => { - const hasPermission = !sender.frameId || sender.frameId === 0; - let tabId = sender.tab ? sender.tab.id : undefined; + const hostname = request.hostname; + const state = request.state; + const tabId = sender.tab?.id; switch (request.type) { case 'DISABLE_ICON': - if (hasPermission && tabId) disableIcon(tabId); - break; - case 'DISABLE_POPUP': - if (hasPermission && tabId) disablePopup(tabId); + if (tabId) disableIcon(tabId); break; case 'ENABLE_ICON': - if (hasPermission && tabId) enableIcon(tabId); + if (tabId) enableIcon(tabId); break; case 'ENABLE_POPUP': - if (hasPermission && tabId) enablePopup(tabId); + if (tabId) enablePopup(tabId); break; case 'GET_CACHE': - getCache(request.hostname, callback); + getCache(hostname, callback); break; case 'GET_CLASSES': - getClasses(callback); + query('classes', callback); + break; + case 'GET_SKIPS': + query('skips', callback); break; case 'GET_FIXES': - getFixes(callback); + query('fixes', callback); break; case 'GET_SELECTORS': - getSelectors(callback); + query('elements', callback); break; case 'GET_TAB': - getTab(callback); + queryTab(callback); break; case 'UPDATE_CACHE': - updateCache(request.hostname, request.state); + updateCache(hostname, state); break; default: break; diff --git a/packages/browser-extension/src/scripts/content.js b/packages/browser-extension/src/scripts/content.js index e5229ca..3f18d0a 100644 --- a/packages/browser-extension/src/scripts/content.js +++ b/packages/browser-extension/src/scripts/content.js @@ -3,7 +3,7 @@ * @type {string[]} */ -let classes = []; +const classes = []; /** * @description Shortcut to send messages to background script @@ -12,12 +12,19 @@ let classes = []; const dispatch = chrome.runtime.sendMessage; +/** + * @description Array of skips to skip + * @type {string[]} + */ + +const skips = []; + /** * @description Array of instructions * @type {string[]} */ -let fixes = []; +const fixes = []; /** * @description Hostname @@ -29,8 +36,7 @@ const hostname = document.location.hostname; * @description Is consent preview page? */ -const isPreview = - hostname.startsWith('consent.') || hostname.startsWith('myprivacy.'); +const preview = hostname.startsWith('consent.') || hostname.startsWith('myprivacy.'); /** * @description Options provided to observer @@ -43,7 +49,7 @@ const options = { childList: true, subtree: true }; * @type {string[]} */ -let selectors = []; +const selectors = []; /** * @description Target provided to observer @@ -53,42 +59,39 @@ const target = document.body || document.documentElement; /** * @description Checks if node element is removable - * @param {Element} node + * @param {any} node + * @returns {boolean} */ const check = (node) => node instanceof HTMLElement && node.parentElement && !['BODY', 'HTML'].includes(node.tagName) && - !(node.id && ['APP', 'ROOT'].includes(node.id.toUpperCase?.())); + !(node.id && ['APP', 'ROOT'].includes(node.id.toUpperCase?.())) && + node.matches(selectors); /** * @description Cleans DOM + * @param {HTMLElement[]} nodes + * @returns {void} */ -const clean = () => { - if (selectors.length) { - const nodes = Array.from(document.querySelectorAll(selectors)); - nodes.filter(check).forEach((node) => (node.outerHTML = '')); - } -}; +const clean = (nodes) => nodes.filter(check).forEach((node) => (node.outerHTML = '')); /** * @description Fixes scroll issues */ const fix = () => { - const body = document.body; - const html = document.documentElement; + if (skips.length && !skips.includes(hostname)) { + for (const item of [document.body, document.documentElement]) { + item?.classList.remove(...classes); + item?.style.setProperty('position', 'initial', 'important'); + item?.style.setProperty('overflow-y', 'initial', 'important'); + } + } - body?.classList.remove(...classes); - body?.style.setProperty('position', 'initial', 'important'); - body?.style.setProperty('overflow-y', 'initial', 'important'); - html?.classList.remove(...classes); - html?.style.setProperty('position', 'initial', 'important'); - html?.style.setProperty('overflow-y', 'initial', 'important'); - - fixes.forEach((fix) => { + for (const fix of fixes) { const [match, selector, action, property] = fix.split('##'); if (hostname.includes(match)) { @@ -96,73 +99,56 @@ const fix = () => { case 'click': { const node = document.querySelector(selector); node?.click(); + break; } case 'remove': { const node = document.querySelector(selector); node?.style?.removeProperty(property); + break; } case 'reset': { const node = document.querySelector(selector); node?.style?.setProperty(property, 'initial', 'important'); + break; } case 'resetAll': { const nodes = document.querySelectorAll(selector); - // prettier-ignore - nodes.forEach((node) => node?.style?.setProperty(property, "initial", "important")); + nodes.forEach((node) => node?.style?.setProperty(property, 'initial', 'important')); + break; } default: break; } } - }); + } }; +/** + * @description Mutation Observer instance + * @type {MutationObserver} + */ + const observer = new MutationObserver((mutations, instance) => { + const nodes = mutations.map((mutation) => Array.from(mutation.addedNodes)).flat(); + instance.disconnect(); fix(); - - if (!isPreview) { - for (const mutation of mutations) { - for (const node of mutation.addedNodes) { - const valid = check(node); - - if (valid && node.matches(selectors)) node.outerHTML = ''; - } - } - } - + if (!preview) clean(nodes); instance.observe(target, options); }); /** - * @description Queries classes selectors - * @returns {Promise<{ classes: string[] }>} + * @description Gets data + * @returns {Promise} */ -const queryClasses = () => - new Promise((resolve) => { - dispatch({ type: 'GET_CLASSES' }, null, resolve); - }); - -/** - * @description Queries fixes instructions - * @returns {Promise<{ fixes: string[] }>} - */ - -const queryFixes = () => - new Promise((resolve) => { - dispatch({ type: 'GET_FIXES' }, null, resolve); - }); - -/** - * @description Queries elements selectors - * @returns {Promise<{ selectors: string }>} - */ - -const querySelectors = () => - new Promise((resolve) => { - dispatch({ type: 'GET_SELECTORS' }, null, resolve); - }); +const promiseAll = () => + Promise.all([ + new Promise((resolve) => dispatch({ type: 'GET_CLASSES' }, null, resolve)), + new Promise((resolve) => dispatch({ type: 'GET_FIXES' }, null, resolve)), + new Promise((resolve) => dispatch({ type: 'GET_SELECTORS' }, null, resolve)), + new Promise((resolve) => dispatch({ type: 'GET_SKIPS' }, null, resolve)), + ]); /** * @description Cleans DOM again after all @@ -171,10 +157,12 @@ const querySelectors = () => document.addEventListener('readystatechange', () => { dispatch({ hostname, type: 'GET_CACHE' }, null, async ({ enabled }) => { - if (document.readyState === 'complete' && enabled && !isPreview) { + if (document.readyState === 'complete' && enabled && !preview) { + const nodes = Array.from(document.querySelectorAll(selectors)); + fix(); - clean(); - setTimeout(clean, 2000); + clean(nodes); + setTimeout(() => clean(nodes), 2000); } }); }); @@ -194,12 +182,12 @@ dispatch({ hostname, type: 'GET_CACHE' }, null, async ({ enabled }) => { dispatch({ type: 'ENABLE_POPUP' }); if (enabled) { - const promises = [queryClasses(), queryFixes(), querySelectors()]; - const results = await Promise.all(promises); + const results = await promiseAll(); - classes = results[0]?.classes ?? []; - fixes = results[1]?.fixes ?? []; - selectors = results[2]?.selectors ?? []; + classes.push(...(results[0]?.classes ?? [])); + fixes.push(...(results[1]?.fixes ?? [])); + selectors.push(...(results[2]?.elements ?? [])); + skips.push(...(results[3]?.skips ?? [])); observer.observe(target, options); dispatch({ type: 'ENABLE_ICON' }); }