From 62b543e4d4152bd5430e2f7ffd066e2e235edc39 Mon Sep 17 00:00:00 2001 From: wanhose Date: Wed, 20 Jul 2022 10:27:05 +0200 Subject: [PATCH] feat(browser-extension): improve scripts --- .../src/scripts/background.js | 260 +++++------------- .../browser-extension/src/scripts/content.js | 113 +++----- .../browser-extension/src/scripts/popup.js | 56 ++-- 3 files changed, 128 insertions(+), 301 deletions(-) diff --git a/packages/browser-extension/src/scripts/background.js b/packages/browser-extension/src/scripts/background.js index 110c5fb..e1f5df2 100644 --- a/packages/browser-extension/src/scripts/background.js +++ b/packages/browser-extension/src/scripts/background.js @@ -3,230 +3,120 @@ * @type {string} */ -const apiUrl = 'https://api.cookie-dialog-monster.com/rest/v1'; +const apiUrl = 'https://api.cookie-dialog-monster.com/rest/v2'; /** - * @description Base data URL - * @type {string} - */ - -const baseDataUrl = 'https://raw.githubusercontent.com/wanhose/cookie-dialog-monster/main/data'; - -/** - * @description Cache data - * @type {{ attributes: string[], classes: string[], fixes: string[], selectors: string[], skips: string[] }} - */ - -let cache = undefined; - -/** - * @description Context menu identifier - * @type {string} - */ - -const contextMenuId = 'CDM_MENU'; - -/** - * @description Cache initial state + * @description Initial state * @type {{ enabled: boolean }} */ const initial = { enabled: true }; /** - * @description Disables icon - * @param {string} tabId + * @description Context menu identifier + * @type {string} */ -const disableIcon = (tabId) => - chrome.browserAction.setIcon({ path: 'assets/icons/disabled.png', tabId }); +const reportMenuItemId = 'REPORT'; /** - * @description Enables icon - * @param {string} tabId + * @description Refreshes data + * @param {void?} callback */ -const enableIcon = (tabId) => - chrome.browserAction.setIcon({ path: 'assets/icons/enabled.png', tabId }); - -/** - * @description Enables popup - * @param {string} tabId - */ - -const enablePopup = (tabId) => chrome.browserAction.setPopup({ popup: 'popup.html', tabId }); - -/** - * @description Retrieves store - * @param {string} hostname - * @param {void} callback - * @returns {{ enabled: boolean }} - */ - -const getStore = (hostname, callback) => { - chrome.storage.local.get(null, (store) => { - callback(store[hostname] ?? initial); - }); -}; - -/** - * @async - * @description Get all data from GitHub - * @param {void} callback - * @returns {Promise<{ attributes: string[], classes: string[], fixes: string[], selectors: string[], skips: string[] }>} - */ - -const getData = async (callback) => { - if (cache) { - callback(cache); - return; - } - - const data = await Promise.all([ - query('classes'), - query('elements'), - query('fixes'), - query('skips'), - ]); - - const result = { - attributes: [ - ...new Set( - data[1].elements.flatMap((element) => { - const attributes = element.match(/(?<=\[)[^(){}[\]]+(?=\])/g); - - return attributes?.length - ? [ - ...attributes.flatMap((attribute) => { - return attribute ? [attribute.replace(/\".*\"|(=|\^|\*|\$)/g, '')] : []; - }), - ] - : []; - }) - ), - ], - classes: data[0].classes, - fixes: data[2].fixes, - selectors: data[1].elements, - skips: data[3].skips, - }; - - if (Object.keys(result).every((key) => result[key].length > 0)) cache = result; - callback(result); -}; - -/** - * @description Retrieves current tab information - * @param {void} [callback] - * @returns {Promise<{ id: string, location: string }>} - */ - -const getTab = (callback) => { - chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { - const tab = tabs[0]; - - callback({ - id: tab?.id, - hostname: tab ? new URL(tab.url).hostname.split('.').slice(-2).join('.') : undefined, +const refreshData = (callback) => { + fetch(`${apiUrl}/data/`).then((result) => { + result.json().then(({ data }) => { + chrome.storage.local.set({ data }); + callback(data); }); }); }; /** * @async - * @description Retrieves data from GitHub - * @param {string} key - * @returns {Promise<{ [key]: string[] }>} - */ - -const query = async (key) => { - try { - const url = `${baseDataUrl}/${key}.txt`; - const response = await fetch(url); - const data = await response.text(); - - if (response.status !== 200) throw new Error(); - - return { [key]: [...new Set(data.split('\n'))] }; - } catch { - return { [key]: [] }; - } -}; - -/** * @description Reports active tab URL + * @param {chrome.tabs.Tab} tab */ -const report = () => { - chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { - const tab = tabs[0]; - const userAgent = window.navigator.userAgent; - const version = chrome.runtime.getManifest().version; +const report = async (tab) => { + const version = chrome.runtime.getManifest().version; + const body = JSON.stringify({ url: tab?.url, version }); + const headers = { 'Content-type': 'application/json' }; + const url = `${apiUrl}/report/`; - if (tab) { - fetch(`${apiUrl}/report/`, { - body: JSON.stringify({ - html: `Browser: ${userAgent}
Site: ${tab.url}
Version: ${version}`, - to: 'hello@wanhose.dev', - subject: 'Cookie Dialog Monster Report', - }), - headers: { - 'Content-type': 'application/json', - }, - method: 'POST', - }); - } - }); + await fetch(url, { body, headers, method: 'POST' }); + chrome.tabs.sendMessage(tab.id, { type: 'SHOW_REPORT_CONFIRMATION' }); }; /** - * @description Update store - * @param {string} [hostname] - * @param {object} [state] + * @description Listens to context menus */ -const updateStore = (hostname, state) => { - chrome.storage.local.get(null, (cache) => { - const current = cache[hostname]; +chrome.contextMenus.onClicked.addListener((info, tab) => { + switch (info.menuItemId) { + case reportMenuItemId: + if (tab) report(tab); + break; + default: + break; + } +}); - chrome.storage.local.set({ - [hostname]: { - enabled: typeof state.enabled === 'undefined' ? current.enabled : state.enabled, - }, - }); +/** + * @description Listens to extension installed/updated + */ + +chrome.runtime.onInstalled.addListener(() => { + chrome.contextMenus.create({ + contexts: ['all'], + documentUrlPatterns: chrome.runtime.getManifest().content_scripts[0].matches, + id: reportMenuItemId, + title: chrome.i18n.getMessage('contextMenuText'), }); -}; +}); + +/** + * @description Listens to first start + */ + +chrome.runtime.onStartup.addListener(() => { + refreshData(); +}); /** * @description Listens to messages */ -chrome.runtime.onMessage.addListener((request, sender, callback) => { - const hostname = request.hostname; - const state = request.state; +chrome.runtime.onMessage.addListener((message, sender, callback) => { + const hostname = message.hostname; const tabId = sender.tab?.id; - switch (request.type) { + switch (message.type) { case 'DISABLE_ICON': - if (tabId) disableIcon(tabId); + if (tabId) chrome.browserAction.setIcon({ path: 'assets/icons/disabled.png', tabId }); break; case 'ENABLE_ICON': - if (tabId) enableIcon(tabId); + if (tabId) chrome.browserAction.setIcon({ path: 'assets/icons/enabled.png', tabId }); break; case 'ENABLE_POPUP': - if (tabId) enablePopup(tabId); + if (tabId) chrome.browserAction.setPopup({ popup: 'popup.html', tabId }); break; case 'GET_DATA': - getData(callback); + chrome.storage.local.get('data', ({ data }) => { + if (data) callback(data); + else refreshData(callback); + }); break; - case 'GET_STORE': - getStore(hostname, callback); + case 'GET_STATE': + // prettier-ignore + if (hostname) chrome.storage.local.get(hostname, (state) => callback(state[hostname] ?? initial)); break; case 'GET_TAB': - getTab(callback); + chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => callback(tabs[0])); break; - case 'UPDATE_STORE': - updateStore(hostname, state); + case 'UPDATE_STATE': + if (hostname) chrome.storage.local.set({ [hostname]: message.state }); break; default: break; @@ -234,23 +124,3 @@ chrome.runtime.onMessage.addListener((request, sender, callback) => { return true; }); - -/** - * @description Creates context menu - */ - -chrome.contextMenus.create({ - contexts: ['all'], - documentUrlPatterns: chrome.runtime.getManifest().content_scripts[0].matches, - id: contextMenuId, - title: chrome.i18n.getMessage('contextMenuText'), -}); - -/** - * @description Listens to context menus - */ - -chrome.contextMenus.onClicked.addListener((info) => { - if (info.menuItemId !== contextMenuId) return; - report(); -}); diff --git a/packages/browser-extension/src/scripts/content.js b/packages/browser-extension/src/scripts/content.js index da2e93f..76ee91e 100644 --- a/packages/browser-extension/src/scripts/content.js +++ b/packages/browser-extension/src/scripts/content.js @@ -1,36 +1,28 @@ /** - * @description Array of selectors - * @type {string[]} + * @description Data properties + * @type {{ classes: string[], fixes: string[], elements: string[], skips: string[] }?} */ -const classes = []; +let data = null; /** * @description Shortcut to send messages to background script - * @type {void} */ const dispatch = chrome.runtime.sendMessage; /** - * @description Array of skips to skip - * @type {string[]} + * @description Forbidden tags to ignore in the DOM */ -const skips = []; +const forbiddenTags = ['BASE', 'BODY', 'HEAD', 'HTML', 'LINK', 'META', 'SCRIPT', 'STYLE', 'TITLE']; /** - * @description Array of instructions - * @type {string[]} + * @description Current hostname + * @type {string} */ -const fixes = []; - -/** - * @description Hostname - */ - -const hostname = document.location.hostname.split('.').slice(-2).join('.'); +const hostname = document.location.hostname.split('.').slice(-3).join('.').replace('www.', ''); /** * @description Options provided to observer @@ -46,56 +38,43 @@ const options = { childList: true, subtree: true }; const preview = hostname.startsWith('consent.') || hostname.startsWith('myprivacy.'); /** - * @description Selectors list - * @type {string[]} - */ - -const selectors = []; - -/** - * @description Target provided to observer - */ - -const target = document.body || document.documentElement; - -/** - * @description Checks if node element is removable - * @param {any} node - * @param {boolean} skipMatch + * @description Matches if node element is removable + * @param {Element} node * @returns {boolean} */ -const check = (node, skipMatch) => +const match = (node) => node instanceof HTMLElement && node.parentElement && - !['BODY', 'HTML'].includes(node.tagName) && - !(node.id && ['APP', 'ROOT'].includes(node.id.toUpperCase?.())) && - (skipMatch || node.matches(selectors)); + !forbiddenTags.includes(node.tagName?.toUpperCase?.()) && + node.matches(data?.elements ?? []); /** * @description Cleans DOM * @param {HTMLElement[]} nodes - * @param {boolean} skipMatch + * @param {boolean?} skipMatch * @returns {void} */ const clean = (nodes, skipMatch) => - nodes.filter((node) => check(node, skipMatch)).forEach((node) => (node.outerHTML = '')); + nodes.filter((node) => skipMatch || match(node)).forEach((node) => node.remove()); /** * @description Fixes scroll issues */ const fix = () => { - if (skips.length && !skips.includes(hostname)) { + document.getElementsByClassName('_31e')[0]?.classList.remove('_31e'); + + if (data?.skips.length && !data.skips.includes(hostname)) { for (const item of [document.body, document.documentElement]) { - item?.classList.remove(...classes); + item?.classList.remove(...(data?.classes ?? [])); item?.style.setProperty('position', 'initial', 'important'); item?.style.setProperty('overflow-y', 'initial', 'important'); } } - for (const fix of fixes) { + for (const fix of data?.fixes ?? []) { const [match, selector, action, property] = fix.split('##'); if (hostname.includes(match)) { @@ -132,54 +111,40 @@ const fix = () => { * @type {MutationObserver} */ -const observer = new MutationObserver((mutations, instance) => { +const observer = new MutationObserver((mutations) => { const nodes = mutations.map((mutation) => Array.from(mutation.addedNodes)).flat(); - instance.disconnect(); fix(); - if (!preview && selectors.length) clean(nodes); - instance.observe(target, options); + if (data?.elements.length && !preview) clean(nodes); }); /** - * @description Cleans DOM again after all - * @listens document#readystatechange + * @description Fixes bfcache issues + * @listens window#pageshow */ -document.addEventListener('readystatechange', () => { - dispatch({ hostname, type: 'GET_STORE' }, null, async ({ enabled }) => { - if (document.readyState === 'complete' && enabled && !preview) { - const nodes = selectors.length ? Array.from(document.querySelectorAll(selectors)) : []; - - fix(); - clean(nodes, true); - setTimeout(() => clean(nodes, true), 2000); - } - }); +window.addEventListener('pageshow', (event) => { + if (event.persisted) { + dispatch({ hostname, type: 'GET_STATE' }, null, (state) => { + if (data?.elements.length && state?.enabled && !preview) { + fix(); + clean(Array.from(document.querySelectorAll(data.elements)), true); + } + }); + } }); /** - * @description Fix bfcache issues - * @listens window#unload + * @description Sets up everything */ -window.addEventListener('unload', () => {}); - -/** - * @description Setups everything and starts to observe if enabled - */ - -dispatch({ hostname, type: 'GET_STORE' }, null, ({ enabled }) => { +dispatch({ hostname, type: 'GET_STATE' }, null, (state) => { dispatch({ type: 'ENABLE_POPUP' }); - if (enabled) { - dispatch({ type: 'GET_DATA' }, null, (data) => { - classes.push(...data.classes); - fixes.push(...data.fixes); - options.attributeFilter = data.attributes; - selectors.push(...data.selectors); - skips.push(...data.skips); - observer.observe(target, options); + if (state?.enabled) { + dispatch({ hostname, type: 'GET_DATA' }, null, (result) => { + data = result; + observer.observe(document.body ?? document.documentElement, options); dispatch({ type: 'ENABLE_ICON' }); }); } diff --git a/packages/browser-extension/src/scripts/popup.js b/packages/browser-extension/src/scripts/popup.js index df0379c..32355a9 100644 --- a/packages/browser-extension/src/scripts/popup.js +++ b/packages/browser-extension/src/scripts/popup.js @@ -1,5 +1,4 @@ /** - * @constant chromeUrl * @description Chrome Web Store link * @type {string} */ @@ -7,15 +6,12 @@ const chromeUrl = 'https://chrome.google.com/webstore/detail/djcbfpkdhdkaflcigibkbpboflaplabg'; /** - * @constant dispatch * @description Shortcut to send messages to background script - * @type {void} */ const dispatch = chrome.runtime.sendMessage; /** - * @constant edgeUrl * @description Edge Add-ons link * @type {string} */ @@ -24,7 +20,6 @@ const edgeUrl = 'https://microsoftedge.microsoft.com/addons/detail/hbogodfciblakeneadpcolhmfckmjcii'; /** - * @constant firefoxUrl * @description Firefox Add-ons link * @type {string} */ @@ -32,7 +27,13 @@ const edgeUrl = const firefoxUrl = 'https://addons.mozilla.org/es/firefox/addon/cookie-dialog-monster/'; /** - * @constant isChromium + * @description Current hostname + * @type {string} + */ + +let hostname = '?'; + +/** * @description Is current browser an instance of Chromium? * @type {boolean} */ @@ -40,7 +41,6 @@ const firefoxUrl = 'https://addons.mozilla.org/es/firefox/addon/cookie-dialog-mo const isChromium = navigator.userAgent.indexOf('Chrome') !== -1; /** - * @constant isEdge * @description Is current browser an instance of Edge? * @type {boolean} */ @@ -48,7 +48,6 @@ const isChromium = navigator.userAgent.indexOf('Chrome') !== -1; const isEdge = navigator.userAgent.indexOf('Edg') !== -1; /** - * @constant isFirefox * @description Is current browser an instance of Firefox? * @type {boolean} */ @@ -59,22 +58,10 @@ const isFirefox = navigator.userAgent.indexOf('Firefox') !== -1; * @description Disables or enables extension on current page */ -const handlePowerChange = () => { - dispatch({ type: 'GET_TAB' }, null, ({ hostname, id }) => { - dispatch({ hostname, type: 'GET_STORE' }, null, ({ enabled }) => { - dispatch({ hostname, state: { enabled: !enabled }, type: 'UPDATE_STORE' }); - chrome.tabs.reload(id, { bypassCache: true }); - }); - }); -}; - -/** - * @description Reload current page - */ - -const handleReload = () => { - dispatch({ type: 'GET_TAB' }, null, ({ id }) => { - chrome.tabs.reload(id, { bypassCache: true }); +const handlePowerChange = async () => { + dispatch({ hostname, type: 'GET_STATE' }, null, (state) => { + dispatch({ hostname, state: { enabled: !state?.enabled }, type: 'UPDATE_STATE' }); + chrome.tabs.reload({ bypassCache: true }); }); }; @@ -106,8 +93,12 @@ const handleRate = (event) => { */ const handleContentLoaded = () => { - dispatch({ type: 'GET_TAB' }, null, ({ hostname }) => { - dispatch({ hostname, type: 'GET_STORE' }, null, ({ enabled }) => { + dispatch({ type: 'GET_TAB' }, null, (tab) => { + hostname = tab?.url + ? new URL(tab.url).hostname.split('.').slice(-3).join('.').replace('www.', '') + : undefined; + + dispatch({ hostname, type: 'GET_STATE' }, null, (state) => { translate(); const host = document.getElementById('host'); @@ -119,13 +110,14 @@ const handleContentLoaded = () => { like.addEventListener('click', handleRate); power.addEventListener('change', handlePowerChange); - reload.addEventListener('click', handleReload); - if (isEdge) store.setAttribute('href', edgeUrl); - else if (isChromium) store.setAttribute('href', chromeUrl); - else if (isFirefox) store.setAttribute('href', firefoxUrl); + reload.addEventListener('click', () => chrome.tabs.reload({ bypassCache: true })); unlike.addEventListener('click', handleRate); - if (location) host.innerText = hostname.replace('www.', ''); - if (!enabled) power.removeAttribute('checked'); + + host.innerText = hostname ?? 'unknown'; + if (isEdge) store?.setAttribute('href', edgeUrl); + else if (isChromium) store?.setAttribute('href', chromeUrl); + else if (isFirefox) store?.setAttribute('href', firefoxUrl); + if (!state.enabled) power.removeAttribute('checked'); }); }); };