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}