1 { config, lib, pkgs, ... }:
5 inherit (lib) mkDefault mkEnableOption mkForce mkIf mkMerge mkOption mkPackageOption;
6 inherit (lib) literalExpression mapAttrs optional optionalString types;
8 cfg = config.services.limesurvey;
9 fpm = config.services.phpfpm.pools.limesurvey;
12 group = config.services.httpd.group;
13 stateDir = "/var/lib/limesurvey";
15 configType = with types; oneOf [ (attrsOf configType) str int bool ] // {
16 description = "limesurvey config type (str, int, bool or attribute set thereof)";
19 limesurveyConfig = pkgs.writeText "config.php" ''
22 \json_decode('${builtins.toJSON cfg.config}', true),
25 'encryptionnonce' => \trim(\file_get_contents(\getenv('CREDENTIALS_DIRECTORY') . DIRECTORY_SEPARATOR . 'encryption_nonce')),
26 'encryptionsecretboxkey' => \trim(\file_get_contents(\getenv('CREDENTIALS_DIRECTORY') . DIRECTORY_SEPARATOR . 'encryption_key')),
33 mysqlLocal = cfg.database.createLocally && cfg.database.type == "mysql";
34 pgsqlLocal = cfg.database.createLocally && cfg.database.type == "pgsql";
40 options.services.limesurvey = {
41 enable = mkEnableOption "Limesurvey web application";
43 package = mkPackageOption pkgs "limesurvey" { };
45 encryptionKey = mkOption {
46 type = types.nullOr types.str;
50 This is a 32-byte key used to encrypt variables in the database.
51 You _must_ change this from the default value.
55 encryptionNonce = mkOption {
56 type = types.nullOr types.str;
60 This is a 24-byte nonce used to encrypt variables in the database.
61 You _must_ change this from the default value.
65 encryptionKeyFile = mkOption {
66 type = types.nullOr types.path;
69 32-byte key used to encrypt variables in the database.
71 Note: It should be string not a store path in order to prevent the password from being world readable
75 encryptionNonceFile = mkOption {
76 type = types.nullOr types.path;
79 24-byte used to encrypt variables in the database.
81 Note: It should be string not a store path in order to prevent the password from being world readable
87 type = types.enum [ "mysql" "pgsql" "odbc" "mssql" ];
90 description = "Database engine to use.";
94 type = types.enum [ "MyISAM" "InnoDB" ];
96 description = "Database storage engine to use.";
101 default = "localhost";
102 description = "Database host address.";
107 default = if cfg.database.type == "pgsql" then 5442 else 3306;
108 defaultText = literalExpression "3306";
109 description = "Database host port.";
114 default = "limesurvey";
115 description = "Database name.";
120 default = "limesurvey";
121 description = "Database user.";
124 passwordFile = mkOption {
125 type = types.nullOr types.path;
127 example = "/run/keys/limesurvey-dbpassword";
129 A file containing the password corresponding to
130 {option}`database.user`.
135 type = types.nullOr types.path;
137 if mysqlLocal then "/run/mysqld/mysqld.sock"
138 else if pgsqlLocal then "/run/postgresql"
141 defaultText = literalExpression "/run/mysqld/mysqld.sock";
142 description = "Path to the unix socket file to use for authentication.";
145 createLocally = mkOption {
147 default = cfg.database.type == "mysql";
148 defaultText = literalExpression "true";
150 Create the database and database user locally.
151 This currently only applies if database type "mysql" is selected.
156 virtualHost = mkOption {
157 type = types.submodule (import ../web-servers/apache-httpd/vhost-options.nix);
158 example = literalExpression ''
160 hostName = "survey.example.org";
161 adminAddr = "webmaster@example.org";
167 Apache configuration can be done by adapting `services.httpd.virtualHosts.<name>`.
168 See [](#opt-services.httpd.virtualHosts) for further information.
172 poolConfig = mkOption {
173 type = with types; attrsOf (oneOf [ str int bool ]);
176 "pm.max_children" = 32;
177 "pm.start_servers" = 2;
178 "pm.min_spare_servers" = 2;
179 "pm.max_spare_servers" = 4;
180 "pm.max_requests" = 500;
183 Options for the LimeSurvey PHP pool. See the documentation on `php-fpm.conf`
184 for details on configuration directives.
192 LimeSurvey configuration. Refer to
193 <https://manual.limesurvey.org/Optional_settings>
194 for details on supported values.
201 config = mkIf cfg.enable {
204 { assertion = cfg.database.createLocally -> cfg.database.type == "mysql";
205 message = "services.limesurvey.createLocally is currently only supported for database type 'mysql'";
207 { assertion = cfg.database.createLocally -> cfg.database.user == user;
208 message = "services.limesurvey.database.user must be set to ${user} if services.limesurvey.database.createLocally is set true";
210 { assertion = cfg.database.createLocally -> cfg.database.socket != null;
211 message = "services.limesurvey.database.socket must be set if services.limesurvey.database.createLocally is set to true";
213 { assertion = cfg.database.createLocally -> cfg.database.passwordFile == null;
214 message = "a password cannot be specified if services.limesurvey.database.createLocally is set to true";
216 { assertion = cfg.encryptionKey != null || cfg.encryptionKeyFile != null;
218 You must set `services.limesurvey.encryptionKeyFile` to a file containing a 32-character uppercase hex string.
220 If this message appears when updating your system, please turn off encryption
221 in the LimeSurvey interface and create backups before filling the key.
224 { assertion = cfg.encryptionNonce != null || cfg.encryptionNonceFile != null;
226 You must set `services.limesurvey.encryptionNonceFile` to a file containing a 24-character uppercase hex string.
228 If this message appears when updating your system, please turn off encryption
229 in the LimeSurvey interface and create backups before filling the nonce.
234 services.limesurvey.config = mapAttrs (name: mkDefault) {
235 runtimePath = "${stateDir}/tmp/runtime";
238 connectionString = "${cfg.database.type}:dbname=${cfg.database.name};host=${if pgsqlLocal then cfg.database.socket else cfg.database.host};port=${toString cfg.database.port}" +
239 optionalString mysqlLocal ";socket=${cfg.database.socket}";
240 username = cfg.database.user;
241 password = mkIf (cfg.database.passwordFile != null) "file_get_contents(\"${toString cfg.database.passwordFile}\");";
242 tablePrefix = "limesurvey_";
244 assetManager.basePath = "${stateDir}/tmp/assets";
247 showScriptName = false;
251 tempdir = "${stateDir}/tmp";
252 uploaddir = "${stateDir}/upload";
253 force_ssl = mkIf (cfg.virtualHost.addSSL || cfg.virtualHost.forceSSL || cfg.virtualHost.onlySSL) "on";
254 config.defaultlang = "en";
258 services.mysql = mkIf mysqlLocal {
260 package = mkDefault pkgs.mariadb;
261 ensureDatabases = [ cfg.database.name ];
263 { name = cfg.database.user;
264 ensurePermissions = {
265 "${cfg.database.name}.*" = "SELECT, CREATE, INSERT, UPDATE, DELETE, ALTER, DROP, INDEX";
271 services.phpfpm.pools.limesurvey = {
273 phpPackage = pkgs.php81;
274 phpEnv.DBENGINE = "${cfg.database.dbEngine}";
275 phpEnv.LIMESURVEY_CONFIG = "${limesurveyConfig}";
276 # App code cannot access credentials directly since the service starts
277 # with the root user so we copy the credentials to a place accessible to Limesurvey
278 phpEnv.CREDENTIALS_DIRECTORY = "${stateDir}/credentials";
280 "listen.owner" = config.services.httpd.user;
281 "listen.group" = config.services.httpd.group;
284 systemd.services.phpfpm-limesurvey.serviceConfig = {
285 ExecStartPre = pkgs.writeShellScript "limesurvey-phpfpm-exec-pre" ''
286 cp -f "''${CREDENTIALS_DIRECTORY}"/encryption_key "${stateDir}/credentials/encryption_key"
287 chown ${user}:${group} "${stateDir}/credentials/encryption_key"
288 cp -f "''${CREDENTIALS_DIRECTORY}"/encryption_nonce "${stateDir}/credentials/encryption_nonce"
289 chown ${user}:${group} "${stateDir}/credentials/encryption_nonce"
292 "encryption_key:${if cfg.encryptionKeyFile != null then cfg.encryptionKeyFile else pkgs.writeText "key" cfg.encryptionKey}"
293 "encryption_nonce:${if cfg.encryptionNonceFile != null then cfg.encryptionNonceFile else pkgs.writeText "nonce" cfg.encryptionKey}"
299 adminAddr = mkDefault cfg.virtualHost.adminAddr;
300 extraModules = [ "proxy_fcgi" ];
301 virtualHosts.${cfg.virtualHost.hostName} = mkMerge [ cfg.virtualHost {
302 documentRoot = mkForce "${cfg.package}/share/limesurvey";
304 Alias "/tmp" "${stateDir}/tmp"
305 <Directory "${stateDir}">
308 Options -Indexes +FollowSymlinks
311 Alias "/upload" "${stateDir}/upload"
312 <Directory "${stateDir}/upload">
318 <Directory "${cfg.package}/share/limesurvey">
319 <FilesMatch "\.php$">
320 <If "-f %{REQUEST_FILENAME}">
321 SetHandler "proxy:unix:${fpm.socket}|fcgi://localhost/"
327 DirectoryIndex index.php
333 systemd.tmpfiles.rules = [
334 "d ${stateDir} 0750 ${user} ${group} - -"
335 "d ${stateDir}/tmp 0750 ${user} ${group} - -"
336 "d ${stateDir}/tmp/assets 0750 ${user} ${group} - -"
337 "d ${stateDir}/tmp/runtime 0750 ${user} ${group} - -"
338 "d ${stateDir}/tmp/upload 0750 ${user} ${group} - -"
339 "d ${stateDir}/credentials 0700 ${user} ${group} - -"
340 "C ${stateDir}/upload 0750 ${user} ${group} - ${cfg.package}/share/limesurvey/upload"
343 systemd.services.limesurvey-init = {
344 wantedBy = [ "multi-user.target" ];
345 before = [ "phpfpm-limesurvey.service" ];
346 after = optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service";
347 environment.DBENGINE = "${cfg.database.dbEngine}";
348 environment.LIMESURVEY_CONFIG = limesurveyConfig;
350 # update or install the database as required
351 ${pkgs.php81}/bin/php ${cfg.package}/share/limesurvey/application/commands/console.php updatedb || \
352 ${pkgs.php81}/bin/php ${cfg.package}/share/limesurvey/application/commands/console.php install admin password admin admin@example.com verbose
359 "encryption_key:${if cfg.encryptionKeyFile != null then cfg.encryptionKeyFile else pkgs.writeText "key" cfg.encryptionKey}"
360 "encryption_nonce:${if cfg.encryptionNonceFile != null then cfg.encryptionNonceFile else pkgs.writeText "nonce" cfg.encryptionKey}"
365 systemd.services.httpd.after = optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service";
367 users.users.${user} = {