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