1{ config, pkgs, lib, ... }:
2
3let
4
5 inherit (lib) mkDefault mkEnableOption mkForce mkIf mkMerge mkOption;
6 inherit (lib) concatStringsSep literalExample 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 buildInputs = [ 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 "MediaWiki";
175
176 package = mkOption {
177 type = types.package;
178 default = pkgs.mediawiki;
179 description = "Which MediaWiki package to use.";
180 };
181
182 name = mkOption {
183 type = types.str;
184 default = "MediaWiki";
185 example = "Foobar Wiki";
186 description = "Name of the wiki.";
187 };
188
189 uploadsDir = mkOption {
190 type = types.nullOr types.path;
191 default = "${stateDir}/uploads";
192 description = ''
193 This directory is used for uploads of pictures. The directory passed here is automatically
194 created and permissions adjusted as required.
195 '';
196 };
197
198 passwordFile = mkOption {
199 type = types.path;
200 description = "A file containing the initial password for the admin user.";
201 example = "/run/keys/mediawiki-password";
202 };
203
204 skins = mkOption {
205 default = {};
206 type = types.attrsOf types.path;
207 description = ''
208 Attribute set of paths whose content is copied to the <filename>skins</filename>
209 subdirectory of the MediaWiki installation in addition to the default skins.
210 '';
211 };
212
213 extensions = mkOption {
214 default = {};
215 type = types.attrsOf (types.nullOr types.path);
216 description = ''
217 Attribute set of paths whose content is copied to the <filename>extensions</filename>
218 subdirectory of the MediaWiki installation and enabled in configuration.
219
220 Use <literal>null</literal> instead of path to enable extensions that are part of MediaWiki.
221 '';
222 example = literalExample ''
223 {
224 Matomo = pkgs.fetchzip {
225 url = "https://github.com/DaSchTour/matomo-mediawiki-extension/archive/v4.0.1.tar.gz";
226 sha256 = "0g5rd3zp0avwlmqagc59cg9bbkn3r7wx7p6yr80s644mj6dlvs1b";
227 };
228 ParserFunctions = null;
229 }
230 '';
231 };
232
233 database = {
234 type = mkOption {
235 type = types.enum [ "mysql" "postgres" "sqlite" "mssql" "oracle" ];
236 default = "mysql";
237 description = "Database engine to use. MySQL/MariaDB is the database of choice by MediaWiki developers.";
238 };
239
240 host = mkOption {
241 type = types.str;
242 default = "localhost";
243 description = "Database host address.";
244 };
245
246 port = mkOption {
247 type = types.port;
248 default = 3306;
249 description = "Database host port.";
250 };
251
252 name = mkOption {
253 type = types.str;
254 default = "mediawiki";
255 description = "Database name.";
256 };
257
258 user = mkOption {
259 type = types.str;
260 default = "mediawiki";
261 description = "Database user.";
262 };
263
264 passwordFile = mkOption {
265 type = types.nullOr types.path;
266 default = null;
267 example = "/run/keys/mediawiki-dbpassword";
268 description = ''
269 A file containing the password corresponding to
270 <option>database.user</option>.
271 '';
272 };
273
274 tablePrefix = mkOption {
275 type = types.nullOr types.str;
276 default = null;
277 description = ''
278 If you only have access to a single database and wish to install more than
279 one version of MediaWiki, or have other applications that also use the
280 database, you can give the table names a unique prefix to stop any naming
281 conflicts or confusion.
282 See <link xlink:href='https://www.mediawiki.org/wiki/Manual:$wgDBprefix'/>.
283 '';
284 };
285
286 socket = mkOption {
287 type = types.nullOr types.path;
288 default = if cfg.database.createLocally then "/run/mysqld/mysqld.sock" else null;
289 defaultText = "/run/mysqld/mysqld.sock";
290 description = "Path to the unix socket file to use for authentication.";
291 };
292
293 createLocally = mkOption {
294 type = types.bool;
295 default = cfg.database.type == "mysql";
296 defaultText = "true";
297 description = ''
298 Create the database and database user locally.
299 This currently only applies if database type "mysql" is selected.
300 '';
301 };
302 };
303
304 virtualHost = mkOption {
305 type = types.submodule (import ../web-servers/apache-httpd/vhost-options.nix);
306 example = literalExample ''
307 {
308 hostName = "mediawiki.example.org";
309 adminAddr = "webmaster@example.org";
310 forceSSL = true;
311 enableACME = true;
312 }
313 '';
314 description = ''
315 Apache configuration can be done by adapting <option>services.httpd.virtualHosts</option>.
316 See <xref linkend="opt-services.httpd.virtualHosts"/> for further information.
317 '';
318 };
319
320 poolConfig = mkOption {
321 type = with types; attrsOf (oneOf [ str int bool ]);
322 default = {
323 "pm" = "dynamic";
324 "pm.max_children" = 32;
325 "pm.start_servers" = 2;
326 "pm.min_spare_servers" = 2;
327 "pm.max_spare_servers" = 4;
328 "pm.max_requests" = 500;
329 };
330 description = ''
331 Options for the MediaWiki PHP pool. See the documentation on <literal>php-fpm.conf</literal>
332 for details on configuration directives.
333 '';
334 };
335
336 extraConfig = mkOption {
337 type = types.lines;
338 description = ''
339 Any additional text to be appended to MediaWiki's
340 LocalSettings.php configuration file. For configuration
341 settings, see <link xlink:href="https://www.mediawiki.org/wiki/Manual:Configuration_settings"/>.
342 '';
343 default = "";
344 example = ''
345 $wgEnableEmail = false;
346 '';
347 };
348
349 };
350 };
351
352 # implementation
353 config = mkIf cfg.enable {
354
355 assertions = [
356 { assertion = cfg.database.createLocally -> cfg.database.type == "mysql";
357 message = "services.mediawiki.createLocally is currently only supported for database type 'mysql'";
358 }
359 { assertion = cfg.database.createLocally -> cfg.database.user == user;
360 message = "services.mediawiki.database.user must be set to ${user} if services.mediawiki.database.createLocally is set true";
361 }
362 { assertion = cfg.database.createLocally -> cfg.database.socket != null;
363 message = "services.mediawiki.database.socket must be set if services.mediawiki.database.createLocally is set to true";
364 }
365 { assertion = cfg.database.createLocally -> cfg.database.passwordFile == null;
366 message = "a password cannot be specified if services.mediawiki.database.createLocally is set to true";
367 }
368 ];
369
370 services.mediawiki.skins = {
371 MonoBook = "${cfg.package}/share/mediawiki/skins/MonoBook";
372 Timeless = "${cfg.package}/share/mediawiki/skins/Timeless";
373 Vector = "${cfg.package}/share/mediawiki/skins/Vector";
374 };
375
376 services.mysql = mkIf cfg.database.createLocally {
377 enable = true;
378 package = mkDefault pkgs.mariadb;
379 ensureDatabases = [ cfg.database.name ];
380 ensureUsers = [
381 { name = cfg.database.user;
382 ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
383 }
384 ];
385 };
386
387 services.phpfpm.pools.mediawiki = {
388 inherit user group;
389 phpEnv.MEDIAWIKI_CONFIG = "${mediawikiConfig}";
390 settings = {
391 "listen.owner" = config.services.httpd.user;
392 "listen.group" = config.services.httpd.group;
393 } // cfg.poolConfig;
394 };
395
396 services.httpd = {
397 enable = true;
398 extraModules = [ "proxy_fcgi" ];
399 virtualHosts.${cfg.virtualHost.hostName} = mkMerge [ cfg.virtualHost {
400 documentRoot = mkForce "${pkg}/share/mediawiki";
401 extraConfig = ''
402 <Directory "${pkg}/share/mediawiki">
403 <FilesMatch "\.php$">
404 <If "-f %{REQUEST_FILENAME}">
405 SetHandler "proxy:unix:${fpm.socket}|fcgi://localhost/"
406 </If>
407 </FilesMatch>
408
409 Require all granted
410 DirectoryIndex index.php
411 AllowOverride All
412 </Directory>
413 '' + optionalString (cfg.uploadsDir != null) ''
414 Alias "/images" "${cfg.uploadsDir}"
415 <Directory "${cfg.uploadsDir}">
416 Require all granted
417 </Directory>
418 '';
419 } ];
420 };
421
422 systemd.tmpfiles.rules = [
423 "d '${stateDir}' 0750 ${user} ${group} - -"
424 "d '${cacheDir}' 0750 ${user} ${group} - -"
425 ] ++ optionals (cfg.uploadsDir != null) [
426 "d '${cfg.uploadsDir}' 0750 ${user} ${group} - -"
427 "Z '${cfg.uploadsDir}' 0750 ${user} ${group} - -"
428 ];
429
430 systemd.services.mediawiki-init = {
431 wantedBy = [ "multi-user.target" ];
432 before = [ "phpfpm-mediawiki.service" ];
433 after = optional cfg.database.createLocally "mysql.service";
434 script = ''
435 if ! test -e "${stateDir}/secret.key"; then
436 tr -dc A-Za-z0-9 </dev/urandom 2>/dev/null | head -c 64 > ${stateDir}/secret.key
437 fi
438
439 echo "exit( wfGetDB( DB_MASTER )->tableExists( 'user' ) ? 1 : 0 );" | \
440 ${pkgs.php}/bin/php ${pkg}/share/mediawiki/maintenance/eval.php --conf ${mediawikiConfig} && \
441 ${pkgs.php}/bin/php ${pkg}/share/mediawiki/maintenance/install.php \
442 --confpath /tmp \
443 --scriptpath / \
444 --dbserver ${cfg.database.host}${optionalString (cfg.database.socket != null) ":${cfg.database.socket}"} \
445 --dbport ${toString cfg.database.port} \
446 --dbname ${cfg.database.name} \
447 ${optionalString (cfg.database.tablePrefix != null) "--dbprefix ${cfg.database.tablePrefix}"} \
448 --dbuser ${cfg.database.user} \
449 ${optionalString (cfg.database.passwordFile != null) "--dbpassfile ${cfg.database.passwordFile}"} \
450 --passfile ${cfg.passwordFile} \
451 ${cfg.name} \
452 admin
453
454 ${pkgs.php}/bin/php ${pkg}/share/mediawiki/maintenance/update.php --conf ${mediawikiConfig} --quick
455 '';
456
457 serviceConfig = {
458 Type = "oneshot";
459 User = user;
460 Group = group;
461 PrivateTmp = true;
462 };
463 };
464
465 systemd.services.httpd.after = optional (cfg.database.createLocally && cfg.database.type == "mysql") "mysql.service";
466
467 users.users.${user} = {
468 group = group;
469 isSystemUser = true;
470 };
471
472 environment.systemPackages = [ mediawikiScripts ];
473 };
474}