Skip to content

Commit

Permalink
firefox: support setting search engine & adding custom engines
Browse files Browse the repository at this point in the history
With this change, it's now possible to configure the default search
engine in Firefox with
`programs.firefox.profiles.<name>.search.default` and add custom
engines with `programs.firefox.profiles.<name>.search.engines`.

Both options need search support to be explicitly enabled with
`programs.firefox.profiles.<name>.search.enable = true`, since this
feature will destructively override any existing search configuration.
  • Loading branch information
kira-bruneau committed May 29, 2022
1 parent 64831f9 commit 16dc28a
Show file tree
Hide file tree
Showing 5 changed files with 344 additions and 1 deletion.
6 changes: 6 additions & 0 deletions modules/lib/maintainers.nix
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@
fingerprint = "2BE3 BAFD 793E A349 ED1F F00F 04D0 CEAF 916A 9A40";
}];
};
kira-bruneau = {
name = "Kira Bruneau";
email = "[email protected]";
github = "kira-bruneau";
githubId = 382041;
};
kubukoz = {
name = "Jakub Kozłowski";
email = "[email protected]";
Expand Down
12 changes: 12 additions & 0 deletions modules/misc/news.nix
Original file line number Diff line number Diff line change
Expand Up @@ -2479,6 +2479,18 @@ in
A new module is available: 'services.mopidy'.
'';
}
{
time = "2022-05-29T00:13:32+00:00";
message = ''
It's now possible to configure the default search engine in Firefox
with `programs.firefox.profiles.<name>.search.default` and add custom
engines with `programs.firefox.profiles.<name>.search.engines`.
Both options need search support to be explicitly enabled with
`programs.firefox.profiles.<name>.search.enable = true`, since this
feature will destructively override any existing search configuration.
'';
}
];
};
}
199 changes: 198 additions & 1 deletion modules/programs/firefox.nix
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ let
'';

in {
meta.maintainers = [ maintainers.rycee ];
meta.maintainers = [ maintainers.rycee maintainers.kira-bruneau ];

imports = [
(mkRemovedOptionModule [ "programs" "firefox" "enableAdobeFlash" ]
Expand Down Expand Up @@ -283,6 +283,86 @@ in {
defaultText = "true if profile ID is 0";
description = "Whether this is a default profile.";
};

search = mkOption {
type = types.submodule {
options = {
enable = mkEnableOption ''
search configuration support. WARNING: Enabling this will
destructively override any existing search configuration
'';

default = mkOption {
type = with types; nullOr str;
example = "DuckDuckGo";
description = ''
The default search engine used in the address bar and search bar.
'';
};

order = mkOption {
type = with types; uniq (listOf str);
default = [ ];
example = ''
[ "DuckDuckGo" "Google" ]
'';
description = ''
The order the search engines are listed in. Any engines
that aren't included in this list will be listed after
these in an unspecified order.
'';
};

engines = mkOption {
type = with types;
attrsOf (attrsOf (pkgs.formats.json { }).type);
default = { };
example = literalExpression ''
{
"Nix Packages" = {
urls = [{
template = "https://search.nixos.org/packages";
params = [
{ name = "type"; value = "packages"; }
{ name = "query"; value = "{searchTerms}"; }
];
}];
icon = "''${pkgs.nixos-icons}/share/icons/hicolor/scalable/apps/nix-snowflake.svg";
definedAliases = [ "@np" ];
};
"NixOS Wiki" = {
urls = [{ template = "https://nixos.wiki/index.php?search={searchTerms}"; }];
iconUpdateURL = "https://nixos.wiki/favicon.png";
updateInterval = 24 * 60 * 60 * 1000; # every day
definedAliases = [ "@nw" ];
};
"Bing".metaData.hidden = true;
"Google".metaData.alias = "@g"; # builtin engines only support specifying one additional alias
}
'';
description = ''
Attribute set of search engine configurations.
Engines that only have metaData specified will be
treated as builtin to Firefox.
See <link xlink:href=
"https://searchfox.org/mozilla-central/rev/669329e284f8e8e2bb28090617192ca9b4ef3380/toolkit/components/search/SearchEngine.jsm#1138-1177"
> SearchEngine.jsm </link> in Firefox's source for
available options. We maintain a mapping to let you specify
all options in the referenced link without underscores,
but it may fall out of date with future options.
icon is also a special option added by home-manager to
make it convenient to specify absolute icon paths.
'';
};
};
};
default = { };
};
};
}));
default = { };
Expand Down Expand Up @@ -376,6 +456,123 @@ in {
mkUserJs profile.settings profile.extraConfig profile.bookmarks;
};

"${profilesPath}/${profile.path}/search.json.mozlz4" =
mkIf profile.search.enable {
force = true;
source = let
settings = {
version = 6;

engines = let
allEngines = (profile.search.engines //
# If search.default isn't in search.engines, assume it's
# app provided and include it in the set of all engines
optionalAttrs (profile.search.default != null
&& !(hasAttr profile.search.default
profile.search.engines)) {
${profile.search.default} = { };
});

# Map allEngines to a list and order by search.order
orderedEngineList = (imap (order: name:
let engine = (allEngines.${name} or { });
in engine // {
inherit name;
metaData = (engine.metaData or { }) // { inherit order; };
}) profile.search.order)
++ (mapAttrsToList (name: config: config // { inherit name; })
(removeAttrs allEngines profile.search.order));

engines = map (config:
let
name = config.name;
isAppProvided = (removeAttrs config [ "name" "metaData" ])
== { };
metaData = config.metaData or { };
in mapAttrs' (name: value: {
# Map nice field names to internal field names.
# This is intended to be exhaustive, but any future fields
# will either have to be specified with an underscore,
# or added to this map.
name = {
name = "_name";
isAppProvided = "_isAppProvided";
loadPath = "_loadPath";
hasPreferredIcon = "_hasPreferredIcon";
searchForm = "__searchForm";
updateInterval = "_updateInterval";
updateURL = "_updateURL";
iconUpdateURL = "_iconUpdateURL";
iconURL = "_iconURL";
iconMapObj = "_iconMapObj";
metaData = "_metaData";
orderHint = "_orderHint";
definedAliases = "_definedAliases";
urls = "_urls";
}.${name} or name;

inherit value;
}) ((removeAttrs config [ "icon" ])
// (optionalAttrs (!isAppProvided)
(optionalAttrs (config ? iconUpdateURL) {
# Convenience to default iconURL to iconUpdateURL so
# the icon is immediately downloaded from the URL
iconURL = config.iconURL or config.iconUpdateURL;
} // optionalAttrs (config ? icon) {
# Convenience to specify absolute path to icon
iconURL = "file://${config.icon}";
} // {
# Required for custom engine configurations,
# loadPaths are unique identifiers that are generally
# formatted like: [source]/path/to/engine.xml
loadPath = ''
[home-manager]/programs.firefox.profiles.${profile.name}.search.engines."${
replaceChars [ "\\" ] [ "\\\\" ] name
}"'';
})) // {
# Required fields for all engine configurations
inherit name isAppProvided metaData;
})) orderedEngineList;
in engines;

metaData = optionalAttrs (profile.search.default != null) {
current = profile.search.default;

# Hash algorithm from
# https://searchfox.org/mozilla-central/rev/de15f9c109f9c474d00faf8032f559c236067c06/toolkit/components/search/SearchUtils.jsm#312-334
hash = let
# home-manager doesn't circumvent user consent and isn't
# acting maliciously. We're modifying the search outside of
# Firefox, but a claim by Mozilla to remove this would be very
# anti-user, and is unlikely to be an issue for our use case.
disclaimer =
"By modifying this file, I agree that I am doing so "
+ "only within $appName itself, using official, user-driven search "
+ "engine selection processes, and in a way which does not circumvent "
+ "user consent. I acknowledge that any attempt to change this file "
+ "from outside of $appName is a malicious act, and will be responded "
+ "to accordingly.";

salt = profile.path + profile.search.default
+ (builtins.replaceStrings [ "$appName" ] [ "Firefox" ]
disclaimer);

hash = removeSuffix "\n" (builtins.readFile
(pkgs.runCommandNoCC "hash" { inherit salt; } ''
echo -n "$salt" | ${pkgs.openssl}/bin/openssl dgst -sha256 -binary | base64 > "$out"
''));
in hash;
} // {
useSavedOrder = length profile.search.order > 0;
};
};
in pkgs.runCommandNoCC "search.json.mozlz4" { } ''
${pkgs.mozlz4a}/bin/mozlz4a ${
pkgs.writeText "search.json" (builtins.toJSON settings)
} "$out"
'';
};

"${profilesPath}/${profile.path}/extensions" =
mkIf (cfg.extensions != [ ]) {
source = "${extensionsEnvPkg}/share/mozilla/${extensionPath}";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
{
"engines": [
{
"_definedAliases": [
"@np"
],
"_iconURL": "file:///run/current-system/sw/share/icons/hicolor/scalable/apps/nix-snowflake.svg",
"_isAppProvided": false,
"_loadPath": "[home-manager]/programs.firefox.profiles.search.search.engines.\"Nix Packages\"",
"_metaData": {
"order": 1
},
"_name": "Nix Packages",
"_urls": [
{
"params": [
{
"name": "type",
"value": "packages"
},
{
"name": "query",
"value": "{searchTerms}"
}
],
"template": "https://search.nixos.org/packages"
}
]
},
{
"_definedAliases": [
"@nw"
],
"_iconURL": "https://nixos.wiki/favicon.png",
"_iconUpdateURL": "https://nixos.wiki/favicon.png",
"_isAppProvided": false,
"_loadPath": "[home-manager]/programs.firefox.profiles.search.search.engines.\"NixOS Wiki\"",
"_metaData": {
"order": 2
},
"_name": "NixOS Wiki",
"_updateInterval": 86400000,
"_urls": [
{
"template": "https://nixos.wiki/index.php?search={searchTerms}"
}
]
},
{
"_isAppProvided": true,
"_metaData": {
"hidden": true
},
"_name": "Bing"
},
{
"_isAppProvided": true,
"_metaData": {},
"_name": "DuckDuckGo"
},
{
"_isAppProvided": true,
"_metaData": {
"alias": "@g"
},
"_name": "Google"
}
],
"metaData": {
"current": "DuckDuckGo",
"hash": "BWvqUiaCuMJ20lbymFf2dqzWyl1cgm1LZhhdWNEp0Cc=",
"useSavedOrder": true
},
"version": 6
}
53 changes: 53 additions & 0 deletions tests/modules/programs/firefox/profile-settings.nix
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,49 @@ lib.mkIf config.test.enableBig {
"kernel.org" = { url = "https://www.kernel.org"; };
};
};

profiles.search = {
id = 3;
search = {
enable = true;
default = "DuckDuckGo";
order = [ "Nix Packages" "NixOS Wiki" ];
engines = {
"Nix Packages" = {
urls = [{
template = "https://search.nixos.org/packages";
params = [
{
name = "type";
value = "packages";
}
{
name = "query";
value = "{searchTerms}";
}
];
}];

icon =
"/run/current-system/sw/share/icons/hicolor/scalable/apps/nix-snowflake.svg";

definedAliases = [ "@np" ];
};

"NixOS Wiki" = {
urls = [{
template = "https://nixos.wiki/index.php?search={searchTerms}";
}];
iconUpdateURL = "https://nixos.wiki/favicon.png";
updateInterval = 24 * 60 * 60 * 1000;
definedAliases = [ "@nw" ];
};

"Bing".metaData.hidden = true;
"Google".metaData.alias = "@g";
};
};
};
};

nixpkgs.overlays = [
Expand Down Expand Up @@ -64,5 +107,15 @@ lib.mkIf config.test.enableBig {
assertFileContent \
$bookmarksFile \
${./profile-settings-expected-bookmarks.html}
compressedSearch=$(normalizeStorePaths \
home-files/.mozilla/firefox/search/search.json.mozlz4)
decompressedSearch=$(dirname $compressedSearch)/search.json
${pkgs.mozlz4a}/bin/mozlz4a -d "$compressedSearch" >(${pkgs.jq}/bin/jq . > "$decompressedSearch")
assertFileContent \
$decompressedSearch \
${./profile-settings-expected-search.json}
'';
}

0 comments on commit 16dc28a

Please sign in to comment.