reorganized code
[ikiwiki.git] / ikiwiki
1 #!/usr/bin/perl -T
2 $ENV{PATH}="/usr/local/bin:/usr/bin:/bin";
3
4 use warnings;
5 use strict;
6 use File::Find;
7 use Memoize;
8 use File::Spec;
9 use HTML::Template;
10 use Getopt::Long;
11 BEGIN {
12         $blosxom::version="is a proper perl module too much to ask?";
13         do "/usr/bin/markdown";
14 }
15
16 my (%links, %oldlinks, %oldpagemtime, %renderedfiles, %pagesources);
17
18 my %config=( #{{{
19         wiki_file_prune_regexp => qr{((^|/).svn/|\.\.|^\.|\/\.|\.html?$)},
20         wiki_link_regexp => qr/\[\[([^\s]+)\]\]/,
21         wiki_file_regexp => qr/(^[-A-Za-z0-9_.:\/+]+$)/,
22         verbose => 0,
23         wikiname => "wiki",
24         default_pageext => ".mdwn",
25         cgi => 0,
26         url => "",
27         cgiurl => "",
28         historyurl => "",
29         svn => 1,
30         anonok => 0,
31         rebuild => 0,
32         wrapper => 0,
33         srcdir => undef,
34         destdir => undef,
35         templatedir => undef,
36 ); #}}}
37
38 GetOptions( #{{{
39         "wikiname=s" => \$config{wikiname},
40         "verbose|v!" => \$config{verbose},
41         "rebuild!" => \$config{rebuild},
42         "wrapper!" => \$config{wrapper},
43         "svn!" => \$config{svn},
44         "anonok!" => \$config{anonok},
45         "cgi!" => \$config{cgi},
46         "url=s" => \$config{url},
47         "cgiurl=s" => \$config{cgiurl},
48         "historyurl=s" => \$config{historyurl},
49         "exclude=s@" => sub {
50                 $config{wiki_file_prune_regexp}=qr/$config{wiki_file_prune_regexp}|$_[1]/;
51         },
52 ) || usage();
53
54 usage() unless @ARGV == 3;
55 $config{srcdir} = possibly_foolish_untaint(shift);
56 $config{templatedir} = possibly_foolish_untaint(shift);
57 $config{destdir} = possibly_foolish_untaint(shift);
58 if ($config{cgi} && ! length $config{url}) {
59         error("Must specify url to wiki with --url when using --cgi");
60 } #}}}
61
62 sub usage { #{{{
63         die "usage: ikiwiki [options] source templates dest\n";
64 } #}}}
65
66 sub error { #{{{
67         if ($config{cgi}) {
68                 print "Content-type: text/html\n\n";
69                 print misctemplate("Error", "<p>Error: @_</p>");
70                 exit 1;
71         }
72         else {
73                 die @_;
74         }
75 } #}}}
76
77 sub debug ($) { #{{{
78         return unless $config{verbose};
79         if (! $config{cgi}) {
80                 print "@_\n";
81         }
82         else {
83                 print STDERR "@_\n";
84         }
85 } #}}}
86
87 sub mtime ($) { #{{{
88         my $page=shift;
89         
90         return (stat($page))[9];
91 } #}}}
92
93 sub possibly_foolish_untaint { #{{{
94         my $tainted=shift;
95         my ($untainted)=$tainted=~/(.*)/;
96         return $untainted;
97 } #}}}
98
99 sub basename ($) { #{{{
100         my $file=shift;
101
102         $file=~s!.*/!!;
103         return $file;
104 } #}}}
105
106 sub dirname ($) { #{{{
107         my $file=shift;
108
109         $file=~s!/?[^/]+$!!;
110         return $file;
111 } #}}}
112
113 sub pagetype ($) { #{{{
114         my $page=shift;
115         
116         if ($page =~ /\.mdwn$/) {
117                 return ".mdwn";
118         }
119         else {
120                 return "unknown";
121         }
122 } #}}}
123
124 sub pagename ($) { #{{{
125         my $file=shift;
126
127         my $type=pagetype($file);
128         my $page=$file;
129         $page=~s/\Q$type\E*$// unless $type eq 'unknown';
130         return $page;
131 } #}}}
132
133 sub htmlpage ($) { #{{{
134         my $page=shift;
135
136         return $page.".html";
137 } #}}}
138
139 sub readfile ($) { #{{{
140         my $file=shift;
141
142         local $/=undef;
143         open (IN, "$file") || error("failed to read $file: $!");
144         my $ret=<IN>;
145         close IN;
146         return $ret;
147 } #}}}
148
149 sub writefile ($$) { #{{{
150         my $file=shift;
151         my $content=shift;
152
153         my $dir=dirname($file);
154         if (! -d $dir) {
155                 my $d="";
156                 foreach my $s (split(m!/+!, $dir)) {
157                         $d.="$s/";
158                         if (! -d $d) {
159                                 mkdir($d) || error("failed to create directory $d: $!");
160                         }
161                 }
162         }
163         
164         open (OUT, ">$file") || error("failed to write $file: $!");
165         print OUT $content;
166         close OUT;
167 } #}}}
168
169 sub findlinks ($) { #{{{
170         my $content=shift;
171
172         my @links;
173         while ($content =~ /$config{wiki_link_regexp}/g) {
174                 push @links, lc($1);
175         }
176         return @links;
177 } #}}}
178
179 sub bestlink ($$) { #{{{
180         # Given a page and the text of a link on the page, determine which
181         # existing page that link best points to. Prefers pages under a
182         # subdirectory with the same name as the source page, failing that
183         # goes down the directory tree to the base looking for matching
184         # pages.
185         my $page=shift;
186         my $link=lc(shift);
187         
188         my $cwd=$page;
189         do {
190                 my $l=$cwd;
191                 $l.="/" if length $l;
192                 $l.=$link;
193
194                 if (exists $links{$l}) {
195                         #debug("for $page, \"$link\", use $l");
196                         return $l;
197                 }
198         } while $cwd=~s!/?[^/]+$!!;
199
200         #print STDERR "warning: page $page, broken link: $link\n";
201         return "";
202 } #}}}
203
204 sub isinlinableimage ($) { #{{{
205         my $file=shift;
206         
207         $file=~/\.(png|gif|jpg|jpeg)$/;
208 } #}}}
209
210 sub htmllink { #{{{
211         my $page=shift;
212         my $link=shift;
213         my $noimageinline=shift; # don't turn links into inline html images
214         my $createsubpage=shift; # force creation of a subpage if page DNE
215
216         my $bestlink=bestlink($page, $link);
217
218         return $link if length $bestlink && $page eq $bestlink;
219         
220         # TODO BUG: %renderedfiles may not have it, if the linked to page
221         # was also added and isn't yet rendered! Note that this bug is
222         # masked by the bug mentioned below that makes all new files
223         # be rendered twice.
224         if (! grep { $_ eq $bestlink } values %renderedfiles) {
225                 $bestlink=htmlpage($bestlink);
226         }
227         if (! grep { $_ eq $bestlink } values %renderedfiles) {
228                 if (! $createsubpage) {
229                         return "<a href=\"$config{cgiurl}?do=create&page=$link&from=$page\">?</a>$link"
230                 }
231                 else {
232                         return "<a href=\"$config{cgiurl}?do=create&page=$page/$link\">?</a>$link"
233                 }
234         }
235         
236         $bestlink=File::Spec->abs2rel($bestlink, dirname($page));
237         
238         if (! $noimageinline && isinlinableimage($bestlink)) {
239                 return "<img src=\"$bestlink\">";
240         }
241         return "<a href=\"$bestlink\">$link</a>";
242 } #}}}
243
244 sub linkify ($$) { #{{{
245         my $content=shift;
246         my $file=shift;
247
248         $content =~ s/$config{wiki_link_regexp}/htmllink(pagename($file), $1)/eg;
249         
250         return $content;
251 } #}}}
252
253 sub htmlize ($$) { #{{{
254         my $type=shift;
255         my $content=shift;
256         
257         if ($type eq '.mdwn') {
258                 return Markdown::Markdown($content);
259         }
260         else {
261                 error("htmlization of $type not supported");
262         }
263 } #}}}
264
265 sub backlinks ($) { #{{{
266         my $page=shift;
267
268         my @links;
269         foreach my $p (keys %links) {
270                 next if bestlink($page, $p) eq $page;
271                 if (grep { length $_ && bestlink($p, $_) eq $page } @{$links{$p}}) {
272                         my $href=File::Spec->abs2rel(htmlpage($p), dirname($page));
273                         
274                         # Trim common dir prefixes from both pages.
275                         my $p_trimmed=$p;
276                         my $page_trimmed=$page;
277                         my $dir;
278                         1 while (($dir)=$page_trimmed=~m!^([^/]+/)!) &&
279                                 defined $dir &&
280                                 $p_trimmed=~s/^\Q$dir\E// &&
281                                 $page_trimmed=~s/^\Q$dir\E//;
282                                        
283                         push @links, { url => $href, page => $p_trimmed };
284                 }
285         }
286
287         return sort { $a->{page} cmp $b->{page} } @links;
288 } #}}}
289         
290 sub parentlinks ($) { #{{{
291         my $page=shift;
292         
293         my @ret;
294         my $pagelink="";
295         my $path="";
296         my $skip=1;
297         foreach my $dir (reverse split("/", $page)) {
298                 if (! $skip) {
299                         unshift @ret, { url => "$path$dir.html", page => $dir };
300                 }
301                 else {
302                         $skip=0;
303                 }
304                 $path.="../";
305         }
306         unshift @ret, { url => $path , page => $config{wikiname} };
307         return @ret;
308 } #}}}
309
310 sub indexlink () { #{{{
311         return "<a href=\"$config{url}\">$config{wikiname}</a>";
312 } #}}}
313
314 sub finalize ($$) { #{{{
315         my $content=shift;
316         my $page=shift;
317
318         my $title=basename($page);
319         $title=~s/_/ /g;
320         
321         my $template=HTML::Template->new(blind_cache => 1,
322                 filename => "$config{templatedir}/page.tmpl");
323         
324         if (length $config{cgiurl}) {
325                 $template->param(editurl => "$config{cgiurl}?do=edit&page=$page");
326                 if ($config{svn}) {
327                         $template->param(recentchangesurl => "$config{cgiurl}?do=recentchanges");
328                 }
329         }
330
331         if (length $config{historyurl}) {
332                 my $u=$config{historyurl};
333                 $u=~s/\[\[\]\]/$pagesources{$page}/g;
334                 $template->param(historyurl => $u);
335         }
336         
337         $template->param(
338                 title => $title,
339                 wikiname => $config{wikiname},
340                 parentlinks => [parentlinks($page)],
341                 content => $content,
342                 backlinks => [backlinks($page)],
343                 discussionlink => htmllink($page, "Discussion", 1, 1),
344         );
345         
346         return $template->output;
347 } #}}}
348
349 sub check_overwrite ($$) { #{{{
350         # Important security check. Make sure to call this before saving
351         # any files to the source directory.
352         my $dest=shift;
353         my $src=shift;
354         
355         if (! exists $renderedfiles{$src} && -e $dest && ! $config{rebuild}) {
356                 error("$dest exists and was rendered from ".
357                         join(" ",(grep { $renderedfiles{$_} eq $dest } keys
358                                 %renderedfiles)).
359                         ", not from $src before not overwriting");
360         }
361 } #}}}
362                 
363 sub render ($) { #{{{
364         my $file=shift;
365         
366         my $type=pagetype($file);
367         my $content=readfile("$config{srcdir}/$file");
368         if ($type ne 'unknown') {
369                 my $page=pagename($file);
370                 
371                 $links{$page}=[findlinks($content)];
372                 
373                 $content=linkify($content, $file);
374                 $content=htmlize($type, $content);
375                 $content=finalize($content, $page);
376                 
377                 check_overwrite("$config{destdir}/".htmlpage($page), $page);
378                 writefile("$config{destdir}/".htmlpage($page), $content);
379                 $oldpagemtime{$page}=time;
380                 $renderedfiles{$page}=htmlpage($page);
381         }
382         else {
383                 $links{$file}=[];
384                 check_overwrite("$config{destdir}/$file", $file);
385                 writefile("$config{destdir}/$file", $content);
386                 $oldpagemtime{$file}=time;
387                 $renderedfiles{$file}=$file;
388         }
389 } #}}}
390
391 sub loadindex () { #{{{
392         open (IN, "$config{srcdir}/.ikiwiki/index") || return;
393         while (<IN>) {
394                 $_=possibly_foolish_untaint($_);
395                 chomp;
396                 my ($mtime, $file, $rendered, @links)=split(' ', $_);
397                 my $page=pagename($file);
398                 $pagesources{$page}=$file;
399                 $oldpagemtime{$page}=$mtime;
400                 $oldlinks{$page}=[@links];
401                 $links{$page}=[@links];
402                 $renderedfiles{$page}=$rendered;
403         }
404         close IN;
405 } #}}}
406
407 sub saveindex () { #{{{
408         if (! -d "$config{srcdir}/.ikiwiki") {
409                 mkdir("$config{srcdir}/.ikiwiki");
410         }
411         open (OUT, ">$config{srcdir}/.ikiwiki/index") || error("cannot write to index: $!");
412         foreach my $page (keys %oldpagemtime) {
413                 print OUT "$oldpagemtime{$page} $pagesources{$page} $renderedfiles{$page} ".
414                         join(" ", @{$links{$page}})."\n"
415                                 if $oldpagemtime{$page};
416         }
417         close OUT;
418 } #}}}
419
420 sub rcs_update () { #{{{
421         if (-d "$config{srcdir}/.svn") {
422                 if (system("svn", "update", "--quiet", $config{srcdir}) != 0) {
423                         warn("svn update failed\n");
424                 }
425         }
426 } #}}}
427
428 sub rcs_commit ($) { #{{{
429         my $message=shift;
430
431         if (-d "$config{srcdir}/.svn") {
432                 if (system("svn", "commit", "--quiet", "-m",
433                            possibly_foolish_untaint($message),
434                            $config{srcdir}) != 0) {
435                         warn("svn commit failed\n");
436                 }
437         }
438 } #}}}
439
440 sub rcs_add ($) { #{{{
441         my $file=shift;
442
443         if (-d "$config{srcdir}/.svn") {
444                 my $parent=dirname($file);
445                 while (! -d "$config{srcdir}/$parent/.svn") {
446                         $file=$parent;
447                         $parent=dirname($file);
448                 }
449                 
450                 if (system("svn", "add", "--quiet", "$config{srcdir}/$file") != 0) {
451                         warn("svn add failed\n");
452                 }
453         }
454 } #}}}
455
456 sub rcs_recentchanges ($) { #{{{
457         my $num=shift;
458         my @ret;
459         
460         eval q{use Date::Parse};
461         eval q{use Time::Duration};
462         
463         if (-d "$config{srcdir}/.svn") {
464                 my $info=`LANG=C svn info $config{srcdir}`;
465                 my ($svn_url)=$info=~/^URL: (.*)$/m;
466
467                 # FIXME: currently assumes that the wiki is somewhere
468                 # under trunk in svn, doesn't support other layouts.
469                 my ($svn_base)=$svn_url=~m!(/trunk(?:/.*)?)$!;
470                 
471                 my $div=qr/^--------------------+$/;
472                 my $infoline=qr/^r(\d+)\s+\|\s+([^\s]+)\s+\|\s+(\d+-\d+-\d+\s+\d+:\d+:\d+\s+[-+]?\d+).*/;
473                 my $state='start';
474                 my ($rev, $user, $when, @pages, @message);
475                 foreach (`LANG=C svn log -v '$svn_url'`) {
476                         chomp;
477                         if ($state eq 'start' && /$div/) {
478                                 $state='header';
479                         }
480                         elsif ($state eq 'header' && /$infoline/) {
481                                 $rev=$1;
482                                 $user=$2;
483                                 $when=concise(ago(time - str2time($3)));
484                         }
485                         elsif ($state eq 'header' && /^\s+[A-Z]\s+\Q$svn_base\E\/(.+)$/) {
486                                 push @pages, { link => htmllink("", pagename($1), 1) }
487                                         if length $1;
488                         }
489                         elsif ($state eq 'header' && /^$/) {
490                                 $state='body';
491                         }
492                         elsif ($state eq 'body' && /$div/) {
493                                 my $committype="web";
494                                 if (defined $message[0] &&
495                                     $message[0]->{line}=~/^web commit by (\w+):?(.*)/) {
496                                         $user="$1";
497                                         $message[0]->{line}=$2;
498                                 }
499                                 else {
500                                         $committype="svn";
501                                 }
502                                 
503                                 push @ret, { rev => $rev,
504                                         user => htmllink("", $user, 1),
505                                         committype => $committype,
506                                         when => $when, message => [@message],
507                                         pages => [@pages] } if @pages;
508                                 return @ret if @ret >= $num;
509                                 
510                                 $state='header';
511                                 $rev=$user=$when=undef;
512                                 @pages=@message=();
513                         }
514                         elsif ($state eq 'body') {
515                                 push @message, {line => $_},
516                         }
517                 }
518         }
519
520         return @ret;
521 } #}}}
522
523 sub prune ($) { #{{{
524         my $file=shift;
525
526         unlink($file);
527         my $dir=dirname($file);
528         while (rmdir($dir)) {
529                 $dir=dirname($dir);
530         }
531 } #}}}
532
533 sub refresh () { #{{{
534         # Find existing pages.
535         my %exists;
536         my @files;
537         find({
538                 no_chdir => 1,
539                 wanted => sub {
540                         if (/$config{wiki_file_prune_regexp}/) {
541                                 $File::Find::prune=1;
542                         }
543                         elsif (! -d $_) {
544                                 my ($f)=/$config{wiki_file_regexp}/; # untaint
545                                 if (! defined $f) {
546                                         warn("skipping bad filename $_\n");
547                                 }
548                                 else {
549                                         $f=~s/^\Q$config{srcdir}\E\/?//;
550                                         push @files, $f;
551                                         $exists{pagename($f)}=1;
552                                 }
553                         }
554                 },
555         }, $config{srcdir});
556
557         my %rendered;
558
559         # check for added or removed pages
560         my @add;
561         foreach my $file (@files) {
562                 my $page=pagename($file);
563                 if (! $oldpagemtime{$page}) {
564                         debug("new page $page");
565                         push @add, $file;
566                         $links{$page}=[];
567                         $pagesources{$page}=$file;
568                 }
569         }
570         my @del;
571         foreach my $page (keys %oldpagemtime) {
572                 if (! $exists{$page}) {
573                         debug("removing old page $page");
574                         push @del, $renderedfiles{$page};
575                         prune($config{destdir}."/".$renderedfiles{$page});
576                         delete $renderedfiles{$page};
577                         $oldpagemtime{$page}=0;
578                         delete $pagesources{$page};
579                 }
580         }
581         
582         # render any updated files
583         foreach my $file (@files) {
584                 my $page=pagename($file);
585                 
586                 if (! exists $oldpagemtime{$page} ||
587                     mtime("$config{srcdir}/$file") > $oldpagemtime{$page}) {
588                         debug("rendering changed file $file");
589                         render($file);
590                         $rendered{$file}=1;
591                 }
592         }
593         
594         # if any files were added or removed, check to see if each page
595         # needs an update due to linking to them
596         # TODO: inefficient; pages may get rendered above and again here;
597         # problem is the bestlink may have changed and we won't know until
598         # now
599         if (@add || @del) {
600 FILE:           foreach my $file (@files) {
601                         my $page=pagename($file);
602                         foreach my $f (@add, @del) {
603                                 my $p=pagename($f);
604                                 foreach my $link (@{$links{$page}}) {
605                                         if (bestlink($page, $link) eq $p) {
606                                                 debug("rendering $file, which links to $p");
607                                                 render($file);
608                                                 $rendered{$file}=1;
609                                                 next FILE;
610                                         }
611                                 }
612                         }
613                 }
614         }
615
616         # handle backlinks; if a page has added/removed links, update the
617         # pages it links to
618         # TODO: inefficient; pages may get rendered above and again here;
619         # problem is the backlinks could be wrong in the first pass render
620         # above
621         if (%rendered) {
622                 my %linkchanged;
623                 foreach my $file (keys %rendered, @del) {
624                         my $page=pagename($file);
625                         if (exists $links{$page}) {
626                                 foreach my $link (@{$links{$page}}) {
627                                         $link=bestlink($page, $link);
628                                         if (length $link &&
629                                             ! exists $oldlinks{$page} ||
630                                             ! grep { $_ eq $link } @{$oldlinks{$page}}) {
631                                                 $linkchanged{$link}=1;
632                                         }
633                                 }
634                         }
635                         if (exists $oldlinks{$page}) {
636                                 foreach my $link (@{$oldlinks{$page}}) {
637                                         $link=bestlink($page, $link);
638                                         if (length $link &&
639                                             ! exists $links{$page} ||
640                                             ! grep { $_ eq $link } @{$links{$page}}) {
641                                                 $linkchanged{$link}=1;
642                                         }
643                                 }
644                         }
645                 }
646                 foreach my $link (keys %linkchanged) {
647                         my $linkfile=$pagesources{$link};
648                         if (defined $linkfile) {
649                                 debug("rendering $linkfile, to update its backlinks");
650                                 render($linkfile);
651                         }
652                 }
653         }
654 } #}}}
655
656 sub gen_wrapper () { #{{{
657         eval q{use Cwd 'abs_path'};
658         $config{srcdir}=abs_path($config{srcdir});
659         $config{destdir}=abs_path($config{destdir});
660         my $this=abs_path($0);
661         if (! -x $this) {
662                 error("$this doesn't seem to be executable");
663         }
664
665         my @params=($config{srcdir}, $config{templatedir}, $config{destdir},
666                 "--wikiname=$config{wikiname}");
667         push @params, "--verbose" if $config{verbose};
668         push @params, "--rebuild" if $config{rebuild};
669         push @params, "--nosvn" if !$config{svn};
670         push @params, "--cgi" if $config{cgi};
671         push @params, "--url=$config{url}" if length $config{url};
672         push @params, "--cgiurl=$config{cgiurl}" if length $config{cgiurl};
673         push @params, "--historyurl=$config{historyurl}" if length $config{historyurl};
674         push @params, "--anonok" if $config{anonok};
675         my $params=join(" ", map { "\'$_\'" } @params);
676         my $call='';
677         foreach my $p ($this, $this, @params) {
678                 $call.=qq{"$p", };
679         }
680         $call.="NULL";
681         
682         my @envsave;
683         push @envsave, qw{REMOTE_ADDR QUERY_STRING REQUEST_METHOD REQUEST_URI
684                        CONTENT_TYPE CONTENT_LENGTH GATEWAY_INTERFACE
685                        HTTP_COOKIE} if $config{cgi};
686         my $envsave="";
687         foreach my $var (@envsave) {
688                 $envsave.=<<"EOF"
689         if ((s=getenv("$var")))
690                 asprintf(&newenviron[i++], "%s=%s", "$var", s);
691 EOF
692         }
693         
694         open(OUT, ">ikiwiki-wrap.c") || error("failed to write ikiwiki-wrap.c: $!");;
695         print OUT <<"EOF";
696 /* A wrapper for ikiwiki, can be safely made suid. */
697 #define _GNU_SOURCE
698 #include <stdio.h>
699 #include <unistd.h>
700 #include <stdlib.h>
701 #include <string.h>
702
703 extern char **environ;
704
705 int main (int argc, char **argv) {
706         /* Sanitize environment. */
707         char *s;
708         char *newenviron[$#envsave+3];
709         int i=0;
710 $envsave
711         newenviron[i++]="HOME=$ENV{HOME}";
712         newenviron[i]=NULL;
713         environ=newenviron;
714
715         if (argc == 2 && strcmp(argv[1], "--params") == 0) {
716                 printf("$params\\n");
717                 exit(0);
718         }
719         
720         execl($call);
721         perror("failed to run $this");
722         exit(1);
723 }
724 EOF
725         close OUT;
726         if (system("gcc", "ikiwiki-wrap.c", "-o", "ikiwiki-wrap") != 0) {
727                 error("failed to compile ikiwiki-wrap.c");
728         }
729         unlink("ikiwiki-wrap.c");
730         print "successfully generated ikiwiki-wrap\n";
731         exit 0;
732 } #}}}
733                 
734 sub misctemplate ($$) { #{{{
735         my $title=shift;
736         my $pagebody=shift;
737         
738         my $template=HTML::Template->new(
739                 filename => "$config{templatedir}/misc.tmpl"
740         );
741         $template->param(
742                 title => $title,
743                 indexlink => indexlink(),
744                 wikiname => $config{wikiname},
745                 pagebody => $pagebody,
746         );
747         return $template->output;
748 }#}}}
749
750 sub cgi_recentchanges ($) { #{{{
751         my $q=shift;
752         
753         my $template=HTML::Template->new(
754                 filename => "$config{templatedir}/recentchanges.tmpl"
755         );
756         $template->param(
757                 title => "RecentChanges",
758                 indexlink => indexlink(),
759                 wikiname => $config{wikiname},
760                 changelog => [rcs_recentchanges(100)],
761         );
762         print $q->header, $template->output;
763 } #}}}
764
765 sub userinfo_get ($$) { #{{{
766         my $user=shift;
767         my $field=shift;
768
769         eval q{use Storable};
770         my $userdata=eval{ Storable::lock_retrieve("$config{srcdir}/.ikiwiki/userdb") };
771         if (! defined $userdata || ! ref $userdata || 
772             ! exists $userdata->{$user} || ! ref $userdata->{$user}) {
773                 return "";
774         }
775         return $userdata->{$user}->{$field};
776 } #}}}
777
778 sub userinfo_set ($$) { #{{{
779         my $user=shift;
780         my $info=shift;
781         
782         eval q{use Storable};
783         my $userdata=eval{ Storable::lock_retrieve("$config{srcdir}/.ikiwiki/userdb") };
784         if (! defined $userdata || ! ref $userdata) {
785                 $userdata={};
786         }
787         $userdata->{$user}=$info;
788         my $oldmask=umask(077);
789         my $ret=Storable::lock_store($userdata, "$config{srcdir}/.ikiwiki/userdb");
790         umask($oldmask);
791         return $ret;
792 } #}}}
793
794 sub cgi_signin ($$) { #{{{
795         my $q=shift;
796         my $session=shift;
797
798         eval q{use CGI::FormBuilder};
799         my $form = CGI::FormBuilder->new(
800                 title => "$config{wikiname} signin",
801                 fields => [qw(do page from name password confirm_password email)],
802                 header => 1,
803                 method => 'POST',
804                 validate => {
805                         confirm_password => {
806                                 perl => q{eq $form->field("password")},
807                         },
808                         email => 'EMAIL',
809                 },
810                 required => 'NONE',
811                 javascript => 0,
812                 params => $q,
813                 action => $q->request_uri,
814                 header => 0,
815                 template => (-e "$config{templatedir}/signin.tmpl" ?
816                               "$config{templatedir}/signin.tmpl" : "")
817         );
818         
819         $form->field(name => "name", required => 0);
820         $form->field(name => "do", type => "hidden");
821         $form->field(name => "page", type => "hidden");
822         $form->field(name => "from", type => "hidden");
823         $form->field(name => "password", type => "password", required => 0);
824         $form->field(name => "confirm_password", type => "password", required => 0);
825         $form->field(name => "email", required => 0);
826         if ($q->param("do") ne "signin") {
827                 $form->text("You need to log in before you can edit pages.");
828         }
829         
830         if ($form->submitted) {
831                 # Set required fields based on how form was submitted.
832                 my %required=(
833                         "Login" => [qw(name password)],
834                         "Register" => [qw(name password confirm_password email)],
835                         "Mail Password" => [qw(name)],
836                 );
837                 foreach my $opt (@{$required{$form->submitted}}) {
838                         $form->field(name => $opt, required => 1);
839                 }
840         
841                 # Validate password differently depending on how
842                 # form was submitted.
843                 if ($form->submitted eq 'Login') {
844                         $form->field(
845                                 name => "password",
846                                 validate => sub {
847                                         length $form->field("name") &&
848                                         shift eq userinfo_get($form->field("name"), 'password');
849                                 },
850                         );
851                         $form->field(name => "name", validate => '/^\w+$/');
852                 }
853                 else {
854                         $form->field(name => "password", validate => 'VALUE');
855                 }
856                 # And make sure the entered name exists when logging
857                 # in or sending email, and does not when registering.
858                 if ($form->submitted eq 'Register') {
859                         $form->field(
860                                 name => "name",
861                                 validate => sub {
862                                         my $name=shift;
863                                         length $name &&
864                                         ! userinfo_get($name, "regdate");
865                                 },
866                         );
867                 }
868                 else {
869                         $form->field(
870                                 name => "name",
871                                 validate => sub {
872                                         my $name=shift;
873                                         length $name &&
874                                         userinfo_get($name, "regdate");
875                                 },
876                         );
877                 }
878         }
879         else {
880                 # First time settings.
881                 $form->field(name => "name", comment => "use FirstnameLastName");
882                 $form->field(name => "confirm_password", comment => "(only needed");
883                 $form->field(name => "email",            comment => "for registration)");
884                 if ($session->param("name")) {
885                         $form->field(name => "name", value => $session->param("name"));
886                 }
887         }
888
889         if ($form->submitted && $form->validate) {
890                 if ($form->submitted eq 'Login') {
891                         $session->param("name", $form->field("name"));
892                         if (defined $form->field("do") && 
893                             $form->field("do") ne 'signin') {
894                                 print $q->redirect(
895                                         "$config{cgiurl}?do=".$form->field("do").
896                                         "&page=".$form->field("page").
897                                         "&from=".$form->field("from"));;
898                         }
899                         else {
900                                 print $q->redirect($config{url});
901                         }
902                 }
903                 elsif ($form->submitted eq 'Register') {
904                         my $user_name=$form->field('name');
905                         if (userinfo_set($user_name, {
906                                            'email' => $form->field('email'),
907                                            'password' => $form->field('password'),
908                                            'regdate' => time
909                                          })) {
910                                 $form->field(name => "confirm_password", type => "hidden");
911                                 $form->field(name => "email", type => "hidden");
912                                 $form->text("Registration successful. Now you can Login.");
913                                 print $session->header();
914                                 print misctemplate($form->title, $form->render(submit => ["Login"]));
915                         }
916                         else {
917                                 error("Error saving registration.");
918                         }
919                 }
920                 elsif ($form->submitted eq 'Mail Password') {
921                         my $user_name=$form->field("name");
922                         my $template=HTML::Template->new(
923                                 filename => "$config{templatedir}/passwordmail.tmpl"
924                         );
925                         $template->param(
926                                 user_name => $user_name,
927                                 user_password => userinfo_get($user_name, "password"),
928                                 wikiurl => $config{url},
929                                 wikiname => $config{wikiname},
930                                 REMOTE_ADDR => $ENV{REMOTE_ADDR},
931                         );
932                         
933                         eval q{use Mail::Sendmail};
934                         my ($fromhost) = $config{cgiurl} =~ m!/([^/]+)!;
935                         sendmail(
936                                 To => userinfo_get($user_name, "email"),
937                                 From => "$config{wikiname} admin <".(getpwuid($>))[0]."@".$fromhost.">",
938                                 Subject => "$config{wikiname} information",
939                                 Message => $template->output,
940                         ) or error("Failed to send mail");
941                         
942                         $form->text("Your password has been emailed to you.");
943                         $form->field(name => "name", required => 0);
944                         print $session->header();
945                         print misctemplate($form->title, $form->render(submit => ["Login", "Register", "Mail Password"]));
946                 }
947         }
948         else {
949                 print $session->header();
950                 print misctemplate($form->title, $form->render(submit => ["Login", "Register", "Mail Password"]));
951         }
952 } #}}}
953
954 sub cgi_editpage ($$) { #{{{
955         my $q=shift;
956         my $session=shift;
957
958         eval q{use CGI::FormBuilder};
959         my $form = CGI::FormBuilder->new(
960                 fields => [qw(do from page content comments)],
961                 header => 1,
962                 method => 'POST',
963                 validate => {
964                         content => '/.+/',
965                 },
966                 required => [qw{content}],
967                 javascript => 0,
968                 params => $q,
969                 action => $q->request_uri,
970                 table => 0,
971                 template => "$config{templatedir}/editpage.tmpl"
972         );
973         
974         my ($page)=$form->param('page')=~/$config{wiki_file_regexp}/;
975         if (! defined $page || ! length $page || $page ne $q->param('page') ||
976             $page=~/$config{wiki_file_prune_regexp}/ || $page=~/^\//) {
977                 error("bad page name");
978         }
979         $page=lc($page);
980
981         $form->field(name => "do", type => 'hidden');
982         $form->field(name => "from", type => 'hidden');
983         $form->field(name => "page", value => "$page", force => 1);
984         $form->field(name => "comments", type => "text", size => 80);
985         $form->field(name => "content", type => "textarea", rows => 20,
986                 cols => 80);
987         
988         if ($form->submitted eq "Cancel") {
989                 print $q->redirect("$config{url}/".htmlpage($page));
990                 return;
991         }
992         if (! $form->submitted || ! $form->validate) {
993                 if ($form->field("do") eq "create") {
994                         if (exists $pagesources{lc($page)}) {
995                                 # hmm, someone else made the page in the
996                                 # meantime?
997                                 print $q->redirect("$config{url}/".htmlpage($page));
998                                 return;
999                         }
1000                         
1001                         my @page_locs;
1002                         my ($from)=$form->param('from')=~/$config{wiki_file_regexp}/;
1003                         if (! defined $from || ! length $from ||
1004                             $from ne $form->param('from') ||
1005                             $from=~/$config{wiki_file_prune_regexp}/ || $from=~/^\//) {
1006                                 @page_locs=$page;
1007                         }
1008                         else {
1009                                 my $dir=$from."/";
1010                                 $dir=~s![^/]+/$!!;
1011                                 push @page_locs, $dir.$page;
1012                                 push @page_locs, "$from/$page";
1013                                 while (length $dir) {
1014                                         $dir=~s![^/]+/$!!;
1015                                         push @page_locs, $dir.$page;
1016                                 }
1017                         }
1018
1019                         $form->tmpl_param("page_select", 1);
1020                         $form->field(name => "page", type => 'select',
1021                                 options => \@page_locs);
1022                         $form->title("creating $page");
1023                 }
1024                 elsif ($form->field("do") eq "edit") {
1025                         my $content="";
1026                         if (exists $pagesources{lc($page)}) {
1027                                 $content=readfile("$config{srcdir}/$pagesources{lc($page)}");
1028                                 $content=~s/\n/\r\n/g;
1029                         }
1030                         $form->tmpl_param("page_select", 0);
1031                         $form->field(name => "content", value => $content,
1032                                 force => 1);
1033                         $form->field(name => "page", type => 'hidden');
1034                         $form->title("editing $page");
1035                 }
1036                 
1037                 $form->tmpl_param("can_commit", $config{svn});
1038                 $form->tmpl_param("indexlink", indexlink());
1039                 print $form->render(submit => ["Save Page", "Cancel"]);
1040         }
1041         else {
1042                 # save page
1043                 my $file=$page.$config{default_pageext};
1044                 my $newfile=1;
1045                 if (exists $pagesources{lc($page)}) {
1046                         $file=$pagesources{lc($page)};
1047                         $newfile=0;
1048                 }
1049                 
1050                 my $content=$form->field('content');
1051                 $content=~s/\r\n/\n/g;
1052                 $content=~s/\r/\n/g;
1053                 writefile("$config{srcdir}/$file", $content);
1054                 
1055                 my $message="web commit ";
1056                 if ($session->param("name")) {
1057                         $message.="by ".$session->param("name");
1058                 }
1059                 else {
1060                         $message.="from $ENV{REMOTE_ADDR}";
1061                 }
1062                 if (defined $form->field('comments') &&
1063                     length $form->field('comments')) {
1064                         $message.=": ".$form->field('comments');
1065                 }
1066                 
1067                 if ($config{svn}) {
1068                         if ($newfile) {
1069                                 rcs_add($file);
1070                         }
1071                         # presumably the commit will trigger an update
1072                         # of the wiki
1073                         rcs_commit($message);
1074                 }
1075                 else {
1076                         refresh();
1077                 }
1078                 
1079                 # The trailing question mark tries to avoid broken
1080                 # caches and get the most recent version of the page.
1081                 print $q->redirect("$config{url}/".htmlpage($page)."?updated");
1082         }
1083 } #}}}
1084
1085 sub cgi () { #{{{
1086         eval q{use CGI};
1087         eval q{use CGI::Session};
1088         
1089         my $q=CGI->new;
1090         
1091         my $do=$q->param('do');
1092         if (! defined $do || ! length $do) {
1093                 error("\"do\" parameter missing");
1094         }
1095         
1096         # This does not need a session.
1097         if ($do eq 'recentchanges') {
1098                 cgi_recentchanges($q);
1099                 return;
1100         }
1101         
1102         CGI::Session->name("ikiwiki_session");
1103
1104         my $oldmask=umask(077);
1105         my $session = CGI::Session->new("driver:db_file", $q,
1106                 { FileName => "$config{srcdir}/.ikiwiki/sessions.db" });
1107         umask($oldmask);
1108         
1109         # Everything below this point needs the user to be signed in.
1110         if ((! $config{anonok} && ! defined $session->param("name") ||
1111                 ! userinfo_get($session->param("name"), "regdate")) || $do eq 'signin') {
1112                 cgi_signin($q, $session);
1113         
1114                 # Force session flush with safe umask.
1115                 my $oldmask=umask(077);
1116                 $session->flush;
1117                 umask($oldmask);
1118                 
1119                 return;
1120         }
1121         
1122         if ($do eq 'create' || $do eq 'edit') {
1123                 cgi_editpage($q, $session);
1124         }
1125         else {
1126                 error("unknown do parameter");
1127         }
1128 } #}}}
1129
1130 # main {{{
1131 gen_wrapper() if $config{wrapper};
1132 memoize('pagename');
1133 memoize('bestlink');
1134 loadindex() unless $config{rebuild};
1135 if ($config{cgi}) {
1136         cgi();
1137 }
1138 else {
1139         rcs_update() if $config{svn};
1140         refresh();
1141         saveindex();
1142 }
1143 #}}}