grafana-alloy: don't build the frontend twice
[NixPkgs.git] / nixos / modules / services / web-apps / dokuwiki.nix
bloba075070f38b2bee748f06934b78adc032da5eccd
1 { config, pkgs, lib, ... }:
3 with lib;
5 let
6   inherit (lib.options) showOption showFiles;
8   cfg = config.services.dokuwiki;
9   eachSite = cfg.sites;
10   user = "dokuwiki";
11   webserver = config.services.${cfg.webserver};
13   mkPhpIni = generators.toKeyValue {
14     mkKeyValue = generators.mkKeyValueDefault {} " = ";
15   };
16   mkPhpPackage = cfg: cfg.phpPackage.buildEnv {
17     extraConfig = mkPhpIni cfg.phpOptions;
18   };
20   # "you're escaped" -> "'you\'re escaped'"
21   # https://www.php.net/manual/en/language.types.string.php#language.types.string.syntax.single
22   toPhpString = s: "'${escape [ "'" "\\" ] s}'";
24   dokuwikiAclAuthConfig = hostName: cfg: let
25     inherit (cfg) acl;
26     acl_gen = concatMapStringsSep "\n" (l: "${l.page} \t ${l.actor} \t ${toString l.level}");
27   in pkgs.writeText "acl.auth-${hostName}.php" ''
28     # acl.auth.php
29     # <?php exit()?>
30     #
31     # Access Control Lists
32     #
33     ${if isString acl then acl else acl_gen acl}
34   '';
36   mergeConfig = cfg: {
37     useacl = false; # Dokuwiki default
38     savedir = cfg.stateDir;
39   } // cfg.settings;
41   writePhpFile = name: text: pkgs.writeTextFile {
42     inherit name;
43     text = "<?php\n${text}";
44     checkPhase = "${pkgs.php81}/bin/php --syntax-check $target";
45   };
47   mkPhpValue = v: let
48     isHasAttr = s: isAttrs v && hasAttr s v;
49   in
50     if isString v then toPhpString v
51     # NOTE: If any value contains a , (comma) this will not get escaped
52     else if isList v && any lib.strings.isCoercibleToString v then toPhpString (concatMapStringsSep "," toString v)
53     else if isInt v then toString v
54     else if isBool v then toString (if v then 1 else 0)
55     else if isHasAttr "_file" then "trim(file_get_contents(${toPhpString v._file}))"
56     else if isHasAttr "_raw" then v._raw
57     else abort "The dokuwiki localConf value ${lib.generators.toPretty {} v} can not be encoded."
58   ;
60   mkPhpAttrVals = v: flatten (mapAttrsToList mkPhpKeyVal v);
61   mkPhpKeyVal = k: v: let
62     values = if (isAttrs v && (hasAttr "_file" v || hasAttr "_raw" v )) || !isAttrs v then
63       [" = ${mkPhpValue v};"]
64     else
65       mkPhpAttrVals v;
66   in map (e: "[${toPhpString k}]${e}") (flatten values);
68   dokuwikiLocalConfig = hostName: cfg: let
69     conf_gen = c: map (v: "$conf${v}") (mkPhpAttrVals c);
70   in writePhpFile "local-${hostName}.php" ''
71     ${concatStringsSep "\n" (conf_gen cfg.mergedConfig)}
72   '';
74   dokuwikiPluginsLocalConfig = hostName: cfg: let
75     pc = cfg.pluginsConfig;
76     pc_gen = pc: concatStringsSep "\n" (mapAttrsToList (n: v: "$plugins['${n}'] = ${boolToString v};") pc);
77   in writePhpFile "plugins.local-${hostName}.php" ''
78     ${if isString pc then pc else pc_gen pc}
79   '';
82   pkg = hostName: cfg: cfg.package.combine {
83     inherit (cfg) plugins templates;
85     pname = p: "${p.pname}-${hostName}";
87     basePackage = cfg.package;
88     localConfig = dokuwikiLocalConfig hostName cfg;
89     pluginsConfig = dokuwikiPluginsLocalConfig hostName cfg;
90     aclConfig = if cfg.settings.useacl && cfg.acl != null then dokuwikiAclAuthConfig hostName cfg else null;
91   };
93   aclOpts = { ... }: {
94     options = {
96       page = mkOption {
97         type = types.str;
98         description = "Page or namespace to restrict";
99         example = "start";
100       };
102       actor = mkOption {
103         type = types.str;
104         description = "User or group to restrict";
105         example = "@external";
106       };
108       level = let
109         available = {
110           "none" = 0;
111           "read" = 1;
112           "edit" = 2;
113           "create" = 4;
114           "upload" = 8;
115           "delete" = 16;
116         };
117       in mkOption {
118         type = types.enum ((attrValues available) ++ (attrNames available));
119         apply = x: if isInt x then x else available.${x};
120         description = ''
121           Permission level to restrict the actor(s) to.
122           See <https://www.dokuwiki.org/acl#background_info> for explanation
123         '';
124         example = "read";
125       };
126     };
127   };
129   siteOpts = { options, config, lib, name, ... }:
130     {
132       options = {
133         enable = mkEnableOption "DokuWiki web application";
135         package = mkPackageOption pkgs "dokuwiki" { };
137         stateDir = mkOption {
138           type = types.path;
139           default = "/var/lib/dokuwiki/${name}/data";
140           description = "Location of the DokuWiki state directory.";
141         };
143         acl = mkOption {
144           type = with types; nullOr (listOf (submodule aclOpts));
145           default = null;
146           example = literalExpression ''
147             [
148               {
149                 page = "start";
150                 actor = "@external";
151                 level = "read";
152               }
153               {
154                 page = "*";
155                 actor = "@users";
156                 level = "upload";
157               }
158             ]
159           '';
160           description = ''
161             Access Control Lists: see <https://www.dokuwiki.org/acl>
162             Mutually exclusive with services.dokuwiki.aclFile
163             Set this to a value other than null to take precedence over aclFile option.
165             Warning: Consider using aclFile instead if you do not
166             want to store the ACL in the world-readable Nix store.
167           '';
168         };
170         aclFile = mkOption {
171           type = with types; nullOr str;
172           default = if (config.mergedConfig.useacl && config.acl == null) then "/var/lib/dokuwiki/${name}/acl.auth.php" else null;
173           description = ''
174             Location of the dokuwiki acl rules. Mutually exclusive with services.dokuwiki.acl
175             Mutually exclusive with services.dokuwiki.acl which is preferred.
176             Consult documentation <https://www.dokuwiki.org/acl> for further instructions.
177             Example: <https://github.com/splitbrain/dokuwiki/blob/master/conf/acl.auth.php.dist>
178           '';
179           example = "/var/lib/dokuwiki/${name}/acl.auth.php";
180         };
182         pluginsConfig = mkOption {
183           type = with types; attrsOf bool;
184           default = {
185             authad = false;
186             authldap = false;
187             authmysql = false;
188             authpgsql = false;
189           };
190           description = ''
191             List of the dokuwiki (un)loaded plugins.
192           '';
193         };
195         usersFile = mkOption {
196           type = with types; nullOr str;
197           default = if config.mergedConfig.useacl then "/var/lib/dokuwiki/${name}/users.auth.php" else null;
198           description = ''
199             Location of the dokuwiki users file. List of users. Format:
201                 login:passwordhash:Real Name:email:groups,comma,separated
203             Create passwordHash easily by using:
205                 mkpasswd -5 password `pwgen 8 1`
207             Example: <https://github.com/splitbrain/dokuwiki/blob/master/conf/users.auth.php.dist>
208             '';
209           example = "/var/lib/dokuwiki/${name}/users.auth.php";
210         };
212         plugins = mkOption {
213           type = types.listOf types.path;
214           default = [];
215           description = ''
216                 List of path(s) to respective plugin(s) which are copied from the 'plugin' directory.
218                 ::: {.note}
219                 These plugins need to be packaged before use, see example.
220                 :::
221           '';
222           example = literalExpression ''
223                 let
224                   plugin-icalevents = pkgs.stdenv.mkDerivation rec {
225                     name = "icalevents";
226                     version = "2017-06-16";
227                     src = pkgs.fetchzip {
228                       stripRoot = false;
229                       url = "https://github.com/real-or-random/dokuwiki-plugin-icalevents/releases/download/''${version}/dokuwiki-plugin-icalevents-''${version}.zip";
230                       hash = "sha256-IPs4+qgEfe8AAWevbcCM9PnyI0uoyamtWeg4rEb+9Wc=";
231                     };
232                     installPhase = "mkdir -p $out; cp -R * $out/";
233                   };
234                 # And then pass this theme to the plugin list like this:
235                 in [ plugin-icalevents ]
236           '';
237         };
239         templates = mkOption {
240           type = types.listOf types.path;
241           default = [];
242           description = ''
243                 List of path(s) to respective template(s) which are copied from the 'tpl' directory.
245                 ::: {.note}
246                 These templates need to be packaged before use, see example.
247                 :::
248           '';
249           example = literalExpression ''
250                 let
251                   template-bootstrap3 = pkgs.stdenv.mkDerivation rec {
252                   name = "bootstrap3";
253                   version = "2022-07-27";
254                   src = pkgs.fetchFromGitHub {
255                     owner = "giterlizzi";
256                     repo = "dokuwiki-template-bootstrap3";
257                     rev = "v''${version}";
258                     hash = "sha256-B3Yd4lxdwqfCnfmZdp+i/Mzwn/aEuZ0ovagDxuR6lxo=";
259                   };
260                   installPhase = "mkdir -p $out; cp -R * $out/";
261                 };
262                 # And then pass this theme to the template list like this:
263                 in [ template-bootstrap3 ]
264           '';
265         };
267         poolConfig = mkOption {
268           type = with types; attrsOf (oneOf [ str int bool ]);
269           default = {
270             "pm" = "dynamic";
271             "pm.max_children" = 32;
272             "pm.start_servers" = 2;
273             "pm.min_spare_servers" = 2;
274             "pm.max_spare_servers" = 4;
275             "pm.max_requests" = 500;
276           };
277           description = ''
278             Options for the DokuWiki PHP pool. See the documentation on `php-fpm.conf`
279             for details on configuration directives.
280           '';
281         };
283         phpPackage = mkPackageOption pkgs "php" {
284           default = "php81";
285           example = "php82";
286         };
288         phpOptions = mkOption {
289           type = types.attrsOf types.str;
290           default = {};
291           description = ''
292             Options for PHP's php.ini file for this dokuwiki site.
293           '';
294           example = literalExpression ''
295           {
296             "opcache.interned_strings_buffer" = "8";
297             "opcache.max_accelerated_files" = "10000";
298             "opcache.memory_consumption" = "128";
299             "opcache.revalidate_freq" = "15";
300             "opcache.fast_shutdown" = "1";
301           }
302           '';
303         };
305         settings = mkOption {
306           type = types.attrsOf types.anything;
307           default = {
308             useacl = true;
309             superuser = "admin";
310           };
311           description = ''
312             Structural DokuWiki configuration.
313             Refer to <https://www.dokuwiki.org/config>
314             for details and supported values.
315             Settings can either be directly set from nix,
316             loaded from a file using `._file` or obtained from any
317             PHP function calls using `._raw`.
318           '';
319           example = literalExpression ''
320             {
321               title = "My Wiki";
322               userewrite = 1;
323               disableactions = [ "register" ]; # Will be concatenated with commas
324               plugin.smtp = {
325                 smtp_pass._file = "/var/run/secrets/dokuwiki/smtp_pass";
326                 smtp_user._raw = "getenv('DOKUWIKI_SMTP_USER')";
327               };
328             }
329           '';
330         };
332         mergedConfig = mkOption {
333           readOnly = true;
334           default = mergeConfig config;
335           defaultText = literalExpression ''
336             {
337               useacl = true;
338             }
339           '';
340           description = ''
341             Read only representation of the final configuration.
342           '';
343         };
345     };
346   };
349   options = {
350     services.dokuwiki = {
352       sites = mkOption {
353         type = types.attrsOf (types.submodule siteOpts);
354         default = {};
355         description = "Specification of one or more DokuWiki sites to serve";
356       };
358       webserver = mkOption {
359         type = types.enum [ "nginx" "caddy" ];
360         default = "nginx";
361         description = ''
362           Whether to use nginx or caddy for virtual host management.
364           Further nginx configuration can be done by adapting `services.nginx.virtualHosts.<name>`.
365           See [](#opt-services.nginx.virtualHosts) for further information.
367           Further caddy configuration can be done by adapting `services.caddy.virtualHosts.<name>`.
368           See [](#opt-services.caddy.virtualHosts) for further information.
369         '';
370       };
372     };
373   };
375   # implementation
376   config = mkIf (eachSite != {}) (mkMerge [{
378     services.phpfpm.pools = mapAttrs' (hostName: cfg: (
379       nameValuePair "dokuwiki-${hostName}" {
380         inherit user;
381         group = webserver.group;
383         phpPackage = mkPhpPackage cfg;
384         phpEnv = optionalAttrs (cfg.usersFile != null) {
385           DOKUWIKI_USERS_AUTH_CONFIG = "${cfg.usersFile}";
386         } // optionalAttrs (cfg.mergedConfig.useacl) {
387           DOKUWIKI_ACL_AUTH_CONFIG = if (cfg.acl != null) then "${dokuwikiAclAuthConfig hostName cfg}" else "${toString cfg.aclFile}";
388         };
390         settings = {
391           "listen.owner" = webserver.user;
392           "listen.group" = webserver.group;
393         } // cfg.poolConfig;
394       }
395     )) eachSite;
397   }
399   {
400     systemd.tmpfiles.rules = flatten (mapAttrsToList (hostName: cfg: [
401       "d ${cfg.stateDir}/attic 0750 ${user} ${webserver.group} - -"
402       "d ${cfg.stateDir}/cache 0750 ${user} ${webserver.group} - -"
403       "d ${cfg.stateDir}/index 0750 ${user} ${webserver.group} - -"
404       "d ${cfg.stateDir}/locks 0750 ${user} ${webserver.group} - -"
405       "d ${cfg.stateDir}/log 0750 ${user} ${webserver.group} - -"
406       "d ${cfg.stateDir}/media 0750 ${user} ${webserver.group} - -"
407       "d ${cfg.stateDir}/media_attic 0750 ${user} ${webserver.group} - -"
408       "d ${cfg.stateDir}/media_meta 0750 ${user} ${webserver.group} - -"
409       "d ${cfg.stateDir}/meta 0750 ${user} ${webserver.group} - -"
410       "d ${cfg.stateDir}/pages 0750 ${user} ${webserver.group} - -"
411       "d ${cfg.stateDir}/tmp 0750 ${user} ${webserver.group} - -"
412     ] ++ lib.optional (cfg.aclFile != null) "C ${cfg.aclFile} 0640 ${user} ${webserver.group} - ${pkg hostName cfg}/share/dokuwiki/conf/acl.auth.php.dist"
413     ++ lib.optional (cfg.usersFile != null) "C ${cfg.usersFile} 0640 ${user} ${webserver.group} - ${pkg hostName cfg}/share/dokuwiki/conf/users.auth.php.dist"
414     ) eachSite);
416     users.users.${user} = {
417       group = webserver.group;
418       isSystemUser = true;
419     };
420   }
422   (mkIf (cfg.webserver == "nginx") {
423     services.nginx = {
424       enable = true;
425       virtualHosts = mapAttrs (hostName: cfg: {
426         serverName = mkDefault hostName;
427         root = "${pkg hostName cfg}/share/dokuwiki";
429         locations = {
430           "~ /(conf/|bin/|inc/|install.php)" = {
431             extraConfig = "deny all;";
432           };
434           "~ ^/data/" = {
435             root = "${cfg.stateDir}";
436             extraConfig = "internal;";
437           };
439           "~ ^/lib.*\.(js|css|gif|png|ico|jpg|jpeg)$" = {
440             extraConfig = "expires 365d;";
441           };
443           "/" = {
444             priority = 1;
445             index = "doku.php";
446             extraConfig = ''try_files $uri $uri/ @dokuwiki;'';
447           };
449           "@dokuwiki" = {
450             extraConfig = ''
451               # rewrites "doku.php/" out of the URLs if you set the userwrite setting to .htaccess in dokuwiki config page
452               rewrite ^/_media/(.*) /lib/exe/fetch.php?media=$1 last;
453               rewrite ^/_detail/(.*) /lib/exe/detail.php?media=$1 last;
454               rewrite ^/_export/([^/]+)/(.*) /doku.php?do=export_$1&id=$2 last;
455               rewrite ^/(.*) /doku.php?id=$1&$args last;
456             '';
457           };
459           "~ \\.php$" = {
460             extraConfig = ''
461               try_files $uri $uri/ /doku.php;
462               include ${config.services.nginx.package}/conf/fastcgi_params;
463               fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
464               fastcgi_param REDIRECT_STATUS 200;
465               fastcgi_pass unix:${config.services.phpfpm.pools."dokuwiki-${hostName}".socket};
466               '';
467           };
469         };
470       }) eachSite;
471     };
472   })
474   (mkIf (cfg.webserver == "caddy") {
475     services.caddy = {
476       enable = true;
477       virtualHosts = mapAttrs' (hostName: cfg: (
478         nameValuePair "http://${hostName}" {
479           extraConfig = ''
480             root * ${pkg hostName cfg}/share/dokuwiki
481             file_server
483             encode zstd gzip
484             php_fastcgi unix/${config.services.phpfpm.pools."dokuwiki-${hostName}".socket}
486             @restrict_files {
487               path /data/* /conf/* /bin/* /inc/* /vendor/* /install.php
488             }
490             respond @restrict_files 404
492             @allow_media {
493               path_regexp path ^/_media/(.*)$
494             }
495             rewrite @allow_media /lib/exe/fetch.php?media=/{http.regexp.path.1}
497             @allow_detail   {
498               path /_detail*
499             }
500             rewrite @allow_detail /lib/exe/detail.php?media={path}
502             @allow_export   {
503               path /_export*
504               path_regexp export /([^/]+)/(.*)
505             }
506             rewrite @allow_export /doku.php?do=export_{http.regexp.export.1}&id={http.regexp.export.2}
508             try_files {path} {path}/ /doku.php?id={path}&{query}
509           '';
510         }
511       )) eachSite;
512     };
513   })
515   ]);
517   meta.maintainers = with maintainers; [
518     _1000101
519     onny
520     dandellion
521     e1mo
522   ];