]> sipb.mit.edu Git - ikiwiki.git/blob - IkiWiki/Plugin/inline.pm
Reinstate trail support in inline
[ikiwiki.git] / IkiWiki / Plugin / inline.pm
1 #!/usr/bin/perl
2 # Page inlining and blogging.
3 package IkiWiki::Plugin::inline;
4
5 use warnings;
6 use strict;
7 use Encode;
8 use IkiWiki 3.00;
9 use URI;
10
11 my %knownfeeds;
12 my %page_numfeeds;
13 my @inline;
14 my $nested=0;
15
16 sub import {
17         hook(type => "getopt", id => "inline", call => \&getopt);
18         hook(type => "getsetup", id => "inline", call => \&getsetup);
19         hook(type => "checkconfig", id => "inline", call => \&checkconfig);
20         hook(type => "sessioncgi", id => "inline", call => \&sessioncgi);
21         hook(type => "preprocess", id => "inline", 
22                 call => \&IkiWiki::preprocess_inline, scan => 1);
23         hook(type => "pagetemplate", id => "inline",
24                 call => \&IkiWiki::pagetemplate_inline);
25         hook(type => "format", id => "inline", call => \&format, first => 1);
26         # Hook to change to do pinging since it's called late.
27         # This ensures each page only pings once and prevents slow
28         # pings interrupting page builds.
29         hook(type => "rendered", id => "inline", call => \&IkiWiki::pingurl);
30 }
31
32 sub getopt () {
33         eval q{use Getopt::Long};
34         error($@) if $@;
35         Getopt::Long::Configure('pass_through');
36         GetOptions(
37                 "rss!" => \$config{rss},
38                 "atom!" => \$config{atom},
39                 "allowrss!" => \$config{allowrss},
40                 "allowatom!" => \$config{allowatom},
41                 "pingurl=s" => sub {
42                         push @{$config{pingurl}}, $_[1];
43                 },      
44         );
45 }
46
47 sub getsetup () {
48         return
49                 plugin => {
50                         safe => 1,
51                         rebuild => undef,
52                         section => "core",
53                 },
54                 rss => {
55                         type => "boolean",
56                         example => 0,
57                         description => "enable rss feeds by default?",
58                         safe => 1,
59                         rebuild => 1,
60                 },
61                 atom => {
62                         type => "boolean",
63                         example => 0,
64                         description => "enable atom feeds by default?",
65                         safe => 1,
66                         rebuild => 1,
67                 },
68                 allowrss => {
69                         type => "boolean",
70                         example => 0,
71                         description => "allow rss feeds to be used?",
72                         safe => 1,
73                         rebuild => 1,
74                 },
75                 allowatom => {
76                         type => "boolean",
77                         example => 0,
78                         description => "allow atom feeds to be used?",
79                         safe => 1,
80                         rebuild => 1,
81                 },
82                 pingurl => {
83                         type => "string",
84                         example => "http://rpc.technorati.com/rpc/ping",
85                         description => "urls to ping (using XML-RPC) on feed update",
86                         safe => 1,
87                         rebuild => 0,
88                 },
89 }
90
91 sub checkconfig () {
92         if (($config{rss} || $config{atom}) && ! length $config{url}) {
93                 error(gettext("Must specify url to wiki with --url when using --rss or --atom"));
94         }
95         if ($config{rss}) {
96                 push @{$config{wiki_file_prune_regexps}}, qr/\.rss$/;
97         }
98         if ($config{atom}) {
99                 push @{$config{wiki_file_prune_regexps}}, qr/\.atom$/;
100         }
101         if (! exists $config{pingurl}) {
102                 $config{pingurl}=[];
103         }
104 }
105
106 sub format (@) {
107         my %params=@_;
108
109         # Fill in the inline content generated earlier. This is actually an
110         # optimisation.
111         $params{content}=~s{<div class="inline" id="([^"]+)"></div>}{
112                 delete @inline[$1,]
113         }eg;
114         return $params{content};
115 }
116
117 sub sessioncgi ($$) {
118         my $q=shift;
119         my $session=shift;
120
121         if ($q->param('do') eq 'blog') {
122                 my $page=titlepage(decode_utf8($q->param('title')));
123                 $page=~s/(\/)/"__".ord($1)."__"/eg; # don't create subdirs
124                 # if the page already exists, munge it to be unique
125                 my $from=$q->param('from');
126                 my $add="";
127                 while (exists $IkiWiki::pagecase{lc($from."/".$page.$add)}) {
128                         $add=1 unless length $add;
129                         $add++;
130                 }
131                 $q->param('page', "/$from/$page$add");
132                 # now go create the page
133                 $q->param('do', 'create');
134                 # make sure the editpage plugin is loaded
135                 if (IkiWiki->can("cgi_editpage")) {
136                         IkiWiki::cgi_editpage($q, $session);
137                 }
138                 else {
139                         error(gettext("page editing not allowed"));
140                 }
141                 exit;
142         }
143 }
144
145 # Back to ikiwiki namespace for the rest, this code is very much
146 # internal to ikiwiki even though it's separated into a plugin.
147 package IkiWiki;
148
149 my %toping;
150 my %feedlinks;
151
152 sub preprocess_inline (@) {
153         my %params=@_;
154         
155         if (! exists $params{pages} && ! exists $params{pagenames}) {
156                 error gettext("missing pages parameter");
157         }
158
159         if (! defined wantarray) {
160                 # Running in scan mode: only do the essentials
161
162                 if (yesno($params{trail}) && IkiWiki::Plugin::trail->can("preprocess_trailitems")) {
163                         # default to sorting age, the same as inline itself,
164                         # but let the params override that
165                         IkiWiki::Plugin::trail::preprocess_trailitems(sort => 'age', %params);
166                 }
167
168                 return;
169         }
170
171         if (yesno($params{trail}) && IkiWiki::Plugin::trail->can("preprocess_trailitems")) {
172                 scalar IkiWiki::Plugin::trail::preprocess_trailitems(sort => 'age', %params);
173         }
174
175         my $raw=yesno($params{raw});
176         my $archive=yesno($params{archive});
177         my $rss=(($config{rss} || $config{allowrss}) && exists $params{rss}) ? yesno($params{rss}) : $config{rss};
178         my $atom=(($config{atom} || $config{allowatom}) && exists $params{atom}) ? yesno($params{atom}) : $config{atom};
179         my $quick=exists $params{quick} ? yesno($params{quick}) : 0;
180         my $feeds=exists $params{feeds} ? yesno($params{feeds}) : !$quick && ! $raw;
181         my $emptyfeeds=exists $params{emptyfeeds} ? yesno($params{emptyfeeds}) : 1;
182         my $feedonly=yesno($params{feedonly});
183         if (! exists $params{show} && ! $archive) {
184                 $params{show}=10;
185         }
186         if (! exists $params{feedshow} && exists $params{show}) {
187                 $params{feedshow}=$params{show};
188         }
189         my $desc;
190         if (exists $params{description}) {
191                 $desc = $params{description} 
192         }
193         else {
194                 $desc = $config{wikiname};
195         }
196         my $actions=yesno($params{actions});
197         if (exists $params{template}) {
198                 $params{template}=~s/[^-_a-zA-Z0-9]+//g;
199         }
200         else {
201                 $params{template} = $archive ? "archivepage" : "inlinepage";
202         }
203
204         my @list;
205
206         if (exists $params{pagenames}) {
207                 foreach my $p (qw(sort pages)) {
208                         if (exists $params{$p}) {
209                                 error sprintf(gettext("the %s and %s parameters cannot be used together"),
210                                         "pagenames", $p);
211                         }
212                 }
213
214                 @list = grep { $_ ne '' } 
215                         map { bestlink($params{page}, $_) }
216                         split ' ', $params{pagenames};
217
218                 if (yesno($params{reverse})) {
219                         @list=reverse(@list);
220                 }
221
222                 foreach my $p (@list) {
223                         add_depends($params{page}, $p, deptype($quick ? "presence" : "content"));
224                 }
225         }
226         else {
227                 my $num=0;
228                 if ($params{show}) {
229                         $num=$params{show};
230                 }
231                 if ($params{feedshow} && $num < $params{feedshow} && $num > 0) {
232                         $num=$params{feedshow};
233                 }
234                 if ($params{skip} && $num) {
235                         $num+=$params{skip};
236                 }
237
238                 @list = pagespec_match_list($params{page}, $params{pages},
239                         deptype => deptype($quick ? "presence" : "content"),
240                         filter => sub { $_[0] eq $params{page} },
241                         sort => exists $params{sort} ? $params{sort} : "age",
242                         reverse => yesno($params{reverse}),
243                         ($num ? (num => $num) : ()),
244                 );
245         }
246
247         if (exists $params{skip}) {
248                 @list=@list[$params{skip} .. $#list];
249         }
250         
251         my @feedlist;
252         if ($feeds) {
253                 if (exists $params{feedshow} &&
254                     $params{feedshow} && @list > $params{feedshow}) {
255                         @feedlist=@list[0..$params{feedshow} - 1];
256                 }
257                 else {
258                         @feedlist=@list;
259                 }
260         }
261         
262         if ($params{show} && @list > $params{show}) {
263                 @list=@list[0..$params{show} - 1];
264         }
265
266         if ($feeds && exists $params{feedpages}) {
267                 @feedlist = pagespec_match_list(
268                         $params{page}, "($params{pages}) and ($params{feedpages})",
269                         deptype => deptype($quick ? "presence" : "content"),
270                         list => \@feedlist,
271                 );
272         }
273
274         my ($feedbase, $feednum);
275         if ($feeds) {
276                 # Ensure that multiple feeds on a page go to unique files.
277                 
278                 # Feedfile can lead to conflicts if usedirs is not enabled,
279                 # so avoid supporting it in that case.
280                 delete $params{feedfile} if ! $config{usedirs};
281                 # Tight limits on legal feedfiles, to avoid security issues
282                 # and conflicts.
283                 if (defined $params{feedfile}) {
284                         if ($params{feedfile} =~ /\// ||
285                             $params{feedfile} !~ /$config{wiki_file_regexp}/) {
286                                 error("illegal feedfile");
287                         }
288                         $params{feedfile}=possibly_foolish_untaint($params{feedfile});
289                 }
290                 $feedbase=targetpage($params{page}, "", $params{feedfile});
291
292                 my $feedid=join("\0", $feedbase, map { $_."\0".$params{$_} } sort keys %params);
293                 if (exists $knownfeeds{$feedid}) {
294                         $feednum=$knownfeeds{$feedid};
295                 }
296                 else {
297                         if (exists $page_numfeeds{$params{destpage}}{$feedbase}) {
298                                 if ($feeds) {
299                                         $feednum=$knownfeeds{$feedid}=++$page_numfeeds{$params{destpage}}{$feedbase};
300                                 }
301                         }
302                         else {
303                                 $feednum=$knownfeeds{$feedid}="";
304                                 if ($feeds) {
305                                         $page_numfeeds{$params{destpage}}{$feedbase}=1;
306                                 }
307                         }
308                 }
309         }
310
311         my ($rssurl, $atomurl, $rssdesc, $atomdesc);
312         if ($feeds) {
313                 if ($rss) {
314                         $rssurl=abs2rel($feedbase."rss".$feednum, dirname(htmlpage($params{destpage})));
315                         $rssdesc = sprintf(gettext("%s (RSS feed)"), $desc);
316                 }
317                 if ($atom) {
318                         $atomurl=abs2rel($feedbase."atom".$feednum, dirname(htmlpage($params{destpage})));
319                         $atomdesc = sprintf(gettext("%s (Atom feed)"), $desc);
320                 }
321         }
322
323         my $ret="";
324
325         if (length $config{cgiurl} && ! $params{preview} && (exists $params{rootpage} ||
326             (exists $params{postform} && yesno($params{postform}))) &&
327             IkiWiki->can("cgi_editpage")) {
328                 # Add a blog post form, with feed buttons.
329                 my $formtemplate=template_depends("blogpost.tmpl", $params{page}, blind_cache => 1);
330                 $formtemplate->param(cgiurl => IkiWiki::cgiurl());
331                 $formtemplate->param(rootpage => rootpage(%params));
332                 if ($feeds) {
333                         if ($rss) {
334                                 $formtemplate->param(rssurl => $rssurl);
335                                 $formtemplate->param(rssdesc => $rssdesc);
336                         }
337                         if ($atom) {
338                                 $formtemplate->param(atomurl => $atomurl);
339                                 $formtemplate->param(atomdesc => $atomdesc);
340                         }
341                 }
342                 if (exists $params{postformtext}) {
343                         $formtemplate->param(postformtext =>
344                                 $params{postformtext});
345                 }
346                 else {
347                         $formtemplate->param(postformtext =>
348                                 gettext("Add a new post titled:"));
349                 }
350                 if (exists $params{id}) {
351                         $formtemplate->param(postformid =>
352                                 $params{id});
353                 }
354                 $ret.=$formtemplate->output;
355                 
356                 # The post form includes the feed buttons, so
357                 # emptyfeeds cannot be hidden.
358                 $emptyfeeds=1;
359         }
360         elsif ($feeds && !$params{preview} && ($emptyfeeds || @feedlist)) {
361                 # Add feed buttons.
362                 my $linktemplate=template_depends("feedlink.tmpl", $params{page}, blind_cache => 1);
363                 if ($rss) {
364                         $linktemplate->param(rssurl => $rssurl);
365                         $linktemplate->param(rssdesc => $rssdesc);
366                 }
367                 if ($atom) {
368                         $linktemplate->param(atomurl => $atomurl);
369                         $linktemplate->param(atomdesc => $atomdesc);
370                 }
371                 if (exists $params{id}) {
372                         $linktemplate->param(id => $params{id});
373                 }
374                 $ret.=$linktemplate->output;
375         }
376         
377         if (! $feedonly) {
378                 my $template;
379                 if (! $raw) {
380                         # cannot use wiki pages as templates; template not sanitized due to
381                         # format hook hack
382                         eval {
383                                 $template=template_depends($params{template}.".tmpl", $params{page},
384                                         blind_cache => 1);
385                         };
386                         if ($@) {
387                                 error sprintf(gettext("failed to process template %s"), $params{template}.".tmpl").": $@";
388                         }
389                 }
390                 my $needcontent=$raw || (!($archive && $quick) && $template->query(name => 'content'));
391         
392                 foreach my $page (@list) {
393                         my $file = $pagesources{$page};
394                         my $type = pagetype($file);
395                         if (! $raw) {
396                                 if ($needcontent) {
397                                         # Get the content before populating the
398                                         # template, since getting the content uses
399                                         # the same template if inlines are nested.
400                                         my $content=get_inline_content($page, $params{destpage});
401                                         $template->param(content => $content);
402                                 }
403                                 $template->param(pageurl => urlto($page, $params{destpage}));
404                                 $template->param(inlinepage => $page);
405                                 $template->param(title => pagetitle(basename($page)));
406                                 $template->param(ctime => displaytime($pagectime{$page}, $params{timeformat}, 1));
407                                 $template->param(mtime => displaytime($pagemtime{$page}, $params{timeformat}));
408                                 $template->param(first => 1) if $page eq $list[0];
409                                 $template->param(last => 1) if $page eq $list[$#list];
410                                 $template->param(html5 => $config{html5});
411         
412                                 if ($actions) {
413                                         my $file = $pagesources{$page};
414                                         my $type = pagetype($file);
415                                         if ($config{discussion}) {
416                                                 if ($page !~ /.*\/\Q$config{discussionpage}\E$/i &&
417                                                     (length $config{cgiurl} ||
418                                                      exists $pagesources{$page."/".lc($config{discussionpage})})) {
419                                                         $template->param(have_actions => 1);
420                                                         $template->param(discussionlink =>
421                                                                 htmllink($page,
422                                                                         $params{destpage},
423                                                                         $config{discussionpage},
424                                                                         noimageinline => 1,
425                                                                         forcesubpage => 1));
426                                                 }
427                                         }
428                                         if (length $config{cgiurl} &&
429                                             defined $type &&
430                                             IkiWiki->can("cgi_editpage")) {
431                                                 $template->param(have_actions => 1);
432                                                 $template->param(editurl => cgiurl(do => "edit", page => $page));
433
434                                         }
435                                 }
436         
437                                 run_hooks(pagetemplate => sub {
438                                         shift->(page => $page, destpage => $params{destpage},
439                                                 template => $template,);
440                                 });
441         
442                                 $ret.=$template->output;
443                                 $template->clear_params;
444                         }
445                         else {
446                                 if (defined $type) {
447                                         $ret.="\n".
448                                               linkify($page, $params{destpage},
449                                               preprocess($page, $params{destpage},
450                                               filter($page, $params{destpage},
451                                               readfile(srcfile($file)))));
452                                 }
453                                 else {
454                                         $ret.="\n".
455                                               readfile(srcfile($file));
456                                 }
457                         }
458                 }
459         }
460         
461         if ($feeds && ($emptyfeeds || @feedlist)) {
462                 if ($rss) {
463                         my $rssp=$feedbase."rss".$feednum;
464                         will_render($params{destpage}, $rssp);
465                         if (! $params{preview}) {
466                                 writefile($rssp, $config{destdir},
467                                         genfeed("rss",
468                                                 $config{url}."/".$rssp, $desc, $params{guid}, $params{page}, @feedlist));
469                                 $toping{$params{destpage}}=1 unless $config{rebuild};
470                                 $feedlinks{$params{destpage}}.=qq{<link rel="alternate" type="application/rss+xml" title="$rssdesc" href="$rssurl" />};
471                         }
472                 }
473                 if ($atom) {
474                         my $atomp=$feedbase."atom".$feednum;
475                         will_render($params{destpage}, $atomp);
476                         if (! $params{preview}) {
477                                 writefile($atomp, $config{destdir},
478                                         genfeed("atom", $config{url}."/".$atomp, $desc, $params{guid}, $params{page}, @feedlist));
479                                 $toping{$params{destpage}}=1 unless $config{rebuild};
480                                 $feedlinks{$params{destpage}}.=qq{<link rel="alternate" type="application/atom+xml" title="$atomdesc" href="$atomurl" />};
481                         }
482                 }
483         }
484         
485         clear_inline_content_cache();
486
487         return $ret if $raw || $nested;
488         push @inline, $ret;
489         return "<div class=\"inline\" id=\"$#inline\"></div>\n\n";
490 }
491
492 sub pagetemplate_inline (@) {
493         my %params=@_;
494         my $page=$params{page};
495         my $template=$params{template};
496
497         $template->param(feedlinks => $feedlinks{$page})
498                 if exists $feedlinks{$page} && $template->query(name => "feedlinks");
499 }
500
501 {
502 my %inline_content;
503 my $cached_destpage="";
504
505 sub get_inline_content ($$) {
506         my $page=shift;
507         my $destpage=shift;
508         
509         if (exists $inline_content{$page} && $cached_destpage eq $destpage) {
510                 return $inline_content{$page};
511         }
512
513         my $file=$pagesources{$page};
514         my $type=pagetype($file);
515         my $ret="";
516         if (defined $type) {
517                 $nested++;
518                 $ret=htmlize($page, $destpage, $type,
519                        linkify($page, $destpage,
520                        preprocess($page, $destpage,
521                        filter($page, $destpage,
522                        readfile(srcfile($file))))));
523                 $nested--;
524                 if (isinternal($page)) {
525                         # make inlined text of internal pages searchable
526                         run_hooks(indexhtml => sub {
527                                 shift->(page => $page, destpage => $destpage,
528                                         content => $ret);
529                         });
530                 }
531         }
532         
533         if ($cached_destpage ne $destpage) {
534                 clear_inline_content_cache();
535                 $cached_destpage=$destpage;
536         }
537         return $inline_content{$page}=$ret;
538 }
539
540 sub clear_inline_content_cache () {
541         %inline_content=();
542 }
543
544 }
545
546 sub date_822 ($) {
547         my $time=shift;
548
549         my $lc_time=POSIX::setlocale(&POSIX::LC_TIME);
550         POSIX::setlocale(&POSIX::LC_TIME, "C");
551         my $ret=POSIX::strftime("%a, %d %b %Y %H:%M:%S %z", localtime($time));
552         POSIX::setlocale(&POSIX::LC_TIME, $lc_time);
553         return $ret;
554 }
555
556 sub absolute_urls ($$) {
557         # needed because rss sucks
558         my $html=shift;
559         my $baseurl=shift;
560
561         my $url=$baseurl;
562         $url=~s/[^\/]+$//;
563         my $urltop; # calculated if needed
564
565         my $ret="";
566
567         eval q{use HTML::Parser; use HTML::Tagset};
568         die $@ if $@;
569         my $p = HTML::Parser->new(api_version => 3);
570         $p->handler(default => sub { $ret.=join("", @_) }, "text");
571         $p->handler(start => sub {
572                 my ($tagname, $pos, $text) = @_;
573                 if (ref $HTML::Tagset::linkElements{$tagname}) {
574                         while (4 <= @$pos) {
575                                 # use attribute sets from right to left
576                                 # to avoid invalidating the offsets
577                                 # when replacing the values
578                                 my ($k_offset, $k_len, $v_offset, $v_len) =
579                                         splice(@$pos, -4);
580                                 my $attrname = lc(substr($text, $k_offset, $k_len));
581                                 next unless grep { $_ eq $attrname } @{$HTML::Tagset::linkElements{$tagname}};
582                                 next unless $v_offset; # 0 v_offset means no value
583                                 my $v = substr($text, $v_offset, $v_len);
584                                 $v =~ s/^([\'\"])(.*)\1$/$2/;
585                                 eval q{use HTML::Entities};
586                                 my $dv = decode_entities($v);
587                                 if ($dv=~/^#/) {
588                                         $v=$baseurl.$v; # anchor
589                                 }
590                                 elsif ($dv=~/^(?!\w+:)[^\/]/) {
591                                         $v=$url.$v; # relative url
592                                 }
593                                 elsif ($dv=~/^\//) {
594                                         if (! defined $urltop) {
595                                                 # what is the non path part of the url?
596                                                 my $top_uri = URI->new($url);
597                                                 $top_uri->path_query(""); # reset the path
598                                                 $urltop = $top_uri->as_string;
599                                         }
600                                         $v=$urltop.$v; # url relative to top of site
601                                 }
602                                 $v =~ s/\"/&quot;/g; # since we quote with ""
603                                 substr($text, $v_offset, $v_len) = qq("$v");
604                         }
605                 }
606                 $ret.=$text;
607         }, "tagname, tokenpos, text");
608         $p->parse($html);
609         $p->eof;
610
611         return $ret;
612 }
613
614 sub genfeed ($$$$$@) {
615         my $feedtype=shift;
616         my $feedurl=shift;
617         my $feeddesc=shift;
618         my $guid=shift;
619         my $page=shift;
620         my @pages=@_;
621         
622         my $url=URI->new(encode_utf8(urlto($page,"",1)));
623         
624         my $itemtemplate=template_depends($feedtype."item.tmpl", $page, blind_cache => 1);
625         my $content="";
626         my $lasttime = 0;
627         foreach my $p (@pages) {
628                 my $u=URI->new(encode_utf8(urlto($p, "", 1)));
629                 my $pcontent = absolute_urls(get_inline_content($p, $page), $url);
630
631                 $itemtemplate->param(
632                         title => pagetitle(basename($p)),
633                         url => $u,
634                         permalink => $u,
635                         cdate_822 => date_822($pagectime{$p}),
636                         mdate_822 => date_822($pagemtime{$p}),
637                         cdate_3339 => date_3339($pagectime{$p}),
638                         mdate_3339 => date_3339($pagemtime{$p}),
639                 );
640
641                 if (exists $pagestate{$p}) {
642                         if (exists $pagestate{$p}{meta}{guid}) {
643                                 eval q{use HTML::Entities};
644                                 $itemtemplate->param(guid => HTML::Entities::encode_numeric($pagestate{$p}{meta}{guid}));
645                         }
646
647                         if (exists $pagestate{$p}{meta}{updated}) {
648                                 $itemtemplate->param(mdate_822 => date_822($pagestate{$p}{meta}{updated}));
649                                 $itemtemplate->param(mdate_3339 => date_3339($pagestate{$p}{meta}{updated}));
650                         }
651                 }
652
653                 if ($itemtemplate->query(name => "enclosure")) {
654                         my $file=$pagesources{$p};
655                         my $type=pagetype($file);
656                         if (defined $type) {
657                                 $itemtemplate->param(content => $pcontent);
658                         }
659                         else {
660                                 my $size=(srcfile_stat($file))[8];
661                                 my $mime="unknown";
662                                 eval q{use File::MimeInfo};
663                                 if (! $@) {
664                                         $mime = mimetype($file);
665                                 }
666                                 $itemtemplate->param(
667                                         enclosure => $u,
668                                         type => $mime,
669                                         length => $size,
670                                 );
671                         }
672                 }
673                 else {
674                         $itemtemplate->param(content => $pcontent);
675                 }
676
677                 run_hooks(pagetemplate => sub {
678                         shift->(page => $p, destpage => $page,
679                                 template => $itemtemplate);
680                 });
681
682                 $content.=$itemtemplate->output;
683                 $itemtemplate->clear_params;
684
685                 $lasttime = $pagemtime{$p} if $pagemtime{$p} > $lasttime;
686         }
687
688         my $template=template_depends($feedtype."page.tmpl", $page, blind_cache => 1);
689         $template->param(
690                 title => $page ne "index" ? pagetitle($page) : $config{wikiname},
691                 wikiname => $config{wikiname},
692                 pageurl => $url,
693                 content => $content,
694                 feeddesc => $feeddesc,
695                 guid => $guid,
696                 feeddate => date_3339($lasttime),
697                 feedurl => $feedurl,
698         );
699         run_hooks(pagetemplate => sub {
700                 shift->(page => $page, destpage => $page,
701                         template => $template);
702         });
703         
704         return $template->output;
705 }
706
707 sub pingurl (@) {
708         return unless @{$config{pingurl}} && %toping;
709
710         eval q{require RPC::XML::Client};
711         if ($@) {
712                 debug(gettext("RPC::XML::Client not found, not pinging"));
713                 return;
714         }
715
716         # daemonize here so slow pings don't slow down wiki updates
717         defined(my $pid = fork) or error("Can't fork: $!");
718         return if $pid;
719         chdir '/';
720         POSIX::setsid() or error("Can't start a new session: $!");
721         open STDIN, '/dev/null';
722         open STDOUT, '>/dev/null';
723         open STDERR, '>&STDOUT' or error("Can't dup stdout: $!");
724
725         # Don't need to keep a lock on the wiki as a daemon.
726         IkiWiki::unlockwiki();
727
728         foreach my $page (keys %toping) {
729                 my $title=pagetitle(basename($page), 0);
730                 my $url=urlto($page, "", 1);
731                 foreach my $pingurl (@{$config{pingurl}}) {
732                         debug("Pinging $pingurl for $page");
733                         eval {
734                                 my $client = RPC::XML::Client->new($pingurl);
735                                 my $req = RPC::XML::request->new('weblogUpdates.ping',
736                                         $title, $url);
737                                 my $res = $client->send_request($req);
738                                 if (! ref $res) {
739                                         error("Did not receive response to ping");
740                                 }
741                                 my $r=$res->value;
742                                 if (! exists $r->{flerror} || $r->{flerror}) {
743                                         error("Ping rejected: ".(exists $r->{message} ? $r->{message} : "[unknown reason]"));
744                                 }
745                         };
746                         if ($@) {
747                                 error "Ping failed: $@";
748                         }
749                 }
750         }
751
752         exit 0; # daemon done
753 }
754
755
756 sub rootpage (@) {
757         my %params=@_;
758
759         my $rootpage;
760         if (exists $params{rootpage}) {
761                 $rootpage=bestlink($params{page}, $params{rootpage});
762                 if (!length $rootpage) {
763                         $rootpage=$params{rootpage};
764                 }
765         }
766         else {
767                 $rootpage=$params{page};
768         }
769         return $rootpage;
770 }
771
772 1