1
0
Fork 0
mirror of https://github.com/nix-community/home-manager.git synced 2025-03-06 16:57:03 +00:00

firefox: add support for configuring extensions

This commit refactors programs.firefox.profiles.<name>.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.
This commit is contained in:
HPsaucii 2025-01-31 09:22:38 +00:00 committed by Austin Horstman
parent 3a0cf8f1aa
commit 27ffa35178
4 changed files with 190 additions and 78 deletions

View file

@ -1,15 +1,11 @@
{ lib, ... }: { lib, ... }:
with lib; with lib;
let let
modulePath = [ "programs" "firefox" ]; modulePath = [ "programs" "firefox" ];
moduleName = concatStringsSep "." modulePath; moduleName = concatStringsSep "." modulePath;
mkFirefoxModule = import ./firefox/mkFirefoxModule.nix; mkFirefoxModule = import ./firefox/mkFirefoxModule.nix;
in { in {
meta.maintainers = [ maintainers.rycee hm.maintainers.bricked ]; meta.maintainers = [ maintainers.rycee hm.maintainers.bricked ];
@ -32,14 +28,13 @@ in {
}) })
(mkRemovedOptionModule (modulePath ++ [ "extensions" ]) '' (mkRemovedOptionModule (modulePath ++ [ "extensions" ]) ''
Extensions are now managed per-profile. That is, change from Extensions are now managed per-profile. That is, change from
${moduleName}.extensions = [ foo bar ]; ${moduleName}.extensions = [ foo bar ];
to to
${moduleName}.profiles.myprofile.extensions = [ foo bar ];'') ${moduleName}.profiles.myprofile.extensions.packages = [ foo bar ];'')
(mkRemovedOptionModule (modulePath ++ [ "enableAdobeFlash" ]) (mkRemovedOptionModule (modulePath ++ [ "enableAdobeFlash" ])
"Support for this option has been removed.") "Support for this option has been removed.")
(mkRemovedOptionModule (modulePath ++ [ "enableGoogleTalk" ]) (mkRemovedOptionModule (modulePath ++ [ "enableGoogleTalk" ])

View file

@ -1,13 +1,9 @@
{ modulePath, name, description ? null, wrappedPackageName ? null { modulePath, name, description ? null, wrappedPackageName ? null
, unwrappedPackageName ? null, platforms, visible ? false , unwrappedPackageName ? null, platforms, visible ? false
, enableBookmarks ? true }: , enableBookmarks ? true, }:
{ config, lib, pkgs, ... }: { config, lib, pkgs, ... }:
with lib; with lib;
let let
inherit (pkgs.stdenv.hostPlatform) isDarwin; inherit (pkgs.stdenv.hostPlatform) isDarwin;
appName = name; appName = name;
@ -77,11 +73,13 @@ let
else else
builtins.toJSON pref); builtins.toJSON pref);
mkUserJs = prePrefs: prefs: extraPrefs: bookmarks: mkUserJs = prePrefs: prefs: extraPrefs: bookmarks: extensions:
let let
prefs' = lib.optionalAttrs ([ ] != bookmarks) { prefs' = lib.optionalAttrs ([ ] != bookmarks) {
"browser.bookmarks.file" = toString (browserBookmarksFile bookmarks); "browser.bookmarks.file" = toString (browserBookmarksFile bookmarks);
"browser.places.importBookmarksHTML" = true; "browser.places.importBookmarksHTML" = true;
} // lib.optionalAttrs (extensions != { }) {
"extensions.webextensions.ExtensionStorageIDB.enabled" = false;
} // prefs; } // prefs;
in '' in ''
// Generated by Home Manager. // Generated by Home Manager.
@ -218,7 +216,6 @@ let
# The configuration expected by the Firefox wrapper builder. # The configuration expected by the Firefox wrapper builder.
bcfg = setAttrByPath [ browserName ] fcfg; bcfg = setAttrByPath [ browserName ] fcfg;
in if package == null then in if package == null then
null null
else if isDarwin then else if isDarwin then
@ -230,7 +227,6 @@ let
}) })
else else
(pkgs.wrapFirefox.override { config = bcfg; }) package { }; (pkgs.wrapFirefox.override { config = bcfg; }) package { };
in { in {
options = setAttrByPath modulePath { options = setAttrByPath modulePath {
enable = mkOption { enable = mkOption {
@ -242,7 +238,7 @@ in {
optionalString (description != null) " ${description}" optionalString (description != null) " ${description}"
} }
${optionalString (!visible) ${optionalString (!visible)
"See `programs.firefox` for more configuration options."} "See `${moduleName}` for more configuration options."}
''; '';
}; };
@ -667,8 +663,18 @@ in {
for more information. for more information.
''; '';
}; };
extensions = mkOption { extensions = mkOption {
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.<profile>.extensions
to
${moduleName}.profiles.<profile>.extensions.packages
'' packages);
}) (types.submodule {
options = {
packages = mkOption {
type = types.listOf types.package; type = types.listOf types.package;
default = [ ]; default = [ ];
example = literalExpression '' example = literalExpression ''
@ -677,19 +683,18 @@ in {
] ]
''; '';
description = '' description = ''
List of ${appName} add-on packages to install for this profile. List of ${name} add-on packages to install for this profile.
Some pre-packaged add-ons are accessible from the Some pre-packaged add-ons are accessible from the Nix User Repository.
[Nix User Repository](https://github.com/nix-community/NUR).
Once you have NUR installed run Once you have NUR installed run
```console ```console
$ nix-env -f '<nixpkgs>' -qaP -A nur.repos.rycee.firefox-addons $ nix-env -f '<nixpkgs>' -qaP -A nur.repos.rycee.firefox-addons
``` ```
to list the available ${appName} add-ons. to list the available ${name} add-ons.
Note that it is necessary to manually enable these extensions Note that it is necessary to manually enable these extensions
inside ${appName} after the first installation. inside ${name} after the first installation.
To automatically enable extensions add To automatically enable extensions add
`"extensions.autoDisableScopes" = 0;` `"extensions.autoDisableScopes" = 0;`
@ -698,6 +703,74 @@ in {
''; '';
}; };
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 = ''
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 = { }; default = { };
@ -748,7 +821,7 @@ in {
{ {
assertion = cfg.languagePacks == [ ] || cfg.package != null; assertion = cfg.languagePacks == [ ] || cfg.package != null;
message = '' message = ''
'programs.firefox.languagePacks' requires 'programs.firefox.package' '${moduleName}.languagePacks' requires '${moduleName}.package'
to be set to a non-null value. to be set to a non-null value.
''; '';
} }
@ -776,7 +849,9 @@ in {
"${nativeMessagingHostsJoined}/lib/mozilla/native-messaging-hosts"; "${nativeMessagingHostsJoined}/lib/mozilla/native-messaging-hosts";
recursive = true; recursive = true;
}; };
} ++ flip mapAttrsToList cfg.profiles (_: profile: { } ++ flip mapAttrsToList cfg.profiles (_: profile:
# Merge the regular profile settings with extension settings
mkMerge ([{
"${profilesPath}/${profile.path}/.keep".text = ""; "${profilesPath}/${profile.path}/.keep".text = "";
"${profilesPath}/${profile.path}/chrome/userChrome.css" = "${profilesPath}/${profile.path}/chrome/userChrome.css" =
@ -785,11 +860,12 @@ in {
"${profilesPath}/${profile.path}/chrome/userContent.css" = "${profilesPath}/${profile.path}/chrome/userContent.css" =
mkIf (profile.userContent != "") { text = profile.userContent; }; mkIf (profile.userContent != "") { text = profile.userContent; };
"${profilesPath}/${profile.path}/user.js" = mkIf (profile.preConfig != "" "${profilesPath}/${profile.path}/user.js" = mkIf (profile.preConfig
|| profile.settings != { } || profile.extraConfig != "" != "" || profile.settings != { } || profile.extraConfig != ""
|| profile.bookmarks != [ ]) { || profile.bookmarks != [ ]) {
text = mkUserJs profile.preConfig profile.settings profile.extraConfig text =
profile.bookmarks; mkUserJs profile.preConfig profile.settings profile.extraConfig
profile.bookmarks profile.extensions.settings;
}; };
"${profilesPath}/${profile.path}/containers.json" = "${profilesPath}/${profile.path}/containers.json" =
@ -806,17 +882,26 @@ in {
}; };
"${profilesPath}/${profile.path}/extensions" = "${profilesPath}/${profile.path}/extensions" =
mkIf (profile.extensions != [ ]) { mkIf (profile.extensions.packages != [ ]) {
source = let source = let
extensionsEnvPkg = pkgs.buildEnv { extensionsEnvPkg = pkgs.buildEnv {
name = "hm-firefox-extensions"; name = "hm-firefox-extensions";
paths = profile.extensions; paths = profile.extensions.packages;
}; };
in "${extensionsEnvPkg}/share/mozilla/${extensionPath}"; in "${extensionsEnvPkg}/share/mozilla/${extensionPath}";
recursive = true; recursive = true;
force = 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 { } // setAttrByPath modulePath {
finalPackage = wrapPackage cfg.package; finalPackage = wrapPackage cfg.package;

View file

@ -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}
'';
});
}

View file

@ -0,0 +1 @@
{"selectedFilterLists":["ublock-filters","ublock-badware","ublock-privacy","ublock-unbreak","ublock-quick-fixes"]}