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