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