firefox: support setting search engines

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.

It's also recommended to enable

  programs.firefox.profiles.<name>.search.force = true

since Firefox will replace the symlink for the search configuration on
every launch, but note that you'll loose any existing configuration by
enabling this.
This commit is contained in:
Kira Bruneau 2022-05-27 20:01:44 -04:00 committed by Robert Helgesson
parent c485669ca5
commit 69d19b9839
No known key found for this signature in database
GPG key ID: 36BDAA14C2797E89
4 changed files with 348 additions and 1 deletions

View file

@ -770,6 +770,29 @@ in
A new module is available: 'programs.looking-glass-client'.
'';
}
{
time = "2022-10-22T17:52:30+00:00";
condition = config.programs.firefox.enable;
message = ''
It is 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.
It is also recommended to set
programs.firefox.profiles.<name>.search.force = true
since Firefox will replace the symlink for the search configuration on
every launch, but note that you'll lose any existing configuration by
enabling this.
'';
}
];
};
}

View file

@ -8,6 +8,8 @@ let
cfg = config.programs.firefox;
jsonFormat = pkgs.formats.json { };
mozillaConfigPath =
if isDarwin then "Library/Application Support/Mozilla" else ".mozilla";
@ -106,7 +108,7 @@ let
'';
in {
meta.maintainers = [ maintainers.rycee ];
meta.maintainers = [ maintainers.rycee maintainers.kira-bruneau ];
imports = [
(mkRemovedOptionModule [ "programs" "firefox" "enableAdobeFlash" ]
@ -351,6 +353,87 @@ in {
defaultText = "true if profile ID is 0";
description = "Whether this is a default profile.";
};
search = {
force = mkOption {
type = with types; bool;
default = false;
description = ''
Whether to force replace the existing search
configuration. This is recommended since Firefox will
replace the symlink for the search configuration on every
launch, but note that you'll lose any existing
configuration by enabling this.
'';
};
default = mkOption {
type = with types; nullOr str;
default = null;
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 jsonFormat.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 <varname>metaData</varname> specified will
be treated as builtin to Firefox.
</para><para>
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.
</para><para>
Note, <varname>icon</varname> is also a special option
added by Home Manager to make it convenient to specify
absolute icon paths.
'';
};
};
};
}));
default = { };
@ -444,6 +527,119 @@ in {
mkUserJs profile.settings profile.extraConfig profile.bookmarks;
};
"${profilesPath}/${profile.path}/search.json.mozlz4" = mkIf
(profile.search.default != null || profile.search.order != [ ]
|| profile.search.engines != { }) {
force = profile.search.force;
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 = ((genAttrs [
"name"
"isAppProvided"
"loadPath"
"hasPreferredIcon"
"updateInterval"
"updateURL"
"iconUpdateURL"
"iconURL"
"iconMapObj"
"metaData"
"orderHint"
"definedAliases"
"urls"
] (name: "_${name}")) // {
"searchForm" = "__searchForm";
}).${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 = "@hash@";
} // {
useSavedOrder = profile.search.order != [ ];
};
};
# 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 = appName:
"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
+ disclaimer "Firefox";
in pkgs.runCommand "search.json.mozlz4" {
nativeBuildInputs = with pkgs; [ mozlz4a openssl ];
json = builtins.toJSON settings;
inherit salt;
} ''
export hash=$(echo -n "$salt" | openssl dgst -sha256 -binary | base64)
mozlz4a <(substituteStream json search.json.in --subst-var hash) "$out"
'';
};
"${profilesPath}/${profile.path}/extensions" =
mkIf (cfg.extensions != [ ]) {
source = "${extensionsEnvPkg}/share/mozilla/${extensionPath}";

View file

@ -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
}

View file

@ -60,6 +60,49 @@ lib.mkIf config.test.enableBig {
}
];
};
profiles.search = {
id = 3;
search = {
force = 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 = [
@ -101,5 +144,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}
'';
}