remove accidental makemaker cruft
[ikiwiki] / ikiwiki
1 #!/usr/bin/perl -T
2
3 $ENV{PATH}="/usr/local/bin:/usr/bin:/bin";
4
5 use warnings;
6 use strict;
7 use Memoize;
8 use File::Spec;
9 use HTML::Template;
10 use Getopt::Long;
11
12 my (%links, %oldlinks, %oldpagemtime, %renderedfiles, %pagesources);
13
14 # Holds global config settings, also used by some modules.
15 our %config=( #{{{
16         wiki_file_prune_regexp => qr{((^|/).svn/|\.\.|^\.|\/\.|\.html?$)},
17         wiki_link_regexp => qr/\[\[([^\s\]]+)\]\]/,
18         wiki_file_regexp => qr/(^[-A-Za-z0-9_.:\/+]+$)/,
19         verbose => 0,
20         wikiname => "wiki",
21         default_pageext => ".mdwn",
22         cgi => 0,
23         svn => 1,
24         url => '',
25         cgiurl => '',
26         historyurl => '',
27         diffurl => '',
28         anonok => 0,
29         rebuild => 0,
30         wrapper => undef,
31         wrappermode => undef,
32         srcdir => undef,
33         destdir => undef,
34         templatedir => "/usr/share/ikiwiki/templates",
35         setup => undef,
36         adminuser => undef,
37 ); #}}}
38
39 GetOptions( #{{{
40         "setup|s=s" => \$config{setup},
41         "wikiname=s" => \$config{wikiname},
42         "verbose|v!" => \$config{verbose},
43         "rebuild!" => \$config{rebuild},
44         "wrapper=s" => sub { $config{wrapper}=$_[1] ? $_[1] : "ikiwiki-wrap" },
45         "wrappermode=i" => \$config{wrappermode},
46         "svn!" => \$config{svn},
47         "anonok!" => \$config{anonok},
48         "cgi!" => \$config{cgi},
49         "url=s" => \$config{url},
50         "cgiurl=s" => \$config{cgiurl},
51         "historyurl=s" => \$config{historyurl},
52         "diffurl=s" => \$config{diffurl},
53         "exclude=s@" => sub {
54                 $config{wiki_file_prune_regexp}=qr/$config{wiki_file_prune_regexp}|$_[1]/;
55         },
56         "adminuser=s@" => sub { push @{$config{adminuser}}, $_[1] },
57         "templatedir=s" => sub { $config{templatedir}=possibly_foolish_untaint($_[1]) },
58 ) || usage();
59
60 if (! $config{setup}) {
61         usage() unless @ARGV == 2;
62         $config{srcdir} = possibly_foolish_untaint(shift);
63         $config{destdir} = possibly_foolish_untaint(shift);
64         checkoptions();
65 }
66 #}}}
67
68 sub checkoptions { #{{{
69         if ($config{cgi} && ! length $config{url}) {
70                 error("Must specify url to wiki with --url when using --cgi");
71         }
72         $config{wikistatedir}="$config{srcdir}/.ikiwiki"
73                 unless exists $config{wikistatedir};
74 } #}}}
75
76 sub usage { #{{{
77         die "usage: ikiwiki [options] source dest\n";
78 } #}}}
79
80 sub error { #{{{
81         if ($config{cgi}) {
82                 print "Content-type: text/html\n\n";
83                 print misctemplate("Error", "<p>Error: @_</p>");
84         }
85         die @_;
86 } #}}}
87
88 sub debug ($) { #{{{
89         return unless $config{verbose};
90         if (! $config{cgi}) {
91                 print "@_\n";
92         }
93         else {
94                 print STDERR "@_\n";
95         }
96 } #}}}
97
98 sub mtime ($) { #{{{
99         my $page=shift;
100         
101         return (stat($page))[9];
102 } #}}}
103
104 sub possibly_foolish_untaint { #{{{
105         my $tainted=shift;
106         my ($untainted)=$tainted=~/(.*)/;
107         return $untainted;
108 } #}}}
109
110 sub basename ($) { #{{{
111         my $file=shift;
112
113         $file=~s!.*/!!;
114         return $file;
115 } #}}}
116
117 sub dirname ($) { #{{{
118         my $file=shift;
119
120         $file=~s!/?[^/]+$!!;
121         return $file;
122 } #}}}
123
124 sub pagetype ($) { #{{{
125         my $page=shift;
126         
127         if ($page =~ /\.mdwn$/) {
128                 return ".mdwn";
129         }
130         else {
131                 return "unknown";
132         }
133 } #}}}
134
135 sub pagename ($) { #{{{
136         my $file=shift;
137
138         my $type=pagetype($file);
139         my $page=$file;
140         $page=~s/\Q$type\E*$// unless $type eq 'unknown';
141         return $page;
142 } #}}}
143
144 sub htmlpage ($) { #{{{
145         my $page=shift;
146
147         return $page.".html";
148 } #}}}
149
150 sub readfile ($) { #{{{
151         my $file=shift;
152
153         if (-l $file) {
154                 error("cannot read a symlink ($file)");
155         }
156         
157         local $/=undef;
158         open (IN, "$file") || error("failed to read $file: $!");
159         my $ret=<IN>;
160         close IN;
161         return $ret;
162 } #}}}
163
164 sub writefile ($$) { #{{{
165         my $file=shift;
166         my $content=shift;
167         
168         if (-l $file) {
169                 error("cannot write to a symlink ($file)");
170         }
171
172         my $dir=dirname($file);
173         if (! -d $dir) {
174                 my $d="";
175                 foreach my $s (split(m!/+!, $dir)) {
176                         $d.="$s/";
177                         if (! -d $d) {
178                                 mkdir($d) || error("failed to create directory $d: $!");
179                         }
180                 }
181         }
182         
183         open (OUT, ">$file") || error("failed to write $file: $!");
184         print OUT $content;
185         close OUT;
186 } #}}}
187
188 sub findlinks ($$) { #{{{
189         my $content=shift;
190         my $page=shift;
191
192         my @links;
193         while ($content =~ /(?<!\\)$config{wiki_link_regexp}/g) {
194                 push @links, lc($1);
195         }
196         # Discussion links are a special case since they're not in the text
197         # of the page, but on its template.
198         return @links, "$page/discussion";
199 } #}}}
200
201 sub bestlink ($$) { #{{{
202         # Given a page and the text of a link on the page, determine which
203         # existing page that link best points to. Prefers pages under a
204         # subdirectory with the same name as the source page, failing that
205         # goes down the directory tree to the base looking for matching
206         # pages.
207         my $page=shift;
208         my $link=lc(shift);
209         
210         my $cwd=$page;
211         do {
212                 my $l=$cwd;
213                 $l.="/" if length $l;
214                 $l.=$link;
215
216                 if (exists $links{$l}) {
217                         #debug("for $page, \"$link\", use $l");
218                         return $l;
219                 }
220         } while $cwd=~s!/?[^/]+$!!;
221
222         #print STDERR "warning: page $page, broken link: $link\n";
223         return "";
224 } #}}}
225
226 sub isinlinableimage ($) { #{{{
227         my $file=shift;
228         
229         $file=~/\.(png|gif|jpg|jpeg)$/;
230 } #}}}
231
232 sub htmllink { #{{{
233         my $page=shift;
234         my $link=shift;
235         my $noimageinline=shift; # don't turn links into inline html images
236         my $forcesubpage=shift; # force a link to a subpage
237
238         my $bestlink;
239         if (! $forcesubpage) {
240                 $bestlink=bestlink($page, $link);
241         }
242         else {
243                 $bestlink="$page/".lc($link);
244         }
245
246         return $link if length $bestlink && $page eq $bestlink;
247         
248         # TODO BUG: %renderedfiles may not have it, if the linked to page
249         # was also added and isn't yet rendered! Note that this bug is
250         # masked by the bug mentioned below that makes all new files
251         # be rendered twice.
252         if (! grep { $_ eq $bestlink } values %renderedfiles) {
253                 $bestlink=htmlpage($bestlink);
254         }
255         if (! grep { $_ eq $bestlink } values %renderedfiles) {
256                 return "<a href=\"$config{cgiurl}?do=create&page=$link&from=$page\">?</a>$link"
257         }
258         
259         $bestlink=File::Spec->abs2rel($bestlink, dirname($page));
260         
261         if (! $noimageinline && isinlinableimage($bestlink)) {
262                 return "<img src=\"$bestlink\">";
263         }
264         return "<a href=\"$bestlink\">$link</a>";
265 } #}}}
266
267 sub linkify ($$) { #{{{
268         my $content=shift;
269         my $page=shift;
270
271         $content =~ s{(\\?)$config{wiki_link_regexp}}{
272                 $1 ? "[[$2]]" : htmllink($page, $2)
273         }eg;
274         
275         return $content;
276 } #}}}
277
278 sub htmlize ($$) { #{{{
279         my $type=shift;
280         my $content=shift;
281         
282         if (! $INC{"/usr/bin/markdown"}) {
283                 no warnings 'once';
284                 $blosxom::version="is a proper perl module too much to ask?";
285                 use warnings 'all';
286                 do "/usr/bin/markdown";
287         }
288         
289         if ($type eq '.mdwn') {
290                 return Markdown::Markdown($content);
291         }
292         else {
293                 error("htmlization of $type not supported");
294         }
295 } #}}}
296
297 sub backlinks ($) { #{{{
298         my $page=shift;
299
300         my @links;
301         foreach my $p (keys %links) {
302                 next if bestlink($page, $p) eq $page;
303                 if (grep { length $_ && bestlink($p, $_) eq $page } @{$links{$p}}) {
304                         my $href=File::Spec->abs2rel(htmlpage($p), dirname($page));
305                         
306                         # Trim common dir prefixes from both pages.
307                         my $p_trimmed=$p;
308                         my $page_trimmed=$page;
309                         my $dir;
310                         1 while (($dir)=$page_trimmed=~m!^([^/]+/)!) &&
311                                 defined $dir &&
312                                 $p_trimmed=~s/^\Q$dir\E// &&
313                                 $page_trimmed=~s/^\Q$dir\E//;
314                                        
315                         push @links, { url => $href, page => $p_trimmed };
316                 }
317         }
318
319         return sort { $a->{page} cmp $b->{page} } @links;
320 } #}}}
321         
322 sub parentlinks ($) { #{{{
323         my $page=shift;
324         
325         my @ret;
326         my $pagelink="";
327         my $path="";
328         my $skip=1;
329         foreach my $dir (reverse split("/", $page)) {
330                 if (! $skip) {
331                         $path.="../";
332                         unshift @ret, { url => "$path$dir.html", page => $dir };
333                 }
334                 else {
335                         $skip=0;
336                 }
337         }
338         unshift @ret, { url => length $path ? $path : ".", page => $config{wikiname} };
339         return @ret;
340 } #}}}
341
342 sub indexlink () { #{{{
343         return "<a href=\"$config{url}\">$config{wikiname}</a>";
344 } #}}}
345
346 sub finalize ($$$) { #{{{
347         my $content=shift;
348         my $page=shift;
349         my $mtime=shift;
350
351         my $title=basename($page);
352         $title=~s/_/ /g;
353         
354         my $template=HTML::Template->new(blind_cache => 1,
355                 filename => "$config{templatedir}/page.tmpl");
356         
357         if (length $config{cgiurl}) {
358                 $template->param(editurl => "$config{cgiurl}?do=edit&page=$page");
359                 $template->param(prefsurl => "$config{cgiurl}?do=prefs");
360                 if ($config{svn}) {
361                         $template->param(recentchangesurl => "$config{cgiurl}?do=recentchanges");
362                 }
363         }
364
365         if (length $config{historyurl}) {
366                 my $u=$config{historyurl};
367                 $u=~s/\[\[file\]\]/$pagesources{$page}/g;
368                 $template->param(historyurl => $u);
369         }
370         
371         $template->param(
372                 title => $title,
373                 wikiname => $config{wikiname},
374                 parentlinks => [parentlinks($page)],
375                 content => $content,
376                 backlinks => [backlinks($page)],
377                 discussionlink => htmllink($page, "Discussion", 1, 1),
378                 mtime => scalar(gmtime($mtime)),
379         );
380         
381         return $template->output;
382 } #}}}
383
384 sub check_overwrite ($$) { #{{{
385         # Important security check. Make sure to call this before saving
386         # any files to the source directory.
387         my $dest=shift;
388         my $src=shift;
389         
390         if (! exists $renderedfiles{$src} && -e $dest && ! $config{rebuild}) {
391                 error("$dest already exists and was rendered from ".
392                         join(" ",(grep { $renderedfiles{$_} eq $dest } keys
393                                 %renderedfiles)).
394                         ", before, so not rendering from $src");
395         }
396 } #}}}
397
398 sub render ($) { #{{{
399         my $file=shift;
400         
401         my $type=pagetype($file);
402         my $content=readfile("$config{srcdir}/$file");
403         if ($type ne 'unknown') {
404                 my $page=pagename($file);
405                 
406                 $links{$page}=[findlinks($content, $page)];
407                 
408                 $content=linkify($content, $page);
409                 $content=htmlize($type, $content);
410                 $content=finalize($content, $page,
411                         mtime("$config{srcdir}/$file"));
412                 
413                 check_overwrite("$config{destdir}/".htmlpage($page), $page);
414                 writefile("$config{destdir}/".htmlpage($page), $content);
415                 $oldpagemtime{$page}=time;
416                 $renderedfiles{$page}=htmlpage($page);
417         }
418         else {
419                 $links{$file}=[];
420                 check_overwrite("$config{destdir}/$file", $file);
421                 writefile("$config{destdir}/$file", $content);
422                 $oldpagemtime{$file}=time;
423                 $renderedfiles{$file}=$file;
424         }
425 } #}}}
426
427 sub lockwiki () { #{{{
428         # Take an exclusive lock on the wiki to prevent multiple concurrent
429         # run issues. The lock will be dropped on program exit.
430         if (! -d $config{wikistatedir}) {
431                 mkdir($config{wikistatedir});
432         }
433         open(WIKILOCK, ">$config{wikistatedir}/lockfile") ||
434                 error ("cannot write to $config{wikistatedir}/lockfile: $!");
435         if (! flock(WIKILOCK, 2 | 4)) {
436                 debug("wiki seems to be locked, waiting for lock");
437                 my $wait=600; # arbitrary, but don't hang forever to 
438                               # prevent process pileup
439                 for (1..600) {
440                         return if flock(WIKILOCK, 2 | 4);
441                         sleep 1;
442                 }
443                 error("wiki is locked; waited $wait seconds without lock being freed (possible stuck process or stale lock?)");
444         }
445 } #}}}
446
447 sub unlockwiki () { #{{{
448         close WIKILOCK;
449 } #}}}
450
451 sub loadindex () { #{{{
452         open (IN, "$config{wikistatedir}/index") || return;
453         while (<IN>) {
454                 $_=possibly_foolish_untaint($_);
455                 chomp;
456                 my ($mtime, $file, $rendered, @links)=split(' ', $_);
457                 my $page=pagename($file);
458                 $pagesources{$page}=$file;
459                 $oldpagemtime{$page}=$mtime;
460                 $oldlinks{$page}=[@links];
461                 $links{$page}=[@links];
462                 $renderedfiles{$page}=$rendered;
463         }
464         close IN;
465 } #}}}
466
467 sub saveindex () { #{{{
468         if (! -d $config{wikistatedir}) {
469                 mkdir($config{wikistatedir});
470         }
471         open (OUT, ">$config{wikistatedir}/index") || 
472                 error("cannot write to $config{wikistatedir}/index: $!");
473         foreach my $page (keys %oldpagemtime) {
474                 print OUT "$oldpagemtime{$page} $pagesources{$page} $renderedfiles{$page} ".
475                         join(" ", @{$links{$page}})."\n"
476                                 if $oldpagemtime{$page};
477         }
478         close OUT;
479 } #}}}
480
481 sub rcs_update () { #{{{
482         if (-d "$config{srcdir}/.svn") {
483                 if (system("svn", "update", "--quiet", $config{srcdir}) != 0) {
484                         warn("svn update failed\n");
485                 }
486         }
487 } #}}}
488
489 sub rcs_prepedit ($) { #{{{
490         # Prepares to edit a file under revision control. Returns a token
491         # that must be passed into rcs_commit when the file is ready
492         # for committing.
493         # The file is relative to the srcdir.
494         my $file=shift;
495         
496         if (-d "$config{srcdir}/.svn") {
497                 # For subversion, return the revision of the file when
498                 # editing begins.
499                 my $rev=svn_info("Revision", "$config{srcdir}/$file");
500                 return defined $rev ? $rev : "";
501         }
502 } #}}}
503
504 sub rcs_commit ($$$) { #{{{
505         # Tries to commit the page; returns undef on _success_ and
506         # a version of the page with the rcs's conflict markers on failure.
507         # The file is relative to the srcdir.
508         my $file=shift;
509         my $message=shift;
510         my $rcstoken=shift;
511
512         if (-d "$config{srcdir}/.svn") {
513                 # Check to see if the page has been changed by someone
514                 # else since rcs_prepedit was called.
515                 my ($oldrev)=$rcstoken=~/^([0-9]+)$/; # untaint
516                 my $rev=svn_info("Revision", "$config{srcdir}/$file");
517                 if (defined $rev && defined $oldrev && $rev != $oldrev) {
518                         # Merge their changes into the file that we've
519                         # changed.
520                         chdir($config{srcdir}); # svn merge wants to be here
521                         if (system("svn", "merge", "--quiet", "-r$oldrev:$rev",
522                                    "$config{srcdir}/$file") != 0) {
523                                 warn("svn merge -r$oldrev:$rev failed\n");
524                         }
525                 }
526
527                 if (system("svn", "commit", "--quiet", "-m",
528                            possibly_foolish_untaint($message),
529                            "$config{srcdir}") != 0) {
530                         my $conflict=readfile("$config{srcdir}/$file");
531                         if (system("svn", "revert", "--quiet", "$config{srcdir}/$file") != 0) {
532                                 warn("svn revert failed\n");
533                         }
534                         return $conflict;
535                 }
536         }
537         return undef # success
538 } #}}}
539
540 sub rcs_add ($) { #{{{
541         # filename is relative to the root of the srcdir
542         my $file=shift;
543
544         if (-d "$config{srcdir}/.svn") {
545                 my $parent=dirname($file);
546                 while (! -d "$config{srcdir}/$parent/.svn") {
547                         $file=$parent;
548                         $parent=dirname($file);
549                 }
550                 
551                 if (system("svn", "add", "--quiet", "$config{srcdir}/$file") != 0) {
552                         warn("svn add failed\n");
553                 }
554         }
555 } #}}}
556
557 sub svn_info ($$) { #{{{
558         my $field=shift;
559         my $file=shift;
560
561         my $info=`LANG=C svn info $file`;
562         my ($ret)=$info=~/^$field: (.*)$/m;
563         return $ret;
564 } #}}}
565
566 sub rcs_recentchanges ($) { #{{{
567         my $num=shift;
568         my @ret;
569         
570         eval q{use CGI 'escapeHTML'};
571         eval q{use Date::Parse};
572         eval q{use Time::Duration};
573         
574         if (-d "$config{srcdir}/.svn") {
575                 my $svn_url=svn_info("URL", $config{srcdir});
576
577                 # FIXME: currently assumes that the wiki is somewhere
578                 # under trunk in svn, doesn't support other layouts.
579                 my ($svn_base)=$svn_url=~m!(/trunk(?:/.*)?)$!;
580                 
581                 my $div=qr/^--------------------+$/;
582                 my $infoline=qr/^r(\d+)\s+\|\s+([^\s]+)\s+\|\s+(\d+-\d+-\d+\s+\d+:\d+:\d+\s+[-+]?\d+).*/;
583                 my $state='start';
584                 my ($rev, $user, $when, @pages, @message);
585                 foreach (`LANG=C svn log --limit $num -v '$svn_url'`) {
586                         chomp;
587                         if ($state eq 'start' && /$div/) {
588                                 $state='header';
589                         }
590                         elsif ($state eq 'header' && /$infoline/) {
591                                 $rev=$1;
592                                 $user=$2;
593                                 $when=concise(ago(time - str2time($3)));
594                         }
595                         elsif ($state eq 'header' && /^\s+[A-Z]\s+\Q$svn_base\E\/([^ ]+)(?:$|\s)/) {
596                                 my $file=$1;
597                                 my $diffurl=$config{diffurl};
598                                 $diffurl=~s/\[\[file\]\]/$file/g;
599                                 $diffurl=~s/\[\[r1\]\]/$rev - 1/eg;
600                                 $diffurl=~s/\[\[r2\]\]/$rev/g;
601                                 push @pages, {
602                                         link => htmllink("", pagename($file), 1),
603                                         diffurl => $diffurl,
604                                 } if length $file;
605                         }
606                         elsif ($state eq 'header' && /^$/) {
607                                 $state='body';
608                         }
609                         elsif ($state eq 'body' && /$div/) {
610                                 my $committype="web";
611                                 if (defined $message[0] &&
612                                     $message[0]->{line}=~/^web commit by (\w+):?(.*)/) {
613                                         $user="$1";
614                                         $message[0]->{line}=$2;
615                                 }
616                                 else {
617                                         $committype="svn";
618                                 }
619                                 
620                                 push @ret, { rev => $rev,
621                                         user => htmllink("", $user, 1),
622                                         committype => $committype,
623                                         when => $when, message => [@message],
624                                         pages => [@pages],
625                                 } if @pages;
626                                 return @ret if @ret >= $num;
627                                 
628                                 $state='header';
629                                 $rev=$user=$when=undef;
630                                 @pages=@message=();
631                         }
632                         elsif ($state eq 'body') {
633                                 push @message, {line => escapeHTML($_)},
634                         }
635                 }
636         }
637
638         return @ret;
639 } #}}}
640
641 sub prune ($) { #{{{
642         my $file=shift;
643
644         unlink($file);
645         my $dir=dirname($file);
646         while (rmdir($dir)) {
647                 $dir=dirname($dir);
648         }
649 } #}}}
650
651 sub refresh () { #{{{
652         # find existing pages
653         my %exists;
654         my @files;
655         eval q{use File::Find};
656         find({
657                 no_chdir => 1,
658                 wanted => sub {
659                         if (/$config{wiki_file_prune_regexp}/) {
660                                 no warnings 'once';
661                                 $File::Find::prune=1;
662                                 use warnings "all";
663                         }
664                         elsif (! -d $_ && ! -l $_) {
665                                 my ($f)=/$config{wiki_file_regexp}/; # untaint
666                                 if (! defined $f) {
667                                         warn("skipping bad filename $_\n");
668                                 }
669                                 else {
670                                         $f=~s/^\Q$config{srcdir}\E\/?//;
671                                         push @files, $f;
672                                         $exists{pagename($f)}=1;
673                                 }
674                         }
675                 },
676         }, $config{srcdir});
677
678         my %rendered;
679
680         # check for added or removed pages
681         my @add;
682         foreach my $file (@files) {
683                 my $page=pagename($file);
684                 if (! $oldpagemtime{$page}) {
685                         debug("new page $page");
686                         push @add, $file;
687                         $links{$page}=[];
688                         $pagesources{$page}=$file;
689                 }
690         }
691         my @del;
692         foreach my $page (keys %oldpagemtime) {
693                 if (! $exists{$page}) {
694                         debug("removing old page $page");
695                         push @del, $pagesources{$page};
696                         prune($config{destdir}."/".$renderedfiles{$page});
697                         delete $renderedfiles{$page};
698                         $oldpagemtime{$page}=0;
699                         delete $pagesources{$page};
700                 }
701         }
702         
703         # render any updated files
704         foreach my $file (@files) {
705                 my $page=pagename($file);
706                 
707                 if (! exists $oldpagemtime{$page} ||
708                     mtime("$config{srcdir}/$file") > $oldpagemtime{$page}) {
709                         debug("rendering changed file $file");
710                         render($file);
711                         $rendered{$file}=1;
712                 }
713         }
714         
715         # if any files were added or removed, check to see if each page
716         # needs an update due to linking to them
717         # TODO: inefficient; pages may get rendered above and again here;
718         # problem is the bestlink may have changed and we won't know until
719         # now
720         if (@add || @del) {
721 FILE:           foreach my $file (@files) {
722                         my $page=pagename($file);
723                         foreach my $f (@add, @del) {
724                                 my $p=pagename($f);
725                                 foreach my $link (@{$links{$page}}) {
726                                         if (bestlink($page, $link) eq $p) {
727                                                 debug("rendering $file, which links to $p");
728                                                 render($file);
729                                                 $rendered{$file}=1;
730                                                 next FILE;
731                                         }
732                                 }
733                         }
734                 }
735         }
736
737         # handle backlinks; if a page has added/removed links, update the
738         # pages it links to
739         # TODO: inefficient; pages may get rendered above and again here;
740         # problem is the backlinks could be wrong in the first pass render
741         # above
742         if (%rendered) {
743                 my %linkchanged;
744                 foreach my $file (keys %rendered, @del) {
745                         my $page=pagename($file);
746                         if (exists $links{$page}) {
747                                 foreach my $link (map { bestlink($page, $_) } @{$links{$page}}) {
748                                         if (length $link &&
749                                             ! exists $oldlinks{$page} ||
750                                             ! grep { $_ eq $link } @{$oldlinks{$page}}) {
751                                                 $linkchanged{$link}=1;
752                                         }
753                                 }
754                         }
755                         if (exists $oldlinks{$page}) {
756                                 foreach my $link (map { bestlink($page, $_) } @{$oldlinks{$page}}) {
757                                         if (length $link &&
758                                             ! exists $links{$page} ||
759                                             ! grep { $_ eq $link } @{$links{$page}}) {
760                                                 $linkchanged{$link}=1;
761                                         }
762                                 }
763                         }
764                 }
765                 foreach my $link (keys %linkchanged) {
766                         my $linkfile=$pagesources{$link};
767                         if (defined $linkfile) {
768                                 debug("rendering $linkfile, to update its backlinks");
769                                 render($linkfile);
770                         }
771                 }
772         }
773 } #}}}
774
775 sub gen_wrapper () { #{{{
776         eval q{use Cwd 'abs_path'};
777         $config{srcdir}=abs_path($config{srcdir});
778         $config{destdir}=abs_path($config{destdir});
779         my $this=abs_path($0);
780         if (! -x $this) {
781                 error("$this doesn't seem to be executable");
782         }
783
784         if ($config{setup}) {
785                 error("cannot create a wrapper that uses a setup file");
786         }
787         
788         my @params=($config{srcdir}, $config{destdir},
789                 "--wikiname=$config{wikiname}",
790                 "--templatedir=$config{templatedir}");
791         push @params, "--verbose" if $config{verbose};
792         push @params, "--rebuild" if $config{rebuild};
793         push @params, "--nosvn" if !$config{svn};
794         push @params, "--cgi" if $config{cgi};
795         push @params, "--url=$config{url}" if length $config{url};
796         push @params, "--cgiurl=$config{cgiurl}" if length $config{cgiurl};
797         push @params, "--historyurl=$config{historyurl}" if length $config{historyurl};
798         push @params, "--diffurl=$config{diffurl}" if length $config{diffurl};
799         push @params, "--anonok" if $config{anonok};
800         push @params, "--adminuser=$_" foreach @{$config{adminuser}};
801         my $params=join(" ", @params);
802         my $call='';
803         foreach my $p ($this, $this, @params) {
804                 $call.=qq{"$p", };
805         }
806         $call.="NULL";
807         
808         my @envsave;
809         push @envsave, qw{REMOTE_ADDR QUERY_STRING REQUEST_METHOD REQUEST_URI
810                        CONTENT_TYPE CONTENT_LENGTH GATEWAY_INTERFACE
811                        HTTP_COOKIE} if $config{cgi};
812         my $envsave="";
813         foreach my $var (@envsave) {
814                 $envsave.=<<"EOF"
815         if ((s=getenv("$var")))
816                 asprintf(&newenviron[i++], "%s=%s", "$var", s);
817 EOF
818         }
819         
820         open(OUT, ">ikiwiki-wrap.c") || error("failed to write ikiwiki-wrap.c: $!");;
821         print OUT <<"EOF";
822 /* A wrapper for ikiwiki, can be safely made suid. */
823 #define _GNU_SOURCE
824 #include <stdio.h>
825 #include <unistd.h>
826 #include <stdlib.h>
827 #include <string.h>
828
829 extern char **environ;
830
831 int main (int argc, char **argv) {
832         /* Sanitize environment. */
833         char *s;
834         char *newenviron[$#envsave+3];
835         int i=0;
836 $envsave
837         newenviron[i++]="HOME=$ENV{HOME}";
838         newenviron[i]=NULL;
839         environ=newenviron;
840
841         if (argc == 2 && strcmp(argv[1], "--params") == 0) {
842                 printf("$params\\n");
843                 exit(0);
844         }
845         
846         execl($call);
847         perror("failed to run $this");
848         exit(1);
849 }
850 EOF
851         close OUT;
852         if (system("gcc", "ikiwiki-wrap.c", "-o", possibly_foolish_untaint($config{wrapper})) != 0) {
853                 error("failed to compile ikiwiki-wrap.c");
854         }
855         unlink("ikiwiki-wrap.c");
856         if (defined $config{wrappermode} &&
857             ! chmod(oct($config{wrappermode}), possibly_foolish_untaint($config{wrapper}))) {
858                 error("chmod $config{wrapper}: $!");
859         }
860         print "successfully generated $config{wrapper}\n";
861 } #}}}
862                 
863 sub misctemplate ($$) { #{{{
864         my $title=shift;
865         my $pagebody=shift;
866         
867         my $template=HTML::Template->new(
868                 filename => "$config{templatedir}/misc.tmpl"
869         );
870         $template->param(
871                 title => $title,
872                 indexlink => indexlink(),
873                 wikiname => $config{wikiname},
874                 pagebody => $pagebody,
875         );
876         return $template->output;
877 }#}}}
878
879 sub cgi_recentchanges ($) { #{{{
880         my $q=shift;
881         
882         my $template=HTML::Template->new(
883                 filename => "$config{templatedir}/recentchanges.tmpl"
884         );
885         $template->param(
886                 title => "RecentChanges",
887                 indexlink => indexlink(),
888                 wikiname => $config{wikiname},
889                 changelog => [rcs_recentchanges(100)],
890         );
891         print $q->header, $template->output;
892 } #}}}
893
894 sub userinfo_get ($$) { #{{{
895         my $user=shift;
896         my $field=shift;
897
898         eval q{use Storable};
899         my $userdata=eval{ Storable::lock_retrieve("$config{wikistatedir}/userdb") };
900         if (! defined $userdata || ! ref $userdata || 
901             ! exists $userdata->{$user} || ! ref $userdata->{$user} ||
902             ! exists $userdata->{$user}->{$field}) {
903                 return "";
904         }
905         return $userdata->{$user}->{$field};
906 } #}}}
907
908 sub userinfo_set ($$$) { #{{{
909         my $user=shift;
910         my $field=shift;
911         my $value=shift;
912         
913         eval q{use Storable};
914         my $userdata=eval{ Storable::lock_retrieve("$config{wikistatedir}/userdb") };
915         if (! defined $userdata || ! ref $userdata || 
916             ! exists $userdata->{$user} || ! ref $userdata->{$user}) {
917                 return "";
918         }
919         
920         $userdata->{$user}->{$field}=$value;
921         my $oldmask=umask(077);
922         my $ret=Storable::lock_store($userdata, "$config{wikistatedir}/userdb");
923         umask($oldmask);
924         return $ret;
925 } #}}}
926
927 sub userinfo_setall ($$) { #{{{
928         my $user=shift;
929         my $info=shift;
930         
931         eval q{use Storable};
932         my $userdata=eval{ Storable::lock_retrieve("$config{wikistatedir}/userdb") };
933         if (! defined $userdata || ! ref $userdata) {
934                 $userdata={};
935         }
936         $userdata->{$user}=$info;
937         my $oldmask=umask(077);
938         my $ret=Storable::lock_store($userdata, "$config{wikistatedir}/userdb");
939         umask($oldmask);
940         return $ret;
941 } #}}}
942
943 sub cgi_signin ($$) { #{{{
944         my $q=shift;
945         my $session=shift;
946
947         eval q{use CGI::FormBuilder};
948         my $form = CGI::FormBuilder->new(
949                 title => "signin",
950                 fields => [qw(do page from name password confirm_password email)],
951                 header => 1,
952                 method => 'POST',
953                 validate => {
954                         confirm_password => {
955                                 perl => q{eq $form->field("password")},
956                         },
957                         email => 'EMAIL',
958                 },
959                 required => 'NONE',
960                 javascript => 0,
961                 params => $q,
962                 action => $q->request_uri,
963                 header => 0,
964                 template => (-e "$config{templatedir}/signin.tmpl" ?
965                               "$config{templatedir}/signin.tmpl" : "")
966         );
967         
968         $form->field(name => "name", required => 0);
969         $form->field(name => "do", type => "hidden");
970         $form->field(name => "page", type => "hidden");
971         $form->field(name => "from", type => "hidden");
972         $form->field(name => "password", type => "password", required => 0);
973         $form->field(name => "confirm_password", type => "password", required => 0);
974         $form->field(name => "email", required => 0);
975         if ($q->param("do") ne "signin") {
976                 $form->text("You need to log in first.");
977         }
978         
979         if ($form->submitted) {
980                 # Set required fields based on how form was submitted.
981                 my %required=(
982                         "Login" => [qw(name password)],
983                         "Register" => [qw(name password confirm_password email)],
984                         "Mail Password" => [qw(name)],
985                 );
986                 foreach my $opt (@{$required{$form->submitted}}) {
987                         $form->field(name => $opt, required => 1);
988                 }
989         
990                 # Validate password differently depending on how
991                 # form was submitted.
992                 if ($form->submitted eq 'Login') {
993                         $form->field(
994                                 name => "password",
995                                 validate => sub {
996                                         length $form->field("name") &&
997                                         shift eq userinfo_get($form->field("name"), 'password');
998                                 },
999                         );
1000                         $form->field(name => "name", validate => '/^\w+$/');
1001                 }
1002                 else {
1003                         $form->field(name => "password", validate => 'VALUE');
1004                 }
1005                 # And make sure the entered name exists when logging
1006                 # in or sending email, and does not when registering.
1007                 if ($form->submitted eq 'Register') {
1008                         $form->field(
1009                                 name => "name",
1010                                 validate => sub {
1011                                         my $name=shift;
1012                                         length $name &&
1013                                         ! userinfo_get($name, "regdate");
1014                                 },
1015                         );
1016                 }
1017                 else {
1018                         $form->field(
1019                                 name => "name",
1020                                 validate => sub {
1021                                         my $name=shift;
1022                                         length $name &&
1023                                         userinfo_get($name, "regdate");
1024                                 },
1025                         );
1026                 }
1027         }
1028         else {
1029                 # First time settings.
1030                 $form->field(name => "name", comment => "use FirstnameLastName");
1031                 $form->field(name => "confirm_password", comment => "(only needed");
1032                 $form->field(name => "email",            comment => "for registration)");
1033                 if ($session->param("name")) {
1034                         $form->field(name => "name", value => $session->param("name"));
1035                 }
1036         }
1037
1038         if ($form->submitted && $form->validate) {
1039                 if ($form->submitted eq 'Login') {
1040                         $session->param("name", $form->field("name"));
1041                         if (defined $form->field("do") && 
1042                             $form->field("do") ne 'signin') {
1043                                 print $q->redirect(
1044                                         "$config{cgiurl}?do=".$form->field("do").
1045                                         "&page=".$form->field("page").
1046                                         "&from=".$form->field("from"));;
1047                         }
1048                         else {
1049                                 print $q->redirect($config{url});
1050                         }
1051                 }
1052                 elsif ($form->submitted eq 'Register') {
1053                         my $user_name=$form->field('name');
1054                         if (userinfo_setall($user_name, {
1055                                            'email' => $form->field('email'),
1056                                            'password' => $form->field('password'),
1057                                            'regdate' => time
1058                                          })) {
1059                                 $form->field(name => "confirm_password", type => "hidden");
1060                                 $form->field(name => "email", type => "hidden");
1061                                 $form->text("Registration successful. Now you can Login.");
1062                                 print $session->header();
1063                                 print misctemplate($form->title, $form->render(submit => ["Login"]));
1064                         }
1065                         else {
1066                                 error("Error saving registration.");
1067                         }
1068                 }
1069                 elsif ($form->submitted eq 'Mail Password') {
1070                         my $user_name=$form->field("name");
1071                         my $template=HTML::Template->new(
1072                                 filename => "$config{templatedir}/passwordmail.tmpl"
1073                         );
1074                         $template->param(
1075                                 user_name => $user_name,
1076                                 user_password => userinfo_get($user_name, "password"),
1077                                 wikiurl => $config{url},
1078                                 wikiname => $config{wikiname},
1079                                 REMOTE_ADDR => $ENV{REMOTE_ADDR},
1080                         );
1081                         
1082                         eval q{use Mail::Sendmail};
1083                         my ($fromhost) = $config{cgiurl} =~ m!/([^/]+)!;
1084                         sendmail(
1085                                 To => userinfo_get($user_name, "email"),
1086                                 From => "$config{wikiname} admin <".(getpwuid($>))[0]."@".$fromhost.">",
1087                                 Subject => "$config{wikiname} information",
1088                                 Message => $template->output,
1089                         ) or error("Failed to send mail");
1090                         
1091                         $form->text("Your password has been emailed to you.");
1092                         $form->field(name => "name", required => 0);
1093                         print $session->header();
1094                         print misctemplate($form->title, $form->render(submit => ["Login", "Register", "Mail Password"]));
1095                 }
1096         }
1097         else {
1098                 print $session->header();
1099                 print misctemplate($form->title, $form->render(submit => ["Login", "Register", "Mail Password"]));
1100         }
1101 } #}}}
1102
1103 sub is_admin ($) { #{{{
1104         my $user_name=shift;
1105
1106         return grep { $_ eq $user_name } @{$config{adminuser}};
1107 } #}}}
1108
1109 sub glob_match ($$) { #{{{
1110         my $page=shift;
1111         my $glob=shift;
1112
1113         # turn glob into safe regexp
1114         $glob=quotemeta($glob);
1115         $glob=~s/\\\*/.*/g;
1116         $glob=~s/\\\?/./g;
1117         $glob=~s!\\/!/!g;
1118         
1119         $page=~/^$glob$/i;
1120 } #}}}
1121
1122 sub globlist_match ($$) { #{{{
1123         my $page=shift;
1124         my @globlist=split(" ", shift);
1125
1126         # check any negated globs first
1127         foreach my $glob (@globlist) {
1128                 return 0 if $glob=~/^!(.*)/ && glob_match($page, $1);
1129         }
1130
1131         foreach my $glob (@globlist) {
1132                 return 1 if glob_match($page, $glob);
1133         }
1134         
1135         return 0;
1136 } #}}}
1137
1138 sub page_locked ($$;$) { #{{{
1139         my $page=shift;
1140         my $session=shift;
1141         my $nonfatal=shift;
1142         
1143         my $user=$session->param("name");
1144         return if length $user && is_admin($user);
1145
1146         foreach my $admin (@{$config{adminuser}}) {
1147                 my $locked_pages=userinfo_get($admin, "locked_pages");
1148                 if (globlist_match($page, userinfo_get($admin, "locked_pages"))) {
1149                         return 1 if $nonfatal;
1150                         error(htmllink("", $page, 1)." is locked by ".
1151                               htmllink("", $admin, 1)." and cannot be edited.");
1152                 }
1153         }
1154
1155         return 0;
1156 } #}}}
1157
1158 sub cgi_prefs ($$) { #{{{
1159         my $q=shift;
1160         my $session=shift;
1161
1162         eval q{use CGI::FormBuilder};
1163         my $form = CGI::FormBuilder->new(
1164                 title => "preferences",
1165                 fields => [qw(do name password confirm_password email locked_pages)],
1166                 header => 0,
1167                 method => 'POST',
1168                 validate => {
1169                         confirm_password => {
1170                                 perl => q{eq $form->field("password")},
1171                         },
1172                         email => 'EMAIL',
1173                 },
1174                 required => 'NONE',
1175                 javascript => 0,
1176                 params => $q,
1177                 action => $q->request_uri,
1178                 template => (-e "$config{templatedir}/prefs.tmpl" ?
1179                               "$config{templatedir}/prefs.tmpl" : "")
1180         );
1181         my @buttons=("Save Preferences", "Logout", "Cancel");
1182         
1183         my $user_name=$session->param("name");
1184         $form->field(name => "do", type => "hidden");
1185         $form->field(name => "name", disabled => 1,
1186                 value => $user_name, force => 1);
1187         $form->field(name => "password", type => "password");
1188         $form->field(name => "confirm_password", type => "password");
1189         $form->field(name => "locked_pages", size => 50,
1190                 comment => "(".htmllink("", "GlobList", 1).")");
1191         
1192         if (! is_admin($user_name)) {
1193                 $form->field(name => "locked_pages", type => "hidden");
1194         }
1195         
1196         if (! $form->submitted) {
1197                 $form->field(name => "email", force => 1,
1198                         value => userinfo_get($user_name, "email"));
1199                 $form->field(name => "locked_pages", force => 1,
1200                         value => userinfo_get($user_name, "locked_pages"));
1201         }
1202         
1203         if ($form->submitted eq 'Logout') {
1204                 $session->delete();
1205                 print $q->redirect($config{url});
1206                 return;
1207         }
1208         elsif ($form->submitted eq 'Cancel') {
1209                 print $q->redirect($config{url});
1210                 return;
1211         }
1212         elsif ($form->submitted eq "Save Preferences" && $form->validate) {
1213                 foreach my $field (qw(password email locked_pages)) {
1214                         if (length $form->field($field)) {
1215                                 userinfo_set($user_name, $field, $form->field($field)) || error("failed to set $field");
1216                         }
1217                 }
1218                 $form->text("Preferences saved.");
1219         }
1220         
1221         print $session->header();
1222         print misctemplate($form->title, $form->render(submit => \@buttons));
1223 } #}}}
1224
1225 sub cgi_editpage ($$) { #{{{
1226         my $q=shift;
1227         my $session=shift;
1228
1229         eval q{use CGI::FormBuilder};
1230         my $form = CGI::FormBuilder->new(
1231                 fields => [qw(do rcsinfo from page content comments)],
1232                 header => 1,
1233                 method => 'POST',
1234                 validate => {
1235                         content => '/.+/',
1236                 },
1237                 required => [qw{content}],
1238                 javascript => 0,
1239                 params => $q,
1240                 action => $q->request_uri,
1241                 table => 0,
1242                 template => "$config{templatedir}/editpage.tmpl"
1243         );
1244         my @buttons=("Save Page", "Preview", "Cancel");
1245         
1246         my ($page)=$form->param('page')=~/$config{wiki_file_regexp}/;
1247         if (! defined $page || ! length $page || $page ne $q->param('page') ||
1248             $page=~/$config{wiki_file_prune_regexp}/ || $page=~/^\//) {
1249                 error("bad page name");
1250         }
1251         $page=lc($page);
1252         
1253         my $file=$page.$config{default_pageext};
1254         my $newfile=1;
1255         if (exists $pagesources{lc($page)}) {
1256                 $file=$pagesources{lc($page)};
1257                 $newfile=0;
1258         }
1259
1260         $form->field(name => "do", type => 'hidden');
1261         $form->field(name => "from", type => 'hidden');
1262         $form->field(name => "rcsinfo", type => 'hidden');
1263         $form->field(name => "page", value => "$page", force => 1);
1264         $form->field(name => "comments", type => "text", size => 80);
1265         $form->field(name => "content", type => "textarea", rows => 20,
1266                 cols => 80);
1267         $form->tmpl_param("can_commit", $config{svn});
1268         $form->tmpl_param("indexlink", indexlink());
1269         $form->tmpl_param("helponformattinglink",
1270                 htmllink("", "HelpOnFormatting", 1));
1271         if (! $form->submitted) {
1272                 $form->field(name => "rcsinfo", value => rcs_prepedit($file),
1273                         force => 1);
1274         }
1275         
1276         if ($form->submitted eq "Cancel") {
1277                 print $q->redirect("$config{url}/".htmlpage($page));
1278                 return;
1279         }
1280         elsif ($form->submitted eq "Preview") {
1281                 $form->tmpl_param("page_preview",
1282                         htmlize($config{default_pageext},
1283                                 linkify($form->field('content'), $page)));
1284         }
1285         else {
1286                 $form->tmpl_param("page_preview", "");
1287         }
1288         $form->tmpl_param("page_conflict", "");
1289         
1290         if (! $form->submitted || $form->submitted eq "Preview" || 
1291             ! $form->validate) {
1292                 if ($form->field("do") eq "create") {
1293                         if (exists $pagesources{lc($page)}) {
1294                                 # hmm, someone else made the page in the
1295                                 # meantime?
1296                                 print $q->redirect("$config{url}/".htmlpage($page));
1297                                 return;
1298                         }
1299                         
1300                         my @page_locs;
1301                         my $best_loc;
1302                         my ($from)=$form->param('from')=~/$config{wiki_file_regexp}/;
1303                         if (! defined $from || ! length $from ||
1304                             $from ne $form->param('from') ||
1305                             $from=~/$config{wiki_file_prune_regexp}/ || $from=~/^\//) {
1306                                 @page_locs=$best_loc=$page;
1307                         }
1308                         else {
1309                                 my $dir=$from."/";
1310                                 $dir=~s![^/]+/$!!;
1311                                 
1312                                 if ($page eq 'discussion') {
1313                                         $best_loc="$from/$page";
1314                                 }
1315                                 else {
1316                                         $best_loc=$dir.$page;
1317                                 }
1318                                 
1319                                 push @page_locs, $dir.$page;
1320                                 push @page_locs, "$from/$page";
1321                                 while (length $dir) {
1322                                         $dir=~s![^/]+/$!!;
1323                                         push @page_locs, $dir.$page;
1324                                 }
1325
1326                                 @page_locs = grep {
1327                                         ! exists $pagesources{lc($_)} &&
1328                                         ! page_locked($_, $session, 1)
1329                                 } @page_locs;
1330                         }
1331
1332                         $form->tmpl_param("page_select", 1);
1333                         $form->field(name => "page", type => 'select',
1334                                 options => \@page_locs, value => $best_loc);
1335                         $form->title("creating $page");
1336                 }
1337                 elsif ($form->field("do") eq "edit") {
1338                         page_locked($page, $session);
1339                         if (! defined $form->field('content') || 
1340                             ! length $form->field('content')) {
1341                                 my $content="";
1342                                 if (exists $pagesources{lc($page)}) {
1343                                         $content=readfile("$config{srcdir}/$pagesources{lc($page)}");
1344                                         $content=~s/\n/\r\n/g;
1345                                 }
1346                                 $form->field(name => "content", value => $content,
1347                                         force => 1);
1348                         }
1349                         $form->tmpl_param("page_select", 0);
1350                         $form->field(name => "page", type => 'hidden');
1351                         $form->title("editing $page");
1352                 }
1353                 
1354                 print $form->render(submit => \@buttons);
1355         }
1356         else {
1357                 # save page
1358                 page_locked($page, $session);
1359                 
1360                 my $content=$form->field('content');
1361                 $content=~s/\r\n/\n/g;
1362                 $content=~s/\r/\n/g;
1363                 writefile("$config{srcdir}/$file", $content);
1364                 
1365                 my $message="web commit ";
1366                 if (length $session->param("name")) {
1367                         $message.="by ".$session->param("name");
1368                 }
1369                 else {
1370                         $message.="from $ENV{REMOTE_ADDR}";
1371                 }
1372                 if (defined $form->field('comments') &&
1373                     length $form->field('comments')) {
1374                         $message.=": ".$form->field('comments');
1375                 }
1376                 
1377                 if ($config{svn}) {
1378                         if ($newfile) {
1379                                 rcs_add($file);
1380                         }
1381                         # prevent deadlock with post-commit hook
1382                         unlockwiki();
1383                         # presumably the commit will trigger an update
1384                         # of the wiki
1385                         my $conflict=rcs_commit($file, $message,
1386                                 $form->field("rcsinfo"));
1387                 
1388                         if (defined $conflict) {
1389                                 $form->field(name => "rcsinfo", value => rcs_prepedit($file),
1390                                         force => 1);
1391                                 $form->tmpl_param("page_conflict", 1);
1392                                 $form->field("content", value => $conflict, force => 1);
1393                                 $form->field("do", "edit)");
1394                                 $form->tmpl_param("page_select", 0);
1395                                 $form->field(name => "page", type => 'hidden');
1396                                 $form->title("editing $page");
1397                                 print $form->render(submit => \@buttons);
1398                                 return;
1399                         }
1400                 }
1401                 else {
1402                         loadindex();
1403                         refresh();
1404                         saveindex();
1405                 }
1406                 
1407                 # The trailing question mark tries to avoid broken
1408                 # caches and get the most recent version of the page.
1409                 print $q->redirect("$config{url}/".htmlpage($page)."?updated");
1410         }
1411 } #}}}
1412
1413 sub cgi () { #{{{
1414         eval q{use CGI};
1415         eval q{use CGI::Session};
1416         
1417         my $q=CGI->new;
1418         
1419         my $do=$q->param('do');
1420         if (! defined $do || ! length $do) {
1421                 error("\"do\" parameter missing");
1422         }
1423         
1424         # This does not need a session.
1425         if ($do eq 'recentchanges') {
1426                 cgi_recentchanges($q);
1427                 return;
1428         }
1429         
1430         CGI::Session->name("ikiwiki_session");
1431
1432         my $oldmask=umask(077);
1433         my $session = CGI::Session->new("driver:db_file", $q,
1434                 { FileName => "$config{wikistatedir}/sessions.db" });
1435         umask($oldmask);
1436         
1437         # Everything below this point needs the user to be signed in.
1438         if ((! $config{anonok} && ! defined $session->param("name") ||
1439              ! defined $session->param("name") ||
1440              ! userinfo_get($session->param("name"), "regdate")) || $do eq 'signin') {
1441                 cgi_signin($q, $session);
1442         
1443                 # Force session flush with safe umask.
1444                 my $oldmask=umask(077);
1445                 $session->flush;
1446                 umask($oldmask);
1447                 
1448                 return;
1449         }
1450         
1451         if ($do eq 'create' || $do eq 'edit') {
1452                 cgi_editpage($q, $session);
1453         }
1454         elsif ($do eq 'prefs') {
1455                 cgi_prefs($q, $session);
1456         }
1457         else {
1458                 error("unknown do parameter");
1459         }
1460 } #}}}
1461
1462 sub setup () { # {{{
1463         my $setup=possibly_foolish_untaint($config{setup});
1464         delete $config{setup};
1465         open (IN, $setup) || error("read $setup: $!\n");
1466         local $/=undef;
1467         my $code=<IN>;
1468         ($code)=$code=~/(.*)/s;
1469         close IN;
1470
1471         eval $code;
1472         error($@) if $@;
1473         exit;
1474 } #}}}
1475
1476 # main {{{
1477 setup() if $config{setup};
1478 lockwiki();
1479 if ($config{wrapper}) {
1480         gen_wrapper();
1481         exit;
1482 }
1483 memoize('pagename');
1484 memoize('bestlink');
1485 loadindex() unless $config{rebuild};
1486 if ($config{cgi}) {
1487         cgi();
1488 }
1489 else {
1490         rcs_update() if $config{svn};
1491         refresh();
1492         saveindex();
1493 }
1494 #}}}