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