<html><head><meta name="color-scheme" content="light dark"></head><body><pre style="word-wrap: break-word; white-space: pre-wrap;">#!/usr/bin/perl
# Feed aggregation plugin.
package IkiWiki::Plugin::aggregate;

use warnings;
use strict;
use IkiWiki 3.00;
use HTML::Parser;
use HTML::Tagset;
use HTML::Entities;
use open qw{:utf8 :std};

my %feeds;
my %guids;

sub import {
	hook(type =&gt; "getopt", id =&gt; "aggregate", call =&gt; \&amp;getopt);
	hook(type =&gt; "getsetup", id =&gt; "aggregate", call =&gt; \&amp;getsetup);
	hook(type =&gt; "checkconfig", id =&gt; "aggregate", call =&gt; \&amp;checkconfig,
		last =&gt; 1);
	hook(type =&gt; "needsbuild", id =&gt; "aggregate", call =&gt; \&amp;needsbuild);
	hook(type =&gt; "preprocess", id =&gt; "aggregate", call =&gt; \&amp;preprocess);
        hook(type =&gt; "delete", id =&gt; "aggregate", call =&gt; \&amp;delete);
	hook(type =&gt; "savestate", id =&gt; "aggregate", call =&gt; \&amp;savestate);
	hook(type =&gt; "htmlize", id =&gt; "_aggregated", call =&gt; \&amp;htmlize);
	if (exists $config{aggregate_webtrigger} &amp;&amp; $config{aggregate_webtrigger}) {
		hook(type =&gt; "cgi", id =&gt; "aggregate", call =&gt; \&amp;cgi);
	}
}

sub getopt () {
        eval q{use Getopt::Long};
	error($@) if $@;
        Getopt::Long::Configure('pass_through');
        GetOptions(
		"aggregate" =&gt; \$config{aggregate},
		"aggregateinternal!" =&gt; \$config{aggregateinternal},
	);
}

sub getsetup () {
	return
		plugin =&gt; {
			safe =&gt; 1,
			rebuild =&gt; undef,
		},
		aggregateinternal =&gt; {
			type =&gt; "boolean",
			example =&gt; 1,
			description =&gt; "enable aggregation to internal pages?",
			safe =&gt; 0, # enabling needs manual transition
			rebuild =&gt; 0,
		},
		aggregate_webtrigger =&gt; {
			type =&gt; "boolean",
			example =&gt; 0,
			description =&gt; "allow aggregation to be triggered via the web?",
			safe =&gt; 1,
			rebuild =&gt; 0,
		},
		cookiejar =&gt; {
			type =&gt; "string",
			example =&gt; { file =&gt; "$ENV{HOME}/.ikiwiki/cookies" },
			safe =&gt; 0, # hooks into perl module internals
			description =&gt; "cookie control",
		},
}

sub checkconfig () {
	if (! defined $config{aggregateinternal}) {
		$config{aggregateinternal}=1;
	}
	if (! defined $config{cookiejar}) {
		$config{cookiejar}={ file =&gt; "$ENV{HOME}/.ikiwiki/cookies" };
	}

	# This is done here rather than in a refresh hook because it
	# needs to run before the wiki is locked.
	if ($config{aggregate} &amp;&amp; ! ($config{post_commit} &amp;&amp; 
	                             IkiWiki::commit_hook_enabled())) {
		launchaggregation();
	}
}

sub cgi ($) {
	my $cgi=shift;

	if (defined $cgi-&gt;param('do') &amp;&amp;
	    $cgi-&gt;param("do") eq "aggregate_webtrigger") {
		$|=1;
		print "Content-Type: text/plain\n\n";
		$config{cgi}=0;
		$config{verbose}=1;
		$config{syslog}=0;
		print gettext("Aggregation triggered via web.")."\n\n";
		if (launchaggregation()) {
			IkiWiki::lockwiki();
			IkiWiki::loadindex();
			require IkiWiki::Render;
			IkiWiki::refresh();
			IkiWiki::saveindex();
		}
		else {
			print gettext("Nothing to do right now, all feeds are up-to-date!")."\n";
		}
		exit 0;
	}
}

sub launchaggregation () {
	# See if any feeds need aggregation.
	loadstate();
	my @feeds=needsaggregate();
	return unless @feeds;
	if (! lockaggregate()) {
		debug("an aggregation process is already running");
		return;
	}
	# force a later rebuild of source pages
	$IkiWiki::forcerebuild{$_-&gt;{sourcepage}}=1
		foreach @feeds;

	# Fork a child process to handle the aggregation.
	# The parent process will then handle building the
	# result. This avoids messy code to clear state
	# accumulated while aggregating.
	defined(my $pid = fork) or error("Can't fork: $!");
	if (! $pid) {
		IkiWiki::loadindex();
		# Aggregation happens without the main wiki lock
		# being held. This allows editing pages etc while
		# aggregation is running.
		aggregate(@feeds);

		IkiWiki::lockwiki;
		# Merge changes, since aggregation state may have
		# changed on disk while the aggregation was happening.
		mergestate();
		expire();
		savestate();
		IkiWiki::unlockwiki;
		exit 0;
	}
	waitpid($pid,0);
	if ($?) {
		error "aggregation failed with code $?";
	}

	clearstate();
	unlockaggregate();

	return 1;
}

#  Pages with extension _aggregated have plain html markup, pass through.
sub htmlize (@) {
	my %params=@_;
	return $params{content};
}

# Used by ikiwiki-transition aggregateinternal.
sub migrate_to_internal {
	if (! lockaggregate()) {
		error("an aggregation process is currently running");
	}

	IkiWiki::lockwiki();
	loadstate();
	$config{verbose}=1;

	foreach my $data (values %guids) {
		next unless $data-&gt;{page};
		next if $data-&gt;{expired};
		
		$config{aggregateinternal} = 0;
		my $oldname = "$config{srcdir}/".htmlfn($data-&gt;{page});
		if (! -e $oldname) {
			$oldname = $IkiWiki::Plugin::transient::transientdir."/".htmlfn($data-&gt;{page});
		}

		my $oldoutput = $config{destdir}."/".IkiWiki::htmlpage($data-&gt;{page});
		
		$config{aggregateinternal} = 1;
		my $newname = $IkiWiki::Plugin::transient::transientdir."/".htmlfn($data-&gt;{page});
		
		debug "moving $oldname -&gt; $newname";
		if (-e $newname) {
			if (-e $oldname) {
				error("$newname already exists");
			}
			else {
				debug("already renamed to $newname?");
			}
		}
		elsif (-e $oldname) {
			rename($oldname, $newname) || error("$!");
		}
		else {
			debug("$oldname not found");
		}
		if (-e $oldoutput) {
			require IkiWiki::Render;
			debug("removing output file $oldoutput");
			IkiWiki::prune($oldoutput);
		}
	}
	
	savestate();
	IkiWiki::unlockwiki;
	
	unlockaggregate();
}

sub needsbuild (@) {
	my $needsbuild=shift;
	
	loadstate();

	foreach my $feed (values %feeds) {
		if (exists $pagesources{$feed-&gt;{sourcepage}} &amp;&amp; 
		    grep { $_ eq $pagesources{$feed-&gt;{sourcepage}} } @$needsbuild) {
			# Mark all feeds originating on this page as 
			# not yet seen; preprocess will unmark those that
			# still exist.
			markunseen($feed-&gt;{sourcepage});
		}
	}

	return $needsbuild;
}

sub preprocess (@) {
	my %params=@_;

	foreach my $required (qw{name url}) {
		if (! exists $params{$required}) {
			error sprintf(gettext("missing %s parameter"), $required)
		}
	}

	my $feed={};
	my $name=$params{name};
	if (exists $feeds{$name}) {
		$feed=$feeds{$name};
	}
	else {
		$feeds{$name}=$feed;
	}
	$feed-&gt;{name}=$name;
	$feed-&gt;{sourcepage}=$params{page};
	$feed-&gt;{url}=$params{url};
	my $dir=exists $params{dir} ? $params{dir} : $params{page}."/".titlepage($params{name});
	$dir=~s/^\/+//;
	($dir)=$dir=~/$config{wiki_file_regexp}/;
	$feed-&gt;{dir}=$dir;
	$feed-&gt;{feedurl}=defined $params{feedurl} ? $params{feedurl} : "";
	$feed-&gt;{updateinterval}=defined $params{updateinterval} ? $params{updateinterval} * 60 : 15 * 60;
	$feed-&gt;{expireage}=defined $params{expireage} ? $params{expireage} : 0;
	$feed-&gt;{expirecount}=defined $params{expirecount} ? $params{expirecount} : 0;
        if (exists $params{template}) {
                $params{template}=~s/[^-_a-zA-Z0-9]+//g;
        }
        else {
                $params{template} = "aggregatepost"
        }
	$feed-&gt;{template}=$params{template} . ".tmpl";
	delete $feed-&gt;{unseen};
	$feed-&gt;{lastupdate}=0 unless defined $feed-&gt;{lastupdate};
	$feed-&gt;{lasttry}=$feed-&gt;{lastupdate} unless defined $feed-&gt;{lasttry};
	$feed-&gt;{numposts}=0 unless defined $feed-&gt;{numposts};
	$feed-&gt;{newposts}=0 unless defined $feed-&gt;{newposts};
	$feed-&gt;{message}=gettext("new feed") unless defined $feed-&gt;{message};
	$feed-&gt;{error}=0 unless defined $feed-&gt;{error};
	$feed-&gt;{tags}=[];
	while (@_) {
		my $key=shift;
		my $value=shift;
		if ($key eq 'tag') {
			push @{$feed-&gt;{tags}}, $value;
		}
	}

	return "&lt;a href=\"".$feed-&gt;{url}."\"&gt;".$feed-&gt;{name}."&lt;/a&gt;: ".
	       ($feed-&gt;{error} ? "&lt;em&gt;" : "").$feed-&gt;{message}.
	       ($feed-&gt;{error} ? "&lt;/em&gt;" : "").
	       " (".$feed-&gt;{numposts}." ".gettext("posts").
	       ($feed-&gt;{newposts} ? "; ".$feed-&gt;{newposts}.
	                            " ".gettext("new") : "").
	       ")";
}

sub delete (@) {
	my @files=@_;

	# Remove feed data for removed pages.
	foreach my $file (@files) {
		my $page=pagename($file);
		markunseen($page);
	}
}

sub markunseen ($) {
	my $page=shift;

	foreach my $id (keys %feeds) {
		if ($feeds{$id}-&gt;{sourcepage} eq $page) {
			$feeds{$id}-&gt;{unseen}=1;
		}
	}
}

my $state_loaded=0;

sub loadstate () {
	return if $state_loaded;
	$state_loaded=1;
	if (-e "$config{wikistatedir}/aggregate") {
		open(IN, "&lt;", "$config{wikistatedir}/aggregate") ||
			die "$config{wikistatedir}/aggregate: $!";
		while (&lt;IN&gt;) {
			$_=IkiWiki::possibly_foolish_untaint($_);
			chomp;
			my $data={};
			foreach my $i (split(/ /, $_)) {
				my ($field, $val)=split(/=/, $i, 2);
				if ($field eq "name" || $field eq "feed" ||
				    $field eq "guid" || $field eq "message") {
					$data-&gt;{$field}=decode_entities($val, " \t\n");
				}
				elsif ($field eq "tag") {
					push @{$data-&gt;{tags}}, $val;
				}
				else {
					$data-&gt;{$field}=$val;
				}
			}
			
			if (exists $data-&gt;{name}) {
				$feeds{$data-&gt;{name}}=$data;
			}
			elsif (exists $data-&gt;{guid}) {
				$guids{$data-&gt;{guid}}=$data;
			}
		}

		close IN;
	}
}

sub savestate () {
	return unless $state_loaded;
	garbage_collect();
	my $newfile="$config{wikistatedir}/aggregate.new";
	my $cleanup = sub { unlink($newfile) };
	open (OUT, "&gt;", $newfile) || error("open $newfile: $!", $cleanup);
	foreach my $data (values %feeds, values %guids) {
		my @line;
		foreach my $field (keys %$data) {
			if ($field eq "name" || $field eq "feed" ||
			    $field eq "guid" || $field eq "message") {
				push @line, "$field=".encode_entities($data-&gt;{$field}, " \t\n");
			}
			elsif ($field eq "tags") {
				push @line, "tag=$_" foreach @{$data-&gt;{tags}};
			}
			else {
				push @line, "$field=".$data-&gt;{$field}
					if defined $data-&gt;{$field};
			}
		}
		print OUT join(" ", @line)."\n" || error("write $newfile: $!", $cleanup);
	}
	close OUT || error("save $newfile: $!", $cleanup);
	rename($newfile, "$config{wikistatedir}/aggregate") ||
		error("rename $newfile: $!", $cleanup);

	my $timestamp=undef;
	foreach my $feed (keys %feeds) {
		my $t=$feeds{$feed}-&gt;{lastupdate}+$feeds{$feed}-&gt;{updateinterval};
		if (! defined $timestamp || $timestamp &gt; $t) {
			$timestamp=$t;
		}
	}
	$newfile=~s/\.new$/time/;
	open (OUT, "&gt;", $newfile) || error("open $newfile: $!", $cleanup);
	if (defined $timestamp) {
		print OUT $timestamp."\n";
	}
	close OUT || error("save $newfile: $!", $cleanup);
}

sub garbage_collect () {
	foreach my $name (keys %feeds) {
		# remove any feeds that were not seen while building the pages
		# that used to contain them
		if ($feeds{$name}-&gt;{unseen}) {
			delete $feeds{$name};
		}
	}

	foreach my $guid (values %guids) {
		# any guid whose feed is gone should be removed
		if (! exists $feeds{$guid-&gt;{feed}}) {
			if (exists $guid-&gt;{page}) {
				unlink $IkiWiki::Plugin::transient::transientdir."/".htmlfn($guid-&gt;{page})
					|| unlink "$config{srcdir}/".htmlfn($guid-&gt;{page});
			}
			delete $guids{$guid-&gt;{guid}};
		}
		# handle expired guids
		elsif ($guid-&gt;{expired} &amp;&amp; exists $guid-&gt;{page}) {
			unlink "$config{srcdir}/".htmlfn($guid-&gt;{page});
			unlink $IkiWiki::Plugin::transient::transientdir."/".htmlfn($guid-&gt;{page});
			delete $guid-&gt;{page};
			delete $guid-&gt;{md5};
		}
	}
}

sub mergestate () {
	# Load the current state in from disk, and merge into it
	# values from the state in memory that might have changed
	# during aggregation.
	my %myfeeds=%feeds;
	my %myguids=%guids;
	clearstate();
	loadstate();

	# All that can change in feed state during aggregation is a few
	# fields.
	foreach my $name (keys %myfeeds) {
		if (exists $feeds{$name}) {
			foreach my $field (qw{message lastupdate lasttry
			                      numposts newposts error}) {
				$feeds{$name}-&gt;{$field}=$myfeeds{$name}-&gt;{$field};
			}
		}
	}

	# New guids can be created during aggregation.
	# Guids have a few fields that may be updated during aggregation.
	# It's also possible that guids were removed from the on-disk state
	# while the aggregation was in process. That would only happen if
	# their feed was also removed, so any removed guids added back here
	# will be garbage collected later.
	foreach my $guid (keys %myguids) {
		if (! exists $guids{$guid}) {
			$guids{$guid}=$myguids{$guid};
		}
		else {
			foreach my $field (qw{md5}) {
				$guids{$guid}-&gt;{$field}=$myguids{$guid}-&gt;{$field};
			}
		}
	}
}

sub clearstate () {
	%feeds=();
	%guids=();
	$state_loaded=0;
}

sub expire () {
	foreach my $feed (values %feeds) {
		next unless $feed-&gt;{expireage} || $feed-&gt;{expirecount};
		my $count=0;
		my %seen;
		foreach my $item (sort { ($IkiWiki::pagectime{$b-&gt;{page}} || 0) &lt;=&gt; ($IkiWiki::pagectime{$a-&gt;{page}} || 0) }
		                  grep { exists $_-&gt;{page} &amp;&amp; $_-&gt;{feed} eq $feed-&gt;{name} }
		                  values %guids) {
			if ($feed-&gt;{expireage}) {
				my $days_old = (time - ($IkiWiki::pagectime{$item-&gt;{page}} || 0)) / 60 / 60 / 24;
				if ($days_old &gt; $feed-&gt;{expireage}) {
					debug(sprintf(gettext("expiring %s (%s days old)"),
						$item-&gt;{page}, int($days_old)));
					$item-&gt;{expired}=1;
				}
			}
			elsif ($feed-&gt;{expirecount} &amp;&amp;
			       $count &gt;= $feed-&gt;{expirecount}) {
				debug(sprintf(gettext("expiring %s"), $item-&gt;{page}));
				$item-&gt;{expired}=1;
			}
			else {
				if (! $seen{$item-&gt;{page}}) {
					$seen{$item-&gt;{page}}=1;
					$count++;
				}
			}
		}
	}
}

sub needsaggregate () {
	return values %feeds if $config{rebuild};
	return grep { time - $_-&gt;{lastupdate} &gt;= $_-&gt;{updateinterval} } values %feeds;
}

sub aggregate (@) {
	eval q{use Net::INET6Glue::INET_is_INET6}; # may not be available
	eval q{use XML::Feed};
	error($@) if $@;
	eval q{use URI::Fetch};
	error($@) if $@;

	foreach my $feed (@_) {
		$feed-&gt;{lasttry}=time;
		$feed-&gt;{newposts}=0;
		$feed-&gt;{message}=sprintf(gettext("last checked %s"),
			displaytime($feed-&gt;{lasttry}));
		$feed-&gt;{error}=0;

		debug(sprintf(gettext("checking feed %s ..."), $feed-&gt;{name}));

		if (! length $feed-&gt;{feedurl}) {
			my @urls=XML::Feed-&gt;find_feeds($feed-&gt;{url});
			if (! @urls) {
				$feed-&gt;{message}=sprintf(gettext("could not find feed at %s"), $feed-&gt;{url});
				$feed-&gt;{error}=1;
				debug($feed-&gt;{message});
				next;
			}
			$feed-&gt;{feedurl}=pop @urls;
		}
		my $res=URI::Fetch-&gt;fetch($feed-&gt;{feedurl},
			UserAgent =&gt; LWP::UserAgent-&gt;new(
				cookie_jar =&gt; $config{cookiejar},
			),
		);
		if (! $res) {
			$feed-&gt;{message}=URI::Fetch-&gt;errstr;
			$feed-&gt;{error}=1;
			debug($feed-&gt;{message});
			next;
		}

		# lastupdate is only set if we were able to contact the server
		$feed-&gt;{lastupdate}=$feed-&gt;{lasttry};

		if ($res-&gt;status == URI::Fetch::URI_GONE()) {
			$feed-&gt;{message}=gettext("feed not found");
			$feed-&gt;{error}=1;
			debug($feed-&gt;{message});
			next;
		}
		my $content=$res-&gt;content;
		my $f=eval{XML::Feed-&gt;parse(\$content)};
		if ($@) {
			# One common cause of XML::Feed crashing is a feed
			# that contains invalid UTF-8 sequences. Convert
			# feed to ascii to try to work around.
			$feed-&gt;{message}.=" ".sprintf(gettext("(invalid UTF-8 stripped from feed)"));
			$f=eval {
				$content=Encode::decode_utf8($content, 0);
				XML::Feed-&gt;parse(\$content)
			};
		}
		if ($@) {
			# Another possibility is badly escaped entities.
			$feed-&gt;{message}.=" ".sprintf(gettext("(feed entities escaped)"));
			$content=~s/\&amp;(?!amp)(\w+);/&amp;amp;$1;/g;
			$f=eval {
				$content=Encode::decode_utf8($content, 0);
				XML::Feed-&gt;parse(\$content)
			};
		}
		if ($@) {
			$feed-&gt;{message}=gettext("feed crashed XML::Feed!")." ($@)";
			$feed-&gt;{error}=1;
			debug($feed-&gt;{message});
			next;
		}
		if (! $f) {
			$feed-&gt;{message}=XML::Feed-&gt;errstr;
			$feed-&gt;{error}=1;
			debug($feed-&gt;{message});
			next;
		}

		foreach my $entry ($f-&gt;entries) {
			# XML::Feed doesn't work around XML::Atom's bizarre
			# API, so we will. Real unicode strings? Yes please.
			# See [[bugs/Aggregated_Atom_feeds_are_double-encoded]]
			local $XML::Atom::ForceUnicode = 1;

			my $c=$entry-&gt;content;
			# atom feeds may have no content, only a summary
			if (! defined $c &amp;&amp; ref $entry-&gt;summary) {
				$c=$entry-&gt;summary;
			}

			add_page(
				feed =&gt; $feed,
				copyright =&gt; $f-&gt;copyright,
				title =&gt; defined $entry-&gt;title ? decode_entities($entry-&gt;title) : "untitled",
				link =&gt; $entry-&gt;link,
				content =&gt; (defined $c &amp;&amp; defined $c-&gt;body) ? $c-&gt;body : "",
				guid =&gt; defined $entry-&gt;id ? $entry-&gt;id : time."_".$feed-&gt;{name},
				ctime =&gt; $entry-&gt;issued ? ($entry-&gt;issued-&gt;epoch || time) : time,
				base =&gt; (defined $c &amp;&amp; $c-&gt;can("base")) ? $c-&gt;base : undef,
			);
		}
	}
}

sub add_page (@) {
	my %params=@_;
	
	my $feed=$params{feed};
	my $guid={};
	my $mtime;
	if (exists $guids{$params{guid}}) {
		# updating an existing post
		$guid=$guids{$params{guid}};
		return if $guid-&gt;{expired};
		write_page($feed, $guid, $mtime, \%params);
	}
	else {
		# new post
		$guid-&gt;{guid}=$params{guid};
		$guids{$params{guid}}=$guid;
		$mtime=$params{ctime};
		$feed-&gt;{numposts}++;
		$feed-&gt;{newposts}++;

		# assign it an unused page
		my $page=titlepage($params{title});
		# escape slashes and periods in title so it doesn't specify
		# directory name or trigger ".." disallowing code.
		$page=~s!([/.])!"__".ord($1)."__"!eg;
		$page=$feed-&gt;{dir}."/".$page;
		($page)=$page=~/$config{wiki_file_regexp}/;
		if (! defined $page || ! length $page) {
			$page=$feed-&gt;{dir}."/item";
		}
		my $c="";
		while (exists $IkiWiki::pagecase{lc $page.$c} ||
		       -e $IkiWiki::Plugin::transient::transientdir."/".htmlfn($page.$c) ||
		       -e "$config{srcdir}/".htmlfn($page.$c)) {
			$c++
		}

		$guid-&gt;{page}=$page;
		eval { write_page($feed, $guid, $mtime, \%params) };
		if ($@) {
			# assume failure was due to a too long filename
			# (or o
			$c="";
			$page=$feed-&gt;{dir}."/item";
			while (exists $IkiWiki::pagecase{lc $page.$c} ||
			      -e $IkiWiki::Plugin::transient::transientdir."/".htmlfn($page.$c) ||
			      -e "$config{srcdir}/".htmlfn($page.$c)) {
				$c++
			}

			$guid-&gt;{page}=$page;
			write_page($feed, $guid, $mtime, \%params);
		}

		debug(sprintf(gettext("creating new page %s"), $page));
	}
}

sub write_page ($$$$$) {
	my $feed=shift;
	my $guid=shift;
	my $mtime=shift;
	my %params=%{shift()};

	$guid-&gt;{feed}=$feed-&gt;{name};
	
	# To write or not to write? Need to avoid writing unchanged pages
	# to avoid unneccessary rebuilding. The mtime from rss cannot be
	# trusted; let's use a digest.
	eval q{use Digest::MD5 'md5_hex'};
	error($@) if $@;
	require Encode;
	my $digest=md5_hex(Encode::encode_utf8($params{content}));
	return unless ! exists $guid-&gt;{md5} || $guid-&gt;{md5} ne $digest || $config{rebuild};
	$guid-&gt;{md5}=$digest;

	# Create the page.
	my $template;
	eval {
		$template=template($feed-&gt;{template}, blind_cache =&gt; 1);
	};
	if ($@) {
		print STDERR gettext("failed to process template:")." $@";
		return;
	}
	$template-&gt;param(title =&gt; $params{title})
		if defined $params{title} &amp;&amp; length($params{title});
	$template-&gt;param(content =&gt; wikiescape(htmlabs($params{content},
		defined $params{base} ? $params{base} : $feed-&gt;{feedurl})));
	$template-&gt;param(name =&gt; $feed-&gt;{name});
	$template-&gt;param(url =&gt; $feed-&gt;{url});
	$template-&gt;param(copyright =&gt; $params{copyright})
		if defined $params{copyright} &amp;&amp; length $params{copyright};
	$template-&gt;param(permalink =&gt; IkiWiki::urlabs($params{link}, $feed-&gt;{feedurl}))
		if defined $params{link};
	if (ref $feed-&gt;{tags}) {
		$template-&gt;param(tags =&gt; [map { tag =&gt; $_ }, @{$feed-&gt;{tags}}]);
	}
	writefile(htmlfn($guid-&gt;{page}),
		$IkiWiki::Plugin::transient::transientdir, $template-&gt;output);

	if (defined $mtime &amp;&amp; $mtime &lt;= time) {
		# Set the mtime, this lets the build process get the right
		# creation time on record for the new page.
		utime $mtime, $mtime,
			$IkiWiki::Plugin::transient::transientdir."/".htmlfn($guid-&gt;{page});
		# Store it in pagectime for expiry code to use also.
		$IkiWiki::pagectime{$guid-&gt;{page}}=$mtime
			unless exists $IkiWiki::pagectime{$guid-&gt;{page}};
	}
	else {
		# Dummy value for expiry code.
		$IkiWiki::pagectime{$guid-&gt;{page}}=time
			unless exists $IkiWiki::pagectime{$guid-&gt;{page}};
	}
}

sub wikiescape ($) {
	# escape accidental wikilinks and preprocessor stuff
	return encode_entities(shift, '\[\]');
}

sub htmlabs ($$) {
	# Convert links in html from relative to absolute.
	# Note that this is a heuristic, which is not specified by the rss
	# spec and may not be right for all feeds. Also, see Debian
	# bug #381359.
	my $html=shift;
	my $urlbase=shift;

	my $ret="";
	my $p = HTML::Parser-&gt;new(api_version =&gt; 3);
	$p-&gt;handler(default =&gt; sub { $ret.=join("", @_) }, "text");
	$p-&gt;handler(start =&gt; sub {
		my ($tagname, $pos, $text) = @_;
		if (ref $HTML::Tagset::linkElements{$tagname}) {
			while (4 &lt;= @$pos) {
				# use attribute sets from right to left
				# to avoid invalidating the offsets
				# when replacing the values
				my($k_offset, $k_len, $v_offset, $v_len) =
					splice(@$pos, -4);
				my $attrname = lc(substr($text, $k_offset, $k_len));
				next unless grep { $_ eq $attrname } @{$HTML::Tagset::linkElements{$tagname}};
				next unless $v_offset; # 0 v_offset means no value
				my $v = substr($text, $v_offset, $v_len);
				$v =~ s/^([\'\"])(.*)\1$/$2/;
				my $new_v=IkiWiki::urlabs($v, $urlbase);
				$new_v =~ s/\"/&amp;quot;/g; # since we quote with ""
				substr($text, $v_offset, $v_len) = qq("$new_v");
			}
		}
		$ret.=$text;
	}, "tagname, tokenpos, text");
	$p-&gt;parse($html);
	$p-&gt;eof;

	return $ret;
}

sub htmlfn ($) {
	return shift().".".($config{aggregateinternal} ? "_aggregated" : $config{htmlext});
}

my $aggregatelock;

sub lockaggregate () {
	# Take an exclusive lock to prevent multiple concurrent aggregators.
	# Returns true if the lock was aquired.
	if (! -d $config{wikistatedir}) {
		mkdir($config{wikistatedir});
	}
	open($aggregatelock, '&gt;', "$config{wikistatedir}/aggregatelock") ||
		error ("cannot open to $config{wikistatedir}/aggregatelock: $!");
	if (! flock($aggregatelock, 2 | 4)) { # LOCK_EX | LOCK_NB
		close($aggregatelock) || error("failed closing aggregatelock: $!");
		return 0;
	}
	return 1;
}

sub unlockaggregate () {
	return close($aggregatelock) if $aggregatelock;
	return;
}

1
</pre></body></html>