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