1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6
7 cfg = config.services.gitit;
8
9 homeDir = "/var/lib/gitit";
10
11 toYesNo = b: if b then "yes" else "no";
12
13 gititShared = with cfg.haskellPackages; gitit + "/share/" + pkgs.stdenv.system + "-" + ghc.name + "/" + gitit.pname + "-" + gitit.version;
14
15 gititWithPkgs = hsPkgs: extras: hsPkgs.ghcWithPackages (self: with self; [ gitit ] ++ (extras self));
16
17 gititSh = hsPkgs: extras: with pkgs; let
18 env = gititWithPkgs hsPkgs extras;
19 in writeScript "gitit" ''
20 #!${stdenv.shell}
21 cd $HOME
22 export NIX_GHC="${env}/bin/ghc"
23 export NIX_GHCPKG="${env}/bin/ghc-pkg"
24 export NIX_GHC_DOCDIR="${env}/share/doc/ghc/html"
25 export NIX_GHC_LIBDIR=$( $NIX_GHC --print-libdir )
26 ${env}/bin/gitit -f ${configFile}
27 '';
28
29 gititOptions = {
30
31 enable = mkOption {
32 type = types.bool;
33 default = false;
34 description = "Enable the gitit service.";
35 };
36
37 haskellPackages = mkOption {
38 defaultText = "pkgs.haskellPackages";
39 example = literalExample "pkgs.haskell.packages.ghc784";
40 description = "haskellPackages used to build gitit and plugins.";
41 };
42
43 extraPackages = mkOption {
44 default = self: [];
45 example = literalExample ''
46 haskellPackages: [
47 haskellPackages.wreq
48 ]
49 '';
50 description = ''
51 Extra packages available to ghc when running gitit. The
52 value must be a function which receives the attrset defined
53 in <varname>haskellPackages</varname> as the sole argument.
54 '';
55 };
56
57 address = mkOption {
58 type = types.str;
59 default = "0.0.0.0";
60 description = "IP address on which the web server will listen.";
61 };
62
63 port = mkOption {
64 type = types.int;
65 default = 5001;
66 description = "Port on which the web server will run.";
67 };
68
69 wikiTitle = mkOption {
70 type = types.str;
71 default = "Gitit!";
72 description = "The wiki title.";
73 };
74
75 repositoryType = mkOption {
76 type = types.enum ["git" "darcs" "mercurial"];
77 default = "git";
78 description = "Specifies the type of repository used for wiki content.";
79 };
80
81 repositoryPath = mkOption {
82 type = types.path;
83 default = homeDir + "/wiki";
84 description = ''
85 Specifies the path of the repository directory. If it does not
86 exist, gitit will create it on startup.
87 '';
88 };
89
90 requireAuthentication = mkOption {
91 type = types.enum [ "none" "modify" "read" ];
92 default = "modify";
93 description = ''
94 If 'none', login is never required, and pages can be edited
95 anonymously. If 'modify', login is required to modify the wiki
96 (edit, add, delete pages, upload files). If 'read', login is
97 required to see any wiki pages.
98 '';
99 };
100
101 authenticationMethod = mkOption {
102 type = types.enum [ "form" "http" "generic"];
103 default = "form";
104 description = ''
105 'form' means that users will be logged in and registered using forms
106 in the gitit web interface. 'http' means that gitit will assume that
107 HTTP authentication is in place and take the logged in username from
108 the "Authorization" field of the HTTP request header (in addition,
109 the login/logout and registration links will be suppressed).
110 'generic' means that gitit will assume that some form of
111 authentication is in place that directly sets REMOTE_USER to the name
112 of the authenticated user (e.g. mod_auth_cas on apache). 'rpx' means
113 that gitit will attempt to log in through https://rpxnow.com. This
114 requires that 'rpx-domain', 'rpx-key', and 'base-url' be set below,
115 and that 'curl' be in the system path.
116 '';
117 };
118
119 userFile = mkOption {
120 type = types.path;
121 default = homeDir + "/gitit-users";
122 description = ''
123 Specifies the path of the file containing user login information. If
124 it does not exist, gitit will create it (with an empty user list).
125 This file is not used if 'http' is selected for
126 authentication-method.
127 '';
128 };
129
130 sessionTimeout = mkOption {
131 type = types.int;
132 default = 60;
133 description = ''
134 Number of minutes of inactivity before a session expires.
135 '';
136 };
137
138 staticDir = mkOption {
139 type = types.path;
140 description = ''
141 Specifies the path of the static directory (containing javascript,
142 css, and images). If it does not exist, gitit will create it and
143 populate it with required scripts, stylesheets, and images.
144 '';
145 };
146
147 defaultPageType = mkOption {
148 type = types.enum [ "markdown" "rst" "latex" "html" "markdown+lhs" "rst+lhs" "latex+lhs" ];
149 default = "markdown";
150 description = ''
151 Specifies the type of markup used to interpret pages in the wiki.
152 Possible values are markdown, rst, latex, html, markdown+lhs,
153 rst+lhs, and latex+lhs. (the +lhs variants treat the input as
154 literate Haskell. See pandoc's documentation for more details.) If
155 Markdown is selected, pandoc's syntax extensions (for footnotes,
156 delimited code blocks, etc.) will be enabled. Note that pandoc's
157 restructuredtext parser is not complete, so some pages may not be
158 rendered correctly if rst is selected. The same goes for latex and
159 html.
160 '';
161 };
162
163 math = mkOption {
164 type = types.enum [ "mathml" "raw" "mathjax" "jsmath" "google" ];
165 default = "mathml";
166 description = ''
167 Specifies how LaTeX math is to be displayed. Possible values are
168 mathml, raw, mathjax, jsmath, and google. If mathml is selected,
169 gitit will convert LaTeX math to MathML and link in a script,
170 MathMLinHTML.js, that allows the MathML to be seen in Gecko browsers,
171 IE + mathplayer, and Opera. In other browsers you may get a jumble of
172 characters. If raw is selected, the LaTeX math will be displayed as
173 raw LaTeX math. If mathjax is selected, gitit will link to the
174 remote mathjax script. If jsMath is selected, gitit will link to the
175 script /js/jsMath/easy/load.js, and will assume that jsMath has been
176 installed into the js/jsMath directory. This is the most portable
177 solution. If google is selected, the google chart API is called to
178 render the formula as an image. This requires a connection to google,
179 and might raise a technical or a privacy problem.
180 '';
181 };
182
183 mathJaxScript = mkOption {
184 type = types.str;
185 default = "https://d3eoax9i5htok0.cloudfront.net/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML";
186 description = ''
187 Specifies the path to MathJax rendering script. You might want to
188 use your own MathJax script to render formulas without Internet
189 connection or if you want to use some special LaTeX packages. Note:
190 path specified there cannot be an absolute path to a script on your
191 hdd, instead you should run your (local if you wish) HTTP server
192 which will serve the MathJax.js script. You can easily (in four lines
193 of code) serve MathJax.js using
194 http://happstack.com/docs/crashcourse/FileServing.html Do not forget
195 the "http://" prefix (e.g. http://localhost:1234/MathJax.js).
196 '';
197 };
198
199 showLhsBirdTracks = mkOption {
200 type = types.bool;
201 default = false;
202 description = ''
203 Specifies whether to show Haskell code blocks in "bird style", with
204 "> " at the beginning of each line.
205 '';
206 };
207
208 templatesDir = mkOption {
209 type = types.path;
210 description = ''
211 Specifies the path of the directory containing page templates. If it
212 does not exist, gitit will create it with default templates. Users
213 may wish to edit the templates to customize the appearance of their
214 wiki. The template files are HStringTemplate templates. Variables to
215 be interpolated appear between $\'s. Literal $\'s must be
216 backslash-escaped.
217 '';
218 };
219
220 logFile = mkOption {
221 type = types.path;
222 default = homeDir + "/gitit.log";
223 description = ''
224 Specifies the path of gitit's log file. If it does not exist, gitit
225 will create it. The log is in Apache combined log format.
226 '';
227 };
228
229 logLevel = mkOption {
230 type = types.enum [ "DEBUG" "INFO" "NOTICE" "WARNING" "ERROR" "CRITICAL" "ALERT" "EMERGENCY" ];
231 default = "ERROR";
232 description = ''
233 Determines how much information is logged. Possible values (from
234 most to least verbose) are DEBUG, INFO, NOTICE, WARNING, ERROR,
235 CRITICAL, ALERT, EMERGENCY.
236 '';
237 };
238
239 frontPage = mkOption {
240 type = types.str;
241 default = "Front Page";
242 description = ''
243 Specifies which wiki page is to be used as the wiki's front page.
244 Gitit creates a default front page on startup, if one does not exist
245 already.
246 '';
247 };
248
249 noDelete = mkOption {
250 type = types.str;
251 default = "Front Page, Help";
252 description = ''
253 Specifies pages that cannot be deleted through the web interface.
254 (They can still be deleted directly using git or darcs.) A
255 comma-separated list of page names. Leave blank to allow every page
256 to be deleted.
257 '';
258 };
259
260 noEdit = mkOption {
261 type = types.str;
262 default = "Help";
263 description = ''
264 Specifies pages that cannot be edited through the web interface.
265 Leave blank to allow every page to be edited.
266 '';
267 };
268
269 defaultSummary = mkOption {
270 type = types.str;
271 default = "";
272 description = ''
273 Specifies text to be used in the change description if the author
274 leaves the "description" field blank. If default-summary is blank
275 (the default), the author will be required to fill in the description
276 field.
277 '';
278 };
279
280 tableOfContents = mkOption {
281 type = types.bool;
282 default = true;
283 description = ''
284 Specifies whether to print a tables of contents (with links to
285 sections) on each wiki page.
286 '';
287 };
288
289 plugins = mkOption {
290 type = with types; listOf str;
291 description = ''
292 Specifies a list of plugins to load. Plugins may be specified either
293 by their path or by their module name. If the plugin name starts
294 with Gitit.Plugin., gitit will assume that the plugin is an installed
295 module and will not try to find a source file.
296 '';
297 };
298
299 useCache = mkOption {
300 type = types.bool;
301 default = false;
302 description = ''
303 Specifies whether to cache rendered pages. Note that if use-feed is
304 selected, feeds will be cached regardless of the value of use-cache.
305 '';
306 };
307
308 cacheDir = mkOption {
309 type = types.path;
310 default = homeDir + "/cache";
311 description = "Path where rendered pages will be cached.";
312 };
313
314 maxUploadSize = mkOption {
315 type = types.str;
316 default = "1000K";
317 description = ''
318 Specifies an upper limit on the size (in bytes) of files uploaded
319 through the wiki's web interface. To disable uploads, set this to
320 0K. This will result in the uploads link disappearing and the
321 _upload url becoming inactive.
322 '';
323 };
324
325 maxPageSize = mkOption {
326 type = types.str;
327 default = "1000K";
328 description = "Specifies an upper limit on the size (in bytes) of pages.";
329 };
330
331 debugMode = mkOption {
332 type = types.bool;
333 default = false;
334 description = "Causes debug information to be logged while gitit is running.";
335 };
336
337 compressResponses = mkOption {
338 type = types.bool;
339 default = true;
340 description = "Specifies whether HTTP responses should be compressed.";
341 };
342
343 mimeTypesFile = mkOption {
344 type = types.path;
345 default = "/etc/mime/types.info";
346 description = ''
347 Specifies the path of a file containing mime type mappings. Each
348 line of the file should contain two fields, separated by whitespace.
349 The first field is the mime type, the second is a file extension.
350 For example:
351<programlisting>
352video/x-ms-wmx wmx
353</programlisting>
354 If the file is not found, some simple defaults will be used.
355 '';
356 };
357
358 useReCaptcha = mkOption {
359 type = types.bool;
360 default = false;
361 description = ''
362 If true, causes gitit to use the reCAPTCHA service
363 (http://recaptcha.net) to prevent bots from creating accounts.
364 '';
365 };
366
367 reCaptchaPrivateKey = mkOption {
368 type = with types; nullOr str;
369 default = null;
370 description = ''
371 Specifies the private key for the reCAPTCHA service. To get
372 these, you need to create an account at http://recaptcha.net.
373 '';
374 };
375
376 reCaptchaPublicKey = mkOption {
377 type = with types; nullOr str;
378 default = null;
379 description = ''
380 Specifies the public key for the reCAPTCHA service. To get
381 these, you need to create an account at http://recaptcha.net.
382 '';
383 };
384
385 accessQuestion = mkOption {
386 type = types.str;
387 default = "What is the code given to you by Ms. X?";
388 description = ''
389 Specifies a question that users must answer when they attempt to
390 create an account
391 '';
392 };
393
394 accessQuestionAnswers = mkOption {
395 type = types.str;
396 default = "RED DOG, red dog";
397 description = ''
398 Specifies a question that users must answer when they attempt to
399 create an account, along with a comma-separated list of acceptable
400 answers. This can be used to institute a rudimentary password for
401 signing up as a user on the wiki, or as an alternative to reCAPTCHA.
402 Example:
403 access-question: What is the code given to you by Ms. X?
404 access-question-answers: RED DOG, red dog
405 '';
406 };
407
408 rpxDomain = mkOption {
409 type = with types; nullOr str;
410 default = null;
411 description = ''
412 Specifies the domain and key of your RPX account. The domain is just
413 the prefix of the complete RPX domain, so if your full domain is
414 'https://foo.rpxnow.com/', use 'foo' as the value of rpx-domain.
415 '';
416 };
417
418 rpxKey = mkOption {
419 type = with types; nullOr str;
420 default = null;
421 description = "RPX account access key.";
422 };
423
424 mailCommand = mkOption {
425 type = types.str;
426 default = "sendmail %s";
427 description = ''
428 Specifies the command to use to send notification emails. '%s' will
429 be replaced by the destination email address. The body of the
430 message will be read from stdin. If this field is left blank,
431 password reset will not be offered.
432 '';
433 };
434
435 resetPasswordMessage = mkOption {
436 type = types.lines;
437 default = ''
438 > From: gitit@$hostname$
439 > To: $useremail$
440 > Subject: Wiki password reset
441 >
442 > Hello $username$,
443 >
444 > To reset your password, please follow the link below:
445 > http://$hostname$:$port$$resetlink$
446 >
447 > Regards
448 '';
449 description = ''
450 Gives the text of the message that will be sent to the user should
451 she want to reset her password, or change other registration info.
452 The lines must be indented, and must begin with '>'. The initial
453 spaces and '> ' will be stripped off. $username$ will be replaced by
454 the user's username, $useremail$ by her email address, $hostname$ by
455 the hostname on which the wiki is running (as returned by the
456 hostname system call), $port$ by the port on which the wiki is
457 running, and $resetlink$ by the relative path of a reset link derived
458 from the user's existing hashed password. If your gitit wiki is being
459 proxied to a location other than the root path of $port$, you should
460 change the link to reflect this: for example, to
461 http://$hostname$/path/to/wiki$resetlink$ or
462 http://gitit.$hostname$$resetlink$
463 '';
464 };
465
466 useFeed = mkOption {
467 type = types.bool;
468 default = false;
469 description = ''
470 Specifies whether an ATOM feed should be enabled (for the site and
471 for individual pages).
472 '';
473 };
474
475 baseUrl = mkOption {
476 type = with types; nullOr str;
477 default = null;
478 description = ''
479 The base URL of the wiki, to be used in constructing feed IDs and RPX
480 token_urls. Set this if useFeed is false or authentication-method
481 is 'rpx'.
482 '';
483 };
484
485 absoluteUrls = mkOption {
486 type = types.bool;
487 default = false;
488 description = ''
489 Make wikilinks absolute with respect to the base-url. So, for
490 example, in a wiki served at the base URL '/wiki', on a page
491 Sub/Page, the wikilink '[Cactus]()' will produce a link to
492 '/wiki/Cactus' if absoluteUrls is true, and a relative link to
493 'Cactus' (referring to '/wiki/Sub/Cactus') if absolute-urls is 'no'.
494 '';
495 };
496
497 feedDays = mkOption {
498 type = types.int;
499 default = 14;
500 description = "Number of days to be included in feeds.";
501 };
502
503 feedRefreshTime = mkOption {
504 type = types.int;
505 default = 60;
506 description = "Number of minutes to cache feeds before refreshing.";
507 };
508
509 pdfExport = mkOption {
510 type = types.bool;
511 default = false;
512 description = ''
513 If true, PDF will appear in export options. PDF will be created using
514 pdflatex, which must be installed and in the path. Note that PDF
515 exports create significant additional server load.
516 '';
517 };
518
519 pandocUserData = mkOption {
520 type = with types; nullOr path;
521 default = null;
522 description = ''
523 If a directory is specified, this will be searched for pandoc
524 customizations. These can include a templates/ directory for custom
525 templates for various export formats, an S5 directory for custom S5
526 styles, and a reference.odt for ODT exports. If no directory is
527 specified, $HOME/.pandoc will be searched. See pandoc's README for
528 more information.
529 '';
530 };
531
532 xssSanitize = mkOption {
533 type = types.bool;
534 default = true;
535 description = ''
536 If true, all HTML (including that produced by pandoc) is filtered
537 through xss-sanitize. Set to no only if you trust all of your users.
538 '';
539 };
540 };
541
542 configFile = pkgs.writeText "gitit.conf" ''
543 address: ${cfg.address}
544 port: ${toString cfg.port}
545 wiki-title: ${cfg.wikiTitle}
546 repository-type: ${cfg.repositoryType}
547 repository-path: ${cfg.repositoryPath}
548 require-authentication: ${cfg.requireAuthentication}
549 authentication-method: ${cfg.authenticationMethod}
550 user-file: ${cfg.userFile}
551 session-timeout: ${toString cfg.sessionTimeout}
552 static-dir: ${cfg.staticDir}
553 default-page-type: ${cfg.defaultPageType}
554 math: ${cfg.math}
555 mathjax-script: ${cfg.mathJaxScript}
556 show-lhs-bird-tracks: ${toYesNo cfg.showLhsBirdTracks}
557 templates-dir: ${cfg.templatesDir}
558 log-file: ${cfg.logFile}
559 log-level: ${cfg.logLevel}
560 front-page: ${cfg.frontPage}
561 no-delete: ${cfg.noDelete}
562 no-edit: ${cfg.noEdit}
563 default-summary: ${cfg.defaultSummary}
564 table-of-contents: ${toYesNo cfg.tableOfContents}
565 plugins: ${concatStringsSep "," cfg.plugins}
566 use-cache: ${toYesNo cfg.useCache}
567 cache-dir: ${cfg.cacheDir}
568 max-upload-size: ${cfg.maxUploadSize}
569 max-page-size: ${cfg.maxPageSize}
570 debug-mode: ${toYesNo cfg.debugMode}
571 compress-responses: ${toYesNo cfg.compressResponses}
572 mime-types-file: ${cfg.mimeTypesFile}
573 use-recaptcha: ${toYesNo cfg.useReCaptcha}
574 recaptcha-private-key: ${toString cfg.reCaptchaPrivateKey}
575 recaptcha-public-key: ${toString cfg.reCaptchaPublicKey}
576 access-question: ${cfg.accessQuestion}
577 access-question-answers: ${cfg.accessQuestionAnswers}
578 rpx-domain: ${toString cfg.rpxDomain}
579 rpx-key: ${toString cfg.rpxKey}
580 mail-command: ${cfg.mailCommand}
581 reset-password-message: ${cfg.resetPasswordMessage}
582 use-feed: ${toYesNo cfg.useFeed}
583 base-url: ${toString cfg.baseUrl}
584 absolute-urls: ${toYesNo cfg.absoluteUrls}
585 feed-days: ${toString cfg.feedDays}
586 feed-refresh-time: ${toString cfg.feedRefreshTime}
587 pdf-export: ${toYesNo cfg.pdfExport}
588 pandoc-user-data: ${toString cfg.pandocUserData}
589 xss-sanitize: ${toYesNo cfg.xssSanitize}
590 '';
591
592in
593
594{
595
596 options.services.gitit = gititOptions;
597
598 config = mkIf cfg.enable {
599
600 services.gitit = {
601 haskellPackages = mkDefault pkgs.haskellPackages;
602 staticDir = gititShared + "/data/static";
603 templatesDir = gititShared + "/data/templates";
604 plugins = [ ];
605 };
606
607 users.extraUsers.gitit = {
608 group = config.users.extraGroups.gitit.name;
609 description = "Gitit user";
610 home = homeDir;
611 createHome = true;
612 uid = config.ids.uids.gitit;
613 };
614
615 users.extraGroups.gitit.gid = config.ids.gids.gitit;
616
617 systemd.services.gitit = let
618 uid = toString config.ids.uids.gitit;
619 gid = toString config.ids.gids.gitit;
620 in {
621 description = "Git and Pandoc Powered Wiki";
622 after = [ "network.target" ];
623 wantedBy = [ "multi-user.target" ];
624 path = with pkgs; [ curl ]
625 ++ optional cfg.pdfExport texLiveFull
626 ++ optional (cfg.repositoryType == "darcs") darcs
627 ++ optional (cfg.repositoryType == "mercurial") mercurial
628 ++ optional (cfg.repositoryType == "git") git;
629
630 preStart = let
631 gm = "gitit@${config.networking.hostName}";
632 in
633 with cfg; ''
634 chown ${uid}:${gid} -R ${homeDir}
635 for dir in ${repositoryPath} ${staticDir} ${templatesDir} ${cacheDir}
636 do
637 if [ ! -d $dir ]
638 then
639 mkdir -p $dir
640 find $dir -type d -exec chmod 0750 {} +
641 find $dir -type f -exec chmod 0640 {} +
642 fi
643 done
644 cd ${repositoryPath}
645 ${
646 if repositoryType == "darcs" then
647 ''
648 if [ ! -d _darcs ]
649 then
650 ${pkgs.darcs}/bin/darcs initialize
651 echo "${gm}" > _darcs/prefs/email
652 ''
653 else if repositoryType == "mercurial" then
654 ''
655 if [ ! -d .hg ]
656 then
657 ${pkgs.mercurial}/bin/hg init
658 cat >> .hg/hgrc <<NAMED
659[ui]
660username = gitit ${gm}
661NAMED
662 ''
663 else
664 ''
665 if [ ! -d .git ]
666 then
667 ${pkgs.git}/bin/git init
668 ${pkgs.git}/bin/git config user.email "${gm}"
669 ${pkgs.git}/bin/git config user.name "gitit"
670 ''}
671 chown ${uid}:${gid} -R ${repositoryPath}
672 fi
673 cd -
674 '';
675
676 serviceConfig = {
677 User = config.users.extraUsers.gitit.name;
678 Group = config.users.extraGroups.gitit.name;
679 ExecStart = with cfg; gititSh haskellPackages extraPackages;
680 };
681 };
682 };
683}
684