Add "block drafts" feature

Design doc:
https://docs.google.com/document/d/16AX1tKa1CGSWwZtbW42h1uHy8SEPuv1ZjT_oHxc0UUI/edit

Fixed: twpowertools:84
Change-Id: Ibb172113774c5e2cab14e3d87a178bafed85df0b
diff --git a/src/bg.js b/src/bg.js
index ed18aae..9d22c29 100644
--- a/src/bg.js
+++ b/src/bg.js
@@ -6,6 +6,7 @@
 import {cleanUpOptPermissions} from './common/optionsPermissions.js';
 import {cleanUpOptions, disableItemsWithMissingPermissions} from './common/optionsUtils.js';
 import KillSwitchMechanism from './killSwitch/index.js';
+import {handleBgOptionChange, handleBgOptionsOnStart} from './options/bgHandler.js';
 
 // #!if browser_target == 'chromium_mv3'
 // XMLHttpRequest is not present in service workers (MV3) and is required by the
@@ -47,11 +48,17 @@
 });
 
 // Clean up optional permissions and check that none are missing for enabled
-// features as soon as the extension starts and when the options change.
+// features, and also handle background option changes as soon as the extension
+// starts and when the options change.
 cleanUpOptPermissions();
+handleBgOptionsOnStart();
 
-chrome.storage.sync.onChanged.addListener(() => {
+chrome.storage.sync.onChanged.addListener(changes => {
   cleanUpOptPermissions();
+
+  for (let [key, {oldValue, newValue}] of Object.entries(changes)) {
+    handleBgOptionChange(key);
+  }
 });
 
 chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
diff --git a/src/common/optionsPermissions.js b/src/common/optionsPermissions.js
index 93ba4cf..2f4b050 100644
--- a/src/common/optionsPermissions.js
+++ b/src/common/optionsPermissions.js
@@ -9,6 +9,9 @@
 const requiredPermissions = new Set([
   'storage',
   'alarms',
+// #!if ['chromium', 'chromium_mv3'].includes(browser_target)
+  'declarativeNetRequestWithHostAccess',
+// #!endif
 ]);
 
 // Returns an array of optional permissions needed by |feature|.
diff --git a/src/common/optionsPrototype.json5 b/src/common/optionsPrototype.json5
index 0132436..38cfaca 100644
--- a/src/common/optionsPrototype.json5
+++ b/src/common/optionsPrototype.json5
@@ -110,6 +110,13 @@
     context: 'options',
     killSwitchType: 'option',
   },
+  // #!if ['chromium', 'chromium_mv3'].includes(browser_target)
+  'blockdrafts': {
+    defaultValue: false,
+    context: 'options',
+    killSwitchType: 'option',
+  },
+  // #!endif
 
   // Experiments:
 
diff --git a/src/options/bgHandler.js b/src/options/bgHandler.js
new file mode 100644
index 0000000..a9d80ab
--- /dev/null
+++ b/src/options/bgHandler.js
@@ -0,0 +1,53 @@
+// Most options are dynamic, which means whenever they are enabled or disabled,
+// the effect is immediate. However, some features aren't controlled directly in
+// content scripts or injected scripts but instead in the background
+// script/service worker.
+//
+// An example is the "blockdrafts" feature, which when enabled should enable the
+// static ruleset blocking *DraftMessages requests.
+
+import {isOptionEnabled} from '../common/optionsUtils.js';
+
+// List of features controled in the background:
+export var bgFeatures = [
+  'blockdrafts',
+];
+
+const blockDraftsRuleset = 'blockDrafts';
+
+export function handleBgOptionChange(feature) {
+  isOptionEnabled(feature)
+      .then(enabled => {
+        switch (feature) {
+          // #!if ['chromium', 'chromium_mv3'].includes(browser_target)
+          case 'blockdrafts':
+            chrome.declarativeNetRequest.getEnabledRulesets(rulesets => {
+              if (rulesets === undefined) {
+                throw new Error(
+                    chrome.runtime.lastError.message ??
+                    'Unknown error in chrome.declarativeNetRequest.getEnabledRulesets()');
+              }
+
+              let isRulesetEnabled = rulesets.includes(blockDraftsRuleset);
+              if (!isRulesetEnabled && enabled)
+                chrome.declarativeNetRequest.updateEnabledRulesets(
+                    {enableRulesetIds: [blockDraftsRuleset]});
+              if (isRulesetEnabled && !enabled)
+                chrome.declarativeNetRequest.updateEnabledRulesets(
+                    {disableRulesetIds: [blockDraftsRuleset]});
+            });
+            break;
+            // #!endif
+        }
+      })
+      .catch(err => {
+        console.error(
+            'handleBgOptionChange: error while handling feature "' + feature +
+                '": ',
+            err);
+      });
+}
+
+export function handleBgOptionsOnStart() {
+  for (let feature of bgFeatures) handleBgOptionChange(feature);
+}
diff --git a/src/options/optionsPage.json5 b/src/options/optionsPage.json5
index df14718..24bafdd 100644
--- a/src/options/optionsPage.json5
+++ b/src/options/optionsPage.json5
@@ -14,6 +14,9 @@
         {codename: 'history'},
         {codename: 'batchlock'},
         {codename: 'autorefreshlist'},
+        // #!if ['chromium', 'chromium_mv3'].includes(browser_target)
+        {codename: 'blockdrafts'},
+        // #!endif
       ],
     },
     {
diff --git a/src/static/_locales/en/messages.json b/src/static/_locales/en/messages.json
index 9b4ec2e..e9953e1 100644
--- a/src/static/_locales/en/messages.json
+++ b/src/static/_locales/en/messages.json
@@ -135,6 +135,10 @@
     "message": "Show the number of questions and replies written by the OP within the last <span id='profileindicatoralt_months--container'></span> months next to their username.",
     "description": "Feature checkbox in the options page"
   },
+  "options_blockdrafts": {
+    "message": "Block the sending of your replies as you type to Google servers in the Community Console.",
+    "description": "Feature checkbox in the options page"
+  },
   "options_save": {
     "message": "Save",
     "description": "Button in the options page to save the settings"
diff --git a/src/static/rulesets/blockDrafts.json b/src/static/rulesets/blockDrafts.json
new file mode 100644
index 0000000..1d58538
--- /dev/null
+++ b/src/static/rulesets/blockDrafts.json
@@ -0,0 +1,9 @@
+[{
+    "id": 1,
+    "action": {
+        "type": "block"
+    },
+    "condition": {
+        "urlFilter": "||support*.google.com/s/community/api/*DraftMessage"
+    }
+}]
diff --git a/templates/manifest.gjson b/templates/manifest.gjson
index 2faf7fb..800f2ae 100644
--- a/templates/manifest.gjson
+++ b/templates/manifest.gjson
@@ -52,6 +52,9 @@
 #if defined(CHROMIUM || GECKO)
     "https://support.google.com/*",
 #endif
+#if defined(CHROMIUM || CHROMIUM_MV3)
+    "declarativeNetRequestWithHostAccess",
+#endif
     "storage",
     "alarms"
   ],
@@ -85,6 +88,15 @@
     }
 #endif
   ],
+#if defined(CHROMIUM || CHROMIUM_MV3)
+  "declarative_net_request": {
+    "rule_resources": [{
+      "id": "blockDrafts",
+      "enabled": false,
+      "path": "rulesets/blockDrafts.json"
+    }]
+  },
+#endif
 #if defined(CHROMIUM || GECKO)
   "browser_action": {},
 #endif
diff --git a/webpack.config.js b/webpack.config.js
index 24d3876..05542b9 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -59,6 +59,16 @@
   let overridenLocalePaths =
       localeOverrides.map(l => '**/_locales/' + l.pontoonLocale);
 
+  let preprocessorLoader = {
+    loader: 'webpack-preprocessor-loader',
+    options: {
+      params: {
+        browser_target: env.browser_target,
+        production: args.mode == 'production',
+      },
+    },
+  };
+
   return {
     entry,
     output: {
@@ -103,6 +113,9 @@
           parser: {
             parse: json5.parse,
           },
+          use: [
+            preprocessorLoader,
+          ],
         },
         {
           test: /\.s[ac]ss$/i,
@@ -121,15 +134,7 @@
         {
           test: /\.js$/i,
           use: [
-            {
-              loader: 'webpack-preprocessor-loader',
-              options: {
-                params: {
-                  browser_target: env.browser_target,
-                  production: args.mode == 'production',
-                },
-              },
-            },
+            preprocessorLoader,
           ],
         },
       ]