├── src ├── options │ ├── rule-import-input.css │ ├── constants.js │ ├── rule-tester.css │ ├── loading-dots.css │ ├── alert-popup.css │ ├── loading-dots.js │ ├── alert-popup.js │ ├── modal-dialog.css │ ├── type-input.js │ ├── rule-list.css │ ├── modal-dialog.js │ ├── common.css │ ├── changelog-dialog.js │ ├── rule-import-input.js │ ├── rule-tester.js │ ├── options.css │ ├── rule-list.js │ └── rule-input.css ├── background.html ├── main │ ├── rules │ │ ├── base.js │ │ ├── block.js │ │ ├── secure.js │ │ ├── whitelist.js │ │ └── filter.js │ ├── control.js │ ├── matchers.js │ └── api.js ├── install.js ├── util │ ├── uuid.js │ ├── badges.css │ ├── ui-helpers.js │ ├── i18n.js │ ├── tab.js │ ├── import-export.js │ ├── records.js │ ├── toc.js │ ├── notifier.js │ └── regexp.js ├── migrate.js ├── popup │ ├── browser-action.html │ ├── browser-action.css │ └── browser-action.js └── background.js ├── .github ├── issue_template.md └── workflows │ ├── release.yml │ └── main.yml ├── .gitmodules ├── .gitignore ├── icons ├── ionicons │ ├── add-outline.svg │ ├── bookmark-outline.svg │ ├── code-download-outline.svg │ ├── open-outline.svg │ ├── close-circle-outline.svg │ ├── book-outline.svg │ ├── download-outline.svg │ ├── shuffle-outline.svg │ ├── trash-bin.svg │ ├── backspace-outline.svg │ ├── warning-outline.svg │ ├── list-outline.svg │ ├── options-outline.svg │ └── LICENSE ├── icon.svg ├── icon-block.svg ├── icon-disabled.svg ├── icon-filter.svg ├── icon-redirect.svg ├── icon-secure.svg ├── icon-whitelist.svg └── firefox │ └── copy.svg ├── rules ├── privacy-duckduckgo.json ├── privacy-block-beacon-and-ping.json ├── privacy-qwant-lite.json ├── other-skip-image-downsamplers.json ├── privacy-here.json ├── privacy-linkedin.json ├── privacy-bing.json ├── privacy-youtube.json ├── privacy-facebook.json ├── privacy-amazon.json ├── privacy-common-params.json └── privacy-common-redirectors.json ├── _locales ├── README.rst ├── zh_CN │ └── manual.wiki └── es │ └── messages.json ├── test ├── tld.test.js ├── predefined-rules.test.js ├── patterns.test.js ├── include-exclude.test.js ├── encodeDecode.test.js ├── request-filters.test.js ├── tld-wildcard-matcher.test.js ├── redirect.test.js └── filter.test.js ├── lib ├── tldts │ └── LICENSE └── lit │ └── LICENSE ├── manifest.json ├── README.rst ├── package.json └── CHANGELOG.md /src/options/rule-import-input.css: -------------------------------------------------------------------------------- 1 | * + * { 2 | margin: initial; 3 | } 4 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | ### Expected behavior 2 | 3 | ### Actual behavior 4 | 5 | ### Steps to reproduce the problem 6 | * 7 | * 8 | * -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "src/util/tags-input"] 2 | path = src/util/tags-input 3 | url = https://github.com/tumpio/tags-input.git 4 | branch = request-control 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | web-ext-artifacts/ 3 | manual.html 4 | node_modules 5 | .nyc_output/ 6 | coverage/ 7 | coverage.lcov 8 | 9 | lib/tldts/*.js 10 | lib/lit/lit.css 11 | -------------------------------------------------------------------------------- /icons/ionicons/add-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/background.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /icons/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/icon-block.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/icon-disabled.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/icon-filter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/icon-redirect.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/icon-secure.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/icon-whitelist.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/firefox/copy.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/ionicons/bookmark-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/ionicons/code-download-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/ionicons/open-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/options/constants.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | export const OPTION_SHOW_COUNTER = "option_show_counter"; 6 | export const OPTION_CHANGE_ICON = "option_change_icon"; 7 | -------------------------------------------------------------------------------- /src/main/rules/base.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | export class ControlRule { 6 | constructor({ uuid, tag }) { 7 | this.uuid = uuid; 8 | this.tag = tag; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /icons/ionicons/close-circle-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/ionicons/book-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/install.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | function handleInstalled(details) { 6 | if (details.reason === "install") { 7 | browser.runtime.openOptionsPage(); 8 | } 9 | } 10 | 11 | browser.runtime.onInstalled.addListener(handleInstalled); 12 | -------------------------------------------------------------------------------- /icons/ionicons/download-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/util/uuid.js: -------------------------------------------------------------------------------- 1 | export function uuid() { 2 | const hex = []; 3 | 4 | for (let i = 0; i < 256; i++) { 5 | hex[i] = (i < 16 ? "0" : "") + i.toString(16); 6 | } 7 | 8 | const r = crypto.getRandomValues(new Uint8Array(16)); 9 | 10 | r[6] = (r[6] & 0x0F) | 0x40; 11 | r[8] = (r[8] & 0x3F) | 0x80; 12 | 13 | const h = (...n) => n.map((i) => hex[r[i]]).join(""); 14 | 15 | return `${h(0, 1, 2, 3)}-${h(4, 5)}-${h(6, 7)}-${h(8, 9)}-${h(10, 11, 12, 13, 14, 15)}`; 16 | } 17 | -------------------------------------------------------------------------------- /icons/ionicons/shuffle-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/ionicons/trash-bin.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rules/privacy-duckduckgo.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "uuid": "c7ca4ba4-1a16-4b98-b645-62d22ae582ee", 4 | "pattern": { 5 | "scheme": "*", 6 | "host": [ 7 | "duckduckgo.com" 8 | ], 9 | "path": [ 10 | "l/?*" 11 | ] 12 | }, 13 | "types": [ 14 | "main_frame", 15 | "sub_frame" 16 | ], 17 | "action": "filter", 18 | "active": true, 19 | "tag": "privacy-duckduckgo" 20 | } 21 | ] -------------------------------------------------------------------------------- /icons/ionicons/backspace-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/ionicons/warning-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rules/privacy-block-beacon-and-ping.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "title": "Block Beacon and Ping requests", 4 | "description": "Blocks Beacon and Ping requests. The Beacon API is often used for logging user activity and sending analytics data to the server.", 5 | "tag": "block-beacon-ping", 6 | "uuid": "32db1f93-f99d-4c45-8485-e5c7beec5a69", 7 | "pattern": { 8 | "allUrls": true 9 | }, 10 | "action": "block", 11 | "active": true, 12 | "types": [ 13 | "beacon", 14 | "ping" 15 | ] 16 | } 17 | ] -------------------------------------------------------------------------------- /rules/privacy-qwant-lite.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "title": "Skip Qwant Lite redirect", 4 | "description": "Skip Qwant Lite redirect in search results", 5 | "uuid": "62b1a6c1-76ea-43c1-9d4d-dde55d5855e9", 6 | "pattern": { 7 | "scheme": "*", 8 | "host": [ 9 | "lite.qwant.com" 10 | ], 11 | "path": [ 12 | "redirect/*" 13 | ] 14 | }, 15 | "types": [ 16 | "main_frame" 17 | ], 18 | "action": "redirect", 19 | "active": true, 20 | "redirectUrl": "{pathname/\\/redirect\\/[A-Za-z0-9]+==\\//|decodeBase64|decodeURIComponent}" 21 | } 22 | ] 23 | -------------------------------------------------------------------------------- /src/main/rules/block.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import { ControlRule } from "./base.js"; 6 | 7 | export const BLOCKING_RESPONSE = { cancel: true }; 8 | 9 | export class BlockRule extends ControlRule { 10 | resolve(request) { 11 | this.constructor.notify(this, request); 12 | return BLOCKING_RESPONSE; 13 | } 14 | } 15 | 16 | BlockRule.icon = "/icons/icon-block.svg"; 17 | BlockRule.action = "block"; 18 | -------------------------------------------------------------------------------- /src/main/rules/secure.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import { ControlRule } from "./base.js"; 6 | 7 | export const SECURE_RESPONSE = { upgradeToSecure: true }; 8 | 9 | export class SecureRule extends ControlRule { 10 | resolve(request) { 11 | this.constructor.notify(this, request); 12 | return SECURE_RESPONSE; 13 | } 14 | } 15 | 16 | SecureRule.icon = "/icons/icon-secure.svg"; 17 | SecureRule.action = "secure"; 18 | -------------------------------------------------------------------------------- /icons/ionicons/list-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /_locales/README.rst: -------------------------------------------------------------------------------- 1 | Translating Request Control 2 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 3 | 4 | To begin translation copy **en** folder and its content to a new 5 | directory and name it after the new locale. 6 | 7 | **Note:** Separator for regional variant in directory name must be 8 | written with underscore, e.g. "en_US". 9 | 10 | Locale's directory content: 11 | 12 | - messages.json file - Includes all extension's localised strings including 13 | extension's name and description. 14 | - manual.wiki - Is the localised manual written in `MediaWiki `_. 15 | A Html page will be auto-generated from it using pandoc. 16 | -------------------------------------------------------------------------------- /icons/ionicons/options-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/options/rule-tester.css: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | #body { 6 | max-width: 40em; 7 | } 8 | 9 | .test-result { 10 | background: black; 11 | color: white; 12 | font-weight: bold; 13 | padding: 0.2em 0.5em; 14 | word-break: break-all; 15 | width: 100%; 16 | display: flex; 17 | align-items: baseline; 18 | } 19 | 20 | .card { 21 | border-width: 2px; 22 | } 23 | 24 | .test-result > span:first-child { 25 | margin-right: 0.5em; 26 | } 27 | -------------------------------------------------------------------------------- /src/main/rules/whitelist.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import { ControlRule } from "./base.js"; 6 | 7 | export class WhitelistRule extends ControlRule { 8 | resolve() { 9 | return null; 10 | } 11 | } 12 | 13 | export class LoggedWhitelistRule extends WhitelistRule { 14 | resolve(request) { 15 | this.constructor.notify(this, request); 16 | return null; 17 | } 18 | } 19 | 20 | WhitelistRule.icon = "/icons/icon-whitelist.svg"; 21 | WhitelistRule.action = "whitelist"; 22 | -------------------------------------------------------------------------------- /src/options/loading-dots.css: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | @keyframes loading-dots { 6 | 0% { 7 | opacity: 0.2; 8 | } 9 | 20% { 10 | opacity: 1; 11 | } 12 | 100% { 13 | opacity: 0.2; 14 | } 15 | } 16 | 17 | span { 18 | animation-name: loading-dots; 19 | animation-duration: 1.4s; 20 | animation-iteration-count: infinite; 21 | animation-fill-mode: both; 22 | } 23 | 24 | span:nth-of-type(2) { 25 | animation-delay: 0.2s; 26 | } 27 | 28 | span:nth-of-type(3) { 29 | animation-delay: 0.4s; 30 | } 31 | -------------------------------------------------------------------------------- /rules/other-skip-image-downsamplers.json: -------------------------------------------------------------------------------- 1 | { 2 | "uuid": "07f60a40-9293-406e-aba9-3926bdb0ef2c", 3 | "pattern": { 4 | "scheme": "*", 5 | "host": [ 6 | "*" 7 | ], 8 | "path": [ 9 | "*" 10 | ], 11 | "includes": [ 12 | "/\\/(ap_resize\\/ap_resize|image|imageproxy|resizer\\/resizer|safe_image)(.php)?\\?/" 13 | ] 14 | }, 15 | "types": [ 16 | "image" 17 | ], 18 | "action": "filter", 19 | "active": true, 20 | "title": "Skip image downsamplers", 21 | "description": "This filter retrieves the original pictures from the original domains. Disabling this filter will restore the downsampled images.", 22 | "tag": "skip-image-downsamplers" 23 | } -------------------------------------------------------------------------------- /src/util/badges.css: -------------------------------------------------------------------------------- 1 | .badge-light { 2 | color: black; 3 | background-color: #e1e0e0; 4 | } 5 | 6 | .badge-dark { 7 | color: white; 8 | background-color: #3f3f3f; 9 | } 10 | 11 | .badge-info { 12 | color: #fff; 13 | background-color: #3b67a0; 14 | } 15 | 16 | .badge-origin { 17 | color: #fff; 18 | background-color: #a03b3b; 19 | } 20 | 21 | .badge-success { 22 | color: #fff; 23 | background-color: #3ba03b; 24 | } 25 | 26 | .badge:not(:empty):not(.d-none) { 27 | display: inline-block; 28 | padding: 0.25em 0.5em; 29 | font-size: 0.6rem; 30 | font-weight: 700; 31 | line-height: 1; 32 | text-align: center; 33 | white-space: nowrap; 34 | vertical-align: baseline; 35 | border-radius: 0.15rem; 36 | max-width: 100%; 37 | } 38 | -------------------------------------------------------------------------------- /rules/privacy-here.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "uuid": "e1602933-69a7-4901-9259-42b5551101d0", 4 | "pattern": { 5 | "scheme": "*", 6 | "host": [ 7 | "*.maps.api.here.com", 8 | "*.maps.cit.api.here.com" 9 | ], 10 | "path": [ 11 | "/maptile/2.1/maptile/*" 12 | ] 13 | }, 14 | "types": [ 15 | "image" 16 | ], 17 | "action": "filter", 18 | "active": true, 19 | "title": "HERE maps API 2.5.4", 20 | "tag": "privacy-here", 21 | "paramsFilter": { 22 | "values": [ 23 | "app_code", 24 | "app_id", 25 | "lg", 26 | "token" 27 | ], 28 | "invert": true 29 | }, 30 | "skipRedirectionFilter": true 31 | } 32 | ] -------------------------------------------------------------------------------- /src/options/alert-popup.css: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | :host { 6 | display: flex; 7 | align-items: center; 8 | position: fixed; 9 | right: 0; 10 | margin: 0.5em; 11 | transition: top 0.15s ease-out; 12 | top: 0; 13 | z-index: 100; 14 | background-color: #d50000; 15 | color: #fff; 16 | border-radius: 4px; 17 | padding: 1em; 18 | font-weight: bold; 19 | font-size: 0.9em; 20 | box-shadow: 0 0 5px #888888; 21 | } 22 | 23 | #close { 24 | font-size: 1.5em; 25 | margin: 0; 26 | padding: 0 0.3em; 27 | } 28 | 29 | #close:hover, 30 | #close:focus { 31 | text-decoration: none; 32 | cursor: pointer; 33 | font-weight: normal; 34 | } 35 | -------------------------------------------------------------------------------- /test/tld.test.js: -------------------------------------------------------------------------------- 1 | import {libTld} from "../src/main/url"; 2 | 3 | test("Get domain name", () => { 4 | expect(libTld.getDomain("http://example.com")).toBe("example.com"); 5 | expect(libTld.getDomain("http://nakagyo.kyoto.jp.example.nakagyo.kyoto.jp")).toBe("example.nakagyo.kyoto.jp"); 6 | expect( 7 | libTld.getDomain("http://user:pass@nakagyo.kyoto.jp.example.nakagyo.kyoto.jp:10192/foo") 8 | ).toBe("example.nakagyo.kyoto.jp"); 9 | expect(libTld.getDomain("data:image/png;base64,iVBORw0KG==")).toBe(null); 10 | expect(libTld.getDomain("")).toBe(null); 11 | expect(libTld.getDomain("localhost")).toBe(null); 12 | expect(libTld.getDomain("about:blank")).toBe(null); 13 | expect(libTld.getDomain("192.168.0.1")).toBe("192.168.0.1"); 14 | expect(libTld.getDomain("192.168.0.f0")).toBe("0.f0"); 15 | expect(libTld.getDomain("http://192.168.0.1:8080")).toBe("192.168.0.1"); 16 | }); 17 | -------------------------------------------------------------------------------- /src/options/loading-dots.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | class LoadingDots extends HTMLElement { 6 | constructor() { 7 | super(); 8 | const shadow = this.attachShadow({ mode: "open" }); 9 | const link = document.createElement("link"); 10 | link.setAttribute("rel", "stylesheet"); 11 | link.setAttribute("href", "loading-dots.css"); 12 | shadow.appendChild(link); 13 | 14 | const dot = document.createElement("span"); 15 | dot.textContent = "."; 16 | shadow.appendChild(dot.cloneNode(true)); 17 | shadow.appendChild(dot.cloneNode(true)); 18 | shadow.appendChild(dot.cloneNode(true)); 19 | } 20 | } 21 | 22 | customElements.define("loading-dots", LoadingDots); 23 | -------------------------------------------------------------------------------- /rules/privacy-linkedin.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "uuid": "d78b46f3-6c04-43a8-a907-83c161138e61", 4 | "pattern": { 5 | "scheme": "*", 6 | "host": [ 7 | "www.linkedin.com" 8 | ], 9 | "path": [ 10 | "*" 11 | ], 12 | "excludes": [ 13 | "/\\/checkpoint\\//", 14 | "/\\/jobs\\/\\?/", 15 | "/\\/login\\?/", 16 | "/\\/oauth\\/v2\\//", 17 | "/\\/search\\/\\?/", 18 | "/\\/uas\\/login\\?/", 19 | "/\\/uas\\/logout\\?/" 20 | ] 21 | }, 22 | "types": [ 23 | "main_frame", 24 | "sub_frame" 25 | ], 26 | "action": "filter", 27 | "active": true, 28 | "tag": "privacy-linkedin", 29 | "paramsFilter": { 30 | "values": [ 31 | "url" 32 | ], 33 | "invert": true 34 | } 35 | } 36 | ] 37 | -------------------------------------------------------------------------------- /src/util/ui-helpers.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | export function onToggleButtonChange(e) { 6 | setButtonChecked(e.target, e.target.checked); 7 | } 8 | 9 | export function setButtonChecked(button, checked) { 10 | button.checked = checked === true; 11 | button.parentNode.classList.toggle("active", checked === true); 12 | } 13 | 14 | export function setButtonDisabled(button, disabled) { 15 | button.disabled = disabled === true; 16 | button.parentNode.classList.toggle("disabled", disabled === true); 17 | } 18 | 19 | export function toggleHidden(hidden, ...elements) { 20 | const hiddenClass = "d-none"; 21 | if (typeof hidden === "boolean") { 22 | elements.forEach((element) => element.classList.toggle(hiddenClass, hidden)); 23 | } else if (hidden) { 24 | hidden.classList.toggle(hiddenClass); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /rules/privacy-bing.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "title": "Remove query parameters from Bing", 4 | "description": "Removes possible tracking query parameters used by Bing", 5 | "uuid": "f53e0a5e-d658-4da5-9b12-143e24c4e1ba", 6 | "pattern": { 7 | "scheme": "*", 8 | "host": [ 9 | "*.bing.com" 10 | ], 11 | "path": [ 12 | "*" 13 | ] 14 | }, 15 | "types": [ 16 | "main_frame", 17 | "sub_frame", 18 | "image" 19 | ], 20 | "action": "filter", 21 | "active": true, 22 | "tag": "privacy-bing", 23 | "paramsFilter": { 24 | "values": [ 25 | "cvid", 26 | "ehk", 27 | "form", 28 | "lg", 29 | "pq", 30 | "qs", 31 | "ru", 32 | "sc", 33 | "sk", 34 | "sp" 35 | ] 36 | }, 37 | "skipRedirectionFilter": true 38 | } 39 | ] -------------------------------------------------------------------------------- /lib/tldts/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Thomas Parisot, 2018 Rémi Berson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 4 | associated documentation files (the "Software"), to deal in the Software without restriction, 5 | including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 6 | and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, 7 | subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 12 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 13 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 14 | -------------------------------------------------------------------------------- /src/migrate.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | function handleInstalled(details) { 6 | if (details.reason === "update") { 7 | if (!details.previousVersion) { 8 | details.previousVersion = "1.13"; 9 | } 10 | 11 | const versions = details.previousVersion.split("."); 12 | 13 | // migrate from < 1.14 14 | if (Number(versions[0]) < 1 || (Number(versions[0]) === 1 && Number(versions[1]) < 14)) { 15 | browser.storage.local.get("rules").then((options) => { 16 | for (const rule of options.rules) { 17 | if (rule.uuid === "60f46cfa-b906-4a2d-ab66-8f26dc35e97f") { 18 | rule.redirectDocument = true; 19 | } 20 | } 21 | browser.storage.local.set({ rules: options.rules }); 22 | }); 23 | } 24 | } 25 | } 26 | 27 | browser.runtime.onInstalled.addListener(handleInstalled); 28 | -------------------------------------------------------------------------------- /src/options/alert-popup.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | class AlertPopup extends HTMLElement { 6 | constructor() { 7 | super(); 8 | this.message = ""; 9 | const template = document.getElementById("alert-popup"); 10 | this.attachShadow({ mode: "open" }).appendChild(template.content.cloneNode(true)); 11 | 12 | this.shadowRoot.getElementById("close").addEventListener( 13 | "click", 14 | () => { 15 | this.remove(); 16 | }, 17 | { once: true } 18 | ); 19 | } 20 | 21 | connectedCallback() { 22 | this.shadowRoot.getElementById("message").textContent = this.message; 23 | } 24 | } 25 | 26 | customElements.define("alert-popup", AlertPopup); 27 | 28 | export function showAlertPopup(message) { 29 | const popup = document.createElement("alert-popup"); 30 | popup.message = message; 31 | document.body.append(popup); 32 | } 33 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "__MSG_extensionName__", 3 | "description": "__MSG_extensionDescription__", 4 | "default_locale": "en", 5 | "version": "1.16.0a1", 6 | "manifest_version": 2, 7 | "icons": { 8 | "48": "icons/icon.svg", 9 | "96": "icons/icon.svg" 10 | }, 11 | "browser_specific_settings": { 12 | "gecko": { 13 | "id": "{1b1e6108-2d88-4f0f-a338-01f9dbcccd6f}", 14 | "strict_min_version": "79.0a01" 15 | } 16 | }, 17 | "permissions": [ 18 | "", 19 | "storage", 20 | "webNavigation", 21 | "webRequest", 22 | "webRequestBlocking" 23 | ], 24 | "options_ui": { 25 | "page": "src/options/options.html", 26 | "open_in_tab": true, 27 | "browser_style": false 28 | }, 29 | "browser_action": { 30 | "browser_style": false, 31 | "default_icon": "icons/icon.svg", 32 | "default_title": "__MSG_extensionName__", 33 | "default_popup": "src/popup/browser-action.html" 34 | }, 35 | "background": { 36 | "page": "src/background.html" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/lit/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Arham Jain 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/options/modal-dialog.css: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | :host { 6 | position: fixed; 7 | z-index: 999; 8 | left: 0; 9 | top: 0; 10 | bottom: 0; 11 | width: 100%; 12 | height: 100%; 13 | overflow: auto; 14 | background-color: rgba(0, 0, 0, 0.24); 15 | margin: 0 !important; 16 | } 17 | 18 | #body { 19 | background-color: #fefefe; 20 | margin: 1em auto; 21 | padding: 1em; 22 | border: 1px solid black; 23 | max-width: max-content; 24 | } 25 | 26 | .header { 27 | display: flex; 28 | } 29 | 30 | .header > span:first-child { 31 | flex-grow: 1; 32 | } 33 | 34 | #title { 35 | font-size: 1.2em; 36 | } 37 | 38 | #close { 39 | color: black; 40 | font-size: 1.3em; 41 | font-weight: bold; 42 | margin: 0 0 0 0.8em; 43 | } 44 | 45 | #close:hover, 46 | #close:focus { 47 | text-decoration: none; 48 | cursor: pointer; 49 | font-weight: normal; 50 | } 51 | 52 | #actions { 53 | text-align: end; 54 | } 55 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 1.* 5 | 6 | jobs: 7 | release: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - run: sudo apt-get -y install pandoc 12 | - uses: actions/cache@v2 13 | with: 14 | path: ~/.npm 15 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 16 | restore-keys: | 17 | ${{ runner.os }}-node- 18 | 19 | - run: npm ci 20 | - run: npm test 21 | - run: npm run build 22 | - run: npm run lint-build 23 | 24 | - uses: ffurrer2/extract-release-notes@v1 25 | id: extract_notes 26 | 27 | - uses: softprops/action-gh-release@v1 28 | with: 29 | files: web-ext-artifacts/*.zip 30 | body: ${{ steps.extract_notes.outputs.release_notes }} 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | 34 | - run: npm run deploy 35 | env: 36 | WEB_EXT_CHANNEL: listed 37 | WEB_EXT_API_KEY: ${{ secrets.WEB_EXT_API_KEY }} 38 | WEB_EXT_API_SECRET: ${{ secrets.WEB_EXT_API_SECRET }} 39 | -------------------------------------------------------------------------------- /icons/ionicons/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present Ionic (http://ionic.io/) 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/options/type-input.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | class TypeInput extends HTMLInputElement { 6 | constructor() { 7 | super(); 8 | 9 | const label = document.createElement("label"); 10 | const value = this.getAttribute("value"); 11 | const index = this.getAttribute("index"); 12 | const text = browser.i18n.getMessage(value); 13 | 14 | this.type = "checkbox"; 15 | this.autocomplete = "off"; 16 | this.dataset.index = index; 17 | this.classList.add("type"); 18 | label.classList.add("btn"); 19 | 20 | if (index === 0) { 21 | label.classList.add("active"); 22 | this.checked = true; 23 | } else if (index > 4) { 24 | label.classList.add("d-none"); 25 | this.classList.add("extra-type"); 26 | } 27 | this.replaceWith(label); 28 | label.append(this, text); 29 | } 30 | } 31 | 32 | customElements.define("type-input", TypeInput, { extends: "input" }); 33 | -------------------------------------------------------------------------------- /test/predefined-rules.test.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | 4 | import { createMatchPatterns, createRule } from "../src/main/api"; 5 | 6 | test("Create rules", async () => { 7 | const files = await readRuleFiles(); 8 | const rules = files.flatMap((file) => file.map(createRule)); 9 | expect(rules.length).toBeGreaterThan(0); 10 | }); 11 | 12 | test("Create match patterns", async () => { 13 | const files = await readRuleFiles(); 14 | const allPatterns = files.flatMap((file) => 15 | file.flatMap((rule) => { 16 | const patterns = createMatchPatterns(rule.pattern); 17 | expect(patterns.length).toBeGreaterThan(0); 18 | return patterns; 19 | }) 20 | ); 21 | expect(allPatterns.length).toBeGreaterThan(0); 22 | }); 23 | 24 | async function readRuleFiles() { 25 | const imports = fs 26 | .readdirSync("./rules") 27 | .filter((name) => path.extname(name) === ".json") 28 | .map((name) => path.join("../rules", name)) 29 | .map((path) => import(path)); 30 | const files = await Promise.all(imports); 31 | return files.map((module) => module.default).map((rules) => (Array.isArray(rules) ? rules : [rules])); 32 | } 33 | -------------------------------------------------------------------------------- /src/util/i18n.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | /** 6 | * i18n WebExt-API HTML document translator 7 | * Translates nodes marked with dataset (data-i18n, data-i18n-title, data-i18n-placeholder) 8 | * attributes on DOMContentLoaded. 9 | * @param documentNode 10 | */ 11 | export function translateDocument(documentNode) { 12 | documentNode.querySelectorAll("[data-i18n]").forEach((node) => { 13 | node.textContent = browser.i18n.getMessage(node.dataset.i18n); 14 | }); 15 | documentNode.querySelectorAll("[data-i18n-title]").forEach((node) => { 16 | node.title = browser.i18n.getMessage(node.dataset.i18nTitle); 17 | }); 18 | documentNode.querySelectorAll("[data-i18n-placeholder]").forEach((node) => { 19 | node.placeholder = browser.i18n.getMessage(node.dataset.i18nPlaceholder); 20 | }); 21 | } 22 | 23 | export function translateTemplates() { 24 | for (const template of document.getElementsByTagName("template")) { 25 | translateDocument(template.content); 26 | } 27 | } 28 | 29 | translateDocument(document); 30 | translateTemplates(); 31 | -------------------------------------------------------------------------------- /src/options/rule-list.css: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | rule-list * + * { 6 | margin-top: 0; 7 | margin-bottom: 0; 8 | } 9 | 10 | rule-list .list { 11 | list-style: none; 12 | padding: 0; 13 | margin: 0; 14 | overflow: hidden; 15 | } 16 | 17 | rule-list .list.collapsed { 18 | height: 0; 19 | } 20 | 21 | rule-list .list > * { 22 | position: relative; 23 | margin: 0; 24 | } 25 | 26 | rule-list .header:not(.d-none) { 27 | display: flex; 28 | align-items: center; 29 | padding: 0.2em 0; 30 | font-size: 0.9em; 31 | } 32 | 33 | rule-list .header > * { 34 | margin-right: 0.5em; 35 | } 36 | 37 | rule-list .header > * + * { 38 | margin-top: 0.2em; 39 | margin-bottom: 0.2em; 40 | } 41 | 42 | rule-list .select-all { 43 | margin: 0 0.8rem; 44 | display: flex; 45 | } 46 | 47 | rule-list .header > .collapse-button { 48 | margin-left: auto; 49 | margin-right: 1em; 50 | } 51 | 52 | rule-list .header > .collapse-button:before { 53 | vertical-align: middle; 54 | } 55 | 56 | @media (max-width: 35em) { 57 | rule-list .select-all { 58 | display: none; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/util/tab.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | document.addEventListener("DOMContentLoaded", setTabFromHash); 6 | 7 | window.addEventListener("hashchange", setTabFromHash); 8 | 9 | function setTabFromHash() { 10 | const { hash } = window.location; 11 | const tabHash = "#tab-"; 12 | if (hash.startsWith(tabHash)) { 13 | changeTab(hash.substring(tabHash.length)); 14 | } 15 | } 16 | 17 | function changeTab(tab) { 18 | const tabInfo = tab.split("#"); 19 | const tabSelector = document.querySelector(`.tab-selector[href='#tab-${tabInfo[0]}']`); 20 | if (!tabSelector || tabSelector.classList.contains("active")) { 21 | return; 22 | } 23 | document.querySelectorAll(".tab-selector.active").forEach((selector) => selector.classList.remove("active")); 24 | tabSelector.classList.add("active"); 25 | 26 | document.querySelectorAll(".tab-pane.active").forEach((tab) => tab.classList.remove("active")); 27 | document.getElementById(`tab-${tabInfo[0]}`).classList.add("active"); 28 | 29 | document.title = browser.i18n.getMessage(tabSelector.dataset.tabTitle); 30 | 31 | if (tabInfo[1]) { 32 | history.replaceState(tabInfo[1], tabInfo[1], `#${tabInfo[1]}`); 33 | window.location.hash = tabInfo[1]; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/options/modal-dialog.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | export default class ModalDialog extends HTMLElement { 6 | constructor() { 7 | super(); 8 | const template = document.getElementById("modal-dialog"); 9 | this.attachShadow({ mode: "open" }).appendChild(template.content.cloneNode(true)); 10 | 11 | this._escapeKeyListener = (e) => { 12 | if (e.key === "Escape") { 13 | this.remove(); 14 | } 15 | }; 16 | this.shadowRoot.getElementById("close").addEventListener( 17 | "click", 18 | () => { 19 | this.remove(); 20 | }, 21 | { once: true } 22 | ); 23 | 24 | this.shadowRoot.getElementById("body").addEventListener("click", (e) => e.stopPropagation()); 25 | this.addEventListener( 26 | "click", 27 | () => { 28 | this.remove(); 29 | }, 30 | { once: true } 31 | ); 32 | } 33 | 34 | connectedCallback() { 35 | window.addEventListener("keydown", this._escapeKeyListener); 36 | } 37 | 38 | disconnectedCallback() { 39 | window.removeEventListener("keydown", this._escapeKeyListener); 40 | } 41 | } 42 | 43 | customElements.define("modal-dialog", ModalDialog); 44 | -------------------------------------------------------------------------------- /src/util/import-export.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | /** 6 | * Export object as json file download 7 | */ 8 | export function exportObject(name, object, mimeType = "application/json", replacer = null, space = 2) { 9 | const data = JSON.stringify(object, replacer, space), 10 | blob = new Blob([data], { type: mimeType }), 11 | url = URL.createObjectURL(blob), 12 | link = document.createElement("a"); 13 | 14 | document.body.appendChild(link); 15 | 16 | link.href = url; 17 | link.download = name; 18 | link.click(); 19 | 20 | setTimeout(() => { 21 | URL.revokeObjectURL(url); 22 | link.remove(); 23 | }, 0); 24 | } 25 | 26 | /** 27 | * Import json file as object 28 | * @param file json file 29 | * @returns {Promise} 30 | */ 31 | export function importFile(file) { 32 | const reader = new FileReader(); 33 | return new Promise((resolve, reject) => { 34 | reader.addEventListener("load", (e) => { 35 | try { 36 | const json = JSON.parse(e.target.result); 37 | resolve(json); 38 | } catch (ex) { 39 | reject(ex); 40 | } 41 | }); 42 | reader.addEventListener("error", () => { 43 | reject(new Error("FileReader error")); 44 | }); 45 | reader.readAsText(file); 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /rules/privacy-youtube.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "uuid": "1fa1d22e-7694-46f0-9da6-fa6536433406", 4 | "pattern": { 5 | "scheme": "*", 6 | "host": [ 7 | "*.youtube.com" 8 | ], 9 | "path": [ 10 | "*" 11 | ] 12 | }, 13 | "types": [ 14 | "main_frame", 15 | "sub_frame" 16 | ], 17 | "action": "filter", 18 | "active": true, 19 | "tag": "privacy-youtube", 20 | "paramsFilter": { 21 | "values": [ 22 | "feature", 23 | "gclid", 24 | "kw", 25 | "sp" 26 | ] 27 | }, 28 | "skipRedirectionFilter": true, 29 | "description": "Imported from Neat URL webextension." 30 | }, 31 | { 32 | "title": "Block Youtube mouselogger and watchtime logger", 33 | "uuid": "23f612f1-d903-4715-99a5-5559035400e5", 34 | "pattern": { 35 | "scheme": "*", 36 | "host": [ 37 | "*.youtube.com" 38 | ], 39 | "path": [ 40 | "api/stats/watchtime*", 41 | "youtubei/v1/log_event*" 42 | ] 43 | }, 44 | "types": [ 45 | "image", 46 | "xmlhttprequest" 47 | ], 48 | "action": "block", 49 | "active": false, 50 | "description": "May cause issues after the end of the video if logged in. Disabled by default.", 51 | "tag": "privacy-youtube" 52 | }, 53 | { 54 | "uuid": "4ead820a-429f-4e7c-a7ca-7241305bcedb", 55 | "pattern": { 56 | "scheme": "*", 57 | "host": [ 58 | "youtu.be" 59 | ], 60 | "path": [ 61 | "*" 62 | ] 63 | }, 64 | "action": "redirect", 65 | "active": true, 66 | "redirectUrl": "https://www.youtube.com/watch?v={pathname:1}{search/^\\?/&}{hash}", 67 | "tag": "privacy-youtube", 68 | "types": [ 69 | "main_frame" 70 | ] 71 | } 72 | ] -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | lint: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v2 9 | - uses: actions/cache@v2 10 | with: 11 | path: ~/.npm 12 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 13 | restore-keys: | 14 | ${{ runner.os }}-node- 15 | - run: npm ci 16 | - run: npm run lint 17 | 18 | test: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v2 22 | - uses: actions/cache@v2 23 | with: 24 | path: ~/.npm 25 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 26 | restore-keys: | 27 | ${{ runner.os }}-node- 28 | - run: npm ci 29 | - run: npm test -- --ci --coverage 30 | - uses: codecov/codecov-action@v1 31 | 32 | build: 33 | runs-on: ubuntu-latest 34 | steps: 35 | - uses: actions/checkout@v2 36 | - run: sudo apt-get -y install pandoc 37 | - uses: actions/cache@v2 38 | with: 39 | path: ~/.npm 40 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 41 | restore-keys: | 42 | ${{ runner.os }}-node- 43 | - run: npm ci 44 | - run: npm run build 45 | - uses: actions/upload-artifact@v2 46 | with: 47 | name: web-ext-artifacts 48 | path: web-ext-artifacts/*.zip 49 | 50 | lint-build: 51 | needs: build 52 | runs-on: ubuntu-latest 53 | steps: 54 | - uses: actions/checkout@v2 55 | - uses: actions/cache@v2 56 | with: 57 | path: ~/.npm 58 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 59 | restore-keys: | 60 | ${{ runner.os }}-node- 61 | - uses: actions/download-artifact@v2 62 | with: 63 | name: web-ext-artifacts 64 | path: web-ext-artifacts 65 | - run: npm ci 66 | - run: npm run lint-build 67 | -------------------------------------------------------------------------------- /src/main/rules/filter.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import { createRegexpPattern } from "../../util/regexp.js"; 6 | import { DomainMatcher } from "../matchers.js"; 7 | import { parseInlineUrl, trimQueryParameters, UrlParser } from "../url.js"; 8 | import { BaseRedirectRule } from "./redirect.js"; 9 | 10 | export class FilterRule extends BaseRedirectRule { 11 | constructor({ 12 | uuid, 13 | tag, 14 | paramsFilter = null, 15 | trimAllParams = false, 16 | skipRedirectionFilter = false, 17 | skipOnSameDomain = false, 18 | redirectDocument = false, 19 | } = {}) { 20 | super({ uuid, tag, redirectDocument }); 21 | this.queryParamsPattern = paramsFilter ? createRegexpPattern(paramsFilter.values) : null; 22 | this.invertQueryTrimming = paramsFilter ? paramsFilter.invert : false; 23 | this.removeQueryString = trimAllParams; 24 | this.skipInlineUrlParsing = skipRedirectionFilter; 25 | this.skipOnSameDomain = skipOnSameDomain; 26 | } 27 | 28 | apply(url) { 29 | const trimmedUrl = this.filterQueryParameters(url); 30 | if (this.skipInlineUrlParsing) { 31 | return trimmedUrl; 32 | } 33 | return this.filterInlineUrl(trimmedUrl); 34 | } 35 | 36 | filterQueryParameters(url) { 37 | if (this.removeQueryString) { 38 | const parser = new UrlParser(url); 39 | parser.search = ""; 40 | return parser.href; 41 | } 42 | return trimQueryParameters(url, this.queryParamsPattern, this.invertQueryTrimming); 43 | } 44 | 45 | filterInlineUrl(url) { 46 | const inlineUrl = parseInlineUrl(url); 47 | if (inlineUrl == null || (this.skipOnSameDomain && DomainMatcher.testUrls(url, inlineUrl))) { 48 | return url; 49 | } 50 | return inlineUrl; 51 | } 52 | } 53 | 54 | FilterRule.icon = "/icons/icon-filter.svg"; 55 | FilterRule.action = "filter"; 56 | -------------------------------------------------------------------------------- /rules/privacy-facebook.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "uuid": "07be1337-ceec-4a93-a49e-67e051124a7f", 4 | "tag": "privacy-facebook", 5 | "pattern": { 6 | "scheme": "*", 7 | "host": [ 8 | "l.facebook.*", 9 | "l.instagram.*", 10 | "lm.facebook.*" 11 | ], 12 | "topLevelDomains": [ 13 | "com", 14 | "net" 15 | ], 16 | "path": [ 17 | "?u=*", 18 | "l.php?u=*" 19 | ] 20 | }, 21 | "types": [ 22 | "main_frame", 23 | "sub_frame" 24 | ], 25 | "action": "filter", 26 | "active": true 27 | }, 28 | { 29 | "uuid": "92a96b21-9076-43c2-b030-167bf420e733", 30 | "pattern": { 31 | "scheme": "*", 32 | "host": [ 33 | "m.facebook.com", 34 | "www.facebook.com" 35 | ], 36 | "path": [ 37 | "*" 38 | ] 39 | }, 40 | "types": [ 41 | "main_frame" 42 | ], 43 | "action": "filter", 44 | "active": true, 45 | "tag": "privacy-facebook", 46 | "paramsFilter": { 47 | "values": [ 48 | "_*", 49 | "extid", 50 | "flite", 51 | "fref", 52 | "mt_nav", 53 | "ref", 54 | "referral_code", 55 | "refsrc", 56 | "rid", 57 | "rt", 58 | "sfnsn" 59 | ] 60 | }, 61 | "skipRedirectionFilter": true 62 | }, 63 | { 64 | "uuid": "d5f1deb0-f7b4-4c45-9a7e-6ea04a051e51", 65 | "pattern": { 66 | "scheme": "*", 67 | "host": [ 68 | "fb.me" 69 | ], 70 | "path": [ 71 | "*" 72 | ] 73 | }, 74 | "types": [ 75 | "main_frame", 76 | "sub_frame" 77 | ], 78 | "action": "redirect", 79 | "active": true, 80 | "tag": "privacy-facebook", 81 | "redirectUrl": "https://www.facebook.com/{pathname:1}" 82 | } 83 | ] -------------------------------------------------------------------------------- /src/util/records.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | const records = new Map(); 6 | 7 | export function add(tabId, record) { 8 | let tabRecords = records.get(tabId); 9 | if (!tabRecords) { 10 | tabRecords = []; 11 | records.set(tabId, tabRecords); 12 | } 13 | tabRecords.push(record); 14 | return tabRecords.length; 15 | } 16 | 17 | export function has(tabId) { 18 | return records.has(tabId); 19 | } 20 | 21 | export function keys() { 22 | return records.keys(); 23 | } 24 | 25 | export function clear() { 26 | return records.clear(); 27 | } 28 | 29 | export function getTabRecords() { 30 | return browser.tabs 31 | .query({ 32 | currentWindow: true, 33 | active: true, 34 | }) 35 | .then((tabs) => { 36 | return records.get(tabs[0].id); 37 | }); 38 | } 39 | 40 | export function setTabRecords(tabId, tabRecords) { 41 | return records.set(tabId, tabRecords); 42 | } 43 | 44 | export function removeTabRecords(tabId) { 45 | records.delete(tabId); 46 | } 47 | 48 | export function getLastRedirectRecords(tabId, url, isServerRedirect = false, limit = 5) { 49 | const tabRecords = records.get(tabId); 50 | const lastRecord = getLastRedirectRecord(tabRecords, url, isServerRedirect, limit); 51 | 52 | if (!lastRecord) { 53 | return []; 54 | } 55 | return getLinkedRedirectRecords(lastRecord, tabRecords, limit); 56 | } 57 | 58 | function getLastRedirectRecord(records, url, isServerRedirect, limit) { 59 | let i = 0; 60 | while (i < limit && records.length > 0) { 61 | const record = records.pop(); 62 | if (record.target === url || (isServerRedirect && record.target)) { 63 | return record; 64 | } 65 | i++; 66 | } 67 | return null; 68 | } 69 | 70 | function getLinkedRedirectRecords(record, records, limit) { 71 | let lastRecord = record; 72 | const linked = [lastRecord]; 73 | let i = 0; 74 | while (i < limit && records.length > 0) { 75 | const record = records.pop(); 76 | if (record.target && record.target === lastRecord.url) { 77 | linked.unshift(record); 78 | lastRecord = record; 79 | } 80 | i++; 81 | } 82 | return linked; 83 | } 84 | -------------------------------------------------------------------------------- /test/patterns.test.js: -------------------------------------------------------------------------------- 1 | import {createMatchPatterns, ALL_URLS} from "../src/main/api"; 2 | 3 | 4 | test("Create match patterns", () => { 5 | expect(createMatchPatterns({allUrls: true})).toEqual([ALL_URLS]); 6 | expect(createMatchPatterns({ 7 | scheme: "http", 8 | host: "example.com" 9 | })).toEqual(["http://example.com/"]); 10 | expect(createMatchPatterns({ 11 | scheme: "http", 12 | host: "example.com", 13 | path: "some/path" 14 | })).toEqual(["http://example.com/some/path"]); 15 | expect(createMatchPatterns({ 16 | scheme: "https", 17 | host: ["first.com", "second.com"], 18 | path: "some/path" 19 | }).sort()).toEqual(["https://first.com/some/path", "https://second.com/some/path"].sort()); 20 | expect(createMatchPatterns({ 21 | scheme: "*", 22 | host: ["first.com", "second.com"], 23 | path: ["first/path", "second/path"] 24 | }).sort()).toEqual( 25 | ["*://first.com/first/path", "*://first.com/second/path", "*://second.com/first/path", 26 | "*://second.com/second/path"].sort() 27 | ); 28 | expect(createMatchPatterns({ 29 | scheme: "*", 30 | host: ["first.com", "second.com"], 31 | path: ["first/path", "second/path"] 32 | }).sort()).toEqual( 33 | ["*://first.com/first/path", "*://first.com/second/path", "*://second.com/first/path", 34 | "*://second.com/second/path"].sort() 35 | ); 36 | expect(createMatchPatterns({ 37 | scheme: "https", 38 | host: ["first.com", "second.*"], 39 | path: "some/path", 40 | topLevelDomains: ["com", "org"] 41 | }).sort()).toEqual(["https://first.com/some/path", "https://second.com/some/path", 42 | "https://second.org/some/path"].sort()); 43 | expect(createMatchPatterns({ 44 | scheme: "*", 45 | host: ["first.com", "second.*"], 46 | path: ["first/path", "second/path"], 47 | topLevelDomains: ["com", "org"] 48 | }).sort()).toEqual( 49 | ["*://first.com/first/path", "*://first.com/second/path", "*://second.com/first/path", 50 | "*://second.com/second/path", "*://second.org/first/path", 51 | "*://second.org/second/path"].sort() 52 | ); 53 | expect(createMatchPatterns({ 54 | scheme: "*", 55 | host: ["first.com", "second.*"], 56 | path: ["/first/path", "second/path"], 57 | topLevelDomains: ["com", "org"] 58 | }).sort()).toEqual( 59 | ["*://first.com/first/path", "*://first.com/second/path", "*://second.com/first/path", 60 | "*://second.com/second/path", "*://second.org/first/path", 61 | "*://second.org/second/path"].sort() 62 | ); 63 | }); 64 | -------------------------------------------------------------------------------- /src/popup/browser-action.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 |
18 | 19 | 20 |
21 |
22 | 43 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /test/include-exclude.test.js: -------------------------------------------------------------------------------- 1 | import * as RequestControl from "../src/main/api"; 2 | 3 | let request; 4 | 5 | beforeEach(() => { 6 | request = { url: "http://foo.com/click?p=240631&a=2314955&g=21407340&url=http%3A%2F%2Fbar.com%2F" }; 7 | }); 8 | 9 | test("Include match extender - match", () => { 10 | const [filter] = RequestControl.createRequestFilters({ 11 | action: "filter", 12 | pattern: { 13 | includes: ["cl?ck", "/a=[0-9]+/", "FOO"], 14 | }, 15 | }); 16 | expect(filter.matcher.test(request)).toBeTruthy(); 17 | }); 18 | 19 | test("Include match extender - no match", () => { 20 | const [filter] = RequestControl.createRequestFilters({ 21 | action: "filter", 22 | pattern: { 23 | includes: ["clock", "/a=[a-z]+/"], 24 | }, 25 | }); 26 | expect(filter.matcher.test(request)).toBeFalsy(); 27 | }); 28 | 29 | test("Exclude match extender - match", () => { 30 | const [filter] = RequestControl.createRequestFilters({ 31 | action: "filter", 32 | pattern: { 33 | excludes: ["cl?ck", "/a=\\d+/"], 34 | }, 35 | }); 36 | expect(filter.matcher.test(request)).toBeFalsy(); 37 | }); 38 | 39 | test("Exclude match extender - no match", () => { 40 | const [filter] = RequestControl.createRequestFilters({ 41 | action: "filter", 42 | pattern: { 43 | excludes: ["clock", "/a=[a-z]+/"], 44 | }, 45 | }); 46 | expect(filter.matcher.test(request)).toBeTruthy(); 47 | }); 48 | 49 | test("Combined include, exclude - match include", () => { 50 | const [filter] = RequestControl.createRequestFilters({ 51 | action: "filter", 52 | pattern: { 53 | includes: ["click"], 54 | excludes: ["clock"], 55 | }, 56 | }); 57 | expect(filter.matcher.test(request)).toBeTruthy(); 58 | }); 59 | 60 | test("Combined include, exclude - no match", () => { 61 | const [filter] = RequestControl.createRequestFilters({ 62 | action: "filter", 63 | pattern: { 64 | includes: ["clock"], 65 | excludes: ["clock"], 66 | }, 67 | }); 68 | expect(filter.matcher.test(request)).toBeFalsy(); 69 | }); 70 | 71 | test("Combined include, exclude - match both", () => { 72 | const [filter] = RequestControl.createRequestFilters({ 73 | action: "filter", 74 | pattern: { 75 | includes: ["click"], 76 | excludes: ["click"], 77 | }, 78 | }); 79 | expect(filter.matcher.test(request)).toBeFalsy(); 80 | }); 81 | 82 | test("Invalid regexp - treated as literal string", () => { 83 | const [filter] = RequestControl.createRequestFilters({ 84 | action: "filter", 85 | pattern: { 86 | excludes: ["/click\\/"], 87 | }, 88 | }); 89 | expect(filter.matcher.test({ url: "http://click\\/" })).toBeFalsy(); 90 | }); 91 | -------------------------------------------------------------------------------- /src/util/toc.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | /** 6 | * 7 | * Table of contents generator 8 | * @param documentNode 9 | * @constructor 10 | */ 11 | 12 | export function Toc(documentNode) { 13 | this.tree = new TocBlock("H2"); 14 | const headers = documentNode.querySelectorAll("h2, h3, h4, h5, h6"); 15 | let lastBlock = this.tree; 16 | 17 | for (const header of headers) { 18 | const tocNode = new TocItem(header); 19 | while (tocNode.level < lastBlock.level && lastBlock !== this.tree) { 20 | lastBlock = lastBlock.parent; 21 | } 22 | if (tocNode.level > lastBlock.level) { 23 | const tocBlock = new TocBlock(tocNode.level, lastBlock); 24 | lastBlock.appendChild(tocBlock); 25 | lastBlock = tocBlock; 26 | } 27 | lastBlock.appendChild(tocNode); 28 | } 29 | } 30 | 31 | Toc.prototype.render = function () { 32 | const toc = this.tree.render(); 33 | toc.addEventListener("click", collapseTocBlock); 34 | toc.classList.remove("collapse"); 35 | return toc; 36 | }; 37 | 38 | function TocNode(level) { 39 | this.level = level; 40 | } 41 | 42 | function TocBlock(level, parent) { 43 | TocNode.call(this, level); 44 | this.parent = parent; 45 | this.children = []; 46 | } 47 | 48 | function TocItem(header) { 49 | TocNode.call(this, header.tagName); 50 | this.text = header.textContent; 51 | this.href = `#${header.id}`; 52 | } 53 | TocBlock.prototype = Object.create(TocNode.prototype); 54 | TocBlock.prototype.constructor = TocBlock; 55 | TocItem.prototype = Object.create(TocNode.prototype); 56 | TocItem.prototype.constructor = TocItem; 57 | 58 | TocBlock.prototype.appendChild = function (child) { 59 | this.children.push(child); 60 | }; 61 | TocBlock.prototype.render = function () { 62 | const block = document.createElement("ul"); 63 | for (const item of this.children) { 64 | block.appendChild(item.render()); 65 | } 66 | block.classList.add("collapse"); 67 | return block; 68 | }; 69 | TocItem.prototype.render = function () { 70 | const item = document.createElement("li"); 71 | const link = document.createElement("a"); 72 | link.textContent = this.text; 73 | link.href = this.href; 74 | item.appendChild(link); 75 | return item; 76 | }; 77 | 78 | function collapseTocBlock(e) { 79 | if (e.target.tagName === "A") { 80 | const li = e.target.parentNode; 81 | const others = li.parentNode.querySelectorAll("li + ul:not(.collapse)"); 82 | 83 | for (const other of others) { 84 | other.classList.add("collapse"); 85 | } 86 | 87 | if (li.nextElementSibling && li.nextElementSibling.tagName === "UL") { 88 | li.nextElementSibling.classList.toggle("collapse"); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/control.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import { ControlRule } from "./rules/base.js"; 6 | import { BlockRule } from "./rules/block.js"; 7 | import { FilterRule } from "./rules/filter.js"; 8 | import { BaseRedirectRule, RedirectRule } from "./rules/redirect.js"; 9 | import { SecureRule } from "./rules/secure.js"; 10 | import { LoggedWhitelistRule, WhitelistRule } from "./rules/whitelist.js"; 11 | 12 | LoggedWhitelistRule.priority = 0; 13 | WhitelistRule.priority = -1; 14 | BlockRule.priority = -2; 15 | SecureRule.priority = -3; 16 | RedirectRule.priority = -4; 17 | FilterRule.priority = -4; 18 | 19 | Object.defineProperty(ControlRule.prototype, "priority", { 20 | get() { 21 | return this.constructor.priority; 22 | }, 23 | }); 24 | 25 | export class CompositeRule { 26 | constructor(ruleA, ruleB) { 27 | if (ruleA instanceof RedirectRule) { 28 | this.rules = [ruleA, ruleB]; 29 | } else { 30 | this.rules = [ruleB, ruleA]; 31 | } 32 | this.priority = ruleA.priority; 33 | } 34 | 35 | add(rule) { 36 | if (rule instanceof RedirectRule) { 37 | this.rules.unshift(rule); 38 | } else { 39 | this.rules.push(rule); 40 | } 41 | } 42 | 43 | resolve(request) { 44 | for (const rule of this.rules) { 45 | const resolve = rule.resolve(request); 46 | if (resolve !== null) { 47 | return resolve; 48 | } 49 | } 50 | return null; 51 | } 52 | } 53 | 54 | export class RequestController { 55 | constructor(notify, updateTab) { 56 | this.requests = new Map(); 57 | ControlRule.notify = notify; 58 | BaseRedirectRule.updateTab = updateTab; 59 | } 60 | 61 | mark(request, rule) { 62 | const current = this.requests.get(request.requestId); 63 | 64 | if (typeof current === "undefined" || rule.priority > current.priority) { 65 | this.requests.set(request.requestId, rule); 66 | return true; 67 | } 68 | 69 | if (rule.priority === current.priority && rule instanceof BaseRedirectRule) { 70 | this.compose(current, rule, request); 71 | return true; 72 | } 73 | 74 | return false; 75 | } 76 | 77 | compose(current, rule, request) { 78 | if (current instanceof CompositeRule) { 79 | current.add(rule); 80 | return; 81 | } 82 | this.requests.set(request.requestId, new CompositeRule(current, rule)); 83 | } 84 | 85 | resolve(request) { 86 | if (!this.requests.has(request.requestId)) { 87 | return null; 88 | } 89 | const rule = this.requests.get(request.requestId); 90 | this.requests.delete(request.requestId); 91 | return rule.resolve(request); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/options/common.css: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | .d-none, 6 | [hidden] { 7 | display: none !important; 8 | } 9 | 10 | .v-hidden { 11 | visibility: hidden; 12 | } 13 | 14 | .c { 15 | padding-top: 0; 16 | padding-bottom: 0; 17 | max-width: 70em; 18 | } 19 | 20 | hr { 21 | border-color: var(--light); 22 | border-width: 1px; 23 | border-bottom: none; 24 | } 25 | 26 | code { 27 | color: brown; 28 | } 29 | 30 | .btn { 31 | display: inline-flex; 32 | align-items: center; 33 | border: 1px solid transparent; 34 | cursor: pointer; 35 | padding: 0.4em 0.5em; 36 | text-transform: initial; 37 | letter-spacing: initial; 38 | font-size: 0.9em; 39 | border-radius: 2px; 40 | vertical-align: middle; 41 | background: #e6e6e6; 42 | color: rgb(12, 12, 13); 43 | } 44 | 45 | .btn.text { 46 | background: none; 47 | display: inline; 48 | border: none; 49 | padding: 0; 50 | font-size: inherit; 51 | color: var(--primary-color); 52 | font: inherit; 53 | vertical-align: inherit; 54 | } 55 | 56 | .btn:disabled, 57 | a.disabled { 58 | opacity: 0.6; 59 | cursor: default; 60 | pointer-events: none; 61 | } 62 | 63 | .btn-danger { 64 | background: #ff3d33; 65 | border-color: #ff3d33; 66 | color: white; 67 | } 68 | 69 | .btn.primary { 70 | background: var(--primary-color); 71 | border: none; 72 | font-weight: bold; 73 | } 74 | 75 | .btn.active { 76 | border-color: #3d7e9a; 77 | } 78 | 79 | .btn > img { 80 | margin-right: 0.5em; 81 | } 82 | 83 | .btn > .badge { 84 | margin-left: 0.5em; 85 | } 86 | 87 | .btn > span { 88 | margin: 0; 89 | } 90 | 91 | input:not(:checked):not(:hover) + .icon { 92 | filter: grayscale(100%); 93 | } 94 | 95 | input:disabled + span { 96 | opacity: 0.4; 97 | } 98 | 99 | pre { 100 | padding: 0.6em; 101 | border-width: 1px; 102 | } 103 | 104 | .collapse-button:hover { 105 | cursor: pointer; 106 | color: #343a40; 107 | -moz-user-select: none; 108 | } 109 | 110 | .collapse-button:before { 111 | content: ""; 112 | display: inline-block; 113 | border-left: 0.4em solid transparent; 114 | border-right: 0.4em solid transparent; 115 | border-top: 0.4em solid #343a40; 116 | border-bottom: none; 117 | vertical-align: super; 118 | margin-right: 0.3em; 119 | } 120 | 121 | .collapse-button.collapsed:before { 122 | border-left: 0.4em solid transparent; 123 | border-right: 0.4em solid transparent; 124 | border-top: none; 125 | border-bottom: 0.4em solid #343a40; 126 | } 127 | 128 | .collapse-button.plus:before { 129 | content: "+"; 130 | border: none; 131 | vertical-align: baseline; 132 | font-weight: bold; 133 | } 134 | 135 | .collapse-button.plus.collapsed:before { 136 | content: "-"; 137 | } 138 | 139 | .middle { 140 | vertical-align: middle; 141 | } 142 | -------------------------------------------------------------------------------- /test/encodeDecode.test.js: -------------------------------------------------------------------------------- 1 | import { RedirectRule } from "../src/main/rules/redirect"; 2 | 3 | test("Decode URI Component", () => { 4 | const request = "http://go.redirectingat.com/?xs=1&id=xxxxxxx&sref=http%3A%2F%2Fwww.vulture.com%2F2018%2F05%2Fthe-end-of-nature-at-storm-king-art-center-in-new-york.html&xcust=xxxxxxxx&url=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FNathaniel_Parker_Willis"; 5 | const target = "https://en.wikipedia.org/wiki/Nathaniel_Parker_Willis"; 6 | const redirectRule = new RedirectRule({ redirectUrl: "{href/.*url=(.*)/$1|decodeURIComponent}" }); 7 | expect(redirectRule.apply(request)).toBe(target); 8 | }); 9 | 10 | test("Encode URI Component", () => { 11 | const request = "https://en.wikipedia.org/wiki/Nathaniel_Parker_Willis"; 12 | const target = "http://go.redirectingat.com/?xs=1&id=xxxxxxx&sref=http%3A%2F%2Fwww.vulture.com%2F2018%2F05%2Fthe-end-of-nature-at-storm-king-art-center-in-new-york.html&xcust=xxxxxxxx&url=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FNathaniel_Parker_Willis"; 13 | const redirectRule = new RedirectRule({ redirectUrl: "http://go.redirectingat.com/?xs=1&id=xxxxxxx&sref=http%3A%2F%2Fwww.vulture.com%2F2018%2F05%2Fthe-end-of-nature-at-storm-king-art-center-in-new-york.html&xcust=xxxxxxxx&url={href|encodeURIComponent}" }); 14 | expect(redirectRule.apply(request)).toBe(target); 15 | }); 16 | 17 | test("Decode URI", () => { 18 | const request = "https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B"; 19 | const target = "https://mozilla.org/?x=шеллы"; 20 | const redirectRule = new RedirectRule({ redirectUrl: "{href|decodeURI}" }); 21 | expect(redirectRule.apply(request)).toBe(target); 22 | }); 23 | 24 | test("Encode URI", () => { 25 | // URL encodes given URI 26 | const request = "https://mozilla.org/?x=шеллы"; 27 | const target = "https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B"; 28 | const redirectRule = new RedirectRule({ redirectUrl: "{href|decodeUri|encodeUri}" }); 29 | expect(redirectRule.apply(request)).toBe(target); 30 | }); 31 | 32 | test("Encode Base64", () => { 33 | // URL encodes given URI 34 | const request = "http://www.imdb.com/title/tt0137523/"; 35 | const target = "http://base64.derefer.me/?aHR0cDovL3d3dy5pbWRiLmNvbS90aXRsZS90dDAxMzc1MjMv"; 36 | const redirectRule = new RedirectRule({ redirectUrl: "http://base64.derefer.me/?{href|encodeBase64}" }); 37 | expect(redirectRule.apply(request)).toBe(target); 38 | }); 39 | 40 | test("Decode Base64", () => { 41 | // URL encodes given URI 42 | const request = "http://base64.derefer.me/?aHR0cDovL3d3dy5pbWRiLmNvbS90aXRsZS90dDAxMzc1MjMv"; 43 | const target = "http://www.imdb.com/title/tt0137523/"; 44 | const redirectRule = new RedirectRule({ redirectUrl: "{search:1|decodeBase64}" }); 45 | expect(redirectRule.apply(request)).toBe(target); 46 | }); 47 | 48 | test("Decode - Encode Base64", () => { 49 | // URL encodes given URI 50 | const request = "http://www.imdb.com/title/tt0137523/"; 51 | const target = "http://www.imdb.com/title/tt0137523/a"; 52 | const redirectRule = new RedirectRule({ redirectUrl: "{href|encodeBase64|decodeBase64}a" }); 53 | expect(redirectRule.apply(request)).toBe(target); 54 | }); 55 | -------------------------------------------------------------------------------- /src/options/changelog-dialog.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import ModalDialog from "./modal-dialog.js"; 6 | 7 | class ChangelogDialog extends ModalDialog { 8 | async connectedCallback() { 9 | super.connectedCallback(); 10 | this.shadowRoot.getElementById("title").textContent = browser.i18n.getMessage("changelog"); 11 | const content = await fetchChangelog(); 12 | this.shadowRoot.getElementById("content").append(...content); 13 | } 14 | } 15 | 16 | customElements.define("changelog-dialog", ChangelogDialog); 17 | 18 | export function showChangelog() { 19 | const dialog = document.createElement("changelog-dialog"); 20 | document.body.append(dialog); 21 | } 22 | 23 | async function fetchChangelog() { 24 | const response = await fetch("/CHANGELOG.md"); 25 | const content = await response.text(); 26 | const elements = []; 27 | 28 | let ul = document.createElement("ul"); 29 | let start = false; 30 | 31 | for (const line of content.split("\n")) { 32 | if (!start) { 33 | if (line.startsWith("##")) { 34 | start = true; 35 | } 36 | continue; 37 | } 38 | if (line.startsWith("-")) { 39 | const li = document.createElement("li"); 40 | const text = line.split(/(#\d+|@\w+)/); 41 | li.textContent = text[0].replace(/^- /, ""); 42 | for (let i = 1; i < text.length; i++) { 43 | if (text[i].startsWith("#")) { 44 | const link = document.createElement("a"); 45 | link.textContent = text[i]; 46 | link.href = `https://github.com/tumpio/requestcontrol/issues/${text[i].substring(1)}`; 47 | link.target = "_blank"; 48 | li.append(link); 49 | } else if (text[i].startsWith("@")) { 50 | const link = document.createElement("a"); 51 | link.textContent = text[i]; 52 | link.href = `https://github.com/${text[i].substring(1)}`; 53 | link.target = "_blank"; 54 | li.append(link); 55 | } else { 56 | li.append(text[i]); 57 | } 58 | } 59 | if (/fix/i.test(line)) { 60 | li.classList.add("fix"); 61 | } else if (/add/i.test(line)) { 62 | li.classList.add("add"); 63 | } else if (/change/i.test(line)) { 64 | li.classList.add("change"); 65 | } else if (/update/i.test(line)) { 66 | li.classList.add("update"); 67 | } else if (/locale/i.test(line)) { 68 | li.classList.add("locale"); 69 | } 70 | ul.appendChild(li); 71 | } else { 72 | const h = document.createElement("h6"); 73 | h.textContent = line.substring(2); 74 | elements.push(h); 75 | ul = document.createElement("ul"); 76 | elements.push(ul); 77 | } 78 | } 79 | return elements; 80 | } 81 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | |BuildStatus| |codecov| 2 | 3 | Request Control - Firefox extension 4 | ----------------------------------- 5 | 6 | An extension to control HTTP requests. Provides front-end for Firefox 7 | `webRequest.onBeforeRequest`_ API for HTTP request management. 8 | 9 | Requests can be controlled with following rules: 10 | 11 | - **Filter Rule** 12 | 13 | Skip URL redirection and remove URL query parameters. 14 | 15 | - **Redirect Rule** 16 | 17 | Rewrite requests with support for `Pattern Capturing`_ to redirect based on the original request. 18 | 19 | - **Secure Rule** 20 | 21 | Upgrade non-secure (HTTP) requests to secure (HTTPS). 22 | 23 | - **Block Rule** 24 | 25 | Block requests before they are made. 26 | 27 | - **Whitelist Rule** 28 | 29 | Whitelist requests from other rules. 30 | 31 | | `Manual`_ 32 | | `FAQ`_ 33 | | `Source code`_ 34 | | `License`_ 35 | 36 | Support 37 | ~~~~~~~ 38 | 39 | - Report bugs 40 | - Suggest new features 41 | - Help to translate 42 | - Contribute 43 | 44 | Development 45 | ~~~~~~~~~~~ 46 | 47 | Clone repository and setup development environment with `npm`_ 48 | 49 | :: 50 | 51 | git clone https://github.com/tumpio/requestcontrol.git 52 | cd requestcontrol 53 | npm install 54 | 55 | Run in Firefox-nightly 56 | 57 | :: 58 | 59 | npm start -- --firefox=nightly 60 | 61 | Run unit tests and lint 62 | 63 | :: 64 | 65 | npm test ; npm run lint 66 | 67 | Build extension 68 | 69 | :: 70 | 71 | npm run build 72 | 73 | External Libraries 74 | ~~~~~~~~~~~~~~~~~~ 75 | 76 | Request Control uses the following external libraries: 77 | 78 | - `lit`_ is licensed under the MIT license. 79 | - `tags-input`_ and its fork by `@pirxpilot`_ are licensed under the MIT license. 80 | - `ionicons`_ is licensed under the MIT license. 81 | - `tldts`_ is licensed under the MIT license. 82 | 83 | License 84 | ~~~~~~~ 85 | 86 | :: 87 | 88 | This Source Code Form is subject to the terms of the Mozilla Public 89 | License, v. 2.0. If a copy of the MPL was not distributed with this 90 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 91 | 92 | .. _webRequest.onBeforeRequest: https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/webRequest/onBeforeRequest 93 | .. _Pattern Capturing: https://github.com/tumpio/requestcontrol/blob/master/_locales/en/manual.wiki#redirect-using-pattern-capturing 94 | .. _Manual: https://github.com/tumpio/requestcontrol/blob/master/_locales/en/manual.wiki 95 | .. _FAQ: https://github.com/tumpio/requestcontrol/issues?utf8=%E2%9C%93&q=label%3Aquestion+ 96 | .. _Source code: https://github.com/tumpio/requestcontrol 97 | .. _License: https://github.com/tumpio/requestcontrol/blob/master/LICENSE 98 | .. _npm: https://www.npmjs.com/ 99 | .. _lit: https://ajusa.github.io/lit/ 100 | .. _tags-input: https://github.com/developit/tags-input 101 | .. _@pirxpilot: https://github.com/pirxpilot/tags-input 102 | .. _ionicons: http://ionicons.com/ 103 | .. _tldts: https://github.com/remusao/tldts 104 | 105 | .. |BuildStatus| image:: https://github.com/tumpio/requestcontrol/workflows/Build/badge.svg?event=push&branch=master 106 | :target: https://github.com/tumpio/requestcontrol/actions 107 | .. |codecov| image:: https://codecov.io/gh/tumpio/requestcontrol/branch/master/graph/badge.svg 108 | :target: https://codecov.io/gh/tumpio/requestcontrol 109 | -------------------------------------------------------------------------------- /src/util/notifier.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import { OPTION_CHANGE_ICON, OPTION_SHOW_COUNTER } from "../options/constants.js"; 6 | 7 | const DISABLED_ICON = "/icons/icon-disabled.svg"; 8 | const DEFAULT_ICON = "/icons/icon.svg"; 9 | 10 | class Notifier { 11 | static notify(tabId, icon, text) { 12 | setIconWithBadgeText(tabId, icon, text); 13 | } 14 | } 15 | 16 | class NotifierNoBadgeText { 17 | static notify(tabId, icon) { 18 | setIcon(tabId, icon); 19 | } 20 | } 21 | 22 | class NotifierNoIcon { 23 | static notify(tabId, _icon, text) { 24 | setBadgeText(tabId, text); 25 | } 26 | } 27 | 28 | class EmptyNotifier { 29 | static notify() {} 30 | } 31 | 32 | let notifier = Notifier; 33 | 34 | updateNotifier(); 35 | 36 | browser.storage.onChanged.addListener((changes) => { 37 | if (OPTION_SHOW_COUNTER in changes) { 38 | if (!changes[OPTION_SHOW_COUNTER].newValue) { 39 | clearBadgeText(); 40 | } 41 | updateNotifier(); 42 | } 43 | if (OPTION_CHANGE_ICON in changes) { 44 | if (!changes[OPTION_CHANGE_ICON].newValue) { 45 | clearIcon(); 46 | } 47 | updateNotifier(); 48 | } 49 | }); 50 | 51 | export function notify(tabId, icon, count) { 52 | notifier.notify(tabId, icon, count.toString()); 53 | } 54 | 55 | export function error() { 56 | setIconWithBadgeText(null, DEFAULT_ICON, "!", { badge: "red", badgeText: "white" }); 57 | } 58 | 59 | export function clear(tabId) { 60 | setIconWithBadgeText(tabId); 61 | } 62 | 63 | export function disabledState() { 64 | setIconWithBadgeText(null, DISABLED_ICON); 65 | clearIcon(); 66 | clearBadgeText(); 67 | } 68 | 69 | export function enabledState() { 70 | setIconWithBadgeText(); 71 | } 72 | 73 | function setIconWithBadgeText(tabId, icon, text, colors) { 74 | setIcon(tabId, icon); 75 | setBadgeText(tabId, text, colors); 76 | } 77 | 78 | function setIcon(tabId = null, icon = DEFAULT_ICON) { 79 | browser.browserAction.setIcon({ 80 | tabId, 81 | path: icon, 82 | }); 83 | } 84 | 85 | function setBadgeText(tabId = null, text = null, colors = { badge: "#eef", badgeText: "#0c0c0d" }) { 86 | browser.browserAction.setBadgeText({ 87 | tabId, 88 | text, 89 | }); 90 | browser.browserAction.setBadgeBackgroundColor({ color: colors.badge }); 91 | browser.browserAction.setBadgeTextColor({ color: colors.badgeText }); 92 | } 93 | 94 | async function updateNotifier() { 95 | const options = await browser.storage.local.get({ 96 | [OPTION_SHOW_COUNTER]: true, 97 | [OPTION_CHANGE_ICON]: true, 98 | }); 99 | if (Object.values(options).every((option) => !option)) { 100 | notifier = EmptyNotifier; 101 | } else if (!options[OPTION_SHOW_COUNTER]) { 102 | notifier = NotifierNoBadgeText; 103 | } else if (!options[OPTION_CHANGE_ICON]) { 104 | notifier = NotifierNoIcon; 105 | } else { 106 | notifier = Notifier; 107 | } 108 | } 109 | 110 | async function clearIcon() { 111 | const tabs = await browser.tabs.query({}); 112 | tabs.forEach((tab) => setIcon(tab.id, null)); 113 | } 114 | 115 | async function clearBadgeText() { 116 | const tabs = await browser.tabs.query({}); 117 | tabs.forEach((tab) => setBadgeText(tab.id)); 118 | } 119 | -------------------------------------------------------------------------------- /src/popup/browser-action.css: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | html, body { 6 | color: #000000; 7 | } 8 | 9 | body { 10 | margin: 0; 11 | font-family: "Open Sans", sans-serif; 12 | font-size: .8em; 13 | } 14 | 15 | .content { 16 | display: flex; 17 | flex-direction: column; 18 | } 19 | 20 | .header { 21 | display: flex; 22 | border-top: 1px solid #cdcdcd; 23 | flex-shrink: 0; 24 | } 25 | 26 | #records { 27 | padding: 0 .3em; 28 | flex-grow: 1; 29 | max-height: 30em; 30 | overflow-y: auto; 31 | } 32 | 33 | .header > .btn { 34 | padding: .8em; 35 | display: inline-flex; 36 | } 37 | 38 | .header > .btn:first-child { 39 | flex-grow: 1; 40 | } 41 | 42 | .entry-header { 43 | display: flex; 44 | align-items: center; 45 | max-width: 100%; 46 | overflow-x: hidden; 47 | padding: .2em 0; 48 | } 49 | 50 | .entry-header:hover { 51 | cursor: pointer; 52 | background: #f7f7f7; 53 | } 54 | 55 | .entry-header > * { 56 | display: inline-block; 57 | flex: none; 58 | margin-right: .3em; 59 | } 60 | 61 | 62 | .entry-header > .url { 63 | flex-grow: 1; 64 | flex-shrink: 1; 65 | overflow: hidden; 66 | text-overflow: ellipsis; 67 | white-space: nowrap; 68 | } 69 | 70 | pre { 71 | white-space: pre-wrap; 72 | word-break: break-all; 73 | } 74 | 75 | #showRules { 76 | background: #f5f5f5; 77 | border-right: 1px solid #cccccc; 78 | } 79 | 80 | #showRules:hover { 81 | background: #e1e1e1; 82 | cursor: pointer; 83 | } 84 | 85 | #toggleActive { 86 | color: white; 87 | max-width: 6em; 88 | background: #cc0d13; 89 | } 90 | 91 | #toggleActive:hover { 92 | opacity: 0.6; 93 | cursor: pointer; 94 | } 95 | 96 | #toggleActive.disabled { 97 | background: #3c763d; 98 | } 99 | 100 | .entry { 101 | border-top: 1px dashed #c6c8ca; 102 | } 103 | 104 | .entry:first-child { 105 | border-top: none; 106 | } 107 | 108 | .url { 109 | color: #80888f; 110 | } 111 | 112 | .action { 113 | font-weight: bold; 114 | } 115 | 116 | .models, 117 | .hidden { 118 | display: none; 119 | } 120 | 121 | #details label, 122 | .timestamp { 123 | color: #777; 124 | } 125 | 126 | .copyButton { 127 | height: 22px; 128 | width: 22px; 129 | cursor: pointer; 130 | margin: 1em; 131 | border: none; 132 | float: right; 133 | } 134 | 135 | .copyButton img { 136 | margin-left: -8px; 137 | } 138 | 139 | .copyButton:hover { 140 | background-color: #ebebeb; 141 | } 142 | 143 | /* Copied tooltip */ 144 | .tooltip { 145 | position: relative; 146 | display: inline-block; 147 | } 148 | 149 | .tooltip .tooltiptext { 150 | visibility: hidden; 151 | width: auto; 152 | background-color: #333; 153 | color: #fff; 154 | text-align: center; 155 | padding: 0.3em; 156 | border-radius: 2px; 157 | 158 | position: absolute; 159 | z-index: 1; 160 | top: -5px; 161 | right: 120%; 162 | opacity: 0; 163 | transition: opacity 0.4s; 164 | } 165 | 166 | .copied .tooltiptext { 167 | visibility: visible; 168 | opacity: 1; 169 | } 170 | 171 | .tooltip .tooltiptext::after { 172 | content: " "; 173 | position: absolute; 174 | top: 50%; 175 | left: 100%; 176 | margin-top: -5px; 177 | border: 5px solid; 178 | border-color: transparent transparent transparent #333; 179 | } 180 | -------------------------------------------------------------------------------- /src/util/regexp.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | /** 6 | * Construct regexp from list of globs (pattern with wildcards) and regexp pattern strings 7 | * @param values list of globs or regexp pattern strings 8 | * @param insensitive if true, set case insensitive flag 9 | * @param containing if false, add string begin and end . 10 | * @returns {RegExp} 11 | */ 12 | export function createRegexpPattern(values, insensitive = true, containing = false) { 13 | const regexpChars = /[$()+.[\\\]^{|}]/g; // excluding "*" and "?" wildcard chars 14 | const regexpParam = /^\/(.*)\/$/; 15 | const flags = insensitive ? "i" : ""; 16 | 17 | let pattern = ""; 18 | for (const param of values) { 19 | const testRegexp = param.match(regexpParam); 20 | pattern += "|"; 21 | pattern += containing ? "" : "^"; 22 | if (testRegexp && isValidRegExp(testRegexp[1])) { 23 | pattern += testRegexp[1]; 24 | } else { 25 | pattern += param.replace(regexpChars, "\\$&").replace(/\*/g, ".*").replace(/\?/g, "."); 26 | } 27 | pattern += containing ? "" : "$"; 28 | } 29 | return new RegExp(pattern.substring(1), flags); 30 | } 31 | 32 | /** 33 | * Transforms a valid match pattern into a regular expression 34 | * which matches all URLs included by that pattern. 35 | * Source: MDN 36 | * @param {string} pattern The pattern to transform. 37 | * @return {RegExp} The pattern's equivalent as a RegExp. 38 | * @throws {TypeError} If the pattern is not a valid MatchPattern 39 | */ 40 | export function matchPatternToRegExp(pattern) { 41 | if (pattern === "" || pattern === "") { 42 | return /^(?:http|https):\/\//; 43 | } 44 | 45 | const schemeSegment = "(\\*|http|https)"; 46 | const hostSegment = "(\\*|(?:\\*\\.)?(?:[^/*]+))?"; 47 | const pathSegment = "(.*)"; 48 | const matchPatternRegExp = new RegExp(`^${schemeSegment}://${hostSegment}/${pathSegment}$`); 49 | const regexpChars = /[$()+.?[\\\]^{|}]/g; // excluding "*" 50 | 51 | const match = matchPatternRegExp.exec(pattern); 52 | if (!match) { 53 | throw new TypeError(`"${pattern}" is not a valid MatchPattern`); 54 | } 55 | 56 | const [, scheme, , path] = match; 57 | let host = match[2]; 58 | if (!host) { 59 | throw new TypeError(`"${pattern}" does not have a valid host`); 60 | } 61 | 62 | let regex = "^"; 63 | 64 | if (scheme === "*") { 65 | regex += "(http|https)"; 66 | } else { 67 | regex += scheme; 68 | } 69 | 70 | regex += "://"; 71 | 72 | if (host && host === "*") { 73 | regex += "[^/]+?"; 74 | } else if (host) { 75 | if (/^\*\./.test(host)) { 76 | regex += "[^/]*?"; 77 | host = host.substring(2); 78 | } 79 | regex += host.replace(regexpChars, "\\$&"); 80 | } 81 | 82 | if (path) { 83 | if (path === "*") { 84 | regex += "(/.*)?"; 85 | } else if (path.charAt(0) !== "/") { 86 | regex += "/"; 87 | regex += path.replace(regexpChars, "\\$&").replace(/\*/g, ".*?"); 88 | regex += "/?"; 89 | } 90 | } 91 | 92 | regex += "$"; 93 | return new RegExp(regex); 94 | } 95 | 96 | function isValidRegExp(pattern) { 97 | try { 98 | new RegExp(pattern); 99 | } catch { 100 | return false; 101 | } 102 | return true; 103 | } 104 | -------------------------------------------------------------------------------- /rules/privacy-amazon.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "title": "Remove Amazon query parameters", 4 | "description": "Removes all non-whitelisted query parameters on both Amazon store and Prime Video.", 5 | "tag": "privacy-amazon", 6 | "uuid": "6dc06a86-2356-4521-a0c5-08372846df15", 7 | "pattern": { 8 | "scheme": "*", 9 | "host": [ 10 | "*.amazon.*", 11 | "*.primevideo.*" 12 | ], 13 | "path": [ 14 | "*" 15 | ], 16 | "excludes": [ 17 | "://www.amazon.*/ap/*", 18 | "://www.amazon.*/gp/*" 19 | ], 20 | "topLevelDomains": [ 21 | "ae", 22 | "ca", 23 | "cn", 24 | "co.jp", 25 | "co.uk", 26 | "com", 27 | "com.au", 28 | "com.br", 29 | "com.mx", 30 | "com.tr", 31 | "de", 32 | "es", 33 | "fr", 34 | "in", 35 | "it", 36 | "nl", 37 | "sa", 38 | "se", 39 | "sg" 40 | ] 41 | }, 42 | "types": [ 43 | "main_frame" 44 | ], 45 | "action": "filter", 46 | "active": true, 47 | "skipRedirectionFilter": true, 48 | "paramsFilter": { 49 | "values": [ 50 | "_encoding", 51 | "arb", 52 | "cartInitiateId", 53 | "claimToken", 54 | "clientContext", 55 | "field-brandtextbin", 56 | "field-keywords", 57 | "fromAnywhere", 58 | "i", 59 | "ie", 60 | "intercept", 61 | "isToBeGiftWrappedBefore", 62 | "k", 63 | "language", 64 | "location", 65 | "mgh", 66 | "node", 67 | "openid.*", 68 | "pageId", 69 | "partialCheckoutCart", 70 | "proceedToCheckout", 71 | "proceedToRetailCheckout", 72 | "rh", 73 | "url" 74 | ], 75 | "invert": true 76 | } 77 | }, 78 | { 79 | "title": "Skip Amazon Picasso redirect", 80 | "description": "Skip Picasso redirect used on occasion by Amazon to track purchases", 81 | "tag": "privacy-amazon", 82 | "uuid": "816c76d6-ad2e-4f7c-94be-e2436c93fe34", 83 | "pattern": { 84 | "scheme": "*", 85 | "host": [ 86 | "*.amazon.*" 87 | ], 88 | "path": [ 89 | "*/picassoRedirect.html/*" 90 | ], 91 | "topLevelDomains": [ 92 | "ae", 93 | "ca", 94 | "cn", 95 | "co.jp", 96 | "co.uk", 97 | "com", 98 | "com.au", 99 | "com.br", 100 | "com.mx", 101 | "com.tr", 102 | "de", 103 | "es", 104 | "fr", 105 | "in", 106 | "it", 107 | "nl", 108 | "sa", 109 | "se", 110 | "sg" 111 | ] 112 | }, 113 | "types": [ 114 | "main_frame", 115 | "sub_frame" 116 | ], 117 | "action": "redirect", 118 | "active": true, 119 | "redirectUrl": "https://{hostname}{search/.*url=(.*)/$1|decodeURIComponent}&alpha" 120 | } 121 | ] 122 | -------------------------------------------------------------------------------- /test/request-filters.test.js: -------------------------------------------------------------------------------- 1 | import { ALL_URLS, createRequestFilters } from "../src/main/api"; 2 | 3 | test("All urls", () => { 4 | const filters = createRequestFilters({ 5 | pattern: { 6 | allUrls: true, 7 | }, 8 | action: "block", 9 | }); 10 | expect(filters.length).toBe(1); 11 | expect(filters[0].urls.length).toBe(1); 12 | expect(filters[0].urls).toContain(ALL_URLS); 13 | }); 14 | 15 | test("Any tld when all urls", () => { 16 | const filters = createRequestFilters({ 17 | pattern: { 18 | scheme: "*", 19 | host: "google.*", 20 | path: "path", 21 | allUrls: true, 22 | anyTLD: true, 23 | }, 24 | action: "block", 25 | }); 26 | expect(filters.length).toBe(1); 27 | expect(filters[0].urls.length).toBe(1); 28 | expect(filters[0].urls).toContain(ALL_URLS); 29 | }); 30 | 31 | test("Any tld - single wildcard host", () => { 32 | const filters = createRequestFilters({ 33 | pattern: { 34 | scheme: "*", 35 | host: "google.*", 36 | path: "path", 37 | anyTLD: true, 38 | }, 39 | action: "block", 40 | }); 41 | expect(filters.length).toBe(1); 42 | expect(filters[0].urls.length).toBe(1); 43 | expect(filters[0].urls).toContain("*://*/path"); 44 | expect(filters[0].matcher.test({ url: "https://google.com" })).toBeTruthy(); 45 | }); 46 | 47 | test("Any tld - multiple wildcard hosts", () => { 48 | const filters = createRequestFilters({ 49 | pattern: { 50 | scheme: "*", 51 | host: ["google.*", "amazon.*"], 52 | path: "path", 53 | anyTLD: true, 54 | }, 55 | action: "block", 56 | }); 57 | expect(filters.length).toBe(1); 58 | expect(filters[0].urls.length).toBe(1); 59 | expect(filters[0].urls).toContain("*://*/path"); 60 | expect(filters[0].matcher.test({ url: "https://google.org" })).toBeTruthy(); 61 | expect(filters[0].matcher.test({ url: "https://amazon.null" })).toBeTruthy(); 62 | }); 63 | 64 | test("Any tld - without wildcardl", () => { 65 | const filters = createRequestFilters({ 66 | pattern: { 67 | scheme: "*", 68 | host: ["google.com", "amazon.*"], 69 | path: "path", 70 | anyTLD: true, 71 | }, 72 | action: "block", 73 | }); 74 | expect(filters.length).toBe(2); 75 | expect(filters[0].urls.length).toBe(1); 76 | expect(filters[1].urls.length).toBe(1); 77 | expect(filters[0].urls).toContain("*://*/path"); 78 | expect(filters[1].urls).toContain("*://google.com/path"); 79 | expect(filters[0].matcher.test({ url: "https://amazon.null" })).toBeTruthy(); 80 | expect(filters[1].matcher.test({ url: "https://google.com" })).toBeTruthy(); 81 | }); 82 | 83 | test("*.* - matches all", () => { 84 | const filters = createRequestFilters({ 85 | pattern: { 86 | scheme: "*", 87 | host: "*.*", 88 | path: "path", 89 | anyTLD: true, 90 | }, 91 | action: "block", 92 | }); 93 | expect(filters.length).toBe(1); 94 | expect(filters[0].urls.length).toBe(1); 95 | expect(filters[0].urls).toContain("*://*/path"); 96 | expect(filters[0].matcher.test({ url: "https://amazon.null" })).toBeTruthy(); 97 | expect(filters[0].matcher.test({ url: "https://google.com" })).toBeTruthy(); 98 | }); 99 | 100 | test("no pattern", () => { 101 | const filters = createRequestFilters({ 102 | action: "block", 103 | }); 104 | expect(filters.length).toBe(0); 105 | }); 106 | 107 | test("no data", () => { 108 | const filters = createRequestFilters(); 109 | expect(filters.length).toBe(0); 110 | }); 111 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "requestcontrol", 3 | "license": "MPL-2.0", 4 | "private": true, 5 | "type": "module", 6 | "repository": { 7 | "type": "git", 8 | "value": "git+https://github.com/tumpio/requestcontrol.git" 9 | }, 10 | "config": { 11 | "ignore": "test/ coverage/ README* **/manual.wiki package*.json" 12 | }, 13 | "scripts": { 14 | "start": "web-ext run -i=$npm_package_config_ignore --bc --url about:debugging#addons", 15 | "test": "node --experimental-vm-modules node_modules/.bin/jest", 16 | "coverage": "codecov", 17 | "lint": "eslint src/ rules/*.json _locales/*/*.json && web-ext lint -i=$npm_package_config_ignore -w", 18 | "lint-build": "addons-linter --warnings-as-errors web-ext-artifacts/request_control-$(node -p \"require('./manifest.json').version\").zip", 19 | "copy-tldts": "cp -v node_modules/tldts-experimental/dist/index.esm.min.js lib/tldts/", 20 | "copy-lit": "cp -v node_modules/@ajusa/lit/src/lit.css lib/lit/", 21 | "build-manual": "find ./_locales/ -iname \"manual.wiki\" -type f -exec sh -c 'pandoc \"$0\" --from=mediawiki --to=html --output \"${0%.wiki}.html\"' {} \\;", 22 | "postinstall": "npm run copy-tldts && npm run copy-lit", 23 | "prebuild": "npm run build-manual", 24 | "build": "web-ext build -i=$npm_package_config_ignore --overwrite-dest --verbose", 25 | "deploy": "web-ext sign --verbose" 26 | }, 27 | "dependencies": { 28 | "@ajusa/lit": "^1.1.0", 29 | "tldts-experimental": "^5.7.38" 30 | }, 31 | "devDependencies": { 32 | "addons-linter": "^3.9.0", 33 | "codecov": "^3.8.2", 34 | "eslint": "^7.30.0", 35 | "eslint-config-problems": "^5.0.0", 36 | "eslint-plugin-import": "^2.23.4", 37 | "eslint-plugin-jest": "^24.3.6", 38 | "eslint-plugin-json": "^3.0.0", 39 | "eslint-plugin-no-unsanitized": "^3.1.5", 40 | "eslint-plugin-node": "^11.1.0", 41 | "eslint-plugin-promise": "^5.1.0", 42 | "eslint-plugin-simple-import-sort": "^7.0.0", 43 | "eslint-plugin-unicorn": "^34.0.1", 44 | "jest": "^27.0.6", 45 | "web-ext": "^6.2.0" 46 | }, 47 | "jest": { 48 | "testEnvironment": "jsdom", 49 | "transform": {}, 50 | "coverageDirectory": "./coverage/", 51 | "collectCoverage": true, 52 | "collectCoverageFrom": [ 53 | "src/main/**/*" 54 | ], 55 | "coverageReporters": [ 56 | "text-summary", 57 | "lcov" 58 | ] 59 | }, 60 | "eslintConfig": { 61 | "extends": [ 62 | "eslint:recommended", 63 | "problems", 64 | "plugin:unicorn/recommended", 65 | "plugin:json/recommended" 66 | ], 67 | "env": { 68 | "es6": true, 69 | "browser": true, 70 | "webextensions": true, 71 | "jest": true 72 | }, 73 | "parserOptions": { 74 | "sourceType": "module", 75 | "ecmaVersion": 12 76 | }, 77 | "plugins": [ 78 | "jest", 79 | "import", 80 | "promise", 81 | "no-unsanitized", 82 | "simple-import-sort" 83 | ], 84 | "rules": { 85 | "indent": [ 86 | "warn", 87 | 4, 88 | { 89 | "SwitchCase": 1 90 | } 91 | ], 92 | "quotes": [ 93 | "warn", 94 | "double" 95 | ], 96 | "semi": [ 97 | "error", 98 | "always" 99 | ], 100 | "no-prototype-builtins": "off", 101 | "unicorn/prevent-abbreviations": "off", 102 | "unicorn/no-null": "off", 103 | "unicorn/prefer-query-selector": "off", 104 | "unicorn/prefer-string-slice": "off", 105 | "unicorn/prefer-dom-node-append": "off", 106 | "unicorn/catch-error-name": "off", 107 | "unicorn/no-array-callback-reference": "off", 108 | "unicorn/no-object-as-default-parameter": "off", 109 | "unicorn/no-array-for-each": "off", 110 | "unicorn/no-array-reduce": "off", 111 | "unicorn/no-static-only-class": "off", 112 | "unicorn/prefer-ternary": "off", 113 | "unicorn/prefer-spread": "off", 114 | "unicorn/require-array-join-separator": "off", 115 | "simple-import-sort/imports": "warn", 116 | "simple-import-sort/exports": "warn" 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/background.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import { ALL_URLS, createRequestFilters } from "./main/api.js"; 6 | import { RequestController } from "./main/control.js"; 7 | import * as notifier from "./util/notifier.js"; 8 | import * as records from "./util/records.js"; 9 | 10 | const listeners = []; 11 | const controller = new RequestController(notify, updateTab); 12 | const storageKeys = ["rules", "disabled"]; 13 | 14 | browser.storage.local.get(storageKeys).then(init); 15 | browser.storage.onChanged.addListener(onOptionsChanged); 16 | 17 | function init(options) { 18 | if (options.disabled) { 19 | browser.tabs.onRemoved.removeListener(records.removeTabRecords); 20 | browser.runtime.onMessage.removeListener(records.getTabRecords); 21 | browser.webNavigation.onCommitted.removeListener(onNavigation); 22 | notifier.disabledState(); 23 | records.clear(); 24 | controller.requests.clear(); 25 | } else { 26 | browser.tabs.onRemoved.addListener(records.removeTabRecords); 27 | browser.runtime.onMessage.addListener(records.getTabRecords); 28 | browser.webNavigation.onCommitted.addListener(onNavigation); 29 | notifier.enabledState(); 30 | addRequestListeners(options.rules); 31 | } 32 | browser.webRequest.handlerBehaviorChanged(); 33 | } 34 | 35 | function onOptionsChanged(changes) { 36 | if (storageKeys.every((key) => !(key in changes))) { 37 | return; 38 | } 39 | while (listeners.length > 0) { 40 | browser.webRequest.onBeforeRequest.removeListener(listeners.pop()); 41 | } 42 | browser.webRequest.onBeforeRequest.removeListener(controlListener); 43 | browser.storage.local.get(storageKeys).then(init); 44 | } 45 | 46 | function addRequestListeners(rules) { 47 | if (!rules) { 48 | return; 49 | } 50 | rules 51 | .filter((rule) => rule.active) 52 | .forEach((data) => { 53 | try { 54 | const filters = createRequestFilters(data, ruleListener); 55 | for (const { rule, matcher, urls, types, incognito } of filters) { 56 | const listener = ruleListener(rule, matcher); 57 | browser.webRequest.onBeforeRequest.addListener(listener, { urls, types, incognito }); 58 | listeners.push(listener); 59 | } 60 | } catch { 61 | notifier.error(); 62 | } 63 | }); 64 | browser.webRequest.onBeforeRequest.addListener(controlListener, { urls: [ALL_URLS] }, ["blocking"]); 65 | } 66 | 67 | function ruleListener(rule, matcher) { 68 | return (request) => { 69 | if (matcher.test(request)) { 70 | controller.mark(request, rule); 71 | } 72 | }; 73 | } 74 | 75 | function controlListener(request) { 76 | return controller.resolve(request); 77 | } 78 | 79 | function updateTab(tabId, url) { 80 | return browser.tabs.update(tabId, { 81 | url, 82 | }); 83 | } 84 | 85 | function notify(rule, request, target = null) { 86 | const count = records.add(request.tabId, { 87 | action: rule.constructor.action, 88 | type: request.type, 89 | url: request.url, 90 | target, 91 | timestamp: request.timeStamp, 92 | rule, 93 | }); 94 | notifier.notify(request.tabId, rule.constructor.icon, count); 95 | } 96 | 97 | function onNavigation(details) { 98 | if (details.frameId !== 0 || !records.has(details.tabId)) { 99 | return; 100 | } 101 | const isServerRedirect = details.transitionQualifiers.includes("server_redirect"); 102 | const keep = records.getLastRedirectRecords(details.tabId, details.url, isServerRedirect); 103 | 104 | if (keep.length > 0) { 105 | records.setTabRecords(details.tabId, keep); 106 | notifier.notify(details.tabId, keep[keep.length - 1].rule.constructor.icon, keep.length); 107 | } else { 108 | records.removeTabRecords(details.tabId); 109 | notifier.clear(details.tabId); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /test/tld-wildcard-matcher.test.js: -------------------------------------------------------------------------------- 1 | import { HostnameWithoutSuffixMatcher } from "../src/main/matchers"; 2 | 3 | test("Domain only", () => { 4 | const matcher = new HostnameWithoutSuffixMatcher("a.*"); 5 | 6 | expect(matcher.test({ url: "https://a.com" })).toBeTruthy(); 7 | expect(matcher.test({ url: "http://a.org" })).toBeTruthy(); 8 | 9 | expect(matcher.test({ url: "https://a" })).toBeFalsy(); 10 | expect(matcher.test({ url: "https://b.com" })).toBeFalsy(); 11 | expect(matcher.test({ url: "https://a.b.com" })).toBeFalsy(); 12 | expect(matcher.test({ url: "https://b.a.com" })).toBeFalsy(); 13 | expect(matcher.test({ url: "https://c.b.a.com" })).toBeFalsy(); 14 | expect(matcher.test({ url: "https://192.168.0.1" })).toBeFalsy(); 15 | }); 16 | 17 | test("With subdomain", () => { 18 | const matcher = new HostnameWithoutSuffixMatcher("b.a.*"); 19 | 20 | expect(matcher.test({ url: "https://b.a.com" })).toBeTruthy(); 21 | expect(matcher.test({ url: "http://b.a.org" })).toBeTruthy(); 22 | 23 | expect(matcher.test({ url: "https://a.com" })).toBeFalsy(); 24 | expect(matcher.test({ url: "http://a.org" })).toBeFalsy(); 25 | 26 | expect(matcher.test({ url: "https://a" })).toBeFalsy(); 27 | expect(matcher.test({ url: "https://b.com" })).toBeFalsy(); 28 | expect(matcher.test({ url: "https://a.b.com" })).toBeFalsy(); 29 | expect(matcher.test({ url: "https://c.b.a.com" })).toBeFalsy(); 30 | expect(matcher.test({ url: "https://192.168.0.1" })).toBeFalsy(); 31 | }); 32 | 33 | test("With subdomain + parts", () => { 34 | const matcher = new HostnameWithoutSuffixMatcher("d.c.b.a.*"); 35 | 36 | expect(matcher.test({ url: "https://d.c.b.a.com" })).toBeTruthy(); 37 | expect(matcher.test({ url: "http://d.c.b.a.co.uk" })).toBeTruthy(); 38 | 39 | expect(matcher.test({ url: "https://a.com" })).toBeFalsy(); 40 | expect(matcher.test({ url: "http://b.a.org" })).toBeFalsy(); 41 | expect(matcher.test({ url: "http://c.b.a.org" })).toBeFalsy(); 42 | 43 | expect(matcher.test({ url: "https://a" })).toBeFalsy(); 44 | expect(matcher.test({ url: "https://b.com" })).toBeFalsy(); 45 | expect(matcher.test({ url: "https://a.b.com" })).toBeFalsy(); 46 | expect(matcher.test({ url: "https://c.b.a.com" })).toBeFalsy(); 47 | expect(matcher.test({ url: "https://192.168.0.1" })).toBeFalsy(); 48 | }); 49 | 50 | test("Subdomain wildcard", () => { 51 | const matcher = new HostnameWithoutSuffixMatcher("*.a.*"); 52 | 53 | expect(matcher.test({ url: "https://a.com" })).toBeTruthy(); 54 | expect(matcher.test({ url: "http://a.org" })).toBeTruthy(); 55 | expect(matcher.test({ url: "https://b.a.com" })).toBeTruthy(); 56 | expect(matcher.test({ url: "http://b.a.org" })).toBeTruthy(); 57 | expect(matcher.test({ url: "https://c.b.a.com" })).toBeTruthy(); 58 | expect(matcher.test({ url: "https://c.b.a.co.uk" })).toBeTruthy(); 59 | 60 | expect(matcher.test({ url: "https://a" })).toBeFalsy(); 61 | expect(matcher.test({ url: "https://aa.com" })).toBeFalsy(); 62 | expect(matcher.test({ url: "https://b.com" })).toBeFalsy(); 63 | expect(matcher.test({ url: "https://a.b.com" })).toBeFalsy(); 64 | expect(matcher.test({ url: "https://192.168.0.1" })).toBeFalsy(); 65 | }); 66 | 67 | test("Subdomain wildcard + parts", () => { 68 | const matcher = new HostnameWithoutSuffixMatcher("*.b.a.*"); 69 | 70 | expect(matcher.test({ url: "https://b.a.com" })).toBeTruthy(); 71 | expect(matcher.test({ url: "http://b.a.org" })).toBeTruthy(); 72 | expect(matcher.test({ url: "https://c.b.a.com" })).toBeTruthy(); 73 | expect(matcher.test({ url: "https://c.b.a.co.uk" })).toBeTruthy(); 74 | 75 | expect(matcher.test({ url: "https://cb.a.com" })).toBeFalsy(); 76 | expect(matcher.test({ url: "https://cb.a.co.uk" })).toBeFalsy(); 77 | 78 | expect(matcher.test({ url: "https://a.com" })).toBeFalsy(); 79 | expect(matcher.test({ url: "http://a.org" })).toBeFalsy(); 80 | expect(matcher.test({ url: "https://a" })).toBeFalsy(); 81 | expect(matcher.test({ url: "https://b.com" })).toBeFalsy(); 82 | expect(matcher.test({ url: "https://a.b.com" })).toBeFalsy(); 83 | expect(matcher.test({ url: "https://192.168.0.1" })).toBeFalsy(); 84 | }); 85 | 86 | test("Invalid pattern - throws", () => { 87 | expect(() => new HostnameWithoutSuffixMatcher("abc")).toThrow(); 88 | }); 89 | -------------------------------------------------------------------------------- /src/main/matchers.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import { createRegexpPattern } from "../util/regexp.js"; 6 | import { libTld, UrlParser } from "./url.js"; 7 | 8 | export class BaseMatcher { 9 | static test() { 10 | return true; 11 | } 12 | } 13 | 14 | export class RequestMatcher { 15 | constructor(matchers) { 16 | this.matchers = matchers; 17 | } 18 | 19 | test(request) { 20 | return this.matchers.every((matcher) => matcher.test(request)); 21 | } 22 | } 23 | 24 | export class IncludeMatcher { 25 | constructor(values) { 26 | this.pattern = createRegexpPattern(values, true, true); 27 | } 28 | 29 | test(request) { 30 | return this.pattern.test(request.url); 31 | } 32 | } 33 | 34 | export class ExcludeMatcher extends IncludeMatcher { 35 | test(request) { 36 | return !super.test(request); 37 | } 38 | } 39 | 40 | export class DomainMatcher { 41 | static test(request) { 42 | return typeof request.thirdParty === "boolean" 43 | ? !request.thirdParty 44 | : DomainMatcher.testUrls(request.originUrl, request.url); 45 | } 46 | 47 | static testUrls(originUrl, targetUrl) { 48 | if (typeof originUrl === "undefined") { 49 | // top-level document 50 | return true; 51 | } 52 | const origin = libTld.getDomain(originUrl); 53 | const outgoing = libTld.getDomain(targetUrl); 54 | if (outgoing === null) { 55 | // expect request to be from same domain 56 | return true; 57 | } 58 | return origin === outgoing; 59 | } 60 | } 61 | 62 | export class OriginMatcher { 63 | static test(request) { 64 | return OriginMatcher.testUrls(request.originUrl, request.url); 65 | } 66 | 67 | static testUrls(originUrl, targetUrl) { 68 | if (typeof originUrl === "undefined") { 69 | // top-level document 70 | return true; 71 | } 72 | const { origin } = new UrlParser(originUrl); 73 | const outgoing = new UrlParser(targetUrl).origin; 74 | return origin === outgoing; 75 | } 76 | } 77 | 78 | export class ThirdPartyDomainMatcher { 79 | static test(request) { 80 | return !DomainMatcher.test(request); 81 | } 82 | } 83 | 84 | export class ThirdPartyOriginMatcher { 85 | static test(request) { 86 | return !OriginMatcher.test(request); 87 | } 88 | } 89 | 90 | export class HostnameWithoutSuffixMatcher { 91 | constructor(hostname) { 92 | const parts = hostname.split("."); 93 | 94 | if (parts.length < 2 || parts.pop() !== "*") { 95 | throw "Incorrect TLD wildcard hostname pattern!"; 96 | } 97 | 98 | this.domainWithoutSuffix = parts.pop(); 99 | this.matchAnySubdomain = parts.length > 0 && parts[0] === "*"; 100 | 101 | if (this.matchAnySubdomain) { 102 | parts.shift(); 103 | } 104 | 105 | this.subdomainparts = parts.join("."); 106 | } 107 | 108 | test(request) { 109 | const { domainWithoutSuffix, subdomain } = libTld.parse(request.url); 110 | return this.domainWithoutSuffix === domainWithoutSuffix && this.testSubdomain(subdomain); 111 | } 112 | 113 | testSubdomain(subdomain) { 114 | if (this.subdomainparts === subdomain) { 115 | return true; 116 | } 117 | if (!this.matchAnySubdomain) { 118 | return false; 119 | } 120 | if (this.subdomainparts === "") { 121 | return true; 122 | } 123 | if (subdomain.length < this.subdomainparts.length + 1) { 124 | return false; 125 | } 126 | const leadingDotIndex = subdomain.length - this.subdomainparts.length - 1; 127 | return subdomain.endsWith(this.subdomainparts) && subdomain[leadingDotIndex] === "."; 128 | } 129 | } 130 | 131 | export class HostnamesWithoutSuffixMatcher { 132 | constructor(hostnames) { 133 | this.matchers = hostnames.includes("*.*") 134 | ? [BaseMatcher] 135 | : hostnames.map((hostname) => new HostnameWithoutSuffixMatcher(hostname)); 136 | } 137 | 138 | test(request) { 139 | return this.matchers.some((matcher) => matcher.test(request)); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /rules/privacy-common-params.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "title": "Remove common tracking query parameters", 4 | "description": "Replicates Neat URL extension. Removes common tracking query parameters from all requests to websites.", 5 | "uuid": "5276a290-b21a-4deb-a86e-aa54c3ff1bcc", 6 | "tag": "privacy-tracking-params", 7 | "pattern": { 8 | "allUrls": true, 9 | "excludes": [ 10 | "*/ucp.php?mode=logout&sid=*", 11 | "https://www.fbsbx.com/captcha/recaptcha/iframe/*" 12 | ] 13 | }, 14 | "types": [ 15 | "main_frame", 16 | "sub_frame" 17 | ], 18 | "action": "filter", 19 | "active": true, 20 | "skipRedirectionFilter": true, 21 | "paramsFilter": { 22 | "values": [ 23 | "CNDID", 24 | "_hsenc", 25 | "_hsmi", 26 | "_openstat", 27 | "action_object_map", 28 | "action_ref_map", 29 | "action_type_map", 30 | "algo_expid", 31 | "algo_pvid", 32 | "at_campaign", 33 | "at_custom*", 34 | "at_medium", 35 | "btsid", 36 | "fb_action_ids", 37 | "fb_action_types", 38 | "fb_ref", 39 | "fb_source", 40 | "fbclid", 41 | "ga_campaign", 42 | "ga_content", 43 | "ga_medium", 44 | "ga_place", 45 | "ga_source", 46 | "ga_term", 47 | "gs_l", 48 | "hmb_campaign", 49 | "hmb_medium", 50 | "hmb_source", 51 | "icid", 52 | "igshid", 53 | "mbid", 54 | "mkt_tok", 55 | "ncid", 56 | "nr_email_referer", 57 | "ref_*", 58 | "referer", 59 | "referrer", 60 | "sc_campaign", 61 | "sc_channel", 62 | "sc_content", 63 | "sc_country", 64 | "sc_geo", 65 | "sc_medium", 66 | "sc_outcome", 67 | "share", 68 | "sid", 69 | "spJobID", 70 | "spMailingID", 71 | "spReportId", 72 | "spUserID", 73 | "sr_share", 74 | "trackingId", 75 | "trk", 76 | "trkCampaign", 77 | "utm_*", 78 | "vero_conv", 79 | "vero_id", 80 | "ws_ab_test", 81 | "xtor", 82 | "yclid" 83 | ] 84 | } 85 | }, 86 | { 87 | "uuid": "a6b8a3a1-5bba-46b1-81a5-8594e3e8b0a1", 88 | "pattern": { 89 | "scheme": "https", 90 | "host": [ 91 | "*.sharepoint.com", 92 | "home.navigator-bs.gmx.com", 93 | "navigator-bs.gmx.com", 94 | "trackbar.navigator-bs.gmx.com", 95 | "www.signbank.org" 96 | ], 97 | "path": [ 98 | "*share=*", 99 | "*sid=*" 100 | ] 101 | }, 102 | "types": [ 103 | "main_frame", 104 | "sub_frame" 105 | ], 106 | "action": "whitelist", 107 | "active": true, 108 | "title": "Exceptions to `sid` and `share` parameters", 109 | "description": "These sites use the URL parameters `sid` and `share` to load different pages. This trips a general-purpose referrer remover.", 110 | "tag": "privacy-tracking-params" 111 | }, 112 | { 113 | "uuid": "1123f3fd-fde5-4992-af96-c580c0f69186", 114 | "pattern": { 115 | "scheme": "*", 116 | "host": [ 117 | "*" 118 | ], 119 | "path": [ 120 | "*/validate-email*" 121 | ] 122 | }, 123 | "types": [ 124 | "main_frame", 125 | "sub_frame" 126 | ], 127 | "action": "whitelist", 128 | "active": true, 129 | "tag": "privacy-tracking-params", 130 | "title": "E-mail verification", 131 | "description": "Most websites send activation links via e-mail to prevent spam. Some of these links are incorrectly filtered." 132 | } 133 | ] 134 | -------------------------------------------------------------------------------- /rules/privacy-common-redirectors.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "title": "Skip external dereferrers and redirectors", 4 | "description": "This filter is applied globally by URL path. As matched globally, it may result in a site not functioning correctly. Disabled by default.", 5 | "tag": "privacy-redirectors", 6 | "uuid": "4a6b8caf-3d0b-4c95-876e-b2cdcde1eb6a", 7 | "pattern": { 8 | "scheme": "*", 9 | "host": [ 10 | "*" 11 | ], 12 | "path": [ 13 | "*/dereferrer/*", 14 | "*/outgoing?*", 15 | "*redir/redirect?*", 16 | "/deref/*", 17 | "away.php?to=*", 18 | "redirect?*" 19 | ] 20 | }, 21 | "types": [ 22 | "main_frame" 23 | ], 24 | "action": "filter", 25 | "active": false 26 | }, 27 | { 28 | "uuid": "3baa759b-9afc-41ef-b770-c60e1d9383d2", 29 | "description": "Filter rule to skip and prevent redirection tracking on multiple hosts.", 30 | "tag": "privacy-redirectors", 31 | "pattern": { 32 | "scheme": "*", 33 | "host": [ 34 | "*.tradedoubler.com", 35 | "out.reddit.com", 36 | "steamcommunity.com" 37 | ], 38 | "path": [ 39 | "*url=*" 40 | ] 41 | }, 42 | "types": [ 43 | "main_frame", 44 | "sub_frame" 45 | ], 46 | "action": "filter", 47 | "active": true 48 | }, 49 | { 50 | "title": "Skip Mozilla's outgoing link redirection service", 51 | "uuid": "3c4d6fa0-e2fb-4079-b3f0-e453ebe289afalsee", 52 | "tag": "privacy-redirectors", 53 | "pattern": { 54 | "scheme": "*", 55 | "host": [ 56 | "outgoing.prod.mozaws.net" 57 | ], 58 | "path": [ 59 | "*" 60 | ] 61 | }, 62 | "types": [ 63 | "main_frame" 64 | ], 65 | "action": "filter", 66 | "active": true 67 | }, 68 | { 69 | "uuid": "3a560339-2f03-4ab1-b6e3-db1edb01a875", 70 | "pattern": { 71 | "scheme": "*", 72 | "host": [ 73 | "*.awstrack.me" 74 | ], 75 | "path": [ 76 | "/L0/*" 77 | ] 78 | }, 79 | "action": "redirect", 80 | "active": true, 81 | "title": "AWS SES (awstrack.me)", 82 | "description": "Strip destination from AWS SES links (awstrack.me), often found in newsletter emails.", 83 | "tag": "privacy-redirectors", 84 | "trimAllParams": true, 85 | "redirectUrl": "[href={pathname/\\/L0\\//$'|decodeURIComponent}]" 86 | }, 87 | { 88 | "uuid": "95758173-ff22-4863-a743-d09a068cca91", 89 | "pattern": { 90 | "scheme": "*", 91 | "host": [ 92 | "cdn.embedly.com" 93 | ], 94 | "path": [ 95 | "/widgets/media.html?src=*" 96 | ] 97 | }, 98 | "types": [ 99 | "sub_frame" 100 | ], 101 | "action": "filter", 102 | "active": true, 103 | "tag": "privacy-redirectors" 104 | }, 105 | { 106 | "uuid": "e3dd89aa-6fd3-41ae-945f-3617fc03902d", 107 | "pattern": { 108 | "scheme": "*", 109 | "host": [ 110 | "c.disquscdn.com" 111 | ], 112 | "path": [ 113 | "get?url=*" 114 | ] 115 | }, 116 | "types": [ 117 | "image" 118 | ], 119 | "action": "filter", 120 | "active": true, 121 | "title": "Skip Disqus image retriever", 122 | "tag": "privacy-redirectors" 123 | }, 124 | { 125 | "uuid": "e3384047-a2b2-433e-a52c-f46eea48305f", 126 | "pattern": { 127 | "scheme": "*", 128 | "host": [ 129 | "disq.us" 130 | ], 131 | "path": [ 132 | "url?url=*" 133 | ] 134 | }, 135 | "types": [ 136 | "main_frame", 137 | "sub_frame" 138 | ], 139 | "action": "redirect", 140 | "active": true, 141 | "title": "Skip Disqus redirector", 142 | "redirectUrl": "{search/\\?url=([^&]*)&.*/$1|decodeURIComponent|/:[A-Za-z0-9-]*$/}", 143 | "tag": "privacy-redirectors", 144 | "description": "A suffix after the destination URL causes the usual approach to fail" 145 | } 146 | ] 147 | -------------------------------------------------------------------------------- /src/options/rule-import-input.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | class RuleImportInput extends HTMLElement { 6 | constructor() { 7 | super(); 8 | this.rules = []; 9 | this._data = {}; 10 | const template = document.getElementById("rule-import-input"); 11 | this.attachShadow({ mode: "open" }).appendChild(template.content.cloneNode(true)); 12 | 13 | this.shadowRoot.getElementById("select").addEventListener("change", (e) => { 14 | if (e.target.checked) { 15 | this.setAttribute("selected", "selected"); 16 | } else { 17 | this.removeAttribute("selected"); 18 | } 19 | this.dispatchEvent( 20 | new CustomEvent("rule-import-selected", { 21 | bubbles: true, 22 | composed: true, 23 | }) 24 | ); 25 | }); 26 | 27 | this.shadowRoot.getElementById("remove-imported").addEventListener("click", () => { 28 | this.dispatchEvent( 29 | new CustomEvent("rule-import-remove-imported", { 30 | bubbles: true, 31 | composed: true, 32 | }) 33 | ); 34 | }); 35 | } 36 | 37 | static get observedAttributes() { 38 | return ["src", "deletable"]; 39 | } 40 | 41 | attributeChangedCallback(name, _oldValue, newValue) { 42 | switch (name) { 43 | case "src": 44 | this.onSourceChanged(newValue); 45 | break; 46 | case "deletable": 47 | this.onDeletableChanged(newValue); 48 | break; 49 | default: 50 | break; 51 | } 52 | } 53 | 54 | onSourceChanged(src) { 55 | const text = this.shadowRoot.getElementById("name"); 56 | const url = this.shadowRoot.getElementById("url"); 57 | if (text.childElementCount === 0) { 58 | text.textContent = src; 59 | } 60 | url.href = src; 61 | this.fetchRules(src); 62 | } 63 | 64 | onDeletableChanged(deletable) { 65 | const deleteButton = this.shadowRoot.getElementById("delete"); 66 | deleteButton.hidden = !deletable; 67 | deleteButton.addEventListener("click", () => { 68 | this.dispatchEvent( 69 | new CustomEvent("rule-import-deleted", { 70 | bubbles: true, 71 | composed: true, 72 | }) 73 | ); 74 | }); 75 | } 76 | 77 | get data() { 78 | return this._data; 79 | } 80 | 81 | set data(value = {}) { 82 | const badge = this.shadowRoot.getElementById("imported"); 83 | const remove = this.shadowRoot.getElementById("remove-imported"); 84 | badge.hidden = !value.imported; 85 | remove.hidden = !value.imported; 86 | this._data = value; 87 | } 88 | 89 | async fetchRules(src) { 90 | const loading = this.shadowRoot.getElementById("loading"); 91 | const error = this.shadowRoot.getElementById("error"); 92 | const select = this.shadowRoot.getElementById("select"); 93 | loading.hidden = false; 94 | error.hidden = true; 95 | select.disabled = true; 96 | this.disabled = true; 97 | 98 | try { 99 | const response = await fetch(src); 100 | 101 | if (!response.ok) { 102 | throw `${response.status} - ${response.statusText}`; 103 | } 104 | 105 | const data = await response.json(); 106 | this.digest = await digest(JSON.stringify(data)); 107 | this.etag = response.headers.get("etag"); 108 | this.rules = Array.isArray(data) ? data : [data]; 109 | this.shadowRoot.getElementById("count").textContent = browser.i18n.getMessage( 110 | "count_rules", 111 | this.rules.length 112 | ); 113 | select.disabled = false; 114 | this.disabled = false; 115 | } catch (e) { 116 | error.title = e; 117 | error.hidden = false; 118 | select.disabled = true; 119 | } 120 | 121 | loading.hidden = true; 122 | } 123 | } 124 | 125 | customElements.define("rule-import-input", RuleImportInput); 126 | 127 | async function digest(text) { 128 | const encoder = new TextEncoder(); 129 | const data = encoder.encode(text); 130 | const digest = await crypto.subtle.digest("SHA-1", data); 131 | const bytes = Array.from(new Uint8Array(digest)); 132 | return bytes.map((byte) => byte.toString(16).padStart(2, "0")).join(""); 133 | } 134 | -------------------------------------------------------------------------------- /src/popup/browser-action.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | document.addEventListener("DOMContentLoaded", async () => { 6 | const { disabled } = await browser.storage.local.get("disabled"); 7 | 8 | updateDisabled(disabled === true); 9 | 10 | for (const copyButton of document.getElementsByClassName("copyButton")) { 11 | copyButton.addEventListener("click", copyText); 12 | copyButton.addEventListener("mouseleave", copied); 13 | } 14 | 15 | document.getElementById("showRules").addEventListener("click", openOptionsPage); 16 | document.getElementById("toggleActive").addEventListener("click", toggleActive); 17 | document.getElementById("editLink").addEventListener("click", editRule); 18 | 19 | if (disabled !== true) { 20 | getRecords(); 21 | } 22 | }); 23 | 24 | async function getRecords() { 25 | const records = await browser.runtime.sendMessage(null); 26 | 27 | if (!records) { 28 | return; 29 | } 30 | 31 | const list = document.getElementById("records"); 32 | 33 | records.forEach((record) => list.prepend(newListItem(record))); 34 | 35 | list.querySelector(".entry:first-child .entry-header").click(); 36 | document.getElementById("records").classList.remove("hidden"); 37 | } 38 | 39 | function newListItem(record) { 40 | const item = document.getElementById("entryTemplate").content.cloneNode(true); 41 | 42 | item.querySelector(".type").textContent = browser.i18n.getMessage(record.type); 43 | item.querySelector(".timestamp").textContent = timestamp(record.timestamp); 44 | item.querySelector(".icon").src = `/icons/icon-${record.action}.svg`; 45 | item.querySelector(".action").textContent = browser.i18n.getMessage(`title_${record.action}`); 46 | item.querySelector(".url").textContent = record.url; 47 | 48 | const tagsNode = item.querySelector(".tags"); 49 | 50 | if (record.rule.tag) { 51 | tagsNode.textContent = decodeURIComponent(record.rule.tag); 52 | } else { 53 | tagsNode.remove(); 54 | } 55 | 56 | item.querySelector(".entry-header").addEventListener("click", function () { 57 | const details = document.getElementById("details"); 58 | this.parentNode.appendChild(details); 59 | showDetails(record); 60 | }); 61 | return item; 62 | } 63 | 64 | function showDetails(details) { 65 | document.getElementById("details").classList.remove("hidden"); 66 | document.getElementById("url").textContent = details.url; 67 | if (details.target) { 68 | document.getElementById("target").textContent = details.target; 69 | document.getElementById("targetBlock").classList.remove("hidden"); 70 | } else { 71 | document.getElementById("targetBlock").classList.add("hidden"); 72 | } 73 | const optionsUrl = browser.runtime.getURL("src/options/options.html"); 74 | document.getElementById("editLink").href = `${optionsUrl}?edit=${details.rule.uuid}`; 75 | } 76 | 77 | function openOptionsPage() { 78 | browser.runtime.openOptionsPage(); 79 | window.close(); 80 | } 81 | 82 | async function toggleActive() { 83 | const disabled = !this.classList.contains("disabled"); 84 | await browser.storage.local.set({ disabled }); 85 | updateDisabled(disabled); 86 | } 87 | 88 | function updateDisabled(disabled) { 89 | const button = document.getElementById("toggleActive"); 90 | const textId = disabled ? "activate_true" : "activate_false"; 91 | const titleId = disabled ? "enable_rules" : "disable_rules"; 92 | button.classList.toggle("disabled", disabled); 93 | button.textContent = browser.i18n.getMessage(textId); 94 | button.title = browser.i18n.getMessage(titleId); 95 | } 96 | 97 | function editRule(e) { 98 | e.preventDefault(); 99 | browser.tabs.create({ 100 | url: this.href, 101 | }); 102 | window.close(); 103 | } 104 | 105 | function timestamp(ms) { 106 | const d = new Date(ms); 107 | const hh = d.getHours().toString().padStart(2, "0"); 108 | const mm = d.getMinutes().toString().padStart(2, "0"); 109 | const ss = d.getSeconds().toString().padStart(2, "0"); 110 | const s = d.getMilliseconds().toString().padStart(3, "0"); 111 | return `${hh}:${mm}:${ss}.${s}`; 112 | } 113 | 114 | function copyText(e) { 115 | const range = document.createRange(); 116 | const text = document.getElementById(e.currentTarget.dataset.copyTarget); 117 | range.selectNodeContents(text); 118 | const selection = window.getSelection(); 119 | selection.removeAllRanges(); 120 | selection.addRange(range); 121 | document.execCommand("Copy"); 122 | e.currentTarget.classList.add("copied"); 123 | } 124 | 125 | function copied(e) { 126 | e.currentTarget.classList.remove("copied"); 127 | } 128 | -------------------------------------------------------------------------------- /src/options/rule-tester.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import { createRequestFilters } from "../main/api.js"; 6 | import { CompositeRule, RequestController } from "../main/control.js"; 7 | import { BlockRule } from "../main/rules/block.js"; 8 | import { FilterRule } from "../main/rules/filter.js"; 9 | import { RedirectRule } from "../main/rules/redirect.js"; 10 | import { SecureRule } from "../main/rules/secure.js"; 11 | import { LoggedWhitelistRule, WhitelistRule } from "../main/rules/whitelist.js"; 12 | import { matchPatternToRegExp } from "../util/regexp.js"; 13 | import ModalDialog from "./modal-dialog.js"; 14 | 15 | let previousTestUrl; 16 | 17 | class RuleTestDialog extends ModalDialog { 18 | constructor() { 19 | super(); 20 | this.rules = []; 21 | const template = document.getElementById("rule-test-dialog"); 22 | this.shadowRoot.getElementById("content").append(template.content.cloneNode(true)); 23 | 24 | this.shadowRoot.getElementById("test-url").addEventListener("input", (e) => { 25 | const result = this.shadowRoot.getElementById("result"); 26 | result.textContent = testRules(e.target.value, this.rules); 27 | }); 28 | } 29 | 30 | connectedCallback() { 31 | super.connectedCallback(); 32 | this.shadowRoot.getElementById("title").textContent = browser.i18n.getMessage("test_selected_rules"); 33 | 34 | const input = this.shadowRoot.getElementById("test-url"); 35 | 36 | if (previousTestUrl) { 37 | input.value = previousTestUrl; 38 | const result = this.shadowRoot.getElementById("result"); 39 | result.textContent = testRules(previousTestUrl, this.rules); 40 | } 41 | input.focus(); 42 | } 43 | 44 | disconnectedCallback() { 45 | super.disconnectedCallback(); 46 | previousTestUrl = this.shadowRoot.getElementById("test-url").value; 47 | } 48 | } 49 | 50 | customElements.define("rule-test-dialog", RuleTestDialog); 51 | 52 | export function showRuleTestDialog(rules) { 53 | const dialog = document.createElement("rule-test-dialog"); 54 | dialog.rules = rules; 55 | document.body.append(dialog); 56 | } 57 | 58 | function testRules(testUrl, rulePatterns) { 59 | try { 60 | new URL(testUrl); 61 | } catch { 62 | return browser.i18n.getMessage("invalid_test_url"); 63 | } 64 | const controller = new RequestController(); 65 | const request = { requestId: 0, url: testUrl }; 66 | 67 | try { 68 | for (const rulePattern of rulePatterns) { 69 | const filters = createRequestFilters(rulePattern); 70 | for (const { rule, urls, matcher } of filters) { 71 | if ( 72 | urls.map(matchPatternToRegExp).some((pattern) => pattern.test(request.url)) && 73 | matcher.test(request) 74 | ) { 75 | controller.mark(request, rule); 76 | break; 77 | } 78 | } 79 | } 80 | } catch { 81 | return browser.i18n.getMessage("error_invalid_rule"); 82 | } 83 | const rule = controller.requests.get(request.requestId); 84 | 85 | if (!rule) { 86 | return browser.i18n.getMessage("no_match"); 87 | } 88 | return testRule(rule, testUrl); 89 | } 90 | 91 | function testRule(rule, testUrl) { 92 | let redirectUrl; 93 | switch (rule.constructor) { 94 | case WhitelistRule: 95 | case LoggedWhitelistRule: 96 | return browser.i18n.getMessage("whitelisted"); 97 | case BlockRule: 98 | return browser.i18n.getMessage("blocked"); 99 | case RedirectRule: 100 | case FilterRule: 101 | redirectUrl = rule.apply(testUrl); 102 | try { 103 | new URL(redirectUrl); 104 | } catch { 105 | return browser.i18n.getMessage("invalid_target_url", redirectUrl); 106 | } 107 | if (redirectUrl === testUrl) { 108 | return browser.i18n.getMessage("matched_no_change"); 109 | } 110 | return redirectUrl; 111 | case CompositeRule: 112 | redirectUrl = rule.rules.reduce((url, r) => { 113 | const change = r.apply(url); 114 | if (change !== null) { 115 | return change; 116 | } 117 | return url; 118 | }, testUrl); 119 | try { 120 | new URL(redirectUrl); 121 | } catch { 122 | return browser.i18n.getMessage("invalid_target_url", redirectUrl); 123 | } 124 | if (redirectUrl === testUrl) { 125 | return browser.i18n.getMessage("matched_no_change"); 126 | } 127 | return redirectUrl; 128 | case SecureRule: 129 | return browser.i18n.getMessage("upgraded_to_secure"); 130 | default: 131 | break; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/options/options.css: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | :root { 6 | --font: "Open Sans", sans-serif; 7 | --primary-color: #3b67a0; 8 | --light: lightgrey; 9 | --background: rgb(249, 249, 250); 10 | --text-color: rgb(0, 0, 0); 11 | } 12 | 13 | body { 14 | font-size: 0.9rem; 15 | background: var(--background); 16 | color: var(--text-color); 17 | } 18 | 19 | .toolbar * + * { 20 | margin-top: 0; 21 | margin-bottom: 0; 22 | } 23 | 24 | #tab-settings h3 { 25 | margin: 0; 26 | } 27 | 28 | #version { 29 | margin: 0; 30 | } 31 | 32 | #tab-settings .btn { 33 | padding: 0.6em; 34 | } 35 | 36 | #tab-settings h4 { 37 | margin: 0.5em 0; 38 | } 39 | 40 | #tabs { 41 | display: flex; 42 | align-items: center; 43 | } 44 | 45 | .no-rules { 46 | border: 2px dashed darkgray; 47 | text-align: center; 48 | padding: 3em; 49 | border-radius: 3px; 50 | background: #fff; 51 | color: #999; 52 | } 53 | 54 | .tab-pane:not(.active) { 55 | display: none; 56 | } 57 | 58 | .tab-selector { 59 | display: flex; 60 | align-items: center; 61 | padding: 0 0.8em; 62 | } 63 | 64 | .tab-selector.active { 65 | color: #343a40; 66 | font-size: 1em; 67 | } 68 | 69 | .tab-selector > img { 70 | margin-right: 0.5em; 71 | } 72 | 73 | .tab-selector > span { 74 | margin: 0.5em auto; 75 | } 76 | 77 | .manual-block-collapse { 78 | cursor: pointer; 79 | -moz-user-select: none; 80 | } 81 | 82 | .manual-content { 83 | max-width: 42em; 84 | margin-left: 0; 85 | } 86 | 87 | .manual-content body { 88 | background: none; 89 | padding: 0; 90 | } 91 | 92 | .manual-content h1 { 93 | display: none; 94 | } 95 | 96 | .manual-content td:first-child { 97 | white-space: nowrap !important; 98 | } 99 | 100 | .manual-content colgroup { 101 | display: none; 102 | } 103 | 104 | .manual-content table { 105 | width: 100%; 106 | overflow-x: auto; 107 | } 108 | 109 | .manual-content td, 110 | .manual-content th { 111 | padding: 0.3em; 112 | border-width: 1px; 113 | } 114 | 115 | .manual-content table p { 116 | margin: 0; 117 | } 118 | 119 | .toc { 120 | top: 0; 121 | float: right; 122 | max-width: 18em; 123 | } 124 | 125 | .toc ul { 126 | line-height: 2; 127 | } 128 | 129 | .toc ul ul { 130 | line-height: 1.5; 131 | margin-bottom: 0.5em; 132 | list-style: none; 133 | padding-left: 1em; 134 | } 135 | 136 | .toc .collapse { 137 | display: none; 138 | } 139 | 140 | .toolbar { 141 | bottom: 0; 142 | background: var(--background); 143 | z-index: 2; 144 | display: flex; 145 | align-items: center; 146 | justify-content: space-between; 147 | padding: 0.2em 0; 148 | margin: 0; 149 | } 150 | 151 | .toolbar > button { 152 | margin: 0 0.5em 0 0; 153 | } 154 | 155 | .toolbar > button:last-child { 156 | margin-right: 0; 157 | } 158 | 159 | .license-clause { 160 | width: 38em; 161 | white-space: pre-wrap; 162 | } 163 | 164 | #new-import-source { 165 | width: 100%; 166 | max-width: 32em; 167 | } 168 | 169 | @media (min-height: 400px) { 170 | .toolbar, 171 | .toc { 172 | position: sticky; 173 | } 174 | } 175 | 176 | @media (max-width: 992px) { 177 | .toc { 178 | display: none; 179 | } 180 | } 181 | 182 | @media (min-width: 35em) { 183 | rule-list { 184 | display: block; 185 | border: 1px solid #d4d4d4; 186 | background: white; 187 | border-radius: 0.15rem; 188 | margin-bottom: 1.3em !important; 189 | box-shadow: 0 0 1px 0 rgba(0, 0, 0, 0.25); 190 | } 191 | } 192 | 193 | @media (max-width: 35em) { 194 | body { 195 | margin: 0; 196 | } 197 | 198 | rule-list { 199 | display: block; 200 | margin-bottom: 1.3em !important; 201 | } 202 | 203 | #tabs { 204 | display: flex; 205 | justify-content: space-between; 206 | align-items: center; 207 | } 208 | 209 | #tabs > .col { 210 | margin: 0; 211 | } 212 | 213 | .tab-selector { 214 | padding: 1em; 215 | } 216 | 217 | .tab-selector:hover { 218 | opacity: 1; 219 | } 220 | 221 | .tab-selector:not(.active) > span { 222 | display: none; 223 | } 224 | 225 | .license-clause { 226 | max-width: 100%; 227 | white-space: normal; 228 | } 229 | } 230 | 231 | @media (min-width: 42em) { 232 | #selectedRules { 233 | display: none; 234 | } 235 | } 236 | 237 | @media (max-width: 42em) { 238 | #selectedRules { 239 | z-index: -1; 240 | } 241 | 242 | .mobile-toolbar { 243 | display: none; 244 | } 245 | 246 | .mobile-toolbar > .btn { 247 | opacity: 1; 248 | font-size: 1em; 249 | justify-content: space-evenly; 250 | height: 20%; 251 | max-height: 5em; 252 | border: none; 253 | margin-bottom: 0.2em !important; 254 | } 255 | 256 | .mobile-toolbar > .btn > img { 257 | width: 2em; 258 | } 259 | 260 | .mobile-toolbar.show { 261 | display: flex; 262 | flex-direction: column-reverse; 263 | position: fixed; 264 | top: 0; 265 | bottom: 0; 266 | left: 0; 267 | right: 0; 268 | margin: auto; 269 | height: 100%; 270 | width: 100%; 271 | justify-content: center; 272 | background: #141313d9; 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /src/main/api.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import { 6 | BaseMatcher, 7 | DomainMatcher, 8 | ExcludeMatcher, 9 | HostnamesWithoutSuffixMatcher, 10 | IncludeMatcher, 11 | OriginMatcher, 12 | RequestMatcher, 13 | ThirdPartyDomainMatcher, 14 | ThirdPartyOriginMatcher, 15 | } from "./matchers.js"; 16 | import { BlockRule } from "./rules/block.js"; 17 | import { FilterRule } from "./rules/filter.js"; 18 | import { RedirectRule } from "./rules/redirect.js"; 19 | import { SecureRule } from "./rules/secure.js"; 20 | import { LoggedWhitelistRule, WhitelistRule } from "./rules/whitelist.js"; 21 | 22 | export const ALL_URLS = ""; 23 | 24 | export function createRequestFilters(data) { 25 | if (!data || !data.pattern) { 26 | return []; 27 | } 28 | 29 | if (!data.pattern.allUrls && data.pattern.anyTLD) { 30 | return createAnyTldRequestFilters(data); 31 | } 32 | 33 | return [ 34 | { 35 | rule: createRule(data), 36 | urls: createMatchPatterns(data.pattern), 37 | matcher: createRequestMatcher(data.pattern), 38 | types: data.types, 39 | incognito: data.pattern.incognito, 40 | }, 41 | ]; 42 | } 43 | 44 | export function createRequestMatcher(pattern, hostnamesWithoutSuffix = []) { 45 | const matchers = []; 46 | 47 | if (pattern.includes) { 48 | for (const value of pattern.includes) { 49 | matchers.push(new IncludeMatcher([value])); 50 | } 51 | } 52 | 53 | if (pattern.excludes) { 54 | matchers.push(new ExcludeMatcher(pattern.excludes)); 55 | } 56 | 57 | switch (pattern.origin) { 58 | case "same-domain": { 59 | matchers.push(DomainMatcher); 60 | break; 61 | } 62 | case "same-origin": { 63 | matchers.push(OriginMatcher); 64 | break; 65 | } 66 | case "third-party-domain": { 67 | matchers.push(ThirdPartyDomainMatcher); 68 | break; 69 | } 70 | case "third-party-origin": { 71 | matchers.push(ThirdPartyOriginMatcher); 72 | break; 73 | } 74 | } 75 | 76 | if (hostnamesWithoutSuffix.length > 0) { 77 | matchers.push(new HostnamesWithoutSuffixMatcher(hostnamesWithoutSuffix)); 78 | } 79 | 80 | return matchers.length > 0 ? new RequestMatcher(matchers) : BaseMatcher; 81 | } 82 | 83 | export function createRule(data) { 84 | switch (data.action) { 85 | case "whitelist": 86 | if (data.log) { 87 | return new LoggedWhitelistRule(data); 88 | } 89 | return new WhitelistRule(data); 90 | case "block": 91 | return new BlockRule(data); 92 | case "redirect": 93 | return new RedirectRule(data); 94 | case "filter": 95 | return new FilterRule(data); 96 | case "secure": 97 | return new SecureRule(data); 98 | default: 99 | throw new Error("Unsupported rule action"); 100 | } 101 | } 102 | 103 | /** 104 | * Construct array of match patterns 105 | * @param pattern pattern of request control rule 106 | * @returns {*} array of match patterns 107 | */ 108 | export function createMatchPatterns(pattern) { 109 | const urls = []; 110 | const hosts = Array.isArray(pattern.host) ? pattern.host : [pattern.host]; 111 | let paths = Array.isArray(pattern.path) ? pattern.path : [pattern.path]; 112 | 113 | if (pattern.allUrls) { 114 | return [ALL_URLS]; 115 | } 116 | 117 | if (!pattern.path || paths.length <= 0) { 118 | paths = [""]; 119 | } 120 | 121 | for (let host of hosts) { 122 | if (isTLDHostPattern(host)) { 123 | host = host.slice(0, -1); 124 | for (const TLD of pattern.topLevelDomains) { 125 | for (const path of paths) { 126 | urls.push(`${pattern.scheme}://${host}${TLD}${prefixPath(path)}`); 127 | } 128 | } 129 | } else { 130 | for (const path of paths) { 131 | urls.push(`${pattern.scheme}://${host}${prefixPath(path)}`); 132 | } 133 | } 134 | } 135 | 136 | return urls; 137 | } 138 | 139 | function createAnyTldRequestFilters(data) { 140 | const filters = []; 141 | const rule = createRule(data); 142 | const hosts = Array.isArray(data.pattern.host) ? data.pattern.host : [data.pattern.host]; 143 | 144 | const withoutSuffix = []; 145 | const withSuffix = []; 146 | 147 | hosts.forEach((host) => (isTLDHostPattern(host) ? withoutSuffix : withSuffix).push(host)); 148 | 149 | if (withoutSuffix.length > 0) { 150 | filters.push({ 151 | rule, 152 | urls: createMatchPatterns({ 153 | scheme: data.pattern.scheme, 154 | host: "*", 155 | path: data.pattern.path, 156 | }), 157 | matcher: createRequestMatcher(data.pattern, withoutSuffix), 158 | types: data.types, 159 | incognito: data.pattern.incognito, 160 | }); 161 | } 162 | 163 | if (withSuffix.length > 0) { 164 | filters.push({ 165 | rule, 166 | urls: createMatchPatterns({ 167 | scheme: data.pattern.scheme, 168 | host: withSuffix, 169 | path: data.pattern.path, 170 | }), 171 | matcher: createRequestMatcher(data.pattern), 172 | types: data.types, 173 | incognito: data.pattern.incognito, 174 | }); 175 | } 176 | 177 | return filters; 178 | } 179 | 180 | export function isTLDHostPattern(host) { 181 | const hostTLDWildcardPattern = /^(.+)\.\*$/; 182 | return hostTLDWildcardPattern.test(host); 183 | } 184 | 185 | function prefixPath(path) { 186 | return path.startsWith("/") ? path : `/${path}`; 187 | } 188 | -------------------------------------------------------------------------------- /src/options/rule-list.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import { newRuleInput } from "./rule-input.js"; 6 | 7 | class RuleList extends HTMLElement { 8 | constructor() { 9 | super(); 10 | const template = document.getElementById("rule-list"); 11 | this.appendChild(template.content.cloneNode(true)); 12 | 13 | this.list = this.querySelector("#list"); 14 | this.querySelector("#icon").src = this.getAttribute("icon"); 15 | this.querySelector("#title").textContent = browser.i18n.getMessage(this.getAttribute("text")); 16 | this.querySelector("#collapse").addEventListener("click", () => this.collapse()); 17 | this.querySelector("#select-all").addEventListener("change", (e) => this.onSelectAll(e)); 18 | 19 | this.addEventListener("rule-selected", () => this.updateHeader()); 20 | this.addEventListener("rule-deleted", (e) => this.onDelete(e)); 21 | this.addEventListener("rule-edit-completed", (e) => this.onEditComplete(e)); 22 | this.addEventListener("rule-action-changed", (e) => this.onActionChange(e)); 23 | this.addEventListener("rule-created", (e) => this.onCreate(e)); 24 | this.addEventListener("rule-changed", (e) => this.onchange(e)); 25 | this.addEventListener("rule-invalid", (e) => this.onInvalid(e)); 26 | } 27 | 28 | get selected() { 29 | return Array.from(this.list.querySelectorAll(".selected"), (selected) => selected.rule); 30 | } 31 | 32 | get size() { 33 | return this.list.childElementCount; 34 | } 35 | 36 | get isEmpty() { 37 | return this.list.childElementCount === 0; 38 | } 39 | 40 | newRule() { 41 | const input = newRuleInput(); 42 | this.list.append(input); 43 | this.updateHeader(); 44 | this.toggle(); 45 | input.setAttribute("new", "new"); 46 | input.toggleEdit(); 47 | input.scrollIntoView(); 48 | input.focus(); 49 | } 50 | 51 | add(rule) { 52 | const ruleInput = newRuleInput(rule); 53 | const { title } = ruleInput; 54 | 55 | if (this.size === 0 || this.list.lastElementChild.title.localeCompare(title) < 0) { 56 | this.list.append(ruleInput); 57 | return; 58 | } 59 | 60 | for (const next of this.list.childNodes) { 61 | if (next.title.localeCompare(title) >= 0) { 62 | next.before(ruleInput); 63 | break; 64 | } 65 | } 66 | } 67 | 68 | addCreated(rule) { 69 | this.add(rule); 70 | this.updateHeader(); 71 | this.toggle(); 72 | } 73 | 74 | addFrom(input) { 75 | this.add(input.rule); 76 | const newInput = this.querySelector(`#rule-${input.rule.uuid}`); 77 | newInput.selected = input.selected; 78 | newInput.toggleSaved(); 79 | this.updateHeader(); 80 | this.toggle(); 81 | } 82 | 83 | toggle() { 84 | this.classList.toggle("d-none", this.size === 0); 85 | } 86 | 87 | collapse() { 88 | this.querySelector("#collapse").classList.toggle("collapsed"); 89 | this.list.classList.toggle("collapsed"); 90 | } 91 | 92 | removeSelected() { 93 | this.list.querySelectorAll(".selected").forEach((ruleInput) => ruleInput.remove()); 94 | this.updateHeader(); 95 | this.toggle(); 96 | } 97 | 98 | removeAll() { 99 | while (this.list.lastChild) { 100 | this.list.lastChild.remove(); 101 | } 102 | } 103 | 104 | edit(uuid) { 105 | const rule = this.querySelector(`#rule-${uuid}`); 106 | if (rule) { 107 | rule.toggleEdit(); 108 | rule.scrollIntoView(); 109 | } 110 | } 111 | 112 | mark(rules, className) { 113 | rules.forEach((rule) => { 114 | const input = this.querySelector(`#rule-${rule.uuid}`); 115 | if (input) { 116 | input.classList.add(className); 117 | } 118 | }); 119 | } 120 | 121 | updateHeader() { 122 | const checkbox = this.querySelector("#select-all"); 123 | 124 | if (!this.list.querySelector(".selected")) { 125 | checkbox.checked = false; 126 | checkbox.indeterminate = false; 127 | } else if (!this.list.querySelector(":scope > :not(.selected)")) { 128 | checkbox.checked = true; 129 | checkbox.indeterminate = false; 130 | } else { 131 | checkbox.checked = false; 132 | checkbox.indeterminate = true; 133 | } 134 | this.updateSelectedText(); 135 | } 136 | 137 | updateSelectedText() { 138 | const count = this.list.querySelectorAll(".selected").length; 139 | const selectedText = this.querySelector("#selected-text"); 140 | selectedText.classList.toggle("d-none", count === 0); 141 | selectedText.textContent = browser.i18n.getMessage("selected_rules_count", [count, this.size]); 142 | } 143 | 144 | onSelectAll(e) { 145 | const { checked } = e.target; 146 | this.list.childNodes.forEach((rule) => { 147 | rule.selected = checked; 148 | }); 149 | this.updateSelectedText(); 150 | this.dispatchEvent( 151 | new CustomEvent("rule-selected", { 152 | bubbles: true, 153 | composed: true, 154 | }) 155 | ); 156 | } 157 | 158 | onCreate(e) { 159 | e.target.remove(); 160 | this.updateHeader(); 161 | this.toggle(); 162 | } 163 | 164 | onDelete(e) { 165 | e.target.remove(); 166 | this.updateHeader(); 167 | this.toggle(); 168 | } 169 | 170 | onEditComplete(e) { 171 | const { action } = e.detail; 172 | if (action !== this.id) { 173 | e.target.remove(); 174 | this.updateHeader(); 175 | this.toggle(); 176 | } 177 | } 178 | 179 | onchange(e) { 180 | if (this.id === "new") { 181 | e.stopPropagation(); 182 | } 183 | } 184 | 185 | onActionChange(e) { 186 | const { input } = e.detail; 187 | const newInput = newRuleInput(input.rule); 188 | 189 | if (this.id === "new") { 190 | newInput.setAttribute("new", "new"); 191 | } 192 | 193 | input.replaceWith(newInput); 194 | newInput.selected = input.selected; 195 | newInput.toggleEdit(); 196 | newInput.notifyChangedIfValid(); 197 | } 198 | 199 | onInvalid(e) { 200 | const { input } = e.detail; 201 | if (this.id !== "new") { 202 | input.reportValidity(); 203 | } 204 | } 205 | } 206 | 207 | customElements.define("rule-list", RuleList); 208 | -------------------------------------------------------------------------------- /src/options/rule-input.css: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | form .btn:not(button) { 6 | border: none; 7 | padding: 0; 8 | background: none; 9 | color: black; 10 | } 11 | 12 | form .btn > input { 13 | margin-right: 0.5em; 14 | } 15 | 16 | form .card, 17 | form .tags-input, 18 | form .card:focus { 19 | border: 1px solid #ccc; 20 | background: white; 21 | padding: 0.2em; 22 | font-size: 0.9em; 23 | border-radius: 2px; 24 | } 25 | 26 | form select.scheme, 27 | form select.scheme:focus { 28 | padding-top: 0.5em; 29 | padding-bottom: 0.5em; 30 | } 31 | 32 | form .row { 33 | border-spacing: 0; 34 | } 35 | 36 | .row { 37 | margin-bottom: 0.3em !important; 38 | } 39 | 40 | .form-header { 41 | font-weight: bold; 42 | margin-bottom: 0.3em !important; 43 | } 44 | 45 | .rule-select { 46 | margin: 0 0.8rem; 47 | display: flex; 48 | } 49 | 50 | .rule-select > input { 51 | margin: 0; 52 | } 53 | 54 | .disabled:not(.editing) > .rule-header { 55 | color: grey; 56 | } 57 | 58 | .disabled .btn-activate { 59 | background: green; 60 | border-color: green; 61 | color: white; 62 | } 63 | 64 | .rule-header { 65 | border-top: 1px solid #cfcfcf; 66 | line-height: 1.3; 67 | display: flex; 68 | vertical-align: middle; 69 | align-items: center; 70 | padding: 0.1em 0; 71 | } 72 | 73 | .disabled:not(.editing) > .rule-header { 74 | background: #f7f7f7; 75 | } 76 | 77 | .selected:not(.editing) > .rule-header { 78 | background: #e0e9f7; 79 | } 80 | 81 | .rule-header-title { 82 | flex-grow: 1; 83 | text-overflow: ellipsis; 84 | overflow: hidden; 85 | margin-bottom: 0; 86 | } 87 | 88 | .rule-header > .information { 89 | text-align: right; 90 | vertical-align: middle; 91 | } 92 | 93 | .rule-header > .information > .badge { 94 | white-space: nowrap; 95 | overflow: -moz-hidden-unscrollable; 96 | text-overflow: ellipsis; 97 | } 98 | 99 | .editing .rule-header { 100 | border-bottom: 1px solid #f5f5f5; 101 | padding: 0.3em 0; 102 | } 103 | 104 | .editing .rule-header > div:first-child > div { 105 | max-height: 10em; 106 | overflow-y: auto; 107 | } 108 | 109 | .btn-delete { 110 | float: right; 111 | } 112 | 113 | .rule-input-buttons { 114 | padding-top: 0.3em; 115 | padding-bottom: 0.3em; 116 | } 117 | 118 | .rule-input { 119 | padding-top: 0.3em; 120 | } 121 | 122 | .title:not([contenteditable="true"]) { 123 | white-space: nowrap; 124 | text-overflow: ellipsis; 125 | overflow: hidden; 126 | display: inline-block; 127 | max-width: 48em; 128 | } 129 | 130 | .rule-header div[class$="-wrap"] { 131 | display: flex; 132 | align-items: baseline; 133 | user-select: none; 134 | } 135 | 136 | .editing .rule-header div[class$="-wrap"] { 137 | display: block; 138 | } 139 | 140 | .rule-header div[class$="-wrap"] > .badge { 141 | padding: 0; 142 | } 143 | 144 | .rule-header .title[contenteditable="true"], 145 | .rule-header .description[contenteditable="true"], 146 | .rule-header .tag[contenteditable="true"] { 147 | color: #4b4b4b; 148 | } 149 | 150 | .rule-header .title[contenteditable="true"]:hover, 151 | .rule-header .description[contenteditable="true"]:hover, 152 | .rule-header .tag[contenteditable="true"]:hover, 153 | .rule-header .title[contenteditable="true"]:focus, 154 | .rule-header .description[contenteditable="true"]:focus, 155 | .rule-header .tag[contenteditable="true"]:focus { 156 | color: black; 157 | } 158 | 159 | .header-info-wrap { 160 | display: flex; 161 | align-items: center; 162 | text-align: end; 163 | } 164 | 165 | .col-trim-parameters { 166 | display: flex; 167 | } 168 | 169 | .col-trim-parameters > :first-child { 170 | flex: 1; 171 | } 172 | 173 | .more-types { 174 | margin: 0 !important; 175 | } 176 | 177 | .redirectUrl { 178 | padding: 0.4em !important; 179 | box-sizing: border-box; 180 | } 181 | 182 | .tags-input { 183 | margin-bottom: 0.3em !important; 184 | } 185 | 186 | @media (min-width: 35em) { 187 | .rule-header-buttons { 188 | min-width: 10em; 189 | text-align: center; 190 | } 191 | 192 | .title:hover { 193 | text-decoration: underline; 194 | cursor: pointer; 195 | } 196 | 197 | .editing .title:hover { 198 | text-decoration: initial; 199 | cursor: initial; 200 | } 201 | 202 | .new:not(.editing) > .rule-header, 203 | .saved:not(.editing) > .rule-header { 204 | border-left: 2px solid green; 205 | } 206 | 207 | .error > .rule-header { 208 | border-left: 2px solid red; 209 | } 210 | 211 | .merged:not(.editing) > .rule-header { 212 | border-left: 2px solid orange; 213 | } 214 | 215 | .form-group-pattern { 216 | display: flex; 217 | margin: 0; 218 | flex-flow: row wrap; 219 | } 220 | 221 | .form-group-pattern > div { 222 | flex-grow: 1; 223 | } 224 | 225 | #tlds-form, 226 | .form-group-pattern > div { 227 | margin-right: 1em; 228 | } 229 | 230 | .form-group-pattern > div:first-child { 231 | flex-grow: 0; 232 | } 233 | 234 | .form-wrap { 235 | display: flex; 236 | align-items: center; 237 | } 238 | 239 | .form-wrap > div:first-child { 240 | flex-grow: 1; 241 | } 242 | 243 | .form-wrap .any-wrap { 244 | min-width: 6em; 245 | white-space: nowrap; 246 | display: flex; 247 | } 248 | 249 | .url-wrap:not(.d-none) + div { 250 | margin-top: 2em !important; 251 | align-self: flex-start; 252 | } 253 | } 254 | 255 | @media (max-width: 35em) { 256 | .editing > .rule-header div[class$="-wrap"] { 257 | display: flex; 258 | align-items: flex-start; 259 | flex-direction: column; 260 | margin-bottom: 0.6em !important; 261 | } 262 | 263 | .editing .header-info-wrap, 264 | .rule-select { 265 | display: none !important; 266 | } 267 | 268 | .information::before { 269 | width: 8px; 270 | height: 8px; 271 | border-radius: 50%; 272 | display: inline-block; 273 | margin: 0 0.3em; 274 | } 275 | 276 | .new:not(.editing) .information::before, 277 | .saved:not(.editing) .information::before { 278 | background: green; 279 | content: ""; 280 | } 281 | 282 | .error .information::before { 283 | background: red; 284 | content: ""; 285 | } 286 | 287 | .merged:not(.editing) .information::before { 288 | background: orange; 289 | content: ""; 290 | } 291 | 292 | .rule-header { 293 | line-height: 1.5; 294 | flex-direction: column; 295 | align-items: initial; 296 | } 297 | 298 | .header-info-wrap { 299 | justify-content: space-between; 300 | } 301 | 302 | .selected > .rule-header { 303 | background: #f8f7ff; 304 | } 305 | 306 | .rule-input { 307 | padding: 0; 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /_locales/zh_CN/manual.wiki: -------------------------------------------------------------------------------- 1 | = Request Control 手册 = 2 | 3 | == Request Control 规则 == 4 | 5 | Request Control 规则由[[#匹配模式|匹配模式]]、[[#类型|类型]]和[[#动作|动作]] 构成。 6 | 7 | 一个请求如果与非禁用规则的匹配模式和类型相匹配,将根据该规则拦截并采取相应动作。 8 | 9 | === 匹配模式 === 10 | 11 | 匹配模式用来筛选出匹配[[#协议|协议]]、[[#主机|主机]]和[[#路径|路径]],以及可选的[[#包括和排除|包括和排除]]匹配模式的请求。 12 | 13 | ==== 协议 ==== 14 | 15 | 支持的协议为 httphttps。 16 | 17 | {| 18 | | http 19 | | 匹配 http 协议的请求。 20 | |- 21 | | https 22 | | 匹配 https 协议的请求。 23 | |- 24 | | http/https 25 | | 同时匹配 http 和 https 协议的请求。 26 | |} 27 | 28 | ==== 主机 ==== 29 | 30 | 主机匹配可以通过下列方式匹配请求的 URL 中的主机(host)。 31 | 32 | {| 33 | | www.example.com 34 | | 匹配完整主机。 35 | | 36 | |- 37 | | *.example.com 38 | | 匹配指定的主机以及它的所有子域名。 39 | | 将会匹配 example.com 的所有子域名,例如 '''www'''.example.com 和 '''good'''.example.com 40 | |- 41 | | www.example.* 42 | | 匹配符合顶级域名列表的指定主机。 (可以配合子域名匹配) 43 | | 需将所需的顶级域名写入到顶级域名列表框(例如 ''com''、''org'')。 44 | |- 45 | | * 46 | | 匹配任何主机。 47 | | 48 | |} 49 | 50 | ==== 路径 ==== 51 | 52 | 路径匹配可以是通配符 "*" 和所有 URL 路径中允许的字符的任意组合。通配符 "*" 能够匹配路径的任意部分,且可以出现多次。 53 | 54 | 下面是一些使用路径匹配的示例。 55 | 56 | {| 57 | | * 58 | | 匹配任意路径。 59 | |- 60 | | path/a/b/ 61 | | 匹配特定路径 "path/a/b/"。 62 | |- 63 | | *b* 64 | | 匹配所有包含 "b" 的路径,只要路径中出现就匹配。 65 | |- 66 | | 67 | | 匹配空路径。 68 | |} 69 | 70 | === 类型 === 71 | 72 | 类型代表请求的资源种类。可以匹配一或多种类型,也可以匹配任意类型。下面列出了所有可能的类型。 73 | 74 | {| 75 | ! 类型 76 | ! 细节 77 | |- 78 | | 文档 79 | | 代表浏览器标签页中的顶级 DOM 文档。(main frame) 80 | |- 81 | | 子文档 82 | | 代表附属于其他文档的 DOM 文档(sub frame) 83 | |- 84 | | 样式表 85 | | 代表样式表(.css 文件)。 86 | |- 87 | | 脚本 88 | | 代表可执行脚本(例如 JavaScript)。 89 | |- 90 | | 图像 91 | | 代表图像(例如 <img> 元素载入的内容)。 92 | |- 93 | | 对象 94 | | 代表对象(<object> 和 <embed> 元素的内容)。 95 | |- 96 | | 插件 97 | | 代表由插件发起的请求。(object_subrequest) 98 | |- 99 | | XMLHttpRequest 100 | | 代表 XMLHttpRequest 请求。 101 | |- 102 | | XSLT 103 | | 代表可扩展样式表转换语言(英文:Extensible Stylesheet Language Transformations,缩写为 XSLT)文档。 104 | |- 105 | | Ping 106 | | 代表因点击带有 ping 属性的 <a> 元素时发出的请求。只有 about:config 中 browser.send_pings 属性被启用时才会发出(默认为禁用)。 107 | |- 108 | | Beacon 109 | | 代表信标([https://developer.mozilla.org/en-US/docs/Web/API/Beacon_API Beacon])请求。 110 | |- 111 | | XML DTD 112 | | 代表被 XML 文档载入的 DTD 文件。 113 | |- 114 | | Font 115 | | 代表通过 CSS @font-face 规则引入的字体。 116 | |- 117 | | Media 118 | | 代表视频或音频。 119 | |- 120 | | WebSocket 121 | | 代表 [https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API WebSocket] 请求。 122 | |- 123 | | CSP Report 124 | | 代表内容安全策略([https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP Content Security Policy])报告。 125 | |- 126 | | Imageset 127 | | 代表加载带 srcset 属性的 <img> 或 <picture> 元素的内容的请求。 128 | |- 129 | | Web Manifest 130 | | 代表加载 Web manifest 的请求。 131 | |- 132 | | Other 133 | | 代表未被归类为上述任何类型的请求。 134 | |} 135 | 136 | === 动作 === 137 | 138 | ; [[File:/icons/icon-filter.svg|16px]] 过滤 139 | : 跳过 URL 重定向,并/或移除 URL 查询参数。 140 | ; [[File:/icons/icon-redirect.svg|16px]] 重定向 141 | : 将请求重定向至手动配置的目标 URL。 142 | ; [[File:/icons/icon-secure.svg|16px]] 加密 143 | : 将不安全的 HTTP 请求升级为 HTTPS 请求。 144 | ; [[File:/icons/icon-block.svg|16px]] 拦截 145 | : 在请求被发出之前取消请求。 146 | ; [[File:/icons/icon-whitelist.svg|16px]] 白名单 147 | : 白名单并可选地在日志中记录请求。 148 | 149 | == 其他 URL 匹配方式 == 150 | 151 | 以下匹配器扩展了 [https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webRequest/RequestFilter WebRequest API] 的匹配功能。 152 | 153 | ==== 包括和排除 ==== 154 | 155 | 通过支持通配符 "?" 和 "*"(其中 "?" 匹配任何单个字符,而 "*" 匹配零或多个字符)的字符串匹配,来过滤匹配的请求。也可通过在匹配模式前后加 "/" 字符来使用正则表达式。 156 | 157 | 包括和排除不区分大小写,而[[#主机|主机]]和[[#路径|路径]]区分大小写。 158 | 159 | 以下是使用包括和排除模式匹配的示例: 160 | 161 | {| 162 | | login 163 | | 匹配所有包括 "login" 的 URL。 164 | |- 165 | | log?n 166 | | 匹配包括例如 "login" 或者 "logon" 等等的 URL。 167 | |- 168 | | a*b 169 | | 匹配先后出现 "a" 和 "b" 的 URL。 170 | |- 171 | | /[?&]a=\d+(&|$)/ 172 | | 匹配包含参数 "a" 的 URL,该参数的值为数字。 173 | |} 174 | 175 | === Match by origin === 176 | 177 | 按照跨域情况过滤匹配的规则。 178 | 179 | {| 180 | | Any 181 | | 匹配任意跨域情况的请求。 182 | |- 183 | | Same domain 184 | | 匹配同域名的请求。 185 | |- 186 | | Same origin 187 | | 匹配同源的请求。按照[https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy 同源策略]判定。 188 | |- 189 | | Third party domain 190 | | 匹配第三方域名的请求。 191 | |- 192 | | Third party origin 193 | | 匹配第三方源的请求。 194 | |} 195 | 196 | == Rule priorities == 197 | 198 | # 白名单规则 199 | # 拦截规则 200 | # 加密规则 201 | # 重定向规则 202 | # 过滤规则 203 | 204 | == 去除 URL 参数 == 205 | 206 | 过滤规则支持去除 URL 参数。可以在要去除的 URL 参数的名称中使用通配符 "*" 和 "?",或者正则表达式。 207 | 208 | 下面是去除 URL 参数的示例。 209 | 210 | {| 211 | | utm_source 212 | | 去除 "utm_source" 参数。 213 | |- 214 | | utm_* 215 | | 去除所有以 "utm_" 开头的参数。 216 | |- 217 | | /[0-9]+/ 218 | | 去除所有参数名称仅由数字构成的参数。 219 | |} 220 | 221 | === 反转要去除的请求参数选项 === 222 | 223 | 仅保留参数列表中所定义的参数,移除所有其他参数。 224 | 225 | === 移除全部请求参数选项 === 226 | 227 | 从匹配的请求中移除所有 URL 查询参数。 228 | 229 | == 使用模式捕获重定向 == 230 | 231 | 重定向规则支持将请求重定向至一个从原始请求 URL 修改而来的 URL。可以使用参数展开来使用和更改目标 URL 的某些组成部分(命名参数);也可以使用重定向指令,更改目标 URL 的某些组成部分(命名参数),比如只更改原始请求 URL 的端口。 232 | 233 | 这两种方法可以结合使用。将首先解析并应用重定向指令,再轮到参数扩展。 234 | 235 | 可以在重定向指令中使用参数展开。 236 | 237 | === 参数展开 === 238 | 239 |
{parameter}
240 | 访问原始请求 URL 中的某个命名参数。本节末尾列出了可用的命名参数。 241 | 242 | 参数展开支持以下字符串操作语法: 243 | 244 | ==== 替换子字符串 ==== 245 | 246 |
{parameter/pattern/replacement}
247 | 248 | 将首个匹配 pattern 的子字符串替换为 replacement。 249 | 250 |
{parameter//pattern/replacement}
251 | 252 | 将所有匹配 pattern 的子字符串替换为 replacement。 253 | 254 | 匹配模式 pattern 使用正则表达式编写。replacement 中支持使用一些特殊替换,包括引用捕获组(capture group),下面的表格描述了支持的特殊替换。 255 | 256 | {| 257 | | $n 258 | | 插入第 n 个捕获组,从 1 开始计数。 259 | |- 260 | | $` 261 | | 插入被匹配到的子字符串之前的部分。 262 | |- 263 | | $' 264 | | 插入被匹配到的子字符串之后的部分。 265 | |- 266 | | $& 267 | | 插入被匹配到的子字符串。 268 | |- 269 | | $$ 270 | | 插入一个 "$" 字符。 271 | |} 272 | 273 | ==== 提取子字符串 ==== 274 | 275 |
{parameter:offset:length}
276 | 277 | 提取扩展参数的一个部分。偏移量 offset 确定起始位置,从 0 开始计数;如果为负值,则从字符串结尾开始算起。 278 | 279 | ==== 解码和编码 ==== 280 | 281 |
{parameter|encodingRule}
282 | 283 | 对匹配模式的展开值进行解码或编码操作。 284 | 285 | {| 286 | | encodeURI 287 | | 将值编码为 URI。不编码以下字符:":"、"/"、";",和 "?"。 288 | |- 289 | | decodeURI 290 | | 将值作为 URI 解码。 291 | |- 292 | | encodeURIComponent 293 | | 将值编码为 URI 组件。会编码所有 URI 保留字符。 294 | |- 295 | | decodeURIComponent 296 | | 将值作为 URI 组件解码。 297 | |- 298 | | encodeBase64 299 | | 将值编码为 Base64 字符串。 300 | |- 301 | | decodeBase64 302 | | 将值作为 Base64 字符串解码。 303 | |} 304 | 305 | ==== 合并参数展开操作 ==== 306 | 307 |
{parameter(manipulation1)|(manipulation2)...|(manipulationN)}
308 | 可以使用管道符 "|" 链式调用多个字符串操作规则。输出结果是被依次操作过的参数值。 309 | 310 | ==== 示例 ==== 311 | 312 | {| 313 | | https://{hostname}/new/path 314 | | 复用原始请求中的主机名。 315 | |- 316 | | https://{hostname/([a-z]{2}).*/$1}/new/path 317 | | 提取原始请求中的主机名的一部分并复用。 318 | |- 319 | | https://{hostname::-3|/.co/.com}/new/path 320 | | 去除原始请求中的主机名中的最后 3 个字符,再将其中的第一个 ".co" 替换为 ".com",再复用。 321 | |- 322 | | {search.url|decodeURIComponent} 323 | | 捕获原始请求的查询参数中的 "url" 参数,并将其解码,作为重定向目标。 324 | |} 325 | 326 | === 重定向指令 === 327 | 328 |
[parameter=value]
329 | 330 | 替换原始请求的特定组成部分。本节末尾列出了可用的命名 URL 参数(named URL parameters)。 331 | 332 |
[parameter={parameter}]
333 | 334 | 可以通过上述参数扩展语法对重定向指令的值进行参数化(parametrize)。 335 | 336 | ==== 示例 ==== 337 | 338 | {| 339 | | [port=8080] 340 | | 将原始请求的端口重定向至 8080。 341 | |- 342 | | [port=8080][hostname=localhost] 343 | | 将原始请求的主机重定向至 localhost:8080。 344 | |- 345 | | [port=8080][hostname=localhost][hash={pathname}] 346 | | 将原始请求的主机重定向至 localhost:8080,并将哈希(hash,#)更改为原始请求的主机名。 347 | |} 348 | 349 | === 命名参数列表 === 350 | 351 | 下表列出了支持的参数名称以及输出示例。 352 | 353 | 作为输入的示例地址: 354 | 355 |
https://www.example.com:8080/some/path?query=value#hash
356 | {| 357 | ! 名称 358 | ! 输出 359 | |- 360 | | protocol 361 | | https: 362 | |- 363 | | hostname 364 | | www.example.com 365 | |- 366 | | port 367 | | :8080 368 | |- 369 | | pathname 370 | | /some/path 371 | |- 372 | | search 373 | | ?query=value 374 | |- 375 | | search.query 376 | | value 377 | |- 378 | | hash 379 | | #hash 380 | |- 381 | | host 382 | | www.example.com:8080 383 | |- 384 | | origin 385 | | https://www.example.com:8080 386 | |- 387 | | href 388 | | https://www.example.com:8080/some/path?query=value#hash 389 | |} 390 | 391 | 此手册页面基于以下 MDN 维基文档中的资料编写,以 [http://creativecommons.org/licenses/by-sa/2.5/ CC-BY-SA 2.5] 协议授权。 392 | 393 | # [https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Match_patterns Match patterns] by [https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Match_patterns$history Mozilla Contributors] is licensed under [http://creativecommons.org/licenses/by-sa/2.5/ CC-BY-SA 2.5]. 394 | # [https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/webRequest/ResourceType webRequest.ResourceType] by [https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/webRequest/ResourceType$history Mozilla Contributors] is licensed under [http://creativecommons.org/licenses/by-sa/2.5/ CC-BY-SA 2.5]. 395 | # [https://developer.mozilla.org/en-US/docs/Web/API/URL URL] by [https://developer.mozilla.org/en-US/docs/Web/API/URL$history Mozilla Contributors] is licensed under [http://creativecommons.org/licenses/by-sa/2.5/ CC-BY-SA 2.5]. 396 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | - Add option to remove toolbar icon counter. #129 9 | - Add support for matching hostnames without suffix (TLD wildcard). #126 10 | - Add private browsing matcher to ignore or only use in private browsing. 11 | 12 | ## [1.15.5] - Jul. 7, 2020 13 | - Fix rule creation after 1.15.3. #131 14 | 15 | ## [1.15.4] - Jul. 3, 2020 16 | - Fix required validation after 1.15.3. #131 17 | 18 | ## [1.15.3] - Jul. 3, 2020 19 | - Fix Redirect rule not being applied when multiple rules were matching a single request. #111 20 | - Fix rule tester with multiple redirect rules. 21 | - Remove backspace deleting added parameters. #128 22 | 23 | ## [1.15.2] - Mar. 28, 2020 24 | - Fix Filter rule not being applied when multiple filter rules were matching a single request. #111 25 | 26 | ## [1.15.1] - Mar. 13, 2020 27 | - Fix popup height when placed to overflow menu. #111 28 | - Fix rule tester regexp escaping. #113 29 | - Fix selected rules exporting. 30 | 31 | ## [1.15.0] - Mar. 11, 2020 32 | - Add Secure action to upgrade HTTP requests to HTTPS. 33 | - Add new default rules: FBCLID stripping rule and FB redirection service rule. #110 by @AreYouLoco 34 | - Fix text color when default color is changed. #112 by @Zocker1999NET 35 | - Fix filter/redirect actions to be applied and logged separately. 36 | - Remove Downloads API dependency for rule export. 37 | - Update icons and badge color. 38 | 39 | ## [1.14.0] - Sep 8, 2019 40 | - Add option to redirect document (update tab) from other requests types. #103 41 | - Fix invalid regexp pattern breaking all other rules. #107 42 | - Keep last record when redirect/filter rule is followed by a server redirection. 43 | 44 | ## [1.13.3] - Aug 22, 2019 45 | - Fix hosts/paths input field overflow issue with long list of values (2nd fix). #104 46 | - Fix updating of input fields for merged rules after import. 47 | - Accept text files on import. 48 | 49 | ## [1.13.2] - Aug 19, 2019 50 | - Fix comma not accepeted in includes/excludes input fields. #105 51 | - Fix hosts/paths input field overflow issue with long list of values. #104 52 | 53 | ## [1.13.1] - Aug 10, 2019 54 | - Fix subdocument redirect regression issue #103 55 | - Fix bad localization key. 56 | 57 | ## [1.13.0] - Aug 5, 2019 58 | - Change query parameter trimming in Filter Rule to not apply to the extracted redirect URL. #99 59 | - Add replace all substring support for Redirect Rule. #101 60 | 61 | ## [1.12.4] - June 29, 2019 62 | - Add workaround for Firefox bug https://bugzilla.mozilla.org/show_bug.cgi?id=1557300. #100 63 | - Other improvements to options (rule delete button, create button / new rule validation fix, type badge) 64 | 65 | ## [1.12.3] - May 12, 2019 66 | - Update style and manual. 67 | - Fix rule edit link in popup. 68 | 69 | ## [1.12.2] - April 28, 2019 70 | - Order rules alphabetically. #90 71 | 72 | ## [1.12.1] - April 27, 2019 73 | - Update style for more compact layout. 74 | 75 | ## [1.12.0] - April 22, 2019 76 | - Add Android support and update layout and style for mobile browsers. #88 77 | - Add speculative request type. #81 78 | - Add support for matching requests by origin. #36 79 | - Close modals on Escape-key. #91 80 | - Fix action buttons disabled issue. #96 81 | - Fix white list rule testing no feedback issue. #92 82 | - Fix percent sign issue with redirect rules. #95 83 | 84 | ## [1.11.1] - September 6, 2018 85 | - Add strict min version requirement for Firefox 60. 86 | - Remove debug console logging #80. 87 | 88 | ## [1.11.0] - August 17, 2018 89 | - Add support for skipping redirection url filtering within same domain. #29 90 | 91 | ## [1.10.1] - August 4, 2018 92 | - Fix excludes/includes with Any URL. #77 93 | 94 | ## [1.10.0] - July 29, 2018 95 | - Add keywords to decode and encode captured patterns for redirect rule #6 96 | - Add includes and excludes pattern support #16 #24 #35 97 | - Add option to control whitelist logging #59 98 | - Add query parameter expansion support for redirect rule #72 99 | - Change to trim unwanted query parameters before inline url parsing #72 100 | - Update active and disabled icons #70 101 | - Fix parsing of redirect instructions with inline brackets #73 102 | 103 | ## [1.9.4] - May 26, 2018 104 | - Fix rule import after 1.9.3 changes. 105 | - Fix showing number of selected rules for new rules. 106 | 107 | ## [1.9.3] - May 24, 2018 108 | - Add alphabetizing patterns. #57 109 | - Add Select / select none input #63 110 | - Add showing number of selected rules #64 111 | - Add UUID for rules. Rules with same UUID will be overwritten when importing rules. 112 | - Fix Rule Tester to escape '?' in path. #65 113 | - Fix redirecting by manipulating 'hostname'. #69 114 | 115 | ## [1.9.2] - May 15, 2018 116 | - Add disabled state icon. 117 | - Fix resolving match patterns with multiple paths and TLDs. #66 118 | 119 | ## [1.9.1] - May 13, 2018 120 | - Fix toolbar icon updating. 121 | - Fix adding two-character long generic domains. #56 122 | - Fix adding comma to trim parameters. #58 123 | 124 | ## [1.9.0] - May 9, 2018 125 | - Add rule tester for testing selected rules against test URL. 126 | - Add support for rule tagging in panel. 127 | - Add rule edit links in panel. 128 | - Add disable/enable button in panel. 129 | - Change Redirect instructions to supports parameter expansion in value. 130 | - Update options style and panel layout. 131 | - Locale: ES Spanish, thanks to @strel at Github! 132 | 133 | ## [1.8.6] - Nov. 26, 2017 134 | - Fix Redirect to static url. 135 | - Fix combining parameter expansion with redirect instructions. 136 | - Add unit tests. 137 | 138 | ## [1.8.5] - Nov. 25, 2017 139 | - Fix query parameter trimming. #50 140 | - Fix icons not showing in rules view. 141 | 142 | ## [1.8.4] - Nov. 18, 2017 143 | - Fix build. 144 | 145 | ## [1.8.3] - Nov. 17, 2017 146 | - Fix regex repetition quantifier not supported in pattern captures. #45 #47 147 | - Fix query parameters trimming on non-standard urls. #48 #40 148 | 149 | ## [1.8.2] - August 6, 2017 150 | - Fix Filter rule to always decode redirection URL. 151 | - Add version in about page. 152 | 153 | ## [1.8.1] - July 22, 2017 154 | - Fix save rule on title/description change. 155 | - Fix any-url host input required validation. 156 | - Add description for default rules. 157 | - Load default rules from file ("/options/default-rules.json"). 158 | - Strip paramsTrim pattern from exported rules. 159 | 160 | ## [1.8.0] - July 19, 2017 161 | - Rules are now auto saved on change. 162 | - Rule name and description are editable. 163 | - Add invert URL parameter trim option. 164 | - Add about page. 165 | - Other changes to rule options display. 166 | 167 | ## [1.7.1] - July 1, 2017 168 | - Fix whitelist/block rule request markers. 169 | - Fix migrate script for Firefox versions before 55. 170 | 171 | ## [1.7.0] - June 29, 2017 172 | - Add rules export and import. #8 173 | - Add toolbar button to list details of applied rules on current tab. #19 174 | - Add tabs to options view. 175 | - Fix trim parameter inconsistency: support literal string params and regexp params. #17 176 | - Fix pageAction details bug with block rules. #19 177 | - Fix filter rule redirection url filtering. #20 178 | - Remove url status icon. #19 179 | 180 | ## [1.6.1] - June 21, 2017 181 | - Add i18n support 182 | - Add request details in page action popup 183 | - Fix query parameters trim with valueless params 184 | 185 | ## [1.6.0] - June 13, 2017 186 | - Add support for multiple rule matching for single request. 187 | - Add support for adding multiple hosts and paths for rules. 188 | - Remove 'Include subdomains' checkbox. 189 | - Improve rules options view. 190 | 191 | ## [1.5.0] - June 1, 2017 192 | - URL parameter filtering is now Filter rule specific. 193 | - Redirection cleaning can now be turned off in Filter rule. 194 | - Added wildcard "*" support for url parameter trimming. 195 | 196 | ## [1.4.3] - May 27, 2017 197 | - Fix url parameter filtering for global rules. 198 | - Fix redirection url parsing from query string. 199 | - Fix default google filter rule, include main frame requests. 200 | - Fix filter action handler to only do tab navigation on sub_frame requests. 201 | 202 | ## [1.4.2] - May 7, 2017 203 | - Fix to escape forward slash in replace regex pattern. 204 | - Add toggle rule edit on double click. 205 | 206 | ## [1.4.1] - April 12, 2017 207 | - Change the default type for new rules to be the document type. 208 | - Change any url and any type buttons to be the rightmost on rule panel. 209 | - Fix more than one parameter expansions failing in redirection address. 210 | - Fix undefined rule title when saving a new rule without changing its action. 211 | 212 | ## [1.4.0] - April 10, 2017 213 | - Add support for pattern capturing (parameter expansion) to redirect based on the original request. 214 | - Add support for parameter instructions to redirect based on the original request. 215 | - Change help page to open in a new page. 216 | - Update help and add attributions to the MDN documents. 217 | - Fix missing title for options page. 218 | 219 | ## [1.3.0] - March 27, 2017 220 | - Add whitelist rules support. 221 | - Add pattern support for creating global rules. 222 | - Change option page to open in new tab. 223 | - Fix input validation that allowed incorrect rule saving. 224 | 225 | ## [1.2.3] - March 15, 2017 226 | - Add toggleable edit mode for rules. 227 | - Change tracking URL parameters input option to use one line tags-input. 228 | - Fix to include WebExtension permission for all urls. 229 | - Fix to include applications key with add-on id in manifest.json. 230 | 231 | ## [1.2.2] - October 30, 2016 232 | - Add support for rule based control with actions (filter, block, redirect). 233 | - Add support for request types. 234 | - Add page action for providing user feedback of handled requests. 235 | - Add help page. 236 | - Add "ng" to the TLDs of pre-defined rule for Google. 237 | - Fix subdomain top-level domain confusion. 238 | - Change TLDs from global list to rule based manual list. 239 | - Change add-on name from JustRedirect! to Request Control. 240 | - Change license from MIT to MPL-2.0. 241 | - Enhance options usability to improve rule creation and match pattern definition (uses Bootstrap CSS). 242 | 243 | ## [1.1.0] - October 1, 2016 244 | - Add match pattern for Google search to prevent outgoing search link tracking. 245 | - Add support for creating match patterns for matching different sub domains (e.g. www.google.*). 246 | - Add match pattern validation. 247 | - Add icon for the add-on. 248 | - Fix to prevent enter key from deleting values on inputs. 249 | - Fix updating redirection listeners on options change. 250 | - Fix adding history entries for redirection origin urls. 251 | 252 | ## [1.0.2] - September 24, 2016 253 | - Add out.reddit.com redirection url pattern. 254 | - Add outgoing.prod.mozaws.net pattern. 255 | - Fix query parameter filtering. 256 | 257 | ## [1.0] - September 23, 2016 258 | - Initial release -------------------------------------------------------------------------------- /_locales/es/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionName": { 3 | "message": "Request Control" 4 | }, 5 | "extensionDescription": { 6 | "message": "Define reglas para controlar peticiones HTTP." 7 | }, 8 | "extensionManual": { 9 | "message": "/_locales/es/manual.html", 10 | "description": "Cambia para apuntar al manual.html localizado." 11 | }, 12 | "title_filter": { 13 | "message": "Filtrado: " 14 | }, 15 | "title_block": { 16 | "message": "Bloqueado: " 17 | }, 18 | "title_redirect": { 19 | "message": "Redirigido: " 20 | }, 21 | "title_whitelist": { 22 | "message": "En lista blanca: " 23 | }, 24 | "rule_title_filter": { 25 | "message": "Regla de filtro para $HOST$", 26 | "placeholders": { 27 | "host": { 28 | "content": "$1", 29 | "example": "ejemplo.com" 30 | } 31 | } 32 | }, 33 | "rule_title_redirect": { 34 | "message": "Regla de redireccionamiento para $HOST$", 35 | "placeholders": { 36 | "host": { 37 | "content": "$1", 38 | "example": "ejemplo.com" 39 | } 40 | } 41 | }, 42 | "rule_title_block": { 43 | "message": "Regla de bloqueo para $HOST$", 44 | "placeholders": { 45 | "host": { 46 | "content": "$1", 47 | "example": "ejemplo.com" 48 | } 49 | } 50 | }, 51 | "rule_title_whitelist": { 52 | "message": "Regla de lista blanca para $HOST$", 53 | "placeholders": { 54 | "host": { 55 | "content": "$1", 56 | "example": "ejemplo.com" 57 | } 58 | } 59 | }, 60 | "rule_title_new": { 61 | "message": "Nueva regla" 62 | }, 63 | "rule_description_new": { 64 | "message": "Crea una nueva regla de control de petición definiendo su patrón, tipo y acción." 65 | }, 66 | "rule_title_hosts": { 67 | "message": "$HOSTS$ y otros $NUM$", 68 | "placeholders": { 69 | "hosts": { 70 | "content": "$1", 71 | "example": "servidor1.com, servidor2.com, servidor3.com" 72 | }, 73 | "num": { 74 | "content": "$2", 75 | "example": "10" 76 | } 77 | } 78 | }, 79 | "rule_description_filter": { 80 | "message": "Filtra el redireccionamiento de URL" 81 | }, 82 | "rule_description_block": { 83 | "message": "Bloquea peticiones antes de que se efectúen." 84 | }, 85 | "rule_description_redirect": { 86 | "message": "Redirige peticiones a $URL$", 87 | "placeholders": { 88 | "url": { 89 | "content": "$1", 90 | "example": "http://ejemplo.com" 91 | } 92 | } 93 | }, 94 | "rule_description_whitelist": { 95 | "message": "Revoca otras reglas y procesa las peticiones con normalidad." 96 | }, 97 | "all_urls": { 98 | "message": "Cualquier URL" 99 | }, 100 | "activate_false": { 101 | "message": "Deshabilitar" 102 | }, 103 | "activate_true": { 104 | "message": "Habilitar" 105 | }, 106 | "show_more_false": { 107 | "message": "◂ Menos" 108 | }, 109 | "show_more_true": { 110 | "message": "Más ▸" 111 | }, 112 | "main_frame": { 113 | "message": "Document" 114 | }, 115 | "sub_frame": { 116 | "message": "Sub document" 117 | }, 118 | "stylesheet": { 119 | "message": "Stylesheet" 120 | }, 121 | "script": { 122 | "message": "Script" 123 | }, 124 | "image": { 125 | "message": "Image" 126 | }, 127 | "object": { 128 | "message": "Object" 129 | }, 130 | "object_subrequest": { 131 | "message": "Plugin" 132 | }, 133 | "xmlhttprequest": { 134 | "message": "XMLHttpRequest" 135 | }, 136 | "xslt": { 137 | "message": "XSLT" 138 | }, 139 | "ping": { 140 | "message": "Ping" 141 | }, 142 | "beacon": { 143 | "message": "Beacon" 144 | }, 145 | "xml_dtd": { 146 | "message": "XML DTD" 147 | }, 148 | "font": { 149 | "message": "Font" 150 | }, 151 | "media": { 152 | "message": "Media" 153 | }, 154 | "websocket": { 155 | "message": "WebSocket" 156 | }, 157 | "csp_report": { 158 | "message": "CSP report" 159 | }, 160 | "imageset": { 161 | "message": "Imageset" 162 | }, 163 | "web_manifest": { 164 | "message": "Web Manifest" 165 | }, 166 | "other": { 167 | "message": "Other" 168 | }, 169 | "options_title": { 170 | "message": "Reglas de Request Control" 171 | }, 172 | "options_description": { 173 | "message": "Define reglas para controlar peticiones HTTP" 174 | }, 175 | "settings_title": { 176 | "message": "Configuración de Request Control" 177 | }, 178 | "manual_title": { 179 | "message": "Manual de Request Control" 180 | }, 181 | "manual": { 182 | "message": "Manual" 183 | }, 184 | "about_title": { 185 | "message": "Acerca de Request Control" 186 | }, 187 | "about": { 188 | "message": "Acerca de" 189 | }, 190 | "see_manual": { 191 | "message": "Vea el manual." 192 | }, 193 | "create_new_rule": { 194 | "message": "Crear nueva regla" 195 | }, 196 | "new_rule": { 197 | "message": "Nueva regla" 198 | }, 199 | "edit": { 200 | "message": "Editar" 201 | }, 202 | "edit_rule": { 203 | "message": "Editar regla" 204 | }, 205 | "disable": { 206 | "message": "Deshabilitar" 207 | }, 208 | "pattern": { 209 | "message": "Patrón" 210 | }, 211 | "scheme": { 212 | "message": "esquema" 213 | }, 214 | "host": { 215 | "message": "servidor" 216 | }, 217 | "tlds": { 218 | "message": "TLDs" 219 | }, 220 | "path": { 221 | "message": "ruta" 222 | }, 223 | "top_level_domains": { 224 | "message": "Dominios de primer nivel" 225 | }, 226 | "types": { 227 | "message": "Tipos" 228 | }, 229 | "any_type": { 230 | "message": "Cualquier tipo" 231 | }, 232 | "action": { 233 | "message": "Acción" 234 | }, 235 | "filter": { 236 | "message": "Filtrado" 237 | }, 238 | "block": { 239 | "message": "Bloqueo" 240 | }, 241 | "redirect": { 242 | "message": "Redireccionamiento" 243 | }, 244 | "whitelist": { 245 | "message": "Lista blanca" 246 | }, 247 | "redirect_to": { 248 | "message": "Redirigir a" 249 | }, 250 | "manual_text_redirect": { 251 | "message": "Usar captura del patrón para redirigir en base a la petición original." 252 | }, 253 | "filter_url_redirection": { 254 | "message": "Filtrar redireccionamiento de URL" 255 | }, 256 | "trim_url_parameters": { 257 | "message": "Recortar parámetros de URL" 258 | }, 259 | "trim_all": { 260 | "message": "Recortar todos" 261 | }, 262 | "invert_trim": { 263 | "message": "Recortado inverso" 264 | }, 265 | "saved": { 266 | "message": "guardado" 267 | }, 268 | "save_rule": { 269 | "message": "regla de guardado" 270 | }, 271 | "remove": { 272 | "message": "Eliminar" 273 | }, 274 | "title_tlds": { 275 | "message": "Nombres de dominio de primer nivel incluidos" 276 | }, 277 | "redirect_url": { 278 | "message": "URL de redireccionamiento" 279 | }, 280 | "placeholder_trim_parameters": { 281 | "message": "Añadir nombres de parámetro a filtrar" 282 | }, 283 | "show_rules": { 284 | "message": "Mostrar reglas" 285 | }, 286 | "request_type": { 287 | "message": "Tipo de petición: $TYPE$", 288 | "placeholders": { 289 | "type": { 290 | "content": "$1", 291 | "example": "Document" 292 | } 293 | } 294 | }, 295 | "timestamp": { 296 | "message": "Marca de tiempo: $TIME$", 297 | "placeholders": { 298 | "time": { 299 | "content": "$1", 300 | "example": "18:32:21" 301 | } 302 | } 303 | }, 304 | "request_url": { 305 | "message": "URL de petición:" 306 | }, 307 | "copy_to_clipboard": { 308 | "message": "Copiar al portapapeles" 309 | }, 310 | "copied": { 311 | "message": "¡Copiado!" 312 | }, 313 | "new_target": { 314 | "message": "Nuevo objetivo:" 315 | }, 316 | "rules": { 317 | "message": "Reglas" 318 | }, 319 | "settings": { 320 | "message": "Configuración" 321 | }, 322 | "back_to_top": { 323 | "message": "Volver arriba" 324 | }, 325 | "export-file-name": { 326 | "message": "reglas-de-request-control.json" 327 | }, 328 | "export_selected": { 329 | "message": "Exportar seleccionadas" 330 | }, 331 | "remove_selected": { 332 | "message": "Eliminar seleccionadas" 333 | }, 334 | "test_selected": { 335 | "message": "Testar seleccionadas" 336 | }, 337 | "export_rules": { 338 | "message": "Exportar reglas" 339 | }, 340 | "export_description": { 341 | "message": "Exporta reglas a un fichero local." 342 | }, 343 | "export": { 344 | "message": "Exportar" 345 | }, 346 | "import_rules": { 347 | "message": "Importar reglas" 348 | }, 349 | "import_description": { 350 | "message": "Importa reglas a un fichero local." 351 | }, 352 | "contents": { 353 | "message": "Contenidos" 354 | }, 355 | "about_description": { 356 | "message": "Una WebExtension de Firefox para la administración de peticiones HTTP." 357 | }, 358 | "faq": { 359 | "message": "Preguntas frecuentes (FAQ)" 360 | }, 361 | "contributors": { 362 | "message": "Contribuidores" 363 | }, 364 | "source_code": { 365 | "message": "Código fuente" 366 | }, 367 | "changelog": { 368 | "message": "Registro de cambios" 369 | }, 370 | "license": { 371 | "message": "Licencia" 372 | }, 373 | "license_clause": { 374 | "message": "Request Control está licenciado bajo Mozilla Public License v2.0. Hay disponible una copia de la MPL en" 375 | }, 376 | "donate": { 377 | "message": "Donar" 378 | }, 379 | "browse_file": { 380 | "message": "Examinar..." 381 | }, 382 | "version": { 383 | "message": "Versión $VERSION$", 384 | "placeholders": { 385 | "version": { 386 | "content": "$1" 387 | } 388 | } 389 | }, 390 | "home_page": { 391 | "message": "Página principal" 392 | }, 393 | "name": { 394 | "message": "Nombre:" 395 | }, 396 | "description": { 397 | "message": "Descripción:" 398 | }, 399 | "set_tag": { 400 | "message": "Añadir etiqueta" 401 | }, 402 | "tag": { 403 | "message": "Etiqueta:" 404 | }, 405 | "test_url": { 406 | "message": "URL de testeo" 407 | }, 408 | "test_selected_rules": { 409 | "message": "Testar reglas seleccionadas" 410 | }, 411 | "invalid_test_url": { 412 | "message": "No es una URL de testeo válida" 413 | }, 414 | "no_match": { 415 | "message": "Sin coincidencia" 416 | }, 417 | "matched_no_change": { 418 | "message": "Coincidente, pero sin cambio" 419 | }, 420 | "whitelisted": { 421 | "message": "En lista blanca" 422 | }, 423 | "blocked": { 424 | "message": "Bloqueada" 425 | }, 426 | "invalid_target_url": { 427 | "message": "URL objetivo no válida: $URL$", 428 | "placeholders": { 429 | "url": { 430 | "content": "$1" 431 | } 432 | } 433 | }, 434 | "enable_rules": { 435 | "message": "Habilitar Request Control" 436 | }, 437 | "disable_rules": { 438 | "message": "Deshabilitar Request Control" 439 | } 440 | } 441 | -------------------------------------------------------------------------------- /test/redirect.test.js: -------------------------------------------------------------------------------- 1 | import { RedirectRule } from "../src/main/rules/redirect"; 2 | 3 | test("Static redirection url", () => { 4 | const request = "https://www.amazon.com/AmazonBasics-Type-C-USB-Male-Cable/dp/B01GGKYQ02/ref=sr_1_1?s=amazonbasics&srs=10112675011&ie=UTF8&qid=1489067885&sr=8-1&keywords=usb-c"; 5 | const target = "https://www.amazon.com/AmazonBasics-Type-C-USB-Male-Cable/dp/B01GGKYQ02/"; 6 | const redirectRule = new RedirectRule({ redirectUrl: "https://www.amazon.com/AmazonBasics-Type-C-USB-Male-Cable/dp/B01GGKYQ02/" }); 7 | expect(redirectRule.apply(request)).toBe(target); 8 | }); 9 | 10 | test("Pattern expansion - Single", () => { 11 | const request = "https://www.amazon.com/AmazonBasics-Type-C-USB-Male-Cable/dp/B01GGKYQ02/ref=sr_1_1?s=amazonbasics&srs=10112675011&ie=UTF8&qid=1489067885&sr=8-1&keywords=usb-c"; 12 | const target = "https://a.b/path/?s=amazonbasics&srs=10112675011&ie=UTF8&qid=1489067885&sr=8-1&keywords=usb-c"; 13 | const redirectRule = new RedirectRule({ redirectUrl: "https://a.b/path/{search}" }); 14 | expect(redirectRule.apply(request)).toBe(target); 15 | }); 16 | 17 | test("Pattern expansion - Multiple", () => { 18 | const request = "https://www.amazon.com/AmazonBasics-Type-C-USB-Male-Cable/dp/B01GGKYQ02/ref=sr_1_1?s=amazonbasics&srs=10112675011&ie=UTF8&qid=1489067885&sr=8-1&keywords=usb-c"; 19 | const target = "https://www.amazon.com/AmazonBasics-Type-C-USB-Male-Cable/dp/B01GGKYQ02/ref=sr_1_1?s=amazonbasics&srs=10112675011&ie=UTF8&qid=1489067885&sr=8-1&keywords=usb-c#myhash"; 20 | const redirectRule = new RedirectRule({ redirectUrl: "{protocol}//{host}{pathname}{search}#myhash" }); 21 | expect(redirectRule.apply(request)).toBe(target); 22 | }); 23 | 24 | test("Pattern expansion - Parameter not found", () => { 25 | const request = "https://www.amazon.com/AmazonBasics-Type-C-USB-Male-Cable/dp/B01GGKYQ02/ref=sr_1_1?s=amazonbasics&srs=10112675011&ie=UTF8&qid=1489067885&sr=8-1&keywords=usb-c"; 26 | const target = "https://a.b/path/{fail}"; 27 | const redirectRule = new RedirectRule({ redirectUrl: target }); 28 | expect(redirectRule.apply(request)).toBe(target); 29 | }); 30 | 31 | test("Substring replace pattern", () => { 32 | const request = "https://www.amazon.com/AmazonBasics-Type-C-USB-Male-Cable/dp/B01GGKYQ02/ref=sr_1_1?s=amazonbasics&srs=10112675011&ie=UTF8&qid=1489067885&sr=8-1&keywords=usb-c"; 33 | const target = "https://www.amazon.com/AmazonBasics-Type-C-USB-Male-Cable/my/path/B01GGKYQ02/ref=sr_1_1?s=amazonbasics&srs=10112675011&ie=UTF8&qid=1489067885&sr=8-1&keywords=usb-c#myhash"; 34 | const redirectRule = new RedirectRule({ redirectUrl: "{protocol}//{host}{pathname/dp/my/path}{search}#myhash" }); 35 | expect(redirectRule.apply(request)).toBe(target); 36 | }); 37 | 38 | test("Substring replace pattern - regexp capture groups", () => { 39 | let request, target, redirectRule; 40 | request = "https://www.amazon.com/AmazonBasics-Type-C-USB-Male-Cable/dp/B01GGKYQ02/ref=sr_1_1?s=amazonbasics&srs=10112675011&ie=UTF8&qid=1489067885&sr=8-1&keywords=usb-c"; 41 | target = "https://www.amazon.com/AmazonBasics-Type-C-USB-Male-Cable/foo/B01GGKYQ02/"; 42 | redirectRule = new RedirectRule({ redirectUrl: "{href/(.*?)\\/dp\\/(.*?)\\/ref=.*/$1/foo/$2/}" }); 43 | expect(redirectRule.apply(request)).toBe(target); 44 | }); 45 | 46 | test("Substring replace pattern - regexp replace", () => { 47 | let request, target, redirectRule; 48 | request = "https://www.dropbox.com/s/vm2mh2lkwsug4gt/rick_morty_at.png?dl=0"; 49 | target = "https://dl.dropboxusercontent.com/s/vm2mh2lkwsug4gt/rick_morty_at.png"; 50 | redirectRule = new RedirectRule({ redirectUrl: "{href/^https?:\\/\\/www\\.dropbox\\.com(\\/s\\/.+\\/.+)\\?dl=\\d$/https://dl.dropboxusercontent.com$1}" }); 51 | expect(redirectRule.apply(request)).toBe(target); 52 | }); 53 | 54 | test("Substring replace pattern - replace all occurences", () => { 55 | let request, target, redirectRule; 56 | request = "http://foo.com/foo/foo?foo=bar#foo"; 57 | target = "http://bar.com/bar/bar?bar=bar#bar"; 58 | redirectRule = new RedirectRule({ redirectUrl: "{href//foo/bar}" }); 59 | expect(redirectRule.apply(request)).toBe(target); 60 | }); 61 | 62 | test("Substring replace pattern - replace backslash", () => { 63 | let request, target, redirectRule; 64 | request = "http:\\/\\/foo.com\\/foo\\/"; 65 | target = "http://foo.com/foo/"; 66 | redirectRule = new RedirectRule({ redirectUrl: "{href//\\\\+/}" }); 67 | expect(redirectRule.apply(request)).toBe(target); 68 | }); 69 | 70 | test("Substring replace pattern - replace all combined", () => { 71 | let request, target, redirectRule; 72 | request = "http://track.steadyhq.com/track/click/12345678/steadyhq.com?p=eyJzIjoidDJLdmg0NVV4MUJkNFh3N3lrUkR6djRMWlJNIiwidiI6MSwicCI6IntcInVcIjoxMjM0NTY3OCxcInZcIjoxLFwidXJsXCI6XCJodHRwczpcXFwvXFxcL3N0ZWFkeWhxLmNvbVxcXC9wcm9qZWN0XFxcL3Bvc3RzXFxcLzlmYjYwMWU0LWIzMjctNGY2YS01NzljLWU1NGM4NDE1YmY0YlwiLFwiaWRcIjpcIjZmOTNmMTZhYWE0NTQyYjk2M2M2NjEwOGMwZTk4ZjJcIixcInVybF9pZHNcIjpbXCI2OWExOTZiYWIzODNmN2YyZDcxMDQxMjA3MWQ5NjhmNmRiYmVlMDQ4XCJdfSJ9"; 73 | target = "https://steadyhq.com/project/posts/9fb601e4-b327-4f6a-579c-e54c8415bf4b"; 74 | redirectRule = new RedirectRule({ redirectUrl: "{search.p|decodeBase64|/.*\"(http.*?)\".*/$1|//\\\\+/}" }); 75 | expect(redirectRule.apply(request)).toBe(target); 76 | }); 77 | 78 | test("Substring replace pattern - regexp repetition quantifiers ", () => { 79 | let request, target, redirectRule; 80 | request = "https://i.imgur.com/cijC2a2l.jpg"; 81 | target = "https://i.imgur.com/cijC2a2.jpg"; 82 | redirectRule = new RedirectRule({ redirectUrl: "{origin}{pathname/l\\.([a-zA-Z]{3,4})$/.$1}{search}{hash}" }); 83 | expect(redirectRule.apply(request)).toBe(target); 84 | 85 | request = "https://example.com/?1234"; 86 | target = "https://example.com/"; 87 | redirectRule = new RedirectRule({ redirectUrl: "{origin}{pathname}{search/(?:[?&]\\d{4,}?(?=$|[?#])|([?&])\\d{4,}?[?&])/$1}{hash}" }); 88 | expect(redirectRule.apply(request)).toBe(target); 89 | }); 90 | 91 | test("Substring extraction pattern", () => { 92 | let request, target, redirectRule; 93 | request = "https://foo.bar/path"; 94 | target = "http://bar.com/some/new/path"; 95 | redirectRule = new RedirectRule({ redirectUrl: "{protocol:0:4}://{host:-3}.com/some/new/path" }); 96 | expect(redirectRule.apply(request)).toBe(target); 97 | }); 98 | 99 | test("String manipulations combined", () => { 100 | let request, target, redirectRule; 101 | request = "http://foo.bar.co.uk/path"; 102 | target = "https://bar.com/some/new/path"; 103 | redirectRule = new RedirectRule({ redirectUrl: "https://{host::-3|/\\.co$/.com|:4}/some/new/path" }); 104 | expect(redirectRule.apply(request)).toBe(target); 105 | }); 106 | 107 | test("String manipulations combined - Bad manipulation", () => { 108 | let request, target, redirectRule; 109 | request = "http://foo.bar.co.uk/path"; 110 | target = "https://foo.bar.com/some/new/path"; 111 | redirectRule = new RedirectRule({ redirectUrl: "https://{host::-3|/\\.co$/.com|fail}/some/new/path" }); 112 | expect(redirectRule.apply(request)).toBe(target); 113 | }); 114 | 115 | test("Redirect instructions - single", () => { 116 | let request, target, redirectRule; 117 | request = "http://foo.bar.co.uk/path"; 118 | target = "http://foo.bar.co.uk:8080/path"; 119 | redirectRule = new RedirectRule({ redirectUrl: "[port=8080]" }); 120 | expect(redirectRule.apply(request)).toBe(target); 121 | }); 122 | 123 | test("Redirect instructions - multiple", () => { 124 | let request, target, redirectRule; 125 | request = "http://foo.bar.co.uk/path"; 126 | target = "http://localhost:8080/path"; 127 | redirectRule = new RedirectRule({ redirectUrl: "[host=localhost][port=8080]" }); 128 | expect(redirectRule.apply(request)).toBe(target); 129 | }); 130 | 131 | test("Redirect instructions - combined", () => { 132 | let request, target, redirectRule; 133 | request = "http://foo.bar.co.uk/path"; 134 | target = "https://bar.com:1234/some/new/path?foo=bar#foobar"; 135 | redirectRule = new RedirectRule({ redirectUrl: "ht[port=1234]tps://{host::-3|/\\.co$/.com|:4}/some/new/path[hash=foobar][search=foo=bar]" }); 136 | expect(redirectRule.apply(request)).toBe(target); 137 | }); 138 | 139 | test("Redirect instructions - combined 2", () => { 140 | let request, target, redirectRule; 141 | request = "http://foo.com/path"; 142 | target = "https://bar.com/path#myhash=com"; 143 | redirectRule = new RedirectRule({ redirectUrl: "[protocol=https][hash=myhash={host:-3}][host={host/foo/bar}]" }); 144 | expect(redirectRule.apply(request)).toBe(target); 145 | }); 146 | 147 | test("Redirect instructions - hostname", () => { 148 | let request, target, redirectRule; 149 | request = "https://en.m.wikipedia.org/wiki/Main_Page"; 150 | target = "https://en.wikipedia.org/wiki/Main_Page"; 151 | redirectRule = new RedirectRule({ redirectUrl: "[hostname={hostname/\\.m\\./.}]" }); 152 | expect(redirectRule.apply(request)).toBe(target); 153 | }); 154 | 155 | test("Redirect instructions - hostname replace pattern", () => { 156 | let request, target, redirectRule; 157 | request = "https://en.m.wikipedia.org/wiki/Main_Page"; 158 | target = "https://en.wikipedia.org/wiki/Main_Page"; 159 | redirectRule = new RedirectRule({ redirectUrl: "[hostname={hostname/\\.[mi]\\./.}]" }); 160 | expect(redirectRule.apply(request)).toBe(target); 161 | }); 162 | 163 | test("Capture Search Parameter - found", () => { 164 | const request = "http://go.redirectingat.com/?xs=1&id=xxxxxxx&sref=http%3A%2F%2Fwww.vulture.com%2F2018%2F05%2Fthe-end-of-nature-at-storm-king-art-center-in-new-york.html&xcust=xxxxxxxx&url=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FNathaniel_Parker_Willis"; 165 | const target = "https://en.wikipedia.org/wiki/Nathaniel_Parker_Willis"; 166 | const redirectRule = new RedirectRule({ redirectUrl: "{search.url|decodeURIComponent}" }); 167 | expect(redirectRule.apply(request)).toBe(target); 168 | }); 169 | 170 | test("Set Search Parameter", () => { 171 | const request = "http://example.com/?query=none"; 172 | const target = "http://example.com/?query=set"; 173 | const redirectRule = new RedirectRule({ redirectUrl: "[search.query=set]" }); 174 | expect(redirectRule.apply(request)).toBe(target); 175 | }); 176 | 177 | test("Set Search Parameter - encode", () => { 178 | const request = "http://example.com/?query=none"; 179 | const target = "http://example.com/?query=http%3A%2F%2Fexample.com"; 180 | const redirectRule = new RedirectRule({ redirectUrl: "[search.query={origin|encodeURIComponent}]" }); 181 | expect(redirectRule.apply(request)).toBe(target); 182 | }); 183 | 184 | test("Capture Search Parameter - not found", () => { 185 | const request = "http://go.redirectingat.com/?xs=1&id=xxxxxxx&sref=http%3A%2F%2Fwww.vulture.com%2F2018%2F05%2Fthe-end-of-nature-at-storm-king-art-center-in-new-york.html&xcust=xxxxxxxx&url=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FNathaniel_Parker_Willis"; 186 | const redirectRule = new RedirectRule({ redirectUrl: "{search.foo|decodeURIComponent}" }); 187 | expect(redirectRule.apply(request)).toBe(""); 188 | }); 189 | -------------------------------------------------------------------------------- /test/filter.test.js: -------------------------------------------------------------------------------- 1 | import { FilterRule } from "../src/main/rules/filter"; 2 | import { parseInlineUrl, trimQueryParameters } from "../src/main/url"; 3 | import { createRegexpPattern } from "../src/util/regexp"; 4 | 5 | test("Filter inline url redirection", () => { 6 | const request = "http://foo.com/click?p=240631&a=2314955&g=21407340&url=http%3A%2F%2Fbar.com%2Fkodin-elektroniikka%2Fintel-core-i7-8700k-3-7-ghz-12mb-socket-1151-p41787528"; 7 | const target = "http://bar.com/kodin-elektroniikka/intel-core-i7-8700k-3-7-ghz-12mb-socket-1151-p41787528"; 8 | expect(new FilterRule().apply(request)).toBe(target); 9 | }); 10 | 11 | test("Skip inline url filtering", () => { 12 | const request = "http://foo.com/click?p=240631&a=2314955&g=21407340&url=http%3A%2F%2Fbar.com%2Fkodin-elektroniikka%2Fintel-core-i7-8700k-3-7-ghz-12mb-socket-1151-p41787528%3Futm_source%3Dmuropaketti%26utm_medium%3Dcpc%26utm_campaign%3Dmuropaketti"; 13 | const target = "http://foo.com/click?p=240631&a=2314955&g=21407340"; 14 | expect(new FilterRule({ 15 | paramsFilter: { values: ["url"] }, 16 | skipRedirectionFilter: true 17 | }).apply(request)).toBe(target); 18 | }); 19 | 20 | test("Skip inline url filtering on same domain", () => { 21 | const request = "http://foo.com/click?p=240631&a=2314955&g=21407340&url=http%3A%2F%2Ffoo.com%2Fkodin-elektroniikka%2Fintel-core-i7-8700k-3-7-ghz-12mb-socket-1151-p41787528%3Futm_source%3Dmuropaketti%26utm_medium%3Dcpc%26utm_campaign%3Dmuropaketti"; 22 | expect(new FilterRule({ 23 | skipOnSameDomain: true 24 | }).apply(request)).toBe(request); 25 | }); 26 | 27 | test("Filter inline url redirection - trim query params before", () => { 28 | const request = "http://foo.com/click?p=240631&a=2314955&g=21407340&url=http%3A%2F%2Ffoo.com%2Fkodin-elektroniikka%2Fintel-core-i7-8700k-3-7-ghz-12mb-socket-1151-p41787528%3Futm_source%3Dmuropaketti%26utm_medium%3Dcpc%26utm_campaign%3Dmuropaketti"; 29 | const target = "http://foo.com/click?p=240631&a=2314955&g=21407340"; 30 | expect(new FilterRule({ 31 | paramsFilter: { values: ["url"] } 32 | }).apply(request)).toBe(target); 33 | }); 34 | 35 | test("Filter inline url redirection - query params trimming not applied on parsed inline url", () => { 36 | const request = "http://foo.com/?adobeRef=62716f6088c511e987842211e560f7e80001&sdtid=13131712&sdop=1&sdpid=128047819&sdfid=9&sdfib=1&lno=1&trd=https%20play%20google%20com%20store%20app%20&pv=&au=&u2=https%3A%2F%2Fplay.google.com%2Fstore%2Fapps%2Fdetails%3Fid%3Dcom.pockettrend.neomonsters"; 37 | const target = "https://play.google.com/store/apps/details?id=com.pockettrend.neomonsters"; 38 | expect(new FilterRule({ 39 | paramsFilter: { 40 | values: ["u2"], 41 | invert: true 42 | } 43 | }).apply(request)).toBe(target); 44 | }); 45 | 46 | test("Trim query parameters", () => { 47 | let request, target, filterRule; 48 | 49 | filterRule = new FilterRule({ paramsFilter: { values: ["utm_*", "feature", "parameter"] } }); 50 | request = "https://www.youtube.com/watch?v=yWtFGtIlzyQ&feature=em-uploademail?parameter&utm_source&key=value?utm_medium=abc¶meter&utm_term?key=value&utm_medium=abc"; 51 | target = "https://www.youtube.com/watch?v=yWtFGtIlzyQ?key=value?key=value"; 52 | expect(filterRule.apply(request)).toBe(target); 53 | 54 | filterRule = new FilterRule({ paramsFilter: { values: ["utm_source", "utm_medium"] } }); 55 | request = "https://www.ghacks.net/2017/04/30/firefox-nightly-marks-legacy-add-ons/?utm_source=feedburner&utm_medium=feed"; 56 | target = "https://www.ghacks.net/2017/04/30/firefox-nightly-marks-legacy-add-ons/"; 57 | expect(filterRule.apply(request)).toBe(target); 58 | 59 | filterRule = new FilterRule({ 60 | paramsFilter: { 61 | values: ["ws_ab_test", "btsid", "algo_expid", "algo_pvid"] 62 | } 63 | }); 64 | request = "https://www.aliexpress.com/item/Xiaomi-Mini-Router-2-4GHz-5GHz-Dual-Band-Max-1167Mbps-Support-Wifi-802-11ac-Xiaomi-Mi/32773978417.html?ws_ab_test=searchweb0_0,searchweb201602_3_10152_10065_10151_10068_436_10136_10137_10157_10060_10138_10155_10062_10156_10154_10056_10055_10054_10059_10099_10103_10102_10096_10169_10147_10052_10053_10142_10107_10050_10051_9985_10084_10083_10080_10082_10081_10110_10111_10112_10113_10114_10181_10183_10182_10078_10079_10073_10070_10123-9985,searchweb201603_2,ppcSwitch_5&btsid=3f9443f8-38ad-472c-b6a6-00b8a3db74a3&algo_expid=8f505cf2-0671-4c52-b976-d7a3169da8bc-6&algo_pvid=8f505cf2-0671-4c52-b976-d7a3169da8bc"; 65 | target = "https://www.aliexpress.com/item/Xiaomi-Mini-Router-2-4GHz-5GHz-Dual-Band-Max-1167Mbps-Support-Wifi-802-11ac-Xiaomi-Mi/32773978417.html"; 66 | expect(filterRule.apply(request)).toBe(target); 67 | 68 | filterRule = new FilterRule({ paramsFilter: { values: ["sid"] } }); 69 | request = "http://forums.mozillazine.org/viewtopic.php?sid=6fa91cda58212e7e869dc2022b9e6217&f=48&t=1920191"; 70 | target = "http://forums.mozillazine.org/viewtopic.php?f=48&t=1920191"; 71 | expect(filterRule.apply(request)).toBe(target); 72 | request = "http://forums.mozillazine.org/viewtopic.php?f=48&sid=6fa91cda58212e7e869dc2022b9e6217&t=1920191"; 73 | expect(filterRule.apply(request)).toBe(target); 74 | request = "http://forums.mozillazine.org/viewtopic.php?f=48&t=1920191&sid=6fa91cda58212e7e869dc2022b9e6217"; 75 | expect(filterRule.apply(request)).toBe(target); 76 | }); 77 | 78 | test("Trim query parameters (inverted)", () => { 79 | const request = "https://www.youtube.com/watch?v=yWtFGtIlzyQ&feature=em-uploademail?parameter&utm_source&key=value?utm_medium=abc¶meter&utm_term?key=value&utm_medium=abc"; 80 | const target = "https://www.youtube.com/watch?feature=em-uploademail?parameter&utm_source?utm_medium=abc¶meter&utm_term?utm_medium=abc"; 81 | expect(new FilterRule({ 82 | paramsFilter: { 83 | values: ["utm_*", "feature", "parameter"], 84 | invert: true 85 | } 86 | }).apply(request)).toBe(target); 87 | }); 88 | 89 | test("Remove all query parameters", () => { 90 | const request = "https://www.youtube.com/watch?v=yWtFGtIlzyQ&feature=em-uploademail#hash"; 91 | const target = "https://www.youtube.com/watch#hash"; 92 | expect(new FilterRule({ trimAllParams: true }).apply(request)).toBe(target); 93 | }); 94 | 95 | test("Filter inline url redirection - trim parameters before inline url parsing", () => { 96 | const request = "http://go.redirectingat.com/?xs=1&id=xxxxxxx&sref=http%3A%2F%2Fwww.vulture.com%2F2018%2F05%2Fthe-end-of-nature-at-storm-king-art-center-in-new-york.html&xcust=xxxxxxxx&url=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FNathaniel_Parker_Willis"; 97 | const target = "https://en.wikipedia.org/wiki/Nathaniel_Parker_Willis"; 98 | expect(new FilterRule({ paramsFilter: { values: ["sref"] } }).apply(request)).toBe(target); 99 | }); 100 | 101 | test("Inline url parsing", () => { 102 | expect( 103 | parseInlineUrl("https://steamcommunity.com/linkfilter/?url=https://addons.mozilla.org/") 104 | ).toBe("https://addons.mozilla.org/"); 105 | expect( 106 | parseInlineUrl("https://outgoing.prod.mozaws.net/v1/ca408bc92003166eec54f20e68d7c771ae749b005b72d054ada33f0ef261367d/https%3A//github.com/tumpio/requestcontrol") 107 | ).toBe("https://github.com/tumpio/requestcontrol"); 108 | expect( 109 | parseInlineUrl("http://www.deviantart.com/users/outgoing?http://foobar2000.org") 110 | ).toBe("http://foobar2000.org"); 111 | expect( 112 | parseInlineUrl("https://www.site2.com/chrome/?i-would-rather-use-firefox=https%3A%2F%2Fwww.mozilla.org/") 113 | ).toBe("https://www.mozilla.org/"); 114 | expect(parseInlineUrl("https://site.com/away.php?to=https://github.com&cc_key=")).toBe("https://github.com"); 115 | expect( 116 | parseInlineUrl("https://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd=1&ved=0ahUKEwiGvaeL1-HTAhUBP5oKHfDoDqQQFggrMAA&url=https%3A%2F%2Faddons.mozilla.org%2F&usg=AFQjCNGoTdPVJJYDmaDkKoFSpuasv6HVCg&cad=rjt") 117 | ).toBe("https://addons.mozilla.org/"); 118 | expect( 119 | parseInlineUrl("https://l.facebook.com/l.php?u=https%3A%2F%2Fwww.fsf.org%2Fcampaigns%2F&h=ATP1kf98S0FxqErjoW8VmdSllIp4veuH2_m1jl69sEEeLzUXbkNXrVnzRMp65r5vf21LJGTgJwR2b66m97zYJoXx951n-pr4ruS1osMvT2c9ITsplpPU37RlSqJsSgba&s=1") 120 | ).toBe("https://www.fsf.org/campaigns/"); 121 | expect( 122 | parseInlineUrl("https://out.reddit.com/t3_5pq7qd?url=https%3A%2F%2Finternethealthreport.org%2Fv01%2F&token=AQAAZV6JWHBBnIcVjV1wvxVg5gKyCQQSdUhGIvuEUmdPZhxhm8kH&app_name=reddit.com") 123 | ).toBe("https://internethealthreport.org/v01/"); 124 | expect( 125 | parseInlineUrl("http://site3.com/?r=https%3A%2F%2Fwww.yr.no%2Fplace%2FNorway%2FNordland%2FBr%C3%B8nn%C3%B8y%2FBr%C3%B8nn%C3%B8ysund%2Fhour_by_hour.html?key=ms&ww=51802") 126 | ).toBe( 127 | "https://www.yr.no/place/Norway/Nordland/Brønnøy/Brønnøysund/hour_by_hour.html" 128 | ); 129 | expect( 130 | parseInlineUrl("http://www.deviantart.com/users/outgoing?https://scontent.ftpa1-1.fna.fbcdn.net/v/t1.0-9/19437615_10154946431942669_5896185388243732024_n.jpg?oh=f7eb69d10ee9217944c18955d3a631ad&oe=5A0F78B4") 131 | ).toBe( 132 | "https://scontent.ftpa1-1.fna.fbcdn.net/v/t1.0-9/19437615_10154946431942669_5896185388243732024_n.jpg?oh=f7eb69d10ee9217944c18955d3a631ad&oe=5A0F78B4" 133 | ); 134 | expect( 135 | parseInlineUrl("http://site.com/?r=https&foo=bar%3A%2F%2Fwww.yr.no%2Fplace%2FNorway%2FNordland%2FBr%C3%B8nn%C3%B8y%2FBr%C3%B8nn%C3%B8ysund%2Fhour_by_hour.html?key=ms&ww=51802") 136 | ).toBe(null); 137 | expect(parseInlineUrl("http://site.com/?r=www.example.com")).toBe(null); 138 | }); 139 | 140 | test("Query parameter trimming", () => { 141 | expect(trimQueryParameters( 142 | "http://site.com/?parameter&utm_source&key=value?utm_medium=abc¶meter&utm_term?key=value&utm_medium=abc", 143 | createRegexpPattern(["utm_source", "utm_medium", "utm_term", "utm_content", "utm_campaign", 144 | "utm_reader", "utm_place"]) 145 | )).toBe("http://site.com/?parameter&key=value?parameter?key=value"); 146 | expect(trimQueryParameters( 147 | "http://site.com/?parameter&utm_source&key=value?utm_medium=abc¶meter&utm_term?key=value&utm_medium=abc", 148 | createRegexpPattern(["utm_medium", "utm_term", "utm_content", "utm_campaign", 149 | "utm_reader", "utm_place"]) 150 | )).toBe("http://site.com/?parameter&utm_source&key=value?parameter?key=value"); 151 | expect(trimQueryParameters( 152 | "http://site.com/??utm_source¶meter&utm_source&key=value???utm_medium=abc¶meter&utm_term?key=value&utm_medium=abc", 153 | createRegexpPattern(["utm_source", "utm_medium", "utm_term", "utm_content", "utm_campaign", 154 | "utm_reader", "utm_place"]) 155 | )).toBe("http://site.com/??parameter&key=value???parameter?key=value"); 156 | expect(trimQueryParameters( 157 | "http://site.com/??utm_source¶meter&utm_source&key=value???utm_medium=abc¶meter&utm_term?key=value&utm_medium=abc", 158 | createRegexpPattern(["u?m_*"]) 159 | )).toBe("http://site.com/??parameter&key=value???parameter?key=value"); 160 | expect(trimQueryParameters( 161 | "http://site.com/?parameter&utm_source&key=value?utm_medium=abc¶meter&utm_term?key=value&utm_medium=abc", 162 | createRegexpPattern(["utm_source", "utm_medium", "utm_term", "utm_content", "utm_campaign", 163 | "utm_reader", "utm_place"]), true 164 | )).toBe("http://site.com/?utm_source?utm_medium=abc&utm_term?utm_medium=abc"); 165 | expect(trimQueryParameters( 166 | "http://site.com/?parameter&utm_source&key=value?utm_medium=abc¶meter&utm_term?key=value&utm_medium=abc", 167 | createRegexpPattern(["/[parmetr]+/"]), true 168 | )).toBe("http://site.com/?parameter?parameter"); 169 | expect(trimQueryParameters( 170 | "http://site.com/?parameter&utm_source&key=value?utm_medium=abc¶meter&utm_term?key=value&utm_medium=abc", 171 | createRegexpPattern(["/..._.{5,}/"]), false 172 | )).toBe("http://site.com/?parameter&key=value?parameter&utm_term?key=value"); 173 | expect(trimQueryParameters( 174 | "http://site.com/?", 175 | createRegexpPattern(["?"]), true 176 | )).toBe("http://site.com/?"); 177 | }); 178 | --------------------------------------------------------------------------------