22 | OptMeowt ("Opt Me Out") 🐾 is a browser extension for sending
23 | Do Not Sell and Do Not Share signals to websites that you visit.
24 | It is part of [Global Privacy Control](https://globalprivacycontrol.org/),
25 | a Do Not Sell standardization effort we are spearheading.
26 |
27 | Feel free to read more about how OptMeowt works in our
28 | GitHub repo.
29 |
30 |
31 |
32 | Do I need an account to use OptMeowt?
33 |
34 |
35 | You do not need an account to use OptMeowt.
36 |
72 | Do you have an FAQ or a place to report bugs?
73 |
74 |
75 | If you have questions about OptMeowt's functionality or
76 | believe you may have found a bug, please check out our
77 | FAQ \ Known quirks
78 | page on our GitHub
79 | Wiki
80 | to see if we have already addressed the issue.
81 | If you cannot find what you are looking for, please feel free
82 | to open an issue and we will address it as soon as we can!
83 |
84 |
85 |
86 | Note: OptMeowt is a work in progress ...
87 |
88 |
89 |
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 |
20 |
28 | OptMeowt has been successfully installed on your browser! Would you like a quick walkthrough of the options page?
29 |
30 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
Welcome to OptMeowt!
43 |
44 |
45 | OptMeowt requires host permissions to be enabled to function correctly, please enable these permissions using the button below. Note that OptMeowt does not collect your data!
46 |
47 |
50 |
51 |
52 |
53 |
54 |
78 |
79 |
102 |
103 |
127 |
128 |
129 |
130 | ** Please note that the privacy preferences you configure in OptMeowt may be influenced by global browser settings, like enabling GPC browser-wide.
131 |