ikiwiki/IkiWiki/Render.pm

987 lines
24 KiB
Perl
Raw Normal View History

2006-04-25 01:09:26 +02:00
#!/usr/bin/perl
package IkiWiki;
use warnings;
use strict;
use IkiWiki;
my (%backlinks, %rendered, %scanned);
our %brokenlinks;
my $links_calculated=0;
sub calculate_links () {
return if $links_calculated;
%backlinks=%brokenlinks=();
foreach my $page (keys %links) {
foreach my $link (@{$links{$page}}) {
my $bestlink=bestlink($page, $link);
if (length $bestlink) {
$backlinks{$bestlink}{$page}=1
if $bestlink ne $page;
}
else {
push @{$brokenlinks{$link}}, $page;
}
}
}
$links_calculated=1;
}
sub backlink_pages ($) {
my $page=shift;
calculate_links();
return keys %{$backlinks{$page}};
}
sub backlinks ($) {
my $page=shift;
my @links;
foreach my $p (backlink_pages($page)) {
my $href=urlto($p, $page);
2010-04-17 18:54:22 +02:00
# Trim common dir prefixes from both pages.
my $p_trimmed=$p;
my $page_trimmed=$page;
my $dir;
1 while (($dir)=$page_trimmed=~m!^([^/]+/)!) &&
defined $dir &&
$p_trimmed=~s/^\Q$dir\E// &&
$page_trimmed=~s/^\Q$dir\E//;
push @links, { url => $href, page => pagetitle($p_trimmed) };
}
return @links;
}
sub genpage ($$) {
my $page=shift;
my $content=shift;
run_hooks(indexhtml => sub {
shift->(page => $page, destpage => $page, content => $content);
});
my $templatefile;
run_hooks(templatefile => sub {
return if defined $templatefile;
my $file=shift->(page => $page);
if (defined $file && defined template_file($file)) {
$templatefile=$file;
}
});
my $template;
if (defined $templatefile) {
$template=template_depends($templatefile, $page,
blind_cache => 1);
}
else {
# no explicit depends as special case
$template=template('page.tmpl',
blind_cache => 1);
}
2010-05-15 02:20:41 +02:00
my $actions=0;
if (length $config{cgiurl}) {
2010-01-04 18:51:45 +01:00
if (IkiWiki->can("cgi_editpage")) {
$template->param(editurl => cgiurl(do => "edit", page => $page));
$actions++;
}
}
2008-07-27 01:37:25 +02:00
if (defined $config{historyurl} && length $config{historyurl}) {
my $u=$config{historyurl};
my $p=uri_escape_utf8($pagesources{$page}, '^A-Za-z0-9\-\._~/');
$u=~s/\[\[file\]\]/$p/g;
$template->param(historyurl => $u);
$actions++;
}
if ($config{discussion}) {
if ($page !~ /.*\/\Q$config{discussionpage}\E$/i &&
(length $config{cgiurl} ||
exists $links{$page."/".$config{discussionpage}})) {
$template->param(discussionlink => htmllink($page, $page, $config{discussionpage}, noimageinline => 1, forcesubpage => 1));
$actions++;
}
}
2010-05-15 02:20:41 +02:00
if ($actions) {
$template->param(have_actions => 1);
}
2010-05-15 02:20:41 +02:00
templateactions($template, $page);
my @backlinks=sort { $a->{page} cmp $b->{page} || $a->{url} cmp $b->{url} } backlinks($page);
my ($backlinks, $more_backlinks);
if (@backlinks <= $config{numbacklinks} || ! $config{numbacklinks}) {
$backlinks=\@backlinks;
$more_backlinks=[];
}
else {
$backlinks=[@backlinks[0..$config{numbacklinks}-1]];
$more_backlinks=[@backlinks[$config{numbacklinks}..$#backlinks]];
}
$template->param(
title => $page eq 'index'
? $config{wikiname}
: pagetitle(basename($page)),
wikiname => $config{wikiname},
content => $content,
backlinks => $backlinks,
more_backlinks => $more_backlinks,
mtime => displaytime($pagemtime{$page}),
ctime => displaytime($pagectime{$page}, undef, 1),
baseurl => baseurl($page),
html5 => $config{html5},
responsive_layout => $config{responsive_layout},
);
run_hooks(pagetemplate => sub {
shift->(page => $page, destpage => $page, template => $template);
});
$content=$template->output;
run_hooks(format => sub {
$content=shift->(
page => $page,
content => $content,
);
});
return $content;
}
sub scan ($) {
my $file=shift;
return if ($config{rebuild} && $phase > PHASE_SCAN) || $scanned{$file};
$scanned{$file}=1;
debug(sprintf(gettext("scanning %s"), $file));
my $type=pagetype($file);
if (defined $type) {
my $srcfile=srcfile($file);
my $content=readfile($srcfile);
my $page=pagename($file);
will_render($page, htmlpage($page), 1);
if ($config{discussion}) {
2006-11-26 20:42:40 +01:00
# Discussion links are a special case since they're
# not in the text of the page, but on its template.
$links{$page}=[ $page."/".lc($config{discussionpage}) ];
}
else {
$links{$page}=[];
}
delete $typedlinks{$page};
# Preprocess in scan-only mode.
preprocess($page, $page, $content, 1);
run_hooks(scan => sub {
shift->(
page => $page,
content => $content,
);
});
}
2006-10-29 00:24:18 +02:00
else {
will_render($file, $file, 1);
}
}
sub fast_file_copy (@) {
2008-07-01 06:42:23 +02:00
my $srcfile=shift;
my $destfile=shift;
my $srcfd=shift;
my $destfd=shift;
my $cleanup=shift;
my $blksize = 16384;
my ($len, $buf, $written);
while ($len = sysread $srcfd, $buf, $blksize) {
if (! defined $len) {
next if $! =~ /^Interrupted/;
error("failed to read $srcfile: $!", $cleanup);
}
my $offset = 0;
while ($len) {
defined($written = syswrite $destfd, $buf, $len, $offset)
or error("failed to write $destfile: $!", $cleanup);
$len -= $written;
$offset += $written;
}
}
}
sub render ($$) {
my $file=shift;
return if $rendered{$file};
debug(shift);
$rendered{$file}=1;
my $type=pagetype($file);
my $srcfile=srcfile($file);
if (defined $type) {
my $page=pagename($file);
delete $depends{$page};
delete $depends_simple{$page};
will_render($page, htmlpage($page), 1);
return if $type=~/^_/;
my $content=htmlize($page, $page, $type,
linkify($page, $page,
preprocess($page, $page,
filter($page, $page,
readfile($srcfile)))));
my $output=htmlpage($page);
writefile($output, $config{destdir}, genpage($page, $content));
}
else {
delete $depends{$file};
delete $depends_simple{$file};
will_render($file, $file, 1);
if ($config{hardlink}) {
# only hardlink if owned by same user
my @stat=stat($srcfile);
if ($stat[4] == $>) {
prep_writefile($file, $config{destdir});
unlink($config{destdir}."/".$file);
if (link($srcfile, $config{destdir}."/".$file)) {
return;
}
}
# if hardlink fails, fall back to copying
}
my $srcfd=readfile($srcfile, 1, 1);
writefile($file, $config{destdir}, undef, 1, sub {
2008-07-01 06:42:23 +02:00
fast_file_copy($srcfile, $file, $srcfd, @_);
});
}
}
sub prune ($;$) {
my $file=shift;
my $up_to=shift;
unlink($file);
my $dir=dirname($file);
while ((! defined $up_to || $dir =~ m{^\Q$up_to\E\/}) && rmdir($dir)) {
$dir=dirname($dir);
}
}
sub srcdir_check () {
# security check, avoid following symlinks in the srcdir path by default
2007-11-26 21:30:44 +01:00
my $test=$config{srcdir};
while (length $test) {
if (-l $test && ! $config{allow_symlinks_before_srcdir}) {
2008-10-29 18:38:26 +01:00
error(sprintf(gettext("symlink found in srcdir path (%s) -- set allow_symlinks_before_srcdir to allow this"), $test));
2007-11-26 21:30:44 +01:00
}
unless ($test=~s/\/+$//) {
$test=dirname($test);
}
}
2008-01-29 19:08:32 +01:00
}
2007-11-26 21:30:44 +01:00
# Finds all files in the srcdir, and the underlaydirs.
# Returns the files, and their corresponding pages.
#
# When run in only_underlay mode, adds only the underlay files to
# the files and pages passed in.
sub find_src_files (;$$$) {
my $only_underlay=shift;
2009-10-06 08:00:34 +02:00
my @files;
if (defined $_[0]) {
@files=@{shift()};
}
2009-10-06 08:00:34 +02:00
my %pages;
if (defined $_[0]) {
%pages=%{shift()};
}
eval q{use File::Find};
error($@) if $@;
Fix issues with combining unicode srcdirs and source files. A short story: Once there was a unicode string, let's call him Srcdir. Along came a crufy old File::Find, who went through a tree and pasted each of the leaves in turn onto Srcdir. But this 90's relic didn't decode the leaves -- despite some of them using unicode! Poor Srcdir, with these leaves stuck on him, tainted them with his nice unicode-ness. They didn't look like leaves at all, but instead garbage. (In other words, perl's unicode support sucks mightily, and drives us all to drink and bad storytelling. But we knew that..) So, srcdir is not normally flagged as unicode, because typically it's pure ascii. And in that case, things work ok; File::Find finds filenames, which are not yet decoded to unicode, and appends them to the srcdir, and then decode_utf8 happily converts the whole thing. But, if the srcdir does contain utf8 characters, that breaks. Or, if a Yaml setup file is used, Yaml::Syck's implicitunicode sets the unicode flag of *all* strings, even those containing only ascii. In either case, srcdir has the unicode flag set; a non-decoded filename is appended, and the flag remains set; and decode_utf8 sees the flag and does *nothing*. The result is that the filename is not decoded, so looks valid and gets skipped. File::Find only sticks the directory and filenames together in no_chdir mode .. but we need that mode for security. In order to retain the security, and avoid the problem, I made it not pass srcdir to File::Find. Instead, chdir to the srcdir, and pass ".". Since "." is ascii, the problem is avoided. Note that chdir srcdir is safe because we check for symlinks in the srcdir path. Note that it takes care to chdir back to the starting location. Because the user may have specified relative paths and so staying in the srcdir might break. A relative path could even be specifed for an underlay dir, so it chdirs back after each.
2010-06-15 22:40:37 +02:00
eval q{use Cwd};
die $@ if $@;
my $origdir=getcwd();
my $abssrcdir=Cwd::abs_path($config{srcdir});
@IkiWiki::underlayfiles=();
Fix issues with combining unicode srcdirs and source files. A short story: Once there was a unicode string, let's call him Srcdir. Along came a crufy old File::Find, who went through a tree and pasted each of the leaves in turn onto Srcdir. But this 90's relic didn't decode the leaves -- despite some of them using unicode! Poor Srcdir, with these leaves stuck on him, tainted them with his nice unicode-ness. They didn't look like leaves at all, but instead garbage. (In other words, perl's unicode support sucks mightily, and drives us all to drink and bad storytelling. But we knew that..) So, srcdir is not normally flagged as unicode, because typically it's pure ascii. And in that case, things work ok; File::Find finds filenames, which are not yet decoded to unicode, and appends them to the srcdir, and then decode_utf8 happily converts the whole thing. But, if the srcdir does contain utf8 characters, that breaks. Or, if a Yaml setup file is used, Yaml::Syck's implicitunicode sets the unicode flag of *all* strings, even those containing only ascii. In either case, srcdir has the unicode flag set; a non-decoded filename is appended, and the flag remains set; and decode_utf8 sees the flag and does *nothing*. The result is that the filename is not decoded, so looks valid and gets skipped. File::Find only sticks the directory and filenames together in no_chdir mode .. but we need that mode for security. In order to retain the security, and avoid the problem, I made it not pass srcdir to File::Find. Instead, chdir to the srcdir, and pass ".". Since "." is ascii, the problem is avoided. Note that chdir srcdir is safe because we check for symlinks in the srcdir path. Note that it takes care to chdir back to the starting location. Because the user may have specified relative paths and so staying in the srcdir might break. A relative path could even be specifed for an underlay dir, so it chdirs back after each.
2010-06-15 22:40:37 +02:00
Fix issues with combining unicode srcdirs and source files. A short story: Once there was a unicode string, let's call him Srcdir. Along came a crufy old File::Find, who went through a tree and pasted each of the leaves in turn onto Srcdir. But this 90's relic didn't decode the leaves -- despite some of them using unicode! Poor Srcdir, with these leaves stuck on him, tainted them with his nice unicode-ness. They didn't look like leaves at all, but instead garbage. In other words, perl's unicode support sucks mightily, and drives us all to drink and bad storytelling. But we knew that.. So, srcdir is not normally flagged as unicode, because typically it's pure ascii. And in that case, things work ok; File::Find finds filenames, which are not yet decoded to unicode, and appends them to the srcdir, and then decode_utf8 happily converts the whole thing. But, if the srcdir does contain utf8 characters, that breaks. Or, if a Yaml setup file is used, Yaml::Syck's implicitunicode sets the unicode flag of *all* strings, even those containing only ascii. In either case, srcdir has the unicode flag set; a non-decoded filename is appended, and decode_utf8 sees the flag and does *nothing*. The result is that the filename is not decoded, so looks valid and gets skipped. File::Find only sticks the directory and filenames together in no_chdir mode .. but we need that mode for security. In order to retain the security, and avoid the problem, I made it not pass srcdir to File::Find. Instead, chdir to the srcdir, and pass ".". Since "." is ascii, the problem is avoided. Note that it takes care to chdir back to the starting location. Because the user may have specified relative paths and so staying in the srcdir might break. A relative path could even be specifed for an underlay dir, so it chdirs back after each.
2010-06-15 22:40:37 +02:00
my ($page, $underlay);
my $helper=sub {
my $file=decode_utf8($_);
return if -l $file || -d _;
Fix issues with combining unicode srcdirs and source files. A short story: Once there was a unicode string, let's call him Srcdir. Along came a crufy old File::Find, who went through a tree and pasted each of the leaves in turn onto Srcdir. But this 90's relic didn't decode the leaves -- despite some of them using unicode! Poor Srcdir, with these leaves stuck on him, tainted them with his nice unicode-ness. They didn't look like leaves at all, but instead garbage. (In other words, perl's unicode support sucks mightily, and drives us all to drink and bad storytelling. But we knew that..) So, srcdir is not normally flagged as unicode, because typically it's pure ascii. And in that case, things work ok; File::Find finds filenames, which are not yet decoded to unicode, and appends them to the srcdir, and then decode_utf8 happily converts the whole thing. But, if the srcdir does contain utf8 characters, that breaks. Or, if a Yaml setup file is used, Yaml::Syck's implicitunicode sets the unicode flag of *all* strings, even those containing only ascii. In either case, srcdir has the unicode flag set; a non-decoded filename is appended, and the flag remains set; and decode_utf8 sees the flag and does *nothing*. The result is that the filename is not decoded, so looks valid and gets skipped. File::Find only sticks the directory and filenames together in no_chdir mode .. but we need that mode for security. In order to retain the security, and avoid the problem, I made it not pass srcdir to File::Find. Instead, chdir to the srcdir, and pass ".". Since "." is ascii, the problem is avoided. Note that chdir srcdir is safe because we check for symlinks in the srcdir path. Note that it takes care to chdir back to the starting location. Because the user may have specified relative paths and so staying in the srcdir might break. A relative path could even be specifed for an underlay dir, so it chdirs back after each.
2010-06-15 22:40:37 +02:00
$file=~s/^\.\///;
return if ! length $file;
$page = pagename($file);
if (! exists $pagesources{$page} &&
file_pruned($file)) {
no warnings 'once';
$File::Find::prune=1;
return;
}
my ($f) = $file =~ /$config{wiki_file_regexp}/; # untaint
if (! defined $f) {
warn(sprintf(gettext("skipping bad filename %s"), $file)."\n");
return;
}
if ($underlay) {
# avoid underlaydir override attacks; see security.mdwn
Fix issues with combining unicode srcdirs and source files. A short story: Once there was a unicode string, let's call him Srcdir. Along came a crufy old File::Find, who went through a tree and pasted each of the leaves in turn onto Srcdir. But this 90's relic didn't decode the leaves -- despite some of them using unicode! Poor Srcdir, with these leaves stuck on him, tainted them with his nice unicode-ness. They didn't look like leaves at all, but instead garbage. (In other words, perl's unicode support sucks mightily, and drives us all to drink and bad storytelling. But we knew that..) So, srcdir is not normally flagged as unicode, because typically it's pure ascii. And in that case, things work ok; File::Find finds filenames, which are not yet decoded to unicode, and appends them to the srcdir, and then decode_utf8 happily converts the whole thing. But, if the srcdir does contain utf8 characters, that breaks. Or, if a Yaml setup file is used, Yaml::Syck's implicitunicode sets the unicode flag of *all* strings, even those containing only ascii. In either case, srcdir has the unicode flag set; a non-decoded filename is appended, and the flag remains set; and decode_utf8 sees the flag and does *nothing*. The result is that the filename is not decoded, so looks valid and gets skipped. File::Find only sticks the directory and filenames together in no_chdir mode .. but we need that mode for security. In order to retain the security, and avoid the problem, I made it not pass srcdir to File::Find. Instead, chdir to the srcdir, and pass ".". Since "." is ascii, the problem is avoided. Note that chdir srcdir is safe because we check for symlinks in the srcdir path. Note that it takes care to chdir back to the starting location. Because the user may have specified relative paths and so staying in the srcdir might break. A relative path could even be specifed for an underlay dir, so it chdirs back after each.
2010-06-15 22:40:37 +02:00
if (! -l "$abssrcdir/$f" && ! -e _) {
if (! $pages{$page}) {
push @files, $f;
push @IkiWiki::underlayfiles, $f;
$pages{$page}=1;
}
}
}
else {
push @files, $f;
if ($pages{$page}) {
debug(sprintf(gettext("%s has multiple possible source pages"), $page));
}
$pages{$page}=1;
}
};
unless ($only_underlay) {
chdir($config{srcdir}) || die "chdir $config{srcdir}: $!";
find({
no_chdir => 1,
wanted => $helper,
}, '.');
chdir($origdir) || die "chdir $origdir: $!";
}
Fix issues with combining unicode srcdirs and source files. A short story: Once there was a unicode string, let's call him Srcdir. Along came a crufy old File::Find, who went through a tree and pasted each of the leaves in turn onto Srcdir. But this 90's relic didn't decode the leaves -- despite some of them using unicode! Poor Srcdir, with these leaves stuck on him, tainted them with his nice unicode-ness. They didn't look like leaves at all, but instead garbage. In other words, perl's unicode support sucks mightily, and drives us all to drink and bad storytelling. But we knew that.. So, srcdir is not normally flagged as unicode, because typically it's pure ascii. And in that case, things work ok; File::Find finds filenames, which are not yet decoded to unicode, and appends them to the srcdir, and then decode_utf8 happily converts the whole thing. But, if the srcdir does contain utf8 characters, that breaks. Or, if a Yaml setup file is used, Yaml::Syck's implicitunicode sets the unicode flag of *all* strings, even those containing only ascii. In either case, srcdir has the unicode flag set; a non-decoded filename is appended, and decode_utf8 sees the flag and does *nothing*. The result is that the filename is not decoded, so looks valid and gets skipped. File::Find only sticks the directory and filenames together in no_chdir mode .. but we need that mode for security. In order to retain the security, and avoid the problem, I made it not pass srcdir to File::Find. Instead, chdir to the srcdir, and pass ".". Since "." is ascii, the problem is avoided. Note that it takes care to chdir back to the starting location. Because the user may have specified relative paths and so staying in the srcdir might break. A relative path could even be specifed for an underlay dir, so it chdirs back after each.
2010-06-15 22:40:37 +02:00
$underlay=1;
foreach (@{$config{underlaydirs}}, $config{underlaydir}) {
if (chdir($_)) {
find({
no_chdir => 1,
wanted => $helper,
}, '.');
chdir($origdir) || die "chdir: $!";
}
};
Fix issues with combining unicode srcdirs and source files. A short story: Once there was a unicode string, let's call him Srcdir. Along came a crufy old File::Find, who went through a tree and pasted each of the leaves in turn onto Srcdir. But this 90's relic didn't decode the leaves -- despite some of them using unicode! Poor Srcdir, with these leaves stuck on him, tainted them with his nice unicode-ness. They didn't look like leaves at all, but instead garbage. In other words, perl's unicode support sucks mightily, and drives us all to drink and bad storytelling. But we knew that.. So, srcdir is not normally flagged as unicode, because typically it's pure ascii. And in that case, things work ok; File::Find finds filenames, which are not yet decoded to unicode, and appends them to the srcdir, and then decode_utf8 happily converts the whole thing. But, if the srcdir does contain utf8 characters, that breaks. Or, if a Yaml setup file is used, Yaml::Syck's implicitunicode sets the unicode flag of *all* strings, even those containing only ascii. In either case, srcdir has the unicode flag set; a non-decoded filename is appended, and decode_utf8 sees the flag and does *nothing*. The result is that the filename is not decoded, so looks valid and gets skipped. File::Find only sticks the directory and filenames together in no_chdir mode .. but we need that mode for security. In order to retain the security, and avoid the problem, I made it not pass srcdir to File::Find. Instead, chdir to the srcdir, and pass ".". Since "." is ascii, the problem is avoided. Note that it takes care to chdir back to the starting location. Because the user may have specified relative paths and so staying in the srcdir might break. A relative path could even be specifed for an underlay dir, so it chdirs back after each.
2010-06-15 22:40:37 +02:00
2009-10-06 08:00:34 +02:00
return \@files, \%pages;
}
# Given a hash of files that have changed, and a hash of files that were
# deleted, should return the same results as find_src_files, with the same
# sanity checks. But a lot faster!
sub process_changed_files ($$) {
my $changed_raw=shift;
my $deleted_raw=shift;
my @files;
my %pages;
foreach my $file (keys %$changed_raw) {
my $page = pagename($file);
next if ! exists $pagesources{$page} && file_pruned($file);
my ($f) = $file =~ /$config{wiki_file_regexp}/; # untaint
if (! defined $f) {
warn(sprintf(gettext("skipping bad filename %s"), $file)."\n");
next;
}
push @files, $f;
if ($pages{$page}) {
debug(sprintf(gettext("%s has multiple possible source pages"), $page));
}
$pages{$page}=1;
}
# So far, we only have the changed files. Now add in all the old
# files that were not changed or deleted, excluding ones that came
# from the underlay.
my %old_underlay;
foreach my $f (@IkiWiki::underlayfiles) {
$old_underlay{$f}=1;
}
foreach my $page (keys %pagesources) {
my $f=$pagesources{$page};
unless ($old_underlay{$f} || exists $pages{$page} || exists $deleted_raw->{$f}) {
$pages{$page}=1;
push @files, $f;
}
}
# add in the underlay
find_src_files(1, \@files, \%pages);
}
2009-10-06 08:00:34 +02:00
sub find_new_files ($) {
my $files=shift;
my @new;
my @internal_new;
my $times_noted;
2009-10-06 08:00:34 +02:00
foreach my $file (@$files) {
my $page=pagename($file);
if ($config{rcs} && $config{gettime} &&
-e "$config{srcdir}/$file") {
if (! $times_noted) {
debug(sprintf(gettext("querying %s for file creation and modification times.."), $config{rcs}));
$times_noted=1;
}
eval {
my $ctime=rcs_getctime($file);
if ($ctime > 0) {
$pagectime{$page}=$ctime;
}
};
if ($@) {
print STDERR $@;
}
my $mtime;
eval {
$mtime=rcs_getmtime($file);
};
if ($@) {
print STDERR $@;
}
elsif ($mtime > 0) {
utime($mtime, $mtime, "$config{srcdir}/$file");
}
}
if (exists $pagesources{$page} && $pagesources{$page} ne $file) {
# the page has changed its type
$forcerebuild{$page}=1;
}
$pagesources{$page}=$file;
if (! $pagemtime{$page}) {
if (isinternal($page)) {
2009-10-06 08:00:34 +02:00
push @internal_new, $file;
}
else {
push @new, $file;
2006-05-26 16:54:47 +02:00
}
$pagecase{lc $page}=$page;
if (! exists $pagectime{$page}) {
my @stat=srcfile_stat($file, 1);
# For the creation time of the page, take the
# inode change time (not creation time!) or
# the modification time, whichever is older.
my $ctime=($stat[10] < $stat[9] ? $stat[10] : $stat[9]);
$pagectime{$page}=$ctime if defined $ctime;
2006-05-26 16:54:47 +02:00
}
}
}
2009-10-06 08:00:34 +02:00
return \@new, \@internal_new;
}
2009-10-06 08:00:34 +02:00
sub find_del_files ($) {
my $pages=shift;
my @del;
my @internal_del;
2010-04-27 00:32:06 +02:00
foreach my $page (keys %pagesources) {
2009-10-06 08:00:34 +02:00
if (! $pages->{$page}) {
if (isinternal($page)) {
2009-10-06 08:00:34 +02:00
push @internal_del, $pagesources{$page};
}
else {
push @del, $pagesources{$page};
}
$links{$page}=[];
delete $typedlinks{$page};
$renderedfiles{$page}=[];
$pagemtime{$page}=0;
}
}
2009-10-06 08:00:34 +02:00
return \@del, \@internal_del;
}
sub remove_del (@) {
foreach my $file (@_) {
my $page=pagename($file);
2009-12-02 19:07:58 +01:00
if (! isinternal($page)) {
debug(sprintf(gettext("removing obsolete %s"), $page));
}
foreach my $old (@{$oldrenderedfiles{$page}}) {
prune($config{destdir}."/".$old, $config{destdir});
}
foreach my $source (keys %destsources) {
if ($destsources{$source} eq $page) {
delete $destsources{$source};
}
}
delete $pagecase{lc $page};
$delpagesources{$page}=$pagesources{$page};
delete $pagesources{$page};
}
}
2009-10-06 08:00:34 +02:00
sub find_changed ($) {
my $files=shift;
my @changed;
my @internal_changed;
foreach my $file (@$files) {
my $page=pagename($file);
my ($srcfile, @stat)=srcfile_stat($file, 1);
if (defined $srcfile &&
(! exists $pagemtime{$page} ||
$stat[9] > $pagemtime{$page} ||
$forcerebuild{$page})) {
$pagemtime{$page}=$stat[9];
if (isinternal($page)) {
my $content = readfile($srcfile);
# Preprocess internal page in scan-only mode.
preprocess($page, $page, $content, 1);
run_hooks(scan => sub {
shift->(
page => $page,
content => $content,
);
});
2009-10-06 08:00:34 +02:00
push @internal_changed, $file;
}
else {
2009-10-06 08:00:34 +02:00
push @changed, $file;
}
}
}
2009-10-06 08:00:34 +02:00
return \@changed, \@internal_changed;
}
2009-10-06 08:00:34 +02:00
sub calculate_old_links ($$) {
my ($changed, $del)=@_;
my %oldlink_targets;
foreach my $file (@$changed, @$del) {
my $page=pagename($file);
if (exists $oldlinks{$page}) {
foreach my $l (@{$oldlinks{$page}}) {
$oldlink_targets{$page}{$l}=bestlink($page, $l);
}
}
}
2009-10-06 08:00:34 +02:00
return \%oldlink_targets;
}
sub derender_internal ($) {
my $file=shift;
my $page=pagename($file);
delete $depends{$page};
delete $depends_simple{$page};
foreach my $old (@{$renderedfiles{$page}}) {
delete $destsources{$old};
}
$renderedfiles{$page}=[];
}
2009-10-06 06:06:37 +02:00
sub render_linkers ($) {
my $f=shift;
my $p=pagename($f);
foreach my $page (keys %{$backlinks{$p}}) {
my $file=$pagesources{$page};
render($file, sprintf(gettext("building %s, which links to %s"), $file, $p));
}
}
sub remove_unrendered () {
foreach my $src (keys %rendered) {
my $page=pagename($src);
foreach my $file (@{$oldrenderedfiles{$page}}) {
if (! grep { $_ eq $file } @{$renderedfiles{$page}}) {
debug(sprintf(gettext("removing %s, no longer built by %s"), $file, $page));
prune($config{destdir}."/".$file, $config{destdir});
}
}
}
}
sub link_types_changed ($$) {
# each is of the form { type => { link => 1 } }
my $new = shift;
my $old = shift;
return 0 if !defined $new && !defined $old;
return 1 if (!defined $new && %$old) || (!defined $old && %$new);
while (my ($type, $links) = each %$new) {
foreach my $link (keys %$links) {
return 1 unless exists $old->{$type}{$link};
}
}
while (my ($type, $links) = each %$old) {
foreach my $link (keys %$links) {
return 1 unless exists $new->{$type}{$link};
}
}
return 0;
}
2009-10-06 08:00:34 +02:00
sub calculate_changed_links ($$$) {
my ($changed, $del, $oldlink_targets)=@_;
my (%backlinkchanged, %linkchangers);
foreach my $file (@$changed, @$del) {
my $page=pagename($file);
if (exists $links{$page}) {
foreach my $l (@{$links{$page}}) {
my $target=bestlink($page, $l);
if (! exists $oldlink_targets->{$page}{$l} ||
$target ne $oldlink_targets->{$page}{$l}) {
2009-10-07 20:08:03 +02:00
$backlinkchanged{$target}=1;
2009-10-06 08:00:34 +02:00
$linkchangers{lc($page)}=1;
}
delete $oldlink_targets->{$page}{$l};
}
}
2009-10-06 08:00:34 +02:00
if (exists $oldlink_targets->{$page} &&
%{$oldlink_targets->{$page}}) {
2009-10-07 20:08:03 +02:00
foreach my $target (values %{$oldlink_targets->{$page}}) {
2009-10-06 08:00:34 +02:00
$backlinkchanged{$target}=1;
}
$linkchangers{lc($page)}=1;
}
# we currently assume that changing the type of a link doesn't
# change backlinks
if (!exists $linkchangers{lc($page)}) {
if (link_types_changed($typedlinks{$page}, $oldtypedlinks{$page})) {
$linkchangers{lc($page)}=1;
}
}
}
2009-10-06 08:00:34 +02:00
return \%backlinkchanged, \%linkchangers;
}
2009-10-06 08:00:34 +02:00
sub render_dependent ($$$$$$$) {
my ($files, $new, $internal_new, $del, $internal_del,
$internal_changed, $linkchangers)=@_;
my @changed=(keys %rendered, @$del);
my @exists_changed=(@$new, @$del);
my %lc_changed = map { lc(pagename($_)) => 1 } @changed;
my %lc_exists_changed = map { lc(pagename($_)) => 1 } @exists_changed;
2010-04-25 02:41:35 +02:00
foreach my $p ("templates/page.tmpl", keys %{$depends_simple{""}}) {
2010-04-25 03:13:53 +02:00
if ($rendered{$p} || grep { $_ eq $p } @$del) {
2010-04-25 02:41:35 +02:00
foreach my $f (@$files) {
next if $rendered{$f};
render($f, sprintf(gettext("building %s, which depends on %s"), $f, $p));
}
return 0;
}
}
2009-10-06 08:00:34 +02:00
foreach my $f (@$files) {
next if $rendered{$f};
my $p=pagename($f);
2010-04-25 02:41:35 +02:00
my $reason = undef;
if (exists $depends_simple{$p} && ! defined $reason) {
foreach my $d (keys %{$depends_simple{$p}}) {
if (($depends_simple{$p}{$d} & $IkiWiki::DEPEND_CONTENT &&
$lc_changed{$d})
||
($depends_simple{$p}{$d} & $IkiWiki::DEPEND_PRESENCE &&
$lc_exists_changed{$d})
||
($depends_simple{$p}{$d} & $IkiWiki::DEPEND_LINKS &&
2009-10-06 08:00:34 +02:00
$linkchangers->{$d})
) {
$reason = $d;
last;
2006-03-24 04:21:46 +01:00
}
}
}
if (exists $depends{$p} && ! defined $reason) {
foreach my $dep (keys %{$depends{$p}}) {
my $sub=pagespec_translate($dep);
next unless defined $sub;
# only consider internal files
# if the page explicitly depends
# on such files
my $internal_dep=$dep =~ /(?:internal|comment|comment_pending)\(/;
my $in=sub {
my $list=shift;
my $type=shift;
2009-10-07 00:45:22 +02:00
foreach my $file (@$list) {
next if $file eq $f;
my $page=pagename($file);
if ($sub->($page, location => $p)) {
if ($type == $IkiWiki::DEPEND_LINKS) {
next unless $linkchangers->{lc($page)};
}
2010-04-20 07:54:42 +02:00
$reason=$page;
return 1;
}
2006-03-24 04:21:46 +01:00
}
return undef;
};
if ($depends{$p}{$dep} & $IkiWiki::DEPEND_CONTENT) {
2010-04-20 07:54:42 +02:00
last if $in->(\@changed, $IkiWiki::DEPEND_CONTENT);
last if $internal_dep && (
$in->($internal_new, $IkiWiki::DEPEND_CONTENT) ||
$in->($internal_del, $IkiWiki::DEPEND_CONTENT) ||
2010-04-20 07:54:42 +02:00
$in->($internal_changed, $IkiWiki::DEPEND_CONTENT)
);
}
if ($depends{$p}{$dep} & $IkiWiki::DEPEND_PRESENCE) {
2010-04-20 07:54:42 +02:00
last if $in->(\@exists_changed, $IkiWiki::DEPEND_PRESENCE);
last if $internal_dep && (
$in->($internal_new, $IkiWiki::DEPEND_PRESENCE) ||
2010-04-20 07:54:42 +02:00
$in->($internal_del, $IkiWiki::DEPEND_PRESENCE)
);
}
if ($depends{$p}{$dep} & $IkiWiki::DEPEND_LINKS) {
2010-04-20 07:54:42 +02:00
last if $in->(\@changed, $IkiWiki::DEPEND_LINKS);
last if $internal_dep && (
$in->($internal_new, $IkiWiki::DEPEND_LINKS) ||
$in->($internal_del, $IkiWiki::DEPEND_LINKS) ||
2010-04-20 07:54:42 +02:00
$in->($internal_changed, $IkiWiki::DEPEND_LINKS)
);
2006-03-24 04:21:46 +01:00
}
}
}
if (defined $reason) {
render($f, sprintf(gettext("building %s, which depends on %s"), $f, $reason));
return 1;
}
}
return 0;
}
2009-10-06 08:00:34 +02:00
sub render_backlinks ($) {
my $backlinkchanged=shift;
foreach my $link (keys %$backlinkchanged) {
my $linkfile=$pagesources{$link};
if (defined $linkfile) {
render($linkfile, sprintf(gettext("building %s, to update its backlinks"), $linkfile));
}
}
}
sub gen_autofile ($$$) {
my $autofile=shift;
my $pages=shift;
my $del=shift;
2010-04-21 21:54:18 +02:00
if (file_pruned($autofile)) {
return;
}
2010-04-21 21:54:18 +02:00
my ($file)="$config{srcdir}/$autofile" =~ /$config{wiki_file_regexp}/; # untaint
if (! defined $file) {
return;
}
# Remember autofiles that were tried, and never try them again later.
if (exists $wikistate{$autofiles{$autofile}{plugin}}{autofile}{$autofile}) {
return;
}
$wikistate{$autofiles{$autofile}{plugin}}{autofile}{$autofile}=1;
if (srcfile($autofile, 1) || file_pruned($autofile)) {
return;
}
if (-l $file || -d _ || -e _) {
return;
}
my $page = pagename($file);
if ($pages->{$page}) {
return;
}
2010-04-21 21:54:18 +02:00
if (grep { $_ eq $autofile } @$del) {
return;
}
$autofiles{$autofile}{generator}->();
$pages->{$page}=1;
return 1;
}
sub refresh () {
$phase = PHASE_SCAN;
srcdir_check();
run_hooks(refresh => sub { shift->() });
my ($files, $pages, $new, $internal_new, $del, $internal_del, $changed, $internal_changed);
2013-11-17 01:51:09 +01:00
my $want_find_changes=$config{only_committed_changes} &&
exists $IkiWiki::hooks{rcs}{rcs_find_changes} &&
exists $IkiWiki::hooks{rcs}{rcs_get_current_rev};
if (! $config{rebuild} && $want_find_changes && defined $IkiWiki::lastrev && length $IkiWiki::lastrev) {
my ($changed_raw, $del_raw);
($changed_raw, $del_raw, $IkiWiki::lastrev) = $IkiWiki::hooks{rcs}{rcs_find_changes}{call}->($IkiWiki::lastrev);
($files, $pages)=process_changed_files($changed_raw, $del_raw);
}
else {
($files, $pages)=find_src_files();
}
2013-11-17 01:51:09 +01:00
if ($want_find_changes) {
if (! defined($IkiWiki::lastrev) || ! length $IkiWiki::lastrev) {
$IkiWiki::lastrev=$IkiWiki::hooks{rcs}{rcs_get_current_rev}{call}->();
}
}
($new, $internal_new)=find_new_files($files);
($del, $internal_del)=find_del_files($pages);
($changed, $internal_changed)=find_changed($files);
my %existingfiles;
run_hooks(needsbuild => sub {
my $ret=shift->($changed, [@$del, @$internal_del]);
if (ref $ret eq 'ARRAY' && $ret != $changed) {
if (! %existingfiles) {
foreach my $f (@$files) {
$existingfiles{$f}=1;
}
}
@$changed=grep $existingfiles{$_}, @$ret;
}
});
2009-10-06 08:00:34 +02:00
my $oldlink_targets=calculate_old_links($changed, $del);
foreach my $file (@$changed) {
scan($file);
}
foreach my $autofile (keys %autofiles) {
if (gen_autofile($autofile, $pages, $del)) {
push @{$files}, $autofile;
push @{$new}, $autofile if find_new_files([$autofile]);
push @{$changed}, $autofile if find_changed([$autofile]);
scan($autofile);
}
}
calculate_links();
# At this point it becomes OK to start matching pagespecs.
$phase = PHASE_RENDER;
# Save some memory in full rebuilds: we no longer need to keep
# track of which pages we've scanned, because we can assume the
# answer is "all of them".
%scanned = () if $config{rebuild};
remove_del(@$del, @$internal_del);
2009-10-06 08:00:34 +02:00
foreach my $file (@$changed) {
render($file, sprintf(gettext("building %s"), $file));
}
2009-10-06 08:00:34 +02:00
foreach my $file (@$internal_new, @$internal_del, @$internal_changed) {
derender_internal($file);
}
2009-10-06 06:06:37 +02:00
run_hooks(build_affected => sub {
my %affected = shift->();
while (my ($page, $message) = each %affected) {
next unless exists $pagesources{$page};
render($pagesources{$page}, $message);
}
});
2009-10-06 08:00:34 +02:00
my ($backlinkchanged, $linkchangers)=calculate_changed_links($changed,
$del, $oldlink_targets);
2009-10-06 06:06:37 +02:00
2009-10-06 08:00:34 +02:00
foreach my $file (@$new, @$del) {
2009-10-06 06:06:37 +02:00
render_linkers($file);
}
if (@$changed || @$internal_changed ||
2009-10-06 08:00:34 +02:00
@$del || @$internal_del || @$internal_new) {
1 while render_dependent($files, $new, $internal_new,
$del, $internal_del, $internal_changed,
$linkchangers);
}
2009-10-06 08:00:34 +02:00
render_backlinks($backlinkchanged);
remove_unrendered();
if (@$del || @$internal_del) {
run_hooks(delete => sub { shift->(@$del, @$internal_del) });
}
if (%rendered) {
run_hooks(rendered => sub { shift->(keys %rendered) });
run_hooks(change => sub { shift->(keys %rendered) }); # back-compat
}
my %all_changed = map { $_ => 1 }
@$new, @$changed, @$del,
@$internal_new, @$internal_changed, @$internal_del;
run_hooks(changes => sub { shift->(keys %all_changed) });
}
sub clean_rendered {
lockwiki();
loadindex();
remove_unrendered();
foreach my $page (keys %oldrenderedfiles) {
foreach my $file (@{$oldrenderedfiles{$page}}) {
prune($config{destdir}."/".$file, $config{destdir});
}
}
}
sub commandline_render () {
lockwiki();
loadindex();
unlockwiki();
# This function behaves as though it's in the render phase;
# all other files are assumed to have been scanned last time.
$phase = PHASE_RENDER;
my $srcfile=possibly_foolish_untaint($config{render});
my $file=$srcfile;
$file=~s/\Q$config{srcdir}\E\/?//;
my $type=pagetype($file);
die sprintf(gettext("ikiwiki: cannot build %s"), $srcfile)."\n" unless defined $type;
my $content=readfile($srcfile);
my $page=pagename($file);
$pagesources{$page}=$file;
$content=filter($page, $page, $content);
$content=preprocess($page, $page, $content);
$content=linkify($page, $page, $content);
$content=htmlize($page, $page, $type, $content);
$pagemtime{$page}=(stat($srcfile))[9];
$pagectime{$page}=$pagemtime{$page} if ! exists $pagectime{$page};
print genpage($page, $content);
exit 0;
}
1