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