1{ config, pkgs, lib, ... }:
2
3let
4
5 inherit (lib) mkDefault mkEnableOption mkForce mkIf mkMerge mkOption;
6 inherit (lib) concatStringsSep literalExpression mapAttrsToList optional optionals optionalString types;
7
8 cfg = config.services.mediawiki;
9 fpm = config.services.phpfpm.pools.mediawiki;
10 user = "mediawiki";
11 group = if cfg.webserver == "apache" then "apache" else "mediawiki";
12
13 cacheDir = "/var/cache/mediawiki";
14 stateDir = "/var/lib/mediawiki";
15
16 pkg = pkgs.stdenv.mkDerivation rec {
17 pname = "mediawiki-full";
18 version = src.version;
19 src = cfg.package;
20
21 installPhase = ''
22 mkdir -p $out
23 cp -r * $out/
24
25 rm -rf $out/share/mediawiki/skins/*
26 rm -rf $out/share/mediawiki/extensions/*
27
28 ${concatStringsSep "\n" (mapAttrsToList (k: v: ''
29 ln -s ${v} $out/share/mediawiki/skins/${k}
30 '') cfg.skins)}
31
32 ${concatStringsSep "\n" (mapAttrsToList (k: v: ''
33 ln -s ${if v != null then v else "$src/share/mediawiki/extensions/${k}"} $out/share/mediawiki/extensions/${k}
34 '') cfg.extensions)}
35 '';
36 };
37
38 mediawikiScripts = pkgs.runCommand "mediawiki-scripts" {
39 nativeBuildInputs = [ pkgs.makeWrapper ];
40 preferLocalBuild = true;
41 } ''
42 mkdir -p $out/bin
43 for i in changePassword.php createAndPromote.php userOptions.php edit.php nukePage.php update.php; do
44 makeWrapper ${pkgs.php}/bin/php $out/bin/mediawiki-$(basename $i .php) \
45 --set MEDIAWIKI_CONFIG ${mediawikiConfig} \
46 --add-flags ${pkg}/share/mediawiki/maintenance/$i
47 done
48 '';
49
50 dbAddr = if cfg.database.socket == null then
51 "${cfg.database.host}:${toString cfg.database.port}"
52 else if cfg.database.type == "mysql" then
53 "${cfg.database.host}:${cfg.database.socket}"
54 else if cfg.database.type == "postgres" then
55 "${cfg.database.socket}"
56 else
57 throw "Unsupported database type: ${cfg.database.type} for socket: ${cfg.database.socket}";
58
59 mediawikiConfig = pkgs.writeText "LocalSettings.php" ''
60 <?php
61 # Protect against web entry
62 if ( !defined( 'MEDIAWIKI' ) ) {
63 exit;
64 }
65
66 $wgSitename = "${cfg.name}";
67 $wgMetaNamespace = false;
68
69 ## The URL base path to the directory containing the wiki;
70 ## defaults for all runtime URL paths are based off of this.
71 ## For more information on customizing the URLs
72 ## (like /w/index.php/Page_title to /wiki/Page_title) please see:
73 ## https://www.mediawiki.org/wiki/Manual:Short_URL
74 $wgScriptPath = "";
75
76 ## The protocol and server name to use in fully-qualified URLs
77 $wgServer = "${cfg.url}";
78
79 ## The URL path to static resources (images, scripts, etc.)
80 $wgResourceBasePath = $wgScriptPath;
81
82 ## The URL path to the logo. Make sure you change this from the default,
83 ## or else you'll overwrite your logo when you upgrade!
84 $wgLogo = "$wgResourceBasePath/resources/assets/wiki.png";
85
86 ## UPO means: this is also a user preference option
87
88 $wgEnableEmail = true;
89 $wgEnableUserEmail = true; # UPO
90
91 $wgPasswordSender = "${cfg.passwordSender}";
92
93 $wgEnotifUserTalk = false; # UPO
94 $wgEnotifWatchlist = false; # UPO
95 $wgEmailAuthentication = true;
96
97 ## Database settings
98 $wgDBtype = "${cfg.database.type}";
99 $wgDBserver = "${dbAddr}";
100 $wgDBport = "${toString cfg.database.port}";
101 $wgDBname = "${cfg.database.name}";
102 $wgDBuser = "${cfg.database.user}";
103 ${optionalString (cfg.database.passwordFile != null) "$wgDBpassword = file_get_contents(\"${cfg.database.passwordFile}\");"}
104
105 ${optionalString (cfg.database.type == "mysql" && cfg.database.tablePrefix != null) ''
106 # MySQL specific settings
107 $wgDBprefix = "${cfg.database.tablePrefix}";
108 ''}
109
110 ${optionalString (cfg.database.type == "mysql") ''
111 # MySQL table options to use during installation or update
112 $wgDBTableOptions = "ENGINE=InnoDB, DEFAULT CHARSET=binary";
113 ''}
114
115 ## Shared memory settings
116 $wgMainCacheType = CACHE_NONE;
117 $wgMemCachedServers = [];
118
119 ${optionalString (cfg.uploadsDir != null) ''
120 $wgEnableUploads = true;
121 $wgUploadDirectory = "${cfg.uploadsDir}";
122 ''}
123
124 $wgUseImageMagick = true;
125 $wgImageMagickConvertCommand = "${pkgs.imagemagick}/bin/convert";
126
127 # InstantCommons allows wiki to use images from https://commons.wikimedia.org
128 $wgUseInstantCommons = false;
129
130 # Periodically send a pingback to https://www.mediawiki.org/ with basic data
131 # about this MediaWiki instance. The Wikimedia Foundation shares this data
132 # with MediaWiki developers to help guide future development efforts.
133 $wgPingback = true;
134
135 ## If you use ImageMagick (or any other shell command) on a
136 ## Linux server, this will need to be set to the name of an
137 ## available UTF-8 locale
138 $wgShellLocale = "C.UTF-8";
139
140 ## Set $wgCacheDirectory to a writable directory on the web server
141 ## to make your wiki go slightly faster. The directory should not
142 ## be publicly accessible from the web.
143 $wgCacheDirectory = "${cacheDir}";
144
145 # Site language code, should be one of the list in ./languages/data/Names.php
146 $wgLanguageCode = "en";
147
148 $wgSecretKey = file_get_contents("${stateDir}/secret.key");
149
150 # Changing this will log out all existing sessions.
151 $wgAuthenticationTokenVersion = "";
152
153 ## For attaching licensing metadata to pages, and displaying an
154 ## appropriate copyright notice / icon. GNU Free Documentation
155 ## License and Creative Commons licenses are supported so far.
156 $wgRightsPage = ""; # Set to the title of a wiki page that describes your license/copyright
157 $wgRightsUrl = "";
158 $wgRightsText = "";
159 $wgRightsIcon = "";
160
161 # Path to the GNU diff3 utility. Used for conflict resolution.
162 $wgDiff = "${pkgs.diffutils}/bin/diff";
163 $wgDiff3 = "${pkgs.diffutils}/bin/diff3";
164
165 # Enabled skins.
166 ${concatStringsSep "\n" (mapAttrsToList (k: v: "wfLoadSkin('${k}');") cfg.skins)}
167
168 # Enabled extensions.
169 ${concatStringsSep "\n" (mapAttrsToList (k: v: "wfLoadExtension('${k}');") cfg.extensions)}
170
171
172 # End of automatically generated settings.
173 # Add more configuration options below.
174
175 ${cfg.extraConfig}
176 '';
177
178in
179{
180 # interface
181 options = {
182 services.mediawiki = {
183
184 enable = mkEnableOption (lib.mdDoc "MediaWiki");
185
186 package = mkOption {
187 type = types.package;
188 default = pkgs.mediawiki;
189 defaultText = literalExpression "pkgs.mediawiki";
190 description = lib.mdDoc "Which MediaWiki package to use.";
191 };
192
193 finalPackage = mkOption {
194 type = types.package;
195 readOnly = true;
196 default = pkg;
197 defaultText = literalExpression "pkg";
198 description = lib.mdDoc ''
199 The final package used by the module. This is the package that will have extensions and skins installed.
200 '';
201 };
202
203 name = mkOption {
204 type = types.str;
205 default = "MediaWiki";
206 example = "Foobar Wiki";
207 description = lib.mdDoc "Name of the wiki.";
208 };
209
210 url = mkOption {
211 type = types.str;
212 default = if cfg.webserver == "apache" then
213 "${if cfg.httpd.virtualHost.addSSL || cfg.httpd.virtualHost.forceSSL || cfg.httpd.virtualHost.onlySSL then "https" else "http"}://${cfg.httpd.virtualHost.hostName}"
214 else
215 "http://localhost";
216 defaultText = literalExpression ''
217 if cfg.webserver == "apache" then
218 "''${if cfg.httpd.virtualHost.addSSL || cfg.httpd.virtualHost.forceSSL || cfg.httpd.virtualHost.onlySSL then "https" else "http"}://''${cfg.httpd.virtualHost.hostName}"
219 else
220 "http://localhost";
221 '';
222 example = "https://wiki.example.org";
223 description = lib.mdDoc "URL of the wiki.";
224 };
225
226 uploadsDir = mkOption {
227 type = types.nullOr types.path;
228 default = "${stateDir}/uploads";
229 description = lib.mdDoc ''
230 This directory is used for uploads of pictures. The directory passed here is automatically
231 created and permissions adjusted as required.
232 '';
233 };
234
235 passwordFile = mkOption {
236 type = types.path;
237 description = lib.mdDoc "A file containing the initial password for the admin user.";
238 example = "/run/keys/mediawiki-password";
239 };
240
241 passwordSender = mkOption {
242 type = types.str;
243 default =
244 if cfg.webserver == "apache" then
245 if cfg.httpd.virtualHost.adminAddr != null then
246 cfg.httpd.virtualHost.adminAddr
247 else
248 config.services.httpd.adminAddr else "root@localhost";
249 defaultText = literalExpression ''
250 if cfg.webserver == "apache" then
251 if cfg.httpd.virtualHost.adminAddr != null then
252 cfg.httpd.virtualHost.adminAddr
253 else
254 config.services.httpd.adminAddr else "root@localhost"
255 '';
256 description = lib.mdDoc "Contact address for password reset.";
257 };
258
259 skins = mkOption {
260 default = {};
261 type = types.attrsOf types.path;
262 description = lib.mdDoc ''
263 Attribute set of paths whose content is copied to the {file}`skins`
264 subdirectory of the MediaWiki installation in addition to the default skins.
265 '';
266 };
267
268 extensions = mkOption {
269 default = {};
270 type = types.attrsOf (types.nullOr types.path);
271 description = lib.mdDoc ''
272 Attribute set of paths whose content is copied to the {file}`extensions`
273 subdirectory of the MediaWiki installation and enabled in configuration.
274
275 Use `null` instead of path to enable extensions that are part of MediaWiki.
276 '';
277 example = literalExpression ''
278 {
279 Matomo = pkgs.fetchzip {
280 url = "https://github.com/DaSchTour/matomo-mediawiki-extension/archive/v4.0.1.tar.gz";
281 sha256 = "0g5rd3zp0avwlmqagc59cg9bbkn3r7wx7p6yr80s644mj6dlvs1b";
282 };
283 ParserFunctions = null;
284 }
285 '';
286 };
287
288 webserver = mkOption {
289 type = types.enum [ "apache" "none" ];
290 default = "apache";
291 description = lib.mdDoc "Webserver to use.";
292 };
293
294 database = {
295 type = mkOption {
296 type = types.enum [ "mysql" "postgres" "sqlite" "mssql" "oracle" ];
297 default = "mysql";
298 description = lib.mdDoc "Database engine to use. MySQL/MariaDB is the database of choice by MediaWiki developers.";
299 };
300
301 host = mkOption {
302 type = types.str;
303 default = "localhost";
304 description = lib.mdDoc "Database host address.";
305 };
306
307 port = mkOption {
308 type = types.port;
309 default = if cfg.database.type == "mysql" then 3306 else 5432;
310 defaultText = literalExpression "3306";
311 description = lib.mdDoc "Database host port.";
312 };
313
314 name = mkOption {
315 type = types.str;
316 default = "mediawiki";
317 description = lib.mdDoc "Database name.";
318 };
319
320 user = mkOption {
321 type = types.str;
322 default = "mediawiki";
323 description = lib.mdDoc "Database user.";
324 };
325
326 passwordFile = mkOption {
327 type = types.nullOr types.path;
328 default = null;
329 example = "/run/keys/mediawiki-dbpassword";
330 description = lib.mdDoc ''
331 A file containing the password corresponding to
332 {option}`database.user`.
333 '';
334 };
335
336 tablePrefix = mkOption {
337 type = types.nullOr types.str;
338 default = null;
339 description = lib.mdDoc ''
340 If you only have access to a single database and wish to install more than
341 one version of MediaWiki, or have other applications that also use the
342 database, you can give the table names a unique prefix to stop any naming
343 conflicts or confusion.
344 See <https://www.mediawiki.org/wiki/Manual:$wgDBprefix>.
345 '';
346 };
347
348 socket = mkOption {
349 type = types.nullOr types.path;
350 default = if (cfg.database.type == "mysql" && cfg.database.createLocally) then
351 "/run/mysqld/mysqld.sock"
352 else if (cfg.database.type == "postgres" && cfg.database.createLocally) then
353 "/run/postgresql"
354 else
355 null;
356 defaultText = literalExpression "/run/mysqld/mysqld.sock";
357 description = lib.mdDoc "Path to the unix socket file to use for authentication.";
358 };
359
360 createLocally = mkOption {
361 type = types.bool;
362 default = cfg.database.type == "mysql" || cfg.database.type == "postgres";
363 defaultText = literalExpression "true";
364 description = lib.mdDoc ''
365 Create the database and database user locally.
366 This currently only applies if database type "mysql" is selected.
367 '';
368 };
369 };
370
371 httpd.virtualHost = mkOption {
372 type = types.submodule (import ../web-servers/apache-httpd/vhost-options.nix);
373 example = literalExpression ''
374 {
375 hostName = "mediawiki.example.org";
376 adminAddr = "webmaster@example.org";
377 forceSSL = true;
378 enableACME = true;
379 }
380 '';
381 description = lib.mdDoc ''
382 Apache configuration can be done by adapting {option}`services.httpd.virtualHosts`.
383 See [](#opt-services.httpd.virtualHosts) for further information.
384 '';
385 };
386
387 poolConfig = mkOption {
388 type = with types; attrsOf (oneOf [ str int bool ]);
389 default = {
390 "pm" = "dynamic";
391 "pm.max_children" = 32;
392 "pm.start_servers" = 2;
393 "pm.min_spare_servers" = 2;
394 "pm.max_spare_servers" = 4;
395 "pm.max_requests" = 500;
396 };
397 description = lib.mdDoc ''
398 Options for the MediaWiki PHP pool. See the documentation on `php-fpm.conf`
399 for details on configuration directives.
400 '';
401 };
402
403 extraConfig = mkOption {
404 type = types.lines;
405 description = lib.mdDoc ''
406 Any additional text to be appended to MediaWiki's
407 LocalSettings.php configuration file. For configuration
408 settings, see <https://www.mediawiki.org/wiki/Manual:Configuration_settings>.
409 '';
410 default = "";
411 example = ''
412 $wgEnableEmail = false;
413 '';
414 };
415
416 };
417 };
418
419 imports = [
420 (lib.mkRenamedOptionModule [ "services" "mediawiki" "virtualHost" ] [ "services" "mediawiki" "httpd" "virtualHost" ])
421 ];
422
423 # implementation
424 config = mkIf cfg.enable {
425
426 assertions = [
427 { assertion = cfg.database.createLocally -> (cfg.database.type == "mysql" || cfg.database.type == "postgres");
428 message = "services.mediawiki.createLocally is currently only supported for database type 'mysql' and 'postgres'";
429 }
430 { assertion = cfg.database.createLocally -> cfg.database.user == user;
431 message = "services.mediawiki.database.user must be set to ${user} if services.mediawiki.database.createLocally is set true";
432 }
433 { assertion = cfg.database.createLocally -> cfg.database.socket != null;
434 message = "services.mediawiki.database.socket must be set if services.mediawiki.database.createLocally is set to true";
435 }
436 { assertion = cfg.database.createLocally -> cfg.database.passwordFile == null;
437 message = "a password cannot be specified if services.mediawiki.database.createLocally is set to true";
438 }
439 ];
440
441 services.mediawiki.skins = {
442 MonoBook = "${cfg.package}/share/mediawiki/skins/MonoBook";
443 Timeless = "${cfg.package}/share/mediawiki/skins/Timeless";
444 Vector = "${cfg.package}/share/mediawiki/skins/Vector";
445 };
446
447 services.mysql = mkIf (cfg.database.type == "mysql" && cfg.database.createLocally) {
448 enable = true;
449 package = mkDefault pkgs.mariadb;
450 ensureDatabases = [ cfg.database.name ];
451 ensureUsers = [{
452 name = cfg.database.user;
453 ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
454 }];
455 };
456
457 services.postgresql = mkIf (cfg.database.type == "postgres" && cfg.database.createLocally) {
458 enable = true;
459 ensureDatabases = [ cfg.database.name ];
460 ensureUsers = [{
461 name = cfg.database.user;
462 ensurePermissions = { "DATABASE \"${cfg.database.name}\"" = "ALL PRIVILEGES"; };
463 }];
464 };
465
466 services.phpfpm.pools.mediawiki = {
467 inherit user group;
468 phpEnv.MEDIAWIKI_CONFIG = "${mediawikiConfig}";
469 settings = (if (cfg.webserver == "apache") then {
470 "listen.owner" = config.services.httpd.user;
471 "listen.group" = config.services.httpd.group;
472 } else {
473 "listen.owner" = user;
474 "listen.group" = group;
475 }) // cfg.poolConfig;
476 };
477
478 services.httpd = lib.mkIf (cfg.webserver == "apache") {
479 enable = true;
480 extraModules = [ "proxy_fcgi" ];
481 virtualHosts.${cfg.httpd.virtualHost.hostName} = mkMerge [
482 cfg.httpd.virtualHost
483 {
484 documentRoot = mkForce "${pkg}/share/mediawiki";
485 extraConfig = ''
486 <Directory "${pkg}/share/mediawiki">
487 <FilesMatch "\.php$">
488 <If "-f %{REQUEST_FILENAME}">
489 SetHandler "proxy:unix:${fpm.socket}|fcgi://localhost/"
490 </If>
491 </FilesMatch>
492
493 Require all granted
494 DirectoryIndex index.php
495 AllowOverride All
496 </Directory>
497 '' + optionalString (cfg.uploadsDir != null) ''
498 Alias "/images" "${cfg.uploadsDir}"
499 <Directory "${cfg.uploadsDir}">
500 Require all granted
501 </Directory>
502 '';
503 }
504 ];
505 };
506
507 systemd.tmpfiles.rules = [
508 "d '${stateDir}' 0750 ${user} ${group} - -"
509 "d '${cacheDir}' 0750 ${user} ${group} - -"
510 ] ++ optionals (cfg.uploadsDir != null) [
511 "d '${cfg.uploadsDir}' 0750 ${user} ${group} - -"
512 "Z '${cfg.uploadsDir}' 0750 ${user} ${group} - -"
513 ];
514
515 systemd.services.mediawiki-init = {
516 wantedBy = [ "multi-user.target" ];
517 before = [ "phpfpm-mediawiki.service" ];
518 after = optional (cfg.database.type == "mysql" && cfg.database.createLocally) "mysql.service"
519 ++ optional (cfg.database.type == "postgres" && cfg.database.createLocally) "postgresql.service";
520 script = ''
521 if ! test -e "${stateDir}/secret.key"; then
522 tr -dc A-Za-z0-9 </dev/urandom 2>/dev/null | head -c 64 > ${stateDir}/secret.key
523 fi
524
525 echo "exit( wfGetDB( DB_MASTER )->tableExists( 'user' ) ? 1 : 0 );" | \
526 ${pkgs.php}/bin/php ${pkg}/share/mediawiki/maintenance/eval.php --conf ${mediawikiConfig} && \
527 ${pkgs.php}/bin/php ${pkg}/share/mediawiki/maintenance/install.php \
528 --confpath /tmp \
529 --scriptpath / \
530 --dbserver "${dbAddr}" \
531 --dbport ${toString cfg.database.port} \
532 --dbname ${cfg.database.name} \
533 ${optionalString (cfg.database.tablePrefix != null) "--dbprefix ${cfg.database.tablePrefix}"} \
534 --dbuser ${cfg.database.user} \
535 ${optionalString (cfg.database.passwordFile != null) "--dbpassfile ${cfg.database.passwordFile}"} \
536 --passfile ${cfg.passwordFile} \
537 --dbtype ${cfg.database.type} \
538 ${cfg.name} \
539 admin
540
541 ${pkgs.php}/bin/php ${pkg}/share/mediawiki/maintenance/update.php --conf ${mediawikiConfig} --quick
542 '';
543
544 serviceConfig = {
545 Type = "oneshot";
546 User = user;
547 Group = group;
548 PrivateTmp = true;
549 };
550 };
551
552 systemd.services.httpd.after = optional (cfg.webserver == "apache" && cfg.database.createLocally && cfg.database.type == "mysql") "mysql.service"
553 ++ optional (cfg.webserver == "apache" && cfg.database.createLocally && cfg.database.type == "postgres") "postgresql.service";
554
555 users.users.${user} = {
556 group = group;
557 isSystemUser = true;
558 };
559 users.groups.${group} = {};
560
561 environment.systemPackages = [ mediawikiScripts ];
562 };
563}