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