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}