├── FUNDING.yml ├── nsf.png ├── plt_logo.png ├── sloan_logo.jpg ├── src ├── assets │ ├── icon.psd │ ├── cat-w-text │ │ ├── check1.png │ │ ├── cross1.png │ │ └── optmeow-logo-circle.png │ ├── face-icons │ │ ├── icon64-face-circle.png │ │ ├── icon128-face-circle.png │ │ ├── optmeow-face-circle-green-128.png │ │ ├── optmeow-face-circle-yellow-128.png │ │ └── optmeow-face-circle-green-ring-128.png │ ├── chrome-firefox-store-assets │ │ ├── web-store-icon-2.png │ │ ├── web-store-icon.png │ │ ├── screenshot-3-chrome.png │ │ ├── screenshot-3-firefox.png │ │ ├── screenshot-4-chrome.jpg │ │ ├── screenshot-4-firefox.png │ │ ├── screenshot-5-chrome.jpg │ │ ├── screenshot-5-firefox.png │ │ ├── large-promo-tile-920-680.png │ │ ├── optmeowt-promo-440x280-1.png │ │ ├── optmeowt-promo-440x280-2.png │ │ ├── optmeowt-promo-920x680-1.png │ │ ├── optmeowt-promo-920x680-2.png │ │ └── marquee-promo-tile-1400-560.png │ ├── chevron-up.svg │ ├── chevron-down.svg │ ├── question-mark.svg │ ├── pause-circle-outline.svg │ ├── shield-outline.svg │ ├── flash-outline.svg │ ├── play-circle-outline.svg │ ├── options-2-outline.svg │ └── gpc-logo.svg ├── data │ ├── headers.js │ └── defaultSettings.js ├── rules │ ├── gpc_exceptions_rules.json │ └── universal_gpc_rules.json ├── common │ ├── settings.js │ ├── editRules.js │ └── editDomainlist.js ├── options │ ├── options.html │ ├── options.js │ ├── views │ │ ├── about-view │ │ │ ├── about-view.js │ │ │ └── about-view.html │ │ ├── main-view │ │ │ ├── main-view.html │ │ │ └── main-view.js │ │ ├── domainlist-view │ │ │ ├── domainlist-view.html │ │ │ └── domainlist-view.js │ │ └── settings-view │ │ │ ├── settings-view.html │ │ │ └── settings-view.js │ ├── components │ │ ├── scaffold-component.html │ │ └── util.js │ ├── dark-mode.css │ └── styles.css ├── content-scripts │ ├── injection │ │ └── gpc-dom.js │ ├── registration │ │ └── gpc-dom.js │ └── contentScript.js ├── manifests │ ├── chrome │ │ ├── manifest-dev.json │ │ └── manifest-dist.json │ └── firefox │ │ ├── manifest-dev.json │ │ └── manifest-dist.json ├── background │ ├── protection │ │ ├── background.js │ │ ├── listeners-chrome.js │ │ ├── listeners-firefox.js │ │ ├── protection-ff.js │ │ └── protection.js │ ├── control.js │ └── storage.js ├── theme │ └── darkmode.js └── popup │ ├── styles.css │ └── popup.html ├── wesleyan_shield.png ├── ui-mockup ├── Mockup v1.0.xd ├── Research.pdf ├── Mockup v1.0.pdf ├── Persona User Profile.pdf └── Popup designs │ ├── Popup.xd │ ├── 5. Flat Final.pdf │ ├── 5. Flat Final.png │ ├── 1. Current version.pdf │ ├── 1. Current version.png │ ├── 2. Cards UI & Flat UI.pdf │ ├── 2. Cards UI & Flat UI.png │ ├── 3. Flat UI w Simple status.pdf │ ├── 3. Flat UI w Simple status.png │ ├── 4. Flat UI w Expanded status.pdf │ └── 4. Flat UI w Expanded status.png ├── chrome-web-store-badge.png ├── firefox-add-ons-badge.png ├── research ├── wijayaEtAlCrawlForGPC2024Poster.pdf ├── aggarwalEtAlInvisibleThreat2023Poster.pdf └── tassoneEtAlEnhancingOnlinePrivacy2022Poster.pdf ├── .gitignore ├── .github └── workflows │ └── node.js.yml ├── LICENSE.md ├── package.json ├── test └── background │ ├── gpc.test.js │ └── cookieRemoval.test.js ├── webpack.config.js └── README_ARCHITECTURE.md /FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: privacy-tech-lab 2 | -------------------------------------------------------------------------------- /nsf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privacy-tech-lab/gpc-optmeowt/HEAD/nsf.png -------------------------------------------------------------------------------- /plt_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privacy-tech-lab/gpc-optmeowt/HEAD/plt_logo.png -------------------------------------------------------------------------------- /sloan_logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privacy-tech-lab/gpc-optmeowt/HEAD/sloan_logo.jpg -------------------------------------------------------------------------------- /src/assets/icon.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privacy-tech-lab/gpc-optmeowt/HEAD/src/assets/icon.psd -------------------------------------------------------------------------------- /wesleyan_shield.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privacy-tech-lab/gpc-optmeowt/HEAD/wesleyan_shield.png -------------------------------------------------------------------------------- /ui-mockup/Mockup v1.0.xd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privacy-tech-lab/gpc-optmeowt/HEAD/ui-mockup/Mockup v1.0.xd -------------------------------------------------------------------------------- /ui-mockup/Research.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privacy-tech-lab/gpc-optmeowt/HEAD/ui-mockup/Research.pdf -------------------------------------------------------------------------------- /chrome-web-store-badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privacy-tech-lab/gpc-optmeowt/HEAD/chrome-web-store-badge.png -------------------------------------------------------------------------------- /firefox-add-ons-badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privacy-tech-lab/gpc-optmeowt/HEAD/firefox-add-ons-badge.png -------------------------------------------------------------------------------- /ui-mockup/Mockup v1.0.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privacy-tech-lab/gpc-optmeowt/HEAD/ui-mockup/Mockup v1.0.pdf -------------------------------------------------------------------------------- /src/assets/cat-w-text/check1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privacy-tech-lab/gpc-optmeowt/HEAD/src/assets/cat-w-text/check1.png -------------------------------------------------------------------------------- /src/assets/cat-w-text/cross1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privacy-tech-lab/gpc-optmeowt/HEAD/src/assets/cat-w-text/cross1.png -------------------------------------------------------------------------------- /ui-mockup/Persona User Profile.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privacy-tech-lab/gpc-optmeowt/HEAD/ui-mockup/Persona User Profile.pdf -------------------------------------------------------------------------------- /ui-mockup/Popup designs/Popup.xd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privacy-tech-lab/gpc-optmeowt/HEAD/ui-mockup/Popup designs/Popup.xd -------------------------------------------------------------------------------- /ui-mockup/Popup designs/5. Flat Final.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privacy-tech-lab/gpc-optmeowt/HEAD/ui-mockup/Popup designs/5. Flat Final.pdf -------------------------------------------------------------------------------- /ui-mockup/Popup designs/5. Flat Final.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privacy-tech-lab/gpc-optmeowt/HEAD/ui-mockup/Popup designs/5. Flat Final.png -------------------------------------------------------------------------------- /research/wijayaEtAlCrawlForGPC2024Poster.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privacy-tech-lab/gpc-optmeowt/HEAD/research/wijayaEtAlCrawlForGPC2024Poster.pdf -------------------------------------------------------------------------------- /src/assets/face-icons/icon64-face-circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privacy-tech-lab/gpc-optmeowt/HEAD/src/assets/face-icons/icon64-face-circle.png -------------------------------------------------------------------------------- /src/assets/cat-w-text/optmeow-logo-circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privacy-tech-lab/gpc-optmeowt/HEAD/src/assets/cat-w-text/optmeow-logo-circle.png -------------------------------------------------------------------------------- /src/assets/face-icons/icon128-face-circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privacy-tech-lab/gpc-optmeowt/HEAD/src/assets/face-icons/icon128-face-circle.png -------------------------------------------------------------------------------- /ui-mockup/Popup designs/1. Current version.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privacy-tech-lab/gpc-optmeowt/HEAD/ui-mockup/Popup designs/1. Current version.pdf -------------------------------------------------------------------------------- /ui-mockup/Popup designs/1. Current version.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privacy-tech-lab/gpc-optmeowt/HEAD/ui-mockup/Popup designs/1. Current version.png -------------------------------------------------------------------------------- /ui-mockup/Popup designs/2. Cards UI & Flat UI.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privacy-tech-lab/gpc-optmeowt/HEAD/ui-mockup/Popup designs/2. Cards UI & Flat UI.pdf -------------------------------------------------------------------------------- /ui-mockup/Popup designs/2. Cards UI & Flat UI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privacy-tech-lab/gpc-optmeowt/HEAD/ui-mockup/Popup designs/2. Cards UI & Flat UI.png -------------------------------------------------------------------------------- /research/aggarwalEtAlInvisibleThreat2023Poster.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privacy-tech-lab/gpc-optmeowt/HEAD/research/aggarwalEtAlInvisibleThreat2023Poster.pdf -------------------------------------------------------------------------------- /ui-mockup/Popup designs/3. Flat UI w Simple status.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privacy-tech-lab/gpc-optmeowt/HEAD/ui-mockup/Popup designs/3. Flat UI w Simple status.pdf -------------------------------------------------------------------------------- /ui-mockup/Popup designs/3. Flat UI w Simple status.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privacy-tech-lab/gpc-optmeowt/HEAD/ui-mockup/Popup designs/3. Flat UI w Simple status.png -------------------------------------------------------------------------------- /research/tassoneEtAlEnhancingOnlinePrivacy2022Poster.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privacy-tech-lab/gpc-optmeowt/HEAD/research/tassoneEtAlEnhancingOnlinePrivacy2022Poster.pdf -------------------------------------------------------------------------------- /src/assets/face-icons/optmeow-face-circle-green-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privacy-tech-lab/gpc-optmeowt/HEAD/src/assets/face-icons/optmeow-face-circle-green-128.png -------------------------------------------------------------------------------- /src/assets/face-icons/optmeow-face-circle-yellow-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privacy-tech-lab/gpc-optmeowt/HEAD/src/assets/face-icons/optmeow-face-circle-yellow-128.png -------------------------------------------------------------------------------- /ui-mockup/Popup designs/4. Flat UI w Expanded status.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privacy-tech-lab/gpc-optmeowt/HEAD/ui-mockup/Popup designs/4. Flat UI w Expanded status.pdf -------------------------------------------------------------------------------- /ui-mockup/Popup designs/4. Flat UI w Expanded status.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privacy-tech-lab/gpc-optmeowt/HEAD/ui-mockup/Popup designs/4. Flat UI w Expanded status.png -------------------------------------------------------------------------------- /src/assets/chrome-firefox-store-assets/web-store-icon-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privacy-tech-lab/gpc-optmeowt/HEAD/src/assets/chrome-firefox-store-assets/web-store-icon-2.png -------------------------------------------------------------------------------- /src/assets/chrome-firefox-store-assets/web-store-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privacy-tech-lab/gpc-optmeowt/HEAD/src/assets/chrome-firefox-store-assets/web-store-icon.png -------------------------------------------------------------------------------- /src/assets/face-icons/optmeow-face-circle-green-ring-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privacy-tech-lab/gpc-optmeowt/HEAD/src/assets/face-icons/optmeow-face-circle-green-ring-128.png -------------------------------------------------------------------------------- /src/assets/chrome-firefox-store-assets/screenshot-3-chrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privacy-tech-lab/gpc-optmeowt/HEAD/src/assets/chrome-firefox-store-assets/screenshot-3-chrome.png -------------------------------------------------------------------------------- /src/assets/chrome-firefox-store-assets/screenshot-3-firefox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privacy-tech-lab/gpc-optmeowt/HEAD/src/assets/chrome-firefox-store-assets/screenshot-3-firefox.png -------------------------------------------------------------------------------- /src/assets/chrome-firefox-store-assets/screenshot-4-chrome.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privacy-tech-lab/gpc-optmeowt/HEAD/src/assets/chrome-firefox-store-assets/screenshot-4-chrome.jpg -------------------------------------------------------------------------------- /src/assets/chrome-firefox-store-assets/screenshot-4-firefox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privacy-tech-lab/gpc-optmeowt/HEAD/src/assets/chrome-firefox-store-assets/screenshot-4-firefox.png -------------------------------------------------------------------------------- /src/assets/chrome-firefox-store-assets/screenshot-5-chrome.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privacy-tech-lab/gpc-optmeowt/HEAD/src/assets/chrome-firefox-store-assets/screenshot-5-chrome.jpg -------------------------------------------------------------------------------- /src/assets/chrome-firefox-store-assets/screenshot-5-firefox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privacy-tech-lab/gpc-optmeowt/HEAD/src/assets/chrome-firefox-store-assets/screenshot-5-firefox.png -------------------------------------------------------------------------------- /src/assets/chrome-firefox-store-assets/large-promo-tile-920-680.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privacy-tech-lab/gpc-optmeowt/HEAD/src/assets/chrome-firefox-store-assets/large-promo-tile-920-680.png -------------------------------------------------------------------------------- /src/assets/chrome-firefox-store-assets/optmeowt-promo-440x280-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privacy-tech-lab/gpc-optmeowt/HEAD/src/assets/chrome-firefox-store-assets/optmeowt-promo-440x280-1.png -------------------------------------------------------------------------------- /src/assets/chrome-firefox-store-assets/optmeowt-promo-440x280-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privacy-tech-lab/gpc-optmeowt/HEAD/src/assets/chrome-firefox-store-assets/optmeowt-promo-440x280-2.png -------------------------------------------------------------------------------- /src/assets/chrome-firefox-store-assets/optmeowt-promo-920x680-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privacy-tech-lab/gpc-optmeowt/HEAD/src/assets/chrome-firefox-store-assets/optmeowt-promo-920x680-1.png -------------------------------------------------------------------------------- /src/assets/chrome-firefox-store-assets/optmeowt-promo-920x680-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privacy-tech-lab/gpc-optmeowt/HEAD/src/assets/chrome-firefox-store-assets/optmeowt-promo-920x680-2.png -------------------------------------------------------------------------------- /src/assets/chrome-firefox-store-assets/marquee-promo-tile-1400-560.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privacy-tech-lab/gpc-optmeowt/HEAD/src/assets/chrome-firefox-store-assets/marquee-promo-tile-1400-560.png -------------------------------------------------------------------------------- /src/assets/chevron-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Local user files 2 | .DS_Store 3 | .idea 4 | .vscode 5 | 6 | # Node.js 7 | node_modules 8 | 9 | # Distribution files 10 | dist 11 | dev 12 | 13 | # local env files 14 | .env.local 15 | .env.*.local 16 | 17 | # Log files 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /src/assets/chevron-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/question-mark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/pause-circle-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/shield-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/flash-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/play-circle-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/options-2-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/data/headers.js: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md 3 | privacy-tech-lab, https://privacytechlab.org/ 4 | */ 5 | 6 | /* 7 | headers.js 8 | ================================================================================ 9 | headers.js exports all opt-out headers to be attached per request 10 | */ 11 | 12 | // headers must contain a name and a value 13 | export const headers = { 14 | "Sec-GPC": { 15 | name: "Sec-GPC", 16 | value: "1", 17 | }, 18 | "Disable-Topics": { 19 | name: 'permissions-policy', 20 | value: 'interest-cohort=()' 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /src/data/defaultSettings.js: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md 3 | privacy-tech-lab, https://privacytechlab.org/ 4 | */ 5 | 6 | /* 7 | defaultSettings.js 8 | ================================================================================ 9 | defaultSettings.js exports the default global extension settings 10 | */ 11 | 12 | export const defaultSettings = { 13 | BROWSER: "$BROWSER", 14 | DOMAINLIST_PRESSED: false, 15 | IS_DOMAINLISTED: false, 16 | IS_ENABLED: true, 17 | TUTORIAL_SHOWN: true, 18 | REQUEST_PERMISSIONS_SHOWN: false, 19 | TUTORIAL_SHOWN_IN_POPUP: true, 20 | WELLKNOWN_CHECK_ENABLED: true, 21 | }; 22 | -------------------------------------------------------------------------------- /src/rules/gpc_exceptions_rules.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 2, 4 | "priority": 1, 5 | "action": { 6 | "type": "modifyHeaders", 7 | "requestHeaders": [ 8 | { "header": "Sec-GPC", "operation": "remove" }, 9 | { "header": "Permissions-Policy", "operation": "remove"} 10 | ] 11 | }, 12 | "condition": { 13 | "urlFilter": "https://example.com", 14 | "resourceTypes": [ 15 | "main_frame", 16 | "sub_frame", 17 | "stylesheet", 18 | "script", 19 | "image", 20 | "font", 21 | "object", 22 | "xmlhttprequest", 23 | "ping", 24 | "csp_report", 25 | "media", 26 | "websocket", 27 | "other" 28 | ] 29 | } 30 | } 31 | ] 32 | -------------------------------------------------------------------------------- /src/common/settings.js: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md 3 | privacy-tech-lab, https://privacytechlab.org/ 4 | */ 5 | 6 | /* 7 | settings.js 8 | ================================================================================ 9 | Shared helpers related to extension settings. 10 | */ 11 | 12 | import { storage, stores } from "../background/storage.js"; 13 | 14 | /** 15 | * Returns whether the well-known check is enabled. 16 | * Defaults to true unless explicitly disabled. 17 | * @returns {Promise} 18 | */ 19 | export async function isWellknownCheckEnabled() { 20 | const enabled = await storage.get(stores.settings, "WELLKNOWN_CHECK_ENABLED"); 21 | return enabled !== false; 22 | } 23 | -------------------------------------------------------------------------------- /src/rules/universal_gpc_rules.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1, 4 | "priority": 1, 5 | "action": { 6 | "type": "modifyHeaders", 7 | "requestHeaders": [ 8 | { "header": "Sec-GPC", "operation": "set", "value": "1" }, 9 | { "header": "Permissions-Policy", "operation": "set", "value": "browsing-topics=()"} 10 | ] 11 | }, 12 | "condition": { 13 | "urlFilter": "*", 14 | "resourceTypes": [ 15 | "main_frame", 16 | "sub_frame", 17 | "stylesheet", 18 | "script", 19 | "image", 20 | "font", 21 | "object", 22 | "xmlhttprequest", 23 | "ping", 24 | "csp_report", 25 | "media", 26 | "websocket", 27 | "other" 28 | ] 29 | } 30 | } 31 | ] 32 | -------------------------------------------------------------------------------- /src/options/options.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 11 | 12 | 16 | 17 | 18 | 19 | OptMeowt 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Tests 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [16.x, 18.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'npm' 29 | - run: npm ci 30 | - run: npm test 31 | -------------------------------------------------------------------------------- /src/options/options.js: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md 3 | privacy-tech-lab, https://privacytechlab.org/ 4 | */ 5 | 6 | /* 7 | options.js 8 | ================================================================================ 9 | options.js starts the process of rendering the main options page 10 | */ 11 | 12 | import { mainView } from "./views/main-view/main-view.js"; 13 | 14 | // CSS TO JS IMPORTS 15 | import "../../node_modules/uikit/dist/css/uikit.min.css"; 16 | import "../../node_modules/animate.css/animate.min.css"; 17 | import "./styles.css"; 18 | 19 | // HTML TO JS IMPORTS - TOP OF `popup.html` 20 | import "../../node_modules/uikit/dist/js/uikit.js"; 21 | import "../../node_modules/uikit/dist/js/uikit-icons.js"; 22 | import "../../node_modules/mustache/mustache.js"; 23 | import "../../node_modules/@popperjs/core/dist/umd/popper.js"; 24 | import "../../node_modules/tippy.js/dist/tippy-bundle.umd.js"; 25 | 26 | /** 27 | * Intializes scripts that build the options page 28 | */ 29 | document.addEventListener("DOMContentLoaded", (event) => { 30 | mainView(); // check event 31 | }); 32 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 privacy-tech-lab 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/content-scripts/injection/gpc-dom.js: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md 3 | privacy-tech-lab, https://privacytechlab.org/ 4 | */ 5 | 6 | /* 7 | injection/gpc-dom.js 8 | ================================================================================ 9 | injection/gpc-dom.js is the static script injected by a related 10 | content script (registered from the extension service worker) for full DOM access 11 | */ 12 | 13 | /** 14 | * Sets Global Privacy Control (GPC) JavaScript property on the DOM 15 | */ 16 | function setDomSignal() { 17 | try { 18 | var GPCVal = true; 19 | 20 | const GPCDomVal = `Object.defineProperties(Navigator.prototype, 21 | { "globalPrivacyControl": { 22 | get: () => ${GPCVal}, 23 | configurable: true, 24 | enumerable: true 25 | }}); 26 | document.currentScript.parentElement.removeChild(document.currentScript); 27 | `; 28 | 29 | const GPCDomElem = document.createElement("script"); 30 | GPCDomElem.innerHTML = GPCDomVal; 31 | document.documentElement.prepend(GPCDomElem); 32 | 33 | } catch (e) { 34 | console.error(`Failed to set DOM signal: ${e}`); 35 | } 36 | } 37 | 38 | setDomSignal(); 39 | -------------------------------------------------------------------------------- /src/options/views/about-view/about-view.js: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md 3 | privacy-tech-lab, https://privacytechlab.org/ 4 | */ 5 | 6 | /* 7 | about-view.js 8 | ================================================================================ 9 | about-view.js loads about-view.html when clicked on the options page 10 | */ 11 | 12 | import { renderParse, fetchParse } from "../../components/util.js"; 13 | 14 | /** 15 | * @typedef headings 16 | * @property {string} headings.title - Title of the given page 17 | * @property {string} headings.subtitle - Subtitle of the given page 18 | */ 19 | const headings = { 20 | title: "About", 21 | subtitle: "Learn more about OptMeowt", 22 | }; 23 | 24 | /** 25 | * Renders the `About` view in the options page 26 | * @param {string} scaffoldTemplate - stringified HTML template 27 | */ 28 | export async function aboutView(scaffoldTemplate) { 29 | const body = renderParse(scaffoldTemplate, headings, "scaffold-component"); 30 | let content = await fetchParse( 31 | "./views/about-view/about-view.html", 32 | "about-view" 33 | ); 34 | 35 | document.getElementById("content").innerHTML = body.innerHTML; 36 | document.getElementById("scaffold-component-body").innerHTML = 37 | content.innerHTML; 38 | } 39 | -------------------------------------------------------------------------------- /src/options/components/scaffold-component.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 11 | 12 | 18 | 35 | -------------------------------------------------------------------------------- /src/content-scripts/registration/gpc-dom.js: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md 3 | privacy-tech-lab, https://privacytechlab.org/ 4 | */ 5 | 6 | /* 7 | registration/gpc-dom.js 8 | ================================================================================ 9 | registration/gpc-dom.js is the content script, registered via the 10 | extension service worker, that injects another static script to provide full 11 | DOM access and permissions 12 | */ 13 | 14 | // High Level: 15 | // Static script that will be injected onto a page to allow it full access, not 16 | // an isolated-world access (see Chrome extension API docs on isolated worlds). 17 | // Necessary to inject the GPC JS property on a page via full DOM permission. 18 | 19 | // Requirements: 20 | // - INJECTION_SCRIPT must also be defined under "web_accessible_resources" 21 | // - This url must be a (semi) absolute path from the compiled project to the script 22 | // (Please see webpack output file directory structure) 23 | // - This script must be registered from the extension service worker w/ same URL 24 | const INJECTION_SCRIPT = "content-scripts/injection/gpc-dom.js"; 25 | 26 | // Based on 27 | // https://stackoverflow.com/questions/9515704/use-a-content-script-to-access-the-page-context-variables-and-functions 28 | function injectStaticScript() { 29 | let s = document.createElement("script"); 30 | s.src = chrome.runtime.getURL(INJECTION_SCRIPT); 31 | s.online = function () { 32 | this.remove(); 33 | }; 34 | document.documentElement.prepend(s); 35 | } 36 | injectStaticScript(); 37 | -------------------------------------------------------------------------------- /src/manifests/chrome/manifest-dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "OptMeowt", 3 | "author": "privacy-tech-lab", 4 | "version": "5.2.0", 5 | "description": "OptMeowt allows Web users to make use of their rights to opt out from the sale and sharing of personal data", 6 | "permissions": [ 7 | "declarativeNetRequest", 8 | "webRequest", 9 | "webNavigation", 10 | "storage", 11 | "activeTab", 12 | "tabs", 13 | "scripting" 14 | ], 15 | "declarative_net_request": { 16 | "rule_resources": [ 17 | { 18 | "id": "universal_GPC", 19 | "enabled": true, 20 | "path": "rules/universal_gpc_rules.json" 21 | }, 22 | { 23 | "id": "GPC_exceptions", 24 | "enabled": true, 25 | "path": "rules/gpc_exceptions_rules.json" 26 | } 27 | ] 28 | }, 29 | "host_permissions": [ 30 | "" 31 | ], 32 | "icons": { 33 | "128": "assets/face-icons/icon128-face-circle.png" 34 | }, 35 | "action": { 36 | "default_title": "OptMeowt", 37 | "default_popup": "popup.html" 38 | }, 39 | "content_scripts": [ 40 | { 41 | "matches": [""], 42 | "js": ["content-scripts/contentScript.js"], 43 | "run_at": "document_start" 44 | } 45 | ], 46 | "options_ui": { 47 | "page": "options.html", 48 | "open_in_tab": true 49 | }, 50 | "background": { 51 | "service_worker": "background.bundle.js" 52 | }, 53 | "web_accessible_resources": [{ 54 | "resources": ["content-scripts/injection/gpc-dom.js"], 55 | "matches": [""] 56 | }], 57 | "manifest_version": 3, 58 | "incognito": "spanning", 59 | "content_security_policy": { 60 | "extension_pages": "script-src 'self'; object-src 'self'", 61 | "sandbox": "sandbox allow-scripts; script-src 'self' 'unsafe-eval'; object-src 'self'" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/manifests/chrome/manifest-dist.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "OptMeowt", 3 | "author": "privacy-tech-lab", 4 | "version": "5.2.0", 5 | "description": "OptMeowt allows Web users to make use of their rights to opt out from the sale and sharing of personal data", 6 | "permissions": [ 7 | "declarativeNetRequest", 8 | "webRequest", 9 | "webNavigation", 10 | "storage", 11 | "activeTab", 12 | "tabs", 13 | "scripting" 14 | ], 15 | "declarative_net_request": { 16 | "rule_resources": [ 17 | { 18 | "id": "universal_GPC", 19 | "enabled": true, 20 | "path": "rules/universal_gpc_rules.json" 21 | }, 22 | { 23 | "id": "GPC_exceptions", 24 | "enabled": true, 25 | "path": "rules/gpc_exceptions_rules.json" 26 | } 27 | ] 28 | }, 29 | "host_permissions": [ 30 | "" 31 | ], 32 | "icons": { 33 | "128": "assets/face-icons/icon128-face-circle.png" 34 | }, 35 | "action": { 36 | "default_title": "OptMeowt", 37 | "default_popup": "popup.html" 38 | }, 39 | "content_scripts": [ 40 | { 41 | "matches": [""], 42 | "js": ["content-scripts/contentScript.js"], 43 | "run_at": "document_start" 44 | } 45 | ], 46 | "options_ui": { 47 | "page": "options.html", 48 | "open_in_tab": true 49 | }, 50 | "background": { 51 | "service_worker": "background.bundle.js" 52 | }, 53 | "web_accessible_resources": [{ 54 | "resources": ["content-scripts/injection/gpc-dom.js"], 55 | "matches": [""] 56 | }], 57 | "manifest_version": 3, 58 | "incognito": "spanning", 59 | "content_security_policy": { 60 | "extension_pages": "script-src 'self'; object-src 'self'", 61 | "sandbox": "sandbox allow-scripts; script-src 'self' 'unsafe-eval'; object-src 'self'" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/manifests/firefox/manifest-dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "OptMeowt", 3 | "author": "privacy-tech-lab", 4 | "version": "5.2.0", 5 | "description": "OptMeowt allows Web users to make use of their rights to opt out from the sale and sharing of personal data", 6 | "permissions": [ 7 | "webRequestBlocking", 8 | "declarativeNetRequest", 9 | "webRequest", 10 | "webNavigation", 11 | "storage", 12 | "activeTab", 13 | "tabs", 14 | "scripting" 15 | ], 16 | "declarative_net_request": { 17 | "rule_resources": [ 18 | { 19 | "id": "universal_GPC", 20 | "enabled": true, 21 | "path": "rules/universal_gpc_rules.json" 22 | }, 23 | { 24 | "id": "GPC_exceptions", 25 | "enabled": true, 26 | "path": "rules/gpc_exceptions_rules.json" 27 | } 28 | ] 29 | }, 30 | "host_permissions": [ 31 | "" 32 | ], 33 | "icons": { 34 | "128": "assets/face-icons/icon128-face-circle.png" 35 | }, 36 | "action": { 37 | "default_title": "OptMeowt", 38 | "default_popup": "popup.html" 39 | }, 40 | "content_scripts": [ 41 | { 42 | "matches": [""], 43 | "js": ["content-scripts/contentScript.js"], 44 | "run_at": "document_start" 45 | } 46 | ], 47 | "options_ui": { 48 | "page": "options.html", 49 | "open_in_tab": true 50 | }, 51 | "background": { 52 | "scripts": ["background.bundle.js"] 53 | }, 54 | "web_accessible_resources": [{ 55 | "resources": ["content-scripts/injection/gpc-dom.js"], 56 | "matches": [""] 57 | }], 58 | "manifest_version": 3, 59 | "incognito": "spanning", 60 | "content_security_policy": { 61 | "extension_pages": "script-src 'self'; object-src 'self'" 62 | }, 63 | "browser_specific_settings": { 64 | "gecko": { 65 | "id": "{7f22397f-fb61-47e2-9e4b-4ddd98faa275}" 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/manifests/firefox/manifest-dist.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "OptMeowt", 3 | "author": "privacy-tech-lab", 4 | "version": "5.2.0", 5 | "description": "OptMeowt allows Web users to make use of their rights to opt out from the sale and sharing of personal data", 6 | "permissions": [ 7 | "webRequestBlocking", 8 | "declarativeNetRequest", 9 | "webRequest", 10 | "webNavigation", 11 | "storage", 12 | "activeTab", 13 | "tabs", 14 | "scripting" 15 | ], 16 | "declarative_net_request": { 17 | "rule_resources": [ 18 | { 19 | "id": "universal_GPC", 20 | "enabled": true, 21 | "path": "rules/universal_gpc_rules.json" 22 | }, 23 | { 24 | "id": "GPC_exceptions", 25 | "enabled": true, 26 | "path": "rules/gpc_exceptions_rules.json" 27 | } 28 | ] 29 | }, 30 | "host_permissions": [ 31 | "" 32 | ], 33 | "icons": { 34 | "128": "assets/face-icons/icon128-face-circle.png" 35 | }, 36 | "action": { 37 | "default_title": "OptMeowt", 38 | "default_popup": "popup.html" 39 | }, 40 | "content_scripts": [ 41 | { 42 | "matches": [""], 43 | "js": ["content-scripts/contentScript.js"], 44 | "run_at": "document_start" 45 | } 46 | ], 47 | "options_ui": { 48 | "page": "options.html", 49 | "open_in_tab": true 50 | }, 51 | "background": { 52 | "scripts": ["background.bundle.js"] 53 | }, 54 | "web_accessible_resources": [{ 55 | "resources": ["content-scripts/injection/gpc-dom.js"], 56 | "matches": [""] 57 | }], 58 | "manifest_version": 3, 59 | "incognito": "spanning", 60 | "content_security_policy": { 61 | "extension_pages": "script-src 'self'; object-src 'self'" 62 | }, 63 | "browser_specific_settings": { 64 | "gecko": { 65 | "id": "{7f22397f-fb61-47e2-9e4b-4ddd98faa275}" 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/background/protection/background.js: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md 3 | privacy-tech-lab, https://privacytechlab.org/ 4 | */ 5 | 6 | /* 7 | background.js 8 | ================================================================================ 9 | background.js is the main background script handling OptMeowt's 10 | main opt-out functionality 11 | */ 12 | 13 | import { enableListeners, disableListeners } from "./listeners-$BROWSER.js"; 14 | import { stores, storage } from "../storage.js"; 15 | import { defaultSettings } from "../../data/defaultSettings.js"; 16 | 17 | // We could alt. use this in place of "building" for chrome/ff, just save it to settings in storage 18 | var userAgent = 19 | window.navigator.userAgent.indexOf("Firefox") > -1 ? "moz" : "chrome"; 20 | 21 | /******************************************************************************/ 22 | 23 | /** 24 | * Enables extension functionality and sets site listeners 25 | * Information regarding the functionality and timing of webRequest and webNavigation 26 | * can be found on Mozilla's & Chrome's API docuentation sites (also linked above) 27 | * 28 | * The actual listeners are located in `listeners-(chosen browser).js` 29 | * The functions called on event occurance are located in `events.js` 30 | * 31 | * HIERARCHY: manifest.json --> protection --> background.js --> listeners-$BROWSER.js --> events.js 32 | */ 33 | 34 | /******************************************************************************/ 35 | 36 | /** 37 | * Initializes the extension 38 | * Place all initialization necessary, as high level as can be, here. 39 | */ 40 | async function init() { 41 | enableListeners(); 42 | } 43 | 44 | function halt() { 45 | disableListeners(); 46 | } 47 | 48 | /******************************************************************************/ 49 | 50 | export const background = { 51 | init, 52 | halt, 53 | }; 54 | -------------------------------------------------------------------------------- /src/theme/darkmode.js: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md 3 | privacy-tech-lab, https://privacytechlab.org/ 4 | */ 5 | 6 | /* 7 | darkmode.js 8 | ================================================================================ 9 | darkmode.js is a snippet of code based on the `darkmode.js` source file 10 | located at https://github.com/sandoche/Darkmode.js/blob/master/src/darkmode.js 11 | 12 | darkmode.js implements a way to store the darkmode option in local storage and 13 | only keeps one class that is appended to the HTML body on toggle. All CSS is 14 | applied via `dark-mode.css` and the `darkmode--activated` attribute. 15 | 16 | GitHub Repo: https://github.com/sandoche/Darkmode.js 17 | */ 18 | 19 | export const IS_BROWSER = typeof window !== "undefined"; 20 | 21 | export default class Darkmode { 22 | constructor(options) { 23 | if (!IS_BROWSER) { 24 | return; 25 | } 26 | 27 | const defaultOptions = { 28 | saveInCookies: true, 29 | autoMatchOsTheme: true, 30 | }; 31 | 32 | options = Object.assign({}, defaultOptions, options); 33 | 34 | const preferedThemeOs = 35 | options.autoMatchOsTheme && 36 | window.matchMedia("(prefers-color-scheme: dark)").matches; 37 | const darkmodeActivated = 38 | window.localStorage.getItem("darkmode") === "true"; 39 | const darkmodeNeverActivatedByAction = 40 | window.localStorage.getItem("darkmode") === null; 41 | 42 | if ( 43 | (darkmodeActivated === true && options.saveInCookies) || 44 | (darkmodeNeverActivatedByAction && preferedThemeOs) 45 | ) { 46 | document.body.classList.add("darkmode--activated"); 47 | } 48 | } 49 | 50 | toggle() { 51 | if (!IS_BROWSER) { 52 | return; 53 | } 54 | const isDarkmode = this.isActivated(); 55 | 56 | document.body.classList.toggle("darkmode--activated"); 57 | window.localStorage.setItem("darkmode", !isDarkmode); 58 | } 59 | 60 | isActivated() { 61 | if (!IS_BROWSER) { 62 | return null; 63 | } 64 | return document.body.classList.contains("darkmode--activated"); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/options/components/util.js: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md 3 | privacy-tech-lab, https://privacytechlab.org/ 4 | */ 5 | 6 | /* 7 | util.js 8 | ================================================================================ 9 | util.js contains global helper functions to help render the options page 10 | */ 11 | 12 | /** 13 | * Get local html file as string 14 | * @param {string} path - location of HTML template 15 | * @returns {string|none} - Returns the stringified HTML template or 16 | * prints an error 17 | */ 18 | import Mustache from "mustache"; 19 | 20 | export async function fetchTemplate(path) { 21 | let response = await fetch(path); 22 | let data = await response.text(); 23 | return data; 24 | } 25 | 26 | /** 27 | * Parse string to html document 28 | * @param {string} template - stringified HTML template 29 | * @returns {HTMLDocument} - also a Document 30 | */ 31 | export function parseTemplate(template) { 32 | let parser = new DOMParser(); 33 | let doc = parser.parseFromString(template, "text/html"); 34 | return doc; 35 | } 36 | 37 | /** 38 | * Fetches and parses html document; returns selected html 39 | * @param {string} path - location of document to be parsed 40 | * @param {string} id - name of the element in doc to be selected 41 | * after it is parsed 42 | * @returns {Object} - element object related to the id parameter 43 | */ 44 | export async function fetchParse(path, id) { 45 | let template = await fetchTemplate(path); 46 | return parseTemplate(template).getElementById(id); 47 | } 48 | 49 | /** 50 | * Renders and parse html document; returns selected html 51 | * @param {string} template - stringified HTML doc template 52 | * @param {Object} data - specifically a `headings` object 53 | * @param {string} id - id of an element in an HTML doc 54 | * @returns {Object} - element object related to the id parameter 55 | */ 56 | export function renderParse(template, data, id) { 57 | let renderedTemplate = Mustache.render(template, data); 58 | return parseTemplate(renderedTemplate).getElementById(id); 59 | } 60 | -------------------------------------------------------------------------------- /src/background/protection/listeners-chrome.js: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md 3 | privacy-tech-lab, https://privacytechlab.org/ 4 | */ 5 | 6 | /* 7 | listeners-chrome.js 8 | ================================================================================ 9 | listeners-chrome.js holds the on-page-visit listeners for chrome that activate 10 | our main functionality 11 | */ 12 | 13 | // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webRequest/onBeforeRequest 14 | // https://developer.chrome.com/docs/extensions/reference/webRequest/ 15 | // This is the extraInfoSpec array of strings 16 | const CHROME_REQUEST_SPEC = ["requestHeaders", "extraHeaders"]; 17 | const CHROME_RESPONSE_SPEC = ["responseHeaders", "extraHeaders"]; 18 | // This is the filter object 19 | const FILTER = { urls: [""] }; 20 | 21 | /** 22 | * Enables extension functionality and sets site listeners 23 | * Information regarding the functionality and timing of webRequest and webNavigation 24 | * can be found on Mozilla's & Chrome's API docuentation sites (also linked above) 25 | * 26 | * The functions called on event occurance are located in `events.js` 27 | */ 28 | function enableListeners(callbacks) { 29 | const { 30 | onBeforeSendHeaders, 31 | onHeadersReceived, 32 | onBeforeNavigate, 33 | onCommitted, 34 | } = callbacks; 35 | 36 | // (4) global Chrome listeners 37 | chrome.webRequest.onBeforeSendHeaders.addListener( 38 | onBeforeSendHeaders, 39 | FILTER, 40 | CHROME_REQUEST_SPEC 41 | ); 42 | chrome.webRequest.onHeadersReceived.addListener( 43 | onHeadersReceived, 44 | FILTER, 45 | CHROME_RESPONSE_SPEC 46 | ); 47 | chrome.webNavigation.onBeforeNavigate.addListener(onBeforeNavigate, FILTER); 48 | chrome.webNavigation.onCommitted.addListener(onCommitted, FILTER); 49 | } 50 | 51 | /** 52 | * Disables background listeners 53 | */ 54 | function disableListeners(callbacks) { 55 | const { 56 | onBeforeSendHeaders, 57 | onHeadersReceived, 58 | onBeforeNavigate, 59 | onCommitted, 60 | } = callbacks; 61 | 62 | chrome.webRequest.onBeforeSendHeaders.removeListener(onBeforeSendHeaders); 63 | chrome.webRequest.onHeadersReceived.removeListener(onHeadersReceived); 64 | chrome.webNavigation.onBeforeNavigate.removeListener(onBeforeNavigate); 65 | chrome.webNavigation.onCommitted.removeListener(onCommitted); 66 | } 67 | 68 | export { enableListeners, disableListeners }; 69 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "optmeowt", 3 | "version": "5.1.2", 4 | "description": "A privacy extension that allows users to exercise rights under GPC", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "prestart": "rimraf dev", 9 | "start": "concurrently -k npm:start:firefox npm:start:chrome", 10 | "start:firefox": "webpack --watch --mode development --env firefox", 11 | "start:chrome": "webpack --watch --mode development --env chrome", 12 | "prebuild": "rimraf dist && mkdir dist && mkdir dist/packages", 13 | "build": "npm run build:firefox && npm run build:chrome", 14 | "build:firefox": "webpack --mode production --env firefox", 15 | "build:chrome": "webpack --mode production --env chrome", 16 | "postbuild:firefox": "cd dist/firefox && zip -rFSX ../packages/ff-optmeowt-$npm_package_version.zip * -x '*.git*' -x '*.DS_Store*' -x '*.txt*'", 17 | "postbuild:chrome": "cd dist/chrome && zip -rFSX ../packages/chrome-optmeowt-$npm_package_version.zip * -x '*.git*' -x '*.DS_Store*' -x '*.txt*'", 18 | "test": "mocha $(find test -name '*.js')" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/privacy-tech-lab/gpc-optmeowt.git" 23 | }, 24 | "author": "", 25 | "license": "ISC", 26 | "bugs": { 27 | "url": "https://github.com/privacy-tech-lab/gpc-optmeowt/issues" 28 | }, 29 | "homepage": "https://github.com/privacy-tech-lab/gpc-optmeowt#readme", 30 | "dependencies": { 31 | "animate.css": "^4.1.1", 32 | "darkmode-js": "^1.5.7", 33 | "file-saver": "^2.0.5", 34 | "idb": "^7.1.1", 35 | "mocha": "^10.8.2", 36 | "mustache": "^4.2.0", 37 | "path": "^0.12.7", 38 | "psl": "^1.8.0", 39 | "puppeteer": "^22.15.0", 40 | "rimraf": "^3.0.2", 41 | "tippy.js": "^6.3.7", 42 | "uikit": "3.6.9" 43 | }, 44 | "devDependencies": { 45 | "@babel/core": "^7.21.3", 46 | "@babel/preset-env": "^7.20.2", 47 | "babel-loader": "^9.1.2", 48 | "clean-webpack-plugin": "^4.0.0", 49 | "concurrently": "^6.2.1", 50 | "copy-webpack-plugin": "^9.0.1", 51 | "css-loader": "^5.2.7", 52 | "file-loader": "^6.2.0", 53 | "html-webpack-plugin": "^5.3.2", 54 | "prettier": "^2.3.2", 55 | "string-replace-loader": "^3.0.3", 56 | "style-loader": "^2.0.0", 57 | "wait-on": "^7.2.0", 58 | "webpack": "^5.94.0", 59 | "webpack-cli": "^4.8.0", 60 | "webpack-dev-server": "^5.2.1", 61 | "workbox-webpack-plugin": "^7.3.0" 62 | }, 63 | "resolutions": { 64 | "ws": "^8.17.1" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/background/protection/listeners-firefox.js: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md 3 | privacy-tech-lab, https://privacytechlab.org/ 4 | */ 5 | 6 | /* 7 | listeners-firefox.js 8 | ================================================================================ 9 | listeners-firefox.js holds the on-page-visit listeners for firefox that activate 10 | our main functionality 11 | */ 12 | 13 | // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webRequest/onBeforeRequest 14 | // https://developer.chrome.com/docs/extensions/reference/webRequest/ 15 | // This is the extraInfoSpec array of strings 16 | const MOZ_REQUEST_SPEC = ["requestHeaders", "blocking"]; 17 | const MOZ_RESPONSE_SPEC = ["responseHeaders", "blocking"]; 18 | 19 | // This is the filter object 20 | const FILTER = { urls: [""] }; 21 | 22 | /** 23 | * Enables extension functionality and sets site listeners 24 | * Information regarding the functionality and timing of webRequest and webNavigation 25 | * can be found on Mozilla's & Chrome's API docuentation sites (also linked above) 26 | * 27 | * The functions called on event occurance are located in `events.js` 28 | */ 29 | function enableListeners(callbacks) { 30 | const { 31 | onBeforeSendHeaders, 32 | onHeadersReceived, 33 | onBeforeNavigate, 34 | onCommitted, 35 | onCompleted, 36 | } = callbacks; 37 | 38 | // (4) global Firefox listeners 39 | chrome.webRequest.onBeforeSendHeaders.addListener( 40 | onBeforeSendHeaders, 41 | FILTER, 42 | MOZ_REQUEST_SPEC 43 | ); 44 | chrome.webRequest.onHeadersReceived.addListener( 45 | onHeadersReceived, 46 | FILTER, 47 | MOZ_RESPONSE_SPEC 48 | ); 49 | chrome.webNavigation.onBeforeNavigate.addListener(onBeforeNavigate); 50 | chrome.webNavigation.onCommitted.addListener(onCommitted); 51 | chrome.webNavigation.onCompleted.addListener(onCompleted); 52 | } 53 | 54 | /** 55 | * Disables background listeners 56 | */ 57 | function disableListeners(callbacks) { 58 | const { 59 | onBeforeSendHeaders, 60 | onHeadersReceived, 61 | onBeforeNavigate, 62 | onCommitted, 63 | onCompleted, 64 | } = callbacks; 65 | 66 | chrome.webRequest.onBeforeSendHeaders.removeListener(onBeforeSendHeaders); 67 | chrome.webRequest.onHeadersReceived.removeListener(onHeadersReceived); 68 | chrome.webNavigation.onBeforeNavigate.removeListener(onBeforeNavigate); 69 | chrome.webNavigation.onCommitted.removeListener(onCommitted); 70 | chrome.webNavigation.onCompleted.removeListener(onCompleted); 71 | } 72 | 73 | export { enableListeners, disableListeners }; 74 | -------------------------------------------------------------------------------- /src/options/views/main-view/main-view.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 12 | 13 | 18 | 84 | -------------------------------------------------------------------------------- /test/background/gpc.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md 3 | privacy-tech-lab, https://privacytechlab.org/ 4 | */ 5 | 6 | /* 7 | gpc.test.js 8 | ================================================================================ 9 | gpc.test.js tests the GPC signal head-fully using Puppeteer and Chromium 10 | */ 11 | 12 | /** 13 | * Tests for testing GPC signals 14 | */ 15 | 16 | import assert from "assert"; 17 | import fs from "fs"; 18 | import path from "path"; 19 | import puppeteer from "puppeteer"; 20 | import { fileURLToPath } from "url"; 21 | 22 | let browser; 23 | 24 | if (!process.env.CI) { 25 | describe("GPC test", function () { 26 | this.timeout(20000); 27 | before(async () => { 28 | const puppeteerOps = { 29 | headless: false, 30 | }; 31 | const args = []; 32 | 33 | const __filename = fileURLToPath(import.meta.url); 34 | const __dirname = path.dirname(__filename); 35 | const projectRoot = path.resolve(__dirname, "../../"); 36 | let extensionPath = path.resolve(projectRoot, "dev/chrome"); 37 | 38 | if (!fs.existsSync(extensionPath)) { 39 | extensionPath = path.resolve(projectRoot, "dist/chrome"); 40 | } 41 | 42 | if (!fs.existsSync(extensionPath)) { 43 | throw new Error( 44 | "Unable to locate an OptMeowt build at dev/chrome or dist/chrome. " + 45 | "Run `npm run start:chrome` or `npm run build:chrome` before running this test." 46 | ); 47 | } 48 | 49 | args.push("--disable-extensions-except=" + extensionPath); 50 | args.push("--load-extension=" + extensionPath); 51 | 52 | puppeteerOps.args = args; 53 | browser = await puppeteer.launch(puppeteerOps); 54 | await new Promise((resolve) => setTimeout(resolve, 1000)); 55 | }); 56 | after(async () => { 57 | await browser.close(); 58 | }); 59 | 60 | it("Tests whether the GPC and header signal are properly set", async () => { 61 | const page = await browser.newPage(); 62 | 63 | await page.goto(`https://global-privacy-control.vercel.app/`); 64 | 65 | await page.reload(); 66 | 67 | const gpc = await page.evaluate(async () => { 68 | await new Promise((resolve) => setTimeout(resolve, 1000)); 69 | 70 | return (async () => { 71 | return navigator.globalPrivacyControl; 72 | })(); 73 | }); 74 | 75 | const header = await page.evaluate(async () => { 76 | function getElementByXpath(path) { 77 | return document.evaluate( 78 | path, 79 | document, 80 | null, 81 | XPathResult.FIRST_ORDERED_NODE_TYPE, 82 | null 83 | ).singleNodeValue; 84 | } 85 | 86 | return getElementByXpath("/html/body/section[2]/div/div[1]/div/h3") 87 | .innerText; 88 | }); 89 | 90 | assert.equal(header, 'Header present \nSec-GPC: "1"'); 91 | assert.equal(gpc, true); 92 | }); 93 | }); 94 | } 95 | -------------------------------------------------------------------------------- /src/options/views/about-view/about-view.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 13 | 14 | 15 | 19 | 90 | -------------------------------------------------------------------------------- /src/options/views/main-view/main-view.js: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md 3 | privacy-tech-lab, https://privacytechlab.org/ 4 | */ 5 | 6 | /* 7 | main-view.js 8 | ================================================================================ 9 | main-view.js handles the navigation between different parts of the options page 10 | and loads them when called through the navigation bar 11 | */ 12 | 13 | 14 | import { fetchTemplate, parseTemplate } from "../../components/util.js"; 15 | import { settingsView } from "../settings-view/settings-view.js"; 16 | import { domainlistView } from "../domainlist-view/domainlist-view.js"; 17 | import { aboutView } from "../about-view/about-view.js"; 18 | import { storage, stores } from "../../../background/storage.js"; 19 | import Darkmode from "../../../theme/darkmode.js"; 20 | 21 | /** 22 | * Opens the `Settings` page 23 | * @param {string} bodyTemplate - stringified HTML template 24 | */ 25 | async function displaySettings(bodyTemplate) { 26 | settingsView(bodyTemplate); 27 | document.querySelector(".navbar-item.active").classList.remove("active"); 28 | document.querySelector("#main-view-settings").classList.add("active"); 29 | } 30 | 31 | /** 32 | * Opens the `Domainlist` page 33 | * @param {string} bodyTemplate - stringified HTML template 34 | */ 35 | function displayDomainlist(bodyTemplate) { 36 | domainlistView(bodyTemplate); 37 | document.querySelector(".navbar-item.active").classList.remove("active"); 38 | document.querySelector("#main-view-domainlist").classList.add("active"); 39 | } 40 | 41 | /** 42 | * Opens the `Display` page 43 | * @param {string} bodyTemplate - stringified HTML template 44 | */ 45 | function displayAbout(bodyTemplate) { 46 | aboutView(bodyTemplate); 47 | document.querySelector(".navbar-item.active").classList.remove("active"); 48 | document.querySelector("#main-view-about").classList.add("active"); 49 | } 50 | 51 | /** 52 | * Prepares the `Main` page elements and intializes the default `Settings` page 53 | */ 54 | export async function mainView() { 55 | let docTemplate = await fetchTemplate("./views/main-view/main-view.html"); 56 | const bodyTemplate = await fetchTemplate( 57 | "./components/scaffold-component.html" 58 | ); 59 | document.body.innerHTML = 60 | parseTemplate(docTemplate).getElementById("main-view").innerHTML; 61 | 62 | let domainlistPressed = await storage.get( 63 | stores.settings, 64 | "DOMAINLIST_PRESSED" 65 | ); 66 | 67 | if (!domainlistPressed) { 68 | settingsView(bodyTemplate); // First page 69 | document.querySelector("#main-view-settings").classList.add("active"); 70 | } else if (domainlistPressed) { 71 | domainlistView(bodyTemplate); // First page 72 | await storage.set(stores.settings, false, "DOMAINLIST_PRESSED"); 73 | document.querySelector("#main-view-domainlist").classList.add("active");} 74 | 75 | document 76 | .getElementById("main-view-settings") 77 | .addEventListener("click", () => displaySettings(bodyTemplate)); 78 | document 79 | .getElementById("main-view-domainlist") 80 | .addEventListener("click", () => displayDomainlist(bodyTemplate)); 81 | document 82 | .getElementById("main-view-about") 83 | .addEventListener("click", () => displayAbout(bodyTemplate)); 84 | 85 | // DARK MODE 86 | const darkmode = new Darkmode(); 87 | 88 | //Listener: Listens for a message sent by popup.js 89 | chrome.runtime.onMessage.addListener(function ( 90 | message, 91 | sender, 92 | sendResponse 93 | ) { 94 | if (message.msg === "DARKSWITCH_PRESSED") { 95 | darkmode.toggle(); 96 | } 97 | if (message.msg === "SHOW_TUTORIAL") { 98 | displaySettings(bodyTemplate); 99 | } 100 | }); 101 | } 102 | -------------------------------------------------------------------------------- /src/background/control.js: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md 3 | privacy-tech-lab, https://privacytechlab.org/ 4 | */ 5 | 6 | /* 7 | control.js 8 | ================================================================================ 9 | control.js manages persistent data, message liseteners, in particular 10 | to manage the state & functionality mode of the extension 11 | */ 12 | 13 | import { 14 | init as initProtection_ff, 15 | halt as haltProtection_ff, 16 | } from "./protection/protection-ff.js"; 17 | import { 18 | init as initProtection_cr, 19 | halt as haltProtection_cr, 20 | } from "./protection/protection.js"; 21 | import { defaultSettings } from "../data/defaultSettings.js"; 22 | import { stores, storage } from "./storage.js"; 23 | import { reloadDynamicRules } from "../common/editRules.js"; 24 | 25 | import { 26 | debug_domainlist_and_dynamicrules, 27 | updateRemovalScript, 28 | } from "../common/editDomainlist.js"; 29 | 30 | async function enable() { 31 | var initProtection = initProtection_cr; 32 | initProtection(); 33 | } 34 | 35 | function disable() { 36 | var haltProtection = haltProtection_cr; 37 | haltProtection(); 38 | } 39 | 40 | /******************************************************************************/ 41 | // Initializers 42 | 43 | // This is the very first thing the extension runs 44 | (async () => { 45 | chrome.scripting.registerContentScripts([ 46 | { 47 | id: "1", 48 | matches: [""], 49 | excludeMatches:["https://example.com/"], 50 | js: ["content-scripts/registration/gpc-dom.js"], 51 | runAt: "document_start", 52 | } 53 | ]); 54 | 55 | // Check if the browser is Firefox 56 | if ("$BROWSER" == "firefox") { 57 | chrome.runtime.onInstalled.addListener(function (details) { 58 | if (details.reason === 'install') { 59 | chrome.runtime.openOptionsPage((result) => {}); 60 | } 61 | }); 62 | } 63 | // Initializes the default settings 64 | let settingsDB = await storage.getStore(stores.settings); 65 | for (let setting in defaultSettings) { 66 | if (typeof settingsDB[setting] === "undefined") { 67 | await storage.set(stores.settings, defaultSettings[setting], setting); 68 | } 69 | } 70 | const localSettings = await chrome.storage.local.get( 71 | "WELLKNOWN_CHECK_ENABLED" 72 | ); 73 | if (typeof localSettings.WELLKNOWN_CHECK_ENABLED === "undefined") { 74 | await chrome.storage.local.set({ 75 | WELLKNOWN_CHECK_ENABLED: defaultSettings["WELLKNOWN_CHECK_ENABLED"], 76 | }); 77 | } 78 | 79 | let isEnabled = await storage.get(stores.settings, "IS_ENABLED"); 80 | 81 | if (isEnabled) { 82 | // Turns on the extension 83 | enable(); 84 | updateRemovalScript(); 85 | reloadDynamicRules(); 86 | } 87 | 88 | })(); 89 | 90 | /******************************************************************************/ 91 | // Mode listeners 92 | 93 | // (1) Handle extension activeness is changed by calling all halt 94 | // - Make sure that I switch extensionmode and separate it from mode.domainlist 95 | // (2) Handle extension functionality with listeners and message passing 96 | 97 | /** 98 | * Listeners for information from --POPUP-- or --OPTIONS-- page 99 | * This is the main "hub" for message passing between the extension components 100 | * https://developer.chrome.com/docs/extensions/mv3/messaging/ 101 | */ 102 | chrome.runtime.onMessage.addListener(async function ( 103 | message 104 | ) { 105 | if (message.msg === "TURN_ON_OFF") { 106 | let isEnabled = message.data.isEnabled; // can be undefined 107 | 108 | if (isEnabled) { 109 | await storage.set(stores.settings, true, "IS_ENABLED"); 110 | enable(); 111 | } else { 112 | await storage.set(stores.settings, false, "IS_ENABLED"); 113 | disable(); 114 | } 115 | } 116 | 117 | if (message.msg === "CHANGE_IS_DOMAINLISTED") { 118 | let isDomainlisted = message.data.isDomainlisted; // can be undefined // not used 119 | } 120 | }); 121 | -------------------------------------------------------------------------------- /src/popup/styles.css: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md 3 | privacy-tech-lab, https://privacytechlab.org/ 4 | */ 5 | 6 | /* 7 | styles.css 8 | ================================================================================ 9 | styles.css is the main css page for OptMeowt's popup page 10 | */ 11 | 12 | :root { 13 | --text-gray: rgb(89, 98, 127); 14 | } 15 | 16 | .small-warning-box { 17 | color: white; 18 | background-color: #db4437; 19 | padding-left: 10px; 20 | padding-right: 5px; 21 | padding-top: 5px; 22 | padding-bottom: 5px; 23 | border-radius: 5px; 24 | text-align: left; 25 | } 26 | 27 | .importexport-button { 28 | background-color: white; 29 | border-color: #888fa1; 30 | color: #888fa1; 31 | padding: 12px 16px; 32 | text-align: center; 33 | text-decoration-color: none; 34 | font-size: 14px; 35 | display: inline-block; 36 | border-radius: 10px; 37 | border-style: solid; 38 | box-shadow: 0 8px 16px -1px rgba(211, 211, 211, 0.2); 39 | outline: none; 40 | transition: all ease 0.25s; 41 | } 42 | .importexport-button:hover { 43 | background-color: var(--accent-color); 44 | border-color: var(--accent-color); 45 | color: white; 46 | box-shadow: 0 6px 12px var(--accent-color-lighter-60); 47 | transition: all ease 0.25s; 48 | } 49 | 50 | .button:hover { 51 | background-color: #df3131 !important; 52 | border: 1px solid #df3131 !important; 53 | transition: all 0.3s ease; 54 | color: #fff !important; 55 | } 56 | 57 | .uspStringElem { 58 | margin: auto; 59 | padding-top: 10px; 60 | padding-bottom: 10px; 61 | padding-right: 8px; 62 | padding-left: 8px; 63 | background-color: white; 64 | border: 1px solid var(--text-gray); 65 | color: var(--text-gray); 66 | text-align: center; 67 | } 68 | 69 | .uspStringElem:hover { 70 | background-color: white; 71 | border: 1px solid var(--text-gray); 72 | color: var(--text-gray); 73 | } 74 | 75 | .uspStringElem:active { 76 | background-color: white; 77 | border: 1px solid var(--text-gray); 78 | color: var(--text-gray); 79 | } 80 | 81 | 82 | 83 | /* 84 | SVG photo assets style 85 | */ 86 | @import "../options/styles.css"; 87 | svg { 88 | fill: #d3d3d3; 89 | transition: all ease 0.5s; 90 | } 91 | 92 | svg:hover { 93 | fill: #5a647d; 94 | transition: all ease 0.5s; 95 | } 96 | 97 | /********************************************************/ 98 | 99 | /* 100 | Blue Heading 101 | */ 102 | .blue-heading { 103 | color: #4472c4; 104 | } 105 | 106 | /*=======================================================================================*/ 107 | /*tutorial popup css*/ 108 | .tippy-box[data-theme~="custom-2"] { 109 | background-color: #87cefa; 110 | box-shadow: 10px 10px 5px 0px rgba(0, 0, 0, 0.31); 111 | color: white; 112 | padding: 10px; 113 | border-radius: 5px; 114 | text-align: left; 115 | float: left; 116 | } 117 | 118 | .tippy-box[data-theme~="custom-2"][data-placement^="bottom"] 119 | > .tippy-arrow::before { 120 | border-bottom-color: #87cefa; 121 | } 122 | 123 | /********************************************************/ 124 | 125 | /* 126 | Animated 3rd party domains/domain list buttons/links 127 | */ 128 | 129 | .domain-list:hover { 130 | background-color: var(--accent-color); 131 | color: white; 132 | transition: ease-out 0.1s; 133 | } 134 | 135 | 136 | .dropdown-tab:hover { 137 | background-color: var(--highlight-light); 138 | color: var(--text-color-darker); 139 | transition: ease-out 0.1s; 140 | } 141 | 142 | .dropdown-tab-click { 143 | background-color: var(--highlight-light); 144 | color: var(--text-color-darker); 145 | } 146 | 147 | /********************************************************/ 148 | 149 | /* 150 | Accepted/rejected text on popup 151 | */ 152 | 153 | .status-text-red { 154 | color: red; 155 | /* border: solid red 2px; 156 | border-radius: 14px; 157 | box-shadow: 0 8px 16px -1px rgba(211, 211, 211, .2); 158 | outline: none; */ 159 | } 160 | 161 | .divide { 162 | border: 0; 163 | /* height: 1px; 164 | background: black; */ 165 | } 166 | 167 | /* .uk-list-divide { 168 | border: 0; 169 | height: 1px; 170 | background: black; 171 | } */ 172 | -------------------------------------------------------------------------------- /src/common/editRules.js: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md 3 | privacy-tech-lab, https://privacytechlab.org/ 4 | */ 5 | 6 | import { storage, stores } from "../background/storage.js"; 7 | 8 | /* 9 | editRules.js 10 | ================================================================================ 11 | editRules.js is an internal API for adding/removing GPC-exclusion dynamic rules 12 | */ 13 | 14 | /** 15 | * Gets fresh rule ID for new DeclarativeNetRequest dynamic rule 16 | * Pulls from already set dynamic rules as opposed to domainlist values 17 | * 18 | * NOTE: Does not 'reserve' the ID. If it isn't used on the client side, 19 | * getFreshId() will spit out the same val next call. 20 | * @returns {Promise<(number|null)>} - number of fresh ID, null if non available 21 | */ 22 | export async function getFreshId() { 23 | const MAX_RULES = 24 | chrome.declarativeNetRequest.MAX_NUMBER_OF_DYNAMIC_AND_SESSION_RULES; 25 | const rules = await chrome.declarativeNetRequest.getDynamicRules(); 26 | let freshId = null; 27 | let usedRuleIds = []; 28 | 29 | for (let i in rules) { 30 | usedRuleIds.push(rules[i]["id"]); 31 | } 32 | usedRuleIds.sort((a, b) => { 33 | return a - b; 34 | }); // Necessary for next for loop 35 | 36 | // Make sure the ID starts at 1 (I think 0 is reserved?) 37 | for (let i = 1; i < MAX_RULES; i++) { 38 | if (i !== usedRuleIds[i - 1]) { 39 | freshId = i; // We have found the first nonzero, unused id 40 | break; 41 | } 42 | } 43 | return freshId; 44 | } 45 | 46 | /** 47 | * Deletes GPC-exclusion rule from rule set 48 | * Does NOT remove from domainlist 49 | * (see declarativeNetRequest) 50 | * @param {number} id - rule id 51 | */ 52 | export async function deleteDynamicRule(id) { 53 | let UpdateRuleOptions = { removeRuleIds: [id] }; 54 | await chrome.declarativeNetRequest.updateDynamicRules(UpdateRuleOptions); 55 | } 56 | 57 | /** 58 | * Deletes all GPC-exclusion dynamic rules 59 | * (see declarativeNetRequest) 60 | */ 61 | export async function deleteAllDynamicRules() { 62 | let MAX_RULES = 63 | chrome.declarativeNetRequest.MAX_NUMBER_OF_DYNAMIC_AND_SESSION_RULES; 64 | let UpdateRuleOptions = { removeRuleIds: [...Array(MAX_RULES).keys()] }; 65 | await chrome.declarativeNetRequest.updateDynamicRules(UpdateRuleOptions); 66 | } 67 | 68 | /** 69 | * Adds domain as a rule to be excluded from receiving GPC signals 70 | * Note id should be fresh, o/w it will overwrite existing rule 71 | * (see getFreshId, declarativeNetRequest) 72 | * @param {number} id - rule id 73 | * @param {string} domain - domain to associate with id 74 | */ 75 | export async function addDynamicRule(id, domain) { 76 | let UpdateRuleOptions = { 77 | addRules: [ 78 | { 79 | id: id, 80 | priority: 2, 81 | action: { 82 | type: "modifyHeaders", 83 | requestHeaders: [ 84 | { "header": "Sec-GPC", "operation": "remove" }, 85 | { "header": "DNT", "operation": "remove" }, 86 | { "header": "Permissions-Policy", "operation": "remove"} 87 | ], 88 | }, 89 | condition: { 90 | urlFilter: domain, 91 | resourceTypes: [ 92 | "main_frame", 93 | "sub_frame", 94 | "stylesheet", 95 | "script", 96 | "image", 97 | "font", 98 | "object", 99 | "xmlhttprequest", 100 | "ping", 101 | "csp_report", 102 | "media", 103 | "websocket", 104 | "other", 105 | ], 106 | }, 107 | }, 108 | ], 109 | removeRuleIds: [id], 110 | }; 111 | await chrome.declarativeNetRequest.updateDynamicRules(UpdateRuleOptions); 112 | return; 113 | } 114 | 115 | /** 116 | * Deletes all rules, queries current domainlist, and re-adds all rules 117 | * - Useful when replacing the domainlist via an import/export 118 | * - Remember rules as of v3.0.0 are 'exclusion' rules, i.e. excluded from 119 | * receiving GPC or other opt-outs. 120 | */ 121 | export async function reloadDynamicRules() { 122 | deleteAllDynamicRules(); 123 | let domainlist = await storage.getStore(stores.domainlist); 124 | 125 | let promises = []; 126 | Object.keys(domainlist).forEach(async (domain) => { 127 | promises.push( 128 | new Promise(async (resolve, reject) => { 129 | let id = domainlist[domain]; 130 | if (id) { 131 | await addDynamicRule(id, domain); 132 | } 133 | resolve(); 134 | }) 135 | ); 136 | }); 137 | } 138 | -------------------------------------------------------------------------------- /src/options/views/domainlist-view/domainlist-view.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 13 | 14 | 15 | 19 | 156 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md 3 | privacy-tech-lab, https://privacytechlab.org/ 4 | */ 5 | 6 | /* 7 | const CopyPlugin = require("copy-webpack-plugin"); 8 | const TerserPlugin = require("terser-webpack-plugin"); 9 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 10 | const { CleanWebpackPlugin } = require("clean-webpack-plugin"); 11 | const path = require("path"); 12 | */ 13 | 14 | 15 | import CopyPlugin from "copy-webpack-plugin"; 16 | import TerserPlugin from "terser-webpack-plugin"; 17 | import HtmlWebpackPlugin from "html-webpack-plugin"; 18 | import { CleanWebpackPlugin } from "clean-webpack-plugin"; 19 | import path from "path"; 20 | import { fileURLToPath } from "url"; 21 | const __filename = fileURLToPath(import.meta.url); 22 | const __dirname = path.dirname(__filename); 23 | 24 | 25 | // ! Implement a "frontend" export in order to use a dev serve 26 | // ! Implement terser for production 27 | // ! Implement file loader for assets 28 | 29 | export default (env, argv) => { 30 | const browser = env.chrome ? "chrome" : "firefox"; // default to firefox build 31 | const isProduction = argv.mode == "production"; // sets bool depending on build 32 | 33 | return { 34 | name: "background", 35 | // This is useful, plus we need it b/c otherwise we get an "unsafe eval" problem 36 | entry: { 37 | background: "./src/background/control.js", 38 | popup: "./src/popup/popup.js", 39 | options: "./src/options/options.js", 40 | }, 41 | output: { 42 | filename: "[name].bundle.js", 43 | path: path.resolve( 44 | __dirname, 45 | `${isProduction ? "dist" : "dev"}/${browser}` 46 | ), 47 | }, 48 | devtool: isProduction ? "source-map" : "cheap-source-map", 49 | devServer: { 50 | open: true, 51 | host: "localhost", 52 | }, 53 | optimization: { 54 | minimize: true, 55 | minimizer: [new TerserPlugin()], 56 | }, 57 | module: { 58 | rules: [ 59 | { 60 | // compile for the correct browser 61 | test: /\.js$/, 62 | exclude: /node_modules/, 63 | loader: "string-replace-loader", 64 | options: { 65 | search: /\$BROWSER/g, 66 | replace: browser, 67 | }, 68 | }, 69 | { 70 | test: /\.js$/, 71 | exclude: /node_modules/, 72 | use: { 73 | loader: "babel-loader", 74 | }, 75 | }, 76 | { 77 | test: /\.css$/, 78 | use: ["style-loader", "css-loader"], 79 | }, 80 | { 81 | test: /\.(png|svg|jpe?g|gif)$/, 82 | loader: "file-loader", 83 | options: { 84 | outputPath: "assets/", 85 | publicPath: "assets/", 86 | name: "[name].[ext]", 87 | }, 88 | }, 89 | ], 90 | }, 91 | 92 | // All of our "extra" stuff is currently being copies over 93 | // When time permits, lets have everything compile correclty 94 | plugins: [ 95 | new CleanWebpackPlugin(), 96 | new CopyPlugin({ 97 | patterns: [ 98 | { 99 | context: path.resolve(__dirname, "src"), 100 | from: "assets", 101 | to: "assets", 102 | }, 103 | ], 104 | }), 105 | new CopyPlugin({ 106 | patterns: [ 107 | { 108 | context: path.resolve(__dirname, "src"), 109 | from: "content-scripts", 110 | to: "content-scripts", 111 | }, 112 | ], 113 | }), 114 | new CopyPlugin({ 115 | patterns: [ 116 | { 117 | context: path.resolve( 118 | __dirname, 119 | env.chrome ? "src/manifests/chrome" : "src/manifests/firefox" 120 | ), 121 | from: isProduction ? "manifest-dist.json" : "manifest-dev.json", 122 | to: "manifest.json", 123 | }, 124 | ], 125 | }), 126 | new CopyPlugin({ 127 | patterns: [ 128 | { 129 | context: path.resolve(__dirname, "src"), 130 | from: "rules", 131 | to: "rules", 132 | }, 133 | ], 134 | }), 135 | new CopyPlugin({ 136 | patterns: [ 137 | { 138 | context: path.resolve(__dirname, "src/options"), 139 | from: "views", 140 | to: "views", 141 | }, 142 | ], 143 | }), 144 | new CopyPlugin({ 145 | patterns: [ 146 | { 147 | context: path.resolve(__dirname, "src/options"), 148 | from: "components", 149 | to: "components", 150 | }, 151 | ], 152 | }), 153 | new HtmlWebpackPlugin({ 154 | filename: "options.html", 155 | template: "src/options/options.html", 156 | chunks: ["options"], 157 | }), 158 | new HtmlWebpackPlugin({ 159 | filename: "popup.html", 160 | template: "src/popup/popup.html", 161 | chunks: ["popup"], 162 | }), 163 | ], 164 | }; 165 | }; 166 | -------------------------------------------------------------------------------- /src/background/storage.js: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md 3 | privacy-tech-lab, https://privacytechlab.org/ 4 | */ 5 | 6 | /* 7 | storage.js 8 | ================================================================================ 9 | storage.js handles OptMeowt's reads/writes of data to some local location 10 | */ 11 | 12 | import { openDB } from "idb"; 13 | import { reloadDynamicRules } from "../common/editRules.js"; 14 | import pkg from 'file-saver'; 15 | const { saveAs } = pkg; 16 | 17 | /******************************************************************************/ 18 | /************************** Enumerated settings *****************************/ 19 | /******************************************************************************/ 20 | 21 | // In general, these functions should be use with async / await for 22 | // syntactic sweetness & synchronous data handling 23 | // i.e., await storage.set(stores.settings, extensionMode.enabled, 'MODE') 24 | const stores = Object.freeze({ 25 | settings: "SETTINGS", 26 | domainlist: "DOMAINLIST", 27 | thirdParties: "THIRDPARTIES", 28 | wellknownInformation: "WELLKNOWNDATA", 29 | }); 30 | 31 | /******************************************************************************/ 32 | /************************* Main Storage Functions ***************************/ 33 | /******************************************************************************/ 34 | 35 | const dbPromise = openDB("extensionDB", 1, { 36 | upgrade: function dbPromiseInternal(db) { 37 | db.createObjectStore(stores.domainlist); 38 | db.createObjectStore(stores.settings); 39 | db.createObjectStore(stores.thirdParties); 40 | db.createObjectStore(stores.wellknownInformation); 41 | }, 42 | }); 43 | 44 | const storage = { 45 | async get(store, key) { 46 | if (typeof key === "undefined") { 47 | return undefined; 48 | } 49 | return (await dbPromise).get(store, key); 50 | }, 51 | async getAll(store) { 52 | return (await dbPromise).getAll(store); 53 | }, 54 | async getAllKeys(store) { 55 | return (await dbPromise).getAllKeys(store); 56 | }, 57 | // returns an object containing the given store 58 | async getStore(store) { 59 | const storeValues = await storage.getAll(store); 60 | const storeKeys = await storage.getAllKeys(store); 61 | let storeCopy = {}; 62 | let key; 63 | for (let index in storeKeys) { 64 | key = storeKeys[index]; 65 | storeCopy[key] = storeValues[index]; 66 | } 67 | return storeCopy; 68 | }, 69 | async set(store, value, key) { 70 | if (typeof key === "undefined") { 71 | return undefined; 72 | } 73 | return (await dbPromise).put(store, value, key); 74 | }, 75 | async delete(store, key) { 76 | if (typeof key === "undefined") { 77 | return undefined; 78 | } 79 | return (await dbPromise).delete(store, key); 80 | }, 81 | async clear(store) { 82 | return (await dbPromise).clear(store); 83 | }, 84 | }; 85 | 86 | /******************************************************************************/ 87 | /********************* Importing/Exporting Domain List **********************/ 88 | /******************************************************************************/ 89 | 90 | async function handleDownload() { 91 | const DOMAINLIST = await storage.getStore(stores.domainlist); 92 | let MANIFEST = chrome.runtime.getManifest(); 93 | let data = { 94 | VERSION: MANIFEST.version, 95 | DOMAINLIST: DOMAINLIST, 96 | }; 97 | 98 | let blob = new Blob([JSON.stringify(data, null, 4)], { 99 | type: "text/plain;charset=utf-8", 100 | }); 101 | saveAs(blob, "OptMeowt_backup.json"); 102 | } 103 | 104 | /** 105 | * Sets-up the process for importing a saved domainlist backup 106 | */ 107 | async function startUpload() { 108 | document.getElementById("upload-domainlist").value = ""; 109 | document.getElementById("upload-domainlist").click(); 110 | } 111 | 112 | /** 113 | * Imports and updates the domainlist in local storage with an imported backup 114 | */ 115 | async function handleUpload() { 116 | await storage.clear(stores.domainlist); 117 | const file = this.files[0]; 118 | const fr = new FileReader(); 119 | fr.onload = function (e) { 120 | const UPLOADED_DATA = JSON.parse(e.target.result); 121 | let version = UPLOADED_DATA.VERSION; 122 | let domainlist = UPLOADED_DATA.DOMAINLIST; 123 | version = version.split("."); 124 | 125 | let domainlist_keys = Object.keys(domainlist); 126 | let domainlist_vals = Object.values(domainlist); 127 | for (let i = 0; i < domainlist_keys.length; i++) { 128 | try { 129 | storage.set(stores.domainlist, domainlist_vals[i], domainlist_keys[i]); 130 | } catch (error) { 131 | alert("Error loading list"); 132 | } 133 | } 134 | // hardcode if it is the new version // check 135 | if (Number(version[0]) >= 3) { 136 | reloadDynamicRules(); 137 | updateRemovalScript(); 138 | } else { 139 | chrome.runtime.sendMessage({ 140 | msg: "FORCE_RELOAD", 141 | }); 142 | } 143 | }; 144 | fr.readAsText(file); 145 | } 146 | 147 | /******************************************************************************/ 148 | /******************************************************************************/ 149 | /******************************************************************************/ 150 | 151 | export { handleDownload, startUpload, handleUpload, stores, storage }; 152 | -------------------------------------------------------------------------------- /src/options/dark-mode.css: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md 3 | privacy-tech-lab, https://privacytechlab.org/ 4 | */ 5 | 6 | .darkmode--activated { 7 | background-color: #252522 !important; 8 | color: #eee !important; 9 | } 10 | 11 | .darkmode--activated .bg-light { 12 | background-color: #333 !important; 13 | } 14 | 15 | .darkmode--activated .bg-white { 16 | background-color: #111 !important; 17 | } 18 | 19 | .darkmode--activated .bg-black { 20 | background-color: #eee !important; 21 | } 22 | 23 | .darkmode--activated .text-color-darker { 24 | color: #dddddd !important; 25 | } 26 | 27 | .darkmode--activated .text-color { 28 | color: #adadad !important; 29 | } 30 | 31 | .darkmode--activated .question { 32 | color: #dddddd !important; 33 | } 34 | 35 | .darkmode--activated .answer { 36 | color: #adadad !important; 37 | } 38 | 39 | .darkmode--activated .divide { 40 | color: #ffff00 !important; 41 | } 42 | 43 | .darkmode--activated .uk-list-divide { 44 | color: #ffff00 !important; 45 | } 46 | 47 | .darkmode--activated .blue-heading { 48 | color: #93caf2 !important; 49 | } 50 | 51 | .darkmode--activated a { 52 | text-decoration: underline; 53 | color: #fff !important; 54 | } 55 | 56 | .darkmode--activated .uk-modal-body, 57 | .darkmode--activated .uk-modal-footer, 58 | .darkmode--activated .uk-modal-header { 59 | background-color: #252522 !important; 60 | } 61 | 62 | /********************************************************/ 63 | 64 | /* 65 | Radio buttons style settings page 66 | */ 67 | .darkmode--activated input[type="radio"] { 68 | width: 35px; 69 | height: 35px; 70 | transition: all ease 0.5s; 71 | } 72 | 73 | .darkmode--activated input[type="radio"]:checked { 74 | transition: all ease 0.25s; 75 | background-image: var(--accent-color); 76 | box-shadow: 0 6px 12px #222221; 77 | } 78 | 79 | .darkmode--activated input[type="radio"]:hover:not(:checked) { 80 | transition: all ease 0.25s; 81 | box-shadow: 0 6px 12px #222221; 82 | background-color: #cbcbcb; 83 | /* border-color: transparent; */ 84 | } 85 | 86 | /* 87 | Settings page 88 | */ 89 | .darkmode--activated .navbar-item.active { 90 | color: #3d87e9; 91 | } 92 | 93 | /* 94 | 'settings-view' domainlist import & export button style 95 | */ 96 | .darkmode--activated .importexport-button { 97 | background-color: #252522 !important; 98 | border-color: #cbcbcb !important; 99 | color: #cbcbcb !important; 100 | box-shadow: 0 8px 16px -1px #111111 !important; 101 | } 102 | .darkmode--activated .importexport-button:hover { 103 | background-color: #d4d4d4 !important; 104 | border-color: #d4d4d4 !important; 105 | color: #4e4e4d !important; 106 | box-shadow: 0 6px 12px #303030 !important; 107 | } 108 | 109 | .darkmode--activated .domainlist-navbar { 110 | background-color: #252522 !important; 111 | color: #cbcbcb !important; 112 | } 113 | 114 | .darkmode--activated .analysis-navbar { 115 | background-color: #252522 !important; 116 | color: #cbcbcb !important; 117 | } 118 | 119 | .darkmode--activated .sticky { 120 | box-shadow: 0 8px 8px -10px #111111 !important; 121 | } 122 | 123 | /********************************************************/ 124 | 125 | /* 126 | Buttons and Switches 127 | */ 128 | 129 | .darkmode--activated .optMode { 130 | border: 1px solid rgb(238, 238, 238); 131 | color: rgb(238, 238, 238); 132 | box-shadow: 0 3px 6px -1px #111111 !important; 133 | } 134 | 135 | .darkmode--activated .optMode:hover { 136 | color: var(--text-white) !important; 137 | } 138 | 139 | .darkmode--activated .search { 140 | background-color: #333 !important; 141 | border-color: #333 !important; 142 | color: #cbcbcb; 143 | } 144 | 145 | .darkmode--activated .button { 146 | background-color: rgb(70, 70, 70) !important; 147 | border: 1px solid rgb(70, 70, 70) !important; 148 | transition: all 0.3s ease; 149 | color: #eee !important; 150 | } 151 | 152 | .darkmode--activated .uk-badge { 153 | background-color: rgb(37, 37, 34) !important; 154 | } 155 | 156 | .darkmode--activated .button:hover { 157 | background-color: rgb(143, 17, 17) !important; 158 | border: 1px solid rgb(143, 17, 17) !important; 159 | transition: all 0.3s ease; 160 | color: #eee !important; 161 | } 162 | 163 | .darkmode--activated .switch input:checked + span { 164 | background: #37be70 !important; 165 | box-shadow: 0 0px 0px 0px rgba(72, 234, 139, 0.2) !important; 166 | } 167 | 168 | .darkmode--activated .switch input + span { 169 | box-shadow: 0 0px 0px 0px rgba(72, 234, 139, 0.2) !important; 170 | } 171 | 172 | .darkmode--activated .domain-list:hover { 173 | background-color: #5f5f5d !important; 174 | color: white !important; 175 | } 176 | 177 | .darkmode--activated .dropdown-tab:hover { 178 | background-color: #3f3f3c !important; 179 | color: #eee !important; 180 | } 181 | 182 | .darkmode--activated .dropdown-tab-click { 183 | background-color: #3f3f3c !important; 184 | color: #eee !important; 185 | } 186 | 187 | .darkmode--activated .wellknown-bg { 188 | background-color: #3f3f3c !important; 189 | border-color: #3f3f3c !important; 190 | color: #eeeeee; 191 | } 192 | 193 | /* .darkmode--activated .dark-checkbox { 194 | background-color: #333 !important; 195 | border: 1px solid #333 !important; 196 | color: #eee !important; 197 | } */ 198 | 199 | /* input[type="radio"]:checked, .darkmode--activated { 200 | box-shadow: 0 6px 12px #000 !important; 201 | } 202 | input[type='radio']:hover:not(:checked), .darkmode--activated { 203 | box-shadow: 0 6px 12px #000 !important; 204 | } */ 205 | -------------------------------------------------------------------------------- /src/content-scripts/contentScript.js: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md 3 | privacy-tech-lab, https://privacytechlab.org/ 4 | */ 5 | 6 | /* 7 | contentScripts.js 8 | ================================================================================ 9 | contentScripts.js runs on every page and passes data to the background page 10 | https://developer.chrome.com/extensions/content_scripts 11 | */ 12 | 13 | // Here is a resource I used to help setup the inject script functionality as 14 | // well as setup message listeners to pass data back to the background 15 | // https://www.freecodecamp.org/news/chrome-extension-message-passing-essentials/ 16 | 17 | /******************************************************************************/ 18 | /******************************************************************************/ 19 | /********** # USPAPI call helper functions **********/ 20 | /******************************************************************************/ 21 | /******************************************************************************/ 22 | 23 | 24 | // To be injected to call the USPAPI function in analysis mode 25 | const uspapi = ` 26 | try { 27 | __uspapi('getUSPData', 1, (data) => { 28 | let currURL = document.URL 29 | window.postMessage({ type: "USPAPI_TO_CONTENT_SCRIPT", result: data, url: currURL }); 30 | }); 31 | } 32 | `; 33 | 34 | const uspapiRequest = ` 35 | try { 36 | __uspapi('getUSPData', 1, (data) => { 37 | let currURL = document.URL 38 | window.postMessage({ type: "USPAPI_TO_CONTENT_SCRIPT_REQUEST", result: data, url: currURL }); 39 | }); 40 | } catch (e) { 41 | window.postMessage({ type: "USPAPI_TO_CONTENT_SCRIPT_REQUEST", result: "USPAPI_FAILED" }); 42 | } 43 | `; 44 | 45 | function injectScript(script) { 46 | const scriptElem = document.createElement("script"); 47 | scriptElem.innerHTML = script; 48 | document.documentElement.prepend(scriptElem); 49 | } 50 | 51 | async function isWellknownCheckEnabled() { 52 | try { 53 | const { WELLKNOWN_CHECK_ENABLED } = await chrome.storage.local.get( 54 | "WELLKNOWN_CHECK_ENABLED" 55 | ); 56 | if (typeof WELLKNOWN_CHECK_ENABLED === "boolean") { 57 | return WELLKNOWN_CHECK_ENABLED; 58 | } 59 | } catch (error) { 60 | print(error); 61 | } 62 | try { 63 | const response = await chrome.runtime.sendMessage({ 64 | msg: "GET_WELLKNOWN_CHECK_ENABLED", 65 | }); 66 | if (response && typeof response.enabled === "boolean") { 67 | return response.enabled; 68 | } 69 | } catch (error) { 70 | print(error); 71 | } 72 | // If we can't determine the setting, err on the side of not fetching. 73 | return false; 74 | } 75 | 76 | /******************************************************************************/ 77 | /******************************************************************************/ 78 | /********** # Main functionality **********/ 79 | /******************************************************************************/ 80 | /******************************************************************************/ 81 | 82 | async function getWellknown(url) { 83 | const response = await fetch(`${url.origin}/.well-known/gpc.json`); 84 | new_url = url = JSON.parse(JSON.stringify(url)); 85 | let wellknownData; 86 | try { 87 | wellknownData = await response.json(); 88 | } catch { 89 | wellknownData = null; 90 | } 91 | chrome.runtime.sendMessage({ 92 | msg: "CONTENT_SCRIPT_WELLKNOWN", 93 | data: wellknownData, 94 | origin_url: new_url 95 | }); 96 | } 97 | 98 | /** 99 | * Passes info to background scripts for processing via messages 100 | * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/sendMessage 101 | * There are other ways to do this, but I use an IIFE to run everything at once 102 | * https://developer.mozilla.org/en-US/docs/Glossary/IIFE 103 | */ 104 | (() => { 105 | /* MAIN CONTENT SCRIPT PROCESSES GO HERE */ 106 | 107 | let url = new URL(location); // location object 108 | isWellknownCheckEnabled().then((shouldCheck) => { 109 | if (shouldCheck) { 110 | getWellknown(url); 111 | } 112 | }); 113 | })(); 114 | 115 | /******************************************************************************/ 116 | /******************************************************************************/ 117 | /********** # Message passing from injected script via window **********/ 118 | /******************************************************************************/ 119 | /******************************************************************************/ 120 | 121 | chrome.runtime.onMessage.addListener(function (message, sender, sendResponse) { // check unused arguments 122 | if (message.msg === "USPAPI_FETCH_REQUEST") { 123 | injectScript(uspapiRequest); 124 | } 125 | }); 126 | 127 | window.addEventListener( 128 | "message", 129 | function (event) { 130 | if ( 131 | event.data.type == "USPAPI_TO_CONTENT_SCRIPT" 132 | ) { 133 | chrome.runtime.sendMessage({ 134 | msg: "USPAPI_TO_BACKGROUND", 135 | data: event.data.result, 136 | location: this.location.href, 137 | }); 138 | } 139 | if (event.data.type == "USPAPI_TO_CONTENT_SCRIPT_REQUEST") { 140 | chrome.runtime.sendMessage({ 141 | msg: "USPAPI_TO_BACKGROUND_FROM_FETCH_REQUEST", 142 | data: event.data.result, 143 | location: this.location.href, 144 | }); 145 | } 146 | }, 147 | false 148 | ); 149 | -------------------------------------------------------------------------------- /src/assets/gpc-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 58 | 59 | -------------------------------------------------------------------------------- /test/background/cookieRemoval.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md 3 | privacy-tech-lab, https://privacytechlab.org/ 4 | */ 5 | 6 | /* 7 | cookieRemoval.test.js 8 | ================================================================================ 9 | Verifies that legacy cookie-based opt-out code was fully removed when the 10 | extension dropped IAB/NAI cookie support. 11 | */ 12 | 13 | import assert from "assert"; 14 | import fs from "fs"; 15 | import path from "path"; 16 | import { fileURLToPath } from "url"; 17 | 18 | const __filename = fileURLToPath(import.meta.url); 19 | const __dirname = path.dirname(__filename); 20 | const repoRoot = path.resolve(__dirname, "../../"); 21 | 22 | const resolvePath = (relativePath) => path.resolve(repoRoot, relativePath); 23 | const readFile = (relativePath) => 24 | fs.readFileSync(resolvePath(relativePath), "utf8"); 25 | 26 | /** 27 | * Recursively searches a directory for a given string. 28 | * Stops early once the string is found. 29 | * NOTE: Only used on the `src` tree which is small enough for sync traversal. 30 | */ 31 | function containsInDir(dirPath, needle) { 32 | const entries = fs.readdirSync(dirPath, { withFileTypes: true }); 33 | for (const entry of entries) { 34 | if (entry.name.startsWith(".")) { 35 | continue; 36 | } 37 | const fullPath = path.join(dirPath, entry.name); 38 | if (entry.isDirectory()) { 39 | if (containsInDir(fullPath, needle)) { 40 | return true; 41 | } 42 | } else { 43 | const content = fs.readFileSync(fullPath, "utf8"); 44 | if (content.includes(needle)) { 45 | return true; 46 | } 47 | } 48 | } 49 | return false; 50 | } 51 | 52 | it("confirms legacy cookie source files were removed", () => { 53 | const removedFiles = [ 54 | "src/background/cookiesIAB.js", 55 | "src/background/protection/cookiesOnInstall.js", 56 | "src/background/storageCookies.js", 57 | "src/data/cookie_list.js", 58 | ]; 59 | 60 | removedFiles.forEach((filePath) => { 61 | assert.strictEqual( 62 | fs.existsSync(resolvePath(filePath)), 63 | false, 64 | `Expected ${filePath} to be removed` 65 | ); 66 | }); 67 | }); 68 | 69 | describe("Check parsing of IAB signal", () => { 70 | it("should not reference parse helpers in chrome protection background", () => { 71 | const content = readFile("src/background/protection/protection.js"); 72 | assert.ok( 73 | !content.includes("parseIAB") && !content.includes("cookiesIAB"), 74 | "chrome protection background still references legacy parse helpers" 75 | ); 76 | }); 77 | 78 | it("should not reference parse helpers in firefox protection background", () => { 79 | const content = readFile("src/background/protection/protection-ff.js"); 80 | assert.ok( 81 | !content.includes("parseIAB") && !content.includes("cookiesIAB"), 82 | "firefox protection background still references legacy parse helpers" 83 | ); 84 | }); 85 | 86 | it("should not reference parse helpers in popup UI state", () => { 87 | const content = readFile("src/popup/popup.js"); 88 | assert.ok( 89 | !content.includes("parseIAB") && !content.includes("cookiesIAB"), 90 | "popup still references legacy parse helpers" 91 | ); 92 | }); 93 | 94 | it("should not ship legacy parse tests", () => { 95 | assert.strictEqual( 96 | fs.existsSync(resolvePath("test/background/parseIAB.test.js")), 97 | false, 98 | "parseIAB.test.js should have been removed" 99 | ); 100 | }); 101 | }); 102 | 103 | describe("Checks if cookie is stored per domain/subdomain", () => { 104 | const storageContent = readFile("src/background/storage.js"); 105 | 106 | it("should not import storageCookies helper", () => { 107 | assert.ok( 108 | !storageContent.includes("storageCookies"), 109 | "storage.js still imports storageCookies helper" 110 | ); 111 | }); 112 | 113 | it("should guard storage.get against undefined keys", () => { 114 | const guardRegex = 115 | /async get\(store, key\)[\s\S]*?if \(typeof key === "undefined"\)/; 116 | assert.ok( 117 | guardRegex.test(storageContent), 118 | "storage.get does not guard against undefined keys" 119 | ); 120 | }); 121 | 122 | it("should guard storage.set against undefined keys", () => { 123 | const guardRegex = 124 | /async set\(store, value, key\)[\s\S]*?if \(typeof key === "undefined"\)/; 125 | assert.ok( 126 | guardRegex.test(storageContent), 127 | "storage.set does not guard against undefined keys" 128 | ); 129 | }); 130 | 131 | it("should guard storage.delete against undefined keys", () => { 132 | const guardRegex = 133 | /async delete\(store, key\)[\s\S]*?if \(typeof key === "undefined"\)/; 134 | assert.ok( 135 | guardRegex.test(storageContent), 136 | "storage.delete does not guard against undefined keys" 137 | ); 138 | }); 139 | 140 | it("should not call addCookiesForGivenDomain helper", () => { 141 | assert.ok( 142 | !storageContent.includes("addCookiesForGivenDomain"), 143 | "storage.js still references addCookiesForGivenDomain" 144 | ); 145 | }); 146 | 147 | it("should not call deleteCookiesForGivenDomain helper", () => { 148 | assert.ok( 149 | !storageContent.includes("deleteCookiesForGivenDomain"), 150 | "storage.js still references deleteCookiesForGivenDomain" 151 | ); 152 | }); 153 | }); 154 | 155 | describe("Check different IAB signals for validity", () => { 156 | const srcRoot = resolvePath("src"); 157 | 158 | it("should not reference isValidSignalIAB helper in source files", () => { 159 | assert.strictEqual( 160 | containsInDir(srcRoot, "isValidSignalIAB"), 161 | false, 162 | "Found isValidSignalIAB reference in src" 163 | ); 164 | }); 165 | 166 | it("should not reference makeCookieIAB helper in source files", () => { 167 | assert.strictEqual( 168 | containsInDir(srcRoot, "makeCookieIAB"), 169 | false, 170 | "Found makeCookieIAB reference in src" 171 | ); 172 | }); 173 | 174 | it("should not reference pruneCookieIAB helper in source files", () => { 175 | assert.strictEqual( 176 | containsInDir(srcRoot, "pruneCookieIAB"), 177 | false, 178 | "Found pruneCookieIAB reference in src" 179 | ); 180 | }); 181 | 182 | it("should not reference legacy cookie_list dataset", () => { 183 | assert.strictEqual( 184 | containsInDir(srcRoot, "cookie_list"), 185 | false, 186 | "Found cookie_list reference in src" 187 | ); 188 | }); 189 | }); 190 | -------------------------------------------------------------------------------- /src/common/editDomainlist.js: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md 3 | privacy-tech-lab, https://privacytechlab.org/ 4 | */ 5 | 6 | /* 7 | editDomainlist.js 8 | ================================================================================ 9 | editDomainlist.js is an internal API modifying the domainlist / modifying the 10 | domainlist simultaneously with the dynamic ruleset 11 | */ 12 | 13 | import { storage, stores } from "../background/storage.js"; 14 | import { 15 | deleteAllDynamicRules, 16 | deleteDynamicRule, 17 | addDynamicRule, 18 | getFreshId, 19 | } from "./editRules.js"; 20 | 21 | /* # Debugging */ 22 | // debug_domainlist_and_dynamicrules 23 | // print_rules_and_domainlist 24 | 25 | /******************************************************************************/ 26 | /******************************************************************************/ 27 | /********** # Standard Operation **********/ 28 | /******************************************************************************/ 29 | /******************************************************************************/ 30 | 31 | async function updateRemovalScript() { 32 | let ex_matches = ["https://example.org/foo/bar.html"]; 33 | let domain; 34 | let domainValue; 35 | const domainlistKeys = await storage.getAllKeys(stores.domainlist); 36 | const domainlistValues = await storage.getAll(stores.domainlist); 37 | for (let index in domainlistKeys) { 38 | domain = domainlistKeys[index]; 39 | domainValue = domainlistValues[index]; 40 | if (domainValue != null) { 41 | ex_matches.push("https://" + domain + "/*"); 42 | ex_matches.push("https://www." + domain + "/*"); 43 | } 44 | } 45 | chrome.scripting 46 | .updateContentScripts([ 47 | { 48 | id: "1", 49 | matches: [""], 50 | excludeMatches: ex_matches, 51 | js: ["content-scripts/registration/gpc-dom.js"], 52 | runAt: "document_start", 53 | }, 54 | ]) 55 | .then(() => {}); 56 | } 57 | 58 | async function createCS(domain){ 59 | let script = await chrome.scripting.getRegisteredContentScripts({ 60 | }); 61 | 62 | let ex_matches = script[0].excludeMatches; 63 | 64 | ex_matches.push("https://" + domain + "/*"); 65 | ex_matches.push("https://www." + domain + "/*"); 66 | 67 | await chrome.scripting.updateContentScripts([ 68 | { 69 | id: "1", 70 | matches: [""], 71 | excludeMatches: ex_matches, 72 | js: ["content-scripts/registration/gpc-dom.js"], 73 | runAt: "document_start", 74 | }, 75 | ]) 76 | .then(() => {}); 77 | } 78 | 79 | async function deleteCS(domain){ 80 | let script = await chrome.scripting.getRegisteredContentScripts({ 81 | }); 82 | let ex_matches = script[0].excludeMatches; 83 | function removeItemOnce(arr, value) { 84 | var index = arr.indexOf(value); 85 | if (index > -1) { 86 | arr.splice(index, 1); 87 | } 88 | return arr; 89 | } 90 | 91 | ex_matches = removeItemOnce(ex_matches,"https://" + domain + "/*"); 92 | ex_matches = removeItemOnce(ex_matches,"https://www." + domain + "/*"); 93 | await chrome.scripting.updateContentScripts([ 94 | { 95 | id: "1", 96 | matches: [""], 97 | excludeMatches: ex_matches, 98 | js: ["content-scripts/registration/gpc-dom.js"], 99 | runAt: "document_start", 100 | }, 101 | ]) 102 | .then(() => {}); 103 | } 104 | 105 | async function deleteDomainlistAndDynamicRules() { 106 | await storage.clear(stores.domainlist); 107 | deleteAllDynamicRules(); 108 | } 109 | 110 | async function addDomainToDomainlistAndRules(domain) { 111 | let id = 1; 112 | id = await getFreshId(); 113 | addDynamicRule(id, domain); // add the rule for the chosen domain 114 | createCS(domain); 115 | await storage.set(stores.domainlist, id, domain); // record what rule the domain is associated to 116 | } 117 | 118 | async function removeDomainFromDomainlistAndRules(domain) { 119 | let id = await storage.get(stores.domainlist, domain); 120 | deleteDynamicRule(id); 121 | deleteCS(domain); 122 | await storage.set(stores.domainlist, null, domain); 123 | } 124 | 125 | /******************************************************************************/ 126 | /******************************************************************************/ 127 | /********** # Debugging **********/ 128 | /******************************************************************************/ 129 | /******************************************************************************/ 130 | 131 | async function debug_domainlist_and_dynamicrules() { 132 | let sampleSites = [ // not called 133 | "a.com", 134 | "b.com", 135 | "c.com", 136 | "d.com", 137 | "e.com", 138 | "f.com", 139 | "g.com", 140 | "h.com", 141 | "i.com", 142 | "j.com", 143 | ]; 144 | await deleteDomainlistAndDynamicRules(); 145 | await print_rules_and_domainlist(); 146 | addDynamicRule(2, "nytimes.com"); // add the rule for the chosen domain 147 | await storage.set(stores.domainlist, 2, "nytimes.com"); // record what rule the domain is associated to 148 | 149 | await print_rules_and_domainlist(); 150 | await addDomainToDomainlistAndRules("a.com"); 151 | await addDomainToDomainlistAndRules("b.com"); 152 | await addDomainToDomainlistAndRules("c.com"); 153 | await addDomainToDomainlistAndRules("d.com"); 154 | await addDomainToDomainlistAndRules("e.com"); 155 | await addDomainToDomainlistAndRules("f.com"); 156 | await addDomainToDomainlistAndRules("g.com"); 157 | await addDomainToDomainlistAndRules("h.com"); 158 | await addDomainToDomainlistAndRules("i.com"); 159 | await addDomainToDomainlistAndRules("j.com"); 160 | await print_rules_and_domainlist(); 161 | } 162 | 163 | async function print_rules_and_domainlist() { 164 | let rules = await chrome.declarativeNetRequest.getDynamicRules(); 165 | let domainlist = await storage.getStore(stores.domainlist); 166 | console.log( 167 | "Here are the curr dynamic rules:", 168 | rules, 169 | "Here is our curr domainlist: ", 170 | domainlist 171 | ); 172 | } 173 | 174 | /******************************************************************************/ 175 | /******************************************************************************/ 176 | /******************************************************************************/ 177 | 178 | export { 179 | deleteDomainlistAndDynamicRules, 180 | addDomainToDomainlistAndRules, 181 | removeDomainFromDomainlistAndRules, 182 | updateRemovalScript, 183 | debug_domainlist_and_dynamicrules, 184 | print_rules_and_domainlist, 185 | deleteCS, 186 | createCS 187 | }; 188 | -------------------------------------------------------------------------------- /README_ARCHITECTURE.md: -------------------------------------------------------------------------------- 1 | # Architecture Overview 2 | 3 | ```txt 4 | src 5 | ├── assets # Static images & files 6 | ├── background # Manages the background script processes 7 | │   ├── protection 8 | │   │ ├── background.js 9 | │   │ ├── listeners-chrome.js 10 | │   │ ├── listeners-firefox.js 11 | │   │ ├── protection-ff.js 12 | │   │ └── protection.js 13 | │   ├── control.js 14 | │   └── storage.js 15 | ├── common # Manages header sending and rules 16 | │   ├── editDomainlist.js 17 | │   └── editRules.js 18 | ├── content-scripts # Runs processes on site on adds DOM signal 19 | │   ├── injection 20 | │   │   └── gpc-dom.js 21 | │   ├── registration 22 | │   │   └── gpc-dom.js 23 | │   └── contentScript.js 24 | ├── data # Stores constant data (DNS signals, settings, etc.) 25 | │   ├── defaultSettings.js 26 | │   ├── headers.js 27 | │   └── regex.js 28 | ├── manifests # Stores manifests 29 | │   ├── chrome 30 | │   │   ├── manifest-dev.json 31 | │   │   └── manifest-dist.json 32 | │   ├── firefox 33 | │   │   ├── manifest-dev.json 34 | │   │   └── manifest-dist.json 35 | ├── options # Options page frontend 36 | │   ├── components 37 | │   │   ├── scaffold-component.html 38 | │   │   └── util.js 39 | │   ├── views 40 | │   │ ├── about-view 41 | │   │ │   ├── about-view.html 42 | │   │ │   └── about-view.js 43 | │   │ ├── domainlist-view 44 | │   │ │   ├── domainlist-view.html 45 | │   │ │   └── domainlist-view.js 46 | │   │ ├── main-view 47 | │   │ │   ├── main-view.html 48 | │   │ │   └── main-view.js 49 | │   │ └── settings-view 50 | │   │ ├── settings-view.html 51 | │   │ └── settings-view.js 52 | │   ├── dark-mode.css 53 | │   ├── options.html 54 | │   ├── options.js 55 | │   └── styles.css 56 | ├── popup # Popup page frontend 57 | │   ├── popup.html 58 | │   ├── popup.js 59 | │   └── styles.css 60 | ├── rules # Manages universal rules 61 | │   ├── gpc_exceptions_rules.json 62 | │   └── universal_gpc_rules.json 63 | └── theme # Contains darkmode 64 |    └── darkmode.js 65 | test 66 | └── background 67 | └── gpc.test.js 68 | ``` 69 | 70 | The following source folders have detailed descriptions further in the document. 71 | 72 | [background](#background)\ 73 | [common](#common)\ 74 | [content-scripts](#content-scripts)\ 75 | [data](#data)\ 76 | [manifests](#manifests)\ 77 | [options](#options)\ 78 | [popup](#popup)\ 79 | [rules](#rules)\ 80 | [theme](#theme) 81 | 82 | ## background 83 | 84 | 1. `protection` 85 | 2. `control.js` 86 | 3. `storage.js` 87 | 88 | ### `src/background/protection` 89 | 90 | 1. `background.js` 91 | 2. `listeners-chrome.js` 92 | 3. `listeners-firefox.js` 93 | 4. `protection.js` 94 | 5. `protection-ff.js` 95 | 96 | #### `protection/background.js` 97 | 98 | Initializes the protection mode listeners. 99 | 100 | #### `protection/listeners-chrome.js` and `protection/listeners-firefox.js` 101 | 102 | Creates listeners for Chrome and Firefox, respectively. 103 | 104 | #### `protection/protection.js` 105 | 106 | Manages the domain list with functions like `logData();`, `updateDomainlistAndSignal();`, `pullToDomainlistCache();`, `syncDomainlists();`. Also responsible for supplying the popup with the proper information with `dataToPopup();`. Also creates listeners to watch the popup for domain list changes. 107 | 108 | #### `protection/protection-ff.js` 109 | 110 | Manages the domain list for Firefox. 111 | 112 | ### `background/control.js` 113 | 114 | Uses `protection.js` to turn the extension on and off. 115 | 116 | ### `background/storage.js` 117 | 118 | Handles storage uploads and downloads. 119 | 120 | ## common 121 | 122 | 1. `editDomainlist.js` 123 | 2. `editRules.js` 124 | 125 | This folder holds common internal API's to be used throughout the extension. 126 | 127 | ### `common/editDomainlist.js` 128 | 129 | Is an internal API to be used for editing a users domain list. 130 | 131 | ### `common/editRules.js` 132 | 133 | Is an internal API to be used for editing rules that allow us to send the GPC header. 134 | 135 | ## content-scripts 136 | 137 | 1. `injection` 138 | 2. `registration` 139 | 3. `contentScript.js` 140 | 141 | This folder contains our main content script and methods for injecting the GPC signal into the DOM. 142 | 143 | ### `src/content-scripts/injection` 144 | 145 | 1. `gpc-dom.js` 146 | 147 | `gpc-dom.js` injects the DOM signal. 148 | 149 | ### `src/content-scripts/registration` 150 | 151 | 1. `gpc-dom.js` 152 | 153 | This file injects `injection/gpc-dom.js` into the page using a static script. (Based on [this stack overflow thread](https://stackoverflow.com/questions/9515704/use-a-content-script-to-access-the-page-context-variables-and-functions)) 154 | 155 | ### `content-scripts/contentScript.js` 156 | 157 | This runs on every page and sends information to signal background processes. 158 | 159 | ## data 160 | 161 | 1. `defaultSettings.js` 162 | 2. `headers.js` 163 | 3. `regex.js` 164 | 165 | This folder contains static data. 166 | 167 | ### `data/defaultSettings.js` 168 | 169 | Contains the default OptMeowt settings. 170 | 171 | ### `data/headers.js` 172 | 173 | Contains the default headers to be attached to online requests. 174 | 175 | ### `data/regex.js` 176 | 177 | Contains regular expressions for finding "do not sell" links and related privacy signals. 178 | 179 | ## manifests 180 | 181 | 1. `chrome` 182 | 2. `firefox` 183 | 184 | Contains the extension manifests 185 | 186 | ### `manifests/chrome` 187 | 188 | 1. `manifest-dev.json` 189 | 2. `manifest-dist.json` 190 | 191 | Contains the development and distribution manifests for Chrome 192 | 193 | ### `manifests/firefox` 194 | 195 | 1. `manifest-dev.json` 196 | 2. `manifest-dist.json` 197 | 198 | Contains the development and distribution manifests for Firefox 199 | 200 | ## options 201 | 202 | 1. `components` 203 | 2. `views` 204 | 3. `dark-mode.css` 205 | 4. `options.html` 206 | 207 | This folder contains all of the frontend code 208 | 209 | ### `options/components` 210 | 211 | 1. `scaffold-component.html` 212 | 2. `util.js` 213 | 214 | This folder contains the basic layout of every options page and helper functions to help render the pages. 215 | 216 | ### `options/views` 217 | 218 | 1. `about-view` 219 | 2. `domainlist-view` 220 | 3. `main-view` 221 | 4. `settings-view` 222 | 223 | Contains all frontend and implementation of the settings pages. 224 | 225 | #### `views/about-view` 226 | 227 | 1. `about-view.html` 228 | 2. `about-view.js` 229 | 230 | Builds the "about" page 231 | 232 | #### `views/domainlist-view` 233 | 234 | 1. `domainlist-view.html` 235 | 2. `domainlist-view.js` 236 | 237 | Builds the domain list page 238 | 239 | #### `views/main-view` 240 | 241 | 1. `main-view.html` 242 | 2. `main-view.js` 243 | 244 | Builds the main options page 245 | 246 | #### `views/settings-view` 247 | 248 | 1. `settings-view.html` 249 | 2. `settings-view.js` 250 | 251 | Builds the settings page 252 | 253 | ### `options/dark-mode.css` 254 | 255 | Contains the dark-mode styles for OptMeowt. 256 | 257 | ### `options/options.html` and `options/options.js` 258 | 259 | Is the entry point for the main options page. 260 | 261 | ### `options/styles.css` 262 | 263 | Contains the basic styles for OptMeowt. 264 | 265 | ## popup 266 | 267 | 1. `popup.html` 268 | 2. `popup.js` 269 | 3. `styles.css` 270 | 271 | Contains the frontend and implementation for the OptMeowt popup. 272 | 273 | ## rules 274 | 275 | 1. `gpc_exception_rules.json` 276 | 2. `universal_gpc_rules.json` 277 | 278 | Contains rule framework for sending GPC headers to sites. 279 | 280 | ## theme 281 | 282 | 1. `darkmode.js` 283 | 284 | Contains the dark mode functionality. 285 | 286 | **Links to APIs:** 287 | 288 | Chrome: [webRequest](https://developer.chrome.com/docs/extensions/reference/webRequest/) and [webNavigation](https://developer.chrome.com/docs/extensions/reference/webNavigation/) 289 | 290 | Firefox: [webRequest](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webRequest) and [webNavigation](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webNavigation) 291 | -------------------------------------------------------------------------------- /src/options/views/settings-view/settings-view.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 13 | 14 | 15 | 19 | 203 | -------------------------------------------------------------------------------- /src/options/styles.css: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md 3 | privacy-tech-lab, https://privacytechlab.org/ 4 | */ 5 | 6 | /* 7 | styles.css 8 | ================================================================================ 9 | styles.css is the main css page for OptMeowt's options page 10 | */ 11 | 12 | @import "./dark-mode.css"; 13 | 14 | /********************************************************/ 15 | 16 | /* 17 | Colors 18 | */ 19 | :root { 20 | --accent-color: #4472c4; 21 | --accent-color-lighter-80: #dae3f3; 22 | --accent-color-lighter-60: #b4c7e7; 23 | --accent-color-lighter-40: #8faadc; 24 | --accent-color-darker-25: #2f5597; 25 | --accent-color-darker-50: #203864; 26 | 27 | /* --text-color: #888fa1; */ 28 | --text-color: #5a647d; 29 | /* --text-color-darker: #5a647d ; */ 30 | --text-color-darker: #353b4a; 31 | --text-color-inactive: #d3d3d3; 32 | 33 | --highlight-light: #f2f2f2; 34 | 35 | --text-gray: rgb(89, 98, 127); 36 | } 37 | 38 | .text-color-darker { 39 | color: var(--text-color-darker); 40 | } 41 | 42 | .text-color { 43 | color: var(--text-color); 44 | } 45 | 46 | /********************************************************/ 47 | 48 | /* 49 | Body style 50 | */ 51 | html { 52 | background-color: inherit; 53 | } 54 | 55 | body { 56 | color: var(--text-color); 57 | user-select: none; 58 | } 59 | 60 | pre, 61 | code { 62 | white-space: pre-line; 63 | } 64 | 65 | a { 66 | text-decoration: underline; 67 | } 68 | 69 | /********************************************************/ 70 | 71 | /* 72 | About page Q/A heading style 73 | */ 74 | .question { 75 | color: var(--text-color-darker); 76 | font-size: x-large; 77 | user-select: none; 78 | } 79 | 80 | .answer { 81 | color: var(--text-color); 82 | font-size: medium; 83 | user-select: none; 84 | } 85 | 86 | /********************************************************/ 87 | 88 | /* 89 | Navbar item style 90 | */ 91 | .navbar-item { 92 | color: var(--text-color-inactive); 93 | opacity: 1; 94 | cursor: pointer; 95 | transition: all ease 0.5s; 96 | } 97 | 98 | .navbar-item.active { 99 | color: var(--accent-color); 100 | } 101 | 102 | /********************************************************/ 103 | 104 | /* 105 | Sticky Domain List Navbar 106 | */ 107 | .domainlist-navbar { 108 | background-color: white; 109 | padding-top: 5px; 110 | } 111 | /* 112 | Sticky Analysis List Navbar 113 | */ 114 | 115 | /* Added with */ 116 | .sticky { 117 | position: fixed; 118 | top: 0; 119 | width: 100%; 120 | transition: all ease 0.25s; 121 | box-shadow: 0 8px 8px -10px var(--accent-color-lighter-60); 122 | z-index: 100; 123 | } 124 | 125 | /********************************************************/ 126 | 127 | /* 128 | Radio buttons style 129 | */ 130 | input[type="radio"] { 131 | width: 35px; 132 | height: 35px; 133 | transition: all ease 0.5s; 134 | } 135 | 136 | input[type="radio"]:checked { 137 | transition: all ease 0.25s; 138 | background-image: var(--accent-color); 139 | box-shadow: 0 4px 8px var(--accent-color-lighter-60); 140 | } 141 | 142 | input[type="radio"]:hover:not(:checked) { 143 | transition: all ease 0.25s; 144 | box-shadow: 0 4px 8px var(--accent-color-lighter-60); 145 | border-color: transparent; 146 | } 147 | 148 | /********************************************************/ 149 | 150 | /* 151 | `domainlist-view` options page checkbox style 152 | */ 153 | .check { 154 | /* appearance: none; */ 155 | transform: scale(1.25); 156 | /* transform-style: inherit; */ 157 | /* z-index: -10; */ 158 | } 159 | 160 | /********************************************************/ 161 | 162 | /* 163 | 'settings-view' domainlist import & export button style 164 | */ 165 | .importexport-button { 166 | background-color: white; 167 | border-color: #888fa1; 168 | color: #888fa1; 169 | padding: 12px 16px; 170 | text-align: center; 171 | text-decoration-color: none; 172 | font-size: 14px; 173 | display: inline-block; 174 | border-radius: 10px; 175 | border-style: solid; 176 | box-shadow: 0 8px 16px -1px rgba(211, 211, 211, 0.2); 177 | outline: none; 178 | transition: all ease 0.25s; 179 | } 180 | .importexport-button:hover { 181 | background-color: var(--accent-color); 182 | border-color: var(--accent-color); 183 | color: white; 184 | box-shadow: 0 6px 12px var(--accent-color-lighter-60); 185 | transition: all ease 0.25s; 186 | } 187 | 188 | .button:hover { 189 | background-color: #df3131 !important; 190 | border: 1px solid #df3131 !important; 191 | transition: all 0.3s ease; 192 | color: #fff !important; 193 | } 194 | 195 | .uspStringElem { 196 | margin: auto; 197 | padding-top: 10px; 198 | padding-bottom: 10px; 199 | padding-right: 8px; 200 | padding-left: 8px; 201 | background-color: white; 202 | border: 1px solid var(--text-gray); 203 | color: var(--text-gray); 204 | text-align: center; 205 | } 206 | 207 | .uspStringElem:hover { 208 | background-color: white; 209 | border: 1px solid var(--text-gray); 210 | color: var(--text-gray); 211 | } 212 | 213 | .uspStringElem:active { 214 | background-color: white; 215 | border: 1px solid var(--text-gray); 216 | color: var(--text-gray); 217 | } 218 | 219 | /********************************************************/ 220 | 221 | /* Animated iOS style switch 222 | https://codepen.io/aaroniker/pen/oaQdQZ 223 | */ 224 | .switch { 225 | cursor: pointer; 226 | } 227 | .switch input { 228 | display: none; 229 | } 230 | .switch input + span { 231 | width: 48px; 232 | height: 28px; 233 | border-radius: 14px; 234 | transition: all 0.3s ease; 235 | display: block; 236 | position: relative; 237 | background: #888fa1; 238 | box-shadow: 0 8px 16px -1px rgba(211, 211, 211, 0.2); 239 | } 240 | .switch input + span:before, 241 | .switch input + span:after { 242 | content: ""; 243 | display: block; 244 | position: absolute; 245 | transition: all 0.3s ease; 246 | } 247 | .switch input + span:before { 248 | top: 5px; 249 | left: 5px; 250 | width: 18px; 251 | height: 18px; 252 | border-radius: 9px; 253 | border: 5px solid #fff; 254 | } 255 | .switch input + span:after { 256 | top: 5px; 257 | left: 32px; 258 | width: 6px; 259 | height: 18px; 260 | border-radius: 40%; 261 | transform-origin: 50% 50%; 262 | background: #fff; 263 | opacity: 0; 264 | } 265 | .switch input + span:active { 266 | transform: scale(0.92); 267 | } 268 | .switch input:checked + span { 269 | background: #48ea8b; 270 | box-shadow: 0 8px 16px -1px rgba(72, 234, 139, 0.2); 271 | } 272 | .switch input:checked + span:before { 273 | width: 0px; 274 | border-radius: 3px; 275 | margin-left: 27px; 276 | border-width: 3px; 277 | background: #fff; 278 | } 279 | .switch input:checked + span:after { 280 | animation: blobChecked 0.35s linear forwards 0.2s; 281 | } 282 | .switch input:not(:checked) + span:before { 283 | animation: blob 0.85s linear forwards 0.2s; 284 | } 285 | @keyframes blob { 286 | 0%, 287 | 100% { 288 | transform: scale(1); 289 | } 290 | 30% { 291 | transform: scale(1.12, 0.94); 292 | } 293 | 60% { 294 | transform: scale(0.96, 1.06); 295 | } 296 | } 297 | @keyframes blobChecked { 298 | 0% { 299 | opacity: 1; 300 | transform: scaleX(1); 301 | } 302 | 30% { 303 | transform: scaleX(1.44); 304 | } 305 | 70% { 306 | transform: scaleX(1.18); 307 | } 308 | 50%, 309 | 99% { 310 | transform: scaleX(1); 311 | opacity: 1; 312 | } 313 | 100% { 314 | transform: scaleX(1); 315 | opacity: 0; 316 | } 317 | } 318 | * { 319 | box-sizing: border-box; 320 | } 321 | *:before, 322 | *:after { 323 | box-sizing: border-box; 324 | } 325 | 326 | *:before, 327 | *:after { 328 | box-sizing: border-box; 329 | } 330 | 331 | .switch-smaller input + span { 332 | width: 48px; 333 | height: 28px; 334 | border-radius: 14px; 335 | transition: all 0.3s ease; 336 | display: block; 337 | position: relative; 338 | background: #888fa1; 339 | box-shadow: 0 8px 16px -1px rgba(211, 211, 211, 0.2); 340 | transform: scale(0.7); 341 | } 342 | .switch-smaller input + span:active { 343 | transform: scale(0.65); 344 | } 345 | 346 | /*========================================================================================*/ 347 | /*Walkthrough popups css*/ 348 | 349 | .tippy-box[data-theme~="custom-1"] { 350 | background-color: #87cefa; 351 | box-shadow: 10px 10px 5px 0px rgba(0, 0, 0, 0.43); 352 | color: white; 353 | border-radius: 5px; 354 | padding: 10px; 355 | text-align: left; 356 | float: left; 357 | } 358 | 359 | .tippy-box[data-theme~="custom-1"] button { 360 | color: white; 361 | } 362 | 363 | .tippy-box[data-theme~="custom-1"][data-placement^="top"] 364 | > .tippy-arrow::before { 365 | border-top-color: #87cefa; 366 | } 367 | .tippy-box[data-theme~="custom-1"][data-placement^="bottom"] 368 | > .tippy-arrow::before { 369 | border-bottom-color: #87cefa; 370 | } 371 | .tippy-box[data-theme~="custom-1"][data-placement^="left"] 372 | > .tippy-arrow::before { 373 | border-left-color: #87cefa; 374 | } 375 | .tippy-box[data-theme~="custom-1"][data-placement^="right"] 376 | > .tippy-arrow::before { 377 | border-right-color: #87cefa; 378 | } 379 | -------------------------------------------------------------------------------- /src/popup/popup.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 11 | 12 | 13 | 14 | 15 | OptMeowt 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 |
25 | 26 | Logo 32 | 33 |
34 |
39 | OptMeowt 40 |
41 |
47 | enable-disable 55 |
56 |
62 | more 69 |
70 |
76 | tour 84 |
85 |
86 |
87 | 88 | 89 |
90 | 91 |
92 | 93 | 94 | 95 |
109 | 110 |
111 | 112 | 115 |
116 |
117 |
122 | 123 |
124 |
125 |
126 |
127 | 130 |
131 |
132 |
133 |
134 |
135 | 136 |
137 | 138 | 139 |
144 |
145 |
146 |
147 | 148 | 149 | 177 | 178 | 179 | 182 | 183 | 184 | 185 | 213 | 214 | 215 | 218 | 219 | 220 | 221 |
227 |
232 | Domain List 233 |
234 |
235 | 236 |
237 | 238 |
239 |
243 | Dark Mode 244 |
245 |
246 |
247 | 255 |
256 |
257 |
258 | 259 |
260 |
261 | 262 | 269 | 270 |
271 |
272 | 273 |
278 |
279 |
280 | Extension Disabled 281 |
282 |
283 | You may enable the extension by clicking the play button above. 284 |
285 |
286 |
287 |
288 | 289 | 290 | -------------------------------------------------------------------------------- /src/options/views/settings-view/settings-view.js: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md 3 | privacy-tech-lab, https://privacytechlab.org/ 4 | */ 5 | 6 | /* 7 | settings-view.js 8 | ================================================================================ 9 | settings-view.js loads settings-view.html when clicked on the options page 10 | */ 11 | 12 | import { renderParse, fetchParse } from "../../components/util.js"; 13 | import { 14 | handleDownload, 15 | startUpload, 16 | handleUpload, 17 | stores, 18 | storage, 19 | } from "../../../background/storage.js"; 20 | 21 | // Used in tutorial 22 | import UIkit from "../../../../node_modules/uikit/dist/js/uikit.js"; 23 | import tippy from "../../../../node_modules/tippy.js/dist/tippy-bundle.umd.js"; 24 | 25 | import "../../../../node_modules/file-saver/src/FileSaver.js"; 26 | import Darkmode from "darkmode-js"; // check darkmode 27 | import { 28 | addDynamicRule, 29 | deleteAllDynamicRules, 30 | reloadDynamicRules, 31 | } from "../../../common/editRules.js"; 32 | import { isWellknownCheckEnabled } from "../../../common/settings.js"; 33 | import { updateRemovalScript } from "../../../common/editDomainlist.js"; 34 | 35 | 36 | 37 | /** 38 | * @typedef headings 39 | * @property {string} headings.title - Title of the given page 40 | * @property {string} headings.subtitle - Subtitle of the given page 41 | */ 42 | const headings = { 43 | title: "Settings", 44 | subtitle: "Adjust extension settings", 45 | }; 46 | 47 | 48 | /** 49 | * Creates the event listeners for the `Settings` page buttons and options 50 | */ 51 | function eventListeners() { 52 | document 53 | .getElementById("settings-view-radio0") 54 | .addEventListener("click", () => { 55 | chrome.runtime.sendMessage({ 56 | msg: "TURN_ON_OFF", 57 | data: { isEnabled: true }, 58 | }); 59 | chrome.runtime.sendMessage({ 60 | msg: "CHANGE_IS_DOMAINLISTED", 61 | data: { isDomainlisted: false }, 62 | }); 63 | chrome.scripting.updateContentScripts([ 64 | { 65 | id: "1", 66 | matches: [""], 67 | excludeMatches: [], 68 | js: ["content-scripts/registration/gpc-dom.js"], 69 | runAt: "document_start", 70 | }, 71 | ]); 72 | deleteAllDynamicRules(); 73 | }); 74 | document 75 | .getElementById("settings-view-radio1") 76 | .addEventListener("click", () => { 77 | chrome.runtime.sendMessage({ 78 | msg: "TURN_ON_OFF", 79 | data: { isEnabled: false }, 80 | }); 81 | chrome.runtime.sendMessage({ 82 | msg: "CHANGE_IS_DOMAINLISTED", 83 | data: { isDomainlisted: false }, 84 | }); 85 | chrome.scripting.updateContentScripts([ 86 | { 87 | id: "1", 88 | matches: ["https://example.com/foo/bar.html"], 89 | excludeMatches: [], 90 | js: ["content-scripts/registration/gpc-dom.js"], 91 | runAt: "document_start", 92 | }, 93 | ]); 94 | addDynamicRule(4999, "*"); 95 | }); 96 | document 97 | .getElementById("settings-view-radio2") 98 | .addEventListener("click", () => { 99 | chrome.runtime.sendMessage({ 100 | msg: "TURN_ON_OFF", 101 | data: { isEnabled: true }, 102 | }); 103 | chrome.runtime.sendMessage({ 104 | msg: "CHANGE_IS_DOMAINLISTED", 105 | data: { isDomainlisted: true }, 106 | }); 107 | updateRemovalScript(); 108 | reloadDynamicRules(); 109 | }); 110 | document 111 | .getElementById("download-button") 112 | .addEventListener("click", handleDownload); 113 | document 114 | .getElementById("wellknown-check-toggle") 115 | .addEventListener("change", async (event) => { 116 | const enabled = event.target.checked; 117 | await storage.set( 118 | stores.settings, 119 | enabled, 120 | "WELLKNOWN_CHECK_ENABLED" 121 | ); 122 | await chrome.storage.local.set({ WELLKNOWN_CHECK_ENABLED: enabled }); 123 | chrome.runtime.sendMessage({ 124 | msg: "TOGGLE_WELLKNOWN_CHECK", 125 | data: { enabled }, 126 | }); 127 | }); 128 | document.getElementById("upload-button").addEventListener("click", () => { 129 | const verify = confirm( 130 | `This option will load a list of domains from a file, clearing all domains currently in the list.\n Do you wish to continue?` 131 | ); 132 | if (verify) { 133 | startUpload(); 134 | } 135 | }); 136 | document 137 | .getElementById("upload-domainlist") 138 | .addEventListener("change", handleUpload, false); 139 | 140 | chrome.runtime.onMessage.addListener(async function (message, _, __) { 141 | if (message.msg === "SHOW_TUTORIAL") { 142 | if ("$BROWSER" == "chrome") { 143 | chrome.tabs.reload(); 144 | } else { 145 | await storage.set(stores.settings, true, "TUTORIAL_SHOWN"); 146 | walkthrough(); 147 | } 148 | } 149 | }); 150 | } 151 | 152 | 153 | /******************************************************************************/ 154 | 155 | /* 156 | * Gives user a walkthrough of install page on first install 157 | */ 158 | 159 | 160 | function walkthrough() { 161 | let modal = UIkit.modal("#welcome-modal"); 162 | modal.show(); 163 | 164 | document.getElementById("modal-button-1").onclick = function () { 165 | modal.hide(); 166 | }; 167 | 168 | document.getElementById("modal-button-2").onclick = function () { 169 | modal.hide(); 170 | tippy(".tutorial-tooltip1", { 171 | content: 172 | "

Set which sites should receive a Do Not Sell signal

", 173 | allowHTML: true, 174 | trigger: "manual", 175 | placement: "right", 176 | offset: [0, -600], 177 | duration: 1000, 178 | theme: "custom-1", 179 | onHide(instance) { 180 | trigger2(); 181 | }, 182 | }); 183 | let tooltip = 184 | document.getElementsByClassName("tutorial-tooltip1")[0]._tippy; 185 | tooltip.show(); 186 | }; 187 | 188 | function trigger2() { 189 | tippy(".tutorial-tooltip2", { 190 | content: 191 | "

Import and export your customized list of sites that should receive a signal

", 192 | allowHTML: true, 193 | trigger: "manual", 194 | duration: 1000, 195 | theme: "custom-1", 196 | placement: "right", 197 | offset: [0, 60], 198 | onHide() { 199 | trigger4(); 200 | }, 201 | }); 202 | let tooltip = 203 | document.getElementsByClassName("tutorial-tooltip2")[0]._tippy; 204 | tooltip.show(); 205 | } 206 | 207 | function trigger4() { 208 | let modal = UIkit.modal("#thank-you-modal"); 209 | modal.show(); 210 | document.getElementById("modal-button-3").onclick = () => { 211 | chrome.tabs.create( 212 | { url: "https://privacytechlab.org/optmeowt" }, 213 | function (tab) {} 214 | ); 215 | }; 216 | } 217 | } 218 | 219 | /* 220 | * Request host permissions upon install 221 | */ 222 | 223 | async function requestPermissionsButton() { 224 | try { 225 | // Request permissions 226 | const response = await browser.permissions.request({ 227 | origins: [""] // Allows host permissions 228 | }); 229 | 230 | // Check if permissions were granted or refused 231 | if (response) { 232 | console.log("Permissions were granted"); 233 | storage.set(stores.settings, true, "REQUEST_PERMISSIONS_SHOWN"); 234 | } else { 235 | console.log("Permissions were refused"); 236 | } 237 | 238 | // Retrieve current permissions after the request 239 | const currentPermissions = await browser.permissions.getAll(); 240 | console.log(`Current permissions:`, currentPermissions); 241 | } catch (error) { 242 | console.error('Error requesting permissions:', error); 243 | } 244 | } 245 | 246 | function requestPermissions() { 247 | let modal = UIkit.modal('#permission-modal'); 248 | modal.show(); 249 | document.getElementById("modal-button-4").onclick = () => { 250 | requestPermissionsButton(); 251 | modal.hide(); 252 | } 253 | } 254 | 255 | /******************************************************************************/ 256 | 257 | /** 258 | * Renders the `Settings` view in the options page 259 | * @param {string} scaffoldTemplate - stringified HTML template 260 | */ 261 | export async function settingsView(scaffoldTemplate) { 262 | const body = renderParse(scaffoldTemplate, headings, "scaffold-component"); 263 | let content = await fetchParse( 264 | "./views/settings-view/settings-view.html", 265 | "settings-view" 266 | ); 267 | 268 | document.getElementById("content").innerHTML = body.innerHTML; 269 | document.getElementById("scaffold-component-body").innerHTML = 270 | content.innerHTML; 271 | 272 | // Render correct extension mode radio button 273 | const isEnabled = await storage.get(stores.settings, "IS_ENABLED"); 274 | const isDomainlisted = await storage.get(stores.settings, "IS_DOMAINLISTED"); 275 | const wellknownCheckEnabled = await isWellknownCheckEnabled(); 276 | 277 | if (isEnabled) { 278 | isDomainlisted 279 | ? (document.getElementById("settings-view-radio2").checked = true) 280 | : (document.getElementById("settings-view-radio0").checked = true); 281 | } else { 282 | document.getElementById("settings-view-radio1").checked = true; 283 | } 284 | 285 | document.getElementById("wellknown-check-toggle").checked = 286 | wellknownCheckEnabled; 287 | 288 | eventListeners(); 289 | 290 | const tutorialShown = await storage.get(stores.settings, "TUTORIAL_SHOWN"); 291 | if (!tutorialShown) { 292 | walkthrough(); 293 | } 294 | storage.set(stores.settings, true, "TUTORIAL_SHOWN"); 295 | 296 | if ("$BROWSER" == "firefox") { 297 | const requestShown = await storage.get(stores.settings, "REQUEST_PERMISSIONS_SHOWN"); 298 | if (!requestShown) { 299 | requestPermissions(); 300 | } 301 | }} 302 | -------------------------------------------------------------------------------- /src/options/views/domainlist-view/domainlist-view.js: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md 3 | privacy-tech-lab, https://privacytechlab.org/ 4 | */ 5 | 6 | /* 7 | domainlist-view.js 8 | ================================================================================ 9 | domainlist-view.js loads domainlist-view.html when clicked on the options page 10 | */ 11 | 12 | import { storage, stores } from "../../../background/storage.js"; 13 | import { renderParse, fetchParse } from "../../components/util.js"; 14 | 15 | import { 16 | addDomainToDomainlistAndRules, 17 | removeDomainFromDomainlistAndRules, 18 | updateRemovalScript, 19 | deleteCS 20 | } from "../../../common/editDomainlist.js"; 21 | import { reloadDynamicRules } from "../../../common/editRules.js"; 22 | 23 | /******************************************************************************/ 24 | /***************************** Toggle Functions *******************************/ 25 | /******************************************************************************/ 26 | 27 | /** 28 | * Generates the HTML that will build the domainlist switch for a given 29 | * domain in the domainlist 30 | * @param {string} domain - Any given domain 31 | * @param {(number|null)} id - Dynamic rule ID if domainlisted as "excluded" 32 | * @return {string} - The stringified checkbox HTML compontent 33 | */ 34 | export function buildToggle(domain, id) { 35 | let toggle; 36 | if (!id) { 37 | toggle = ``; 38 | } else { 39 | toggle = ``; 40 | } 41 | return toggle; 42 | } 43 | 44 | /** 45 | * Creates an event listener that toggles a given domain's stored value in 46 | * the domainlist if a user clicks on the object with the given element ID 47 | * @param {string} elementId - HTML element to be linked to the listener 48 | * @param {string} domain - domain to be changed in domainlist 49 | */ 50 | export async function toggleListener(elementId, domain) { 51 | document.getElementById(elementId).addEventListener("click", async () => { 52 | const domainId = await storage.get(stores.domainlist, domain); 53 | if (domainId == null) { 54 | await addDomainToDomainlistAndRules(domain); 55 | } else { 56 | await removeDomainFromDomainlistAndRules(domain); 57 | } 58 | 59 | chrome.runtime.sendMessage({ 60 | msg: "FORCE_RELOAD", 61 | }); 62 | }); 63 | } 64 | 65 | function showConfirmModal(message, callback) { 66 | const modal = document.getElementById("confirm-modal"); 67 | const yesButton = document.getElementById("confirm-yes"); 68 | const noButton = document.getElementById("confirm-no"); 69 | 70 | // Set the message in the modal 71 | modal.querySelector("p").textContent = message; 72 | 73 | // Show the modal 74 | modal.classList.remove("hidden"); 75 | 76 | // Handle "Yes" button click 77 | yesButton.onclick = () => { 78 | callback(true); // Pass true to the callback if "Yes" was clicked 79 | modal.classList.add("hidden"); // Hide the modal 80 | }; 81 | 82 | // Handle "No" button click 83 | noButton.onclick = () => { 84 | callback(false); // Pass false to the callback if "No" was clicked 85 | modal.classList.add("hidden"); // Hide the modal 86 | }; 87 | } 88 | 89 | function showAlert(message, callback) { 90 | const modal = document.getElementById("alert-modal"); 91 | const okButton = document.getElementById("alert-ok"); 92 | 93 | // Set the message in the modal 94 | modal.querySelector("p").textContent = message; 95 | 96 | // Show the modal 97 | modal.classList.remove("hidden"); 98 | 99 | // Handle "OK" button click 100 | okButton.onclick = () => { 101 | callback(); // Call the callback after the alert is dismissed 102 | modal.classList.add("hidden"); // Hide the modal 103 | }; 104 | } 105 | 106 | /** 107 | * Creates the specific Domain List toggles as well as the perm delete 108 | * buttons for each domain 109 | */ 110 | async function createToggleListeners() { 111 | const domainlistKeys = await storage.getAllKeys(stores.domainlist); 112 | const domainlistValues = await storage.getAll(stores.domainlist); 113 | let domain; 114 | let domainValue; 115 | for (let index in domainlistKeys) { 116 | domain = domainlistKeys[index]; 117 | domainValue = domainlistValues[index]; 118 | // MAKE SURE THE ID MATCHES EXACTLY 119 | toggleListener(domain, domain); 120 | deleteButtonListener(domain); 121 | } 122 | } 123 | 124 | /** 125 | * Delete buttons for each domain 126 | * @param {string} domain 127 | */ 128 | function deleteButtonListener(domain) { 129 | document 130 | .getElementById(`delete ${domain}`) 131 | .addEventListener("click", async () => { 132 | const deletePrompt = `Are you sure you would like to delete this domain from the Domain List?`; 133 | const successPrompt = `Successfully deleted ${domain} from the Domain List.`; 134 | 135 | showConfirmModal(deletePrompt, async (confirmed) => { 136 | if (confirmed) { 137 | // Proceed with deletion if user confirms 138 | await storage.delete(stores.domainlist, domain); 139 | 140 | reloadDynamicRules(); 141 | updateRemovalScript(); 142 | deleteCS(); 143 | 144 | // Replacing alert() with custom showAlert() 145 | showAlert(successPrompt, () => { 146 | document.getElementById(`li ${domain}`).remove(); 147 | }); 148 | } 149 | }); 150 | }); 151 | } 152 | /******************************************************************************/ 153 | 154 | /** 155 | * @typedef headings 156 | * @property {string} headings.title - Title of the given page 157 | * @property {string} headings.subtitle - Subtitle of the given page 158 | */ 159 | const headings = { 160 | title: "Domain List", 161 | subtitle: 162 | "Toggle which domains you would like to receive Do Not Sell signals in Protection Mode", 163 | }; 164 | 165 | /** 166 | * Creates the event listeners for the `domainlist` page buttons and options 167 | */ 168 | async function eventListeners() { 169 | await createToggleListeners(); 170 | 171 | document.getElementById("delete-all-domains").addEventListener("click", async () => { 172 | const deletePrompt = `Are you sure you would like to delete all domains from the Domain List?`; 173 | const successPrompt = `Successfully deleted all domains from the Domain List.`; 174 | 175 | showConfirmModal(deletePrompt, async (confirmed) => { 176 | if (confirmed) { 177 | // If user clicks "Yes", proceed with deletion 178 | const domainlistKeys = await storage.getAllKeys(stores.domainlist); 179 | 180 | for (let domain of domainlistKeys) { 181 | await storage.delete(stores.domainlist, domain); 182 | } 183 | 184 | reloadDynamicRules(); 185 | updateRemovalScript(); 186 | deleteCS(); 187 | 188 | // Show success message using the custom alert modal 189 | showAlert(successPrompt, () => { 190 | document.getElementById("domainlist-main").innerHTML = ""; // Clears the list visually 191 | }); 192 | } else { 193 | // No action taken if user clicks "No" 194 | } 195 | }); 196 | }); 197 | 198 | window.onscroll = function () { 199 | stickyNavbar(); 200 | }; 201 | var nb = document.getElementById("domainlist-navbar"); 202 | var sticky = nb.offsetTop; 203 | 204 | /** 205 | * Sticky navbar 206 | */ 207 | function stickyNavbar() { 208 | if (window.pageYOffset >= sticky) { 209 | nb.classList.add("sticky"); 210 | } else { 211 | nb.classList.remove("sticky"); 212 | } 213 | } 214 | } 215 | 216 | /** 217 | * Builds the list of domains in the domainlist, and their respective 218 | * options, to be displayed 219 | */ 220 | async function buildList() { 221 | let items = ""; 222 | let domain; 223 | let domainValue; 224 | const domainlistKeys = await storage.getAllKeys(stores.domainlist); 225 | const domainlistValues = await storage.getAll(stores.domainlist); 226 | for (let index in domainlistKeys) { 227 | domain = domainlistKeys[index]; 228 | domainValue = domainlistValues[index]; 229 | items += 230 | ` 231 |

  • 232 |
    233 |
    234 | 240 |
    241 |
    242 | ${domain} 243 |
    244 |
    251 | 254 |
    255 | 273 |
    274 |
  • 275 | `; 276 | } 277 | document.getElementById("domainlist-main").innerHTML = items; 278 | } 279 | 280 | /** 281 | * Renders the `domain list` view in the options page 282 | * @param {string} scaffoldTemplate - stringified HTML template 283 | */ 284 | export async function domainlistView(scaffoldTemplate) { 285 | const body = renderParse(scaffoldTemplate, headings, "scaffold-component"); 286 | let content = await fetchParse( 287 | "./views/domainlist-view/domainlist-view.html", 288 | "domainlist-view" 289 | ); 290 | 291 | document.getElementById("content").innerHTML = body.innerHTML; 292 | document.getElementById("scaffold-component-body").innerHTML = 293 | content.innerHTML; 294 | 295 | await buildList(); 296 | eventListeners(); 297 | } 298 | -------------------------------------------------------------------------------- /src/background/protection/protection-ff.js: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md 3 | privacy-tech-lab, https://privacytechlab.org/ 4 | */ 5 | 6 | /* 7 | protection.js 8 | ================================================================================ 9 | protection.js (1) Implements our per-site functionality for the background listeners 10 | (2) Handles cached values & message passing to popup & options page 11 | */ 12 | 13 | import { stores, storage } from "../storage.js"; 14 | import { defaultSettings } from "../../data/defaultSettings.js"; 15 | import { headers } from "../../data/headers.js"; 16 | import { enableListeners, disableListeners } from "./listeners-$BROWSER.js"; 17 | import psl from "psl"; 18 | import { isWellknownCheckEnabled } from "../../common/settings.js"; 19 | 20 | /******************************************************************************/ 21 | /******************************************************************************/ 22 | /********** # Initializers (cached values) **********/ 23 | /******************************************************************************/ 24 | /******************************************************************************/ 25 | 26 | var domainlist = {}; // Caches & mirrors domainlist in storage 27 | var isDomainlisted = defaultSettings["IS_DOMAINLISTED"]; 28 | var tabs = {}; // Caches all tab infomration, i.e. requests, etc. 29 | var wellknown = {}; // Caches wellknown info to be sent to popup 30 | var signalPerTab = {}; // Caches if a signal is sent to render the popup icon 31 | var activeTabID = 0; // Caches current active tab id 32 | var sendSignal = true; // Caches if the signal can be sent to the curr domain 33 | var domPrev3rdParties = {}; //stores all the 3rd parties by domain (resets when you quit chrome) 34 | var globalParsedDomain; 35 | 36 | async function reloadVars() { 37 | let storedDomainlisted = await storage.get( 38 | stores.settings, 39 | "IS_DOMAINLISTED" 40 | ); 41 | if (storedDomainlisted) { 42 | isDomainlisted = storedDomainlisted; 43 | } 44 | } 45 | 46 | reloadVars(); 47 | 48 | 49 | /******************************************************************************/ 50 | /******************************************************************************/ 51 | /********** # Lisetener callbacks - Main functionality **********/ 52 | /******************************************************************************/ 53 | /******************************************************************************/ 54 | 55 | /* 56 | * The four following functions are all related to the four main listeners in 57 | * `background.js`. These four functions implement all the other helper 58 | * functions below 59 | */ 60 | 61 | const listenerCallbacks = { 62 | /** 63 | * Handles all signal processessing prior to sending request headers 64 | * @param {object} details - retrieved info passed into callback 65 | * @returns {array} details.requestHeaders from addHeaders 66 | */ 67 | onBeforeSendHeaders: async (details) => { 68 | await updateDomainlist(details); 69 | 70 | if (true) { 71 | signalPerTab[details.tabId] = true; 72 | return addHeaders(details); 73 | } 74 | }, 75 | 76 | /** 77 | * @param {object} details - retrieved info passed into callback 78 | */ 79 | onHeadersReceived: (details) => { 80 | logData(details); 81 | }, 82 | 83 | /** 84 | * @param {object} details - retrieved info passed into callback 85 | */ 86 | onBeforeNavigate: (details) => { 87 | // Resets certain cached info 88 | if (details.frameId === 0) { 89 | wellknown[details.tabId] = null; 90 | signalPerTab[details.tabId] = false; 91 | tabs[activeTabID].REQUEST_DOMAINS = {}; 92 | } 93 | }, 94 | 95 | /** 96 | * Adds DOM property 97 | * @param {object} details - retrieved info passed into callback 98 | */ 99 | onCommitted: async (details) => { 100 | if (true) { 101 | addDomSignal(details); 102 | updatePopupIcon(details); 103 | } 104 | }, 105 | }; // closes listenerCallbacks object 106 | 107 | /******************************************************************************/ 108 | /******************************************************************************/ 109 | /********** # Listener helper fxns - Main functionality **********/ 110 | /******************************************************************************/ 111 | /******************************************************************************/ 112 | 113 | /** 114 | * Attaches headers from `headers.js` to details.requestHeaders 115 | * @param {object} details - retrieved info passed into callback 116 | * @returns {array} details.requestHeaders 117 | */ 118 | function addHeaders(details) { 119 | console.log("addHeaders called"); 120 | for (let signal in headers) { 121 | let s = headers[signal]; 122 | details.requestHeaders.push({ name: s.name, value: s.value }); 123 | } 124 | return { requestHeaders: details.requestHeaders }; 125 | } 126 | 127 | /** 128 | * Runs `dom.js` to attach DOM signal 129 | * @param {object} details - retrieved info passed into callback 130 | */ 131 | function addDomSignal(details) { 132 | console.log("addDomSignal called"); 133 | chrome.scripting.executeScript(details.tabId, { 134 | file: "../../content-scripts/injection/gpc-dom.js", 135 | frameId: details.frameId, // Supposed to solve multiple injections 136 | // as opposed to allFrames: true 137 | runAt: "document_start", 138 | }); 139 | } 140 | 141 | function getCurrentParsedDomain() { 142 | return new Promise((resolve, reject) => { 143 | try { 144 | chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) { 145 | let tab = tabs[0]; 146 | let url = new URL(tab.url); 147 | let parsed = psl.parse(url.hostname); 148 | let domain = parsed.domain; 149 | globalParsedDomain = domain; // for global scope variable 150 | resolve(domain); 151 | }); 152 | } catch(e) { 153 | reject(); 154 | } 155 | }) 156 | } 157 | 158 | /** 159 | * Checks whether a particular domain should receive a DNS signal 160 | * (1) Parse url to get domain for domainlist 161 | * (2) Update domains by adding current domain to domainlist in storage. 162 | * (3) Updates the 3rd party list for the currentDomain 163 | * (4) Check to see if we should send signal. 164 | * 165 | * Currently, it only adds to domainlist store as NULL if it doesnt exist 166 | * @param {Object} details - callback object according to Chrome API 167 | */ 168 | async function updateDomainlist(details) { 169 | let url = new URL(details.url); 170 | let parsedUrl = psl.parse(url.hostname); 171 | let parsedDomain = parsedUrl.domain; 172 | if (parsedDomain == null || parsedDomain == undefined) { 173 | return; 174 | } 175 | 176 | let parsedDomainVal = domainlist[parsedDomain]; 177 | if (parsedDomainVal === undefined) { 178 | storage.set(stores.domainlist, null, parsedDomain); // Sets to storage async 179 | domainlist[parsedDomain] = null; // Sets to cache 180 | parsedDomainVal = null; 181 | } 182 | 183 | //get the current parsed domain--this is used to store 3rd parties (using globalParsedDomain variable) 184 | 185 | let currentDomain = await getCurrentParsedDomain(); 186 | //initialize the objects 187 | if (!(activeTabID in domPrev3rdParties)){ 188 | domPrev3rdParties[activeTabID] = {}; 189 | } 190 | if (!(currentDomain in domPrev3rdParties[activeTabID]) ){ 191 | domPrev3rdParties[activeTabID][currentDomain] = {}; 192 | } 193 | //as they come in, add the parsedDomain to the object with null value (just a placeholder) 194 | domPrev3rdParties[activeTabID][currentDomain][parsedDomain] = null; 195 | 196 | 197 | (isDomainlisted) 198 | ? ((parsedDomainVal === null) ? sendSignal = true : sendSignal = false) 199 | : sendSignal = true; 200 | } 201 | 202 | function updatePopupIcon(details) { 203 | if (wellknown[details.tabId] === undefined) { 204 | wellknown[details.tabId] = null; 205 | } 206 | if (wellknown[details.tabId] === null) { 207 | chrome.browserAction.setIcon({ 208 | tabId: details.tabId, 209 | path: "assets/face-icons/optmeow-face-circle-green-ring-128.png", 210 | }); 211 | } 212 | } 213 | 214 | function logData(details) { 215 | let url = new URL(details.url); 216 | let parsed = psl.parse(url.hostname); 217 | 218 | if (tabs[details.tabId] === undefined) { 219 | tabs[details.tabId] = { DOMAIN: null, REQUEST_DOMAINS: {}, TIMESTAMP: 0 }; 220 | tabs[details.tabId].REQUEST_DOMAINS[parsed.domain] = { 221 | URLS: {}, 222 | RESPONSE: details.responseHeaders, 223 | TIMESTAMP: details.timeStamp, 224 | }; 225 | tabs[details.tabId].REQUEST_DOMAINS[parsed.domain].URLS = { 226 | URL: details.url, 227 | RESPONSE: details.responseHeaders, 228 | }; 229 | } else { 230 | if (tabs[details.tabId].REQUEST_DOMAINS[parsed.domain] === undefined) { 231 | tabs[details.tabId].REQUEST_DOMAINS[parsed.domain] = { 232 | URLS: {}, 233 | RESPONSE: details.responseHeaders, 234 | TIMESTAMP: details.timeStamp, 235 | }; 236 | tabs[details.tabId].REQUEST_DOMAINS[parsed.domain].URLS[details.url] = { 237 | RESPONSE: details.responseHeaders, 238 | }; 239 | } else { 240 | tabs[details.tabId].REQUEST_DOMAINS[parsed.domain].URLS[details.url] = { 241 | RESPONSE: details.responseHeaders, 242 | }; 243 | } 244 | } 245 | } 246 | 247 | async function pullToDomainlistCache() { 248 | let domain; 249 | let domainlistKeys = await storage.getAllKeys(stores.domainlist); 250 | let domainlistValues = await storage.getAll(stores.domainlist); 251 | for (let key in domainlistKeys) { 252 | domain = domainlistKeys[key]; 253 | domainlist[domain] = domainlistValues[key]; 254 | } 255 | } 256 | 257 | async function syncDomainlists() { 258 | // (1) Reconstruct a domainlist indexedDB object from storage 259 | // (2) Iterate through local domainlist 260 | // --- If item in cache NOT in domainlistKeys/domainlistDB, add to storage 261 | // via storage.set() 262 | // (3) Iterate through all domain keys in indexedDB domainlist 263 | // --- If key NOT in cached domainlist, add to cached domainlist 264 | 265 | let domainlistKeys = await storage.getAllKeys(stores.domainlist); 266 | let domainlistValues = await storage.getAll(stores.domainlist); 267 | let domainlistDB = {}; 268 | let domain; 269 | for (let key in domainlistKeys) { 270 | domain = domainlistKeys[key]; 271 | domainlistDB[domain] = domainlistValues[key]; 272 | } 273 | 274 | for (let domainKey in domainlist) { 275 | if (!domainlistDB[domainKey]) { 276 | await storage.set(stores.domainlist, domainlist[domainKey], domainKey); 277 | } 278 | } 279 | 280 | for (let domainKey in domainlistDB) { 281 | if (!domainlist[domainKey]) { 282 | domainlist[domainKey] = domainlistDB[domainKey]; 283 | } 284 | } 285 | } 286 | 287 | /** 288 | * whether the curr site should get privacy signals 289 | * (We need to try and make a synchronous version, esp. for DOM issue & related 290 | * message passing with the contentscript which injects the DOM signal) 291 | * @returns {bool} sendSignal 292 | */ 293 | async function sendPrivacySignal(domain) { 294 | let sendSignal; 295 | const extensionEnabled = await storage.get(stores.settings, "IS_ENABLED"); 296 | const extensionDomainlisted = await storage.get( 297 | stores.settings, 298 | "IS_DOMAINLISTED" 299 | ); 300 | const domainDomainlisted = await storage.get(stores.domainlist, domain); 301 | 302 | if (extensionEnabled) { 303 | if (extensionDomainlisted) { 304 | // Recall we must flip the value of the domainlisted domain 305 | // due to how to how defined domainlisted values, corresponding to MV3 306 | // declarativeNetRequest rule exceptions 307 | // (i.e., null => no rule exists, valued => exception rule exists) 308 | sendSignal = !domainDomainlisted ? true : false; 309 | } else { 310 | sendSignal = true; 311 | } 312 | } else { 313 | sendSignal = false; 314 | } 315 | return sendSignal; 316 | } 317 | 318 | /******************************************************************************/ 319 | /******************************************************************************/ 320 | /********** # Message Passing - Popup helper fxns **********/ 321 | /******************************************************************************/ 322 | /******************************************************************************/ 323 | 324 | function handleSendMessageError() { 325 | const error = chrome.runtime.lastError; 326 | if (error) { 327 | console.warn(error.message); 328 | } 329 | } 330 | 331 | // Info back to popup 332 | function dataToPopup() { 333 | 334 | let requestsData = {}; 335 | 336 | if (tabs[activeTabID] !== undefined) { 337 | 338 | requestsData = domPrev3rdParties[activeTabID][globalParsedDomain]; 339 | console.log("requests by tabID:", domPrev3rdParties); 340 | } 341 | 342 | chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) { 343 | let tabID = tabs[0]["id"]; 344 | let wellknownData = wellknown[tabID]; 345 | 346 | let popupData = { 347 | requests: requestsData, 348 | wellknown: wellknownData, 349 | }; 350 | 351 | chrome.runtime.sendMessage( 352 | { 353 | msg: "POPUP_PROTECTION_DATA", 354 | data: popupData, 355 | }, 356 | handleSendMessageError 357 | ); 358 | }); 359 | } 360 | 361 | /******************************************************************************/ 362 | /******************************************************************************/ 363 | /********** # Message passing **********/ 364 | /******************************************************************************/ 365 | /******************************************************************************/ 366 | 367 | /** 368 | * Currently only handles syncing domainlists between storage and memory 369 | * This runs when the popup disconnects from the background page 370 | * @param {Port} port 371 | */ 372 | function onConnectHandler(port) { 373 | if (port.name === "POPUP") { 374 | port.onDisconnect.addListener(function () { 375 | syncDomainlists(); 376 | }); 377 | } 378 | } 379 | 380 | /** 381 | * This is currently only to handle adding the GPC DOM signal. 382 | * I'm not sure how to fit it into an async call, it doesn't want to connect. 383 | * It would be nice to merge the two onMessage handlers. 384 | * TODO: This method still seems to have a timing issue. Doesn't always show DOM signal as thumbs up on reference site. 385 | * @returns {Bool} true (lets us send asynchronous responses to senders) 386 | */ 387 | function onMessageHandlerSynchronous(message, sender, sendResponse) { 388 | if (message.msg === "APPEND_GPC_PROP") { 389 | let url = new URL(sender.origin); 390 | let parsed = psl.parse(url.hostname); 391 | let domain = parsed.domain; 392 | 393 | const r = sendPrivacySignal(domain); 394 | r.then((r) => { 395 | const response = { 396 | msg: "APPEND_GPC_PROP_RESPONSE", 397 | sendGPC: r, 398 | }; 399 | sendResponse(response); 400 | }); 401 | } 402 | return true; 403 | } 404 | 405 | /** 406 | * Listeners for information from --POPUP-- or --OPTIONS-- page 407 | * This is the main "hub" for message passing between the extension components 408 | * https://developer.chrome.com/docs/extensions/mv3/messaging/ 409 | */ 410 | async function onMessageHandlerAsync(message, sender, sendResponse) { 411 | if (message.msg === "GET_WELLKNOWN_CHECK_ENABLED") { 412 | const enabled = await isWellknownCheckEnabled(); 413 | await chrome.storage.local.set({ WELLKNOWN_CHECK_ENABLED: enabled }); 414 | sendResponse({ enabled }); 415 | return true; 416 | } 417 | if (message.msg === "TOGGLE_WELLKNOWN_CHECK") { 418 | const enabled = message.data?.enabled !== false; 419 | await storage.set(stores.settings, enabled, "WELLKNOWN_CHECK_ENABLED"); 420 | await chrome.storage.local.set({ WELLKNOWN_CHECK_ENABLED: enabled }); 421 | if (!enabled) { 422 | await storage.clear(stores.wellknownInformation); 423 | wellknown = {}; 424 | } 425 | } 426 | if (message.msg === "CHANGE_IS_DOMAINLISTED") { 427 | isDomainlisted = message.data.isDomainlisted; 428 | storage.set(stores.settings, isDomainlisted, "IS_DOMAINLISTED"); 429 | } 430 | if (message.msg === "SET_TO_DOMAINLIST") { 431 | let { domain, key } = message.data; 432 | domainlist[domain] = key; // Sets to cache 433 | storage.set(stores.domainlist, key, domain); // Sets to long term storage 434 | } 435 | if (message.msg === "REMOVE_FROM_DOMAINLIST") { 436 | let domain = message.data; 437 | delete domainlist[domain]; 438 | } 439 | if (message.msg === "POPUP_PROTECTION") { 440 | dataToPopup(); 441 | } 442 | if (message.msg === "CONTENT_SCRIPT_WELLKNOWN") { 443 | const wellknownCheckEnabled = await isWellknownCheckEnabled(); 444 | if (!wellknownCheckEnabled) { 445 | return true; 446 | } 447 | let tabID = sender.tab.id; 448 | wellknown[tabID] = message.data; 449 | if (wellknown[tabID]["gpc"] === true) { 450 | setTimeout(() => {}, 10000); 451 | if (signalPerTab[tabID] === true) { 452 | chrome.browserAction.setIcon({ 453 | tabId: tabID, 454 | path: "assets/face-icons/optmeow-face-circle-green-128.png", 455 | }); 456 | } 457 | } 458 | } 459 | 460 | if (message.msg === "CONTENT_SCRIPT_TAB") { 461 | let url = new URL(sender.origin); 462 | let parsed = psl.parse(url.hostname); 463 | let domain = parsed.domain; 464 | let tabID = sender.tab.id; 465 | if (tabs[tabID] === undefined) { 466 | tabs[tabID] = { 467 | DOMAIN: domain, 468 | REQUEST_DOMAINS: {}, 469 | TIMESTAMP: message.data, 470 | }; 471 | } else if (tabs[tabID].DOMAIN !== domain) { 472 | tabs[tabID].DOMAIN = domain; 473 | let urls = tabs[tabID]["REQUEST_DOMAINS"]; 474 | for (let key in urls) { 475 | if (urls[key]["TIMESTAMP"] >= message.data) { 476 | tabs[tabID]["REQUEST_DOMAINS"][key] = urls[key]; 477 | } else { 478 | delete tabs[tabID]["REQUEST_DOMAINS"][key]; 479 | } 480 | } 481 | tabs[tabID]["TIMESTAMP"] = message.data; 482 | } 483 | } 484 | if (message.msg === "FORCE_RELOAD") { 485 | pullToDomainlistCache(); 486 | } 487 | return true; // Async callbacks require this 488 | } 489 | 490 | function initMessagePassing() { 491 | chrome.runtime.onConnect.addListener(onConnectHandler); 492 | chrome.runtime.onMessage.addListener(onMessageHandlerAsync); 493 | chrome.runtime.onMessage.addListener(onMessageHandlerSynchronous); 494 | } 495 | 496 | function closeMessagePassing() { 497 | chrome.runtime.onConnect.removeListener(onConnectHandler); 498 | chrome.runtime.onMessage.removeListener(onMessageHandlerAsync); 499 | chrome.runtime.onMessage.removeListener(onMessageHandlerSynchronous); 500 | } 501 | 502 | /******************************************************************************/ 503 | /******************************************************************************/ 504 | /********** # Other initializers - run once per enable **********/ 505 | /******************************************************************************/ 506 | /******************************************************************************/ 507 | 508 | /** 509 | * Listener for tab switch that updates the cached current tab variable 510 | */ 511 | function onActivatedProtectionMode(info) { 512 | activeTabID = info.tabId; 513 | } 514 | 515 | // Handles misc. setup & setup listeners 516 | function initSetup() { 517 | pullToDomainlistCache(); 518 | 519 | // Runs on startup to initialize the cached current tab variable 520 | chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) { 521 | if (tabs.id) { 522 | activeTabID = tabs.id; 523 | } 524 | }); 525 | 526 | chrome.tabs.onActivated.addListener(onActivatedProtectionMode); 527 | } 528 | 529 | function closeSetup() { 530 | chrome.tabs.onActivated.removeListener(onActivatedProtectionMode); 531 | } 532 | 533 | /** 534 | * Inteded to facilitate transitioning between analysis & protection modes 535 | */ 536 | function wipeLocalVars() { 537 | domainlist = {}; // Caches & mirrors domainlist in storage 538 | tabs = {}; // Caches all tab infomration, i.e. requests, etc. 539 | wellknown = {}; // Caches wellknown info to be sent to popup 540 | signalPerTab = {}; // Caches if a signal is sent to render the popup icon 541 | activeTabID = 0; // Caches current active tab id 542 | sendSignal = false; // Caches if the signal can be sent to the curr domain 543 | } 544 | 545 | /******************************************************************************/ 546 | /******************************************************************************/ 547 | /********** # Exportable init / halt functions **********/ 548 | /******************************************************************************/ 549 | /******************************************************************************/ 550 | 551 | export function init() { 552 | reloadVars(); 553 | enableListeners(listenerCallbacks); 554 | initMessagePassing(); 555 | initSetup(); 556 | } 557 | 558 | 559 | export function halt() { 560 | disableListeners(listenerCallbacks); 561 | closeMessagePassing(); 562 | closeSetup(); 563 | wipeLocalVars(); 564 | } 565 | -------------------------------------------------------------------------------- /src/background/protection/protection.js: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md 3 | privacy-tech-lab, https://privacytechlab.org/ 4 | */ 5 | 6 | /* 7 | protection.js 8 | ================================================================================ 9 | protection.js (1) Implements our per-site functionality for the background listeners 10 | (2) Handles cached values & message passing to popup & options page 11 | */ 12 | 13 | import { stores, storage } from "./../storage.js"; 14 | import { defaultSettings } from "../../data/defaultSettings.js"; 15 | import { enableListeners, disableListeners } from "./listeners-$BROWSER.js"; 16 | import psl from "psl"; 17 | 18 | import { 19 | addDynamicRule, 20 | deleteDynamicRule, 21 | reloadDynamicRules, 22 | } from "../../common/editRules.js"; 23 | import { isWellknownCheckEnabled } from "../../common/settings.js"; 24 | 25 | /******************************************************************************/ 26 | /******************************************************************************/ 27 | /********** # Initializers (cached values) **********/ 28 | /******************************************************************************/ 29 | /******************************************************************************/ 30 | 31 | var domainlist = {}; // Caches & mirrors domainlist in storage 32 | var isDomainlisted = defaultSettings["IS_DOMAINLISTED"]; 33 | var tabs = {}; // Caches all tab infomration, i.e. requests, etc. 34 | var wellknown = {}; // Caches wellknown info to be sent to popup 35 | var signalPerTab = {}; // Caches if a signal is sent to render the popup icon 36 | var activeTabID = 0; // Caches current active tab id 37 | var sendSignal = true; // Caches if the signal can be sent to the curr domain 38 | var domPrev3rdParties = {}; //stores all the 3rd parties by domain (resets when you quit chrome) 39 | var globalParsedDomain; 40 | var setup = false; 41 | 42 | async function reloadVars() { 43 | let storedDomainlisted = await storage.get( 44 | stores.settings, 45 | "IS_DOMAINLISTED" 46 | ); 47 | if (storedDomainlisted) { 48 | isDomainlisted = storedDomainlisted; 49 | } 50 | } 51 | 52 | reloadVars(); 53 | 54 | /******************************************************************************/ 55 | /******************************************************************************/ 56 | /********** # Lisetener callbacks - Main functionality **********/ 57 | /******************************************************************************/ 58 | /******************************************************************************/ 59 | 60 | /* 61 | * The four following functions are all related to the four main listeners in 62 | * `background.js`. These four functions implement all the other helper 63 | * functions below 64 | */ 65 | 66 | const listenerCallbacks = { 67 | /** 68 | * Handles all signal processessing prior to sending request headers 69 | * @param {object} details - retrieved info passed into callback 70 | * @returns {array} 71 | */ 72 | onBeforeSendHeaders: async (details) => { 73 | await updateDomainlist(details); 74 | }, 75 | 76 | /** 77 | * @param {object} details - retrieved info passed into callback 78 | */ 79 | onHeadersReceived: async (details) => { 80 | //if (!setup){ 81 | //initSetup(); 82 | //} 83 | await logData(details); 84 | await sendData(); 85 | 86 | 87 | 88 | }, 89 | 90 | /** 91 | * @param {object} details - retrieved info passed into callback 92 | */ 93 | onBeforeNavigate: (details) => { 94 | // Resets certain cached info 95 | }, 96 | 97 | /** 98 | * Adds DOM property 99 | * @param {object} details - retrieved info passed into callback 100 | */ 101 | onCommitted: async (details) => { 102 | await updateDomainlist(details); 103 | }, 104 | 105 | onCompleted: async (details) => { 106 | await sendData(); 107 | } 108 | 109 | }; // closes listenerCallbacks object 110 | 111 | /******************************************************************************/ 112 | /******************************************************************************/ 113 | /********** # Listener helper fxns - Main functionality **********/ 114 | /******************************************************************************/ 115 | /******************************************************************************/ 116 | 117 | 118 | async function sendData(){ 119 | let activeTab = await chrome.tabs.query({ active: true, currentWindow: true }); 120 | let activeTabID = activeTab.length > 0 ? activeTab[0].id : null; 121 | 122 | if (activeTabID === null) { 123 | return; 124 | } 125 | 126 | let currentDomain = await getCurrentParsedDomain(); 127 | if (!currentDomain) { 128 | return; 129 | } 130 | 131 | const partiesForTab = domPrev3rdParties?.[activeTabID]; 132 | const info = partiesForTab ? partiesForTab[currentDomain] : null; 133 | 134 | if (!info) { 135 | await storage.delete(stores.thirdParties, currentDomain); 136 | return; 137 | } 138 | 139 | const data = Object.keys(info).filter(Boolean); 140 | await storage.set(stores.thirdParties, data, currentDomain); 141 | 142 | } 143 | 144 | 145 | function getCurrentParsedDomain() { 146 | return new Promise((resolve) => { 147 | chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) { 148 | try { 149 | const tab = tabs && tabs[0]; 150 | if (!tab || !tab.url) { 151 | return resolve(null); 152 | } 153 | const url = new URL(tab.url); 154 | const parsed = psl.parse(url.hostname); 155 | const domain = parsed && parsed.domain ? parsed.domain : null; 156 | globalParsedDomain = domain; // for global scope variable 157 | resolve(domain); 158 | } catch(e) { 159 | resolve(null); 160 | } 161 | }); 162 | }); 163 | } 164 | 165 | 166 | /** 167 | * Checks whether a particular domain should receive a DNS signal 168 | * (1) Parse url to get domain for domainlist 169 | * (2) Update domains by adding current domain to domainlist in storage. 170 | * (3) Updates the 3rd party list for the currentDomain 171 | * (4) Check to see if we should send signal. 172 | * 173 | * Currently, it only adds to domainlist store as NULL if it doesnt exist 174 | * @param {Object} details - callback object according to Chrome API 175 | */ 176 | async function updateDomainlist(details) { 177 | if (!details || !details.url) { 178 | return; 179 | } 180 | 181 | let parsedDomain; 182 | try { 183 | let url = new URL(details.url); 184 | let parsedUrl = psl.parse(url.hostname); 185 | parsedDomain = parsedUrl.domain; 186 | } catch (e) { 187 | return; 188 | } 189 | 190 | if (!parsedDomain) { 191 | return; 192 | } 193 | 194 | let currDomainValue = await storage.get(stores.domainlist, parsedDomain); 195 | let id = details.tabId; 196 | 197 | if (currDomainValue === undefined) { 198 | await storage.set(stores.domainlist, null, parsedDomain); // Sets to storage async 199 | } 200 | 201 | let currentDomain = await getCurrentParsedDomain(); 202 | if (!currentDomain) { 203 | return; 204 | } 205 | 206 | //get the current parsed domain--this is used to store 3rd parties (using globalParsedDomain variable) 207 | if (!(id in domPrev3rdParties)){ 208 | domPrev3rdParties[id] = {}; 209 | } 210 | if (!(currentDomain in domPrev3rdParties[id]) ){ 211 | domPrev3rdParties[id][currentDomain] = {}; 212 | } 213 | //as they come in, add the parsedDomain to the object with null value (just a placeholder) 214 | domPrev3rdParties[id][currentDomain][parsedDomain] = null; 215 | 216 | 217 | } 218 | 219 | function updatePopupIcon(tabId) { 220 | chrome.action.setIcon({ 221 | tabId: tabId, 222 | path: "assets/face-icons/optmeow-face-circle-green-ring-128.png", 223 | }); 224 | } 225 | 226 | async function logData(details) { 227 | let url = new URL(details.url); 228 | let parsed = psl.parse(url.hostname); 229 | 230 | if (tabs[details.tabId] === undefined) { 231 | tabs[details.tabId] = { DOMAIN: null, REQUEST_DOMAINS: {}, TIMESTAMP: 0 }; 232 | tabs[details.tabId].REQUEST_DOMAINS[parsed.domain] = { 233 | URLS: {}, 234 | RESPONSE: details.responseHeaders, 235 | TIMESTAMP: details.timeStamp, 236 | }; 237 | tabs[details.tabId].REQUEST_DOMAINS[parsed.domain].URLS = { 238 | URL: details.url, 239 | RESPONSE: details.responseHeaders, 240 | }; 241 | } else { 242 | if (tabs[details.tabId].REQUEST_DOMAINS[parsed.domain] === undefined) { 243 | tabs[details.tabId].REQUEST_DOMAINS[parsed.domain] = { 244 | URLS: {}, 245 | RESPONSE: details.responseHeaders, 246 | TIMESTAMP: details.timeStamp, 247 | }; 248 | tabs[details.tabId].REQUEST_DOMAINS[parsed.domain].URLS[details.url] = { 249 | RESPONSE: details.responseHeaders, 250 | }; 251 | } else { 252 | tabs[details.tabId].REQUEST_DOMAINS[parsed.domain].URLS[details.url] = { 253 | RESPONSE: details.responseHeaders, 254 | }; 255 | } 256 | } 257 | 258 | } 259 | 260 | async function pullToDomainlistCache() { 261 | let domain; 262 | let domainlistKeys = await storage.getAllKeys(stores.domainlist); 263 | let domainlistValues = await storage.getAll(stores.domainlist); 264 | 265 | for (let key in domainlistKeys) { 266 | domain = domainlistKeys[key]; 267 | domainlist[domain] = domainlistValues[key]; 268 | } 269 | } 270 | 271 | async function syncDomainlists() { 272 | // (1) Reconstruct a domainlist indexedDB object from storage 273 | // (2) Iterate through local domainlist 274 | // --- If item in cache NOT in domainlistKeys/domainlistDB, add to storage 275 | // via storage.set() 276 | // (3) Iterate through all domain keys in indexedDB domainlist 277 | // --- If key NOT in cached domainlist, add to cached domainlist 278 | 279 | let domainlistKeys = await storage.getAllKeys(stores.domainlist); 280 | let domainlistValues = await storage.getAll(stores.domainlist); 281 | let domainlistDB = {}; 282 | let domain; 283 | for (let key in domainlistKeys) { 284 | domain = domainlistKeys[key]; 285 | domainlistDB[domain] = domainlistValues[key]; 286 | } 287 | 288 | for (let domainKey in domainlist) { 289 | if (!domainlistDB[domainKey]) { 290 | await storage.set(stores.domainlist, domainlist[domainKey], domainKey); 291 | } 292 | } 293 | 294 | for (let domainKey in domainlistDB) { 295 | if (!domainlist[domainKey]) { 296 | domainlist[domainKey] = domainlistDB[domainKey]; 297 | } 298 | } 299 | } 300 | 301 | /** 302 | * whether the curr site should get privacy signals 303 | * (We need to try and make a synchronous version, esp. for DOM issue & related 304 | * message passing with the contentscript which injects the DOM signal) 305 | * @returns {bool} sendSignal 306 | */ 307 | async function sendPrivacySignal(domain) { 308 | let sendSignal; 309 | const extensionEnabled = await storage.get(stores.settings, "IS_ENABLED"); 310 | const extensionDomainlisted = await storage.get( 311 | stores.settings, 312 | "IS_DOMAINLISTED" 313 | ); 314 | const domainDomainlisted = await storage.get(stores.domainlist, domain); 315 | 316 | if (extensionEnabled) { 317 | if (extensionDomainlisted) { 318 | // Recall we must flip the value of the domainlisted domain 319 | // due to how to how defined domainlisted values, corresponding to MV3 320 | // declarativeNetRequest rule exceptions 321 | // (i.e., null => no rule exists, valued => exception rule exists) 322 | sendSignal = !domainDomainlisted ? true : false; 323 | } else { 324 | sendSignal = true; 325 | } 326 | } else { 327 | sendSignal = false; 328 | } 329 | return sendSignal; 330 | } 331 | 332 | /******************************************************************************/ 333 | /******************************************************************************/ 334 | /********** # Message Passing - Popup helper fxns **********/ 335 | /******************************************************************************/ 336 | /******************************************************************************/ 337 | 338 | function handleSendMessageError() { 339 | const error = chrome.runtime.lastError; 340 | if (error) { 341 | console.warn(error.message); 342 | } 343 | } 344 | async function dataToPopupHelper(){ 345 | //data gets sent back every time the popup is clicked 346 | let domain = await getCurrentParsedDomain(); 347 | if (!domain) { 348 | return []; 349 | } 350 | 351 | let parties = await storage.get(stores.thirdParties, domain); 352 | if (!Array.isArray(parties)) { 353 | return []; 354 | } 355 | 356 | return parties; 357 | } 358 | 359 | // Info back to popup 360 | async function dataToPopup(wellknownData) { 361 | let requestsData = await dataToPopupHelper(); //get requests from the helper 362 | chrome.tabs.query({active: true, currentWindow: true}, function(tabs){ 363 | let popupData = { 364 | requests: requestsData, 365 | wellknown: wellknownData, 366 | }; 367 | 368 | chrome.runtime.sendMessage( 369 | { 370 | msg: "POPUP_PROTECTION_DATA", 371 | data: popupData, 372 | }, 373 | handleSendMessageError 374 | ); 375 | }); 376 | } 377 | 378 | async function dataToPopupRequests() { 379 | let requestsData = await dataToPopupHelper(); //get requests from the helper 380 | console.log("requests data in DTPR: ", requestsData) 381 | 382 | chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) { 383 | chrome.runtime.sendMessage( 384 | { 385 | msg: "POPUP_PROTECTION_DATA_REQUESTS", 386 | data: requestsData, 387 | }, 388 | handleSendMessageError 389 | ); 390 | }); 391 | } 392 | 393 | /******************************************************************************/ 394 | /******************************************************************************/ 395 | /********** # Message passing **********/ 396 | /******************************************************************************/ 397 | /******************************************************************************/ 398 | 399 | /** 400 | * Currently only handles syncing domainlists between storage and memory 401 | * This runs when the popup disconnects from the background page 402 | * @param {Port} port 403 | */ 404 | function onConnectHandler(port) { 405 | if (port.name === "POPUP") { 406 | port.onDisconnect.addListener(function () { 407 | syncDomainlists(); 408 | }); 409 | } 410 | } 411 | 412 | /** 413 | * This is currently only to handle adding the GPC DOM signal. 414 | * I'm not sure how to fit it into an async call, it doesn't want to connect. 415 | * It would be nice to merge the two onMessage handlers. 416 | * TODO: This method still seems to have a timing issue. Doesn't always show DOM signal as thumbs up on reference site. 417 | * @returns {Bool} true (lets us send asynchronous responses to senders) 418 | */ 419 | function onMessageHandlerSynchronous(message, sender, sendResponse) { 420 | if (message.msg === "APPEND_GPC_PROP") { 421 | let url = new URL(sender.origin); 422 | let parsed = psl.parse(url.hostname); 423 | let domain = parsed.domain; 424 | 425 | const r = sendPrivacySignal(domain); 426 | r.then((r) => { 427 | const response = { 428 | msg: "APPEND_GPC_PROP_RESPONSE", 429 | sendGPC: r, 430 | }; 431 | sendResponse(response); 432 | }); 433 | } 434 | //return true; 435 | } 436 | 437 | /** 438 | * Listeners for information from --POPUP-- or --OPTIONS-- page 439 | * This is the main "hub" for message passing between the extension components 440 | * https://developer.chrome.com/docs/extensions/mv3/messaging/ 441 | */ 442 | async function onMessageHandlerAsync(message, sender, sendResponse) { 443 | if (message.msg === "GET_WELLKNOWN_CHECK_ENABLED") { 444 | const enabled = await isWellknownCheckEnabled(); 445 | await chrome.storage.local.set({ WELLKNOWN_CHECK_ENABLED: enabled }); 446 | sendResponse({ enabled }); 447 | return true; 448 | } 449 | if (message.msg === "TOGGLE_WELLKNOWN_CHECK") { 450 | const enabled = message.data?.enabled !== false; 451 | await storage.set(stores.settings, enabled, "WELLKNOWN_CHECK_ENABLED"); 452 | await chrome.storage.local.set({ WELLKNOWN_CHECK_ENABLED: enabled }); 453 | if (!enabled) { 454 | await storage.clear(stores.wellknownInformation); 455 | wellknown = {}; 456 | } 457 | } 458 | if (message.msg === "CHANGE_IS_DOMAINLISTED") { 459 | let isDomainlisted = message.data.isDomainlisted; 460 | storage.set(stores.settings, isDomainlisted, "IS_DOMAINLISTED"); 461 | } 462 | if (message.msg === "SET_TO_DOMAINLIST") { 463 | let { domain, key } = message.data; 464 | domainlist[domain] = key; // Sets to cache 465 | addDynamicRule(id, domain); 466 | storage.set(stores.domainlist, key, domain); // Sets to long term storage 467 | } 468 | if (message.msg === "POPUP_PROTECTION_REQUESTS") { 469 | console.log("info queried"); 470 | await dataToPopupRequests(); 471 | } 472 | if (message.msg === "CONTENT_SCRIPT_WELLKNOWN") { 473 | const wellknownCheckEnabled = await isWellknownCheckEnabled(); 474 | if (!wellknownCheckEnabled) { 475 | return true; 476 | } 477 | // sender.origin not working for Firefox MV3, instead added a new message argument, message.origin_url 478 | //let url = new URL(sender.origin); 479 | let url = new URL(message.origin_url); 480 | let parsed = psl.parse(url.hostname); 481 | let domain = parsed.domain; 482 | 483 | let tabID = sender.tab.id; 484 | let wellknown = []; 485 | let sendSignal = await storage.get(stores.domainlist, domain); 486 | 487 | wellknown[tabID] = message.data; 488 | let wellknownData = message.data; 489 | 490 | await storage.set(stores.wellknownInformation, wellknownData, domain); 491 | 492 | //await sendData(); 493 | 494 | if (wellknown[tabID] === null && sendSignal == null) { 495 | updatePopupIcon(tabID); 496 | } else if (wellknown[tabID]["gpc"] === true && sendSignal == null) { 497 | chrome.action.setIcon({ 498 | tabId: tabID, 499 | path: "assets/face-icons/optmeow-face-circle-green-128.png", 500 | }); 501 | } 502 | chrome.runtime.onMessage.addListener(async function (message, _, __) { 503 | if (message.msg === "POPUP_PROTECTION") { 504 | await dataToPopup(wellknownData); 505 | } 506 | }); 507 | } 508 | 509 | if (message.msg === "CONTENT_SCRIPT_TAB") { 510 | let url = new URL(sender.origin); 511 | let parsed = psl.parse(url.hostname); 512 | let domain = parsed.domain; 513 | let tabID = sender.tab.id; 514 | if (tabs[tabID] === undefined) { 515 | tabs[tabID] = { 516 | DOMAIN: domain, 517 | REQUEST_DOMAINS: {}, 518 | TIMESTAMP: message.data, 519 | }; 520 | } else if (tabs[tabID].DOMAIN !== domain) { 521 | tabs[tabID].DOMAIN = domain; 522 | let urls = tabs[tabID]["REQUEST_DOMAINS"]; 523 | for (let key in urls) { 524 | if (urls[key]["TIMESTAMP"] >= message.data) { 525 | tabs[tabID]["REQUEST_DOMAINS"][key] = urls[key]; 526 | } else { 527 | delete tabs[tabID]["REQUEST_DOMAINS"][key]; 528 | } 529 | } 530 | tabs[tabID]["TIMESTAMP"] = message.data; 531 | } 532 | } 533 | return true; // Async callbacks require this 534 | } 535 | 536 | function initMessagePassing() { 537 | chrome.runtime.onConnect.addListener(onConnectHandler); 538 | chrome.runtime.onMessage.addListener(onMessageHandlerAsync); 539 | chrome.runtime.onMessage.addListener(onMessageHandlerSynchronous); 540 | } 541 | 542 | function closeMessagePassing() { 543 | chrome.runtime.onConnect.removeListener(onConnectHandler); 544 | chrome.runtime.onMessage.removeListener(onMessageHandlerAsync); 545 | chrome.runtime.onMessage.removeListener(onMessageHandlerSynchronous); 546 | } 547 | 548 | /******************************************************************************/ 549 | /******************************************************************************/ 550 | /********** # Other initializers - run once per enable **********/ 551 | /******************************************************************************/ 552 | /******************************************************************************/ 553 | 554 | /** 555 | * Listener for tab switch that updates the cached current tab variable 556 | */ 557 | function onActivatedProtectionMode(info) { 558 | activeTabID = info.tabId; 559 | console.log("onActivatedProtectionMode called"); 560 | } 561 | 562 | // Handles misc. setup & setup listeners 563 | function initSetup() { 564 | pullToDomainlistCache(); 565 | 566 | // Runs on startup to initialize the cached current tab variable 567 | chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) { 568 | if (tabs.id) { 569 | activeTabID = tabs.id; 570 | } 571 | }); 572 | 573 | chrome.tabs.onActivated.addListener(onActivatedProtectionMode); 574 | setup = true; 575 | } 576 | 577 | function closeSetup() { 578 | chrome.tabs.onActivated.removeListener(onActivatedProtectionMode); 579 | } 580 | 581 | /** 582 | * Inteded to facilitate transitioning between analysis & protection modes 583 | */ 584 | function wipeLocalVars() { 585 | domainlist = {}; // Caches & mirrors domainlist in storage 586 | tabs = {}; // Caches all tab infomration, i.e. requests, etc. 587 | wellknown = {}; // Caches wellknown info to be sent to popup 588 | signalPerTab = {}; // Caches if a signal is sent to render the popup icon 589 | activeTabID = 0; // Caches current active tab id 590 | sendSignal = false; // Caches if the signal can be sent to the curr domain 591 | } 592 | 593 | /******************************************************************************/ 594 | /******************************************************************************/ 595 | /********** # Exportable init / halt functions **********/ 596 | /******************************************************************************/ 597 | /******************************************************************************/ 598 | 599 | export function init() { 600 | reloadVars(); 601 | enableListeners(listenerCallbacks); 602 | initMessagePassing(); 603 | initSetup(); 604 | } 605 | 606 | export function halt() { 607 | disableListeners(listenerCallbacks); 608 | closeMessagePassing(); 609 | closeSetup(); 610 | wipeLocalVars(); 611 | } 612 | --------------------------------------------------------------------------------