1{ config, lib, pkgs, ... }:
2
3let
4
5 inherit (lib) mkDefault mkEnableOption mkForce mkIf mkMerge mkOption;
6 inherit (lib) literalExpression mapAttrs optional optionalString types;
7
8 cfg = config.services.limesurvey;
9 fpm = config.services.phpfpm.pools.limesurvey;
10
11 user = "limesurvey";
12 group = config.services.httpd.group;
13 stateDir = "/var/lib/limesurvey";
14
15 pkg = pkgs.limesurvey;
16
17 configType = with types; oneOf [ (attrsOf configType) str int bool ] // {
18 description = "limesurvey config type (str, int, bool or attribute set thereof)";
19 };
20
21 limesurveyConfig = pkgs.writeText "config.php" ''
22 <?php
23 return json_decode('${builtins.toJSON cfg.config}', true);
24 ?>
25 '';
26
27 mysqlLocal = cfg.database.createLocally && cfg.database.type == "mysql";
28 pgsqlLocal = cfg.database.createLocally && cfg.database.type == "pgsql";
29
30in
31{
32 # interface
33
34 options.services.limesurvey = {
35 enable = mkEnableOption (lib.mdDoc "Limesurvey web application");
36
37 encryptionKey = mkOption {
38 type = types.str;
39 default = "E17687FC77CEE247F0E22BB3ECF27FDE8BEC310A892347EC13013ABA11AA7EB5";
40 description = lib.mdDoc ''
41 This is a 32-byte key used to encrypt variables in the database.
42 You _must_ change this from the default value.
43 '';
44 };
45
46 encryptionNonce = mkOption {
47 type = types.str;
48 default = "1ACC8555619929DB91310BE848025A427B0F364A884FFA77";
49 description = lib.mdDoc ''
50 This is a 24-byte nonce used to encrypt variables in the database.
51 You _must_ change this from the default value.
52 '';
53 };
54
55 database = {
56 type = mkOption {
57 type = types.enum [ "mysql" "pgsql" "odbc" "mssql" ];
58 example = "pgsql";
59 default = "mysql";
60 description = lib.mdDoc "Database engine to use.";
61 };
62
63 dbEngine = mkOption {
64 type = types.enum [ "MyISAM" "InnoDB" ];
65 default = "InnoDB";
66 description = lib.mdDoc "Database storage engine to use.";
67 };
68
69 host = mkOption {
70 type = types.str;
71 default = "localhost";
72 description = lib.mdDoc "Database host address.";
73 };
74
75 port = mkOption {
76 type = types.port;
77 default = if cfg.database.type == "pgsql" then 5442 else 3306;
78 defaultText = literalExpression "3306";
79 description = lib.mdDoc "Database host port.";
80 };
81
82 name = mkOption {
83 type = types.str;
84 default = "limesurvey";
85 description = lib.mdDoc "Database name.";
86 };
87
88 user = mkOption {
89 type = types.str;
90 default = "limesurvey";
91 description = lib.mdDoc "Database user.";
92 };
93
94 passwordFile = mkOption {
95 type = types.nullOr types.path;
96 default = null;
97 example = "/run/keys/limesurvey-dbpassword";
98 description = lib.mdDoc ''
99 A file containing the password corresponding to
100 {option}`database.user`.
101 '';
102 };
103
104 socket = mkOption {
105 type = types.nullOr types.path;
106 default =
107 if mysqlLocal then "/run/mysqld/mysqld.sock"
108 else if pgsqlLocal then "/run/postgresql"
109 else null
110 ;
111 defaultText = literalExpression "/run/mysqld/mysqld.sock";
112 description = lib.mdDoc "Path to the unix socket file to use for authentication.";
113 };
114
115 createLocally = mkOption {
116 type = types.bool;
117 default = cfg.database.type == "mysql";
118 defaultText = literalExpression "true";
119 description = lib.mdDoc ''
120 Create the database and database user locally.
121 This currently only applies if database type "mysql" is selected.
122 '';
123 };
124 };
125
126 virtualHost = mkOption {
127 type = types.submodule (import ../web-servers/apache-httpd/vhost-options.nix);
128 example = literalExpression ''
129 {
130 hostName = "survey.example.org";
131 adminAddr = "webmaster@example.org";
132 forceSSL = true;
133 enableACME = true;
134 }
135 '';
136 description = lib.mdDoc ''
137 Apache configuration can be done by adapting `services.httpd.virtualHosts.<name>`.
138 See [](#opt-services.httpd.virtualHosts) for further information.
139 '';
140 };
141
142 poolConfig = mkOption {
143 type = with types; attrsOf (oneOf [ str int bool ]);
144 default = {
145 "pm" = "dynamic";
146 "pm.max_children" = 32;
147 "pm.start_servers" = 2;
148 "pm.min_spare_servers" = 2;
149 "pm.max_spare_servers" = 4;
150 "pm.max_requests" = 500;
151 };
152 description = lib.mdDoc ''
153 Options for the LimeSurvey PHP pool. See the documentation on `php-fpm.conf`
154 for details on configuration directives.
155 '';
156 };
157
158 config = mkOption {
159 type = configType;
160 default = {};
161 description = lib.mdDoc ''
162 LimeSurvey configuration. Refer to
163 <https://manual.limesurvey.org/Optional_settings>
164 for details on supported values.
165 '';
166 };
167 };
168
169 # implementation
170
171 config = mkIf cfg.enable {
172
173 assertions = [
174 { assertion = cfg.database.createLocally -> cfg.database.type == "mysql";
175 message = "services.limesurvey.createLocally is currently only supported for database type 'mysql'";
176 }
177 { assertion = cfg.database.createLocally -> cfg.database.user == user;
178 message = "services.limesurvey.database.user must be set to ${user} if services.limesurvey.database.createLocally is set true";
179 }
180 { assertion = cfg.database.createLocally -> cfg.database.socket != null;
181 message = "services.limesurvey.database.socket must be set if services.limesurvey.database.createLocally is set to true";
182 }
183 { assertion = cfg.database.createLocally -> cfg.database.passwordFile == null;
184 message = "a password cannot be specified if services.limesurvey.database.createLocally is set to true";
185 }
186 ];
187
188 services.limesurvey.config = mapAttrs (name: mkDefault) {
189 runtimePath = "${stateDir}/tmp/runtime";
190 components = {
191 db = {
192 connectionString = "${cfg.database.type}:dbname=${cfg.database.name};host=${if pgsqlLocal then cfg.database.socket else cfg.database.host};port=${toString cfg.database.port}" +
193 optionalString mysqlLocal ";socket=${cfg.database.socket}";
194 username = cfg.database.user;
195 password = mkIf (cfg.database.passwordFile != null) "file_get_contents(\"${toString cfg.database.passwordFile}\");";
196 tablePrefix = "limesurvey_";
197 };
198 assetManager.basePath = "${stateDir}/tmp/assets";
199 urlManager = {
200 urlFormat = "path";
201 showScriptName = false;
202 };
203 };
204 config = {
205 tempdir = "${stateDir}/tmp";
206 uploaddir = "${stateDir}/upload";
207 encryptionnonce = cfg.encryptionNonce;
208 encryptionsecretboxkey = cfg.encryptionKey;
209 force_ssl = mkIf (cfg.virtualHost.addSSL || cfg.virtualHost.forceSSL || cfg.virtualHost.onlySSL) "on";
210 config.defaultlang = "en";
211 };
212 };
213
214 services.mysql = mkIf mysqlLocal {
215 enable = true;
216 package = mkDefault pkgs.mariadb;
217 ensureDatabases = [ cfg.database.name ];
218 ensureUsers = [
219 { name = cfg.database.user;
220 ensurePermissions = {
221 "${cfg.database.name}.*" = "SELECT, CREATE, INSERT, UPDATE, DELETE, ALTER, DROP, INDEX";
222 };
223 }
224 ];
225 };
226
227 services.phpfpm.pools.limesurvey = {
228 inherit user group;
229 phpPackage = pkgs.php80;
230 phpEnv.DBENGINE = "${cfg.database.dbEngine}";
231 phpEnv.LIMESURVEY_CONFIG = "${limesurveyConfig}";
232 settings = {
233 "listen.owner" = config.services.httpd.user;
234 "listen.group" = config.services.httpd.group;
235 } // cfg.poolConfig;
236 };
237
238 services.httpd = {
239 enable = true;
240 adminAddr = mkDefault cfg.virtualHost.adminAddr;
241 extraModules = [ "proxy_fcgi" ];
242 virtualHosts.${cfg.virtualHost.hostName} = mkMerge [ cfg.virtualHost {
243 documentRoot = mkForce "${pkg}/share/limesurvey";
244 extraConfig = ''
245 Alias "/tmp" "${stateDir}/tmp"
246 <Directory "${stateDir}">
247 AllowOverride all
248 Require all granted
249 Options -Indexes +FollowSymlinks
250 </Directory>
251
252 Alias "/upload" "${stateDir}/upload"
253 <Directory "${stateDir}/upload">
254 AllowOverride all
255 Require all granted
256 Options -Indexes
257 </Directory>
258
259 <Directory "${pkg}/share/limesurvey">
260 <FilesMatch "\.php$">
261 <If "-f %{REQUEST_FILENAME}">
262 SetHandler "proxy:unix:${fpm.socket}|fcgi://localhost/"
263 </If>
264 </FilesMatch>
265
266 AllowOverride all
267 Options -Indexes
268 DirectoryIndex index.php
269 </Directory>
270 '';
271 } ];
272 };
273
274 systemd.tmpfiles.rules = [
275 "d ${stateDir} 0750 ${user} ${group} - -"
276 "d ${stateDir}/tmp 0750 ${user} ${group} - -"
277 "d ${stateDir}/tmp/assets 0750 ${user} ${group} - -"
278 "d ${stateDir}/tmp/runtime 0750 ${user} ${group} - -"
279 "d ${stateDir}/tmp/upload 0750 ${user} ${group} - -"
280 "C ${stateDir}/upload 0750 ${user} ${group} - ${pkg}/share/limesurvey/upload"
281 ];
282
283 systemd.services.limesurvey-init = {
284 wantedBy = [ "multi-user.target" ];
285 before = [ "phpfpm-limesurvey.service" ];
286 after = optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service";
287 environment.DBENGINE = "${cfg.database.dbEngine}";
288 environment.LIMESURVEY_CONFIG = limesurveyConfig;
289 script = ''
290 # update or install the database as required
291 ${pkgs.php80}/bin/php ${pkg}/share/limesurvey/application/commands/console.php updatedb || \
292 ${pkgs.php80}/bin/php ${pkg}/share/limesurvey/application/commands/console.php install admin password admin admin@example.com verbose
293 '';
294 serviceConfig = {
295 User = user;
296 Group = group;
297 Type = "oneshot";
298 };
299 };
300
301 systemd.services.httpd.after = optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service";
302
303 users.users.${user} = {
304 group = group;
305 isSystemUser = true;
306 };
307
308 };
309}