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