banned_users move to setup file, stage 1
[ikiwiki] / IkiWiki.pm
1 #!/usr/bin/perl
2
3 package IkiWiki;
4
5 use warnings;
6 use strict;
7 use Encode;
8 use HTML::Entities;
9 use URI::Escape q{uri_escape_utf8};
10 use POSIX;
11 use Storable;
12 use open qw{:utf8 :std};
13
14 use vars qw{%config %links %oldlinks %pagemtime %pagectime %pagecase
15             %pagestate %renderedfiles %oldrenderedfiles %pagesources
16             %destsources %depends %hooks %forcerebuild $gettext_obj};
17
18 use Exporter q{import};
19 our @EXPORT = qw(hook debug error template htmlpage add_depends pagespec_match
20                  bestlink htmllink readfile writefile pagetype srcfile pagename
21                  displaytime will_render gettext urlto targetpage
22                  add_underlay
23                  %config %links %pagestate %renderedfiles
24                  %pagesources %destsources);
25 our $VERSION = 2.00; # plugin interface version, next is ikiwiki version
26 our $version='unknown'; # VERSION_AUTOREPLACE done by Makefile, DNE
27 my $installdir=''; # INSTALLDIR_AUTOREPLACE done by Makefile, DNE
28
29 # Optimisation.
30 use Memoize;
31 memoize("abs2rel");
32 memoize("pagespec_translate");
33 memoize("file_pruned");
34
35 sub getsetup () { #{{{
36         wikiname => {
37                 type => "string",
38                 default => "wiki",
39                 description => "name of the wiki",
40                 safe => 1,
41                 rebuild => 1,
42         },
43         srcdir => {
44                 type => "string",
45                 default => undef,
46                 example => "$ENV{HOME}/wiki",
47                 description => "where the source of the wiki is located",
48                 safe => 0, # path
49                 rebuild => 1,
50         },
51         destdir => {
52                 type => "string",
53                 default => undef,
54                 example => "/var/www/wiki",
55                 description => "where to build the wiki",
56                 safe => 0, # path
57                 rebuild => 1,
58         },
59         adminuser => {
60                 type => "string",
61                 default => [],
62                 description => "user names of wiki admins",
63                 safe => 1,
64                 rebuild => 0,
65         },
66         adminemail => {
67                 type => "string",
68                 default => undef,
69                 example => 'me@example.com',
70                 description => "contact email for wiki",
71                 safe => 1,
72                 rebuild => 0,
73         },
74         url => {
75                 type => "string",
76                 default => '',
77                 example => "http://example.com/wiki",
78                 description => "base url to the wiki",
79                 safe => 1,
80                 rebuild => 1,
81         },
82         cgiurl => {
83                 type => "string",
84                 default => '',
85                 example => "http://example.com/wiki/ikiwiki.cgi",
86                 description => "url to the ikiwiki.cgi",
87                 safe => 1,
88                 rebuild => 1,
89         },
90         cgi_wrapper => {
91                 type => "string",
92                 default => '',
93                 example => "/var/www/wiki/ikiwiki.cgi",
94                 description => "cgi executable to generate",
95                 safe => 0, # file
96                 rebuild => 0,
97         },
98         cgi_wrappermode => {
99                 type => "string",
100                 default => '06755',
101                 description => "mode for cgi_wrapper (can safely be made suid)",
102                 safe => 0,
103                 rebuild => 0,
104         },
105         rcs => {
106                 type => "string",
107                 default => '',
108                 description => "rcs backend to use",
109                 safe => 0, # don't allow overriding
110                 rebuild => 0,
111         },
112         default_plugins => {
113                 type => "internal",
114                 default => [qw{mdwn link inline htmlscrubber passwordauth
115                                 openid signinedit lockedit conditional
116                                 recentchanges parentlinks}],
117                 description => "plugins to enable by default",
118                 safe => 0,
119                 rebuild => 1,
120         },
121         add_plugins => {
122                 type => "string",
123                 default => [],
124                 description => "plugins to add to the default configuration",
125                 safe => 1,
126                 rebuild => 1,
127         },
128         disable_plugins => {
129                 type => "string",
130                 default => [],
131                 description => "plugins to disable",
132                 safe => 1,
133                 rebuild => 1,
134         },
135         templatedir => {
136                 type => "string",
137                 default => "$installdir/share/ikiwiki/templates",
138                 description => "location of template files",
139                 safe => 0, # path
140                 rebuild => 1,
141         },
142         underlaydir => {
143                 type => "string",
144                 default => "$installdir/share/ikiwiki/basewiki",
145                 description => "base wiki source location",
146                 safe => 0, # path
147                 rebuild => 0,
148         },
149         wrappers => {
150                 type => "internal",
151                 default => [],
152                 description => "wrappers to generate",
153                 safe => 0,
154                 rebuild => 0,
155         },
156         underlaydirs => {
157                 type => "internal",
158                 default => [],
159                 description => "additional underlays to use",
160                 safe => 0,
161                 rebuild => 0,
162         },
163         verbose => {
164                 type => "boolean",
165                 default => 0,
166                 description => "display verbose messages when building?",
167                 safe => 1,
168                 rebuild => 0,
169         },
170         syslog => {
171                 type => "boolean",
172                 default => 0,
173                 description => "log to syslog?",
174                 safe => 1,
175                 rebuild => 0,
176         },
177         usedirs => {
178                 type => "boolean",
179                 default => 1,
180                 description => "create output files named page/index.html?",
181                 safe => 0, # changing requires manual transition
182                 rebuild => 1,
183         },
184         prefix_directives => {
185                 type => "boolean",
186                 default => 0,
187                 description => "use '!'-prefixed preprocessor directives?",
188                 safe => 0, # changing requires manual transition
189                 rebuild => 1,
190         },
191         discussion => {
192                 type => "boolean",
193                 default => 1,
194                 description => "enable Discussion pages?",
195                 safe => 1,
196                 rebuild => 1,
197         },
198         default_pageext => {
199                 type => "string",
200                 default => "mdwn",
201                 description => "extension to use for new pages",
202                 safe => 0, # not sanitized
203                 rebuild => 0,
204         },
205         htmlext => {
206                 type => "string",
207                 default => "html",
208                 description => "extension to use for html files",
209                 safe => 0, # not sanitized
210                 rebuild => 1,
211         },
212         timeformat => {
213                 type => "string",
214                 default => '%c',
215                 description => "strftime format string to display date",
216                 safe => 1,
217                 rebuild => 1,
218         },
219         locale => {
220                 type => "string",
221                 default => undef,
222                 example => "en_US.UTF-8",
223                 description => "UTF-8 locale to use",
224                 safe => 0,
225                 rebuild => 1,
226         },
227         sslcookie => {
228                 type => "boolean",
229                 default => 0,
230                 description => "only send cookies over SSL connections?",
231                 safe => 1,
232                 rebuild => 0,
233         },
234         userdir => {
235                 type => "string",
236                 default => "",
237                 example => "users",
238                 description => "put user pages below specified page",
239                 safe => 1,
240                 rebuild => 1,
241         },
242         numbacklinks => {
243                 type => "integer",
244                 default => 10,
245                 description => "how many backlinks to show before hiding excess (0 to show all)",
246                 safe => 1,
247                 rebuild => 1,
248         },
249         hardlink => {
250                 type => "boolean",
251                 default => 0,
252                 description => "attempt to hardlink source files? (optimisation for large files)",
253                 safe => 0, # paranoia
254                 rebuild => 0,
255         },
256         umask => {
257                 type => "integer",
258                 description => "",
259                 example => "022",
260                 description => "force ikiwiki to use a particular umask",
261                 safe => 0, # paranoia
262                 rebuild => 0,
263         },
264         libdir => {
265                 type => "string",
266                 default => "",
267                 example => "$ENV{HOME}/.ikiwiki/",
268                 description => "extra library and plugin directory",
269                 safe => 0, # directory
270                 rebuild => 0,
271         },
272         ENV => {
273                 type => "string", 
274                 default => {},
275                 description => "environment variables",
276                 safe => 0, # paranoia
277                 rebuild => 0,
278         },
279         exclude => {
280                 type => "string",
281                 default => undef,
282                 example => '\.wav$',
283                 description => "regexp of source files to ignore",
284                 safe => 0, # regexp
285                 rebuild => 1,
286         },
287         banned_users => {
288                 type => "string",
289                 default => [],
290                 description => "users who cannot use the wiki",
291                 safe => 1,
292                 rebuild => 0,
293         },
294         wiki_file_prune_regexps => {
295                 type => "internal",
296                 default => [qr/(^|\/)\.\.(\/|$)/, qr/^\./, qr/\/\./,
297                         qr/\.x?html?$/, qr/\.ikiwiki-new$/,
298                         qr/(^|\/).svn\//, qr/.arch-ids\//, qr/{arch}\//,
299                         qr/(^|\/)_MTN\//,
300                         qr/\.dpkg-tmp$/],
301                 description => "regexps of source files to ignore",
302                 safe => 0,
303                 rebuild => 1,
304         },
305         wiki_file_regexp => {
306                 type => "internal",
307                 default => qr/(^[-[:alnum:]_.:\/+]+$)/,
308                 description => "regexp of legal source files",
309                 safe => 0,
310                 rebuild => 1,
311         },
312         web_commit_regexp => {
313                 type => "internal",
314                 default => qr/^web commit (by (.*?(?=: |$))|from (\d+\.\d+\.\d+\.\d+)):?(.*)/,
315                 description => "regexp to parse web commits from logs",
316                 safe => 0,
317                 rebuild => 0,
318         },
319         cgi => {
320                 type => "internal",
321                 default => 0,
322                 description => "run as a cgi",
323                 safe => 0,
324                 rebuild => 0,
325         },
326         cgi_disable_uploads => {
327                 type => "internal",
328                 default => 1,
329                 description => "whether CGI should accept file uploads",
330                 safe => 0,
331                 rebuild => 0,
332         },
333         post_commit => {
334                 type => "internal",
335                 default => 0,
336                 description => "run as a post-commit hook",
337                 safe => 0,
338                 rebuild => 0,
339         },
340         rebuild => {
341                 type => "internal",
342                 default => 0,
343                 description => "running in rebuild mode",
344                 safe => 0,
345                 rebuild => 0,
346         },
347         refresh => {
348                 type => "internal",
349                 default => 0,
350                 description => "running in refresh mode",
351                 safe => 0,
352                 rebuild => 0,
353         },
354         getctime => {
355                 type => "internal",
356                 default => 0,
357                 description => "running in getctime mode",
358                 safe => 0,
359                 rebuild => 0,
360         },
361         w3mmode => {
362                 type => "internal",
363                 default => 0,
364                 description => "running in w3mmode",
365                 safe => 0,
366                 rebuild => 0,
367         },
368         setup => {
369                 type => "internal",
370                 default => undef,
371                 description => "setup file to read",
372                 safe => 0,
373                 rebuild => 0,
374         },
375 } #}}}
376
377 sub defaultconfig () { #{{{
378         my %s=getsetup();
379         my @ret;
380         foreach my $key (keys %s) {
381                 push @ret, $key, $s{$key}->{default};
382         }
383         use Data::Dumper;
384         return @ret;
385 } #}}}
386
387 sub checkconfig () { #{{{
388         # locale stuff; avoid LC_ALL since it overrides everything
389         if (defined $ENV{LC_ALL}) {
390                 $ENV{LANG} = $ENV{LC_ALL};
391                 delete $ENV{LC_ALL};
392         }
393         if (defined $config{locale}) {
394                 if (POSIX::setlocale(&POSIX::LC_ALL, $config{locale})) {
395                         $ENV{LANG}=$config{locale};
396                         $gettext_obj=undef;
397                 }
398         }
399
400         if (ref $config{ENV} eq 'HASH') {
401                 foreach my $val (keys %{$config{ENV}}) {
402                         $ENV{$val}=$config{ENV}{$val};
403                 }
404         }
405
406         if ($config{w3mmode}) {
407                 eval q{use Cwd q{abs_path}};
408                 error($@) if $@;
409                 $config{srcdir}=possibly_foolish_untaint(abs_path($config{srcdir}));
410                 $config{destdir}=possibly_foolish_untaint(abs_path($config{destdir}));
411                 $config{cgiurl}="file:///\$LIB/ikiwiki-w3m.cgi/".$config{cgiurl}
412                         unless $config{cgiurl} =~ m!file:///!;
413                 $config{url}="file://".$config{destdir};
414         }
415
416         if ($config{cgi} && ! length $config{url}) {
417                 error(gettext("Must specify url to wiki with --url when using --cgi"));
418         }
419         
420         $config{wikistatedir}="$config{srcdir}/.ikiwiki"
421                 unless exists $config{wikistatedir};
422
423         if (defined $config{umask}) {
424                 umask(possibly_foolish_untaint($config{umask}));
425         }
426
427         run_hooks(checkconfig => sub { shift->() });
428
429         return 1;
430 } #}}}
431
432 sub listplugins () { #{{{
433         my %ret;
434
435         foreach my $dir (@INC, $config{libdir}) {
436                 next unless defined $dir && length $dir;
437                 foreach my $file (glob("$dir/IkiWiki/Plugin/*.pm")) {
438                         my ($plugin)=$file=~/.*\/(.*)\.pm$/;
439                         $ret{$plugin}=1;
440                 }
441         }
442         foreach my $dir ($config{libdir}, "$installdir/lib/ikiwiki") {
443                 next unless defined $dir && length $dir;
444                 foreach my $file (glob("$dir/plugins/*")) {
445                         $ret{basename($file)}=1 if -x $file;
446                 }
447         }
448
449         return keys %ret;
450 } #}}}
451
452 sub loadplugins () { #{{{
453         if (defined $config{libdir} && length $config{libdir}) {
454                 unshift @INC, possibly_foolish_untaint($config{libdir});
455         }
456
457         loadplugin($_) foreach @{$config{default_plugins}}, @{$config{add_plugins}};
458         
459         if ($config{rcs}) {
460                 if (exists $IkiWiki::hooks{rcs}) {
461                         error(gettext("cannot use multiple rcs plugins"));
462                 }
463                 loadplugin($config{rcs});
464         }
465         if (! exists $IkiWiki::hooks{rcs}) {
466                 loadplugin("norcs");
467         }
468
469         run_hooks(getopt => sub { shift->() });
470         if (grep /^-/, @ARGV) {
471                 print STDERR "Unknown option: $_\n"
472                         foreach grep /^-/, @ARGV;
473                 usage();
474         }
475
476         return 1;
477 } #}}}
478
479 sub loadplugin ($) { #{{{
480         my $plugin=shift;
481
482         return if grep { $_ eq $plugin} @{$config{disable_plugins}};
483
484         foreach my $dir (defined $config{libdir} ? possibly_foolish_untaint($config{libdir}) : undef,
485                          "$installdir/lib/ikiwiki") {
486                 if (defined $dir && -x "$dir/plugins/$plugin") {
487                         require IkiWiki::Plugin::external;
488                         import IkiWiki::Plugin::external "$dir/plugins/$plugin";
489                         return 1;
490                 }
491         }
492
493         my $mod="IkiWiki::Plugin::".possibly_foolish_untaint($plugin);
494         eval qq{use $mod};
495         if ($@) {
496                 error("Failed to load plugin $mod: $@");
497         }
498         return 1;
499 } #}}}
500
501 sub error ($;$) { #{{{
502         my $message=shift;
503         my $cleaner=shift;
504         log_message('err' => $message) if $config{syslog};
505         if (defined $cleaner) {
506                 $cleaner->();
507         }
508         die $message."\n";
509 } #}}}
510
511 sub debug ($) { #{{{
512         return unless $config{verbose};
513         return log_message(debug => @_);
514 } #}}}
515
516 my $log_open=0;
517 sub log_message ($$) { #{{{
518         my $type=shift;
519
520         if ($config{syslog}) {
521                 require Sys::Syslog;
522                 if (! $log_open) {
523                         Sys::Syslog::setlogsock('unix');
524                         Sys::Syslog::openlog('ikiwiki', '', 'user');
525                         $log_open=1;
526                 }
527                 return eval {
528                         Sys::Syslog::syslog($type, "[$config{wikiname}] %s", join(" ", @_));
529                 };
530         }
531         elsif (! $config{cgi}) {
532                 return print "@_\n";
533         }
534         else {
535                 return print STDERR "@_\n";
536         }
537 } #}}}
538
539 sub possibly_foolish_untaint ($) { #{{{
540         my $tainted=shift;
541         my ($untainted)=$tainted=~/(.*)/s;
542         return $untainted;
543 } #}}}
544
545 sub basename ($) { #{{{
546         my $file=shift;
547
548         $file=~s!.*/+!!;
549         return $file;
550 } #}}}
551
552 sub dirname ($) { #{{{
553         my $file=shift;
554
555         $file=~s!/*[^/]+$!!;
556         return $file;
557 } #}}}
558
559 sub pagetype ($) { #{{{
560         my $page=shift;
561         
562         if ($page =~ /\.([^.]+)$/) {
563                 return $1 if exists $hooks{htmlize}{$1};
564         }
565         return;
566 } #}}}
567
568 sub isinternal ($) { #{{{
569         my $page=shift;
570         return exists $pagesources{$page} &&
571                 $pagesources{$page} =~ /\._([^.]+)$/;
572 } #}}}
573
574 sub pagename ($) { #{{{
575         my $file=shift;
576
577         my $type=pagetype($file);
578         my $page=$file;
579         $page=~s/\Q.$type\E*$// if defined $type;
580         return $page;
581 } #}}}
582
583 sub targetpage ($$) { #{{{
584         my $page=shift;
585         my $ext=shift;
586         
587         if (! $config{usedirs} || $page =~ /^index$/ ) {
588                 return $page.".".$ext;
589         } else {
590                 return $page."/index.".$ext;
591         }
592 } #}}}
593
594 sub htmlpage ($) { #{{{
595         my $page=shift;
596         
597         return targetpage($page, $config{htmlext});
598 } #}}}
599
600 sub srcfile_stat { #{{{
601         my $file=shift;
602         my $nothrow=shift;
603
604         return "$config{srcdir}/$file", stat(_) if -e "$config{srcdir}/$file";
605         foreach my $dir (@{$config{underlaydirs}}, $config{underlaydir}) {
606                 return "$dir/$file", stat(_) if -e "$dir/$file";
607         }
608         error("internal error: $file cannot be found in $config{srcdir} or underlay") unless $nothrow;
609         return;
610 } #}}}
611
612 sub srcfile ($;$) { #{{{
613         return (srcfile_stat(@_))[0];
614 } #}}}
615
616 sub add_underlay ($) { #{{{
617         my $dir=shift;
618
619         if ($dir=~/^\//) {
620                 unshift @{$config{underlaydirs}}, $dir;
621         }
622         else {
623                 unshift @{$config{underlaydirs}}, "$config{underlaydir}/../$dir";
624         }
625
626         return 1;
627 } #}}}
628
629 sub readfile ($;$$) { #{{{
630         my $file=shift;
631         my $binary=shift;
632         my $wantfd=shift;
633
634         if (-l $file) {
635                 error("cannot read a symlink ($file)");
636         }
637         
638         local $/=undef;
639         open (my $in, "<", $file) || error("failed to read $file: $!");
640         binmode($in) if ($binary);
641         return \*$in if $wantfd;
642         my $ret=<$in>;
643         close $in || error("failed to read $file: $!");
644         return $ret;
645 } #}}}
646
647 sub prep_writefile ($$) { #{{{
648         my $file=shift;
649         my $destdir=shift;
650         
651         my $test=$file;
652         while (length $test) {
653                 if (-l "$destdir/$test") {
654                         error("cannot write to a symlink ($test)");
655                 }
656                 $test=dirname($test);
657         }
658
659         my $dir=dirname("$destdir/$file");
660         if (! -d $dir) {
661                 my $d="";
662                 foreach my $s (split(m!/+!, $dir)) {
663                         $d.="$s/";
664                         if (! -d $d) {
665                                 mkdir($d) || error("failed to create directory $d: $!");
666                         }
667                 }
668         }
669
670         return 1;
671 } #}}}
672
673 sub writefile ($$$;$$) { #{{{
674         my $file=shift; # can include subdirs
675         my $destdir=shift; # directory to put file in
676         my $content=shift;
677         my $binary=shift;
678         my $writer=shift;
679         
680         prep_writefile($file, $destdir);
681         
682         my $newfile="$destdir/$file.ikiwiki-new";
683         if (-l $newfile) {
684                 error("cannot write to a symlink ($newfile)");
685         }
686         
687         my $cleanup = sub { unlink($newfile) };
688         open (my $out, '>', $newfile) || error("failed to write $newfile: $!", $cleanup);
689         binmode($out) if ($binary);
690         if ($writer) {
691                 $writer->(\*$out, $cleanup);
692         }
693         else {
694                 print $out $content or error("failed writing to $newfile: $!", $cleanup);
695         }
696         close $out || error("failed saving $newfile: $!", $cleanup);
697         rename($newfile, "$destdir/$file") || 
698                 error("failed renaming $newfile to $destdir/$file: $!", $cleanup);
699
700         return 1;
701 } #}}}
702
703 my %cleared;
704 sub will_render ($$;$) { #{{{
705         my $page=shift;
706         my $dest=shift;
707         my $clear=shift;
708
709         # Important security check.
710         if (-e "$config{destdir}/$dest" && ! $config{rebuild} &&
711             ! grep { $_ eq $dest } (@{$renderedfiles{$page}}, @{$oldrenderedfiles{$page}})) {
712                 error("$config{destdir}/$dest independently created, not overwriting with version from $page");
713         }
714
715         if (! $clear || $cleared{$page}) {
716                 $renderedfiles{$page}=[$dest, grep { $_ ne $dest } @{$renderedfiles{$page}}];
717         }
718         else {
719                 foreach my $old (@{$renderedfiles{$page}}) {
720                         delete $destsources{$old};
721                 }
722                 $renderedfiles{$page}=[$dest];
723                 $cleared{$page}=1;
724         }
725         $destsources{$dest}=$page;
726
727         return 1;
728 } #}}}
729
730 sub bestlink ($$) { #{{{
731         my $page=shift;
732         my $link=shift;
733         
734         my $cwd=$page;
735         if ($link=~s/^\/+//) {
736                 # absolute links
737                 $cwd="";
738         }
739         $link=~s/\/$//;
740
741         do {
742                 my $l=$cwd;
743                 $l.="/" if length $l;
744                 $l.=$link;
745
746                 if (exists $links{$l}) {
747                         return $l;
748                 }
749                 elsif (exists $pagecase{lc $l}) {
750                         return $pagecase{lc $l};
751                 }
752         } while $cwd=~s!/?[^/]+$!!;
753
754         if (length $config{userdir}) {
755                 my $l = "$config{userdir}/".lc($link);
756                 if (exists $links{$l}) {
757                         return $l;
758                 }
759                 elsif (exists $pagecase{lc $l}) {
760                         return $pagecase{lc $l};
761                 }
762         }
763
764         #print STDERR "warning: page $page, broken link: $link\n";
765         return "";
766 } #}}}
767
768 sub isinlinableimage ($) { #{{{
769         my $file=shift;
770         
771         return $file =~ /\.(png|gif|jpg|jpeg)$/i;
772 } #}}}
773
774 sub pagetitle ($;$) { #{{{
775         my $page=shift;
776         my $unescaped=shift;
777
778         if ($unescaped) {
779                 $page=~s/(__(\d+)__|_)/$1 eq '_' ? ' ' : chr($2)/eg;
780         }
781         else {
782                 $page=~s/(__(\d+)__|_)/$1 eq '_' ? ' ' : "&#$2;"/eg;
783         }
784
785         return $page;
786 } #}}}
787
788 sub titlepage ($) { #{{{
789         my $title=shift;
790         $title=~s/([^-[:alnum:]:+\/.])/$1 eq ' ' ? '_' : "__".ord($1)."__"/eg;
791         return $title;
792 } #}}}
793
794 sub linkpage ($) { #{{{
795         my $link=shift;
796         $link=~s/([^-[:alnum:]:+\/._])/$1 eq ' ' ? '_' : "__".ord($1)."__"/eg;
797         return $link;
798 } #}}}
799
800 sub cgiurl (@) { #{{{
801         my %params=@_;
802
803         return $config{cgiurl}."?".
804                 join("&amp;", map $_."=".uri_escape_utf8($params{$_}), keys %params);
805 } #}}}
806
807 sub baseurl (;$) { #{{{
808         my $page=shift;
809
810         return "$config{url}/" if ! defined $page;
811         
812         $page=htmlpage($page);
813         $page=~s/[^\/]+$//;
814         $page=~s/[^\/]+\//..\//g;
815         return $page;
816 } #}}}
817
818 sub abs2rel ($$) { #{{{
819         # Work around very innefficient behavior in File::Spec if abs2rel
820         # is passed two relative paths. It's much faster if paths are
821         # absolute! (Debian bug #376658; fixed in debian unstable now)
822         my $path="/".shift;
823         my $base="/".shift;
824
825         require File::Spec;
826         my $ret=File::Spec->abs2rel($path, $base);
827         $ret=~s/^// if defined $ret;
828         return $ret;
829 } #}}}
830
831 sub displaytime ($;$) { #{{{
832         my $time=shift;
833         my $format=shift;
834         if (! defined $format) {
835                 $format=$config{timeformat};
836         }
837
838         # strftime doesn't know about encodings, so make sure
839         # its output is properly treated as utf8
840         return decode_utf8(POSIX::strftime($format, localtime($time)));
841 } #}}}
842
843 sub beautify_urlpath ($) { #{{{
844         my $url=shift;
845
846         if ($config{usedirs}) {
847                 $url =~ s!/index.$config{htmlext}$!/!;
848         }
849
850         # Ensure url is not an empty link, and
851         # if it's relative, make that explicit to avoid colon confusion.
852         if ($url !~ /^\//) {
853                 $url="./$url";
854         }
855
856         return $url;
857 } #}}}
858
859 sub urlto ($$;$) { #{{{
860         my $to=shift;
861         my $from=shift;
862         my $absolute=shift;
863         
864         if (! length $to) {
865                 return beautify_urlpath(baseurl($from)."index.$config{htmlext}");
866         }
867
868         if (! $destsources{$to}) {
869                 $to=htmlpage($to);
870         }
871
872         if ($absolute) {
873                 return $config{url}.beautify_urlpath("/".$to);
874         }
875
876         my $link = abs2rel($to, dirname(htmlpage($from)));
877
878         return beautify_urlpath($link);
879 } #}}}
880
881 sub htmllink ($$$;@) { #{{{
882         my $lpage=shift; # the page doing the linking
883         my $page=shift; # the page that will contain the link (different for inline)
884         my $link=shift;
885         my %opts=@_;
886
887         $link=~s/\/$//;
888
889         my $bestlink;
890         if (! $opts{forcesubpage}) {
891                 $bestlink=bestlink($lpage, $link);
892         }
893         else {
894                 $bestlink="$lpage/".lc($link);
895         }
896
897         my $linktext;
898         if (defined $opts{linktext}) {
899                 $linktext=$opts{linktext};
900         }
901         else {
902                 $linktext=pagetitle(basename($link));
903         }
904         
905         return "<span class=\"selflink\">$linktext</span>"
906                 if length $bestlink && $page eq $bestlink &&
907                    ! defined $opts{anchor};
908         
909         if (! $destsources{$bestlink}) {
910                 $bestlink=htmlpage($bestlink);
911
912                 if (! $destsources{$bestlink}) {
913                         return $linktext unless length $config{cgiurl};
914                         return "<span class=\"createlink\"><a href=\"".
915                                 cgiurl(
916                                         do => "create",
917                                         page => lc($link),
918                                         from => $lpage
919                                 ).
920                                 "\" rel=\"nofollow\">?</a>$linktext</span>"
921                 }
922         }
923         
924         $bestlink=abs2rel($bestlink, dirname(htmlpage($page)));
925         $bestlink=beautify_urlpath($bestlink);
926         
927         if (! $opts{noimageinline} && isinlinableimage($bestlink)) {
928                 return "<img src=\"$bestlink\" alt=\"$linktext\" />";
929         }
930
931         if (defined $opts{anchor}) {
932                 $bestlink.="#".$opts{anchor};
933         }
934
935         my @attrs;
936         if (defined $opts{rel}) {
937                 push @attrs, ' rel="'.$opts{rel}.'"';
938         }
939         if (defined $opts{class}) {
940                 push @attrs, ' class="'.$opts{class}.'"';
941         }
942
943         return "<a href=\"$bestlink\"@attrs>$linktext</a>";
944 } #}}}
945
946 sub userlink ($) { #{{{
947         my $user=shift;
948
949         my $oiduser=eval { openiduser($user) };
950         if (defined $oiduser) {
951                 return "<a href=\"$user\">$oiduser</a>";
952         }
953         else {
954                 eval q{use CGI 'escapeHTML'};
955                 error($@) if $@;
956
957                 return htmllink("", "", escapeHTML(
958                         length $config{userdir} ? $config{userdir}."/".$user : $user
959                 ), noimageinline => 1);
960         }
961 } #}}}
962
963 sub htmlize ($$$$) { #{{{
964         my $page=shift;
965         my $destpage=shift;
966         my $type=shift;
967         my $content=shift;
968         
969         my $oneline = $content !~ /\n/;
970
971         if (exists $hooks{htmlize}{$type}) {
972                 $content=$hooks{htmlize}{$type}{call}->(
973                         page => $page,
974                         content => $content,
975                 );
976         }
977         else {
978                 error("htmlization of $type not supported");
979         }
980
981         run_hooks(sanitize => sub {
982                 $content=shift->(
983                         page => $page,
984                         destpage => $destpage,
985                         content => $content,
986                 );
987         });
988         
989         if ($oneline) {
990                 # hack to get rid of enclosing junk added by markdown
991                 # and other htmlizers
992                 $content=~s/^<p>//i;
993                 $content=~s/<\/p>$//i;
994                 chomp $content;
995         }
996
997         return $content;
998 } #}}}
999
1000 sub linkify ($$$) { #{{{
1001         my $page=shift;
1002         my $destpage=shift;
1003         my $content=shift;
1004
1005         run_hooks(linkify => sub {
1006                 $content=shift->(
1007                         page => $page,
1008                         destpage => $destpage,
1009                         content => $content,
1010                 );
1011         });
1012         
1013         return $content;
1014 } #}}}
1015
1016 our %preprocessing;
1017 our $preprocess_preview=0;
1018 sub preprocess ($$$;$$) { #{{{
1019         my $page=shift; # the page the data comes from
1020         my $destpage=shift; # the page the data will appear in (different for inline)
1021         my $content=shift;
1022         my $scan=shift;
1023         my $preview=shift;
1024
1025         # Using local because it needs to be set within any nested calls
1026         # of this function.
1027         local $preprocess_preview=$preview if defined $preview;
1028
1029         my $handle=sub {
1030                 my $escape=shift;
1031                 my $prefix=shift;
1032                 my $command=shift;
1033                 my $params=shift;
1034                 if (length $escape) {
1035                         return "[[$prefix$command $params]]";
1036                 }
1037                 elsif (exists $hooks{preprocess}{$command}) {
1038                         return "" if $scan && ! $hooks{preprocess}{$command}{scan};
1039                         # Note: preserve order of params, some plugins may
1040                         # consider it significant.
1041                         my @params;
1042                         while ($params =~ m{
1043                                 (?:([-\w]+)=)?          # 1: named parameter key?
1044                                 (?:
1045                                         """(.*?)"""     # 2: triple-quoted value
1046                                 |
1047                                         "([^"]+)"       # 3: single-quoted value
1048                                 |
1049                                         (\S+)           # 4: unquoted value
1050                                 )
1051                                 (?:\s+|$)               # delimiter to next param
1052                         }sgx) {
1053                                 my $key=$1;
1054                                 my $val;
1055                                 if (defined $2) {
1056                                         $val=$2;
1057                                         $val=~s/\r\n/\n/mg;
1058                                         $val=~s/^\n+//g;
1059                                         $val=~s/\n+$//g;
1060                                 }
1061                                 elsif (defined $3) {
1062                                         $val=$3;
1063                                 }
1064                                 elsif (defined $4) {
1065                                         $val=$4;
1066                                 }
1067
1068                                 if (defined $key) {
1069                                         push @params, $key, $val;
1070                                 }
1071                                 else {
1072                                         push @params, $val, '';
1073                                 }
1074                         }
1075                         if ($preprocessing{$page}++ > 3) {
1076                                 # Avoid loops of preprocessed pages preprocessing
1077                                 # other pages that preprocess them, etc.
1078                                 return "[[!$command <span class=\"error\">".
1079                                         sprintf(gettext("preprocessing loop detected on %s at depth %i"),
1080                                                 $page, $preprocessing{$page}).
1081                                         "</span>]]";
1082                         }
1083                         my $ret;
1084                         if (! $scan) {
1085                                 $ret=eval {
1086                                         $hooks{preprocess}{$command}{call}->(
1087                                                 @params,
1088                                                 page => $page,
1089                                                 destpage => $destpage,
1090                                                 preview => $preprocess_preview,
1091                                         );
1092                                 };
1093                                 if ($@) {
1094                                         chomp $@;
1095                                         $ret="[[!$command <span class=\"error\">".
1096                                                 gettext("Error").": $@"."</span>]]";
1097                                 }
1098                         }
1099                         else {
1100                                 # use void context during scan pass
1101                                 eval {
1102                                         $hooks{preprocess}{$command}{call}->(
1103                                                 @params,
1104                                                 page => $page,
1105                                                 destpage => $destpage,
1106                                                 preview => $preprocess_preview,
1107                                         );
1108                                 };
1109                                 $ret="";
1110                         }
1111                         $preprocessing{$page}--;
1112                         return $ret;
1113                 }
1114                 else {
1115                         return "[[$prefix$command $params]]";
1116                 }
1117         };
1118         
1119         my $regex;
1120         if ($config{prefix_directives}) {
1121                 $regex = qr{
1122                         (\\?)           # 1: escape?
1123                         \[\[(!)         # directive open; 2: prefix
1124                         ([-\w]+)        # 3: command
1125                         (               # 4: the parameters..
1126                                 \s+     # Must have space if parameters present
1127                                 (?:
1128                                         (?:[-\w]+=)?            # named parameter key?
1129                                         (?:
1130                                                 """.*?"""       # triple-quoted value
1131                                                 |
1132                                                 "[^"]+"         # single-quoted value
1133                                                 |
1134                                                 [^\s\]]+        # unquoted value
1135                                         )
1136                                         \s*                     # whitespace or end
1137                                                                 # of directive
1138                                 )
1139                         *)?             # 0 or more parameters
1140                         \]\]            # directive closed
1141                 }sx;
1142         }
1143         else {
1144                 $regex = qr{
1145                         (\\?)           # 1: escape?
1146                         \[\[(!?)        # directive open; 2: optional prefix
1147                         ([-\w]+)        # 3: command
1148                         \s+
1149                         (               # 4: the parameters..
1150                                 (?:
1151                                         (?:[-\w]+=)?            # named parameter key?
1152                                         (?:
1153                                                 """.*?"""       # triple-quoted value
1154                                                 |
1155                                                 "[^"]+"         # single-quoted value
1156                                                 |
1157                                                 [^\s\]]+        # unquoted value
1158                                         )
1159                                         \s*                     # whitespace or end
1160                                                                 # of directive
1161                                 )
1162                         *)              # 0 or more parameters
1163                         \]\]            # directive closed
1164                 }sx;
1165         }
1166
1167         $content =~ s{$regex}{$handle->($1, $2, $3, $4)}eg;
1168         return $content;
1169 } #}}}
1170
1171 sub filter ($$$) { #{{{
1172         my $page=shift;
1173         my $destpage=shift;
1174         my $content=shift;
1175
1176         run_hooks(filter => sub {
1177                 $content=shift->(page => $page, destpage => $destpage, 
1178                         content => $content);
1179         });
1180
1181         return $content;
1182 } #}}}
1183
1184 sub indexlink () { #{{{
1185         return "<a href=\"$config{url}\">$config{wikiname}</a>";
1186 } #}}}
1187
1188 my $wikilock;
1189
1190 sub lockwiki (;$) { #{{{
1191         my $wait=@_ ? shift : 1;
1192         # Take an exclusive lock on the wiki to prevent multiple concurrent
1193         # run issues. The lock will be dropped on program exit.
1194         if (! -d $config{wikistatedir}) {
1195                 mkdir($config{wikistatedir});
1196         }
1197         open($wikilock, '>', "$config{wikistatedir}/lockfile") ||
1198                 error ("cannot write to $config{wikistatedir}/lockfile: $!");
1199         if (! flock($wikilock, 2 | 4)) { # LOCK_EX | LOCK_NB
1200                 if ($wait) {
1201                         debug("wiki seems to be locked, waiting for lock");
1202                         my $wait=600; # arbitrary, but don't hang forever to 
1203                                       # prevent process pileup
1204                         for (1..$wait) {
1205                                 return if flock($wikilock, 2 | 4);
1206                                 sleep 1;
1207                         }
1208                         error("wiki is locked; waited $wait seconds without lock being freed (possible stuck process or stale lock?)");
1209                 }
1210                 else {
1211                         return 0;
1212                 }
1213         }
1214         return 1;
1215 } #}}}
1216
1217 sub unlockwiki () { #{{{
1218         return close($wikilock) if $wikilock;
1219         return;
1220 } #}}}
1221
1222 my $commitlock;
1223
1224 sub commit_hook_enabled () { #{{{
1225         open($commitlock, '+>', "$config{wikistatedir}/commitlock") ||
1226                 error("cannot write to $config{wikistatedir}/commitlock: $!");
1227         if (! flock($commitlock, 1 | 4)) { # LOCK_SH | LOCK_NB to test
1228                 close($commitlock) || error("failed closing commitlock: $!");
1229                 return 0;
1230         }
1231         close($commitlock) || error("failed closing commitlock: $!");
1232         return 1;
1233 } #}}}
1234
1235 sub disable_commit_hook () { #{{{
1236         open($commitlock, '>', "$config{wikistatedir}/commitlock") ||
1237                 error("cannot write to $config{wikistatedir}/commitlock: $!");
1238         if (! flock($commitlock, 2)) { # LOCK_EX
1239                 error("failed to get commit lock");
1240         }
1241         return 1;
1242 } #}}}
1243
1244 sub enable_commit_hook () { #{{{
1245         return close($commitlock) if $commitlock;
1246         return;
1247 } #}}}
1248
1249 sub loadindex () { #{{{
1250         %oldrenderedfiles=%pagectime=();
1251         if (! $config{rebuild}) {
1252                 %pagesources=%pagemtime=%oldlinks=%links=%depends=
1253                 %destsources=%renderedfiles=%pagecase=%pagestate=();
1254         }
1255         my $in;
1256         if (! open ($in, "<", "$config{wikistatedir}/indexdb")) {
1257                 if (-e "$config{wikistatedir}/index") {
1258                         system("ikiwiki-transition", "indexdb", $config{srcdir});
1259                         open ($in, "<", "$config{wikistatedir}/indexdb") || return;
1260                 }
1261                 else {
1262                         return;
1263                 }
1264         }
1265         my $ret=Storable::fd_retrieve($in);
1266         if (! defined $ret) {
1267                 return 0;
1268         }
1269         my %index=%$ret;
1270         foreach my $src (keys %index) {
1271                 my %d=%{$index{$src}};
1272                 my $page=pagename($src);
1273                 $pagectime{$page}=$d{ctime};
1274                 if (! $config{rebuild}) {
1275                         $pagesources{$page}=$src;
1276                         $pagemtime{$page}=$d{mtime};
1277                         $renderedfiles{$page}=$d{dest};
1278                         if (exists $d{links} && ref $d{links}) {
1279                                 $links{$page}=$d{links};
1280                                 $oldlinks{$page}=[@{$d{links}}];
1281                         }
1282                         if (exists $d{depends}) {
1283                                 $depends{$page}=$d{depends};
1284                         }
1285                         if (exists $d{state}) {
1286                                 $pagestate{$page}=$d{state};
1287                         }
1288                 }
1289                 $oldrenderedfiles{$page}=[@{$d{dest}}];
1290         }
1291         foreach my $page (keys %pagesources) {
1292                 $pagecase{lc $page}=$page;
1293         }
1294         foreach my $page (keys %renderedfiles) {
1295                 $destsources{$_}=$page foreach @{$renderedfiles{$page}};
1296         }
1297         return close($in);
1298 } #}}}
1299
1300 sub saveindex () { #{{{
1301         run_hooks(savestate => sub { shift->() });
1302
1303         my %hookids;
1304         foreach my $type (keys %hooks) {
1305                 $hookids{$_}=1 foreach keys %{$hooks{$type}};
1306         }
1307         my @hookids=keys %hookids;
1308
1309         if (! -d $config{wikistatedir}) {
1310                 mkdir($config{wikistatedir});
1311         }
1312         my $newfile="$config{wikistatedir}/indexdb.new";
1313         my $cleanup = sub { unlink($newfile) };
1314         open (my $out, '>', $newfile) || error("cannot write to $newfile: $!", $cleanup);
1315         my %index;
1316         foreach my $page (keys %pagemtime) {
1317                 next unless $pagemtime{$page};
1318                 my $src=$pagesources{$page};
1319
1320                 $index{$src}={
1321                         ctime => $pagectime{$page},
1322                         mtime => $pagemtime{$page},
1323                         dest => $renderedfiles{$page},
1324                         links => $links{$page},
1325                 };
1326
1327                 if (exists $depends{$page}) {
1328                         $index{$src}{depends} = $depends{$page};
1329                 }
1330
1331                 if (exists $pagestate{$page}) {
1332                         foreach my $id (@hookids) {
1333                                 foreach my $key (keys %{$pagestate{$page}{$id}}) {
1334                                         $index{$src}{state}{$id}{$key}=$pagestate{$page}{$id}{$key};
1335                                 }
1336                         }
1337                 }
1338         }
1339         my $ret=Storable::nstore_fd(\%index, $out);
1340         return if ! defined $ret || ! $ret;
1341         close $out || error("failed saving to $newfile: $!", $cleanup);
1342         rename($newfile, "$config{wikistatedir}/indexdb") ||
1343                 error("failed renaming $newfile to $config{wikistatedir}/indexdb", $cleanup);
1344         
1345         return 1;
1346 } #}}}
1347
1348 sub template_file ($) { #{{{
1349         my $template=shift;
1350
1351         foreach my $dir ($config{templatedir}, "$installdir/share/ikiwiki/templates") {
1352                 return "$dir/$template" if -e "$dir/$template";
1353         }
1354         return;
1355 } #}}}
1356
1357 sub template_params (@) { #{{{
1358         my $filename=template_file(shift);
1359
1360         if (! defined $filename) {
1361                 return if wantarray;
1362                 return "";
1363         }
1364
1365         my @ret=(
1366                 filter => sub {
1367                         my $text_ref = shift;
1368                         ${$text_ref} = decode_utf8(${$text_ref});
1369                 },
1370                 filename => $filename,
1371                 loop_context_vars => 1,
1372                 die_on_bad_params => 0,
1373                 @_
1374         );
1375         return wantarray ? @ret : {@ret};
1376 } #}}}
1377
1378 sub template ($;@) { #{{{
1379         require HTML::Template;
1380         return HTML::Template->new(template_params(@_));
1381 } #}}}
1382
1383 sub misctemplate ($$;@) { #{{{
1384         my $title=shift;
1385         my $pagebody=shift;
1386         
1387         my $template=template("misc.tmpl");
1388         $template->param(
1389                 title => $title,
1390                 indexlink => indexlink(),
1391                 wikiname => $config{wikiname},
1392                 pagebody => $pagebody,
1393                 baseurl => baseurl(),
1394                 @_,
1395         );
1396         run_hooks(pagetemplate => sub {
1397                 shift->(page => "", destpage => "", template => $template);
1398         });
1399         return $template->output;
1400 }#}}}
1401
1402 sub hook (@) { # {{{
1403         my %param=@_;
1404         
1405         if (! exists $param{type} || ! ref $param{call} || ! exists $param{id}) {
1406                 error 'hook requires type, call, and id parameters';
1407         }
1408
1409         return if $param{no_override} && exists $hooks{$param{type}}{$param{id}};
1410         
1411         $hooks{$param{type}}{$param{id}}=\%param;
1412         return 1;
1413 } # }}}
1414
1415 sub run_hooks ($$) { # {{{
1416         # Calls the given sub for each hook of the given type,
1417         # passing it the hook function to call.
1418         my $type=shift;
1419         my $sub=shift;
1420
1421         if (exists $hooks{$type}) {
1422                 my @deferred;
1423                 foreach my $id (keys %{$hooks{$type}}) {
1424                         if ($hooks{$type}{$id}{last}) {
1425                                 push @deferred, $id;
1426                                 next;
1427                         }
1428                         $sub->($hooks{$type}{$id}{call});
1429                 }
1430                 foreach my $id (@deferred) {
1431                         $sub->($hooks{$type}{$id}{call});
1432                 }
1433         }
1434
1435         return 1;
1436 } #}}}
1437
1438 sub rcs_update () { #{{{
1439         $hooks{rcs}{rcs_update}{call}->(@_);
1440 } #}}}
1441
1442 sub rcs_prepedit ($) { #{{{
1443         $hooks{rcs}{rcs_prepedit}{call}->(@_);
1444 } #}}}
1445
1446 sub rcs_commit ($$$;$$) { #{{{
1447         $hooks{rcs}{rcs_commit}{call}->(@_);
1448 } #}}}
1449
1450 sub rcs_commit_staged ($$$) { #{{{
1451         $hooks{rcs}{rcs_commit_staged}{call}->(@_);
1452 } #}}}
1453
1454 sub rcs_add ($) { #{{{
1455         $hooks{rcs}{rcs_add}{call}->(@_);
1456 } #}}}
1457
1458 sub rcs_remove ($) { #{{{
1459         $hooks{rcs}{rcs_remove}{call}->(@_);
1460 } #}}}
1461
1462 sub rcs_rename ($$) { #{{{
1463         $hooks{rcs}{rcs_rename}{call}->(@_);
1464 } #}}}
1465
1466 sub rcs_recentchanges ($) { #{{{
1467         $hooks{rcs}{rcs_recentchanges}{call}->(@_);
1468 } #}}}
1469
1470 sub rcs_diff ($) { #{{{
1471         $hooks{rcs}{rcs_diff}{call}->(@_);
1472 } #}}}
1473
1474 sub rcs_getctime ($) { #{{{
1475         $hooks{rcs}{rcs_getctime}{call}->(@_);
1476 } #}}}
1477
1478 sub globlist_to_pagespec ($) { #{{{
1479         my @globlist=split(' ', shift);
1480
1481         my (@spec, @skip);
1482         foreach my $glob (@globlist) {
1483                 if ($glob=~/^!(.*)/) {
1484                         push @skip, $glob;
1485                 }
1486                 else {
1487                         push @spec, $glob;
1488                 }
1489         }
1490
1491         my $spec=join(' or ', @spec);
1492         if (@skip) {
1493                 my $skip=join(' and ', @skip);
1494                 if (length $spec) {
1495                         $spec="$skip and ($spec)";
1496                 }
1497                 else {
1498                         $spec=$skip;
1499                 }
1500         }
1501         return $spec;
1502 } #}}}
1503
1504 sub is_globlist ($) { #{{{
1505         my $s=shift;
1506         return ( $s =~ /[^\s]+\s+([^\s]+)/ && $1 ne "and" && $1 ne "or" );
1507 } #}}}
1508
1509 sub safequote ($) { #{{{
1510         my $s=shift;
1511         $s=~s/[{}]//g;
1512         return "q{$s}";
1513 } #}}}
1514
1515 sub add_depends ($$) { #{{{
1516         my $page=shift;
1517         my $pagespec=shift;
1518         
1519         return unless pagespec_valid($pagespec);
1520
1521         if (! exists $depends{$page}) {
1522                 $depends{$page}=$pagespec;
1523         }
1524         else {
1525                 $depends{$page}=pagespec_merge($depends{$page}, $pagespec);
1526         }
1527
1528         return 1;
1529 } # }}}
1530
1531 sub file_pruned ($$) { #{{{
1532         require File::Spec;
1533         my $file=File::Spec->canonpath(shift);
1534         my $base=File::Spec->canonpath(shift);
1535         $file =~ s#^\Q$base\E/+##;
1536
1537         my $regexp='('.join('|', @{$config{wiki_file_prune_regexps}}).')';
1538         return $file =~ m/$regexp/ && $file ne $base;
1539 } #}}}
1540
1541 sub gettext { #{{{
1542         # Only use gettext in the rare cases it's needed.
1543         if ((exists $ENV{LANG} && length $ENV{LANG}) ||
1544             (exists $ENV{LC_ALL} && length $ENV{LC_ALL}) ||
1545             (exists $ENV{LC_MESSAGES} && length $ENV{LC_MESSAGES})) {
1546                 if (! $gettext_obj) {
1547                         $gettext_obj=eval q{
1548                                 use Locale::gettext q{textdomain};
1549                                 Locale::gettext->domain('ikiwiki')
1550                         };
1551                         if ($@) {
1552                                 print STDERR "$@";
1553                                 $gettext_obj=undef;
1554                                 return shift;
1555                         }
1556                 }
1557                 return $gettext_obj->get(shift);
1558         }
1559         else {
1560                 return shift;
1561         }
1562 } #}}}
1563
1564 sub yesno ($) { #{{{
1565         my $val=shift;
1566
1567         return (defined $val && lc($val) eq gettext("yes"));
1568 } #}}}
1569
1570 sub pagespec_merge ($$) { #{{{
1571         my $a=shift;
1572         my $b=shift;
1573
1574         return $a if $a eq $b;
1575
1576         # Support for old-style GlobLists.
1577         if (is_globlist($a)) {
1578                 $a=globlist_to_pagespec($a);
1579         }
1580         if (is_globlist($b)) {
1581                 $b=globlist_to_pagespec($b);
1582         }
1583
1584         return "($a) or ($b)";
1585 } #}}}
1586
1587 sub pagespec_translate ($) { #{{{
1588         my $spec=shift;
1589
1590         # Support for old-style GlobLists.
1591         if (is_globlist($spec)) {
1592                 $spec=globlist_to_pagespec($spec);
1593         }
1594
1595         # Convert spec to perl code.
1596         my $code="";
1597         while ($spec=~m{
1598                 \s*             # ignore whitespace
1599                 (               # 1: match a single word
1600                         \!              # !
1601                 |
1602                         \(              # (
1603                 |
1604                         \)              # )
1605                 |
1606                         \w+\([^\)]*\)   # command(params)
1607                 |
1608                         [^\s()]+        # any other text
1609                 )
1610                 \s*             # ignore whitespace
1611         }igx) {
1612                 my $word=$1;
1613                 if (lc $word eq 'and') {
1614                         $code.=' &&';
1615                 }
1616                 elsif (lc $word eq 'or') {
1617                         $code.=' ||';
1618                 }
1619                 elsif ($word eq "(" || $word eq ")" || $word eq "!") {
1620                         $code.=' '.$word;
1621                 }
1622                 elsif ($word =~ /^(\w+)\((.*)\)$/) {
1623                         if (exists $IkiWiki::PageSpec::{"match_$1"}) {
1624                                 $code.="IkiWiki::PageSpec::match_$1(\$page, ".safequote($2).", \@_)";
1625                         }
1626                         else {
1627                                 $code.=' 0';
1628                         }
1629                 }
1630                 else {
1631                         $code.=" IkiWiki::PageSpec::match_glob(\$page, ".safequote($word).", \@_)";
1632                 }
1633         }
1634
1635         if (! length $code) {
1636                 $code=0;
1637         }
1638
1639         no warnings;
1640         return eval 'sub { my $page=shift; '.$code.' }';
1641 } #}}}
1642
1643 sub pagespec_match ($$;@) { #{{{
1644         my $page=shift;
1645         my $spec=shift;
1646         my @params=@_;
1647
1648         # Backwards compatability with old calling convention.
1649         if (@params == 1) {
1650                 unshift @params, 'location';
1651         }
1652
1653         my $sub=pagespec_translate($spec);
1654         return IkiWiki::FailReason->new("syntax error in pagespec \"$spec\"") if $@;
1655         return $sub->($page, @params);
1656 } #}}}
1657
1658 sub pagespec_valid ($) { #{{{
1659         my $spec=shift;
1660
1661         my $sub=pagespec_translate($spec);
1662         return ! $@;
1663 } #}}}
1664         
1665 sub glob2re ($) { #{{{
1666         my $re=quotemeta(shift);
1667         $re=~s/\\\*/.*/g;
1668         $re=~s/\\\?/./g;
1669         return $re;
1670 } #}}}
1671
1672 package IkiWiki::FailReason;
1673
1674 use overload ( #{{{
1675         '""'    => sub { ${$_[0]} },
1676         '0+'    => sub { 0 },
1677         '!'     => sub { bless $_[0], 'IkiWiki::SuccessReason'},
1678         fallback => 1,
1679 ); #}}}
1680
1681 sub new { #{{{
1682         my $class = shift;
1683         my $value = shift;
1684         return bless \$value, $class;
1685 } #}}}
1686
1687 package IkiWiki::SuccessReason;
1688
1689 use overload ( #{{{
1690         '""'    => sub { ${$_[0]} },
1691         '0+'    => sub { 1 },
1692         '!'     => sub { bless $_[0], 'IkiWiki::FailReason'},
1693         fallback => 1,
1694 ); #}}}
1695
1696 sub new { #{{{
1697         my $class = shift;
1698         my $value = shift;
1699         return bless \$value, $class;
1700 }; #}}}
1701
1702 package IkiWiki::PageSpec;
1703
1704 sub match_glob ($$;@) { #{{{
1705         my $page=shift;
1706         my $glob=shift;
1707         my %params=@_;
1708         
1709         my $from=exists $params{location} ? $params{location} : '';
1710         
1711         # relative matching
1712         if ($glob =~ m!^\./!) {
1713                 $from=~s#/?[^/]+$##;
1714                 $glob=~s#^\./##;
1715                 $glob="$from/$glob" if length $from;
1716         }
1717
1718         my $regexp=IkiWiki::glob2re($glob);
1719         if ($page=~/^$regexp$/i) {
1720                 if (! IkiWiki::isinternal($page) || $params{internal}) {
1721                         return IkiWiki::SuccessReason->new("$glob matches $page");
1722                 }
1723                 else {
1724                         return IkiWiki::FailReason->new("$glob matches $page, but the page is an internal page");
1725                 }
1726         }
1727         else {
1728                 return IkiWiki::FailReason->new("$glob does not match $page");
1729         }
1730 } #}}}
1731
1732 sub match_internal ($$;@) { #{{{
1733         return match_glob($_[0], $_[1], @_, internal => 1)
1734 } #}}}
1735
1736 sub match_link ($$;@) { #{{{
1737         my $page=shift;
1738         my $link=lc(shift);
1739         my %params=@_;
1740
1741         my $from=exists $params{location} ? $params{location} : '';
1742
1743         # relative matching
1744         if ($link =~ m!^\.! && defined $from) {
1745                 $from=~s#/?[^/]+$##;
1746                 $link=~s#^\./##;
1747                 $link="$from/$link" if length $from;
1748         }
1749
1750         my $links = $IkiWiki::links{$page};
1751         return IkiWiki::FailReason->new("$page has no links") unless $links && @{$links};
1752         my $bestlink = IkiWiki::bestlink($from, $link);
1753         foreach my $p (@{$links}) {
1754                 if (length $bestlink) {
1755                         return IkiWiki::SuccessReason->new("$page links to $link")
1756                                 if $bestlink eq IkiWiki::bestlink($page, $p);
1757                 }
1758                 else {
1759                         return IkiWiki::SuccessReason->new("$page links to page $p matching $link")
1760                                 if match_glob($p, $link, %params);
1761                 }
1762         }
1763         return IkiWiki::FailReason->new("$page does not link to $link");
1764 } #}}}
1765
1766 sub match_backlink ($$;@) { #{{{
1767         return match_link($_[1], $_[0], @_);
1768 } #}}}
1769
1770 sub match_created_before ($$;@) { #{{{
1771         my $page=shift;
1772         my $testpage=shift;
1773
1774         if (exists $IkiWiki::pagectime{$testpage}) {
1775                 if ($IkiWiki::pagectime{$page} < $IkiWiki::pagectime{$testpage}) {
1776                         return IkiWiki::SuccessReason->new("$page created before $testpage");
1777                 }
1778                 else {
1779                         return IkiWiki::FailReason->new("$page not created before $testpage");
1780                 }
1781         }
1782         else {
1783                 return IkiWiki::FailReason->new("$testpage has no ctime");
1784         }
1785 } #}}}
1786
1787 sub match_created_after ($$;@) { #{{{
1788         my $page=shift;
1789         my $testpage=shift;
1790
1791         if (exists $IkiWiki::pagectime{$testpage}) {
1792                 if ($IkiWiki::pagectime{$page} > $IkiWiki::pagectime{$testpage}) {
1793                         return IkiWiki::SuccessReason->new("$page created after $testpage");
1794                 }
1795                 else {
1796                         return IkiWiki::FailReason->new("$page not created after $testpage");
1797                 }
1798         }
1799         else {
1800                 return IkiWiki::FailReason->new("$testpage has no ctime");
1801         }
1802 } #}}}
1803
1804 sub match_creation_day ($$;@) { #{{{
1805         if ((gmtime($IkiWiki::pagectime{shift()}))[3] == shift) {
1806                 return IkiWiki::SuccessReason->new('creation_day matched');
1807         }
1808         else {
1809                 return IkiWiki::FailReason->new('creation_day did not match');
1810         }
1811 } #}}}
1812
1813 sub match_creation_month ($$;@) { #{{{
1814         if ((gmtime($IkiWiki::pagectime{shift()}))[4] + 1 == shift) {
1815                 return IkiWiki::SuccessReason->new('creation_month matched');
1816         }
1817         else {
1818                 return IkiWiki::FailReason->new('creation_month did not match');
1819         }
1820 } #}}}
1821
1822 sub match_creation_year ($$;@) { #{{{
1823         if ((gmtime($IkiWiki::pagectime{shift()}))[5] + 1900 == shift) {
1824                 return IkiWiki::SuccessReason->new('creation_year matched');
1825         }
1826         else {
1827                 return IkiWiki::FailReason->new('creation_year did not match');
1828         }
1829 } #}}}
1830
1831 1