From 69d19b9839638fc487b370e0600a03577a559081 Mon Sep 17 00:00:00 2001 From: Kira Bruneau Date: Fri, 27 May 2022 20:01:44 -0400 Subject: [PATCH] firefox: support setting search engines With this change, it's now possible to configure the default search engine in Firefox with programs.firefox.profiles..search.default and add custom engines with programs.firefox.profiles..search.engines. It's also recommended to enable programs.firefox.profiles..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. --- modules/misc/news.nix | 23 ++ modules/programs/firefox.nix | 198 +++++++++++++++++- .../profile-settings-expected-search.json | 75 +++++++ .../programs/firefox/profile-settings.nix | 53 +++++ 4 files changed, 348 insertions(+), 1 deletion(-) create mode 100644 tests/modules/programs/firefox/profile-settings-expected-search.json diff --git a/modules/misc/news.nix b/modules/misc/news.nix index b88ba7e41..88a1dce96 100644 --- a/modules/misc/news.nix +++ b/modules/misc/news.nix @@ -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..search.default + + and add custom engines with + + programs.firefox.profiles..search.engines. + + It is also recommended to set + + programs.firefox.profiles..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. + ''; + } ]; }; } diff --git a/modules/programs/firefox.nix b/modules/programs/firefox.nix index bf0eac15b..06c9aac7a 100644 --- a/modules/programs/firefox.nix +++ b/modules/programs/firefox.nix @@ -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 metaData specified will + be treated as builtin to Firefox. + + See SearchEngine.jsm + 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. + + Note, icon 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}"; diff --git a/tests/modules/programs/firefox/profile-settings-expected-search.json b/tests/modules/programs/firefox/profile-settings-expected-search.json new file mode 100644 index 000000000..ceee27ee6 --- /dev/null +++ b/tests/modules/programs/firefox/profile-settings-expected-search.json @@ -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 +} diff --git a/tests/modules/programs/firefox/profile-settings.nix b/tests/modules/programs/firefox/profile-settings.nix index 23bf1285d..b28e64597 100644 --- a/tests/modules/programs/firefox/profile-settings.nix +++ b/tests/modules/programs/firefox/profile-settings.nix @@ -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} ''; }