diff --git a/nixos/doc/manual/from_md/release-notes/rl-2211.section.xml b/nixos/doc/manual/from_md/release-notes/rl-2211.section.xml
index d434b76da214..252f727b7fc1 100644
--- a/nixos/doc/manual/from_md/release-notes/rl-2211.section.xml
+++ b/nixos/doc/manual/from_md/release-notes/rl-2211.section.xml
@@ -243,6 +243,14 @@
services.schleuder.
+
+
+ Dolibarr,
+ an enterprise resource planning and customer relationship
+ manager. Enable using
+ services.dolibarr.
+
+
expressvpn,
diff --git a/nixos/doc/manual/release-notes/rl-2211.section.md b/nixos/doc/manual/release-notes/rl-2211.section.md
index a90e0f896eca..2e7b734b348e 100644
--- a/nixos/doc/manual/release-notes/rl-2211.section.md
+++ b/nixos/doc/manual/release-notes/rl-2211.section.md
@@ -88,6 +88,8 @@ In addition to numerous new and upgraded packages, this release has the followin
- [schleuder](https://schleuder.org/), a mailing list manager with PGP support. Enable using [services.schleuder](#opt-services.schleuder.enable).
+- [Dolibarr](https://www.dolibarr.org/), an enterprise resource planning and customer relationship manager. Enable using [services.dolibarr](#opt-services.dolibarr.enable).
+
- [expressvpn](https://www.expressvpn.com), the CLI client for ExpressVPN. Available as [services.expressvpn](#opt-services.expressvpn.enable).
- [Grafana Tempo](https://www.grafana.com/oss/tempo/), a distributed tracing store. Available as [services.tempo](#opt-services.tempo.enable).
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 132bbdcd933b..12692d7bfbe6 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -1065,6 +1065,7 @@
./services/web-apps/discourse.nix
./services/web-apps/documize.nix
./services/web-apps/dokuwiki.nix
+ ./services/web-apps/dolibarr.nix
./services/web-apps/engelsystem.nix
./services/web-apps/ethercalc.nix
./services/web-apps/fluidd.nix
diff --git a/nixos/modules/services/web-apps/dolibarr.nix b/nixos/modules/services/web-apps/dolibarr.nix
new file mode 100644
index 000000000000..2b2e2a6214dc
--- /dev/null
+++ b/nixos/modules/services/web-apps/dolibarr.nix
@@ -0,0 +1,320 @@
+{ config, pkgs, lib, ... }:
+let
+ inherit (lib) any boolToString concatStringsSep isBool isString literalExpression mapAttrsToList mkDefault mkEnableOption mkIf mkOption optionalAttrs types;
+
+ package = pkgs.dolibarr.override { inherit (cfg) stateDir; };
+
+ cfg = config.services.dolibarr;
+ vhostCfg = config.services.nginx.virtualHosts."${cfg.domain}";
+
+ mkConfigFile = filename: settings:
+ let
+ # hack in special logic for secrets so we read them from a separate file avoiding the nix store
+ secretKeys = [ "force_install_databasepass" "dolibarr_main_db_pass" "dolibarr_main_instance_unique_id" ];
+
+ toStr = k: v:
+ if (any (str: k == str) secretKeys) then v
+ else if isString v then "'${v}'"
+ else if isBool v then boolToString v
+ else if isNull v then "null"
+ else toString v
+ ;
+ in
+ pkgs.writeText filename ''
+
+ If left as the default value this user will automatically be created
+ on system activation, otherwise you are responsible for
+ ensuring the user exists before the dolibarr application starts.
+
+ '';
+ };
+
+ group = mkOption {
+ type = types.str;
+ default = "dolibarr";
+ description = ''
+ Group account under which dolibarr runs.
+
+
+ If left as the default value this group will automatically be created
+ on system activation, otherwise you are responsible for
+ ensuring the group exists before the dolibarr application starts.
+
+ '';
+ };
+
+ stateDir = mkOption {
+ type = types.str;
+ default = "/var/lib/dolibarr";
+ description = ''
+ State and configuration directory dolibarr will use.
+ '';
+ };
+
+ database = {
+ host = mkOption {
+ type = types.str;
+ default = "localhost";
+ description = "Database host address.";
+ };
+ port = mkOption {
+ type = types.port;
+ default = 3306;
+ description = "Database host port.";
+ };
+ name = mkOption {
+ type = types.str;
+ default = "dolibarr";
+ description = "Database name.";
+ };
+ user = mkOption {
+ type = types.str;
+ default = "dolibarr";
+ description = "Database username.";
+ };
+ passwordFile = mkOption {
+ type = with types; nullOr path;
+ default = null;
+ example = "/run/keys/dolibarr-dbpassword";
+ description = "Database password file.";
+ };
+ createLocally = mkOption {
+ type = types.bool;
+ default = true;
+ description = "Create the database and database user locally.";
+ };
+ };
+
+ settings = mkOption {
+ type = with types; (attrsOf (oneOf [ bool int str ]));
+ default = { };
+ description = lib.mdDoc "Dolibarr settings, see for details.";
+ };
+
+ nginx = mkOption {
+ type = types.nullOr (types.submodule (
+ lib.recursiveUpdate
+ (import ../web-servers/nginx/vhost-options.nix { inherit config lib; })
+ {
+ # enable encryption by default,
+ # as sensitive login and Dolibarr (ERP) data should not be transmitted in clear text.
+ options.forceSSL.default = true;
+ options.enableACME.default = true;
+ }
+ ));
+ default = null;
+ example = lib.literalExpression ''
+ {
+ serverAliases = [
+ "dolibarr.''${config.networking.domain}"
+ "erp.''${config.networking.domain}"
+ ];
+ enableACME = false;
+ }
+ '';
+ description = lib.mdDoc ''
+ With this option, you can customize an nginx virtual host which already has sensible defaults for Dolibarr.
+ Set to {} if you do not need any customization to the virtual host.
+ If enabled, then by default, the {option}`serverName` is
+ `''${domain}`,
+ SSL is active, and certificates are acquired via ACME.
+ If this is set to null (the default), no nginx virtualHost will be configured.
+ '';
+ };
+
+ poolConfig = mkOption {
+ type = with types; attrsOf (oneOf [ str int bool ]);
+ default = {
+ "pm" = "dynamic";
+ "pm.max_children" = 32;
+ "pm.start_servers" = 2;
+ "pm.min_spare_servers" = 2;
+ "pm.max_spare_servers" = 4;
+ "pm.max_requests" = 500;
+ };
+ description = lib.mdDoc ''
+ Options for the Dolibarr PHP pool. See the documentation on [`php-fpm.conf`](https://www.php.net/manual/en/install.fpm.configuration.php)
+ for details on configuration directives.
+ '';
+ };
+ };
+
+ # implementation
+ config = mkIf cfg.enable {
+
+ assertions = [
+ { assertion = cfg.database.createLocally -> cfg.database.user == cfg.user;
+ message = "services.dolibarr.database.user must match services.dolibarr.user if the database is to be automatically provisioned";
+ }
+ ];
+
+ services.dolibarr.settings = {
+ dolibarr_main_url_root = "https://${cfg.domain}";
+ dolibarr_main_document_root = "${package}/htdocs";
+ dolibarr_main_url_root_alt = "/custom";
+ dolibarr_main_data_root = "${cfg.stateDir}/documents";
+
+ dolibarr_main_db_host = cfg.database.host;
+ dolibarr_main_db_port = toString cfg.database.port;
+ dolibarr_main_db_name = cfg.database.name;
+ dolibarr_main_db_prefix = "llx_";
+ dolibarr_main_db_user = cfg.database.user;
+ dolibarr_main_db_pass = mkIf (cfg.database.passwordFile != null) ''
+ file_get_contents("${cfg.database.passwordFile}")
+ '';
+ dolibarr_main_db_type = "mysqli";
+ dolibarr_main_db_character_set = mkDefault "utf8";
+ dolibarr_main_db_collation = mkDefault "utf8_unicode_ci";
+
+ # Authentication settings
+ dolibarr_main_authentication = mkDefault "dolibarr";
+
+ # Security settings
+ dolibarr_main_prod = true;
+ dolibarr_main_force_https = vhostCfg.forceSSL;
+ dolibarr_main_restrict_os_commands = "${pkgs.mariadb}/bin/mysqldump, ${pkgs.mariadb}/bin/mysql";
+ dolibarr_nocsrfcheck = false;
+ dolibarr_main_instance_unique_id = ''
+ file_get_contents("${cfg.stateDir}/dolibarr_main_instance_unique_id")
+ '';
+ dolibarr_mailing_limit_sendbyweb = false;
+ };
+
+ systemd.tmpfiles.rules = [
+ "d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group}"
+ "d '${cfg.stateDir}/documents' 0750 ${cfg.user} ${cfg.group}"
+ "f '${cfg.stateDir}/conf.php' 0660 ${cfg.user} ${cfg.group}"
+ "L '${cfg.stateDir}/install.forced.php' - ${cfg.user} ${cfg.group} - ${mkConfigFile "install.forced.php" install}"
+ ];
+
+ services.mysql = mkIf cfg.database.createLocally {
+ enable = mkDefault true;
+ package = mkDefault pkgs.mariadb;
+ ensureDatabases = [ cfg.database.name ];
+ ensureUsers = [
+ { name = cfg.database.user;
+ ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
+ }
+ ];
+ };
+
+ services.nginx.enable = mkIf (cfg.nginx != null) true;
+ services.nginx.virtualHosts."${cfg.domain}" = mkIf (cfg.nginx != null) (lib.mkMerge [
+ cfg.nginx
+ ({
+ root = lib.mkForce "${package}/htdocs";
+ locations."/".index = "index.php";
+ locations."~ [^/]\\.php(/|$)" = {
+ extraConfig = ''
+ fastcgi_split_path_info ^(.+?\.php)(/.*)$;
+ fastcgi_pass unix:${config.services.phpfpm.pools.dolibarr.socket};
+ '';
+ };
+ })
+ ]);
+
+ systemd.services."phpfpm-dolibarr".after = mkIf cfg.database.createLocally [ "mysql.service" ];
+ services.phpfpm.pools.dolibarr = {
+ inherit (cfg) user group;
+ phpPackage = pkgs.php.buildEnv {
+ extensions = { enabled, all }: enabled ++ [ all.calendar ];
+ # recommended by dolibarr web application
+ extraConfig = ''
+ session.use_strict_mode = 1
+ session.cookie_samesite = "Lax"
+ ; open_basedir = "${package}/htdocs, ${cfg.stateDir}"
+ allow_url_fopen = 0
+ 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"
+ '';
+ };
+
+ settings = {
+ "listen.mode" = "0660";
+ "listen.owner" = cfg.user;
+ "listen.group" = cfg.group;
+ } // cfg.poolConfig;
+ };
+
+ # there are several challenges with dolibarr and NixOS which we can address here
+ # - the dolibarr installer cannot be entirely automated, though it can partially be by including a file called install.forced.php
+ # - the dolibarr installer requires write access to its config file during installation, though not afterwards
+ # - 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
+ systemd.services.dolibarr-config = {
+ description = "dolibarr configuration file management via NixOS";
+ wantedBy = [ "multi-user.target" ];
+
+ script = ''
+ # 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
+ ${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);"
+
+ # replace configuration file generated by installer with the NixOS generated configuration file
+ install -m 644 ${mkConfigFile "conf.php" cfg.settings} '${cfg.stateDir}/conf.php'
+ '';
+
+ serviceConfig = {
+ Type = "oneshot";
+ User = cfg.user;
+ Group = cfg.group;
+ RemainAfterExit = "yes";
+ };
+
+ unitConfig = {
+ ConditionFileNotEmpty = "${cfg.stateDir}/conf.php";
+ };
+ };
+
+ users.users.dolibarr = mkIf (cfg.user == "dolibarr" ) {
+ isSystemUser = true;
+ group = cfg.group;
+ };
+
+ users.groups = optionalAttrs (cfg.group == "dolibarr") {
+ dolibarr = { };
+ };
+
+ users.users."${config.services.nginx.group}".extraGroups = [ cfg.group ];
+ };
+}
diff --git a/nixos/tests/dolibarr.nix b/nixos/tests/dolibarr.nix
new file mode 100644
index 000000000000..2f012a0c67da
--- /dev/null
+++ b/nixos/tests/dolibarr.nix
@@ -0,0 +1,59 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
+ name = "dolibarr";
+ meta.maintainers = [ lib.maintainers.raitobezarius ];
+
+ nodes.machine =
+ { ... }:
+ {
+ services.dolibarr = {
+ enable = true;
+ domain = "localhost";
+ nginx = {
+ forceSSL = false;
+ enableACME = false;
+ };
+ };
+
+ networking.firewall.allowedTCPPorts = [ 80 ];
+ };
+
+ testScript = ''
+ from html.parser import HTMLParser
+ start_all()
+
+ csrf_token = None
+ class TokenParser(HTMLParser):
+ def handle_starttag(self, tag, attrs):
+ attrs = dict(attrs) # attrs is an assoc list originally
+ if tag == 'input' and attrs.get('name') == 'token':
+ csrf_token = attrs.get('value')
+ print(f'[+] Caught CSRF token: {csrf_token}')
+ def handle_endtag(self, tag): pass
+ def handle_data(self, data): pass
+
+ machine.wait_for_unit("phpfpm-dolibarr.service")
+ machine.wait_for_unit("nginx.service")
+ machine.wait_for_open_port(80)
+ # Sanity checks on URLs.
+ # machine.succeed("curl -fL http://localhost/index.php")
+ # machine.succeed("curl -fL http://localhost/")
+ # Perform installation.
+ machine.succeed('curl -fL -X POST http://localhost/install/check.php -F selectlang=auto')
+ machine.succeed('curl -fL -X POST http://localhost/install/fileconf.php -F selectlang=auto')
+ # First time is to write the configuration file correctly.
+ machine.succeed('curl -fL -X POST http://localhost/install/step1.php -F "testpost=ok" -F "action=set" -F "selectlang=auto"')
+ # Now, we have a proper conf.php in $stateDir.
+ assert 'nixos' in machine.succeed("cat /var/lib/dolibarr/conf.php")
+ 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"')
+ machine.succeed('curl -fL -X POST http://localhost/install/step4.php --data "testpost=ok&action=set&selectlang=auto"')
+ 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"')
+ # Now, we have installed the machine, let's verify we still have the right configuration.
+ assert 'nixos' in machine.succeed("cat /var/lib/dolibarr/conf.php")
+ # We do not want any redirect now as we have installed the machine.
+ machine.succeed('curl -f -X POST http://localhost')
+ # Test authentication to the webservice.
+ parser = TokenParser()
+ parser.feed(machine.succeed('curl -f -X GET http://localhost/index.php?mainmenu=login&username=root'))
+ machine.succeed(f'curl -f -X POST http://localhost/index.php?mainmenu=login&token={csrf_token}&username=root&password=hunter2')
+ '';
+})
diff --git a/pkgs/servers/web-apps/dolibarr/default.nix b/pkgs/servers/web-apps/dolibarr/default.nix
new file mode 100644
index 000000000000..5c4256155ddc
--- /dev/null
+++ b/pkgs/servers/web-apps/dolibarr/default.nix
@@ -0,0 +1,41 @@
+{ stdenv, lib, fetchFromGitHub, nixosTests, stateDir ? "/var/lib/dolibarr" }:
+
+stdenv.mkDerivation rec {
+ pname = "dolibarr";
+ version = "15.0.3";
+
+ src = fetchFromGitHub {
+ owner = "Dolibarr";
+ repo = "dolibarr";
+ rev = version;
+ sha256 = "sha256-HMOYj93ZvqM0FQjt313yuGj/r9ELqQlnNkg/CxrBjRM=";
+ };
+
+ dontBuild = true;
+
+ postPatch = ''
+ find . -type f -name "*.php" -print0 | xargs -0 sed -i 's|/etc/dolibarr|${stateDir}|g'
+
+ substituteInPlace htdocs/filefunc.inc.php \
+ --replace '//$conffile = ' '$conffile = ' \
+ --replace '//$conffiletoshow = ' '$conffiletoshow = '
+
+ substituteInPlace htdocs/install/inc.php \
+ --replace '//$conffile = ' '$conffile = ' \
+ --replace '//$conffiletoshow = ' '$conffiletoshow = '
+ '';
+
+ installPhase = ''
+ mkdir -p "$out"
+ cp -r * $out
+ '';
+
+ passthru.tests = { inherit (nixosTests) dolibarr; };
+
+ meta = with lib; {
+ description = "A enterprise resource planning (ERP) and customer relationship manager (CRM) server";
+ homepage = "https://dolibarr.org/";
+ license = licenses.gpl3Plus;
+ maintainers = [ maintainers.raitobezarius ];
+ };
+}
diff --git a/pkgs/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix
index 3580e83f4fb6..96942304fca3 100644
--- a/pkgs/top-level/all-packages.nix
+++ b/pkgs/top-level/all-packages.nix
@@ -3641,6 +3641,8 @@ with pkgs;
dokuwiki = callPackage ../servers/web-apps/dokuwiki { };
+ dolibarr = callPackage ../servers/web-apps/dolibarr { };
+
doppler = callPackage ../tools/security/doppler {};
dosage = callPackage ../applications/graphics/dosage { };