at 23.11-pre 21 kB view raw
1{ config, pkgs, lib, ... }: 2 3with lib; 4 5let 6 inherit (lib.options) showOption showFiles; 7 8 cfg = config.services.dokuwiki; 9 eachSite = cfg.sites; 10 user = "dokuwiki"; 11 webserver = config.services.${cfg.webserver}; 12 13 mkPhpIni = generators.toKeyValue { 14 mkKeyValue = generators.mkKeyValueDefault {} " = "; 15 }; 16 mkPhpPackage = cfg: cfg.phpPackage.buildEnv { 17 extraConfig = mkPhpIni cfg.phpOptions; 18 }; 19 20 dokuwikiAclAuthConfig = hostName: cfg: let 21 inherit (cfg) acl; 22 acl_gen = concatMapStringsSep "\n" (l: "${l.page} \t ${l.actor} \t ${toString l.level}"); 23 in pkgs.writeText "acl.auth-${hostName}.php" '' 24 # acl.auth.php 25 # <?php exit()?> 26 # 27 # Access Control Lists 28 # 29 ${if isString acl then acl else acl_gen acl} 30 ''; 31 32 mergeConfig = cfg: { 33 useacl = false; # Dokuwiki default 34 savedir = cfg.stateDir; 35 } // cfg.settings; 36 37 writePhpFile = name: text: pkgs.writeTextFile { 38 inherit name; 39 text = "<?php\n${text}"; 40 checkPhase = "${pkgs.php81}/bin/php --syntax-check $target"; 41 }; 42 43 mkPhpValue = v: let 44 isHasAttr = s: isAttrs v && hasAttr s v; 45 in 46 if isString v then escapeShellArg v 47 # NOTE: If any value contains a , (comma) this will not get escaped 48 else if isList v && any lib.strings.isCoercibleToString v then escapeShellArg (concatMapStringsSep "," toString v) 49 else if isInt v then toString v 50 else if isBool v then toString (if v then 1 else 0) 51 else if isHasAttr "_file" then "trim(file_get_contents(${lib.escapeShellArg v._file}))" 52 else if isHasAttr "_raw" then v._raw 53 else abort "The dokuwiki localConf value ${lib.generators.toPretty {} v} can not be encoded." 54 ; 55 56 mkPhpAttrVals = v: flatten (mapAttrsToList mkPhpKeyVal v); 57 mkPhpKeyVal = k: v: let 58 values = if (isAttrs v && (hasAttr "_file" v || hasAttr "_raw" v )) || !isAttrs v then 59 [" = ${mkPhpValue v};"] 60 else 61 mkPhpAttrVals v; 62 in map (e: "[${escapeShellArg k}]${e}") (flatten values); 63 64 dokuwikiLocalConfig = hostName: cfg: let 65 conf_gen = c: map (v: "$conf${v}") (mkPhpAttrVals c); 66 in writePhpFile "local-${hostName}.php" '' 67 ${concatStringsSep "\n" (conf_gen cfg.mergedConfig)} 68 ''; 69 70 dokuwikiPluginsLocalConfig = hostName: cfg: let 71 pc = cfg.pluginsConfig; 72 pc_gen = pc: concatStringsSep "\n" (mapAttrsToList (n: v: "$plugins['${n}'] = ${boolToString v};") pc); 73 in writePhpFile "plugins.local-${hostName}.php" '' 74 ${if isString pc then pc else pc_gen pc} 75 ''; 76 77 78 pkg = hostName: cfg: cfg.package.combine { 79 inherit (cfg) plugins templates; 80 81 pname = p: "${p.pname}-${hostName}"; 82 83 basePackage = cfg.package; 84 localConfig = dokuwikiLocalConfig hostName cfg; 85 pluginsConfig = dokuwikiPluginsLocalConfig hostName cfg; 86 aclConfig = if cfg.settings.useacl && cfg.acl != null then dokuwikiAclAuthConfig hostName cfg else null; 87 }; 88 89 aclOpts = { ... }: { 90 options = { 91 92 page = mkOption { 93 type = types.str; 94 description = lib.mdDoc "Page or namespace to restrict"; 95 example = "start"; 96 }; 97 98 actor = mkOption { 99 type = types.str; 100 description = lib.mdDoc "User or group to restrict"; 101 example = "@external"; 102 }; 103 104 level = let 105 available = { 106 "none" = 0; 107 "read" = 1; 108 "edit" = 2; 109 "create" = 4; 110 "upload" = 8; 111 "delete" = 16; 112 }; 113 in mkOption { 114 type = types.enum ((attrValues available) ++ (attrNames available)); 115 apply = x: if isInt x then x else available.${x}; 116 description = lib.mdDoc '' 117 Permission level to restrict the actor(s) to. 118 See <https://www.dokuwiki.org/acl#background_info> for explanation 119 ''; 120 example = "read"; 121 }; 122 }; 123 }; 124 125 # The current implementations of `doRename`, `mkRenamedOptionModule` do not provide the full options path when used with submodules. 126 # They would only show `settings.useacl' instead of `services.dokuwiki.sites."site1.local".settings.useacl' 127 # The partial re-implementation of these functions is done to help users in debugging by showing the full path. 128 mkRenamed = from: to: { config, options, name, ... }: let 129 pathPrefix = [ "services" "dokuwiki" "sites" name ]; 130 fromPath = pathPrefix ++ from; 131 fromOpt = getAttrFromPath from options; 132 toOp = getAttrsFromPath to config; 133 toPath = pathPrefix ++ to; 134 in { 135 options = setAttrByPath from (mkOption { 136 visible = false; 137 description = lib.mdDoc "Alias of {option}${showOption toPath}"; 138 apply = x: builtins.trace "Obsolete option `${showOption fromPath}' is used. It was renamed to ${showOption toPath}" toOp; 139 }); 140 config = mkMerge [ 141 { 142 warnings = optional fromOpt.isDefined 143 "The option `${showOption fromPath}' defined in ${showFiles fromOpt.files} has been renamed to `${showOption toPath}'."; 144 } 145 (lib.modules.mkAliasAndWrapDefsWithPriority (setAttrByPath to) fromOpt) 146 ]; 147 }; 148 149 siteOpts = { options, config, lib, name, ... }: 150 { 151 imports = [ 152 (mkRenamed [ "aclUse" ] [ "settings" "useacl" ]) 153 (mkRenamed [ "superUser" ] [ "settings" "superuser" ]) 154 (mkRenamed [ "disableActions" ] [ "settings" "disableactions" ]) 155 ({ config, options, ... }: let 156 showPath = suffix: lib.options.showOption ([ "services" "dokuwiki" "sites" name ] ++ suffix); 157 replaceExtraConfig = "Please use `${showPath ["settings"]}' to pass structured settings instead."; 158 ecOpt = options.extraConfig; 159 ecPath = showPath [ "extraConfig" ]; 160 in { 161 options.extraConfig = mkOption { 162 visible = false; 163 apply = x: throw "The option ${ecPath} can no longer be used since it's been removed.\n${replaceExtraConfig}"; 164 }; 165 config.assertions = [ 166 { 167 assertion = !ecOpt.isDefined; 168 message = "The option definition `${ecPath}' in ${showFiles ecOpt.files} no longer has any effect; please remove it.\n${replaceExtraConfig}"; 169 } 170 { 171 assertion = config.mergedConfig.useacl -> (config.acl != null || config.aclFile != null); 172 message = "Either ${showPath [ "acl" ]} or ${showPath [ "aclFile" ]} is mandatory if ${showPath [ "settings" "useacl" ]} is true"; 173 } 174 { 175 assertion = config.usersFile != null -> config.mergedConfig.useacl != false; 176 message = "${showPath [ "settings" "useacl" ]} is required when ${showPath [ "usersFile" ]} is set (Currently defined as `${config.usersFile}' in ${showFiles options.usersFile.files})."; 177 } 178 ]; 179 }) 180 ]; 181 182 options = { 183 enable = mkEnableOption (lib.mdDoc "DokuWiki web application"); 184 185 package = mkOption { 186 type = types.package; 187 default = pkgs.dokuwiki; 188 defaultText = literalExpression "pkgs.dokuwiki"; 189 description = lib.mdDoc "Which DokuWiki package to use."; 190 }; 191 192 stateDir = mkOption { 193 type = types.path; 194 default = "/var/lib/dokuwiki/${name}/data"; 195 description = lib.mdDoc "Location of the DokuWiki state directory."; 196 }; 197 198 acl = mkOption { 199 type = with types; nullOr (listOf (submodule aclOpts)); 200 default = null; 201 example = literalExpression '' 202 [ 203 { 204 page = "start"; 205 actor = "@external"; 206 level = "read"; 207 } 208 { 209 page = "*"; 210 actor = "@users"; 211 level = "upload"; 212 } 213 ] 214 ''; 215 description = lib.mdDoc '' 216 Access Control Lists: see <https://www.dokuwiki.org/acl> 217 Mutually exclusive with services.dokuwiki.aclFile 218 Set this to a value other than null to take precedence over aclFile option. 219 220 Warning: Consider using aclFile instead if you do not 221 want to store the ACL in the world-readable Nix store. 222 ''; 223 }; 224 225 aclFile = mkOption { 226 type = with types; nullOr str; 227 default = if (config.mergedConfig.useacl && config.acl == null) then "/var/lib/dokuwiki/${name}/acl.auth.php" else null; 228 description = lib.mdDoc '' 229 Location of the dokuwiki acl rules. Mutually exclusive with services.dokuwiki.acl 230 Mutually exclusive with services.dokuwiki.acl which is preferred. 231 Consult documentation <https://www.dokuwiki.org/acl> for further instructions. 232 Example: <https://github.com/splitbrain/dokuwiki/blob/master/conf/acl.auth.php.dist> 233 ''; 234 example = "/var/lib/dokuwiki/${name}/acl.auth.php"; 235 }; 236 237 pluginsConfig = mkOption { 238 type = with types; attrsOf bool; 239 default = { 240 authad = false; 241 authldap = false; 242 authmysql = false; 243 authpgsql = false; 244 }; 245 description = lib.mdDoc '' 246 List of the dokuwiki (un)loaded plugins. 247 ''; 248 }; 249 250 usersFile = mkOption { 251 type = with types; nullOr str; 252 default = if config.mergedConfig.useacl then "/var/lib/dokuwiki/${name}/users.auth.php" else null; 253 description = lib.mdDoc '' 254 Location of the dokuwiki users file. List of users. Format: 255 256 login:passwordhash:Real Name:email:groups,comma,separated 257 258 Create passwordHash easily by using: 259 260 mkpasswd -5 password `pwgen 8 1` 261 262 Example: <https://github.com/splitbrain/dokuwiki/blob/master/conf/users.auth.php.dist> 263 ''; 264 example = "/var/lib/dokuwiki/${name}/users.auth.php"; 265 }; 266 267 plugins = mkOption { 268 type = types.listOf types.path; 269 default = []; 270 description = lib.mdDoc '' 271 List of path(s) to respective plugin(s) which are copied from the 'plugin' directory. 272 273 ::: {.note} 274 These plugins need to be packaged before use, see example. 275 ::: 276 ''; 277 example = literalExpression '' 278 let 279 plugin-icalevents = pkgs.stdenv.mkDerivation rec { 280 name = "icalevents"; 281 version = "2017-06-16"; 282 src = pkgs.fetchzip { 283 stripRoot = false; 284 url = "https://github.com/real-or-random/dokuwiki-plugin-icalevents/releases/download/''${version}/dokuwiki-plugin-icalevents-''${version}.zip"; 285 hash = "sha256-IPs4+qgEfe8AAWevbcCM9PnyI0uoyamtWeg4rEb+9Wc="; 286 }; 287 installPhase = "mkdir -p $out; cp -R * $out/"; 288 }; 289 # And then pass this theme to the plugin list like this: 290 in [ plugin-icalevents ] 291 ''; 292 }; 293 294 templates = mkOption { 295 type = types.listOf types.path; 296 default = []; 297 description = lib.mdDoc '' 298 List of path(s) to respective template(s) which are copied from the 'tpl' directory. 299 300 ::: {.note} 301 These templates need to be packaged before use, see example. 302 ::: 303 ''; 304 example = literalExpression '' 305 let 306 template-bootstrap3 = pkgs.stdenv.mkDerivation rec { 307 name = "bootstrap3"; 308 version = "2022-07-27"; 309 src = pkgs.fetchFromGitHub { 310 owner = "giterlizzi"; 311 repo = "dokuwiki-template-bootstrap3"; 312 rev = "v''${version}"; 313 hash = "sha256-B3Yd4lxdwqfCnfmZdp+i/Mzwn/aEuZ0ovagDxuR6lxo="; 314 }; 315 installPhase = "mkdir -p $out; cp -R * $out/"; 316 }; 317 # And then pass this theme to the template list like this: 318 in [ template-bootstrap3 ] 319 ''; 320 }; 321 322 poolConfig = mkOption { 323 type = with types; attrsOf (oneOf [ str int bool ]); 324 default = { 325 "pm" = "dynamic"; 326 "pm.max_children" = 32; 327 "pm.start_servers" = 2; 328 "pm.min_spare_servers" = 2; 329 "pm.max_spare_servers" = 4; 330 "pm.max_requests" = 500; 331 }; 332 description = lib.mdDoc '' 333 Options for the DokuWiki PHP pool. See the documentation on `php-fpm.conf` 334 for details on configuration directives. 335 ''; 336 }; 337 338 phpPackage = mkOption { 339 type = types.package; 340 relatedPackages = [ "php80" "php81" ]; 341 default = pkgs.php81; 342 defaultText = "pkgs.php81"; 343 description = lib.mdDoc '' 344 PHP package to use for this dokuwiki site. 345 ''; 346 }; 347 348 phpOptions = mkOption { 349 type = types.attrsOf types.str; 350 default = {}; 351 description = lib.mdDoc '' 352 Options for PHP's php.ini file for this dokuwiki site. 353 ''; 354 example = literalExpression '' 355 { 356 "opcache.interned_strings_buffer" = "8"; 357 "opcache.max_accelerated_files" = "10000"; 358 "opcache.memory_consumption" = "128"; 359 "opcache.revalidate_freq" = "15"; 360 "opcache.fast_shutdown" = "1"; 361 } 362 ''; 363 }; 364 365 settings = mkOption { 366 type = types.attrsOf types.anything; 367 default = { 368 useacl = true; 369 superuser = "admin"; 370 }; 371 description = lib.mdDoc '' 372 Structural DokuWiki configuration. 373 Refer to <https://www.dokuwiki.org/config> 374 for details and supported values. 375 Settings can either be directly set from nix, 376 loaded from a file using `._file` or obtained from any 377 PHP function calls using `._raw`. 378 ''; 379 example = literalExpression '' 380 { 381 title = "My Wiki"; 382 userewrite = 1; 383 disableactions = [ "register" ]; # Will be concatenated with commas 384 plugin.smtp = { 385 smtp_pass._file = "/var/run/secrets/dokuwiki/smtp_pass"; 386 smtp_user._raw = "getenv('DOKUWIKI_SMTP_USER')"; 387 }; 388 } 389 ''; 390 }; 391 392 mergedConfig = mkOption { 393 readOnly = true; 394 default = mergeConfig config; 395 defaultText = literalExpression '' 396 { 397 useacl = true; 398 } 399 ''; 400 description = lib.mdDoc '' 401 Read only representation of the final configuration. 402 ''; 403 }; 404 405 # Required for the mkRenamedOptionModule 406 # TODO: Remove me once https://github.com/NixOS/nixpkgs/issues/96006 is fixed 407 # or we don't have any more notes about the removal of extraConfig, ... 408 warnings = mkOption { 409 type = types.listOf types.unspecified; 410 default = [ ]; 411 visible = false; 412 internal = true; 413 }; 414 assertions = mkOption { 415 type = types.listOf types.unspecified; 416 default = [ ]; 417 visible = false; 418 internal = true; 419 }; 420 }; 421 }; 422in 423{ 424 options = { 425 services.dokuwiki = { 426 427 sites = mkOption { 428 type = types.attrsOf (types.submodule siteOpts); 429 default = {}; 430 description = lib.mdDoc "Specification of one or more DokuWiki sites to serve"; 431 }; 432 433 webserver = mkOption { 434 type = types.enum [ "nginx" "caddy" ]; 435 default = "nginx"; 436 description = lib.mdDoc '' 437 Whether to use nginx or caddy for virtual host management. 438 439 Further nginx configuration can be done by adapting `services.nginx.virtualHosts.<name>`. 440 See [](#opt-services.nginx.virtualHosts) for further information. 441 442 Further caddy configuration can be done by adapting `services.caddy.virtualHosts.<name>`. 443 See [](#opt-services.caddy.virtualHosts) for further information. 444 ''; 445 }; 446 447 }; 448 }; 449 450 # implementation 451 config = mkIf (eachSite != {}) (mkMerge [{ 452 453 warnings = flatten (mapAttrsToList (_: cfg: cfg.warnings) eachSite); 454 455 assertions = flatten (mapAttrsToList (_: cfg: cfg.assertions) eachSite); 456 457 services.phpfpm.pools = mapAttrs' (hostName: cfg: ( 458 nameValuePair "dokuwiki-${hostName}" { 459 inherit user; 460 group = webserver.group; 461 462 phpPackage = mkPhpPackage cfg; 463 phpEnv = optionalAttrs (cfg.usersFile != null) { 464 DOKUWIKI_USERS_AUTH_CONFIG = "${cfg.usersFile}"; 465 } // optionalAttrs (cfg.mergedConfig.useacl) { 466 DOKUWIKI_ACL_AUTH_CONFIG = if (cfg.acl != null) then "${dokuwikiAclAuthConfig hostName cfg}" else "${toString cfg.aclFile}"; 467 }; 468 469 settings = { 470 "listen.owner" = webserver.user; 471 "listen.group" = webserver.group; 472 } // cfg.poolConfig; 473 } 474 )) eachSite; 475 476 } 477 478 { 479 systemd.tmpfiles.rules = flatten (mapAttrsToList (hostName: cfg: [ 480 "d ${cfg.stateDir}/attic 0750 ${user} ${webserver.group} - -" 481 "d ${cfg.stateDir}/cache 0750 ${user} ${webserver.group} - -" 482 "d ${cfg.stateDir}/index 0750 ${user} ${webserver.group} - -" 483 "d ${cfg.stateDir}/locks 0750 ${user} ${webserver.group} - -" 484 "d ${cfg.stateDir}/log 0750 ${user} ${webserver.group} - -" 485 "d ${cfg.stateDir}/media 0750 ${user} ${webserver.group} - -" 486 "d ${cfg.stateDir}/media_attic 0750 ${user} ${webserver.group} - -" 487 "d ${cfg.stateDir}/media_meta 0750 ${user} ${webserver.group} - -" 488 "d ${cfg.stateDir}/meta 0750 ${user} ${webserver.group} - -" 489 "d ${cfg.stateDir}/pages 0750 ${user} ${webserver.group} - -" 490 "d ${cfg.stateDir}/tmp 0750 ${user} ${webserver.group} - -" 491 ] ++ lib.optional (cfg.aclFile != null) "C ${cfg.aclFile} 0640 ${user} ${webserver.group} - ${pkg hostName cfg}/share/dokuwiki/conf/acl.auth.php.dist" 492 ++ lib.optional (cfg.usersFile != null) "C ${cfg.usersFile} 0640 ${user} ${webserver.group} - ${pkg hostName cfg}/share/dokuwiki/conf/users.auth.php.dist" 493 ) eachSite); 494 495 users.users.${user} = { 496 group = webserver.group; 497 isSystemUser = true; 498 }; 499 } 500 501 (mkIf (cfg.webserver == "nginx") { 502 services.nginx = { 503 enable = true; 504 virtualHosts = mapAttrs (hostName: cfg: { 505 serverName = mkDefault hostName; 506 root = "${pkg hostName cfg}/share/dokuwiki"; 507 508 locations = { 509 "~ /(conf/|bin/|inc/|install.php)" = { 510 extraConfig = "deny all;"; 511 }; 512 513 "~ ^/data/" = { 514 root = "${cfg.stateDir}"; 515 extraConfig = "internal;"; 516 }; 517 518 "~ ^/lib.*\.(js|css|gif|png|ico|jpg|jpeg)$" = { 519 extraConfig = "expires 365d;"; 520 }; 521 522 "/" = { 523 priority = 1; 524 index = "doku.php"; 525 extraConfig = ''try_files $uri $uri/ @dokuwiki;''; 526 }; 527 528 "@dokuwiki" = { 529 extraConfig = '' 530 # rewrites "doku.php/" out of the URLs if you set the userwrite setting to .htaccess in dokuwiki config page 531 rewrite ^/_media/(.*) /lib/exe/fetch.php?media=$1 last; 532 rewrite ^/_detail/(.*) /lib/exe/detail.php?media=$1 last; 533 rewrite ^/_export/([^/]+)/(.*) /doku.php?do=export_$1&id=$2 last; 534 rewrite ^/(.*) /doku.php?id=$1&$args last; 535 ''; 536 }; 537 538 "~ \\.php$" = { 539 extraConfig = '' 540 try_files $uri $uri/ /doku.php; 541 include ${config.services.nginx.package}/conf/fastcgi_params; 542 fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 543 fastcgi_param REDIRECT_STATUS 200; 544 fastcgi_pass unix:${config.services.phpfpm.pools."dokuwiki-${hostName}".socket}; 545 ''; 546 }; 547 548 }; 549 }) eachSite; 550 }; 551 }) 552 553 (mkIf (cfg.webserver == "caddy") { 554 services.caddy = { 555 enable = true; 556 virtualHosts = mapAttrs' (hostName: cfg: ( 557 nameValuePair "http://${hostName}" { 558 extraConfig = '' 559 root * ${pkg hostName cfg}/share/dokuwiki 560 file_server 561 562 encode zstd gzip 563 php_fastcgi unix/${config.services.phpfpm.pools."dokuwiki-${hostName}".socket} 564 565 @restrict_files { 566 path /data/* /conf/* /bin/* /inc/* /vendor/* /install.php 567 } 568 569 respond @restrict_files 404 570 571 @allow_media { 572 path_regexp path ^/_media/(.*)$ 573 } 574 rewrite @allow_media /lib/exe/fetch.php?media=/{http.regexp.path.1} 575 576 @allow_detail { 577 path /_detail* 578 } 579 rewrite @allow_detail /lib/exe/detail.php?media={path} 580 581 @allow_export { 582 path /_export* 583 path_regexp export /([^/]+)/(.*) 584 } 585 rewrite @allow_export /doku.php?do=export_{http.regexp.export.1}&id={http.regexp.export.2} 586 587 try_files {path} {path}/ /doku.php?id={path}&{query} 588 ''; 589 } 590 )) eachSite; 591 }; 592 }) 593 594 ]); 595 596 meta.maintainers = with maintainers; [ 597 _1000101 598 onny 599 dandellion 600 e1mo 601 ]; 602}