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