added --getctime
[ikiwiki] / ikiwiki
1 #!/usr/bin/perl -T
2 $ENV{PATH}="/usr/local/bin:/usr/bin:/bin";
3
4 package IkiWiki;
5 use warnings;
6 use strict;
7 use File::Spec;
8 use HTML::Template;
9 use lib '.'; # For use without installation, removed by Makefile.
10
11 use vars qw{%config %links %oldlinks %oldpagemtime %pagectime
12             %renderedfiles %pagesources %inlinepages};
13
14 sub usage () { #{{{
15         die "usage: ikiwiki [options] source dest\n";
16 } #}}}
17
18 sub getconfig () { #{{{
19         if (! exists $ENV{WRAPPED_OPTIONS}) {
20                 %config=(
21                         wiki_file_prune_regexp => qr{((^|/).svn/|\.\.|^\.|\/\.|\.html?$)},
22                         wiki_link_regexp => qr/\[\[([^\s\]]+)\]\]/,
23                         wiki_processor_regexp => qr/\[\[(\w+)\s+([^\]]+)\]\]/,
24                         wiki_file_regexp => qr/(^[-A-Za-z0-9_.:\/+]+$)/,
25                         verbose => 0,
26                         wikiname => "wiki",
27                         default_pageext => ".mdwn",
28                         cgi => 0,
29                         svn => 1,
30                         url => '',
31                         cgiurl => '',
32                         historyurl => '',
33                         diffurl => '',
34                         anonok => 0,
35                         rss => 0,
36                         rebuild => 0,
37                         getctime => 0,
38                         wrapper => undef,
39                         wrappermode => undef,
40                         srcdir => undef,
41                         destdir => undef,
42                         templatedir => "/usr/share/ikiwiki/templates",
43                         setup => undef,
44                         adminuser => undef,
45                 );
46
47                 eval q{use Getopt::Long};
48                 GetOptions(
49                         "setup|s=s" => \$config{setup},
50                         "wikiname=s" => \$config{wikiname},
51                         "verbose|v!" => \$config{verbose},
52                         "rebuild!" => \$config{rebuild},
53                         "getctime" => \$config{getctime},
54                         "wrappermode=i" => \$config{wrappermode},
55                         "svn!" => \$config{svn},
56                         "anonok!" => \$config{anonok},
57                         "rss!" => \$config{rss},
58                         "cgi!" => \$config{cgi},
59                         "url=s" => \$config{url},
60                         "cgiurl=s" => \$config{cgiurl},
61                         "historyurl=s" => \$config{historyurl},
62                         "diffurl=s" => \$config{diffurl},
63                         "exclude=s@" => sub {
64                                 $config{wiki_file_prune_regexp}=qr/$config{wiki_file_prune_regexp}|$_[1]/;
65                         },
66                         "adminuser=s@" => sub {
67                                 push @{$config{adminuser}}, $_[1]
68                         },
69                         "templatedir=s" => sub {
70                                 $config{templatedir}=possibly_foolish_untaint($_[1])
71                         },
72                         "wrapper:s" => sub {
73                                 $config{wrapper}=$_[1] ? $_[1] : "ikiwiki-wrap"
74                         },
75                 ) || usage();
76
77                 if (! $config{setup}) {
78                         usage() unless @ARGV == 2;
79                         $config{srcdir} = possibly_foolish_untaint(shift @ARGV);
80                         $config{destdir} = possibly_foolish_untaint(shift @ARGV);
81                         checkconfig();
82                 }
83         }
84         else {
85                 # wrapper passes a full config structure in the environment
86                 # variable
87                 eval possibly_foolish_untaint($ENV{WRAPPED_OPTIONS});
88                 checkconfig();
89         }
90 } #}}}
91
92 sub checkconfig () { #{{{
93         if ($config{cgi} && ! length $config{url}) {
94                 error("Must specify url to wiki with --url when using --cgi\n");
95         }
96         if ($config{rss} && ! length $config{url}) {
97                 error("Must specify url to wiki with --url when using --rss\n");
98         }
99         
100         $config{wikistatedir}="$config{srcdir}/.ikiwiki"
101                 unless exists $config{wikistatedir};
102         
103         if ($config{svn}) {
104                 require IkiWiki::Rcs::SVN;
105                 $config{rcs}=1;
106         }
107         else {
108                 require IkiWiki::Rcs::Stub;
109                 $config{rcs}=0;
110         }
111 } #}}}
112
113 sub error ($) { #{{{
114         if ($config{cgi}) {
115                 print "Content-type: text/html\n\n";
116                 print misctemplate("Error", "<p>Error: @_</p>");
117         }
118         die @_;
119 } #}}}
120
121 sub possibly_foolish_untaint ($) { #{{{
122         my $tainted=shift;
123         my ($untainted)=$tainted=~/(.*)/;
124         return $untainted;
125 } #}}}
126
127 sub debug ($) { #{{{
128         return unless $config{verbose};
129         if (! $config{cgi}) {
130                 print "@_\n";
131         }
132         else {
133                 print STDERR "@_\n";
134         }
135 } #}}}
136
137 sub basename ($) { #{{{
138         my $file=shift;
139
140         $file=~s!.*/!!;
141         return $file;
142 } #}}}
143
144 sub dirname ($) { #{{{
145         my $file=shift;
146
147         $file=~s!/?[^/]+$!!;
148         return $file;
149 } #}}}
150
151 sub pagetype ($) { #{{{
152         my $page=shift;
153         
154         if ($page =~ /\.mdwn$/) {
155                 return ".mdwn";
156         }
157         else {
158                 return "unknown";
159         }
160 } #}}}
161
162 sub pagename ($) { #{{{
163         my $file=shift;
164
165         my $type=pagetype($file);
166         my $page=$file;
167         $page=~s/\Q$type\E*$// unless $type eq 'unknown';
168         return $page;
169 } #}}}
170
171 sub htmlpage ($) { #{{{
172         my $page=shift;
173
174         return $page.".html";
175 } #}}}
176
177 sub readfile ($) { #{{{
178         my $file=shift;
179
180         if (-l $file) {
181                 error("cannot read a symlink ($file)");
182         }
183         
184         local $/=undef;
185         open (IN, "$file") || error("failed to read $file: $!");
186         my $ret=<IN>;
187         close IN;
188         return $ret;
189 } #}}}
190
191 sub writefile ($$) { #{{{
192         my $file=shift;
193         my $content=shift;
194         
195         if (-l $file) {
196                 error("cannot write to a symlink ($file)");
197         }
198
199         my $dir=dirname($file);
200         if (! -d $dir) {
201                 my $d="";
202                 foreach my $s (split(m!/+!, $dir)) {
203                         $d.="$s/";
204                         if (! -d $d) {
205                                 mkdir($d) || error("failed to create directory $d: $!");
206                         }
207                 }
208         }
209         
210         open (OUT, ">$file") || error("failed to write $file: $!");
211         print OUT $content;
212         close OUT;
213 } #}}}
214
215 sub bestlink ($$) { #{{{
216         # Given a page and the text of a link on the page, determine which
217         # existing page that link best points to. Prefers pages under a
218         # subdirectory with the same name as the source page, failing that
219         # goes down the directory tree to the base looking for matching
220         # pages.
221         my $page=shift;
222         my $link=lc(shift);
223         
224         my $cwd=$page;
225         do {
226                 my $l=$cwd;
227                 $l.="/" if length $l;
228                 $l.=$link;
229
230                 if (exists $links{$l}) {
231                         #debug("for $page, \"$link\", use $l");
232                         return $l;
233                 }
234         } while $cwd=~s!/?[^/]+$!!;
235
236         #print STDERR "warning: page $page, broken link: $link\n";
237         return "";
238 } #}}}
239
240 sub isinlinableimage ($) { #{{{
241         my $file=shift;
242         
243         $file=~/\.(png|gif|jpg|jpeg)$/;
244 } #}}}
245
246 sub pagetitle ($) { #{{{
247         my $page=shift;
248         $page=~s/__(\d+)__/&#$1;/g;
249         $page=~y/_/ /;
250         return $page;
251 } #}}}
252
253 sub htmllink ($$;$$) { #{{{
254         my $page=shift;
255         my $link=shift;
256         my $noimageinline=shift; # don't turn links into inline html images
257         my $forcesubpage=shift; # force a link to a subpage
258
259         my $bestlink;
260         if (! $forcesubpage) {
261                 $bestlink=bestlink($page, $link);
262         }
263         else {
264                 $bestlink="$page/".lc($link);
265         }
266
267         my $linktext=pagetitle(basename($link));
268         
269         return $linktext if length $bestlink && $page eq $bestlink;
270         
271         # TODO BUG: %renderedfiles may not have it, if the linked to page
272         # was also added and isn't yet rendered! Note that this bug is
273         # masked by the bug mentioned below that makes all new files
274         # be rendered twice.
275         if (! grep { $_ eq $bestlink } values %renderedfiles) {
276                 $bestlink=htmlpage($bestlink);
277         }
278         if (! grep { $_ eq $bestlink } values %renderedfiles) {
279                 return "<span><a href=\"$config{cgiurl}?do=create&page=$link&from=$page\">?</a>$linktext</span>"
280         }
281         
282         $bestlink=File::Spec->abs2rel($bestlink, dirname($page));
283         
284         if (! $noimageinline && isinlinableimage($bestlink)) {
285                 return "<img src=\"$bestlink\">";
286         }
287         return "<a href=\"$bestlink\">$linktext</a>";
288 } #}}}
289
290 sub indexlink () { #{{{
291         return "<a href=\"$config{url}\">$config{wikiname}</a>";
292 } #}}}
293
294 sub lockwiki () { #{{{
295         # Take an exclusive lock on the wiki to prevent multiple concurrent
296         # run issues. The lock will be dropped on program exit.
297         if (! -d $config{wikistatedir}) {
298                 mkdir($config{wikistatedir});
299         }
300         open(WIKILOCK, ">$config{wikistatedir}/lockfile") ||
301                 error ("cannot write to $config{wikistatedir}/lockfile: $!");
302         if (! flock(WIKILOCK, 2 | 4)) {
303                 debug("wiki seems to be locked, waiting for lock");
304                 my $wait=600; # arbitrary, but don't hang forever to 
305                               # prevent process pileup
306                 for (1..600) {
307                         return if flock(WIKILOCK, 2 | 4);
308                         sleep 1;
309                 }
310                 error("wiki is locked; waited $wait seconds without lock being freed (possible stuck process or stale lock?)");
311         }
312 } #}}}
313
314 sub unlockwiki () { #{{{
315         close WIKILOCK;
316 } #}}}
317
318 sub loadindex () { #{{{
319         open (IN, "$config{wikistatedir}/index") || return;
320         while (<IN>) {
321                 $_=possibly_foolish_untaint($_);
322                 chomp;
323                 my %items;
324                 $items{link}=[];
325                 foreach my $i (split(/ /, $_)) {
326                         my ($item, $val)=split(/=/, $i, 2);
327                         push @{$items{$item}}, $val;
328                 }
329
330                 next unless exists $items{src}; # skip bad lines for now
331
332                 my $page=pagename($items{src}[0]);
333                 if (! $config{rebuild}) {
334                         $pagesources{$page}=$items{src}[0];
335                         $oldpagemtime{$page}=$items{mtime}[0];
336                         $oldlinks{$page}=[@{$items{link}}];
337                         $links{$page}=[@{$items{link}}];
338                         $inlinepages{$page}=join(" ", @{$items{inlinepage}})
339                                 if exists $items{inlinepage};
340                         $renderedfiles{$page}=$items{dest}[0];
341                 }
342                 $pagectime{$page}=$items{ctime}[0];
343         }
344         close IN;
345 } #}}}
346
347 sub saveindex () { #{{{
348         if (! -d $config{wikistatedir}) {
349                 mkdir($config{wikistatedir});
350         }
351         open (OUT, ">$config{wikistatedir}/index") || 
352                 error("cannot write to $config{wikistatedir}/index: $!");
353         foreach my $page (keys %oldpagemtime) {
354                 next unless $oldpagemtime{$page};
355                 my $line="mtime=$oldpagemtime{$page} ".
356                         "ctime=$pagectime{$page} ".
357                         "src=$pagesources{$page} ".
358                         "dest=$renderedfiles{$page}";
359                 $line.=" link=$_" foreach @{$links{$page}};
360                 if (exists $inlinepages{$page}) {
361                         $line.=" inlinepage=$_" foreach split " ", $inlinepages{$page};
362                 }
363                 print OUT $line."\n";
364         }
365         close OUT;
366 } #}}}
367
368 sub misctemplate ($$) { #{{{
369         my $title=shift;
370         my $pagebody=shift;
371         
372         my $template=HTML::Template->new(
373                 filename => "$config{templatedir}/misc.tmpl"
374         );
375         $template->param(
376                 title => $title,
377                 indexlink => indexlink(),
378                 wikiname => $config{wikiname},
379                 pagebody => $pagebody,
380         );
381         return $template->output;
382 }#}}}
383
384 sub userinfo_get ($$) { #{{{
385         my $user=shift;
386         my $field=shift;
387
388         eval q{use Storable};
389         my $userdata=eval{ Storable::lock_retrieve("$config{wikistatedir}/userdb") };
390         if (! defined $userdata || ! ref $userdata || 
391             ! exists $userdata->{$user} || ! ref $userdata->{$user} ||
392             ! exists $userdata->{$user}->{$field}) {
393                 return "";
394         }
395         return $userdata->{$user}->{$field};
396 } #}}}
397
398 sub userinfo_set ($$$) { #{{{
399         my $user=shift;
400         my $field=shift;
401         my $value=shift;
402         
403         eval q{use Storable};
404         my $userdata=eval{ Storable::lock_retrieve("$config{wikistatedir}/userdb") };
405         if (! defined $userdata || ! ref $userdata || 
406             ! exists $userdata->{$user} || ! ref $userdata->{$user}) {
407                 return "";
408         }
409         
410         $userdata->{$user}->{$field}=$value;
411         my $oldmask=umask(077);
412         my $ret=Storable::lock_store($userdata, "$config{wikistatedir}/userdb");
413         umask($oldmask);
414         return $ret;
415 } #}}}
416
417 sub userinfo_setall ($$) { #{{{
418         my $user=shift;
419         my $info=shift;
420         
421         eval q{use Storable};
422         my $userdata=eval{ Storable::lock_retrieve("$config{wikistatedir}/userdb") };
423         if (! defined $userdata || ! ref $userdata) {
424                 $userdata={};
425         }
426         $userdata->{$user}=$info;
427         my $oldmask=umask(077);
428         my $ret=Storable::lock_store($userdata, "$config{wikistatedir}/userdb");
429         umask($oldmask);
430         return $ret;
431 } #}}}
432
433 sub is_admin ($) { #{{{
434         my $user_name=shift;
435
436         return grep { $_ eq $user_name } @{$config{adminuser}};
437 } #}}}
438
439 sub glob_match ($$) { #{{{
440         my $page=shift;
441         my $glob=shift;
442
443         # turn glob into safe regexp
444         $glob=quotemeta($glob);
445         $glob=~s/\\\*/.*/g;
446         $glob=~s/\\\?/./g;
447         $glob=~s!\\/!/!g;
448         
449         $page=~/^$glob$/i;
450 } #}}}
451
452 sub globlist_match ($$) { #{{{
453         my $page=shift;
454         my @globlist=split(" ", shift);
455
456         # check any negated globs first
457         foreach my $glob (@globlist) {
458                 return 0 if $glob=~/^!(.*)/ && glob_match($page, $1);
459         }
460
461         foreach my $glob (@globlist) {
462                 return 1 if glob_match($page, $glob);
463         }
464         
465         return 0;
466 } #}}}
467
468 sub main () { #{{{
469         getconfig();
470         
471         if ($config{cgi}) {
472                 lockwiki();
473                 loadindex();
474                 require IkiWiki::CGI;
475                 cgi();
476         }
477         elsif ($config{setup}) {
478                 require IkiWiki::Setup;
479                 setup();
480         }
481         elsif ($config{wrapper}) {
482                 lockwiki();
483                 require IkiWiki::Wrapper;
484                 gen_wrapper();
485         }
486         else {
487                 lockwiki();
488                 loadindex();
489                 require IkiWiki::Render;
490                 rcs_update();
491                 rcs_getctime() if $config{getctime};
492                 refresh();
493                 saveindex();
494         }
495 } #}}}
496
497 main;