1 { config, pkgs, lib, ... }:
5 inherit (lib) mkDefault mkEnableOption mkPackageOption mkForce mkIf mkMerge mkOption;
6 inherit (lib) concatStringsSep literalExpression mapAttrsToList optional optionals optionalString types;
8 cfg = config.services.mediawiki;
9 fpm = config.services.phpfpm.pools.mediawiki;
12 if cfg.webserver == "apache" then
13 config.services.httpd.group
14 else if cfg.webserver == "nginx" then
15 config.services.nginx.group
18 cacheDir = "/var/cache/mediawiki";
19 stateDir = "/var/lib/mediawiki";
21 # https://www.mediawiki.org/wiki/Compatibility
24 pkg = pkgs.stdenv.mkDerivation rec {
25 pname = "mediawiki-full";
26 inherit (src) version;
33 # try removing directories before symlinking to allow overwriting any builtin extension or skin
34 ${concatStringsSep "\n" (mapAttrsToList (k: v: ''
35 rm -rf $out/share/mediawiki/skins/${k}
36 ln -s ${v} $out/share/mediawiki/skins/${k}
39 ${concatStringsSep "\n" (mapAttrsToList (k: v: ''
40 rm -rf $out/share/mediawiki/extensions/${k}
41 ln -s ${if v != null then v else "$src/share/mediawiki/extensions/${k}"} $out/share/mediawiki/extensions/${k}
46 mediawikiScripts = pkgs.runCommand "mediawiki-scripts" {
47 nativeBuildInputs = [ pkgs.makeWrapper ];
48 preferLocalBuild = true;
51 for i in changePassword.php createAndPromote.php resetUserEmail.php userOptions.php edit.php nukePage.php update.php; do
52 makeWrapper ${php}/bin/php $out/bin/mediawiki-$(basename $i .php) \
53 --set MEDIAWIKI_CONFIG ${mediawikiConfig} \
54 --add-flags ${pkg}/share/mediawiki/maintenance/$i
58 dbAddr = if cfg.database.socket == null then
59 "${cfg.database.host}:${toString cfg.database.port}"
60 else if cfg.database.type == "mysql" then
61 "${cfg.database.host}:${cfg.database.socket}"
62 else if cfg.database.type == "postgres" then
63 "${cfg.database.socket}"
65 throw "Unsupported database type: ${cfg.database.type} for socket: ${cfg.database.socket}";
67 mediawikiConfig = pkgs.writeTextFile {
68 name = "LocalSettings.php";
70 ${php}/bin/php --syntax-check "$target"
74 # Protect against web entry
75 if ( !defined( 'MEDIAWIKI' ) ) {
79 $wgSitename = "${cfg.name}";
80 $wgMetaNamespace = false;
82 ## The URL base path to the directory containing the wiki;
83 ## defaults for all runtime URL paths are based off of this.
84 ## For more information on customizing the URLs
85 ## (like /w/index.php/Page_title to /wiki/Page_title) please see:
86 ## https://www.mediawiki.org/wiki/Manual:Short_URL
87 $wgScriptPath = "${lib.optionalString (cfg.webserver == "nginx") "/w"}";
89 ## The protocol and server name to use in fully-qualified URLs
90 $wgServer = "${cfg.url}";
92 ## The URL path to static resources (images, scripts, etc.)
93 $wgResourceBasePath = $wgScriptPath;
95 ${lib.optionalString (cfg.webserver == "nginx") ''
96 $wgArticlePath = "/wiki/$1";
97 $wgUsePathInfo = true;
100 ## The URL path to the logo. Make sure you change this from the default,
101 ## or else you'll overwrite your logo when you upgrade!
102 $wgLogo = "$wgResourceBasePath/resources/assets/wiki.png";
104 ## UPO means: this is also a user preference option
106 $wgEnableEmail = true;
107 $wgEnableUserEmail = true; # UPO
109 $wgPasswordSender = "${cfg.passwordSender}";
111 $wgEnotifUserTalk = false; # UPO
112 $wgEnotifWatchlist = false; # UPO
113 $wgEmailAuthentication = true;
116 $wgDBtype = "${cfg.database.type}";
117 $wgDBserver = "${dbAddr}";
118 $wgDBport = "${toString cfg.database.port}";
119 $wgDBname = "${cfg.database.name}";
120 $wgDBuser = "${cfg.database.user}";
121 ${optionalString (cfg.database.passwordFile != null) "$wgDBpassword = file_get_contents(\"${cfg.database.passwordFile}\");"}
123 ${optionalString (cfg.database.type == "mysql" && cfg.database.tablePrefix != null) ''
124 # MySQL specific settings
125 $wgDBprefix = "${cfg.database.tablePrefix}";
128 ${optionalString (cfg.database.type == "mysql") ''
129 # MySQL table options to use during installation or update
130 $wgDBTableOptions = "ENGINE=InnoDB, DEFAULT CHARSET=binary";
133 ## Shared memory settings
134 $wgMainCacheType = CACHE_NONE;
135 $wgMemCachedServers = [];
137 ${optionalString (cfg.uploadsDir != null) ''
138 $wgEnableUploads = true;
139 $wgUploadDirectory = "${cfg.uploadsDir}";
142 $wgUseImageMagick = true;
143 $wgImageMagickConvertCommand = "${pkgs.imagemagick}/bin/convert";
145 # InstantCommons allows wiki to use images from https://commons.wikimedia.org
146 $wgUseInstantCommons = false;
148 # Periodically send a pingback to https://www.mediawiki.org/ with basic data
149 # about this MediaWiki instance. The Wikimedia Foundation shares this data
150 # with MediaWiki developers to help guide future development efforts.
153 ## If you use ImageMagick (or any other shell command) on a
154 ## Linux server, this will need to be set to the name of an
155 ## available UTF-8 locale
156 $wgShellLocale = "C.UTF-8";
158 ## Set $wgCacheDirectory to a writable directory on the web server
159 ## to make your wiki go slightly faster. The directory should not
160 ## be publicly accessible from the web.
161 $wgCacheDirectory = "${cacheDir}";
163 # Site language code, should be one of the list in ./languages/data/Names.php
164 $wgLanguageCode = "en";
166 $wgSecretKey = file_get_contents("${stateDir}/secret.key");
168 # Changing this will log out all existing sessions.
169 $wgAuthenticationTokenVersion = "";
171 ## For attaching licensing metadata to pages, and displaying an
172 ## appropriate copyright notice / icon. GNU Free Documentation
173 ## License and Creative Commons licenses are supported so far.
174 $wgRightsPage = ""; # Set to the title of a wiki page that describes your license/copyright
179 # Path to the GNU diff3 utility. Used for conflict resolution.
180 $wgDiff = "${pkgs.diffutils}/bin/diff";
181 $wgDiff3 = "${pkgs.diffutils}/bin/diff3";
184 ${concatStringsSep "\n" (mapAttrsToList (k: v: "wfLoadSkin('${k}');") cfg.skins)}
186 # Enabled extensions.
187 ${concatStringsSep "\n" (mapAttrsToList (k: v: "wfLoadExtension('${k}');") cfg.extensions)}
190 # End of automatically generated settings.
191 # Add more configuration options below.
197 withTrailingSlash = str: if lib.hasSuffix "/" str then str else "${str}/";
202 services.mediawiki = {
204 enable = mkEnableOption "MediaWiki";
206 package = mkPackageOption pkgs "mediawiki" { };
208 finalPackage = mkOption {
209 type = types.package;
212 defaultText = literalExpression "pkg";
214 The final package used by the module. This is the package that will have extensions and skins installed.
220 default = "MediaWiki";
221 example = "Foobar Wiki";
222 description = "Name of the wiki.";
228 if cfg.webserver == "apache" then
229 "${if cfg.httpd.virtualHost.addSSL || cfg.httpd.virtualHost.forceSSL || cfg.httpd.virtualHost.onlySSL then "https" else "http"}://${cfg.httpd.virtualHost.hostName}"
230 else if cfg.webserver == "nginx" then
232 hasSSL = host: host.forceSSL || host.addSSL;
234 "${if hasSSL config.services.nginx.virtualHosts.${cfg.nginx.hostName} then "https" else "http"}://${cfg.nginx.hostName}"
238 if "mediawiki uses ssl" then "{"https" else "http"}://''${cfg.hostName}" else "http://localhost";
240 example = "https://wiki.example.org";
241 description = "URL of the wiki.";
244 uploadsDir = mkOption {
245 type = types.nullOr types.path;
246 default = "${stateDir}/uploads";
248 This directory is used for uploads of pictures. The directory passed here is automatically
249 created and permissions adjusted as required.
253 passwordFile = mkOption {
256 A file containing the initial password for the administrator account "admin".
258 example = "/run/keys/mediawiki-password";
261 passwordSender = mkOption {
264 if cfg.webserver == "apache" then
265 if cfg.httpd.virtualHost.adminAddr != null then
266 cfg.httpd.virtualHost.adminAddr
268 config.services.httpd.adminAddr else "root@localhost";
269 defaultText = literalExpression ''
270 if cfg.webserver == "apache" then
271 if cfg.httpd.virtualHost.adminAddr != null then
272 cfg.httpd.virtualHost.adminAddr
274 config.services.httpd.adminAddr else "root@localhost"
276 description = "Contact address for password reset.";
281 type = types.attrsOf types.path;
283 Attribute set of paths whose content is copied to the {file}`skins`
284 subdirectory of the MediaWiki installation in addition to the default skins.
288 extensions = mkOption {
290 type = types.attrsOf (types.nullOr types.path);
292 Attribute set of paths whose content is copied to the {file}`extensions`
293 subdirectory of the MediaWiki installation and enabled in configuration.
295 Use `null` instead of path to enable extensions that are part of MediaWiki.
297 example = literalExpression ''
299 Matomo = pkgs.fetchzip {
300 url = "https://github.com/DaSchTour/matomo-mediawiki-extension/archive/v4.0.1.tar.gz";
301 sha256 = "0g5rd3zp0avwlmqagc59cg9bbkn3r7wx7p6yr80s644mj6dlvs1b";
303 ParserFunctions = null;
308 webserver = mkOption {
309 type = types.enum [ "apache" "none" "nginx" ];
311 description = "Webserver to use.";
316 type = types.enum [ "mysql" "postgres" "mssql" "oracle" ];
318 description = "Database engine to use. MySQL/MariaDB is the database of choice by MediaWiki developers.";
323 default = "localhost";
324 description = "Database host address.";
329 default = if cfg.database.type == "mysql" then 3306 else 5432;
330 defaultText = literalExpression "3306";
331 description = "Database host port.";
336 default = "mediawiki";
337 description = "Database name.";
342 default = "mediawiki";
343 description = "Database user.";
346 passwordFile = mkOption {
347 type = types.nullOr types.path;
349 example = "/run/keys/mediawiki-dbpassword";
351 A file containing the password corresponding to
352 {option}`database.user`.
356 tablePrefix = mkOption {
357 type = types.nullOr types.str;
360 If you only have access to a single database and wish to install more than
361 one version of MediaWiki, or have other applications that also use the
362 database, you can give the table names a unique prefix to stop any naming
363 conflicts or confusion.
364 See <https://www.mediawiki.org/wiki/Manual:$wgDBprefix>.
369 type = types.nullOr types.path;
370 default = if (cfg.database.type == "mysql" && cfg.database.createLocally) then
371 "/run/mysqld/mysqld.sock"
372 else if (cfg.database.type == "postgres" && cfg.database.createLocally) then
376 defaultText = literalExpression "/run/mysqld/mysqld.sock";
377 description = "Path to the unix socket file to use for authentication.";
380 createLocally = mkOption {
382 default = cfg.database.type == "mysql" || cfg.database.type == "postgres";
383 defaultText = literalExpression "true";
385 Create the database and database user locally.
386 This currently only applies if database type "mysql" is selected.
391 nginx.hostName = mkOption {
393 example = literalExpression ''wiki.example.com'';
394 default = "localhost";
396 The hostname to use for the nginx virtual host.
397 This is used to generate the nginx configuration.
401 httpd.virtualHost = mkOption {
402 type = types.submodule (import ../web-servers/apache-httpd/vhost-options.nix);
403 example = literalExpression ''
405 hostName = "mediawiki.example.org";
406 adminAddr = "webmaster@example.org";
412 Apache configuration can be done by adapting {option}`services.httpd.virtualHosts`.
413 See [](#opt-services.httpd.virtualHosts) for further information.
417 poolConfig = mkOption {
418 type = with types; attrsOf (oneOf [ str int bool ]);
421 "pm.max_children" = 32;
422 "pm.start_servers" = 2;
423 "pm.min_spare_servers" = 2;
424 "pm.max_spare_servers" = 4;
425 "pm.max_requests" = 500;
428 Options for the MediaWiki PHP pool. See the documentation on `php-fpm.conf`
429 for details on configuration directives.
433 extraConfig = mkOption {
436 Any additional text to be appended to MediaWiki's
437 LocalSettings.php configuration file. For configuration
438 settings, see <https://www.mediawiki.org/wiki/Manual:Configuration_settings>.
442 $wgEnableEmail = false;
450 (lib.mkRenamedOptionModule [ "services" "mediawiki" "virtualHost" ] [ "services" "mediawiki" "httpd" "virtualHost" ])
454 config = mkIf cfg.enable {
457 { assertion = cfg.database.createLocally -> (cfg.database.type == "mysql" || cfg.database.type == "postgres");
458 message = "services.mediawiki.createLocally is currently only supported for database type 'mysql' and 'postgres'";
460 { assertion = cfg.database.createLocally -> cfg.database.user == user && cfg.database.name == cfg.database.user;
461 message = "services.mediawiki.database.user must be set to ${user} if services.mediawiki.database.createLocally is set true";
463 { assertion = cfg.database.createLocally -> cfg.database.socket != null;
464 message = "services.mediawiki.database.socket must be set if services.mediawiki.database.createLocally is set to true";
466 { assertion = cfg.database.createLocally -> cfg.database.passwordFile == null;
467 message = "a password cannot be specified if services.mediawiki.database.createLocally is set to true";
471 services.mediawiki.skins = {
472 MonoBook = "${cfg.package}/share/mediawiki/skins/MonoBook";
473 Timeless = "${cfg.package}/share/mediawiki/skins/Timeless";
474 Vector = "${cfg.package}/share/mediawiki/skins/Vector";
477 services.mysql = mkIf (cfg.database.type == "mysql" && cfg.database.createLocally) {
479 package = mkDefault pkgs.mariadb;
480 ensureDatabases = [ cfg.database.name ];
482 name = cfg.database.user;
483 ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
487 services.postgresql = mkIf (cfg.database.type == "postgres" && cfg.database.createLocally) {
489 ensureDatabases = [ cfg.database.name ];
491 name = cfg.database.user;
492 ensureDBOwnership = true;
496 services.phpfpm.pools.mediawiki = {
498 phpEnv.MEDIAWIKI_CONFIG = "${mediawikiConfig}";
500 settings = (if (cfg.webserver == "apache") then {
501 "listen.owner" = config.services.httpd.user;
502 "listen.group" = config.services.httpd.group;
503 } else if (cfg.webserver == "nginx") then {
504 "listen.owner" = config.services.nginx.user;
505 "listen.group" = config.services.nginx.group;
507 "listen.owner" = user;
508 "listen.group" = group;
509 }) // cfg.poolConfig;
512 services.httpd = lib.mkIf (cfg.webserver == "apache") {
514 extraModules = [ "proxy_fcgi" ];
515 virtualHosts.${cfg.httpd.virtualHost.hostName} = mkMerge [
516 cfg.httpd.virtualHost
518 documentRoot = mkForce "${pkg}/share/mediawiki";
520 <Directory "${pkg}/share/mediawiki">
521 <FilesMatch "\.php$">
522 <If "-f %{REQUEST_FILENAME}">
523 SetHandler "proxy:unix:${fpm.socket}|fcgi://localhost/"
528 DirectoryIndex index.php
531 '' + optionalString (cfg.uploadsDir != null) ''
532 Alias "/images" "${cfg.uploadsDir}"
533 <Directory "${cfg.uploadsDir}">
540 # inspired by https://www.mediawiki.org/wiki/Manual:Short_URL/Nginx
541 services.nginx = lib.mkIf (cfg.webserver == "nginx") {
543 virtualHosts.${config.services.mediawiki.nginx.hostName} = {
544 root = "${pkg}/share/mediawiki";
546 "~ ^/w/(index|load|api|thumb|opensearch_desc|rest|img_auth)\\.php$".extraConfig = ''
547 rewrite ^/w/(.*) /$1 break;
548 include ${config.services.nginx.package}/conf/fastcgi.conf;
549 fastcgi_index index.php;
550 fastcgi_pass unix:${config.services.phpfpm.pools.mediawiki.socket};
552 "/w/images/".alias = withTrailingSlash cfg.uploadsDir;
553 # Deny access to deleted images folder
554 "/w/images/deleted".extraConfig = ''
557 # MediaWiki assets (usually images)
558 "~ ^/w/resources/(assets|lib|src)".extraConfig = ''
559 rewrite ^/w(/.*) $1 break;
560 add_header Cache-Control "public";
563 # Assets, scripts and styles from skins and extensions
564 "~ ^/w/(skins|extensions)/.+\\.(css|js|gif|jpg|jpeg|png|svg|wasm|ttf|woff|woff2)$".extraConfig = ''
565 rewrite ^/w(/.*) $1 break;
566 add_header Cache-Control "public";
570 # Handling for Mediawiki REST API, see [[mw:API:REST_API]]
571 "/w/rest.php/".tryFiles = "$uri $uri/ /w/rest.php?$query_string";
573 # Handling for the article path (pretty URLs)
574 "/wiki/".extraConfig = ''
575 rewrite ^/wiki/(?<pagename>.*)$ /w/index.php;
578 # Explicit access to the root website, redirect to main page (adapt as needed)
579 "= /".extraConfig = ''
583 # Every other entry point will be disallowed.
584 # Add specific rules for other entry points/images as needed above this
592 systemd.tmpfiles.rules = [
593 "d '${stateDir}' 0750 ${user} ${group} - -"
594 "d '${cacheDir}' 0750 ${user} ${group} - -"
595 ] ++ optionals (cfg.uploadsDir != null) [
596 "d '${cfg.uploadsDir}' 0750 ${user} ${group} - -"
597 "Z '${cfg.uploadsDir}' 0750 ${user} ${group} - -"
600 systemd.services.mediawiki-init = {
601 wantedBy = [ "multi-user.target" ];
602 before = [ "phpfpm-mediawiki.service" ];
603 after = optional (cfg.database.type == "mysql" && cfg.database.createLocally) "mysql.service"
604 ++ optional (cfg.database.type == "postgres" && cfg.database.createLocally) "postgresql.service";
606 if ! test -e "${stateDir}/secret.key"; then
607 tr -dc A-Za-z0-9 </dev/urandom 2>/dev/null | head -c 64 > ${stateDir}/secret.key
610 echo "exit( wfGetDB( DB_MASTER )->tableExists( 'user' ) ? 1 : 0 );" | \
611 ${php}/bin/php ${pkg}/share/mediawiki/maintenance/eval.php --conf ${mediawikiConfig} && \
612 ${php}/bin/php ${pkg}/share/mediawiki/maintenance/install.php \
615 --dbserver ${lib.escapeShellArg dbAddr} \
616 --dbport ${toString cfg.database.port} \
617 --dbname ${lib.escapeShellArg cfg.database.name} \
618 ${optionalString (cfg.database.tablePrefix != null) "--dbprefix ${lib.escapeShellArg cfg.database.tablePrefix}"} \
619 --dbuser ${lib.escapeShellArg cfg.database.user} \
620 ${optionalString (cfg.database.passwordFile != null) "--dbpassfile ${lib.escapeShellArg cfg.database.passwordFile}"} \
621 --passfile ${lib.escapeShellArg cfg.passwordFile} \
622 --dbtype ${cfg.database.type} \
623 ${lib.escapeShellArg cfg.name} \
626 ${php}/bin/php ${pkg}/share/mediawiki/maintenance/update.php --conf ${mediawikiConfig} --quick
637 systemd.services.httpd.after = optional (cfg.webserver == "apache" && cfg.database.createLocally && cfg.database.type == "mysql") "mysql.service"
638 ++ optional (cfg.webserver == "apache" && cfg.database.createLocally && cfg.database.type == "postgres") "postgresql.service";
640 users.users.${user} = {
644 users.groups.${group} = {};
646 environment.systemPackages = [ mediawikiScripts ];