1{ config, pkgs, lib, ... }:
2
3let
4 inherit (lib) mkDefault mkEnableOption mkForce mkIf mkMerge mkOption types maintainers recursiveUpdate;
5 inherit (lib) any attrValues concatMapStrings concatMapStringsSep flatten literalExpression;
6 inherit (lib) filterAttrs mapAttrs mapAttrs' mapAttrsToList nameValuePair optional optionalAttrs optionalString;
7
8 cfg = migrateOldAttrs config.services.dokuwiki;
9 eachSite = cfg.sites;
10 user = "dokuwiki";
11 webserver = config.services.${cfg.webserver};
12 stateDir = hostName: "/var/lib/dokuwiki/${hostName}/data";
13
14 # Migrate config.services.dokuwiki.<hostName> to config.services.dokuwiki.sites.<hostName>
15 oldSites = filterAttrs (o: _: o != "sites" && o != "webserver");
16 migrateOldAttrs = cfg: cfg // { sites = cfg.sites // oldSites cfg; };
17
18 dokuwikiAclAuthConfig = hostName: cfg: pkgs.writeText "acl.auth-${hostName}.php" ''
19 # acl.auth.php
20 # <?php exit()?>
21 #
22 # Access Control Lists
23 #
24 ${toString cfg.acl}
25 '';
26
27 dokuwikiLocalConfig = hostName: cfg: pkgs.writeText "local-${hostName}.php" ''
28 <?php
29 $conf['savedir'] = '${cfg.stateDir}';
30 $conf['superuser'] = '${toString cfg.superUser}';
31 $conf['useacl'] = '${toString cfg.aclUse}';
32 $conf['disableactions'] = '${cfg.disableActions}';
33 ${toString cfg.extraConfig}
34 '';
35
36 dokuwikiPluginsLocalConfig = hostName: cfg: pkgs.writeText "plugins.local-${hostName}.php" ''
37 <?php
38 ${cfg.pluginsConfig}
39 '';
40
41
42 pkg = hostName: cfg: pkgs.stdenv.mkDerivation rec {
43 pname = "dokuwiki-${hostName}";
44 version = src.version;
45 src = cfg.package;
46
47 installPhase = ''
48 mkdir -p $out
49 cp -r * $out/
50
51 # symlink the dokuwiki config
52 ln -s ${dokuwikiLocalConfig hostName cfg} $out/share/dokuwiki/local.php
53
54 # symlink plugins config
55 ln -s ${dokuwikiPluginsLocalConfig hostName cfg} $out/share/dokuwiki/plugins.local.php
56
57 # symlink acl
58 ln -s ${dokuwikiAclAuthConfig hostName cfg} $out/share/dokuwiki/acl.auth.php
59
60 # symlink additional plugin(s) and templates(s)
61 ${concatMapStringsSep "\n" (template: "ln -s ${template} $out/share/dokuwiki/lib/tpl/${template.name}") cfg.templates}
62 ${concatMapStringsSep "\n" (plugin: "ln -s ${plugin} $out/share/dokuwiki/lib/plugins/${plugin.name}") cfg.plugins}
63 '';
64 };
65
66 siteOpts = { config, lib, name, ... }:
67 {
68 options = {
69 enable = mkEnableOption "DokuWiki web application.";
70
71 package = mkOption {
72 type = types.package;
73 default = pkgs.dokuwiki;
74 defaultText = literalExpression "pkgs.dokuwiki";
75 description = "Which DokuWiki package to use.";
76 };
77
78 stateDir = mkOption {
79 type = types.path;
80 default = "/var/lib/dokuwiki/${name}/data";
81 description = "Location of the DokuWiki state directory.";
82 };
83
84 acl = mkOption {
85 type = types.nullOr types.lines;
86 default = null;
87 example = "* @ALL 8";
88 description = ''
89 Access Control Lists: see <link xlink:href="https://www.dokuwiki.org/acl"/>
90 Mutually exclusive with services.dokuwiki.aclFile
91 Set this to a value other than null to take precedence over aclFile option.
92
93 Warning: Consider using aclFile instead if you do not
94 want to store the ACL in the world-readable Nix store.
95 '';
96 };
97
98 aclFile = mkOption {
99 type = with types; nullOr str;
100 default = if (config.aclUse && config.acl == null) then "/var/lib/dokuwiki/${name}/acl.auth.php" else null;
101 description = ''
102 Location of the dokuwiki acl rules. Mutually exclusive with services.dokuwiki.acl
103 Mutually exclusive with services.dokuwiki.acl which is preferred.
104 Consult documentation <link xlink:href="https://www.dokuwiki.org/acl"/> for further instructions.
105 Example: <link xlink:href="https://github.com/splitbrain/dokuwiki/blob/master/conf/acl.auth.php.dist"/>
106 '';
107 example = "/var/lib/dokuwiki/${name}/acl.auth.php";
108 };
109
110 aclUse = mkOption {
111 type = types.bool;
112 default = true;
113 description = ''
114 Necessary for users to log in into the system.
115 Also limits anonymous users. When disabled,
116 everyone is able to create and edit content.
117 '';
118 };
119
120 pluginsConfig = mkOption {
121 type = types.lines;
122 default = ''
123 $plugins['authad'] = 0;
124 $plugins['authldap'] = 0;
125 $plugins['authmysql'] = 0;
126 $plugins['authpgsql'] = 0;
127 '';
128 description = ''
129 List of the dokuwiki (un)loaded plugins.
130 '';
131 };
132
133 superUser = mkOption {
134 type = types.nullOr types.str;
135 default = "@admin";
136 description = ''
137 You can set either a username, a list of usernames (“admin1,admin2”),
138 or the name of a group by prepending an @ char to the groupname
139 Consult documentation <link xlink:href="https://www.dokuwiki.org/config:superuser"/> for further instructions.
140 '';
141 };
142
143 usersFile = mkOption {
144 type = with types; nullOr str;
145 default = if config.aclUse then "/var/lib/dokuwiki/${name}/users.auth.php" else null;
146 description = ''
147 Location of the dokuwiki users file. List of users. Format:
148 login:passwordhash:Real Name:email:groups,comma,separated
149 Create passwordHash easily by using:$ mkpasswd -5 password `pwgen 8 1`
150 Example: <link xlink:href="https://github.com/splitbrain/dokuwiki/blob/master/conf/users.auth.php.dist"/>
151 '';
152 example = "/var/lib/dokuwiki/${name}/users.auth.php";
153 };
154
155 disableActions = mkOption {
156 type = types.nullOr types.str;
157 default = "";
158 example = "search,register";
159 description = ''
160 Disable individual action modes. Refer to
161 <link xlink:href="https://www.dokuwiki.org/config:action_modes"/>
162 for details on supported values.
163 '';
164 };
165
166 plugins = mkOption {
167 type = types.listOf types.path;
168 default = [];
169 description = ''
170 List of path(s) to respective plugin(s) which are copied from the 'plugin' directory.
171 <note><para>These plugins need to be packaged before use, see example.</para></note>
172 '';
173 example = literalExpression ''
174 let
175 # Let's package the icalevents plugin
176 plugin-icalevents = pkgs.stdenv.mkDerivation {
177 name = "icalevents";
178 # Download the plugin from the dokuwiki site
179 src = pkgs.fetchurl {
180 url = "https://github.com/real-or-random/dokuwiki-plugin-icalevents/releases/download/2017-06-16/dokuwiki-plugin-icalevents-2017-06-16.zip";
181 sha256 = "e40ed7dd6bbe7fe3363bbbecb4de481d5e42385b5a0f62f6a6ce6bf3a1f9dfa8";
182 };
183 sourceRoot = ".";
184 # We need unzip to build this package
185 buildInputs = [ pkgs.unzip ];
186 # Installing simply means copying all files to the output directory
187 installPhase = "mkdir -p $out; cp -R * $out/";
188 };
189 # And then pass this theme to the plugin list like this:
190 in [ plugin-icalevents ]
191 '';
192 };
193
194 templates = mkOption {
195 type = types.listOf types.path;
196 default = [];
197 description = ''
198 List of path(s) to respective template(s) which are copied from the 'tpl' directory.
199 <note><para>These templates need to be packaged before use, see example.</para></note>
200 '';
201 example = literalExpression ''
202 let
203 # Let's package the bootstrap3 theme
204 template-bootstrap3 = pkgs.stdenv.mkDerivation {
205 name = "bootstrap3";
206 # Download the theme from the dokuwiki site
207 src = pkgs.fetchurl {
208 url = "https://github.com/giterlizzi/dokuwiki-template-bootstrap3/archive/v2019-05-22.zip";
209 sha256 = "4de5ff31d54dd61bbccaf092c9e74c1af3a4c53e07aa59f60457a8f00cfb23a6";
210 };
211 # We need unzip to build this package
212 buildInputs = [ pkgs.unzip ];
213 # Installing simply means copying all files to the output directory
214 installPhase = "mkdir -p $out; cp -R * $out/";
215 };
216 # And then pass this theme to the template list like this:
217 in [ template-bootstrap3 ]
218 '';
219 };
220
221 poolConfig = mkOption {
222 type = with types; attrsOf (oneOf [ str int bool ]);
223 default = {
224 "pm" = "dynamic";
225 "pm.max_children" = 32;
226 "pm.start_servers" = 2;
227 "pm.min_spare_servers" = 2;
228 "pm.max_spare_servers" = 4;
229 "pm.max_requests" = 500;
230 };
231 description = ''
232 Options for the DokuWiki PHP pool. See the documentation on <literal>php-fpm.conf</literal>
233 for details on configuration directives.
234 '';
235 };
236
237 extraConfig = mkOption {
238 type = types.nullOr types.lines;
239 default = null;
240 example = ''
241 $conf['title'] = 'My Wiki';
242 $conf['userewrite'] = 1;
243 '';
244 description = ''
245 DokuWiki configuration. Refer to
246 <link xlink:href="https://www.dokuwiki.org/config"/>
247 for details on supported values.
248 '';
249 };
250
251 };
252
253 };
254in
255{
256 # interface
257 options = {
258 services.dokuwiki = mkOption {
259 type = types.submodule {
260 # Used to support old interface
261 freeformType = types.attrsOf (types.submodule siteOpts);
262
263 # New interface
264 options.sites = mkOption {
265 type = types.attrsOf (types.submodule siteOpts);
266 default = {};
267 description = "Specification of one or more DokuWiki sites to serve";
268 };
269
270 options.webserver = mkOption {
271 type = types.enum [ "nginx" "caddy" ];
272 default = "nginx";
273 description = ''
274 Whether to use nginx or caddy for virtual host management.
275
276 Further nginx configuration can be done by adapting <literal>services.nginx.virtualHosts.<name></literal>.
277 See <xref linkend="opt-services.nginx.virtualHosts"/> for further information.
278
279 Further apache2 configuration can be done by adapting <literal>services.httpd.virtualHosts.<name></literal>.
280 See <xref linkend="opt-services.httpd.virtualHosts"/> for further information.
281 '';
282 };
283 };
284 default = {};
285 description = "DokuWiki configuration";
286 };
287
288 };
289
290 # implementation
291 config = mkIf (eachSite != {}) (mkMerge [{
292
293 assertions = flatten (mapAttrsToList (hostName: cfg:
294 [{
295 assertion = cfg.aclUse -> (cfg.acl != null || cfg.aclFile != null);
296 message = "Either services.dokuwiki.sites.${hostName}.acl or services.dokuwiki.sites.${hostName}.aclFile is mandatory if aclUse true";
297 }
298 {
299 assertion = cfg.usersFile != null -> cfg.aclUse != false;
300 message = "services.dokuwiki.sites.${hostName}.aclUse must must be true if usersFile is not null";
301 }
302 ]) eachSite);
303
304 warnings = mapAttrsToList (hostName: _: ''services.dokuwiki."${hostName}" is deprecated use services.dokuwiki.sites."${hostName}"'') (oldSites cfg);
305
306 services.phpfpm.pools = mapAttrs' (hostName: cfg: (
307 nameValuePair "dokuwiki-${hostName}" {
308 inherit user;
309 group = webserver.group;
310
311 phpEnv = {
312 DOKUWIKI_LOCAL_CONFIG = "${dokuwikiLocalConfig hostName cfg}";
313 DOKUWIKI_PLUGINS_LOCAL_CONFIG = "${dokuwikiPluginsLocalConfig hostName cfg}";
314 } // optionalAttrs (cfg.usersFile != null) {
315 DOKUWIKI_USERS_AUTH_CONFIG = "${cfg.usersFile}";
316 } //optionalAttrs (cfg.aclUse) {
317 DOKUWIKI_ACL_AUTH_CONFIG = if (cfg.acl != null) then "${dokuwikiAclAuthConfig hostName cfg}" else "${toString cfg.aclFile}";
318 };
319
320 settings = {
321 "listen.owner" = webserver.user;
322 "listen.group" = webserver.group;
323 } // cfg.poolConfig;
324 }
325 )) eachSite;
326
327 }
328
329 {
330 systemd.tmpfiles.rules = flatten (mapAttrsToList (hostName: cfg: [
331 "d ${stateDir hostName}/attic 0750 ${user} ${webserver.group} - -"
332 "d ${stateDir hostName}/cache 0750 ${user} ${webserver.group} - -"
333 "d ${stateDir hostName}/index 0750 ${user} ${webserver.group} - -"
334 "d ${stateDir hostName}/locks 0750 ${user} ${webserver.group} - -"
335 "d ${stateDir hostName}/media 0750 ${user} ${webserver.group} - -"
336 "d ${stateDir hostName}/media_attic 0750 ${user} ${webserver.group} - -"
337 "d ${stateDir hostName}/media_meta 0750 ${user} ${webserver.group} - -"
338 "d ${stateDir hostName}/meta 0750 ${user} ${webserver.group} - -"
339 "d ${stateDir hostName}/pages 0750 ${user} ${webserver.group} - -"
340 "d ${stateDir hostName}/tmp 0750 ${user} ${webserver.group} - -"
341 ] ++ lib.optional (cfg.aclFile != null) "C ${cfg.aclFile} 0640 ${user} ${webserver.group} - ${pkg hostName cfg}/share/dokuwiki/conf/acl.auth.php.dist"
342 ++ lib.optional (cfg.usersFile != null) "C ${cfg.usersFile} 0640 ${user} ${webserver.group} - ${pkg hostName cfg}/share/dokuwiki/conf/users.auth.php.dist"
343 ) eachSite);
344
345 users.users.${user} = {
346 group = webserver.group;
347 isSystemUser = true;
348 };
349 }
350
351 (mkIf (cfg.webserver == "nginx") {
352 services.nginx = {
353 enable = true;
354 virtualHosts = mapAttrs (hostName: cfg: {
355 serverName = mkDefault hostName;
356 root = "${pkg hostName cfg}/share/dokuwiki";
357
358 locations = {
359 "~ /(conf/|bin/|inc/|install.php)" = {
360 extraConfig = "deny all;";
361 };
362
363 "~ ^/data/" = {
364 root = "${stateDir hostName}";
365 extraConfig = "internal;";
366 };
367
368 "~ ^/lib.*\.(js|css|gif|png|ico|jpg|jpeg)$" = {
369 extraConfig = "expires 365d;";
370 };
371
372 "/" = {
373 priority = 1;
374 index = "doku.php";
375 extraConfig = ''try_files $uri $uri/ @dokuwiki;'';
376 };
377
378 "@dokuwiki" = {
379 extraConfig = ''
380 # rewrites "doku.php/" out of the URLs if you set the userwrite setting to .htaccess in dokuwiki config page
381 rewrite ^/_media/(.*) /lib/exe/fetch.php?media=$1 last;
382 rewrite ^/_detail/(.*) /lib/exe/detail.php?media=$1 last;
383 rewrite ^/_export/([^/]+)/(.*) /doku.php?do=export_$1&id=$2 last;
384 rewrite ^/(.*) /doku.php?id=$1&$args last;
385 '';
386 };
387
388 "~ \\.php$" = {
389 extraConfig = ''
390 try_files $uri $uri/ /doku.php;
391 include ${pkgs.nginx}/conf/fastcgi_params;
392 fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
393 fastcgi_param REDIRECT_STATUS 200;
394 fastcgi_pass unix:${config.services.phpfpm.pools."dokuwiki-${hostName}".socket};
395 '';
396 };
397
398 };
399 }) eachSite;
400 };
401 })
402
403 (mkIf (cfg.webserver == "caddy") {
404 services.caddy = {
405 enable = true;
406 virtualHosts = mapAttrs' (hostName: cfg: (
407 nameValuePair "http://${hostName}" {
408 extraConfig = ''
409 root * ${pkg hostName cfg}/share/dokuwiki
410 file_server
411
412 encode zstd gzip
413 php_fastcgi unix/${config.services.phpfpm.pools."dokuwiki-${hostName}".socket}
414
415 @restrict_files {
416 path /data/* /conf/* /bin/* /inc/* /vendor/* /install.php
417 }
418
419 respond @restrict_files 404
420
421 @allow_media {
422 path_regexp path ^/_media/(.*)$
423 }
424 rewrite @allow_media /lib/exe/fetch.php?media=/{http.regexp.path.1}
425
426 @allow_detail {
427 path /_detail*
428 }
429 rewrite @allow_detail /lib/exe/detail.php?media={path}
430
431 @allow_export {
432 path /_export*
433 path_regexp export /([^/]+)/(.*)
434 }
435 rewrite @allow_export /doku.php?do=export_{http.regexp.export.1}&id={http.regexp.export.2}
436
437 try_files {path} {path}/ /doku.php?id={path}&{query}
438 '';
439 }
440 )) eachSite;
441 };
442 })
443
444 ]);
445
446 meta.maintainers = with maintainers; [
447 _1000101
448 onny
449 ];
450}