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