From 27ffa35178bc6f359293a8809d32c5da23aaabc7 Mon Sep 17 00:00:00 2001 From: HPsaucii Date: Fri, 31 Jan 2025 09:22:38 +0000 Subject: [PATCH] firefox: add support for configuring extensions This commit refactors programs.firefox.profiles..extensions in order to support both installation of extensions (addons) and their configuration. It does this by setting the `extensions.webextensions.ExtensionStorageIDB.enabled` user_pref to false. When this preference is set to false, support for storing extension settings in sqlite databases, also known as IndexedDB or IDB, is reverted back to the JSON format present in firefox versions prior to version 63, as seen here: https://blog.mozilla.org/addons/2018/08/03/new-backend-for-storage-local-api/ IndexedDB was made the default due to performance improvements, but had the consequence of removing any possibility of declarative extension configuration without the assistance of firefox's policy system. The policy system is supported by a small amount of extensions, such as uBlock Origin, but has to be explicitly supported. Even when supported, it provides significantly less granular control when compared to the JSON storage format. --- modules/programs/firefox.nix | 7 +- modules/programs/firefox/mkFirefoxModule.nix | 229 ++++++++++++------ .../firefox/profiles/extensions/default.nix | 31 +++ .../profiles/extensions/expected-storage.js | 1 + 4 files changed, 190 insertions(+), 78 deletions(-) create mode 100644 tests/modules/programs/firefox/profiles/extensions/default.nix create mode 100644 tests/modules/programs/firefox/profiles/extensions/expected-storage.js diff --git a/modules/programs/firefox.nix b/modules/programs/firefox.nix index 2a2667c35..853c3385b 100644 --- a/modules/programs/firefox.nix +++ b/modules/programs/firefox.nix @@ -1,15 +1,11 @@ { lib, ... }: - with lib; - let - modulePath = [ "programs" "firefox" ]; moduleName = concatStringsSep "." modulePath; mkFirefoxModule = import ./firefox/mkFirefoxModule.nix; - in { meta.maintainers = [ maintainers.rycee hm.maintainers.bricked ]; @@ -32,14 +28,13 @@ in { }) (mkRemovedOptionModule (modulePath ++ [ "extensions" ]) '' - Extensions are now managed per-profile. That is, change from ${moduleName}.extensions = [ foo bar ]; to - ${moduleName}.profiles.myprofile.extensions = [ foo bar ];'') + ${moduleName}.profiles.myprofile.extensions.packages = [ foo bar ];'') (mkRemovedOptionModule (modulePath ++ [ "enableAdobeFlash" ]) "Support for this option has been removed.") (mkRemovedOptionModule (modulePath ++ [ "enableGoogleTalk" ]) diff --git a/modules/programs/firefox/mkFirefoxModule.nix b/modules/programs/firefox/mkFirefoxModule.nix index 76954dd9c..115af89d9 100644 --- a/modules/programs/firefox/mkFirefoxModule.nix +++ b/modules/programs/firefox/mkFirefoxModule.nix @@ -1,13 +1,9 @@ { modulePath, name, description ? null, wrappedPackageName ? null , unwrappedPackageName ? null, platforms, visible ? false -, enableBookmarks ? true }: - +, enableBookmarks ? true, }: { config, lib, pkgs, ... }: - with lib; - let - inherit (pkgs.stdenv.hostPlatform) isDarwin; appName = name; @@ -77,11 +73,13 @@ let else builtins.toJSON pref); - mkUserJs = prePrefs: prefs: extraPrefs: bookmarks: + mkUserJs = prePrefs: prefs: extraPrefs: bookmarks: extensions: let prefs' = lib.optionalAttrs ([ ] != bookmarks) { "browser.bookmarks.file" = toString (browserBookmarksFile bookmarks); "browser.places.importBookmarksHTML" = true; + } // lib.optionalAttrs (extensions != { }) { + "extensions.webextensions.ExtensionStorageIDB.enabled" = false; } // prefs; in '' // Generated by Home Manager. @@ -218,7 +216,6 @@ let # The configuration expected by the Firefox wrapper builder. bcfg = setAttrByPath [ browserName ] fcfg; - in if package == null then null else if isDarwin then @@ -230,7 +227,6 @@ let }) else (pkgs.wrapFirefox.override { config = bcfg; }) package { }; - in { options = setAttrByPath modulePath { enable = mkOption { @@ -242,7 +238,7 @@ in { optionalString (description != null) " ${description}" } ${optionalString (!visible) - "See `programs.firefox` for more configuration options."} + "See `${moduleName}` for more configuration options."} ''; }; @@ -667,37 +663,114 @@ in { for more information. ''; }; - extensions = mkOption { - type = types.listOf types.package; - default = [ ]; - example = literalExpression '' - with pkgs.nur.repos.rycee.firefox-addons; [ - privacy-badger - ] - ''; + type = types.coercedTo (types.listOf types.package) (packages: { + packages = mkIf (builtins.length packages > 0) (warn '' + In order to support declarative extension configuration, + extension installation has been moved from + ${moduleName}.profiles..extensions + to + ${moduleName}.profiles..extensions.packages + '' packages); + }) (types.submodule { + options = { + packages = mkOption { + type = types.listOf types.package; + default = [ ]; + example = literalExpression '' + with pkgs.nur.repos.rycee.firefox-addons; [ + privacy-badger + ] + ''; + description = '' + List of ${name} add-on packages to install for this profile. + Some pre-packaged add-ons are accessible from the Nix User Repository. + Once you have NUR installed run + + ```console + $ nix-env -f '' -qaP -A nur.repos.rycee.firefox-addons + ``` + + to list the available ${name} add-ons. + + Note that it is necessary to manually enable these extensions + inside ${name} after the first installation. + + To automatically enable extensions add + `"extensions.autoDisableScopes" = 0;` + to + [{option}`${moduleName}.profiles..settings`](#opt-${moduleName}.profiles._name_.settings) + ''; + }; + + settings = mkOption { + default = { }; + example = literalExpression '' + { + # Example with uBlock origin's extensionID + "uBlock0@raymondhill.net".settings = { + selectedFilterLists = [ + "ublock-filters" + "ublock-badware" + "ublock-privacy" + "ublock-unbreak" + "ublock-quick-fixes" + ]; + }; + + # Example with Stylus' UUID-form extensionID + "{7a7a4a92-a2a0-41d1-9fd7-1e92480d612d}".settings = { + dbInChromeStorage = true; # required for Stylus + } + } + ''; + description = '' + Attribute set of options for each extension. + The keys of the attribute set consist of the ID of the extension + or its UUID wrapped in curly braces. + ''; + type = types.attrsOf (types.submodule { + options = { + settings = mkOption { + type = types.attrsOf jsonFormat.type; + description = + "Json formatted options for the specified extensionID"; + }; + force = mkOption { + type = types.bool; + default = false; + example = true; + description = '' + Forcibly override any existing configuration for + this extension. + ''; + }; + }; + }); + }; + }; + }); + default = { }; description = '' - List of ${appName} add-on packages to install for this profile. - Some pre-packaged add-ons are accessible from the - [Nix User Repository](https://github.com/nix-community/NUR). - Once you have NUR installed run - - ```console - $ nix-env -f '' -qaP -A nur.repos.rycee.firefox-addons - ``` - - to list the available ${appName} add-ons. - - Note that it is necessary to manually enable these extensions - inside ${appName} after the first installation. - - To automatically enable extensions add - `"extensions.autoDisableScopes" = 0;` - to - [{option}`${moduleName}.profiles..settings`](#opt-${moduleName}.profiles._name_.settings) + Submodule for installing and configuring extensions. + ''; + example = literalExpression '' + { + packages = with pkgs.nur.repos.rycee.firefox-addons; [ + ublock-origin + ]; + settings."uBlock0@raymondhill.net".settings = { + selectedFilterLists = [ + "ublock-filters" + "ublock-badware" + "ublock-privacy" + "ublock-unbreak" + "ublock-quick-fixes" + ]; + }; + } ''; }; - }; })); default = { }; @@ -748,7 +821,7 @@ in { { assertion = cfg.languagePacks == [ ] || cfg.package != null; message = '' - 'programs.firefox.languagePacks' requires 'programs.firefox.package' + '${moduleName}.languagePacks' requires '${moduleName}.package' to be set to a non-null value. ''; } @@ -776,47 +849,59 @@ in { "${nativeMessagingHostsJoined}/lib/mozilla/native-messaging-hosts"; recursive = true; }; - } ++ flip mapAttrsToList cfg.profiles (_: profile: { - "${profilesPath}/${profile.path}/.keep".text = ""; + } ++ flip mapAttrsToList cfg.profiles (_: profile: + # Merge the regular profile settings with extension settings + mkMerge ([{ + "${profilesPath}/${profile.path}/.keep".text = ""; - "${profilesPath}/${profile.path}/chrome/userChrome.css" = - mkIf (profile.userChrome != "") { text = profile.userChrome; }; + "${profilesPath}/${profile.path}/chrome/userChrome.css" = + mkIf (profile.userChrome != "") { text = profile.userChrome; }; - "${profilesPath}/${profile.path}/chrome/userContent.css" = - mkIf (profile.userContent != "") { text = profile.userContent; }; + "${profilesPath}/${profile.path}/chrome/userContent.css" = + mkIf (profile.userContent != "") { text = profile.userContent; }; - "${profilesPath}/${profile.path}/user.js" = mkIf (profile.preConfig != "" - || profile.settings != { } || profile.extraConfig != "" - || profile.bookmarks != [ ]) { - text = mkUserJs profile.preConfig profile.settings profile.extraConfig - profile.bookmarks; - }; + "${profilesPath}/${profile.path}/user.js" = mkIf (profile.preConfig + != "" || profile.settings != { } || profile.extraConfig != "" + || profile.bookmarks != [ ]) { + text = + mkUserJs profile.preConfig profile.settings profile.extraConfig + profile.bookmarks profile.extensions.settings; + }; - "${profilesPath}/${profile.path}/containers.json" = - mkIf (profile.containers != { }) { - text = mkContainersJson profile.containers; - force = profile.containersForce; - }; + "${profilesPath}/${profile.path}/containers.json" = + mkIf (profile.containers != { }) { + text = mkContainersJson profile.containers; + force = profile.containersForce; + }; - "${profilesPath}/${profile.path}/search.json.mozlz4" = - mkIf (profile.search.enable) { - enable = profile.search.enable; - force = profile.search.force; - source = profile.search.file; - }; + "${profilesPath}/${profile.path}/search.json.mozlz4" = + mkIf (profile.search.enable) { + enable = profile.search.enable; + force = profile.search.force; + source = profile.search.file; + }; - "${profilesPath}/${profile.path}/extensions" = - mkIf (profile.extensions != [ ]) { - source = let - extensionsEnvPkg = pkgs.buildEnv { - name = "hm-firefox-extensions"; - paths = profile.extensions; - }; - in "${extensionsEnvPkg}/share/mozilla/${extensionPath}"; - recursive = true; - force = true; - }; - })); + "${profilesPath}/${profile.path}/extensions" = + mkIf (profile.extensions.packages != [ ]) { + source = let + extensionsEnvPkg = pkgs.buildEnv { + name = "hm-firefox-extensions"; + paths = profile.extensions.packages; + }; + in "${extensionsEnvPkg}/share/mozilla/${extensionPath}"; + recursive = true; + force = true; + }; + }] ++ + # Add extension settings as separate attributes + optional (profile.extensions.settings != { }) (mkMerge (mapAttrsToList + (name: settingConfig: { + "${profilesPath}/${profile.path}/browser-extension-data/${name}/storage.js" = + { + force = settingConfig.force; + text = generators.toJSON { } settingConfig.settings; + }; + }) profile.extensions.settings))))); } // setAttrByPath modulePath { finalPackage = wrapPackage cfg.package; diff --git a/tests/modules/programs/firefox/profiles/extensions/default.nix b/tests/modules/programs/firefox/profiles/extensions/default.nix new file mode 100644 index 000000000..577518390 --- /dev/null +++ b/tests/modules/programs/firefox/profiles/extensions/default.nix @@ -0,0 +1,31 @@ +modulePath: +{ config, lib, pkgs, ... }: +with lib; +let + cfg = getAttrFromPath modulePath config; + + firefoxMockOverlay = import ../../setup-firefox-mock-overlay.nix modulePath; +in { + imports = [ firefoxMockOverlay ]; + + config = mkIf config.test.enableBig (setAttrByPath modulePath { + enable = true; + profiles.extensions = { + extensions.settings."uBlock0@raymondhill.net".settings = { + selectedFilterLists = [ + "ublock-filters" + "ublock-badware" + "ublock-privacy" + "ublock-unbreak" + "ublock-quick-fixes" + ]; + }; + }; + } // { + nmt.script = '' + assertFileContent \ + home-files/${cfg.configPath}/extensions/uBlock0@raymondhill.net/storage.js \ + ${./expected-storage.js} + ''; + }); +} diff --git a/tests/modules/programs/firefox/profiles/extensions/expected-storage.js b/tests/modules/programs/firefox/profiles/extensions/expected-storage.js new file mode 100644 index 000000000..66c2a8d0f --- /dev/null +++ b/tests/modules/programs/firefox/profiles/extensions/expected-storage.js @@ -0,0 +1 @@ +{"selectedFilterLists":["ublock-filters","ublock-badware","ublock-privacy","ublock-unbreak","ublock-quick-fixes"]}