Repository: AliasIO/demodal Branch: master Commit: d128b3606914 Files: 32 Total size: 53.5 KB Directory structure: gitextract_jysvujgv/ ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── _locales/ │ ├── en/ │ │ └── messages.json │ └── ru/ │ └── messages.json ├── background/ │ ├── background.html │ └── background.js ├── common/ │ ├── common.css │ └── common.js ├── content/ │ └── content.js ├── definitions/ │ ├── consent.json │ ├── donate.json │ ├── email.json │ ├── message.json │ ├── offer.json │ ├── paywall.json │ └── signup.json ├── inject/ │ └── inject.js ├── manifest-v2.json ├── manifest-v3.json ├── options/ │ ├── options.css │ ├── options.html │ └── options.js ├── package.json ├── popup/ │ ├── popup.css │ ├── popup.html │ └── popup.js └── run ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # editorconfig.org root = true [*] indent_style = space indent_size = 2 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.md] trim_trailing_whitespace = false ================================================ FILE: .eslintignore ================================================ **/lib/* node_modules ================================================ FILE: .eslintrc.js ================================================ module.exports = { root: true, env: { browser: true, node: true, }, parserOptions: { parser: 'babel-eslint', }, extends: [ '@nuxtjs', 'prettier', 'prettier/vue', 'plugin:prettier/recommended', 'plugin:nuxt/recommended', 'plugin:json/recommended', ], plugins: ['prettier'], } ================================================ FILE: .gitignore ================================================ build/* manifest.json node_modules package-lock.json !.gitkeep Thumbs.db Desktop.ini *.DS_Store *.log ._* .idea ================================================ FILE: .prettierrc ================================================ { "semi": false, "arrowParens": "always", "singleQuote": true } ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2021 Elbert Alias Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Demodal Demodal is a browser extension that automatically removes content blocking modals including paywalls, discount offers, promts to sign up or enter your email address and more. Modal dialogues such as paywalls, discount offers, cookie prompts and GDPR banners are user-hostile interfaces that demand your attention and interrupt your browsing experience. Demodal puts you back in control, letting you focus on the content. # Installation ### Google Chrome https://chrome.google.com/webstore/detail/demodal/fjhbdkfknppikobblnjibmkmogjeffcf Install locally: * Clone this repository * Run `./run manifest v3` or copy `manifest-v3.json` to `manifest.json` * Go to `about:extensions` * Enable 'Developer mode' * Click 'Load unpacked' * Select the project folder ### Mozilla Firefox https://addons.mozilla.org/firefox/addon/demodal Install locally: * Clone this repository * Run `./run manifest v2` or copy `manifest-v2.json` to `manifest.json` * Go go `about:debugging#/runtime/this-firefox` * Click 'Load Temporary Add-on' * Select `manifest.json` # Contributing **This extension is in early development. You can help by reporting websites with modals that didn't get blocked, or by creating your own definitions and sharing them with us and the community. The aim is to build up a comprehensive set of rules over time to block modals anywhere.** Demodal is not an ad-blocker. Only create definitions for UI elements that intefere with reading of content. When submitting a pull request, please include a screenshot of the element that's being blocked and a link to a website to test. Every definition should clearly match one modal type (e.g. consent request or paywall). Don't create overly broad definitions (e.g. `div.modal`) that could block legimitate modals. # Specification ## Modal types | Type | Description | |------------|--------------| | `consent` | Cookie and GDPR notices. | `donate` | Prompts to make a donation. | `email` | Prompts to enter your email address. | `message` | General messages and notifications. | `offer` | Promotions and discounts. | `paywall` | Prompts to sign up for a paid subscription. | `signup` | Prompts to create an account. ## Definitions Definitions are located in [`/definitions`](/definitions), file-separated by modal type. Definitions are grouped by URL pattern. ```javascript { " [ ... ]": { // URL pattern // Definition "if [ ... ]": { // Condition "": " [ ... ]" // Action }, // Definition (shorthand, no condition) "": " [ ... ]" // Action } } ``` **Examples** ```javascript { "*.example.com *.example.org": { "if $(.modal)": { ".modal": "remove" // Remove element if present }, ".modal": "addClass hide" // Remove element (shorthand) "if defined(ModalDialog)": { "ModalDialog.close": "call" // Call function if defined }, "if defined(ModalDialog)": { "ModalDialog.setClosed": "call true" // Call function with arguments }, } } ``` ## URL pattern URL patterns are defined as [globs](https://en.wikipedia.org/wiki/Glob_(programming)), allowing wildcards (`*`). | Glob | Matches | |---------------------------|---------| | `*` | Any URL. | `*.example.com` | Apex domain and any subdomain, e.g. `example.com`, `www.example.com`. | `example.com example.org` | `example.com` and `example.org`. | `*.example.com/*/about` | E.g. `www.example.com/en/about`. ## Conditions Conditions start with `if`, followed by one or more functions. If all functions evaluate to `true`, the specified actions are run. ### Functions | Function | Argument | Description | |-------------|----------------------|-------------| | `$()` | [Query selector](https://developer.mozilla.org/docs/Web/API/Document/querySelector) | Tests if an HTML element exists. | `defined()` | JavaScript property | Tests if a JavaScript property exists. | `sleep()` | Time in milliseconds | Returns true after the specified time has passed ## Actions Actions are run when the condition is met, or if no condition is specified. ### Functions | Function | Argument | Description | |----------------|------------|-------------| | `remove` | | Remove the HTML element. | `removeParent` | Number | Remove parent node. | `removeIf` | String | Remove the HTML element if its text content matches a string. | `addClass` | Class name | Add a class. | `removeClass` | Class name | Remove a class. | `addStyle` | Styles | Appends CSS to the style attribute. | `removeStyle` | | Clears the style attribute. | `click` | | Click the HTML element. | `call` | | Call the function. Any arguments will be passed to the function. ================================================ FILE: _locales/en/messages.json ================================================ { "modalType": { "message": "Type" }, "blockedModals": { "message": "Blocked" }, "blockedModalsPage": { "message": "This page" }, "blockedModalsTotal": { "message": "All time" }, "modalTypes": { "message": "Modal types" }, "modalTypeOffer": { "message": "Offers" }, "modalTypeOfferHelp": { "message": "Promotions and discounts." }, "modalTypePaywall": { "message": "Paywalls" }, "modalTypePaywallHelp": { "message": "Prompts to sign up for a paid subscription." }, "modalTypeConsent": { "message": "Consent requests" }, "modalTypeConsentHelp": { "message": "Cookie and GDPR notices." }, "modalTypeSignup": { "message": "Sign-up prompts" }, "modalTypeSignupHelp": { "message": "Prompts to create an account." }, "modalTypeEmail": { "message": "Email prompts" }, "modalTypeEmailHelp": { "message": "Prompts to enter your email address." }, "modalTypeDonate": { "message": "Donation requests" }, "modalTypeDonateHelp": { "message": "Prompts to make a donation." }, "modalTypeMessage": { "message": "Messages" }, "modalTypeMessageHelp": { "message": "General messages and notifications." }, "modalStatsEmpty": { "message": "No modals blocked." }, "allowOnWebsite": { "message": "Allow modals on this website" }, "options": { "message": "Options" }, "optionModalTypesHelp": { "message": "Choose the modal types you wish to block." }, "tabBlockedModals": { "message": "Blocked modals" }, "blockedModalsHelp": { "message": "This extension is in early development. If a modal did not get blocked, please report it or create a new definition and share it." }, "tabDefinitions": { "message": "Definitions" }, "definitionsHelp": { "message": "Did a modal not get blocked? Create your own definition here and share it." }, "readMore": { "message": "Read more" } } ================================================ FILE: _locales/ru/messages.json ================================================ { "modalType": { "message": "Тип" }, "blockedModals": { "message": "Заблокировано" }, "blockedModalsPage": { "message": "На этой странице" }, "blockedModalsTotal": { "message": "За всё время" }, "modalTypes": { "message": "Типы всплывающих сообщений" }, "modalTypeOffer": { "message": "Акции и скидки" }, "modalTypeOfferHelp": { "message": "Предложения купить что либо." }, "modalTypePaywall": { "message": "Платный доступ" }, "modalTypePaywallHelp": { "message": "Предложения оформить платную подписку." }, "modalTypeConsent": { "message": "Уведомление о cookie" }, "modalTypeConsentHelp": { "message": "Уведомления о cookie и GDPR." }, "modalTypeSignup": { "message": "Предложения зарегистрироваться" }, "modalTypeSignupHelp": { "message": "Навязчивое предложение создать аккаунт." }, "modalTypeEmail": { "message": "Email рассылки" }, "modalTypeEmailHelp": { "message": "Навязчивое предложение указать адрес электронной почты." }, "modalTypeDonate": { "message": "Просьба о пожертвовании" }, "modalTypeDonateHelp": { "message": "Предложения сделать пожертвование (задонатить)." }, "modalTypeMessage": { "message": "Уведомления" }, "modalTypeMessageHelp": { "message": "Назойливые сообщения и уведомления." }, "modalStatsEmpty": { "message": "Ни одно всплывающие сообщение не заблокировано." }, "allowOnWebsite": { "message": "Разрешить всплывающие сообщения на этом сайте" }, "options": { "message": "Настройки" }, "optionModalTypesHelp": { "message": "Выберите, какие всплывающие сообщения вы хотите блокировать." }, "tabBlockedModals": { "message": "Заблокированные всплывающие сообщения" }, "blockedModalsHelp": { "message": "Это расширение находится на ранней стадии разработки. Если всплывающие сообщение не был заблокирован, пожалуйста, сообщите об этом или создайте новое правило для его блокировки и поделитесь им." }, "tabDefinitions": { "message": "Правила блокировки" }, "definitionsHelp": { "message": "Всплывающие сообщение не было заблокировано? Создайте свое правило блокировки здесь и поделитесь им." }, "readMore": { "message": "Читать подробнее" } } ================================================ FILE: background/background.html ================================================ ================================================ FILE: background/background.js ================================================ /* globals chrome, importScripts, Common */ if (typeof importScripts !== 'undefined') { importScripts(chrome.runtime.getURL(`common/common.js`)) } const { modalTypes, transformDefinitions } = Common const definitions = [] async function loadDefinitions() { try { definitions.push( ...(await transformDefinitions( await Promise.all( modalTypes.map(async (type) => ({ type, definitions: JSON.parse( await ( await fetch(chrome.runtime.getURL(`definitions/${type}.json`)) ).text() ), })) ) )) ) } catch (error) { Background.error(error) } } const Background = { async getDefinitions() { const url = this.sender.url // Get custom definitions const { customDefinitions: definitionsByType } = await chrome.storage.sync.get({ customDefinitions: {}, }) const customDefinitions = await transformDefinitions( modalTypes.map((type) => ({ type, definitions: definitionsByType[type] || {}, })) ) return [...definitions, ...customDefinitions] .filter(({ regExps }) => regExps.some((_regExp) => { const regExp = new RegExp(_regExp, 'i') return ( regExp.test(url) || regExp.test(`${url.replace(/^http(s)?:\/\//, 'http$1://www.')}`) ) }) ) .map(({ definitions }) => definitions) .flat() }, setBadge(text) { return chrome.action.setBadgeText({ text: String(text), tabId: this.sender.tab.id, }) }, log(...args) { // eslint-disable-next-line no-console console.log(...args) }, error(error) { // eslint-disable-next-line no-console console.error(error) }, } chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { const { func } = request if (func) { const args = request.args || [] if (request.func === 'log') { // eslint-disable-next-line no-console console.log('content:', ...args) } else if (request.func === 'error') { const [message, stack] = args const error = new Error(`content: ${message}`) error.stack = stack Background.error(error) } else { Promise.resolve(Background[func].call({ request, sender }, ...args)) .then((response) => { // eslint-disable-next-line no-console console.log(`content: ${func}(${args.join(', ')})`, response) sendResponse(response) }) .catch((error) => Background.error(error)) } } return true }) chrome.action.setBadgeBackgroundColor({ color: '#4755b3', }) loadDefinitions().then(() => // eslint-disable-next-line no-console console.log(`init ok: ${definitions.length} definitions`) ) ================================================ FILE: common/common.css ================================================ :root { --color-primary: #667aff; --color-primary-dark: #4755b3; --color-primary-light: #f1f3ff; --color-heading: #333; --color-text: #4a4a4a; --color-grey: #ccc; --color-grey-light: #f4f4f4; --color-white: #fff; --radius: 5px; } *, *::before, *::after { box-sizing: border-box; } * { margin: 0; } body { background-color: var(--color-white); color: var(--color-text); direction: __MSG_@@bidi_dir__; font-family: Helvetica, Arial, sans-serif; font-size: .9rem; line-height: 1.5rem; -webkit-font-smoothing: antialiased; } p { margin-bottom: 1rem; } a { color: var(--color-primary); } input, button, textarea, select { background-color: var(--color-white); font: inherit; } input[type="text"], textarea, select { display: block; padding: .5rem; border: 1px solid var(--color-grey); border-radius: var(--radius); margin-bottom: 1rem; width: 100%; } input[type="checkbox"], input[type="radio"] { margin-right: .5rem; } textarea.code { font-family: Monaco, monospace; font-size: .7rem; line-height: 1.1rem; } h2 { color: var(--color-heading); font-size: .9rem; margin-bottom: .5rem; } table { border-collapse: collapse; border-style: hidden; border-spacing: 0; font-size: .8rem; width: 100%; } th { color: var(--color-heading); text-align: left; } th, td { padding: .25rem .5rem; } th:first-child, td:first-child { border-radius: var(--radius) 0 0 var(--radius); } th:last-child, td:last-child { border-radius: 0 var(--radius) var(--radius) 0; } tr:first-child th, tr:nth-child(even) td { background: var(--color-primary-light); } hr { border-color: var(--color-grey); border-style: solid; border-width: 1px 0 0 0; } .row { display: flex; align-items: center; justify-content: space-between; } .label { font-weight: bold; } .checkbox label { display: flex; align-items: center; margin-bottom: .5rem; } .radio label { display: inline-flex; align-items: center; margin-right: .5rem; } .help { font-size: .8rem; margin-bottom: .5rem; } .input-error { color: #ff5353; font-size: .8rem; margin: -.5rem 0 .5rem 0; } .input-error:empty { margin: 0; } .hidden { display: none; } .icon { color: #fff; height: 1.1rem; vertical-align: middle; width: 1.1rem; } .container { margin: 1rem; } .tabs { border-bottom: 1px solid var(--color-grey); display: flex; } .tab { border-bottom: 2px solid transparent; color: var(--color-primary); cursor: pointer; padding: .8rem 1rem; margin-bottom: -1px; } .tab:hover { border-bottom-color: var(--color-primary); } .tab--active { border-bottom-color: var(--color-primary); } .alert { background-color: #ffffcc; color: #999900; font-size: .8rem; padding: 1rem; } .alert a, .alert a:hover, .alert a:active, .alert a:visited { color: #999900; } ================================================ FILE: common/common.js ================================================ /* globals chrome */ // Manifest v2 polyfill if (chrome.runtime.getManifest().manifest_version === 2) { chrome.action = chrome.browserAction chrome.storage.sync = { get: (...args) => new Promise((resolve) => chrome.storage.local.get(...args, resolve)), set: (...args) => new Promise((resolve) => chrome.storage.local.set(...args, resolve)), } } const Common = { modalTypes: [ 'offer', 'paywall', 'email', 'signup', 'consent', 'donate', 'message', ], $: typeof window !== 'undefined' ? document.querySelector.bind(document) : () => {}, $$: typeof window !== 'undefined' ? document.querySelectorAll.bind(document) : () => {}, isObject(object) { return typeof object === 'object' && object && !Array.isArray(object) }, arrayify(item) { return Array.isArray(item) ? item : [item] }, debounce(func, wait) { let timeout return (...args) => { const debounced = () => { clearTimeout(timeout) func(...args) } clearTimeout(timeout) timeout = setTimeout(debounced, wait) } }, i18n() { const elements = Common.$$('[data-i18n]') elements.forEach((element) => { element.textContent = chrome.i18n.getMessage(element.dataset.i18n) }) }, capitalize(string) { return string.charAt(0).toUpperCase() + string.slice(1) }, removeChildren(element) { while (element.firstChild) { element.removeChild(element.firstChild) } }, el(name) { return document.createElement(name) }, getActiveTab() { return new Promise((resolve) => { chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => resolve(tabs[0]) ) }) }, async tokenify(string) { const tokens = [] let bracketLevel = 0 let quote = false let token = '' for (const char of string.split('')) { token += char if (token === 'if ') { token = '' } switch (char) { case "'": if (!bracketLevel) { quote = !quote if (!quote) { tokens.push(token.replace(/(^'|'$)/g, '')) token = '' } } break case '(': if (!quote) { bracketLevel++ } break case ')': if (!quote) { bracketLevel-- if (!bracketLevel) { const [, func, arg] = token.trim().match(/([^(]+)\((.+)\)$/) if (!Common.Functions[func]) { throw new Error(`Function does not exist: ${func}`) } try { await Common.Functions[func].call({ validateOnly: true }, arg) } catch (error) { throw new Error(`Invalid argument: ${arg}`) } tokens.push({ func, arg }) token = '' } } break } } if (token) { tokens.push(token) } return tokens }, async transformDefinitions(definitionsByType) { const definitionsByGlob = [] for (const { type, definitions } of definitionsByType) { if (!Common.isObject(definitions)) { throw new TypeError( `Unexpected definitions type, expected object: ${definitions}` ) } for (const glob of Object.keys(definitions)) { definitionsByGlob[glob] = definitionsByGlob[glob] || { glob, regExps: Common.globToRegExp(glob), definitions: [], } definitionsByGlob[glob].definitions.push( ...( await Promise.all( Common.arrayify(definitions[glob]).map((definition) => { try { if (!Common.isObject(definition)) { throw new TypeError( `Unexpected definition type, expected object: ${definition}` ) } return Promise.all( Object.keys(definition).map(async (key) => { const conditions = key.startsWith('if ') ? await Common.tokenify(key) : null if (conditions && !conditions.length) { throw new Error(`Invalid condition: ${key}`) } let actions = [] if (conditions) { if (!Common.isObject(definition[key])) { throw new TypeError( `Invalid actions type, expected object: ${definition[key]}` ) } actions = Object.keys(definition[key]).map( (selector) => ({ selector, action: definition[key][selector], }) ) } else { actions = [{ selector: key, action: definition[key] }] } actions = await Promise.all( actions.map(async ({ selector, action }) => { if (typeof action !== 'string') { throw new TypeError( `Unexpected action type, expected string` ) } const [func, ...splitArgs] = action.split(' ') const args = await Common.tokenify( splitArgs.join(' ') ) return { selector, func, args } }) ) actions.forEach(({ selector, func, args }) => { try { Common.$(selector) } catch (error) { throw new Error( `Invalid action selector: ${selector}` ) } if (!Common.Actions[func]) { throw new Error( `Invalid action function: ${func} (${selector})` ) } }) return { type, conditions: conditions || [], actions } }) ) } catch (error) { throw new Error(`${error.message || error} in ${glob}`) } }) ) ).flat() ) } } return Object.values(definitionsByGlob).flat() }, globToRegExp(glob) { const globs = glob.split(' ') return globs.map((glob) => { try { if (glob !== '*') { if (!glob.includes('.')) { throw new Error('Invalid glob') } // eslint-disable-next-line no-new new URL(`https://${glob.replace('*', 'test')}`) } return new RegExp( glob === '*' ? '^https?://.+' : `^https?://${glob.replace('.', '\\.').replace('*', '[^./]+')}\\b` ).source } catch (error) { throw new Error(`Invalid URL pattern: ${glob}`) } }) }, // Run a script in the context of the page inject(func, ...args) { return new Promise((resolve) => { const uid = Math.floor(Math.random() * 900000) + 100000 const script = Common.el('script') script.src = chrome.runtime.getURL('inject/inject.js') script.dataset.demodal = 'true' script.onload = () => { const receiveMessage = ({ data: { demodalResponse } }) => { if (!demodalResponse || demodalResponse.uid !== uid) { return } window.removeEventListener('message', receiveMessage) script.remove() resolve(demodalResponse.message) } window.addEventListener('message', receiveMessage) window.postMessage({ demodalRequest: { func, args, uid } }) } document.body.append(script) }) }, Actions: { remove() { this.remove() return true }, removeParent(level = 1) { let node = this for (let index = 0; index < level; index++) { node = node ? node.parentNode : null } if (node) { node.remove() return true } else { return false } }, removeIf(...args) { if ( args.every((string) => this.textContent.toLowerCase().includes(string.toLowerCase().trim()) ) ) { this.remove() return true } return false }, addClass(...args) { this.classList.add(...args) return true }, removeClass(...args) { if (args[0] === '*') { this.className = '' } else { this.classList.remove(...args) } return true }, addStyle(...args) { this.style = `${this.style}; ${args[0]}` return true }, removeStyle() { this.style = '' return true }, click() { this.click() return true }, call(...args) { Common.inject('call', ...args) return true }, }, Functions: { $(selector) { try { return !!Common.$(selector) } catch (error) { throw new Error(`Invalid selector: ${selector}`) } }, async defined(...args) { if (this.validateOnly) { return true } return await Common.inject('defined', ...args) }, sleep(ms = 0) { if (this.validateOnly) { return true } return new Promise((resolve) => setTimeout(() => resolve(true), parseInt(ms, 10)) ) }, }, Background: { call(func, ...args) { return new Promise((resolve, reject) => chrome.runtime.sendMessage({ func, args }, (response) => chrome.runtime.lastError ? reject(new Error(chrome.runtime.lastError.message)) : resolve(response) ) ) }, }, Content: { call(func, ...args) { return new Promise((resolve, reject) => Common.getActiveTab().then((tab) => chrome.tabs.sendMessage(tab.id, { func, args }, (response) => chrome.runtime.lastError ? reject(new Error(chrome.runtime.lastError.message)) : resolve(response) ) ) ) }, }, } ================================================ FILE: content/content.js ================================================ /* eslint-env browser */ /* globals chrome, Common */ const { $, debounce, Background, Actions, Functions } = Common const definitions = [] const blockedModals = {} // Functions that can be called from the popup script const Content = { getBlockedModals() { return blockedModals }, getUrl() { return window.location.href }, reload() { window.location.reload() }, async isAllowed(url = window.location.href) { // Check if hostname is in allow list const { allowList } = await chrome.storage.sync.get({ allowList: [], }) let { hostname } = new URL(url) hostname = hostname.replace(/^www\./, '') return allowList.includes(hostname) }, } // Log messages in the background script console function log(...messages) { const error = messages[0] if (error instanceof Error) { Background.call('error', error.toString(), error.stack) } else { Background.call('log', ...messages) } } const run = async () => { try { if (await Content.isAllowed()) { return } await Promise.all( definitions.map(async (definition) => { const { type, conditions, actions, completed } = definition if ( completed || !( await Promise.all( conditions.map(({ func, arg }) => Functions[func](arg)) ) ).every((result) => result) ) { return } let found = false actions.forEach(({ selector, func, args }) => { let success = false switch (func) { case 'call': success = Actions[func](selector, ...args) break default: // eslint-disable-next-line no-case-declarations const node = $(selector) if (node) { success = Actions[func].call(node, ...args) } } if (success) { log(`action: ${selector}: ${func}(${args.join(', ')})`) } found = found || success }) if (found) { definition.completed = true blockedModals[type] = (blockedModals[type] || 0) + 1 Background.call( 'setBadge', Object.values(blockedModals).reduce((sum, value) => sum + value, 0) ) // Update all-time totals chrome.storage.sync .get({ blockedModals: {}, }) .then(({ blockedModals }) => { blockedModals[type] = (blockedModals[type] || 0) + 1 chrome.storage.sync.set({ blockedModals }) }) } }) ) } catch (error) { log(error) } } // Listen for messages from popup chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { const { func } = request if (func) { const args = request.args || [] Promise.resolve(Content[func].call({ request, sender }, ...args)) .then((response) => { // eslint-disable-next-line no-console log(`popup: ${func}(${args.join(', ')})`, response) sendResponse(response) }) .catch((error) => log(error)) } return true }) // ;(async () => { try { definitions.push(...(await Background.call('getDefinitions'))) const runDebounced = debounce(() => run(), 500) const mutationObserver = new MutationObserver((mutations) => { // Avoid infinite loop if mutation was done by us if ( mutations.every((mutation) => Array.from(mutation.addedNodes).every((node) => node.dataset.demodal) ) ) { return } runDebounced() }) mutationObserver.observe(document.body, { subtree: true, childList: true }) run() } catch (error) { log(error) } })() ================================================ FILE: definitions/consent.json ================================================ { "*": { "#cookie-law-info-bar": "remove", "#moove_gdpr_cookie_info_bar": "remove", "if defined(BorlabsCookie)": { "BorlabsCookie.hideCookieBox": "call" }, "#cookie-banner": "remove", ".cookie-banner": "remove", "#onetrust-consent-sdk": "remove", "#CybotCookiebotDialogBodyButtonDecline": "click", ".cookie-bar__close": "click", "[aria-label='cookieconsent']": "remove", ".cookie-warning-modal": "remove", ".cookie-consent": "remove", "#cookie-notice": "remove", ".cookie-notice-big": "remove", "#cookie-bar": "remove", "if $(#didomi-host)": { "#didomi-host": "remove", "body": "removeClass didomi-popup-open" }, "#cookieBanner": "remove", "#cconsent-bar": "remove", ".CookieConsent": "remove", "#gdpr-banner": "remove", "#gdpr-cookie-message": "remove", ".cookie-notice": "remove", ".cc-banner": "remove", ".js-cookie-consent-overlay": "remove", "#ccc": "remove", "#iubenda-cs-banner": "remove", ".wppopups-whole": "removeIf 'cookies'", ".gdpr-disclaimer": "remove", "._gdprDisclaimer": "remove", ".ce-cookie-banner": "remove", ".cookie-msg": "remove", ".cookie-panel": "remove", ".cookie-popup": "remove", "#cookie-popup-wrapper": "remove", "[data-component='CookieBanner']": "remove", ".t-consentPrompt": "remove", "#privacy-consent": "remove", "if $(#acceptationCMPWall)": { "#acceptationCMPWall": "remove", "body": "removeStyle" }, ".cookies_message": "remove", "#cookieConsent": "remove", ".cookie-policy": "remove", "#cookieNoticeAlert": "remove" }, "*.stackexchange.com *.superuser.com *.stackoverflow.com *.mathoverflow.net *.serverfault.com *.askubuntu.com *.stackapps.com": { ".js-consent-banner": "remove" }, "*.deepl.com": { "#dl_cookieBanner": "remove" }, "*.lego.com": { ".cookies-used-notice": "remove" }, "*.redhat.com": { "#truste-consent-track": "remove", ".redhat-cookie-banner": "remove" }, "*.openweathermap.org": { "#stick-footer-panel": "remove" } } ================================================ FILE: definitions/donate.json ================================================ { "*.theguardian.com": { ".site-message--banner": "remove", "#bottom-banner": "removeIf 'Support the Guardian'" }, "*.science.org": { ".alert-donation": "remove" } } ================================================ FILE: definitions/email.json ================================================ { "*": { "if $(.elementor-popup-modal)": { ".elementor-popup-modal": "remove", "body": "removeClass dialog-prevent-scroll" }, "if $(.sgpb-popup-dialog-main-div-wrapper input[type='email'])": { ".sgpb-popup-dialog-main-div-wrapper": "remove" }, "if $([aria-modal='true'] .klaviyo-close-form)": { "[aria-modal='true'] .klaviyo-close-form": "click" }, "#shopify-section-newsletter-popup .modal__close": "click", ".close-newsletter": "click", ".cp-popup-container": "removeIf 'subscribe'", ".js_modal_exit_intent": "removeIf 'subscribe'" }, "*.npr.org": { ".newsletter-stickybar": "remove" }, "*.redhat.com": { ".subscribe-sidebar": "remove" } } ================================================ FILE: definitions/message.json ================================================ { "*": { "if $([id^='sp_message_container_'])": { "[id^='sp_message_container_']": "remove", "html": "removeClass sp-message-open" } }, "*.heraldsun.com.au": { ".DialogBox": "remove" }, "*.simplywall.st": { "if $([data-cy-id='careers-upsell'])": { "[data-cy-id='careers-upsell']": "removeParent" } } } ================================================ FILE: definitions/offer.json ================================================ { "*.forbes.com": { "[external-event='offer-close-modal']": "click" }, "*.theconversation.com": { ".promo": "remove" } } ================================================ FILE: definitions/paywall.json ================================================ { "*.nytimes.com": { "if $(#gateway-content)": { "#gateway-content": "remove", "#app > div > div": "removeClass *", "#app > div > div > div:last-child": "remove", "#site-content": "removeStyle" } }, "*.newyorker.com": { ".paywall-bar": "remove", ".paywall-modal": "remove" }, "*.wired.com": { ".persistent-bottom": "remove" }, "*.washingtonpost.com": { "if $(.paywall-overlay) sleep(1000)": { ".paywall-overlay": "remove", "html": "removeStyle", "body": "addStyle 'overflow: inherit; position: inherit;'" }, ".softwall-overlay": "remove", "[id^='softwall-']": "remove" }, "*.simplywall.st": { "if $(#modal-container)": { "#modal-container": "remove", "#root": "addStyle filter: none" } }, "*.theguardian.com": { "#bottom-banner": "removeIf 'Start free trial'" }, "*.bloomberg.com": { "if $(#fortress-paywall-container-root)": { "#fortress-paywall-container-root": "remove", "body": "addStyle overflow: auto" } }, "*.nikkei.com": { ".pw-widget--benefit-pop-up .pianoj-ribbon-close": "click" }, "*.telegraph.co.uk": { ".martech-modal-component-overlay": "remove" }, "*.nationalgeographic.com": { "if $(.EmailStickyFooter__Modal)": { ".EmailStickyFooter__Modal": "remove", ".Scroll--locked": "removeClass Scroll--locked", "body": "removeStyle" } } } ================================================ FILE: definitions/signup.json ================================================ { "*": { ".wbounce-modal": "removeIf 'newsletter'", ".leadinModal": "remove", ".wppopups-whole": "removeIf 'sign up'", ".exit-intent": "removeIf 'sign up'", ".exit-intent-modal": "remove", "[id^='smsbump-form']": "remove", "#ouibounce-modal": "remove" }, "*.smh.com.au *.theage.com.au": { "[data-testid='registration-prompt']": "remove", "#registrationWall": "remove" }, "*.ishka.com.au": { "if $(.sign_up_karma_club_popup) defined(CloseClubPoup)": { "CloseClubPoup": "call" } }, "*.nytimes.com": { ".MAG_web_all_Monthly-Sale-dock": "remove" }, "*.theatlantic.com": { "#paywall[data-category^='nudge']": "remove" }, "*.bloomberg.com": { "#fortress-preblocked-container-root": "remove" }, "*.ieee.org": { ".lightbox-popup": "removeIf 'create an account'" }, "*.afr.com": { "if $([data-testid='SubscriptionPrompt-close'])": { "[data-testid='SubscriptionPrompt-close']": "click" } }, "*.pinterest.com *.pinterest.com.au": { "if $([data-test-id='giftWrap'])": { "[data-test-id='giftWrap']": "remove", "body": "removeStyle" } }, "*.vic.gov.au": { "#subscribe-banner-react": "remove" }, "*.nature.com": { ".c-site-messages--nature-briefing-email-variant": "remove" }, "*.boredpanda.com": { "if $(#subscribe-before-leaving)": { "#subscribe-before-leaving": "remove", "#modal-backdrop": "remove", "body": "removeClass modal-open" } }, "*.bbc.com": { "if $(.tp-modal-open)": { "body": "removeClass tp-modal-open", ".tp-modal": "remove", ".tp-backdrop": "remove" } }, "*.ibtimes.com": { ".grwf2_backdrop": "remove", ".wf2-popover": "remove" }, "vk.com": { "#page_bottom_banners_root": "remove", "if $(.UnauthActionBox)": { ".UnauthActionBox__close": "click" } } } ================================================ FILE: inject/inject.js ================================================ /* eslint-env browser */ ;(function () { try { const chainToProp = (chain) => { return chain .split('.') .reduce( (value, method) => value && value instanceof Object && Object.prototype.hasOwnProperty.call(value, method) ? value[method] : undefined, window ) } const Functions = { defined(chain) { return chainToProp(chain) !== undefined }, call(chain, ...args) { chainToProp(chain)?.(...args) }, } const receiveMessage = ({ data: { demodalRequest } }) => { if (!demodalRequest) { return } const { func, args, uid } = demodalRequest removeEventListener('message', receiveMessage) postMessage({ demodalResponse: { uid, message: func && Functions[func] ? Functions[func](...(args || [])) : false, }, }) } addEventListener('message', receiveMessage) } catch (error) { // Fail quietly } })() ================================================ FILE: manifest-v2.json ================================================ { "name": "Demodal - Block modals and overlays", "description": "Demodal automatically removes content blocking modals including paywalls, discount offers, promts to sign up or enter your email address and more.", "version": "1.0.3", "manifest_version": 2, "default_locale": "en", "background": { "page": "background/background.html" }, "content_scripts": [ { "matches": ["https://*/*", "http://*/*"], "js": [ "common/common.js", "content/content.js" ] } ], "permissions": ["storage", "activeTab"], "browser_action": { "default_popup": "popup/popup.html", "default_icon": { "128": "images/icon-128.png", "256": "images/icon-256.png" } }, "options_ui":{ "page": "options/options.html", "open_in_tab": true }, "icons": { "128": "images/icon-128.png", "256": "images/icon-256.png" } } ================================================ FILE: manifest-v3.json ================================================ { "name": "Demodal - Block modals and overlays", "description": "Demodal automatically removes content blocking modals including paywalls, discount offers, email address prompts and more.", "version": "1.0.3", "manifest_version": 3, "default_locale": "en", "background": { "service_worker": "background/background.js" }, "content_scripts": [ { "matches": ["https://*/*", "http://*/*"], "js": [ "common/common.js", "content/content.js" ] } ], "permissions": ["storage", "activeTab", "scripting"], "action": { "default_popup": "popup/popup.html", "default_icon": { "128": "images/icon-128.png", "256": "images/icon-256.png" } }, "options_page": "options/options.html", "icons": { "128": "images/icon-128.png", "256": "images/icon-256.png" } } ================================================ FILE: options/options.css ================================================ .container { max-width: 600px; margin-left: auto; margin-right: auto; } ================================================ FILE: options/options.html ================================================

================================================ FILE: options/options.js ================================================ /* globals chrome, Common */ const { $, i18n, el, capitalize, modalTypes } = Common i18n() // ;(async () => { const { optionBlockModalTypes } = await chrome.storage.sync.get({ optionBlockModalTypes: modalTypes.reduce( (options, type) => ({ ...options, [type]: true }), {} ), }) modalTypes.forEach((type) => { const div1 = el('div') const label = el('label') const checkbox = el('input') const div2 = el('div') div1.className = 'checkbox' checkbox.type = 'checkbox' checkbox.checked = optionBlockModalTypes[type] checkbox.addEventListener('change', () => { optionBlockModalTypes[type] = checkbox.checked chrome.storage.sync.set({ optionBlockModalTypes }) }) div2.textContent = chrome.i18n.getMessage( `modalType${capitalize(type)}Help` ) div2.className = 'help' label.append( checkbox, chrome.i18n.getMessage(`modalType${capitalize(type)}`) ) div1.append(label, div2) $('#modal-types').append(div1) }) })() ================================================ FILE: package.json ================================================ { "devDependencies": { "@nuxtjs/eslint-config": "^3.1.0", "@nuxtjs/eslint-module": "^2.0.0", "babel-eslint": "^10.1.0", "eslint": "^7.13.0", "eslint-config-prettier": "^6.15.0", "eslint-plugin-json": "^2.1.2", "eslint-plugin-nuxt": "^1.0.0", "eslint-plugin-prettier": "^3.1.4", "prettier": "^2.1.2" } } ================================================ FILE: popup/popup.css ================================================ body { width: 500px; } .header { background-color: var(--color-primary); display: flex; align-items: center; padding: 1rem; } .header__logo { margin-right: .8rem; width: 24px; } .header__title { color: var(--color-white); font-size: .9rem; text-transform: uppercase; } .header__links { text-align: right; flex: 1 0; } .content { opacity: .5; } .content.visible { opacity: 1; } #input-definitions { height: 300px; } #input-debug { height: 300px; } ================================================ FILE: popup/popup.html ================================================

Demodal


================================================ FILE: popup/popup.js ================================================ /* globals chrome, Common */ const { $, $$, i18n, capitalize, removeChildren, el, debounce, modalTypes, transformDefinitions, Content, } = Common function renderTotals(prefix, totals) { let any = false removeChildren($(`${prefix}__stats tbody`)) modalTypes.forEach((type) => { const tr = el('tr') const td1 = el('td') const td2 = el('td') if (totals[type]) { td1.textContent = chrome.i18n.getMessage(`modalType${capitalize(type)}`) td2.textContent = totals[type] tr.appendChild(td1) tr.appendChild(td2) $(`${prefix}__stats tbody`).appendChild(tr) any = true } }) if (any) { $(`${prefix}__stats`).classList.remove('hidden') $(`${prefix}__missing`).classList.add('hidden') } } i18n() // ;(async () => { $('.content').classList.remove('visible') $(`#blocked-page__stats`).classList.add('hidden') $(`#blocked-page__missing`).classList.remove('hidden') $(`#blocked-total__stats`).classList.add('hidden') $(`#blocked-total__missing`).classList.remove('hidden') $$('.tab').forEach((tab) => tab.addEventListener('click', (e) => { $$('.tab').forEach((tab) => tab.classList.remove('tab--active')) e.target.classList.add('tab--active') $$('[data-tab-content]').forEach((tabContent) => tabContent.classList.add('hidden') ) $(`[data-tab-content='${e.target.dataset.tab}']`).classList.remove( 'hidden' ) }) ) // Blocked modals tab $('.link-options').addEventListener('click', (e) => { e.preventDefault() chrome.runtime.openOptionsPage() }) // Show all-time totals const { blockedModals: totals } = await chrome.storage.sync.get({ blockedModals: {}, }) renderTotals('#blocked-total', totals) let connected = true try { // Show page totals const blockedModals = await Content.call('getBlockedModals') renderTotals('#blocked-page', blockedModals) } catch (error) { // eslint-disable-next-line no-console console.error(error) connected = false } // Show page specific content only if we have a content script connection $$('.if-connected').forEach((element) => element.classList[connected ? 'remove' : 'add']('hidden') ) if (connected) { // Add/remove hostnames to allow list const allowed = await Content.call('isAllowed') $('#input-allowed').checked = allowed $('#input-allowed').addEventListener('click', async (el) => { const { allowList } = await chrome.storage.sync.get({ allowList: [], }) const url = await Content.call('getUrl') let { hostname } = new URL(url) hostname = hostname.replace(/^www\./, '') if (el.target.checked) { allowList.push(hostname) } else { const index = allowList.findIndex((_hostname) => _hostname === hostname) if (index !== -1) { allowList.splice(index, 1) } } chrome.storage.sync.set({ allowList }) Content.call('reload') }) } // Definitions tab // Modal type select modalTypes.forEach((type) => { const option = el('option') option.value = type option.textContent = chrome.i18n.getMessage(`modalType${capitalize(type)}`) $('#input-modal-types').append(option) }) $('#input-modal-types').addEventListener('change', (event) => { const modalType = event.target.value $('#errors-definitions').textContent = '' chrome.storage.sync .get({ customDefinitions: {}, }) .then( ({ customDefinitions }) => ($('#input-definitions').value = JSON.stringify( customDefinitions[modalType] || {}, null, 2 )) ) }) $('#input-modal-types').dispatchEvent(new Event('change')) // Edit / debug toggle $$('input[name="input-mode"]').forEach((el) => el.addEventListener('change', (event) => { const debug = event.target.value === 'debug' $('#input-definitions').classList[debug ? 'add' : 'remove']('hidden') $('#input-debug').classList[debug ? 'remove' : 'add']('hidden') }) ) // Reload page $('.reload').addEventListener('click', () => Content.call('reload')) // Format JSON on blur $('#input-definitions').addEventListener('blur', (event) => { const json = event.target.value || '{}' try { const definitions = JSON.parse(json) event.target.value = JSON.stringify(definitions, null, 2) const selectedType = $('#input-modal-types').value chrome.storage.sync .get({ customDefinitions: modalTypes.reduce( (definitions, type) => ({ ...definitions, [type]: {} }), {} ), }) .then(({ customDefinitions }) => chrome.storage.sync.set({ customDefinitions: { ...customDefinitions, [selectedType]: definitions, }, }) ) } catch (error) { // eslint-disable-next-line no-console console.error(error) } }) // Validate JSON on change $('#input-definitions').addEventListener( 'input', debounce((event) => { const json = event.target.value || '{}' try { const definitionsByType = [ { type: 'offers', definitions: JSON.parse(json), }, ] const definitions = transformDefinitions(definitionsByType) $('#input-debug').textContent = JSON.stringify(definitions, null, 2) $('#errors-definitions').textContent = '' } catch (error) { // eslint-disable-next-line no-console console.error(error) $('#errors-definitions').textContent = error.message || error.toString() } }, 500) ) $('.content').classList.add('visible') })() ================================================ FILE: run ================================================ #!/bin/bash case $1 in manifest) if [[ -f manifest-$2.json ]]; then cat manifest-$2.json > manifest.json echo Switched to manifest $2 else echo Invalid argument $2 fi ;; build) version=$2 if [ -z "$version" ]; then echo "No version specified" exit fi sed -i '' -r "s/\"version\": \".+\"/\"version\": \"$2\"/" manifest-v2.json sed -i '' -r "s/\"version\": \".+\"/\"version\": \"$2\"/" manifest-v3.json find . -name '.DS_Store' -type f -delete files="_locales background common content definitions images inject options popup manifest.json" cp manifest.json manifest.json.bak cat manifest-v2.json > manifest.json rm build/* zip -r build/demodal-v2.zip $files cat manifest-v3.json > manifest.json zip -r build/demodal-v3.zip $files mv manifest.json.bak manifest.json ;; *) echo Invalid argument $1 ;; esac