]> sipb.mit.edu Git - ikiwiki.git/blob - IkiWiki.pm
add news item for ikiwiki 1.33
[ikiwiki.git] / IkiWiki.pm
1 #!/usr/bin/perl
2
3 package IkiWiki;
4 use warnings;
5 use strict;
6 use Encode;
7 use HTML::Entities;
8 use open qw{:utf8 :std};
9
10 use vars qw{%config %links %oldlinks %oldpagemtime %pagectime %pagecase
11             %renderedfiles %oldrenderedfiles %pagesources %depends %hooks
12             %forcerebuild};
13
14 use Exporter q{import};
15 our @EXPORT = qw(hook debug error template htmlpage add_depends pagespec_match
16                  bestlink htmllink readfile writefile pagetype srcfile pagename
17                  displaytime will_render
18                  %config %links %renderedfiles %pagesources);
19 our $VERSION = 1.01; # plugin interface version
20
21 # Optimisation.
22 use Memoize;
23 memoize("abs2rel");
24 memoize("pagespec_translate");
25
26 my $installdir=''; # INSTALLDIR_AUTOREPLACE done by Makefile, DNE
27 our $version='unknown'; # VERSION_AUTOREPLACE done by Makefile, DNE
28
29 sub defaultconfig () { #{{{
30         wiki_file_prune_regexp => qr{((^|/).svn/|\.\.|^\.|\/\.|\.x?html?$|\.rss$|\.atom$|.arch-ids/|{arch}/)},
31         wiki_link_regexp => qr/\[\[(?:([^\]\|]+)\|)?([^\s\]]+)\]\]/,
32         wiki_file_regexp => qr/(^[-[:alnum:]_.:\/+]+$)/,
33         verbose => 0,
34         syslog => 0,
35         wikiname => "wiki",
36         default_pageext => "mdwn",
37         cgi => 0,
38         rcs => 'svn',
39         notify => 0,
40         url => '',
41         cgiurl => '',
42         historyurl => '',
43         diffurl => '',
44         anonok => 0,
45         rss => 0,
46         atom => 0,
47         discussion => 1,
48         rebuild => 0,
49         refresh => 0,
50         getctime => 0,
51         w3mmode => 0,
52         wrapper => undef,
53         wrappermode => undef,
54         svnrepo => undef,
55         svnpath => "trunk",
56         srcdir => undef,
57         destdir => undef,
58         pingurl => [],
59         templatedir => "$installdir/share/ikiwiki/templates",
60         underlaydir => "$installdir/share/ikiwiki/basewiki",
61         setup => undef,
62         adminuser => undef,
63         adminemail => undef,
64         plugin => [qw{mdwn inline htmlscrubber}],
65         timeformat => '%c',
66         locale => undef,
67         sslcookie => 0,
68         httpauth => 0,
69 } #}}}
70    
71 sub checkconfig () { #{{{
72         # locale stuff; avoid LC_ALL since it overrides everything
73         if (defined $ENV{LC_ALL}) {
74                 $ENV{LANG} = $ENV{LC_ALL};
75                 delete $ENV{LC_ALL};
76         }
77         if (defined $config{locale}) {
78                 eval q{use POSIX};
79                 error($@) if $@;
80                 $ENV{LANG} = $config{locale}
81                         if POSIX::setlocale(&POSIX::LC_TIME, $config{locale});
82         }
83
84         if ($config{w3mmode}) {
85                 eval q{use Cwd q{abs_path}};
86                 error($@) if $@;
87                 $config{srcdir}=possibly_foolish_untaint(abs_path($config{srcdir}));
88                 $config{destdir}=possibly_foolish_untaint(abs_path($config{destdir}));
89                 $config{cgiurl}="file:///\$LIB/ikiwiki-w3m.cgi/".$config{cgiurl}
90                         unless $config{cgiurl} =~ m!file:///!;
91                 $config{url}="file://".$config{destdir};
92         }
93
94         if ($config{cgi} && ! length $config{url}) {
95                 error("Must specify url to wiki with --url when using --cgi\n");
96         }
97         if (($config{rss} || $config{atom}) && ! length $config{url}) {
98                 error("Must specify url to wiki with --url when using --rss or --atom\n");
99         }
100         
101         $config{wikistatedir}="$config{srcdir}/.ikiwiki"
102                 unless exists $config{wikistatedir};
103         
104         if ($config{rcs}) {
105                 eval qq{require IkiWiki::Rcs::$config{rcs}};
106                 if ($@) {
107                         error("Failed to load RCS module IkiWiki::Rcs::$config{rcs}: $@");
108                 }
109         }
110         else {
111                 require IkiWiki::Rcs::Stub;
112         }
113
114         run_hooks(checkconfig => sub { shift->() });
115 } #}}}
116
117 sub loadplugins () { #{{{
118         foreach my $plugin (@{$config{plugin}}) {
119                 my $mod="IkiWiki::Plugin::".possibly_foolish_untaint($plugin);
120                 eval qq{use $mod};
121                 if ($@) {
122                         error("Failed to load plugin $mod: $@");
123                 }
124         }
125         run_hooks(getopt => sub { shift->() });
126         if (grep /^-/, @ARGV) {
127                 print STDERR "Unknown option: $_\n"
128                         foreach grep /^-/, @ARGV;
129                 usage();
130         }
131 } #}}}
132
133 sub error ($) { #{{{
134         if ($config{cgi}) {
135                 print "Content-type: text/html\n\n";
136                 print misctemplate("Error", "<p>Error: @_</p>");
137         }
138         log_message(error => @_);
139         exit(1);
140 } #}}}
141
142 sub debug ($) { #{{{
143         return unless $config{verbose};
144         log_message(debug => @_);
145 } #}}}
146
147 my $log_open=0;
148 sub log_message ($$) { #{{{
149         my $type=shift;
150
151         if ($config{syslog}) {
152                 require Sys::Syslog;
153                 unless ($log_open) {
154                         Sys::Syslog::setlogsock('unix');
155                         Sys::Syslog::openlog('ikiwiki', '', 'user');
156                         $log_open=1;
157                 }
158                 eval {
159                         Sys::Syslog::syslog($type, join(" ", @_));
160                 }
161         }
162         elsif (! $config{cgi}) {
163                 print "@_\n";
164         }
165         else {
166                 print STDERR "@_\n";
167         }
168 } #}}}
169
170 sub possibly_foolish_untaint ($) { #{{{
171         my $tainted=shift;
172         my ($untainted)=$tainted=~/(.*)/;
173         return $untainted;
174 } #}}}
175
176 sub basename ($) { #{{{
177         my $file=shift;
178
179         $file=~s!.*/+!!;
180         return $file;
181 } #}}}
182
183 sub dirname ($) { #{{{
184         my $file=shift;
185
186         $file=~s!/*[^/]+$!!;
187         return $file;
188 } #}}}
189
190 sub pagetype ($) { #{{{
191         my $page=shift;
192         
193         if ($page =~ /\.([^.]+)$/) {
194                 return $1 if exists $hooks{htmlize}{$1};
195         }
196         return undef;
197 } #}}}
198
199 sub pagename ($) { #{{{
200         my $file=shift;
201
202         my $type=pagetype($file);
203         my $page=$file;
204         $page=~s/\Q.$type\E*$// if defined $type;
205         return $page;
206 } #}}}
207
208 sub htmlpage ($) { #{{{
209         my $page=shift;
210
211         return $page.".html";
212 } #}}}
213
214 sub srcfile ($) { #{{{
215         my $file=shift;
216
217         return "$config{srcdir}/$file" if -e "$config{srcdir}/$file";
218         return "$config{underlaydir}/$file" if -e "$config{underlaydir}/$file";
219         error("internal error: $file cannot be found");
220 } #}}}
221
222 sub readfile ($;$) { #{{{
223         my $file=shift;
224         my $binary=shift;
225
226         if (-l $file) {
227                 error("cannot read a symlink ($file)");
228         }
229         
230         local $/=undef;
231         open (IN, $file) || error("failed to read $file: $!");
232         binmode(IN) if ($binary);
233         my $ret=<IN>;
234         close IN;
235         return $ret;
236 } #}}}
237
238 sub writefile ($$$;$) { #{{{
239         my $file=shift; # can include subdirs
240         my $destdir=shift; # directory to put file in
241         my $content=shift;
242         my $binary=shift;
243         
244         my $test=$file;
245         while (length $test) {
246                 if (-l "$destdir/$test") {
247                         error("cannot write to a symlink ($test)");
248                 }
249                 $test=dirname($test);
250         }
251
252         my $dir=dirname("$destdir/$file");
253         if (! -d $dir) {
254                 my $d="";
255                 foreach my $s (split(m!/+!, $dir)) {
256                         $d.="$s/";
257                         if (! -d $d) {
258                                 mkdir($d) || error("failed to create directory $d: $!");
259                         }
260                 }
261         }
262         
263         open (OUT, ">$destdir/$file") || error("failed to write $destdir/$file: $!");
264         binmode(OUT) if ($binary);
265         print OUT $content;
266         close OUT;
267 } #}}}
268
269 my %cleared;
270 sub will_render ($$;$) { #{{{
271         my $page=shift;
272         my $dest=shift;
273         my $clear=shift;
274
275         # Important security check.
276         if (-e "$config{destdir}/$dest" && ! $config{rebuild} &&
277             ! grep { $_ eq $dest } (@{$renderedfiles{$page}}, @{$oldrenderedfiles{$page}})) {
278                 error("$config{destdir}/$dest independently created, not overwriting with version from $page");
279         }
280
281         if (! $clear || $cleared{$page}) {
282                 $renderedfiles{$page}=[$dest, grep { $_ ne $dest } @{$renderedfiles{$page}}];
283         }
284         else {
285                 $renderedfiles{$page}=[$dest];
286                 $cleared{$page}=1;
287         }
288 } #}}}
289
290 sub bestlink ($$) { #{{{
291         my $page=shift;
292         my $link=shift;
293         
294         my $cwd=$page;
295         do {
296                 my $l=$cwd;
297                 $l.="/" if length $l;
298                 $l.=$link;
299
300                 if (exists $links{$l}) {
301                         return $l;
302                 }
303                 elsif (exists $pagecase{lc $l}) {
304                         return $pagecase{lc $l};
305                 }
306         } while $cwd=~s!/?[^/]+$!!;
307
308         #print STDERR "warning: page $page, broken link: $link\n";
309         return "";
310 } #}}}
311
312 sub isinlinableimage ($) { #{{{
313         my $file=shift;
314         
315         $file=~/\.(png|gif|jpg|jpeg)$/i;
316 } #}}}
317
318 sub pagetitle ($) { #{{{
319         my $page=shift;
320         $page=~s/__(\d+)__/&#$1;/g;
321         $page=~y/_/ /;
322         return $page;
323 } #}}}
324
325 sub titlepage ($) { #{{{
326         my $title=shift;
327         $title=~y/ /_/;
328         $title=~s/([^-[:alnum:]_:+\/.])/"__".ord($1)."__"/eg;
329         return $title;
330 } #}}}
331
332 sub cgiurl (@) { #{{{
333         my %params=@_;
334
335         return $config{cgiurl}."?".join("&amp;", map "$_=$params{$_}", keys %params);
336 } #}}}
337
338 sub baseurl (;$) { #{{{
339         my $page=shift;
340
341         return "$config{url}/" if ! defined $page;
342         
343         $page=~s/[^\/]+$//;
344         $page=~s/[^\/]+\//..\//g;
345         return $page;
346 } #}}}
347
348 sub abs2rel ($$) { #{{{
349         # Work around very innefficient behavior in File::Spec if abs2rel
350         # is passed two relative paths. It's much faster if paths are
351         # absolute! (Debian bug #376658)
352         my $path="/".shift;
353         my $base="/".shift;
354
355         require File::Spec;
356         my $ret=File::Spec->abs2rel($path, $base);
357         $ret=~s/^// if defined $ret;
358         return $ret;
359 } #}}}
360
361 sub displaytime ($) { #{{{
362         my $time=shift;
363
364         eval q{use POSIX};
365         error($@) if $@;
366         # strftime doesn't know about encodings, so make sure
367         # its output is properly treated as utf8
368         return decode_utf8(POSIX::strftime(
369                         $config{timeformat}, localtime($time)));
370 } #}}}
371
372 sub htmllink ($$$;$$$) { #{{{
373         my $lpage=shift; # the page doing the linking
374         my $page=shift; # the page that will contain the link (different for inline)
375         my $link=shift;
376         my $noimageinline=shift; # don't turn links into inline html images
377         my $forcesubpage=shift; # force a link to a subpage
378         my $linktext=shift; # set to force the link text to something
379
380         my $bestlink;
381         if (! $forcesubpage) {
382                 $bestlink=bestlink($lpage, $link);
383         }
384         else {
385                 $bestlink="$lpage/".lc($link);
386         }
387
388         $linktext=pagetitle(basename($link)) unless defined $linktext;
389         
390         return "<span class=\"selflink\">$linktext</span>"
391                 if length $bestlink && $page eq $bestlink;
392         
393         # TODO BUG: %renderedfiles may not have it, if the linked to page
394         # was also added and isn't yet rendered! Note that this bug is
395         # masked by the bug that makes all new files be rendered twice.
396         if (! grep { $_ eq $bestlink } map { @{$_} } values %renderedfiles) {
397                 $bestlink=htmlpage($bestlink);
398         }
399         if (! grep { $_ eq $bestlink } map { @{$_} } values %renderedfiles) {
400                 return "<span><a href=\"".
401                         cgiurl(do => "create", page => lc($link), from => $page).
402                         "\">?</a>$linktext</span>"
403         }
404         
405         $bestlink=abs2rel($bestlink, dirname($page));
406         
407         if (! $noimageinline && isinlinableimage($bestlink)) {
408                 return "<img src=\"$bestlink\" alt=\"$linktext\" />";
409         }
410         return "<a href=\"$bestlink\">$linktext</a>";
411 } #}}}
412
413 sub htmlize ($$$) { #{{{
414         my $page=shift;
415         my $type=shift;
416         my $content=shift;
417
418         if (exists $hooks{htmlize}{$type}) {
419                 $content=$hooks{htmlize}{$type}{call}->(
420                         page => $page,
421                         content => $content,
422                 );
423         }
424         else {
425                 error("htmlization of $type not supported");
426         }
427
428         run_hooks(sanitize => sub {
429                 $content=shift->(
430                         page => $page,
431                         content => $content,
432                 );
433         });
434
435         return $content;
436 } #}}}
437
438 sub linkify ($$$) { #{{{
439         my $lpage=shift; # the page containing the links
440         my $page=shift; # the page the link will end up on (different for inline)
441         my $content=shift;
442
443         $content =~ s{(\\?)$config{wiki_link_regexp}}{
444                 $2 ? ( $1 ? "[[$2|$3]]" : htmllink($lpage, $page, titlepage($3), 0, 0, pagetitle($2)))
445                    : ( $1 ? "[[$3]]" :    htmllink($lpage, $page, titlepage($3)))
446         }eg;
447         
448         return $content;
449 } #}}}
450
451 my %preprocessing;
452 sub preprocess ($$$;$) { #{{{
453         my $page=shift; # the page the data comes from
454         my $destpage=shift; # the page the data will appear in (different for inline)
455         my $content=shift;
456         my $scan=shift;
457
458         my $handle=sub {
459                 my $escape=shift;
460                 my $command=shift;
461                 my $params=shift;
462                 if (length $escape) {
463                         return "[[$command $params]]";
464                 }
465                 elsif (exists $hooks{preprocess}{$command}) {
466                         return "" if $scan && ! $hooks{preprocess}{$command}{scan};
467                         # Note: preserve order of params, some plugins may
468                         # consider it significant.
469                         my @params;
470                         while ($params =~ /(?:(\w+)=)?(?:"""(.*?)"""|"([^"]+)"|(\S+))(?:\s+|$)/sg) {
471                                 my $key=$1;
472                                 my $val;
473                                 if (defined $2) {
474                                         $val=$2;
475                                         $val=~s/\r\n/\n/mg;
476                                         $val=~s/^\n+//g;
477                                         $val=~s/\n+$//g;
478                                 }
479                                 elsif (defined $3) {
480                                         $val=$3;
481                                 }
482                                 elsif (defined $4) {
483                                         $val=$4;
484                                 }
485
486                                 if (defined $key) {
487                                         push @params, $key, $val;
488                                 }
489                                 else {
490                                         push @params, $val, '';
491                                 }
492                         }
493                         if ($preprocessing{$page}++ > 3) {
494                                 # Avoid loops of preprocessed pages preprocessing
495                                 # other pages that preprocess them, etc.
496                                 return "[[$command preprocessing loop detected on $page at depth $preprocessing{$page}]]";
497                         }
498                         my $ret=$hooks{preprocess}{$command}{call}->(
499                                 @params,
500                                 page => $page,
501                                 destpage => $destpage,
502                         );
503                         $preprocessing{$page}--;
504                         return $ret;
505                 }
506                 else {
507                         return "[[$command $params]]";
508                 }
509         };
510         
511         $content =~ s{(\\?)\[\[(\w+)\s+((?:(?:\w+=)?(?:""".*?"""|"[^"]+"|[^\s\]]+)\s*)*)\]\]}{$handle->($1, $2, $3)}seg;
512         return $content;
513 } #}}}
514
515 sub filter ($$) { #{{{
516         my $page=shift;
517         my $content=shift;
518
519         run_hooks(filter => sub {
520                 $content=shift->(page => $page, content => $content);
521         });
522
523         return $content;
524 } #}}}
525
526 sub indexlink () { #{{{
527         return "<a href=\"$config{url}\">$config{wikiname}</a>";
528 } #}}}
529
530 sub lockwiki () { #{{{
531         # Take an exclusive lock on the wiki to prevent multiple concurrent
532         # run issues. The lock will be dropped on program exit.
533         if (! -d $config{wikistatedir}) {
534                 mkdir($config{wikistatedir});
535         }
536         open(WIKILOCK, ">$config{wikistatedir}/lockfile") ||
537                 error ("cannot write to $config{wikistatedir}/lockfile: $!");
538         if (! flock(WIKILOCK, 2 | 4)) {
539                 debug("wiki seems to be locked, waiting for lock");
540                 my $wait=600; # arbitrary, but don't hang forever to 
541                               # prevent process pileup
542                 for (1..600) {
543                         return if flock(WIKILOCK, 2 | 4);
544                         sleep 1;
545                 }
546                 error("wiki is locked; waited $wait seconds without lock being freed (possible stuck process or stale lock?)");
547         }
548 } #}}}
549
550 sub unlockwiki () { #{{{
551         close WIKILOCK;
552 } #}}}
553
554 sub loadindex () { #{{{
555         open (IN, "$config{wikistatedir}/index") || return;
556         while (<IN>) {
557                 $_=possibly_foolish_untaint($_);
558                 chomp;
559                 my %items;
560                 $items{link}=[];
561                 $items{dest}=[];
562                 foreach my $i (split(/ /, $_)) {
563                         my ($item, $val)=split(/=/, $i, 2);
564                         push @{$items{$item}}, decode_entities($val);
565                 }
566
567                 next unless exists $items{src}; # skip bad lines for now
568
569                 my $page=pagename($items{src}[0]);
570                 if (! $config{rebuild}) {
571                         $pagesources{$page}=$items{src}[0];
572                         $oldpagemtime{$page}=$items{mtime}[0];
573                         $oldlinks{$page}=[@{$items{link}}];
574                         $links{$page}=[@{$items{link}}];
575                         $depends{$page}=$items{depends}[0] if exists $items{depends};
576                         $renderedfiles{$page}=[@{$items{dest}}];
577                         $oldrenderedfiles{$page}=[@{$items{dest}}];
578                         $pagecase{lc $page}=$page;
579                 }
580                 $pagectime{$page}=$items{ctime}[0];
581         }
582         close IN;
583 } #}}}
584
585 sub saveindex () { #{{{
586         run_hooks(savestate => sub { shift->() });
587
588         if (! -d $config{wikistatedir}) {
589                 mkdir($config{wikistatedir});
590         }
591         open (OUT, ">$config{wikistatedir}/index") || 
592                 error("cannot write to $config{wikistatedir}/index: $!");
593         foreach my $page (keys %oldpagemtime) {
594                 next unless $oldpagemtime{$page};
595                 my $line="mtime=$oldpagemtime{$page} ".
596                         "ctime=$pagectime{$page} ".
597                         "src=$pagesources{$page}";
598                 $line.=" dest=$_" foreach @{$renderedfiles{$page}};
599                 my %count;
600                 $line.=" link=$_" foreach grep { ++$count{$_} == 1 } @{$links{$page}};
601                 if (exists $depends{$page}) {
602                         $line.=" depends=".encode_entities($depends{$page}, " \t\n");
603                 }
604                 print OUT $line."\n";
605         }
606         close OUT;
607 } #}}}
608
609 sub template_params (@) { #{{{
610         my $filename=shift;
611         
612         require HTML::Template;
613         return filter => sub {
614                         my $text_ref = shift;
615                         $$text_ref=&Encode::decode_utf8($$text_ref);
616                 },
617                 filename => "$config{templatedir}/$filename",
618                 loop_context_vars => 1,
619                 die_on_bad_params => 0,
620                 @_;
621 } #}}}
622
623 sub template ($;@) { #{{{
624         HTML::Template->new(template_params(@_));
625 } #}}}
626
627 sub misctemplate ($$;@) { #{{{
628         my $title=shift;
629         my $pagebody=shift;
630         
631         my $template=template("misc.tmpl");
632         $template->param(
633                 title => $title,
634                 indexlink => indexlink(),
635                 wikiname => $config{wikiname},
636                 pagebody => $pagebody,
637                 baseurl => baseurl(),
638                 @_,
639         );
640         run_hooks(pagetemplate => sub {
641                 shift->(page => "", destpage => "", template => $template);
642         });
643         return $template->output;
644 }#}}}
645
646 sub hook (@) { # {{{
647         my %param=@_;
648         
649         if (! exists $param{type} || ! ref $param{call} || ! exists $param{id}) {
650                 error "hook requires type, call, and id parameters";
651         }
652
653         return if $param{no_override} && exists $hooks{$param{type}}{$param{id}};
654         
655         $hooks{$param{type}}{$param{id}}=\%param;
656 } # }}}
657
658 sub run_hooks ($$) { # {{{
659         # Calls the given sub for each hook of the given type,
660         # passing it the hook function to call.
661         my $type=shift;
662         my $sub=shift;
663
664         if (exists $hooks{$type}) {
665                 foreach my $id (keys %{$hooks{$type}}) {
666                         $sub->($hooks{$type}{$id}{call});
667                 }
668         }
669 } #}}}
670
671 sub globlist_to_pagespec ($) { #{{{
672         my @globlist=split(' ', shift);
673
674         my (@spec, @skip);
675         foreach my $glob (@globlist) {
676                 if ($glob=~/^!(.*)/) {
677                         push @skip, $glob;
678                 }
679                 else {
680                         push @spec, $glob;
681                 }
682         }
683
684         my $spec=join(" or ", @spec);
685         if (@skip) {
686                 my $skip=join(" and ", @skip);
687                 if (length $spec) {
688                         $spec="$skip and ($spec)";
689                 }
690                 else {
691                         $spec=$skip;
692                 }
693         }
694         return $spec;
695 } #}}}
696
697 sub is_globlist ($) { #{{{
698         my $s=shift;
699         $s=~/[^\s]+\s+([^\s]+)/ && $1 ne "and" && $1 ne "or";
700 } #}}}
701
702 sub safequote ($) { #{{{
703         my $s=shift;
704         $s=~s/[{}]//g;
705         return "q{$s}";
706 } #}}}
707
708 sub pagespec_merge ($$) { #{{{
709         my $a=shift;
710         my $b=shift;
711
712         return $a if $a eq $b;
713
714         # Support for old-style GlobLists.
715         if (is_globlist($a)) {
716                 $a=globlist_to_pagespec($a);
717         }
718         if (is_globlist($b)) {
719                 $b=globlist_to_pagespec($b);
720         }
721
722         return "($a) or ($b)";
723 } #}}}
724
725 sub pagespec_translate ($) { #{{{
726         # This assumes that $page is in scope in the function
727         # that evalulates the translated pagespec code.
728         my $spec=shift;
729
730         # Support for old-style GlobLists.
731         if (is_globlist($spec)) {
732                 $spec=globlist_to_pagespec($spec);
733         }
734
735         # Convert spec to perl code.
736         my $code="";
737         while ($spec=~m/\s*(\!|\(|\)|\w+\([^\)]+\)|[^\s()]+)\s*/ig) {
738                 my $word=$1;
739                 if (lc $word eq "and") {
740                         $code.=" &&";
741                 }
742                 elsif (lc $word eq "or") {
743                         $code.=" ||";
744                 }
745                 elsif ($word eq "(" || $word eq ")" || $word eq "!") {
746                         $code.=" ".$word;
747                 }
748                 elsif ($word =~ /^(link|backlink|created_before|created_after|creation_month|creation_year|creation_day)\((.+)\)$/) {
749                         $code.=" match_$1(\$page, ".safequote($2).")";
750                 }
751                 else {
752                         $code.=" match_glob(\$page, ".safequote($word).")";
753                 }
754         }
755
756         return $code;
757 } #}}}
758
759 sub add_depends ($$) { #{{{
760         my $page=shift;
761         my $pagespec=shift;
762         
763         if (! exists $depends{$page}) {
764                 $depends{$page}=$pagespec;
765         }
766         else {
767                 $depends{$page}=pagespec_merge($depends{$page}, $pagespec);
768         }
769 } # }}}
770
771 sub pagespec_match ($$) { #{{{
772         my $page=shift;
773         my $spec=shift;
774
775         return eval pagespec_translate($spec);
776 } #}}}
777
778 sub match_glob ($$) { #{{{
779         my $page=shift;
780         my $glob=shift;
781
782         # turn glob into safe regexp
783         $glob=quotemeta($glob);
784         $glob=~s/\\\*/.*/g;
785         $glob=~s/\\\?/./g;
786
787         return $page=~/^$glob$/i;
788 } #}}}
789
790 sub match_link ($$) { #{{{
791         my $page=shift;
792         my $link=lc(shift);
793
794         my $links = $links{$page} or return undef;
795         foreach my $p (@$links) {
796                 return 1 if lc $p eq $link;
797         }
798         return 0;
799 } #}}}
800
801 sub match_backlink ($$) { #{{{
802         match_link(pop, pop);
803 } #}}}
804
805 sub match_created_before ($$) { #{{{
806         my $page=shift;
807         my $testpage=shift;
808
809         if (exists $pagectime{$testpage}) {
810                 return $pagectime{$page} < $pagectime{$testpage};
811         }
812         else {
813                 return 0;
814         }
815 } #}}}
816
817 sub match_created_after ($$) { #{{{
818         my $page=shift;
819         my $testpage=shift;
820
821         if (exists $pagectime{$testpage}) {
822                 return $pagectime{$page} > $pagectime{$testpage};
823         }
824         else {
825                 return 0;
826         }
827 } #}}}
828
829 sub match_creation_day ($$) { #{{{
830         return ((gmtime($pagectime{shift()}))[3] == shift);
831 } #}}}
832
833 sub match_creation_month ($$) { #{{{
834         return ((gmtime($pagectime{shift()}))[4] + 1 == shift);
835 } #}}}
836
837 sub match_creation_year ($$) { #{{{
838         return ((gmtime($pagectime{shift()}))[5] + 1900 == shift);
839 } #}}}
840
841 1