···250250 </listitem>
251251 <listitem>
252252 <para>
253253+ <link xlink:href="https://www.dolibarr.org/">Dolibarr</link>,
254254+ an enterprise resource planning and customer relationship
255255+ manager. Enable using
256256+ <link linkend="opt-services.dolibarr.enable">services.dolibarr</link>.
257257+ </para>
258258+ </listitem>
259259+ <listitem>
260260+ <para>
253261 <link xlink:href="https://www.expressvpn.com">expressvpn</link>,
254262 the CLI client for ExpressVPN. Available as
255263 <link linkend="opt-services.expressvpn.enable">services.expressvpn</link>.
+2
nixos/doc/manual/release-notes/rl-2211.section.md
···90909191- [schleuder](https://schleuder.org/), a mailing list manager with PGP support. Enable using [services.schleuder](#opt-services.schleuder.enable).
92929393+- [Dolibarr](https://www.dolibarr.org/), an enterprise resource planning and customer relationship manager. Enable using [services.dolibarr](#opt-services.dolibarr.enable).
9494+9395- [expressvpn](https://www.expressvpn.com), the CLI client for ExpressVPN. Available as [services.expressvpn](#opt-services.expressvpn.enable).
94969597- [Grafana Tempo](https://www.grafana.com/oss/tempo/), a distributed tracing store. Available as [services.tempo](#opt-services.tempo.enable).
···11# Module for MiniDLNA, a simple DLNA server.
22{ config, lib, pkgs, ... }:
33-43with lib;
5465let
···3433 default = {};
3534 description = lib.mdDoc ''
3635 The contents of MiniDLNA's configuration file.
3737- When the service is activated, a basic template is generated
3838- from the current options opened here.
3636+ When the service is activated, a basic template is generated from the current options opened here.
3937 '';
4038 type = types.submodule {
4139 freeformType = settingsFormat.type;
···4644 example = [ "/data/media" "V,/home/alice/video" ];
4745 description = lib.mdDoc ''
4846 Directories to be scanned for media files.
4949- The prefixes `A,`,`V,` and
5050- `P,` restrict a directory to audio, video
5151- or image files. The directories must be accessible to the
5252- `minidlna` user account.
4747+ The `A,` `V,` `P,` prefixes restrict a directory to audio, video or image files.
4848+ The directories must be accessible to the `minidlna` user account.
5349 '';
5450 };
5551 options.notify_interval = mkOption {
···5753 default = 90000;
5854 description = lib.mdDoc ''
5955 The interval between announces (in seconds).
6060- Instead of waiting on announces, one can open port UDP 1900 or
6161- set `openFirewall` option to use SSDP discovery.
6262- Furthermore announce interval has now been set as 90000 in order
6363- to prevent disconnects with certain clients and to rely solely
6464- on the SSDP method.
5656+ Instead of waiting for announces, you should set `openFirewall` option to use SSDP discovery.
5757+ Furthermore, this option has been set to 90000 in order to prevent disconnects with certain
5858+ clients and relies solely on the discovery.
65596666- Lower values (e.g. 60 seconds) should be used if one does not
6767- want to utilize SSDP. By default miniDLNA will announce its
6868- presence on the network approximately every 15 minutes. Many
6969- people prefer shorter announce intervals on their home networks,
7070- especially when DLNA clients are started on demand.
7171-6060+ Lower values (e.g. 30 seconds) should be used if you can't use the discovery.
7261 Some relevant information can be found here:
7362 https://sourceforge.net/p/minidlna/discussion/879957/thread/1389d197/
7463 '';
···8675 };
8776 options.friendly_name = mkOption {
8877 type = types.str;
8989- default = "${config.networking.hostName} MiniDLNA";
7878+ default = config.networking.hostName;
9079 defaultText = literalExpression "config.networking.hostName";
9180 example = "rpi3";
9281 description = lib.mdDoc "Name that the DLNA server presents to clients.";
···116105 options.wide_links = mkOption {
117106 type = types.enum [ "yes" "no" ];
118107 default = "no";
119119- description = lib.mdDoc "Set this to yes to allow symlinks that point outside user-defined media_dirs.";
108108+ description = lib.mdDoc "Set this to yes to allow symlinks that point outside user-defined `media_dir`.";
120109 };
121110 };
122111 };
+320
nixos/modules/services/web-apps/dolibarr.nix
···11+{ config, pkgs, lib, ... }:
22+let
33+ inherit (lib) any boolToString concatStringsSep isBool isString literalExpression mapAttrsToList mkDefault mkEnableOption mkIf mkOption optionalAttrs types;
44+55+ package = pkgs.dolibarr.override { inherit (cfg) stateDir; };
66+77+ cfg = config.services.dolibarr;
88+ vhostCfg = config.services.nginx.virtualHosts."${cfg.domain}";
99+1010+ mkConfigFile = filename: settings:
1111+ let
1212+ # hack in special logic for secrets so we read them from a separate file avoiding the nix store
1313+ secretKeys = [ "force_install_databasepass" "dolibarr_main_db_pass" "dolibarr_main_instance_unique_id" ];
1414+1515+ toStr = k: v:
1616+ if (any (str: k == str) secretKeys) then v
1717+ else if isString v then "'${v}'"
1818+ else if isBool v then boolToString v
1919+ else if isNull v then "null"
2020+ else toString v
2121+ ;
2222+ in
2323+ pkgs.writeText filename ''
2424+ <?php
2525+ ${concatStringsSep "\n" (mapAttrsToList (k: v: "\$${k} = ${toStr k v};") settings)}
2626+ '';
2727+2828+ # see https://github.com/Dolibarr/dolibarr/blob/develop/htdocs/install/install.forced.sample.php for all possible values
2929+ install = {
3030+ force_install_noedit = 2;
3131+ force_install_main_data_root = "${cfg.stateDir}/documents";
3232+ force_install_nophpinfo = true;
3333+ force_install_lockinstall = "444";
3434+ force_install_distrib = "nixos";
3535+ force_install_type = "mysqli";
3636+ force_install_dbserver = cfg.database.host;
3737+ force_install_port = toString cfg.database.port;
3838+ force_install_database = cfg.database.name;
3939+ force_install_databaselogin = cfg.database.user;
4040+4141+ force_install_mainforcehttps = vhostCfg.forceSSL;
4242+ force_install_createuser = false;
4343+ force_install_dolibarrlogin = null;
4444+ } // optionalAttrs (cfg.database.passwordFile != null) {
4545+ force_install_databasepass = ''file_get_contents("${cfg.database.passwordFile}")'';
4646+ };
4747+in
4848+{
4949+ # interface
5050+ options.services.dolibarr = {
5151+ enable = mkEnableOption "dolibarr";
5252+5353+ domain = mkOption {
5454+ type = types.str;
5555+ default = "localhost";
5656+ description = ''
5757+ Domain name of your server.
5858+ '';
5959+ };
6060+6161+ user = mkOption {
6262+ type = types.str;
6363+ default = "dolibarr";
6464+ description = ''
6565+ User account under which dolibarr runs.
6666+6767+ <note><para>
6868+ If left as the default value this user will automatically be created
6969+ on system activation, otherwise you are responsible for
7070+ ensuring the user exists before the dolibarr application starts.
7171+ </para></note>
7272+ '';
7373+ };
7474+7575+ group = mkOption {
7676+ type = types.str;
7777+ default = "dolibarr";
7878+ description = ''
7979+ Group account under which dolibarr runs.
8080+8181+ <note><para>
8282+ If left as the default value this group will automatically be created
8383+ on system activation, otherwise you are responsible for
8484+ ensuring the group exists before the dolibarr application starts.
8585+ </para></note>
8686+ '';
8787+ };
8888+8989+ stateDir = mkOption {
9090+ type = types.str;
9191+ default = "/var/lib/dolibarr";
9292+ description = ''
9393+ State and configuration directory dolibarr will use.
9494+ '';
9595+ };
9696+9797+ database = {
9898+ host = mkOption {
9999+ type = types.str;
100100+ default = "localhost";
101101+ description = "Database host address.";
102102+ };
103103+ port = mkOption {
104104+ type = types.port;
105105+ default = 3306;
106106+ description = "Database host port.";
107107+ };
108108+ name = mkOption {
109109+ type = types.str;
110110+ default = "dolibarr";
111111+ description = "Database name.";
112112+ };
113113+ user = mkOption {
114114+ type = types.str;
115115+ default = "dolibarr";
116116+ description = "Database username.";
117117+ };
118118+ passwordFile = mkOption {
119119+ type = with types; nullOr path;
120120+ default = null;
121121+ example = "/run/keys/dolibarr-dbpassword";
122122+ description = "Database password file.";
123123+ };
124124+ createLocally = mkOption {
125125+ type = types.bool;
126126+ default = true;
127127+ description = "Create the database and database user locally.";
128128+ };
129129+ };
130130+131131+ settings = mkOption {
132132+ type = with types; (attrsOf (oneOf [ bool int str ]));
133133+ default = { };
134134+ description = lib.mdDoc "Dolibarr settings, see <https://github.com/Dolibarr/dolibarr/blob/develop/htdocs/conf/conf.php.example> for details.";
135135+ };
136136+137137+ nginx = mkOption {
138138+ type = types.nullOr (types.submodule (
139139+ lib.recursiveUpdate
140140+ (import ../web-servers/nginx/vhost-options.nix { inherit config lib; })
141141+ {
142142+ # enable encryption by default,
143143+ # as sensitive login and Dolibarr (ERP) data should not be transmitted in clear text.
144144+ options.forceSSL.default = true;
145145+ options.enableACME.default = true;
146146+ }
147147+ ));
148148+ default = null;
149149+ example = lib.literalExpression ''
150150+ {
151151+ serverAliases = [
152152+ "dolibarr.''${config.networking.domain}"
153153+ "erp.''${config.networking.domain}"
154154+ ];
155155+ enableACME = false;
156156+ }
157157+ '';
158158+ description = lib.mdDoc ''
159159+ With this option, you can customize an nginx virtual host which already has sensible defaults for Dolibarr.
160160+ Set to {} if you do not need any customization to the virtual host.
161161+ If enabled, then by default, the {option}`serverName` is
162162+ `''${domain}`,
163163+ SSL is active, and certificates are acquired via ACME.
164164+ If this is set to null (the default), no nginx virtualHost will be configured.
165165+ '';
166166+ };
167167+168168+ poolConfig = mkOption {
169169+ type = with types; attrsOf (oneOf [ str int bool ]);
170170+ default = {
171171+ "pm" = "dynamic";
172172+ "pm.max_children" = 32;
173173+ "pm.start_servers" = 2;
174174+ "pm.min_spare_servers" = 2;
175175+ "pm.max_spare_servers" = 4;
176176+ "pm.max_requests" = 500;
177177+ };
178178+ description = lib.mdDoc ''
179179+ Options for the Dolibarr PHP pool. See the documentation on [`php-fpm.conf`](https://www.php.net/manual/en/install.fpm.configuration.php)
180180+ for details on configuration directives.
181181+ '';
182182+ };
183183+ };
184184+185185+ # implementation
186186+ config = mkIf cfg.enable {
187187+188188+ assertions = [
189189+ { assertion = cfg.database.createLocally -> cfg.database.user == cfg.user;
190190+ message = "services.dolibarr.database.user must match services.dolibarr.user if the database is to be automatically provisioned";
191191+ }
192192+ ];
193193+194194+ services.dolibarr.settings = {
195195+ dolibarr_main_url_root = "https://${cfg.domain}";
196196+ dolibarr_main_document_root = "${package}/htdocs";
197197+ dolibarr_main_url_root_alt = "/custom";
198198+ dolibarr_main_data_root = "${cfg.stateDir}/documents";
199199+200200+ dolibarr_main_db_host = cfg.database.host;
201201+ dolibarr_main_db_port = toString cfg.database.port;
202202+ dolibarr_main_db_name = cfg.database.name;
203203+ dolibarr_main_db_prefix = "llx_";
204204+ dolibarr_main_db_user = cfg.database.user;
205205+ dolibarr_main_db_pass = mkIf (cfg.database.passwordFile != null) ''
206206+ file_get_contents("${cfg.database.passwordFile}")
207207+ '';
208208+ dolibarr_main_db_type = "mysqli";
209209+ dolibarr_main_db_character_set = mkDefault "utf8";
210210+ dolibarr_main_db_collation = mkDefault "utf8_unicode_ci";
211211+212212+ # Authentication settings
213213+ dolibarr_main_authentication = mkDefault "dolibarr";
214214+215215+ # Security settings
216216+ dolibarr_main_prod = true;
217217+ dolibarr_main_force_https = vhostCfg.forceSSL;
218218+ dolibarr_main_restrict_os_commands = "${pkgs.mariadb}/bin/mysqldump, ${pkgs.mariadb}/bin/mysql";
219219+ dolibarr_nocsrfcheck = false;
220220+ dolibarr_main_instance_unique_id = ''
221221+ file_get_contents("${cfg.stateDir}/dolibarr_main_instance_unique_id")
222222+ '';
223223+ dolibarr_mailing_limit_sendbyweb = false;
224224+ };
225225+226226+ systemd.tmpfiles.rules = [
227227+ "d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group}"
228228+ "d '${cfg.stateDir}/documents' 0750 ${cfg.user} ${cfg.group}"
229229+ "f '${cfg.stateDir}/conf.php' 0660 ${cfg.user} ${cfg.group}"
230230+ "L '${cfg.stateDir}/install.forced.php' - ${cfg.user} ${cfg.group} - ${mkConfigFile "install.forced.php" install}"
231231+ ];
232232+233233+ services.mysql = mkIf cfg.database.createLocally {
234234+ enable = mkDefault true;
235235+ package = mkDefault pkgs.mariadb;
236236+ ensureDatabases = [ cfg.database.name ];
237237+ ensureUsers = [
238238+ { name = cfg.database.user;
239239+ ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
240240+ }
241241+ ];
242242+ };
243243+244244+ services.nginx.enable = mkIf (cfg.nginx != null) true;
245245+ services.nginx.virtualHosts."${cfg.domain}" = mkIf (cfg.nginx != null) (lib.mkMerge [
246246+ cfg.nginx
247247+ ({
248248+ root = lib.mkForce "${package}/htdocs";
249249+ locations."/".index = "index.php";
250250+ locations."~ [^/]\\.php(/|$)" = {
251251+ extraConfig = ''
252252+ fastcgi_split_path_info ^(.+?\.php)(/.*)$;
253253+ fastcgi_pass unix:${config.services.phpfpm.pools.dolibarr.socket};
254254+ '';
255255+ };
256256+ })
257257+ ]);
258258+259259+ systemd.services."phpfpm-dolibarr".after = mkIf cfg.database.createLocally [ "mysql.service" ];
260260+ services.phpfpm.pools.dolibarr = {
261261+ inherit (cfg) user group;
262262+ phpPackage = pkgs.php.buildEnv {
263263+ extensions = { enabled, all }: enabled ++ [ all.calendar ];
264264+ # recommended by dolibarr web application
265265+ extraConfig = ''
266266+ session.use_strict_mode = 1
267267+ session.cookie_samesite = "Lax"
268268+ ; open_basedir = "${package}/htdocs, ${cfg.stateDir}"
269269+ allow_url_fopen = 0
270270+ disable_functions = "pcntl_alarm, pcntl_fork, pcntl_waitpid, pcntl_wait, pcntl_wifexited, pcntl_wifstopped, pcntl_wifsignaled, pcntl_wifcontinued, pcntl_wexitstatus, pcntl_wtermsig, pcntl_wstopsig, pcntl_signal, pcntl_signal_get_handler, pcntl_signal_dispatch, pcntl_get_last_error, pcntl_strerror, pcntl_sigprocmask, pcntl_sigwaitinfo, pcntl_sigtimedwait, pcntl_exec, pcntl_getpriority, pcntl_setpriority, pcntl_async_signals"
271271+ '';
272272+ };
273273+274274+ settings = {
275275+ "listen.mode" = "0660";
276276+ "listen.owner" = cfg.user;
277277+ "listen.group" = cfg.group;
278278+ } // cfg.poolConfig;
279279+ };
280280+281281+ # there are several challenges with dolibarr and NixOS which we can address here
282282+ # - the dolibarr installer cannot be entirely automated, though it can partially be by including a file called install.forced.php
283283+ # - the dolibarr installer requires write access to its config file during installation, though not afterwards
284284+ # - the dolibarr config file generally holds secrets generated by the installer, though the config file is a php file so we can read and write these secrets from an external file
285285+ systemd.services.dolibarr-config = {
286286+ description = "dolibarr configuration file management via NixOS";
287287+ wantedBy = [ "multi-user.target" ];
288288+289289+ script = ''
290290+ # extract the 'main instance unique id' secret that the dolibarr installer generated for us, store it in a file for use by our own NixOS generated configuration file
291291+ ${pkgs.php}/bin/php -r "include '${cfg.stateDir}/conf.php'; file_put_contents('${cfg.stateDir}/dolibarr_main_instance_unique_id', \$dolibarr_main_instance_unique_id);"
292292+293293+ # replace configuration file generated by installer with the NixOS generated configuration file
294294+ install -m 644 ${mkConfigFile "conf.php" cfg.settings} '${cfg.stateDir}/conf.php'
295295+ '';
296296+297297+ serviceConfig = {
298298+ Type = "oneshot";
299299+ User = cfg.user;
300300+ Group = cfg.group;
301301+ RemainAfterExit = "yes";
302302+ };
303303+304304+ unitConfig = {
305305+ ConditionFileNotEmpty = "${cfg.stateDir}/conf.php";
306306+ };
307307+ };
308308+309309+ users.users.dolibarr = mkIf (cfg.user == "dolibarr" ) {
310310+ isSystemUser = true;
311311+ group = cfg.group;
312312+ };
313313+314314+ users.groups = optionalAttrs (cfg.group == "dolibarr") {
315315+ dolibarr = { };
316316+ };
317317+318318+ users.users."${config.services.nginx.group}".extraGroups = [ cfg.group ];
319319+ };
320320+}
+59
nixos/tests/dolibarr.nix
···11+import ./make-test-python.nix ({ pkgs, lib, ... }: {
22+ name = "dolibarr";
33+ meta.maintainers = [ lib.maintainers.raitobezarius ];
44+55+ nodes.machine =
66+ { ... }:
77+ {
88+ services.dolibarr = {
99+ enable = true;
1010+ domain = "localhost";
1111+ nginx = {
1212+ forceSSL = false;
1313+ enableACME = false;
1414+ };
1515+ };
1616+1717+ networking.firewall.allowedTCPPorts = [ 80 ];
1818+ };
1919+2020+ testScript = ''
2121+ from html.parser import HTMLParser
2222+ start_all()
2323+2424+ csrf_token = None
2525+ class TokenParser(HTMLParser):
2626+ def handle_starttag(self, tag, attrs):
2727+ attrs = dict(attrs) # attrs is an assoc list originally
2828+ if tag == 'input' and attrs.get('name') == 'token':
2929+ csrf_token = attrs.get('value')
3030+ print(f'[+] Caught CSRF token: {csrf_token}')
3131+ def handle_endtag(self, tag): pass
3232+ def handle_data(self, data): pass
3333+3434+ machine.wait_for_unit("phpfpm-dolibarr.service")
3535+ machine.wait_for_unit("nginx.service")
3636+ machine.wait_for_open_port(80)
3737+ # Sanity checks on URLs.
3838+ # machine.succeed("curl -fL http://localhost/index.php")
3939+ # machine.succeed("curl -fL http://localhost/")
4040+ # Perform installation.
4141+ machine.succeed('curl -fL -X POST http://localhost/install/check.php -F selectlang=auto')
4242+ machine.succeed('curl -fL -X POST http://localhost/install/fileconf.php -F selectlang=auto')
4343+ # First time is to write the configuration file correctly.
4444+ machine.succeed('curl -fL -X POST http://localhost/install/step1.php -F "testpost=ok" -F "action=set" -F "selectlang=auto"')
4545+ # Now, we have a proper conf.php in $stateDir.
4646+ assert 'nixos' in machine.succeed("cat /var/lib/dolibarr/conf.php")
4747+ machine.succeed('curl -fL -X POST http://localhost/install/step2.php --data "testpost=ok&action=set&dolibarr_main_db_character_set=utf8&dolibarr_main_db_collation=utf8_unicode_ci&selectlang=auto"')
4848+ machine.succeed('curl -fL -X POST http://localhost/install/step4.php --data "testpost=ok&action=set&selectlang=auto"')
4949+ machine.succeed('curl -fL -X POST http://localhost/install/step5.php --data "testpost=ok&action=set&login=root&pass=hunter2&pass_verif=hunter2&selectlang=auto"')
5050+ # Now, we have installed the machine, let's verify we still have the right configuration.
5151+ assert 'nixos' in machine.succeed("cat /var/lib/dolibarr/conf.php")
5252+ # We do not want any redirect now as we have installed the machine.
5353+ machine.succeed('curl -f -X POST http://localhost')
5454+ # Test authentication to the webservice.
5555+ parser = TokenParser()
5656+ parser.feed(machine.succeed('curl -f -X GET http://localhost/index.php?mainmenu=login&username=root'))
5757+ machine.succeed(f'curl -f -X POST http://localhost/index.php?mainmenu=login&token={csrf_token}&username=root&password=hunter2')
5858+ '';
5959+})
···11+{ lib
22+, rustPlatform
33+, fetchFromGitHub
44+}:
55+66+rustPlatform.buildRustPackage rec {
77+ pname = "swayest-workstyle";
88+ version = "1.3.0";
99+1010+ src = fetchFromGitHub {
1111+ owner = "Lyr-7D1h";
1212+ repo = "swayest_workstyle";
1313+ rev = version;
1414+ sha256 = "sha256-LciTrYbmJV0y0H6QH88vTBXbDdDSx6FQtO4J/CFLjtY=";
1515+ };
1616+1717+ cargoSha256 = "sha256-34Ij3Hd1JI6d1vhv1XomFc9SFoB/6pbS39upLk+NeQM=";
1818+1919+ doCheck = false; # No tests
2020+2121+ meta = with lib; {
2222+ description = "Map sway workspace names to icons defined depending on the windows inside of the workspace";
2323+ homepage = "https://github.com/Lyr-7D1h/swayest_workstyle";
2424+ license = licenses.mit;
2525+ platforms = platforms.linux;
2626+ maintainers = with maintainers; [ miangraham ];
2727+ mainProgram = "sworkstyle";
2828+ };
2929+}
+2-2
pkgs/common-updater/unstable-updater.nix
···8899# This is an updater for unstable packages that should always use the latest
1010# commit.
1111-{ url ? null # The git url, if empty it will be set to src.url
1111+{ url ? null # The git url, if empty it will be set to src.gitRepoUrl
1212, branch ? null
1313, stableVersion ? false # Use version format according to RFC 107 (i.e. LAST_TAG+date=YYYY-MM-DD)
1414, tagPrefix ? "" # strip this prefix from a tag name when using stable version
···4646 esac
4747 done
48484949- # By default we set url to src.url
4949+ # By default we set url to src.gitRepoUrl
5050 if [[ -z "$url" ]]; then
5151 url="$(${nix}/bin/nix-instantiate $systemArg --eval -E \
5252 "with import ./. {}; $UPDATE_NIX_ATTR_PATH.src.gitRepoUrl" \
···5050 '';
51515252 meta = with lib; {
5353- broken = stdenv.isDarwin;
5353+ broken = stdenv.isDarwin; # see https://github.com/NixOS/nixpkgs/pull/189446 for partial fix
5454 description = "A video processing framework with the future in mind";
5555 homepage = "http://www.vapoursynth.com/";
5656 license = licenses.lgpl21;