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