]> sipb.mit.edu Git - ikiwiki.git/blob - IkiWiki/Render.pm
split up refresh
[ikiwiki.git] / 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, @needsbuild, %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         foreach my $file (@files) {
404                 my $page=pagename($file);
405                 my ($srcfile, @stat)=srcfile_stat($file);
406                 if (! exists $pagemtime{$page} ||
407                     $stat[9] > $pagemtime{$page} ||
408                     $forcerebuild{$page}) {
409                         $pagemtime{$page}=$stat[9];
410
411                         if (isinternal($page)) {
412                                 # Preprocess internal page in scan-only mode.
413                                 preprocess($page, $page, readfile($srcfile), 1);
414                                 push @internal_change, $file;
415                         }
416                         else {
417                                 push @needsbuild, $file;
418                         }
419                 }
420         }
421 }
422
423 sub calculate_old_links () {
424         foreach my $file (@needsbuild, @del) {
425                 my $page=pagename($file);
426                 if (exists $oldlinks{$page}) {
427                         foreach my $l (@{$oldlinks{$page}}) {
428                                 $oldlink_targets{$page}{$l}=bestlink($page, $l);
429                         }
430                 }
431         }
432 }
433
434 sub derender_internal ($) {
435         my $file=shift;
436         my $page=pagename($file);
437         delete $depends{$page};
438         delete $depends_simple{$page};
439         foreach my $old (@{$renderedfiles{$page}}) {
440                 delete $destsources{$old};
441         }
442         $renderedfiles{$page}=[];
443 }
444
445 sub render_linkers () {
446         foreach my $f (@new, @del) {
447                 my $p=pagename($f);
448                 foreach my $page (keys %{$backlinks{$p}}) {
449                         my $file=$pagesources{$page};
450                         render($file, sprintf(gettext("building %s, which links to %s"), $file, $p));
451                 }
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         foreach my $file (@needsbuild, @del) {
469                 my $page=pagename($file);
470                 my %link_targets;
471                 if (exists $links{$page}) {
472                         foreach my $l (@{$links{$page}}) {
473                                 my $target=bestlink($page, $l);
474                                 if (! exists $oldlink_targets{$page}{$l} ||
475                                     $target ne $oldlink_targets{$page}{$l}) {
476                                         $backlinkchanged{$l}=1;
477                                         $linkchangers{lc($page)}=1;
478                                 }
479                                 delete $oldlink_targets{$page}{$l};
480                         }
481                 }
482                 if (exists $oldlink_targets{$page} &&
483                     %{$oldlink_targets{$page}}) {
484                         foreach my $target (keys %{$oldlink_targets{$page}}) {
485                                 $backlinkchanged{$target}=1;
486                         }
487                         $linkchangers{lc($page)}=1;
488                 }
489         }
490 }
491
492 sub render_dependent () {
493         my @changed=(keys %rendered, @del);
494         my @exists_changed=(@new, @del);
495         
496         my %lc_changed = map { lc(pagename($_)) => 1 } @changed;
497         my %lc_exists_changed = map { lc(pagename($_)) => 1 } @exists_changed;
498          
499         foreach my $f (@files) {
500                 next if $rendered{$f};
501                 my $p=pagename($f);
502                 my $reason = undef;
503         
504                 if (exists $depends_simple{$p}) {
505                         foreach my $d (keys %{$depends_simple{$p}}) {
506                                 if (($depends_simple{$p}{$d} & $IkiWiki::DEPEND_CONTENT &&
507                                      $lc_changed{$d})
508                                     ||
509                                     ($depends_simple{$p}{$d} & $IkiWiki::DEPEND_PRESENCE &&
510                                      $lc_exists_changed{$d})
511                                     ||
512                                     ($depends_simple{$p}{$d} & $IkiWiki::DEPEND_LINKS &&
513                                      $linkchangers{$d})
514                                 ) {
515                                         $reason = $d;
516                                         last;
517                                 }
518                         }
519                 }
520         
521                 if (exists $depends{$p} && ! defined $reason) {
522                         D: foreach my $d (keys %{$depends{$p}}) {
523                                 my $sub=pagespec_translate($d);
524                                 next if $@ || ! defined $sub;
525
526                                 # only consider internal files
527                                 # if the page explicitly depends
528                                 # on such files
529                                 my $internal_dep=$d =~ /internal\(/;
530
531                                 my @candidates;
532                                 if ($depends{$p}{$d} & $IkiWiki::DEPEND_PRESENCE) {
533                                         @candidates=@exists_changed;
534                                         push @candidates, @internal
535                                                 if $internal_dep;
536                                 }
537                                 if (($depends{$p}{$d} & ($IkiWiki::DEPEND_CONTENT | $IkiWiki::DEPEND_LINKS))) {
538                                         @candidates=@changed;
539                                         push @candidates, @internal, @internal_change
540                                                 if $internal_dep;
541                                 }
542
543                                 foreach my $file (@candidates) {
544                                         next if $file eq $f;
545                                         my $page=pagename($file);
546                                         if ($sub->($page, location => $p)) {
547                                                 if ($depends{$p}{$d} & $IkiWiki::DEPEND_LINKS) {
548                                                         next unless $linkchangers{lc($page)};
549                                                 }
550                                                 $reason = $page;
551                                                 last D;
552                                         }
553                                 }
554                         }
555                 }
556         
557                 if (defined $reason) {
558                         render($f, sprintf(gettext("building %s, which depends on %s"), $f, $reason));
559                         return 1;
560                 }
561         }
562
563         return 0;
564 }
565
566 sub render_backlinks () {
567         foreach my $link (keys %backlinkchanged) {
568                 my $linkfile=$pagesources{$link};
569                 if (defined $linkfile) {
570                         render($linkfile, sprintf(gettext("building %s, to update its backlinks"), $linkfile));
571                 }
572         }
573 }
574
575 sub refresh () {
576         srcdir_check();
577         run_hooks(refresh => sub { shift->() });
578         @files=@{find_src_files()};
579         process_new_files();
580         process_del_files();
581         find_needsbuild();
582         run_hooks(needsbuild => sub { shift->(\@needsbuild) });
583         calculate_old_links();
584
585         foreach my $file (@needsbuild) {
586                 scan($file);
587         }
588
589         calculate_links();
590
591         foreach my $file (@needsbuild) {
592                 render($file, sprintf(gettext("building %s"), $file));
593         }
594
595         foreach my $file (@internal, @internal_change) {
596                 derender_internal($file);
597         }
598         
599         calculate_changed_links();
600         render_linkers();
601         
602         if (@needsbuild || @del || @internal || @internal_change) {
603                 1 while render_dependent();
604         }
605
606         render_backlinks();
607         remove_unrendered();
608
609         if (@del) {
610                 run_hooks(delete => sub { shift->(@del) });
611         }
612         if (%rendered) {
613                 run_hooks(change => sub { shift->(keys %rendered) });
614         }
615 }
616
617 sub commandline_render () {
618         lockwiki();
619         loadindex();
620         unlockwiki();
621
622         my $srcfile=possibly_foolish_untaint($config{render});
623         my $file=$srcfile;
624         $file=~s/\Q$config{srcdir}\E\/?//;
625
626         my $type=pagetype($file);
627         die sprintf(gettext("ikiwiki: cannot build %s"), $srcfile)."\n" unless defined $type;
628         my $content=readfile($srcfile);
629         my $page=pagename($file);
630         $pagesources{$page}=$file;
631         $content=filter($page, $page, $content);
632         $content=preprocess($page, $page, $content);
633         $content=linkify($page, $page, $content);
634         $content=htmlize($page, $page, $type, $content);
635         $pagemtime{$page}=(stat($srcfile))[9];
636         $pagectime{$page}=$pagemtime{$page} if ! exists $pagectime{$page};
637
638         print genpage($page, $content);
639         exit 0;
640 }
641
642 1