{ config, pkgs, lib, ... }: let inherit (lib) mkDefault mkEnableOption mkPackageOption mkForce mkIf mkMerge mkOption; inherit (lib) concatStringsSep literalExpression mapAttrsToList optional optionals optionalString types; cfg = config.services.mediawiki; fpm = config.services.phpfpm.pools.mediawiki; user = "mediawiki"; group = if cfg.webserver == "apache" then config.services.httpd.group else if cfg.webserver == "nginx" then config.services.nginx.group else "mediawiki"; cacheDir = "/var/cache/mediawiki"; stateDir = "/var/lib/mediawiki"; # https://www.mediawiki.org/wiki/Compatibility php = pkgs.php81; pkg = pkgs.stdenv.mkDerivation rec { pname = "mediawiki-full"; inherit (src) version; src = cfg.package; installPhase = '' mkdir -p $out cp -r * $out/ # try removing directories before symlinking to allow overwriting any builtin extension or skin ${concatStringsSep "\n" (mapAttrsToList (k: v: '' rm -rf $out/share/mediawiki/skins/${k} ln -s ${v} $out/share/mediawiki/skins/${k} '') cfg.skins)} ${concatStringsSep "\n" (mapAttrsToList (k: v: '' rm -rf $out/share/mediawiki/extensions/${k} ln -s ${if v != null then v else "$src/share/mediawiki/extensions/${k}"} $out/share/mediawiki/extensions/${k} '') cfg.extensions)} ''; }; mediawikiScripts = pkgs.runCommand "mediawiki-scripts" { nativeBuildInputs = [ pkgs.makeWrapper ]; preferLocalBuild = true; } '' mkdir -p $out/bin for i in changePassword.php createAndPromote.php userOptions.php edit.php nukePage.php update.php; do makeWrapper ${php}/bin/php $out/bin/mediawiki-$(basename $i .php) \ --set MEDIAWIKI_CONFIG ${mediawikiConfig} \ --add-flags ${pkg}/share/mediawiki/maintenance/$i done ''; dbAddr = if cfg.database.socket == null then "${cfg.database.host}:${toString cfg.database.port}" else if cfg.database.type == "mysql" then "${cfg.database.host}:${cfg.database.socket}" else if cfg.database.type == "postgres" then "${cfg.database.socket}" else throw "Unsupported database type: ${cfg.database.type} for socket: ${cfg.database.socket}"; mediawikiConfig = pkgs.writeTextFile { name = "LocalSettings.php"; checkPhase = '' ${php}/bin/php --syntax-check "$target" ''; text = '' . ''; }; socket = mkOption { type = types.nullOr types.path; default = if (cfg.database.type == "mysql" && cfg.database.createLocally) then "/run/mysqld/mysqld.sock" else if (cfg.database.type == "postgres" && cfg.database.createLocally) then "/run/postgresql" else null; defaultText = literalExpression "/run/mysqld/mysqld.sock"; description = "Path to the unix socket file to use for authentication."; }; createLocally = mkOption { type = types.bool; default = cfg.database.type == "mysql" || cfg.database.type == "postgres"; defaultText = literalExpression "true"; description = '' Create the database and database user locally. This currently only applies if database type "mysql" is selected. ''; }; }; nginx.hostName = mkOption { type = types.str; example = literalExpression ''wiki.example.com''; default = "localhost"; description = '' The hostname to use for the nginx virtual host. This is used to generate the nginx configuration. ''; }; httpd.virtualHost = mkOption { type = types.submodule (import ../web-servers/apache-httpd/vhost-options.nix); example = literalExpression '' { hostName = "mediawiki.example.org"; adminAddr = "webmaster@example.org"; forceSSL = true; enableACME = true; } ''; description = '' Apache configuration can be done by adapting {option}`services.httpd.virtualHosts`. See [](#opt-services.httpd.virtualHosts) for further information. ''; }; poolConfig = mkOption { type = with types; attrsOf (oneOf [ str int bool ]); default = { "pm" = "dynamic"; "pm.max_children" = 32; "pm.start_servers" = 2; "pm.min_spare_servers" = 2; "pm.max_spare_servers" = 4; "pm.max_requests" = 500; }; description = '' Options for the MediaWiki PHP pool. See the documentation on `php-fpm.conf` for details on configuration directives. ''; }; extraConfig = mkOption { type = types.lines; description = '' Any additional text to be appended to MediaWiki's LocalSettings.php configuration file. For configuration settings, see . ''; default = ""; example = '' $wgEnableEmail = false; ''; }; }; }; imports = [ (lib.mkRenamedOptionModule [ "services" "mediawiki" "virtualHost" ] [ "services" "mediawiki" "httpd" "virtualHost" ]) ]; # implementation config = mkIf cfg.enable { assertions = [ { assertion = cfg.database.createLocally -> (cfg.database.type == "mysql" || cfg.database.type == "postgres"); message = "services.mediawiki.createLocally is currently only supported for database type 'mysql' and 'postgres'"; } { assertion = cfg.database.createLocally -> cfg.database.user == user && cfg.database.name == cfg.database.user; message = "services.mediawiki.database.user must be set to ${user} if services.mediawiki.database.createLocally is set true"; } { assertion = cfg.database.createLocally -> cfg.database.socket != null; message = "services.mediawiki.database.socket must be set if services.mediawiki.database.createLocally is set to true"; } { assertion = cfg.database.createLocally -> cfg.database.passwordFile == null; message = "a password cannot be specified if services.mediawiki.database.createLocally is set to true"; } ]; services.mediawiki.skins = { MonoBook = "${cfg.package}/share/mediawiki/skins/MonoBook"; Timeless = "${cfg.package}/share/mediawiki/skins/Timeless"; Vector = "${cfg.package}/share/mediawiki/skins/Vector"; }; services.mysql = mkIf (cfg.database.type == "mysql" && cfg.database.createLocally) { enable = true; package = mkDefault pkgs.mariadb; ensureDatabases = [ cfg.database.name ]; ensureUsers = [{ name = cfg.database.user; ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; }; }]; }; services.postgresql = mkIf (cfg.database.type == "postgres" && cfg.database.createLocally) { enable = true; ensureDatabases = [ cfg.database.name ]; ensureUsers = [{ name = cfg.database.user; ensureDBOwnership = true; }]; }; services.phpfpm.pools.mediawiki = { inherit user group; phpEnv.MEDIAWIKI_CONFIG = "${mediawikiConfig}"; phpPackage = php; settings = (if (cfg.webserver == "apache") then { "listen.owner" = config.services.httpd.user; "listen.group" = config.services.httpd.group; } else if (cfg.webserver == "nginx") then { "listen.owner" = config.services.nginx.user; "listen.group" = config.services.nginx.group; } else { "listen.owner" = user; "listen.group" = group; }) // cfg.poolConfig; }; services.httpd = lib.mkIf (cfg.webserver == "apache") { enable = true; extraModules = [ "proxy_fcgi" ]; virtualHosts.${cfg.httpd.virtualHost.hostName} = mkMerge [ cfg.httpd.virtualHost { documentRoot = mkForce "${pkg}/share/mediawiki"; extraConfig = '' SetHandler "proxy:unix:${fpm.socket}|fcgi://localhost/" Require all granted DirectoryIndex index.php AllowOverride All '' + optionalString (cfg.uploadsDir != null) '' Alias "/images" "${cfg.uploadsDir}" Require all granted ''; } ]; }; # inspired by https://www.mediawiki.org/wiki/Manual:Short_URL/Nginx services.nginx = lib.mkIf (cfg.webserver == "nginx") { enable = true; virtualHosts.${config.services.mediawiki.nginx.hostName} = { root = "${pkg}/share/mediawiki"; locations = { "~ ^/w/(index|load|api|thumb|opensearch_desc|rest|img_auth)\\.php$".extraConfig = '' rewrite ^/w/(.*) /$1 break; include ${config.services.nginx.package}/conf/fastcgi.conf; fastcgi_index index.php; fastcgi_pass unix:${config.services.phpfpm.pools.mediawiki.socket}; ''; "/w/images/".alias = withTrailingSlash cfg.uploadsDir; # Deny access to deleted images folder "/w/images/deleted".extraConfig = '' deny all; ''; # MediaWiki assets (usually images) "~ ^/w/resources/(assets|lib|src)".extraConfig = '' rewrite ^/w(/.*) $1 break; add_header Cache-Control "public"; expires 7d; ''; # Assets, scripts and styles from skins and extensions "~ ^/w/(skins|extensions)/.+\\.(css|js|gif|jpg|jpeg|png|svg|wasm|ttf|woff|woff2)$".extraConfig = '' rewrite ^/w(/.*) $1 break; add_header Cache-Control "public"; expires 7d; ''; # Handling for Mediawiki REST API, see [[mw:API:REST_API]] "/w/rest.php/".tryFiles = "$uri $uri/ /w/rest.php?$query_string"; # Handling for the article path (pretty URLs) "/wiki/".extraConfig = '' rewrite ^/wiki/(?.*)$ /w/index.php; ''; # Explicit access to the root website, redirect to main page (adapt as needed) "= /".extraConfig = '' return 301 /wiki/; ''; # Every other entry point will be disallowed. # Add specific rules for other entry points/images as needed above this "/".extraConfig = '' return 404; ''; }; }; }; systemd.tmpfiles.rules = [ "d '${stateDir}' 0750 ${user} ${group} - -" "d '${cacheDir}' 0750 ${user} ${group} - -" ] ++ optionals (cfg.uploadsDir != null) [ "d '${cfg.uploadsDir}' 0750 ${user} ${group} - -" "Z '${cfg.uploadsDir}' 0750 ${user} ${group} - -" ]; systemd.services.mediawiki-init = { wantedBy = [ "multi-user.target" ]; before = [ "phpfpm-mediawiki.service" ]; after = optional (cfg.database.type == "mysql" && cfg.database.createLocally) "mysql.service" ++ optional (cfg.database.type == "postgres" && cfg.database.createLocally) "postgresql.service"; script = '' if ! test -e "${stateDir}/secret.key"; then tr -dc A-Za-z0-9 /dev/null | head -c 64 > ${stateDir}/secret.key fi echo "exit( wfGetDB( DB_MASTER )->tableExists( 'user' ) ? 1 : 0 );" | \ ${php}/bin/php ${pkg}/share/mediawiki/maintenance/eval.php --conf ${mediawikiConfig} && \ ${php}/bin/php ${pkg}/share/mediawiki/maintenance/install.php \ --confpath /tmp \ --scriptpath / \ --dbserver ${lib.escapeShellArg dbAddr} \ --dbport ${toString cfg.database.port} \ --dbname ${lib.escapeShellArg cfg.database.name} \ ${optionalString (cfg.database.tablePrefix != null) "--dbprefix ${lib.escapeShellArg cfg.database.tablePrefix}"} \ --dbuser ${lib.escapeShellArg cfg.database.user} \ ${optionalString (cfg.database.passwordFile != null) "--dbpassfile ${lib.escapeShellArg cfg.database.passwordFile}"} \ --passfile ${lib.escapeShellArg cfg.passwordFile} \ --dbtype ${cfg.database.type} \ ${lib.escapeShellArg cfg.name} \ admin ${php}/bin/php ${pkg}/share/mediawiki/maintenance/update.php --conf ${mediawikiConfig} --quick ''; serviceConfig = { Type = "oneshot"; User = user; Group = group; PrivateTmp = true; }; }; systemd.services.httpd.after = optional (cfg.webserver == "apache" && cfg.database.createLocally && cfg.database.type == "mysql") "mysql.service" ++ optional (cfg.webserver == "apache" && cfg.database.createLocally && cfg.database.type == "postgres") "postgresql.service"; users.users.${user} = { inherit group; isSystemUser = true; }; users.groups.${group} = {}; environment.systemPackages = [ mediawikiScripts ]; }; }