diff --git a/.eslintrc.json b/.eslintrc.json index 34bf2d87..22a593cb 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -8,12 +8,15 @@ "es6": true, "webextensions": true }, + "globals": { + "globalThis": false + }, "extends": [ "eslint:recommended" ], "rules": { "no-console": 0, - "no-unused-vars": ["warn", { "vars": "all", "args": "all" } ], + "no-unused-vars": ["warn", { "vars": "all", "args": "after-used" } ], "no-undef": ["warn"], "no-proto": ["error"], "prefer-arrow-callback": ["warn"], diff --git a/dnr-block-only/README.md b/dnr-block-only/README.md new file mode 100644 index 00000000..94916c3d --- /dev/null +++ b/dnr-block-only/README.md @@ -0,0 +1,42 @@ +# dnr-block-only + +Demonstrates how to block network requests without host permissions using the +declarativeNetRequest API with the `declarative_net_request` manifest key. + +## What it does + +This extension blocks: + +- network requests to URLs containing "blocksub" (except for top-level + navigations). +- top-level navigation to URLs containing "blocktop". +- all requests containing "blockall". + +Load `testpage.html` to see the extension in action. +This demo page does not need to be packaged with the extension. + +# What it shows + +This example shows how to: + +- use the declarativeNetRequest API through the `declarative_net_request` + manifest key. +- use the "resourceTypes" and "excludedResourceTypes" conditions of a + declarativeNetRequest rule. +- block network requests without host permissions using the + "declarativeNetRequest" permission, which triggers the "Block content on any + page" permission warning at install time. + +This example is the only cross-browser way to block network requests (at least +in Firefox, Chrome, and Safari). The webRequest API is an alternative way to +implement this functionality, but is only available in Firefox (MV2 and MV3) +and in Chrome (MV2 only). Safari does not support the webRequest API. + +## Comparison with Manifest Version 2 + +While this example uses `"manifest_version": 3`, the functionality is not +specific to Manifest Version 3. + +To create a MV2 version of the extension, modify `manifest.json` as follows: + +- Set `manifest_version` to 2. diff --git a/dnr-block-only/manifest.json b/dnr-block-only/manifest.json new file mode 100644 index 00000000..ed039746 --- /dev/null +++ b/dnr-block-only/manifest.json @@ -0,0 +1,16 @@ +{ + "manifest_version": 3, + "name": "Block only, without host_permissions", + "description": "Blocks requests to 'blocksub', 'blocktop', and 'blockall'. Uses the 'declarativeNetRequest' permission, meaning that host_permissions in manifest.json are not needed.", + "version": "0.1", + "permissions": [ + "declarativeNetRequest" + ], + "declarative_net_request": { + "rule_resources": [{ + "id": "ruleset", + "enabled": true, + "path": "rules.json" + }] + } +} diff --git a/dnr-block-only/rules.json b/dnr-block-only/rules.json new file mode 100644 index 00000000..5d87193c --- /dev/null +++ b/dnr-block-only/rules.json @@ -0,0 +1,31 @@ +[ + { + "id": 1, + "condition": { + "urlFilter": "blocksub" + }, + "action": { + "type": "block" + } + }, + { + "id": 2, + "condition": { + "urlFilter": "blocktop", + "resourceTypes": ["main_frame"] + }, + "action": { + "type": "block" + } + }, + { + "id": 3, + "condition": { + "urlFilter": "blockall", + "excludedResourceTypes": [] + }, + "action": { + "type": "block" + } + } +] diff --git a/dnr-block-only/testpage.html b/dnr-block-only/testpage.html new file mode 100644 index 00000000..02b1f54f --- /dev/null +++ b/dnr-block-only/testpage.html @@ -0,0 +1,97 @@ + + + + + + + + +

Block requests containing 'blocksub' (except main_frame)

+Given the following rule: +
+  {
+    "id": 1,
+    "condition": {
+      "urlFilter": "blocksub"
+    },
+    "action": {
+      "type": "block"
+    }
+  },
+
+
+ rule 1 will block this image (containing 'blocksub'): + +
+
+ rule 1 will not block this image (does not contain 'blocksub'): + +
+
+ rule 1 will not block top-level navigations to + https://developer.mozilla.org/favicon.ico?blocksub + because top-level navigation ("main_frame") requests are not matched + when "resourceTypes" and "excludedResourceTypes" are not specified. +
+ +

Block main_frame requests containing 'blocktop'

+Given the following rule: +
+  {
+    "id": 2,
+    "condition": {
+      "urlFilter": "blocktop",
+      "resourceTypes": ["main_frame"]
+    },
+    "action": {
+      "type": "block"
+    }
+  },
+
+
+ rule 2 will block top-level navigation to + https://developer.mozilla.org/favicon.ico?blocktop + because the "resourceTypes" array contains "main_frame". +
+
+ rule 2 will not block this image (containing 'blocktop') + + because "image" is not in the "resourceTypes" array. +
+ +

Block all requests containing 'blockall'

+Given the following rule: +
+  {
+    "id": 3,
+    "condition": {
+      "urlFilter": "blockall",
+      "excludedResourceTypes": []
+    },
+    "action": {
+      "type": "block"
+    }
+  }
+
+
+ rule 3 will block this image (containing 'blockall'): + +
+
+ rule 3 will block top-level navigation to + https://developer.mozilla.org/favicon.ico?blockall + because "excludedResourceTypes" is set to an empty array. +
+ Note: not blocked in Chrome due to https://crbug.com/1432871. +
+ + + diff --git a/dnr-dynamic-with-options/README.md b/dnr-dynamic-with-options/README.md new file mode 100644 index 00000000..993ca1b2 --- /dev/null +++ b/dnr-dynamic-with-options/README.md @@ -0,0 +1,105 @@ +# dnr-dynamic-with-options + +Demonstrates a generic way to request host permissions and register +declarativeNetRequest rules to modify network requests, without any +install-time permission warnings. The `options_ui` page offers a way to request +permissions and register declarative net request (DNR) rules. + +## What it does + +After loading the extension, visit the extension options page: + +1. Visit `about:addons`. +2. Go to the extension at "DNR Dynamic with options". +3. Click on Preferences to view its options page (options.html). + +On the options page: + +1. Input the list of host permissions and click on "Grant host permissions". +2. Input the list of declarativeNetRequest rules and click "Save". +3. Trigger a network request to verify that the rule matched. + +### Example for options page + +Host permissions: + +```json +["*://example.com/"] +``` + +DNR rules: + +```json +[ + { + "id": 1, + "priority": 1, + "condition": { + "urlFilter": "|https://example.com/", + "resourceTypes": [ + "main_frame" + ] + }, + "action": { + "type": "block" + } + } +] +``` + +Manual test case: Visit https://example.com/ and verify that it is blocked. + +# What it shows + +How to create an extension with no install-time permission warnings and +request (host) permissions as needed: + +- declares the "declarativeNetRequestWithHostAccess" permission, which + unlocks the declarativeNetRequest API without install-time warning. + In contrast, the "declarativeNetRequest" permission has the same effect, + but has the "Block content on any page" permission warning. +- declares the most permissive match pattern in `optional_host_permissions`. +- calls `permissions.request` to request host permissions. +- uses `permissions.getAll` and `permissions.remove` to reset permissions. + +How to retrieve and dynamically register declarativeNetRequest rules, using: + +- `declarativeNetRequest.getDynamicRules` and + `declarativeNetRequest.updateDynamicRules` to manage DNR rules that persist + across extension restarts. These rules also persist across browser restarts, + unless the extension is loaded temporarily or unloaded. +- `declarativeNetRequest.getSessionRules` and + `declarativeNetRequest.updateSessionRules` to manage DNR rules that are + session-scoped, that is, cleared when an extension unloads or the browser + quits. + +How these registered DNR rules can modify network requests without requiring an +active extension script in the background, in a cross-browser way (at least in +Firefox, Chrome, and Safari). + +## Note on `optional_host_permissions` and `optional_permissions` + +Firefox does not support `optional_host_permissions` permissions, it +supports host permissions in `optional_permissions` +(https://bugzilla.mozilla.org/show_bug.cgi?id=1766026). + +Chrome recognizes `optional_host_permissions` but does not support host +permissions in `optional_permissions`. + +To support both, include `optional_host_permissions` and `optional_permissions` +in your manifest.json. + +## Comparison with Manifest Version 2 + +While this example uses `"manifest_version": 3`, the functionality is not +specific to Manifest Version 3. + +To create a MV2 version of the extension, modify `manifest.json` as follows: + +- Set `manifest_version` to 2. +- Use `optional_permissions` instead of `optional_host_permissions` to list + optional host permissions. + - In this example, `optional_permissions` is present with + the same value as `optional_host_permissions` for the reasons explained in + the previous section. The latter is MV3-only and can be removed from a MV2 + manifest. diff --git a/dnr-dynamic-with-options/manifest.json b/dnr-dynamic-with-options/manifest.json new file mode 100644 index 00000000..bc16e834 --- /dev/null +++ b/dnr-dynamic-with-options/manifest.json @@ -0,0 +1,12 @@ +{ + "manifest_version": 3, + "name": "DNR dynamic with options", + "description": "Modify requests according to the rules specified by the user in the options page.", + "version": "0.1", + "permissions": ["declarativeNetRequestWithHostAccess"], + "optional_host_permissions": ["*://*/"], + "optional_permissions": ["*://*/"], + "options_ui": { + "page": "options.html" + } +} diff --git a/dnr-dynamic-with-options/options.css b/dnr-dynamic-with-options/options.css new file mode 100644 index 00000000..254fe16e --- /dev/null +++ b/dnr-dynamic-with-options/options.css @@ -0,0 +1,8 @@ +.input-and-buttons legend { + font-weight: bold; +} +.input-and-buttons textarea { + display: block; + width: 100%; + min-height: 7em; +} diff --git a/dnr-dynamic-with-options/options.html b/dnr-dynamic-with-options/options.html new file mode 100644 index 00000000..bea7c417 --- /dev/null +++ b/dnr-dynamic-with-options/options.html @@ -0,0 +1,36 @@ + + + + + + + + +
+ Allowed host permissions + Specify the JSON-formatted list of allowed host permissions (documentation: origins). + + + + +
+ +
+ Dynamic declarativeNetRequest rules (persists across restarts) + Specify the JSON-formatted list of dynamic DNR rules (documentation: declarativeNetRequest rules). + + + +
+ +
+ Session-scoped declarativeNetRequest rules (cleared on extension unload/reload) + Specify the JSON-formatted list of session DNR rules (documentation: declarativeNetRequest rules). + + + +
+ + + + diff --git a/dnr-dynamic-with-options/options.js b/dnr-dynamic-with-options/options.js new file mode 100644 index 00000000..ab1471a0 --- /dev/null +++ b/dnr-dynamic-with-options/options.js @@ -0,0 +1,90 @@ +"use strict"; + +if (typeof browser == "undefined") { + // `browser` is not defined in Chrome, but Manifest V3 extensions in Chrome + // also support promises in the `chrome` namespace, like Firefox. To easily + // test the example without modifications, polyfill "browser" to "chrome". + globalThis.browser = chrome; +} + +function initializePrefHandlerForHostPermissions() { + const textarea = document.getElementById("input-host-permissions"); + const statusOutput = document.getElementById("status-host-permissions"); + document.getElementById("grant-host-permissions").onclick = async () => { + try { + let origins = JSON.parse(textarea.value); + statusOutput.value = "Requesting permissions"; + let ok = await browser.permissions.request({ origins }); + statusOutput.value = ok ? "Permissions granted" : "Permissions denied"; + } catch (e) { + statusOutput.value = `Failed to grant permissions: ${e}`; + } + }; + document.getElementById("reset-host-permissions").onclick = async () => { + let permissions = await browser.permissions.getAll(); + await browser.permissions.remove({ origins: permissions.origins }); + statusOutput.value = `Removed: ${JSON.stringify(permissions.origins)}`; + }; + + browser.permissions.getAll().then( + permissions => { + textarea.value = JSON.stringify(permissions.origins, null, 2); + } + ); +} + +function serializeRules(rules) { + // The getDynamicRules and getSessionRules APIs returns the rules, including + // optional keys. For readability, we strip all optional keys. + // JSON.stringify will drop keys if the replacer function returns undefined. + const replacer = (key, value) => value === null ? undefined : value; + return JSON.stringify(rules, replacer, 2); +} + +function initializePrefHandlerForDynamicDNR() { + const textarea = document.getElementById("input-dynamic-rules"); + const statusOutput = document.getElementById("status-dynamic-rules"); + document.getElementById("save-dynamic-rules").onclick = async () => { + try { + let newRules = JSON.parse(textarea.value); + let oldRules = await browser.declarativeNetRequest.getDynamicRules(); + await browser.declarativeNetRequest.updateDynamicRules({ + removeRuleIds: oldRules.map(rule => rule.id), + addRules: newRules, + }); + statusOutput.value = `Saved ${newRules.length} rules`; + } catch (e) { + statusOutput.value = `Failed to save rules: ${e}`; + } + }; + + browser.declarativeNetRequest.getDynamicRules().then(rules => { + textarea.value = serializeRules(rules); + }); +} + +function initializePrefHandlerForSessionDNR() { + const textarea = document.getElementById("input-session-rules"); + const statusOutput = document.getElementById("status-session-rules"); + document.getElementById("save-session-rules").onclick = async () => { + try { + let newRules = JSON.parse(textarea.value); + let oldRules = await browser.declarativeNetRequest.getSessionRules(); + await browser.declarativeNetRequest.updateSessionRules({ + removeRuleIds: oldRules.map(rule => rule.id), + addRules: newRules, + }); + statusOutput.value = `Saved ${newRules.length} rules`; + } catch (e) { + statusOutput.value = `Failed to save rules: ${e}`; + } + }; + + browser.declarativeNetRequest.getSessionRules().then(rules => { + textarea.value = serializeRules(rules); + }); +} + +initializePrefHandlerForHostPermissions(); +initializePrefHandlerForDynamicDNR(); +initializePrefHandlerForSessionDNR(); diff --git a/dnr-redirect-url/README.md b/dnr-redirect-url/README.md new file mode 100644 index 00000000..27bc5276 --- /dev/null +++ b/dnr-redirect-url/README.md @@ -0,0 +1,64 @@ +# dnr-redirect-url + +Demonstrates multiple ways to redirect requests using the declarativeNetRequest +API through the `declarative_net_request` manifest key. Demonstrates aspects of +Manifest Version 3 (MV3): `action`, `host_permissions`, and +`web_accessible_resources`. + +## What it does + +This extension redirects requests from the example.com domain to other +destinations: + +- example.com/ to `redirectTarget.html` packaged with the extension. +- example.com/ew to extensionworkshop.com +- https://www.example.com/[anything] to the same URL but the domain changed to + example.com and `?redirected_from_www=1` appended to the URL. +- example.com URLs matching regular expression `^https?://([^?]+)$` to the same + URL but with the scheme set to `https:` (if it was `http:` before), and with + `?redirected_by_regex` appended. + +Redirecting requires host permissions for the pre-redirect URLs. In Firefox +(and Safari), Manifest V3 extensions do not have access to these by default. +The permission to these can be granted from the extension action popup. + +# What it shows + +This extension shows how to: + +- use the declarativeNetRequest API through the `declarative_net_request` + manifest key, along with the "declarativeNetRequestWithHostAccess" + permission. This permission does not trigger a permission warning. (Compared + to the "declarativeNetRequest" permission, which has the same effect but + displays the "Block content on any page" permission warning.) +- use the `action` API to offer a UI surface with which the user can interact. +- use the `permissions.contains` API to check whether an extension is granted + host permissions. +- use the `permissions.request` API to request host permissions as needed. +- redirect requests to another website. +- redirect requests to a page packaged in the extension and listed in + `web_accessible_resources`. +- redirect requests and transform the URL with the `transform` and + `queryTransform` options. +- redirect a URL matching a regular expression in `regexFilter` to a + destination composed from `regexSubstitution` and the matched URL. +- use "priority" to establish a guaranteed order of precedence between rules. + This results in a predictable redirect outcome when there are multiple + matching rule conditions for a given request. + +## Comparison with Manifest Version 2 + +While this example uses `"manifest_version": 3`, the functionality is not +specific to Manifest Version 3. + +To create a MV2 version of the extension, modify `manifest.json` as follows: + +- Set `manifest_version` to 2. +- Rename `host_permissions` to `optional_permissions`. +- Rename `action` to `browser_action`. +- Set `web_accessible_resources` to `["redirectTarget.html"]` + +As an alternative to renaming `host_permissions` to `optional_permissions`, +add the match patterns in the `host_permissions` array to the +`permissions` key of the MV2 manifest. Then the user does not need to opt in to +the host permission, and the extension works immediately after installation. diff --git a/dnr-redirect-url/manifest.json b/dnr-redirect-url/manifest.json new file mode 100644 index 00000000..e9417744 --- /dev/null +++ b/dnr-redirect-url/manifest.json @@ -0,0 +1,24 @@ +{ + "manifest_version": 3, + "name": "Redirect example.com requests", + "description": "Redirects example.com requests. Redirects always require host_permissions.", + "version": "0.1", + "permissions": ["declarativeNetRequestWithHostAccess"], + "host_permissions": ["*://*.example.com/"], + "declarative_net_request": { + "rule_resources": [ + { + "id": "ruleset", + "enabled": true, + "path": "redirect-rules.json" + } + ] + }, + "action": { + "default_popup": "popup.html" + }, + "web_accessible_resources": [{ + "resources": ["redirectTarget.html"], + "matches": ["*://example.com/*"] + }] +} diff --git a/dnr-redirect-url/popup.html b/dnr-redirect-url/popup.html new file mode 100644 index 00000000..ac92ca5b --- /dev/null +++ b/dnr-redirect-url/popup.html @@ -0,0 +1,29 @@ + + + + + + + + +

Host permission requirement

+ To redirect requests, the extension needs host permissions.
+ While "Manage Extensions" (about:addons) offers a built-in UI to grant or revoke permissions, this extension uses the permissions API to build the request into the UI: +

+ + +

Test cases

+ There are four rules in redirect-rules.json; each rule has a test case here. + + + + + diff --git a/dnr-redirect-url/popup.js b/dnr-redirect-url/popup.js new file mode 100644 index 00000000..0c2ee8c6 --- /dev/null +++ b/dnr-redirect-url/popup.js @@ -0,0 +1,37 @@ +"use strict"; + +if (typeof browser == "undefined") { + // `browser` is not defined in Chrome, but Manifest V3 extensions in Chrome + // also support promises in the `chrome` namespace, like Firefox. To easily + // test the example without modifications, polyfill "browser" to "chrome". + globalThis.browser = chrome; +} + +const permissions = { + // This origin is listed in host_permissions: + origins: ["*://*.example.com/"], +}; + +const checkbox_host_permission = document.getElementById("checkbox_host_permission"); +checkbox_host_permission.onchange = async () => { + if (checkbox_host_permission.checked) { + let granted = await browser.permissions.request(permissions); + if (!granted) { + // Permission request was denied by the user. + checkbox_host_permission.checked = false; + } + } else { + try { + await browser.permissions.remove(permissions); + } catch (e) { + // While Chrome allows granting of host_permissions that have manually + // been revoked by the user, it fails when revoking them, with + // "Error: You cannot remove required permissions." + console.error(e); + checkbox_host_permission.checked = true; + } + } +}; +browser.permissions.contains(permissions).then(granted => { + checkbox_host_permission.checked = granted; +}); diff --git a/dnr-redirect-url/redirect-rules.json b/dnr-redirect-url/redirect-rules.json new file mode 100644 index 00000000..bc75b95a --- /dev/null +++ b/dnr-redirect-url/redirect-rules.json @@ -0,0 +1,65 @@ +[ + { + "id": 1, + "priority": 4, + "condition": { + "urlFilter": "||example.com/|", + "resourceTypes": ["main_frame"] + }, + "action": { + "type": "redirect", + "redirect": { + "extensionPath": "/redirectTarget.html" + } + } + }, + { + "id": 2, + "priority": 3, + "condition": { + "urlFilter": "||example.com/ew", + "resourceTypes": ["main_frame"] + }, + "action": { + "type": "redirect", + "redirect": { + "url": "https://extensionworkshop.com/" + } + } + }, + { + "id": 3, + "priority": 2, + "condition": { + "urlFilter": "|https://www.example.com/", + "resourceTypes": ["main_frame"] + }, + "action": { + "type": "redirect", + "redirect": { + "transform": { + "host": "example.com", + "queryTransform": { + "addOrReplaceParams": [ + { "key": "redirected_from_www", "value": "1" } + ] + } + } + } + } + }, + { + "id": 4, + "condition": { + "regexFilter": "^https?://([^?]+)$", + "requestDomains": ["example.com"], + "resourceTypes": ["main_frame"] + }, + "action": { + "type": "redirect", + "redirect": { + "regexSubstitution": "https://\\1?redirected_by_regex" + } + } + } +] diff --git a/dnr-redirect-url/redirectTarget.html b/dnr-redirect-url/redirectTarget.html new file mode 100644 index 00000000..a9b1d677 --- /dev/null +++ b/dnr-redirect-url/redirectTarget.html @@ -0,0 +1,38 @@ + + + + + +redirectTarget.html + + + This page is the redirect target of requests matching rule 1 from redirect-rules.json.
+ The pattern ||example.com/| means: (sub)domain of example.com, with path "/" and nothing else before the end of the URL. +
+  {
+    "id": 1,
+    "priority": 4,
+    "condition": {
+      "urlFilter": "||example.com/|",
+      "resourceTypes": ["main_frame"]
+    },
+    "action": {
+      "type": "redirect",
+      "redirect": {
+        "extensionPath": "/redirectTarget.html"
+      }
+    }
+  },
+
+ +For the redirect to have succeeded, three conditions must be met: + + + +See popup.html for the permissions UI and examples. + + diff --git a/examples.json b/examples.json index 4057a0a2..994dd9ce 100644 --- a/examples.json +++ b/examples.json @@ -156,6 +156,43 @@ "javascript_apis": [], "name": "discogs-search" }, + { + "description": "Demonstrates how to block network requests without host permissions using the declarativeNetRequest API with the `declarative_net_request` manifest key.", + "javascript_apis": [ + "declarativeNetRequest.Rule", + "declarativeNetRequest.RuleAction", + "declarativeNetRequest.RuleCondition" + ], + "name": "dnr-block-only" + }, + { + "description": "Demonstrates a generic way to request host permissions and register declarativeNetRequest rules to modify network requests, without any install-time permission warnings. The options_ui page offers a way to request permissions and register DNR rules.", + "javascript_apis": [ + "declarativeNetRequest.Rule", + "declarativeNetRequest.getDynamicRules", + "declarativeNetRequest.getSessionRules", + "declarativeNetRequest.updateDynamicRules", + "declarativeNetRequest.updateSessionRules", + "permissions.getAll", + "permissions.remove", + "permissions.request" + ], + "name": "dnr-dynamic-with-options" + }, + { + "description": "Demonstrates multiple ways to redirect requests using the declarativeNetRequest API through the `declarative_net_request` manifest key. Demonstrates aspects of Manifest Version 3 (MV3): action, host_permissions, and web_accessible_resources, and includes a comparison with Manifest Version 2 (MV2).", + "javascript_apis": [ + "declarativeNetRequest.Redirect", + "declarativeNetRequest.Rule", + "declarativeNetRequest.RuleAction", + "declarativeNetRequest.RuleCondition", + "declarativeNetRequest.URLTransform", + "permissions.contains", + "permissions.remove", + "permissions.request" + ], + "name": "dnr-redirect-url" + }, { "description": "Dynamic theme example", "javascript_apis": [