1{ config, lib, pkgs, ... }:
2
3let
4 inherit (lib) mkDefault mkEnableOption mkForce mkIf mkMerge mkOption types;
5 inherit (lib) concatStringsSep literalExpression mapAttrsToList optional optionalString;
6
7 cfg = config.services.moodle;
8 fpm = config.services.phpfpm.pools.moodle;
9
10 user = "moodle";
11 group = config.services.httpd.group;
12 stateDir = "/var/lib/moodle";
13
14 moodleConfig = pkgs.writeText "config.php" ''
15 <?php // Moodle configuration file
16
17 unset($CFG);
18 global $CFG;
19 $CFG = new stdClass();
20
21 $CFG->dbtype = '${ { mysql = "mariadb"; pgsql = "pgsql"; }.${cfg.database.type} }';
22 $CFG->dblibrary = 'native';
23 $CFG->dbhost = '${cfg.database.host}';
24 $CFG->dbname = '${cfg.database.name}';
25 $CFG->dbuser = '${cfg.database.user}';
26 ${optionalString (cfg.database.passwordFile != null) "$CFG->dbpass = file_get_contents('${cfg.database.passwordFile}');"}
27 $CFG->prefix = 'mdl_';
28 $CFG->dboptions = array (
29 'dbpersist' => 0,
30 'dbport' => '${toString cfg.database.port}',
31 ${optionalString (cfg.database.socket != null) "'dbsocket' => '${cfg.database.socket}',"}
32 'dbcollation' => 'utf8mb4_unicode_ci',
33 );
34
35 $CFG->wwwroot = '${if cfg.virtualHost.addSSL || cfg.virtualHost.forceSSL || cfg.virtualHost.onlySSL then "https" else "http"}://${cfg.virtualHost.hostName}';
36 $CFG->dataroot = '${stateDir}';
37 $CFG->admin = 'admin';
38
39 $CFG->directorypermissions = 02777;
40 $CFG->disableupdateautodeploy = true;
41
42 $CFG->pathtogs = '${pkgs.ghostscript}/bin/gs';
43 $CFG->pathtophp = '${phpExt}/bin/php';
44 $CFG->pathtodu = '${pkgs.coreutils}/bin/du';
45 $CFG->aspellpath = '${pkgs.aspell}/bin/aspell';
46 $CFG->pathtodot = '${pkgs.graphviz}/bin/dot';
47
48 ${cfg.extraConfig}
49
50 require_once('${cfg.package}/share/moodle/lib/setup.php');
51
52 // There is no php closing tag in this file,
53 // it is intentional because it prevents trailing whitespace problems!
54 '';
55
56 mysqlLocal = cfg.database.createLocally && cfg.database.type == "mysql";
57 pgsqlLocal = cfg.database.createLocally && cfg.database.type == "pgsql";
58
59 phpExt = pkgs.php81.buildEnv {
60 extensions = { all, ... }: with all; [ iconv mbstring curl openssl tokenizer soap ctype zip gd simplexml dom intl sqlite3 pgsql pdo_sqlite pdo_pgsql pdo_odbc pdo_mysql pdo mysqli session zlib xmlreader fileinfo filter opcache exif sodium ];
61 extraConfig = "max_input_vars = 5000";
62 };
63in
64{
65 # interface
66 options.services.moodle = {
67 enable = mkEnableOption (lib.mdDoc "Moodle web application");
68
69 package = mkOption {
70 type = types.package;
71 default = pkgs.moodle;
72 defaultText = literalExpression "pkgs.moodle";
73 description = lib.mdDoc "The Moodle package to use.";
74 };
75
76 initialPassword = mkOption {
77 type = types.str;
78 example = "correcthorsebatterystaple";
79 description = lib.mdDoc ''
80 Specifies the initial password for the admin, i.e. the password assigned if the user does not already exist.
81 The password specified here is world-readable in the Nix store, so it should be changed promptly.
82 '';
83 };
84
85 database = {
86 type = mkOption {
87 type = types.enum [ "mysql" "pgsql" ];
88 default = "mysql";
89 description = lib.mdDoc "Database engine to use.";
90 };
91
92 host = mkOption {
93 type = types.str;
94 default = "localhost";
95 description = lib.mdDoc "Database host address.";
96 };
97
98 port = mkOption {
99 type = types.port;
100 description = lib.mdDoc "Database host port.";
101 default = {
102 mysql = 3306;
103 pgsql = 5432;
104 }.${cfg.database.type};
105 defaultText = literalExpression "3306";
106 };
107
108 name = mkOption {
109 type = types.str;
110 default = "moodle";
111 description = lib.mdDoc "Database name.";
112 };
113
114 user = mkOption {
115 type = types.str;
116 default = "moodle";
117 description = lib.mdDoc "Database user.";
118 };
119
120 passwordFile = mkOption {
121 type = types.nullOr types.path;
122 default = null;
123 example = "/run/keys/moodle-dbpassword";
124 description = lib.mdDoc ''
125 A file containing the password corresponding to
126 {option}`database.user`.
127 '';
128 };
129
130 socket = mkOption {
131 type = types.nullOr types.path;
132 default =
133 if mysqlLocal then "/run/mysqld/mysqld.sock"
134 else if pgsqlLocal then "/run/postgresql"
135 else null;
136 defaultText = literalExpression "/run/mysqld/mysqld.sock";
137 description = lib.mdDoc "Path to the unix socket file to use for authentication.";
138 };
139
140 createLocally = mkOption {
141 type = types.bool;
142 default = true;
143 description = lib.mdDoc "Create the database and database user locally.";
144 };
145 };
146
147 virtualHost = mkOption {
148 type = types.submodule (import ../web-servers/apache-httpd/vhost-options.nix);
149 example = literalExpression ''
150 {
151 hostName = "moodle.example.org";
152 adminAddr = "webmaster@example.org";
153 forceSSL = true;
154 enableACME = true;
155 }
156 '';
157 description = lib.mdDoc ''
158 Apache configuration can be done by adapting {option}`services.httpd.virtualHosts`.
159 See [](#opt-services.httpd.virtualHosts) for further information.
160 '';
161 };
162
163 poolConfig = mkOption {
164 type = with types; attrsOf (oneOf [ str int bool ]);
165 default = {
166 "pm" = "dynamic";
167 "pm.max_children" = 32;
168 "pm.start_servers" = 2;
169 "pm.min_spare_servers" = 2;
170 "pm.max_spare_servers" = 4;
171 "pm.max_requests" = 500;
172 };
173 description = lib.mdDoc ''
174 Options for the Moodle PHP pool. See the documentation on `php-fpm.conf`
175 for details on configuration directives.
176 '';
177 };
178
179 extraConfig = mkOption {
180 type = types.lines;
181 default = "";
182 description = lib.mdDoc ''
183 Any additional text to be appended to the config.php
184 configuration file. This is a PHP script. For configuration
185 details, see <https://docs.moodle.org/37/en/Configuration_file>.
186 '';
187 example = ''
188 $CFG->disableupdatenotifications = true;
189 '';
190 };
191 };
192
193 # implementation
194 config = mkIf cfg.enable {
195
196 assertions = [
197 { assertion = cfg.database.createLocally -> cfg.database.user == user;
198 message = "services.moodle.database.user must be set to ${user} if services.moodle.database.createLocally is set true";
199 }
200 { assertion = cfg.database.createLocally -> cfg.database.passwordFile == null;
201 message = "a password cannot be specified if services.moodle.database.createLocally is set to true";
202 }
203 ];
204
205 services.mysql = mkIf mysqlLocal {
206 enable = true;
207 package = mkDefault pkgs.mariadb;
208 ensureDatabases = [ cfg.database.name ];
209 ensureUsers = [
210 { name = cfg.database.user;
211 ensurePermissions = {
212 "${cfg.database.name}.*" = "SELECT, INSERT, UPDATE, DELETE, CREATE, CREATE TEMPORARY TABLES, DROP, INDEX, ALTER";
213 };
214 }
215 ];
216 };
217
218 services.postgresql = mkIf pgsqlLocal {
219 enable = true;
220 ensureDatabases = [ cfg.database.name ];
221 ensureUsers = [
222 { name = cfg.database.user;
223 ensurePermissions = { "DATABASE ${cfg.database.name}" = "ALL PRIVILEGES"; };
224 }
225 ];
226 };
227
228 services.phpfpm.pools.moodle = {
229 inherit user group;
230 phpPackage = phpExt;
231 phpEnv.MOODLE_CONFIG = "${moodleConfig}";
232 phpOptions = ''
233 zend_extension = opcache.so
234 opcache.enable = 1
235 max_input_vars = 5000
236 '';
237 settings = {
238 "listen.owner" = config.services.httpd.user;
239 "listen.group" = config.services.httpd.group;
240 } // cfg.poolConfig;
241 };
242
243 services.httpd = {
244 enable = true;
245 adminAddr = mkDefault cfg.virtualHost.adminAddr;
246 extraModules = [ "proxy_fcgi" ];
247 virtualHosts.${cfg.virtualHost.hostName} = mkMerge [ cfg.virtualHost {
248 documentRoot = mkForce "${cfg.package}/share/moodle";
249 extraConfig = ''
250 <Directory "${cfg.package}/share/moodle">
251 <FilesMatch "\.php$">
252 <If "-f %{REQUEST_FILENAME}">
253 SetHandler "proxy:unix:${fpm.socket}|fcgi://localhost/"
254 </If>
255 </FilesMatch>
256 Options -Indexes
257 DirectoryIndex index.php
258 </Directory>
259 '';
260 } ];
261 };
262
263 systemd.tmpfiles.rules = [
264 "d '${stateDir}' 0750 ${user} ${group} - -"
265 ];
266
267 systemd.services.moodle-init = {
268 wantedBy = [ "multi-user.target" ];
269 before = [ "phpfpm-moodle.service" ];
270 after = optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service";
271 environment.MOODLE_CONFIG = moodleConfig;
272 script = ''
273 ${phpExt}/bin/php ${cfg.package}/share/moodle/admin/cli/check_database_schema.php && rc=$? || rc=$?
274
275 [ "$rc" == 1 ] && ${phpExt}/bin/php ${cfg.package}/share/moodle/admin/cli/upgrade.php \
276 --non-interactive \
277 --allow-unstable
278
279 [ "$rc" == 2 ] && ${phpExt}/bin/php ${cfg.package}/share/moodle/admin/cli/install_database.php \
280 --agree-license \
281 --adminpass=${cfg.initialPassword}
282
283 true
284 '';
285 serviceConfig = {
286 User = user;
287 Group = group;
288 Type = "oneshot";
289 };
290 };
291
292 systemd.services.moodle-cron = {
293 description = "Moodle cron service";
294 after = [ "moodle-init.service" ];
295 environment.MOODLE_CONFIG = moodleConfig;
296 serviceConfig = {
297 User = user;
298 Group = group;
299 ExecStart = "${phpExt}/bin/php ${cfg.package}/share/moodle/admin/cli/cron.php";
300 };
301 };
302
303 systemd.timers.moodle-cron = {
304 description = "Moodle cron timer";
305 wantedBy = [ "timers.target" ];
306 timerConfig = {
307 OnCalendar = "minutely";
308 };
309 };
310
311 systemd.services.httpd.after = optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service";
312
313 users.users.${user} = {
314 group = group;
315 isSystemUser = true;
316 };
317 };
318}