make links dependencies fire if broken links change
[ikiwiki] / IkiWiki / Render.pm
1 #!/usr/bin/perl
2
3 package IkiWiki;
4
5 use warnings;
6 use strict;
7 use IkiWiki;
8 use Encode;
9
10 my %backlinks;
11 our %brokenlinks;
12 my $links_calculated=0;
13
14 sub calculate_links () {
15         return if $links_calculated;
16         %backlinks=%brokenlinks=();
17         foreach my $page (keys %links) {
18                 foreach my $link (@{$links{$page}}) {
19                         my $bestlink=bestlink($page, $link);
20                         if (length $bestlink) {
21                                 $backlinks{$bestlink}{$page}=1
22                                         if $bestlink ne $page;
23                         }
24                         else {
25                                 push @{$brokenlinks{$link}}, $page;
26                         }
27                 }
28         }
29         $links_calculated=1;
30 }
31
32 sub backlink_pages ($) {
33         my $page=shift;
34
35         calculate_links();
36
37         return keys %{$backlinks{$page}};
38 }
39
40 sub backlinks ($) {
41         my $page=shift;
42
43         my @links;
44         foreach my $p (backlink_pages($page)) {
45                 my $href=urlto($p, $page);
46                 
47                 # Trim common dir prefixes from both pages.
48                 my $p_trimmed=$p;
49                 my $page_trimmed=$page;
50                 my $dir;
51                 1 while (($dir)=$page_trimmed=~m!^([^/]+/)!) &&
52                         defined $dir &&
53                         $p_trimmed=~s/^\Q$dir\E// &&
54                         $page_trimmed=~s/^\Q$dir\E//;
55                                
56                 push @links, { url => $href, page => pagetitle($p_trimmed) };
57         }
58         return @links;
59 }
60
61 sub find_changed_links (@_) {
62         my %linkchanged;
63         my %linkchangers;
64         foreach my $file (@_) {
65                 my $page=pagename($file);
66         
67                 if (exists $links{$page}) {
68                         foreach my $l (@{$links{$page}}) {
69                                 my $link=bestlink($page, $l);
70                                 if (length $link) {
71                                         if (! exists $oldlinks{$page} ||
72                                             ! grep { bestlink($page, $_) eq $link } @{$oldlinks{$page}}) {
73                                                 $linkchanged{$link}=1;
74                                                 $linkchangers{lc($page)}=1;
75                                         }
76                                 }
77                                 else {
78                                         if (! grep { lc $_ eq lc $l } @{$oldlinks{$page}}) {
79                                                 $linkchangers{lc($page)}=1
80                                         }
81                                 }
82                                 
83                         }
84                 }
85                 if (exists $oldlinks{$page}) {
86                         foreach my $l (@{$oldlinks{$page}}) {
87                                 my $link=bestlink($page, $l);
88                                 if (length $link) {
89                                         if (! exists $links{$page} || 
90                                             ! grep { bestlink($page, $_) eq $link } @{$links{$page}}) {
91                                                 $linkchanged{$link}=1;
92                                                 $linkchangers{lc($page)}=1;
93                                         }
94                                 }
95                                 else {
96                                         if (! grep { lc $_ eq lc $l } @{$links{$page}}) {
97                                                 $linkchangers{lc($page)}=1
98                                         }
99                                 }
100                         }
101                 }
102         }
103
104         return \%linkchanged, \%linkchangers;
105 }
106
107 sub genpage ($$) {
108         my $page=shift;
109         my $content=shift;
110
111         my $templatefile;
112         run_hooks(templatefile => sub {
113                 return if defined $templatefile;
114                 my $file=shift->(page => $page);
115                 if (defined $file && defined template_file($file)) {
116                         $templatefile=$file;
117                 }
118         });
119         my $template=template(defined $templatefile ? $templatefile : 'page.tmpl', blind_cache => 1);
120         my $actions=0;
121
122         if (length $config{cgiurl}) {
123                 $template->param(editurl => cgiurl(do => "edit", page => $page))
124                         if IkiWiki->can("cgi_editpage");
125                 $template->param(prefsurl => cgiurl(do => "prefs"))
126                         if exists $hooks{auth};
127                 $actions++;
128         }
129                 
130         if (defined $config{historyurl} && length $config{historyurl}) {
131                 my $u=$config{historyurl};
132                 $u=~s/\[\[file\]\]/$pagesources{$page}/g;
133                 $template->param(historyurl => $u);
134                 $actions++;
135         }
136         if ($config{discussion}) {
137                 if ($page !~ /.*\/\Q$config{discussionpage}\E$/ &&
138                    (length $config{cgiurl} ||
139                     exists $links{$page."/".$config{discussionpage}})) {
140                         $template->param(discussionlink => htmllink($page, $page, $config{discussionpage}, noimageinline => 1, forcesubpage => 1));
141                         $actions++;
142                 }
143         }
144
145         if ($actions) {
146                 $template->param(have_actions => 1);
147         }
148
149         my @backlinks=sort { $a->{page} cmp $b->{page} } backlinks($page);
150         my ($backlinks, $more_backlinks);
151         if (@backlinks <= $config{numbacklinks} || ! $config{numbacklinks}) {
152                 $backlinks=\@backlinks;
153                 $more_backlinks=[];
154         }
155         else {
156                 $backlinks=[@backlinks[0..$config{numbacklinks}-1]];
157                 $more_backlinks=[@backlinks[$config{numbacklinks}..$#backlinks]];
158         }
159
160         $template->param(
161                 title => $page eq 'index' 
162                         ? $config{wikiname} 
163                         : pagetitle(basename($page)),
164                 wikiname => $config{wikiname},
165                 content => $content,
166                 backlinks => $backlinks,
167                 more_backlinks => $more_backlinks,
168                 mtime => displaytime($pagemtime{$page}),
169                 ctime => displaytime($pagectime{$page}),
170                 baseurl => baseurl($page),
171         );
172
173         run_hooks(pagetemplate => sub {
174                 shift->(page => $page, destpage => $page, template => $template);
175         });
176         
177         $content=$template->output;
178         
179         run_hooks(postscan => sub {
180                 shift->(page => $page, content => $content);
181         });
182
183         run_hooks(format => sub {
184                 $content=shift->(
185                         page => $page,
186                         content => $content,
187                 );
188         });
189
190         return $content;
191 }
192
193 sub scan ($) {
194         my $file=shift;
195
196         my $type=pagetype($file);
197         if (defined $type) {
198                 my $srcfile=srcfile($file);
199                 my $content=readfile($srcfile);
200                 my $page=pagename($file);
201                 will_render($page, htmlpage($page), 1);
202
203                 if ($config{discussion}) {
204                         # Discussion links are a special case since they're
205                         # not in the text of the page, but on its template.
206                         $links{$page}=[ $page."/".lc($config{discussionpage}) ];
207                 }
208                 else {
209                         $links{$page}=[];
210                 }
211
212                 run_hooks(scan => sub {
213                         shift->(
214                                 page => $page,
215                                 content => $content,
216                         );
217                 });
218
219                 # Preprocess in scan-only mode.
220                 preprocess($page, $page, $content, 1);
221         }
222         else {
223                 will_render($file, $file, 1);
224         }
225 }
226
227 sub fast_file_copy (@) {
228         my $srcfile=shift;
229         my $destfile=shift;
230         my $srcfd=shift;
231         my $destfd=shift;
232         my $cleanup=shift;
233
234         my $blksize = 16384;
235         my ($len, $buf, $written);
236         while ($len = sysread $srcfd, $buf, $blksize) {
237                 if (! defined $len) {
238                         next if $! =~ /^Interrupted/;
239                         error("failed to read $srcfile: $!", $cleanup);
240                 }
241                 my $offset = 0;
242                 while ($len) {
243                         defined($written = syswrite $destfd, $buf, $len, $offset)
244                                 or error("failed to write $destfile: $!", $cleanup);
245                         $len -= $written;
246                         $offset += $written;
247                 }
248         }
249 }
250
251 sub render ($) {
252         my $file=shift;
253         
254         my $type=pagetype($file);
255         my $srcfile=srcfile($file);
256         if (defined $type) {
257                 my $page=pagename($file);
258                 delete $depends{$page};
259                 delete $depends_simple{$page};
260                 will_render($page, htmlpage($page), 1);
261                 return if $type=~/^_/;
262                 
263                 my $content=htmlize($page, $page, $type,
264                         linkify($page, $page,
265                         preprocess($page, $page,
266                         filter($page, $page,
267                         readfile($srcfile)))));
268                 
269                 my $output=htmlpage($page);
270                 writefile($output, $config{destdir}, genpage($page, $content));
271         }
272         else {
273                 delete $depends{$file};
274                 delete $depends_simple{$file};
275                 will_render($file, $file, 1);
276                 
277                 if ($config{hardlink}) {
278                         # only hardlink if owned by same user
279                         my @stat=stat($srcfile);
280                         if ($stat[4] == $>) {
281                                 prep_writefile($file, $config{destdir});
282                                 unlink($config{destdir}."/".$file);
283                                 if (link($srcfile, $config{destdir}."/".$file)) {
284                                         return;
285                                 }
286                         }
287                         # if hardlink fails, fall back to copying
288                 }
289                 
290                 my $srcfd=readfile($srcfile, 1, 1);
291                 writefile($file, $config{destdir}, undef, 1, sub {
292                         fast_file_copy($srcfile, $file, $srcfd, @_);
293                 });
294         }
295 }
296
297 sub prune ($) {
298         my $file=shift;
299
300         unlink($file);
301         my $dir=dirname($file);
302         while (rmdir($dir)) {
303                 $dir=dirname($dir);
304         }
305 }
306
307 sub srcdir_check () {
308         # security check, avoid following symlinks in the srcdir path by default
309         my $test=$config{srcdir};
310         while (length $test) {
311                 if (-l $test && ! $config{allow_symlinks_before_srcdir}) {
312                         error(sprintf(gettext("symlink found in srcdir path (%s) -- set allow_symlinks_before_srcdir to allow this"), $test));
313                 }
314                 unless ($test=~s/\/+$//) {
315                         $test=dirname($test);
316                 }
317         }
318         
319 }
320
321 sub find_src_files () {
322         my (@files, %pages);
323         eval q{use File::Find};
324         error($@) if $@;
325         find({
326                 no_chdir => 1,
327                 wanted => sub {
328                         $_=decode_utf8($_);
329                         if (file_pruned($_, $config{srcdir})) {
330                                 $File::Find::prune=1;
331                         }
332                         elsif (! -l $_ && ! -d _) {
333                                 my ($f)=/$config{wiki_file_regexp}/; # untaint
334                                 if (! defined $f) {
335                                         warn(sprintf(gettext("skipping bad filename %s"), $_)."\n");
336                                 }
337                                 else {
338                                         $f=~s/^\Q$config{srcdir}\E\/?//;
339                                         push @files, $f;
340                                         my $pagename = pagename($f);
341                                         if ($pages{$pagename}) {
342                                                 debug(sprintf(gettext("%s has multiple possible source pages"), $pagename));
343                                         }
344                                         $pages{$pagename}=1;
345                                 }
346                         }
347                 },
348         }, $config{srcdir});
349         foreach my $dir (@{$config{underlaydirs}}, $config{underlaydir}) {
350                 find({
351                         no_chdir => 1,
352                         wanted => sub {
353                                 $_=decode_utf8($_);
354                                 if (file_pruned($_, $dir)) {
355                                         $File::Find::prune=1;
356                                 }
357                                 elsif (! -l $_ && ! -d _) {
358                                         my ($f)=/$config{wiki_file_regexp}/; # untaint
359                                         if (! defined $f) {
360                                                 warn(sprintf(gettext("skipping bad filename %s"), $_)."\n");
361                                         }
362                                         else {
363                                                 $f=~s/^\Q$dir\E\/?//;
364                                                 # avoid underlaydir
365                                                 # override attacks; see
366                                                 # security.mdwn
367                                                 if (! -l "$config{srcdir}/$f" && 
368                                                     ! -e _) {
369                                                         my $page=pagename($f);
370                                                         if (! $pages{$page}) {
371                                                                 push @files, $f;
372                                                                 $pages{$page}=1;
373                                                         }
374                                                 }
375                                         }
376                                 }
377                         },
378                 }, $dir);
379         };
380
381         # Returns a list of all source files found, and a hash of 
382         # the corresponding page names.
383         return \@files, \%pages;
384 }
385
386 sub refresh () {
387         srcdir_check();
388         run_hooks(refresh => sub { shift->() });
389         my ($files, $exists)=find_src_files();
390
391         my (%rendered, @add, @del, @internal, @internal_change);
392         # check for added or removed pages
393         foreach my $file (@$files) {
394                 my $page=pagename($file);
395                 if (exists $pagesources{$page} && $pagesources{$page} ne $file) {
396                         # the page has changed its type
397                         $forcerebuild{$page}=1;
398                 }
399                 $pagesources{$page}=$file;
400                 if (! $pagemtime{$page}) {
401                         if (isinternal($page)) {
402                                 push @internal, $file;
403                         }
404                         else {
405                                 push @add, $file;
406                                 if ($config{getctime} && -e "$config{srcdir}/$file") {
407                                         eval {
408                                                 my $time=rcs_getctime("$config{srcdir}/$file");
409                                                 $pagectime{$page}=$time;
410                                         };
411                                         if ($@) {
412                                                 print STDERR $@;
413                                         }
414                                 }
415                         }
416                         $pagecase{lc $page}=$page;
417                         if (! exists $pagectime{$page}) {
418                                 $pagectime{$page}=(srcfile_stat($file))[10];
419                         }
420                 }
421         }
422         foreach my $page (keys %pagemtime) {
423                 if (! $exists->{$page}) {
424                         if (isinternal($page)) {
425                                 push @internal, $pagesources{$page};
426                         }
427                         else {
428                                 debug(sprintf(gettext("removing old page %s"), $page));
429                                 push @del, $pagesources{$page};
430                         }
431                         $links{$page}=[];
432                         $renderedfiles{$page}=[];
433                         $pagemtime{$page}=0;
434                         foreach my $old (@{$oldrenderedfiles{$page}}) {
435                                 prune($config{destdir}."/".$old);
436                         }
437                         delete $pagesources{$page};
438                         foreach my $source (keys %destsources) {
439                                 if ($destsources{$source} eq $page) {
440                                         delete $destsources{$source};
441                                 }
442                         }
443                 }
444         }
445
446         # find changed and new files
447         my @needsbuild;
448         foreach my $file (@$files) {
449                 my $page=pagename($file);
450                 my ($srcfile, @stat)=srcfile_stat($file);
451                 if (! exists $pagemtime{$page} ||
452                     $stat[9] > $pagemtime{$page} ||
453                     $forcerebuild{$page}) {
454                         $pagemtime{$page}=$stat[9];
455                         if (isinternal($page)) {
456                                 push @internal_change, $file;
457                                 # Preprocess internal page in scan-only mode.
458                                 preprocess($page, $page, readfile($srcfile), 1);
459                         }
460                         else {
461                                 push @needsbuild, $file;
462                         }
463                 }
464         }
465         run_hooks(needsbuild => sub { shift->(\@needsbuild) });
466
467         # scan and render files
468         foreach my $file (@needsbuild) {
469                 debug(sprintf(gettext("scanning %s"), $file));
470                 scan($file);
471         }
472         calculate_links();
473         foreach my $file (@needsbuild) {
474                 debug(sprintf(gettext("building %s"), $file));
475                 render($file);
476                 $rendered{$file}=1;
477         }
478         foreach my $file (@internal, @internal_change) {
479                 # internal pages are not rendered
480                 my $page=pagename($file);
481                 delete $depends{$page};
482                 delete $depends_simple{$page};
483                 foreach my $old (@{$renderedfiles{$page}}) {
484                         delete $destsources{$old};
485                 }
486                 $renderedfiles{$page}=[];
487         }
488         
489         # rebuild pages that link to added or removed pages
490         if (@add || @del) {
491                 foreach my $f (@add, @del) {
492                         my $p=pagename($f);
493                         foreach my $page (keys %{$backlinks{$p}}) {
494                                 my $file=$pagesources{$page};
495                                 next if $rendered{$file};
496                                 debug(sprintf(gettext("building %s, which links to %s"), $file, $p));
497                                 render($file);
498                                 $rendered{$file}=1;
499                         }
500                 }
501         }
502
503         if (%rendered || @del || @internal || @internal_change) {
504                 my @changed=(keys %rendered, @del);
505                 my ($linkchanged, $linkchangers)=find_changed_links(@changed);
506
507                 my $unsettled;
508                 do {
509                         $unsettled=0;
510                         @changed=(keys %rendered, @del);
511                         my @exists_changed=(@add, @del);
512         
513                         my %lc_changed = map { lc(pagename($_)) => 1 } @changed;
514                         my %lc_exists_changed = map { lc(pagename($_)) => 1 } @exists_changed;
515          
516                         # rebuild dependant pages
517                         foreach my $f (@$files) {
518                                 next if $rendered{$f};
519                                 my $p=pagename($f);
520                                 my $reason = undef;
521         
522                                 if (exists $depends_simple{$p}) {
523                                         foreach my $d (keys %{$depends_simple{$p}}) {
524                                                 if (($depends_simple{$p}{$d} & $IkiWiki::DEPEND_CONTENT &&
525                                                      $lc_changed{$d})
526                                                     ||
527                                                     ($depends_simple{$p}{$d} & $IkiWiki::DEPEND_PRESENCE &&
528                                                      $lc_exists_changed{$d})
529                                                     ||
530                                                     ($depends_simple{$p}{$d} & $IkiWiki::DEPEND_LINKS &&
531                                                      $linkchangers->{$d})
532                                                 ) {
533                                                         $reason = $d;
534                                                         last;
535                                                 }
536                                         }
537                                 }
538         
539                                 if (exists $depends{$p} && ! defined $reason) {
540                                         D: foreach my $d (keys %{$depends{$p}}) {
541                                                 my $sub=pagespec_translate($d);
542                                                 next if $@ || ! defined $sub;
543         
544                                                 # only consider internal files
545                                                 # if the page explicitly depends
546                                                 # on such files
547                                                 my $internal_dep=$d =~ /internal\(/;
548
549                                                 my @candidates;
550                                                 if ($depends{$p}{$d} & $IkiWiki::DEPEND_PRESENCE) {
551                                                         @candidates=@exists_changed;
552                                                         push @candidates, @internal
553                                                                 if $internal_dep;
554                                                 }
555                                                 if (($depends{$p}{$d} & ($IkiWiki::DEPEND_CONTENT | $IkiWiki::DEPEND_LINKS))) {
556                                                         @candidates=@changed;
557                                                         push @candidates, @internal, @internal_change
558                                                                 if $internal_dep;
559                                                 }
560
561                                                 foreach my $file (@candidates) {
562                                                         next if $file eq $f;
563                                                         my $page=pagename($file);
564                                                         if ($sub->($page, location => $p)) {
565                                                                 if ($depends{$p}{$d} & $IkiWiki::DEPEND_LINKS) {
566                                                                         next unless $linkchangers->{lc($page)};
567                                                                 }
568                                                                 $reason = $page;
569                                                                 last D;
570                                                         }
571                                                 }
572                                         }
573                                 }
574         
575                                 if (defined $reason) {
576                                         debug(sprintf(gettext("building %s, which depends on %s"), $f, $reason));
577                                         render($f);
578                                         $rendered{$f}=1;
579                                         $unsettled=1;
580                                         last;
581                                 }
582                         }
583                 } while $unsettled;
584                 
585                 # update backlinks at end
586                 foreach my $link (keys %{$linkchanged}) {
587                         my $linkfile=$pagesources{$link};
588                         if (defined $linkfile) {
589                                 next if $rendered{$linkfile};
590                                 debug(sprintf(gettext("building %s, to update its backlinks"), $linkfile));
591                                 render($linkfile);
592                                 $rendered{$linkfile}=1;
593                         }
594                 }
595         }
596
597         # remove no longer rendered files
598         foreach my $src (keys %rendered) {
599                 my $page=pagename($src);
600                 foreach my $file (@{$oldrenderedfiles{$page}}) {
601                         if (! grep { $_ eq $file } @{$renderedfiles{$page}}) {
602                                 debug(sprintf(gettext("removing %s, no longer built by %s"), $file, $page));
603                                 prune($config{destdir}."/".$file);
604                         }
605                 }
606         }
607
608         if (@del) {
609                 run_hooks(delete => sub { shift->(@del) });
610         }
611         if (%rendered) {
612                 run_hooks(change => sub { shift->(keys %rendered) });
613         }
614 }
615
616 sub commandline_render () {
617         lockwiki();
618         loadindex();
619         unlockwiki();
620
621         my $srcfile=possibly_foolish_untaint($config{render});
622         my $file=$srcfile;
623         $file=~s/\Q$config{srcdir}\E\/?//;
624
625         my $type=pagetype($file);
626         die sprintf(gettext("ikiwiki: cannot build %s"), $srcfile)."\n" unless defined $type;
627         my $content=readfile($srcfile);
628         my $page=pagename($file);
629         $pagesources{$page}=$file;
630         $content=filter($page, $page, $content);
631         $content=preprocess($page, $page, $content);
632         $content=linkify($page, $page, $content);
633         $content=htmlize($page, $page, $type, $content);
634         $pagemtime{$page}=(stat($srcfile))[9];
635         $pagectime{$page}=$pagemtime{$page} if ! exists $pagectime{$page};
636
637         print genpage($page, $content);
638         exit 0;
639 }
640
641 1