]> sipb.mit.edu Git - ikiwiki.git/blob - IkiWiki/Plugin/inline.pm
Merge branch 'master' into autoconfig
[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 2.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);
23         hook(type => "pagetemplate", id => "inline",
24                 call => \&IkiWiki::pagetemplate_inline);
25         hook(type => "format", id => "inline", call => \&format);
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 => "change", id => "inline", 
30                 call => \&IkiWiki::pingurl);
31
32 } # }}}
33
34 sub getopt () { #{{{
35         eval q{use Getopt::Long};
36         error($@) if $@;
37         Getopt::Long::Configure('pass_through');
38         GetOptions(
39                 "rss!" => \$config{rss},
40                 "atom!" => \$config{atom},
41                 "allowrss!" => \$config{allowrss},
42                 "allowatom!" => \$config{allowatom},
43                 "pingurl=s" => sub {
44                         push @{$config{pingurl}}, $_[1];
45                 },      
46         );
47 } #}}}
48
49 sub getsetup () { #{{{
50         return
51                 rss => {
52                         type => "boolean",
53                         default => 0,
54                         description => "enable rss feeds by default",
55                         safe => 1,
56                         rebuild => 1,
57                 },
58                 atom => {
59                         type => "boolean",
60                         default => 0,
61                         description => "enable atom feeds by default",
62                         safe => 1,
63                         rebuild => 1,
64                 },
65                 allowrss => {
66                         type => "boolean",
67                         default => 0,
68                         description => "allow rss feeds to be used",
69                         safe => 1,
70                         rebuild => 1,
71                 },
72                 allowatom => {
73                         type => "boolean",
74                         default => 0,
75                         description => "allow atom feeds to be used",
76                         safe => 1,
77                         rebuild => 1,
78                 },
79                 pingurl => {
80                         type => "string",
81                         default => "",
82                         example => "http://rpc.technorati.com/rpc/ping",
83                         description => "urls to ping (using XMP-RPC) on feed update",
84                         safe => 1,
85                         rebuild => 0,
86                 },
87 } #}}}
88
89 sub checkconfig () { #{{{
90         if (($config{rss} || $config{atom}) && ! length $config{url}) {
91                 error(gettext("Must specify url to wiki with --url when using --rss or --atom"));
92         }
93         if ($config{rss}) {
94                 push @{$config{wiki_file_prune_regexps}}, qr/\.rss$/;
95         }
96         if ($config{atom}) {
97                 push @{$config{wiki_file_prune_regexps}}, qr/\.atom$/;
98         }
99 } #}}}
100
101 sub format (@) { #{{{
102         my %params=@_;
103
104         # Fill in the inline content generated earlier. This is actually an
105         # optimisation.
106         $params{content}=~s{<div class="inline" id="([^"]+)"></div>}{
107                 delete @inline[$1,]
108         }eg;
109         return $params{content};
110 } #}}}
111
112 sub sessioncgi () { #{{{
113         my $q=shift;
114         my $session=shift;
115
116         if ($q->param('do') eq 'blog') {
117                 my $page=IkiWiki::titlepage(decode_utf8($q->param('title')));
118                 $page=~s/(\/)/"__".ord($1)."__"/eg; # don't create subdirs
119                 # if the page already exists, munge it to be unique
120                 my $from=$q->param('from');
121                 my $add="";
122                 while (exists $IkiWiki::pagecase{lc($from."/".$page.$add)}) {
123                         $add=1 unless length $add;
124                         $add++;
125                 }
126                 $q->param('page', $page.$add);
127                 # now go create the page
128                 $q->param('do', 'create');
129                 IkiWiki::cgi_editpage($q, $session);
130                 exit;
131         }
132 }
133
134 # Back to ikiwiki namespace for the rest, this code is very much
135 # internal to ikiwiki even though it's separated into a plugin.
136 package IkiWiki;
137
138 my %toping;
139 my %feedlinks;
140
141 sub preprocess_inline (@) { #{{{
142         my %params=@_;
143         
144         if (! exists $params{pages}) {
145                 error gettext("missing pages parameter");
146         }
147         my $raw=yesno($params{raw});
148         my $archive=yesno($params{archive});
149         my $rss=(($config{rss} || $config{allowrss}) && exists $params{rss}) ? yesno($params{rss}) : $config{rss};
150         my $atom=(($config{atom} || $config{allowatom}) && exists $params{atom}) ? yesno($params{atom}) : $config{atom};
151         my $quick=exists $params{quick} ? yesno($params{quick}) : 0;
152         my $feeds=exists $params{feeds} ? yesno($params{feeds}) : !$quick;
153         my $feedonly=yesno($params{feedonly});
154         if (! exists $params{show} && ! $archive) {
155                 $params{show}=10;
156         }
157         if (! exists $params{feedshow} && exists $params{show}) {
158                 $params{feedshow}=$params{show};
159         }
160         my $desc;
161         if (exists $params{description}) {
162                 $desc = $params{description} 
163         }
164         else {
165                 $desc = $config{wikiname};
166         }
167         my $actions=yesno($params{actions});
168         if (exists $params{template}) {
169                 $params{template}=~s/[^-_a-zA-Z0-9]+//g;
170         }
171         else {
172                 $params{template} = $archive ? "archivepage" : "inlinepage";
173         }
174
175         my @list;
176         foreach my $page (keys %pagesources) {
177                 next if $page eq $params{page};
178                 if (pagespec_match($page, $params{pages}, location => $params{page})) {
179                         push @list, $page;
180                 }
181         }
182
183         if (exists $params{sort} && $params{sort} eq 'title') {
184                 @list=sort @list;
185         }
186         elsif (exists $params{sort} && $params{sort} eq 'mtime') {
187                 @list=sort { $pagemtime{$b} <=> $pagemtime{$a} } @list;
188         }
189         elsif (! exists $params{sort} || $params{sort} eq 'age') {
190                 @list=sort { $pagectime{$b} <=> $pagectime{$a} } @list;
191         }
192         else {
193                 return sprintf(gettext("unknown sort type %s"), $params{sort});
194         }
195
196         if (yesno($params{reverse})) {
197                 @list=reverse(@list);
198         }
199
200         if (exists $params{skip}) {
201                 @list=@list[$params{skip} .. scalar @list - 1];
202         }
203         
204         my @feedlist;
205         if ($feeds) {
206                 if (exists $params{feedshow} &&
207                     $params{feedshow} && @list > $params{feedshow}) {
208                         @feedlist=@list[0..$params{feedshow} - 1];
209                 }
210                 else {
211                         @feedlist=@list;
212                 }
213         }
214         
215         if ($params{show} && @list > $params{show}) {
216                 @list=@list[0..$params{show} - 1];
217         }
218
219         add_depends($params{page}, $params{pages});
220         # Explicitly add all currently displayed pages as dependencies, so
221         # that if they are removed or otherwise changed, the inline will be
222         # sure to be updated.
223         add_depends($params{page}, join(" or ", $#list >= $#feedlist ? @list : @feedlist));
224
225         my $feednum="";
226
227         my $feedid=join("\0", map { $_."\0".$params{$_} } sort keys %params);
228         if (exists $knownfeeds{$feedid}) {
229                 $feednum=$knownfeeds{$feedid};
230         }
231         else {
232                 if (exists $page_numfeeds{$params{destpage}}) {
233                         if ($feeds) {
234                                 $feednum=$knownfeeds{$feedid}=++$page_numfeeds{$params{destpage}};
235                         }
236                 }
237                 else {
238                         $feednum=$knownfeeds{$feedid}="";
239                         if ($feeds) {
240                                 $page_numfeeds{$params{destpage}}=1;
241                         }
242                 }
243         }
244
245         my $rssurl=basename(rsspage($params{destpage}).$feednum) if $feeds && $rss;
246         my $atomurl=basename(atompage($params{destpage}).$feednum) if $feeds && $atom;
247         my $ret="";
248
249         if ($config{cgiurl} && ! $params{preview} && (exists $params{rootpage} ||
250                         (exists $params{postform} && yesno($params{postform})))) {
251                 # Add a blog post form, with feed buttons.
252                 my $formtemplate=template("blogpost.tmpl", blind_cache => 1);
253                 $formtemplate->param(cgiurl => $config{cgiurl});
254                 $formtemplate->param(rootpage => 
255                         exists $params{rootpage} ? $params{rootpage} : $params{page});
256                 $formtemplate->param(rssurl => $rssurl) if $feeds && $rss;
257                 $formtemplate->param(atomurl => $atomurl) if $feeds && $atom;
258                 if (exists $params{postformtext}) {
259                         $formtemplate->param(postformtext =>
260                                 $params{postformtext});
261                 }
262                 else {
263                         $formtemplate->param(postformtext =>
264                                 gettext("Add a new post titled:"));
265                 }
266                 $ret.=$formtemplate->output;
267         }
268         elsif ($feeds && !$params{preview}) {
269                 # Add feed buttons.
270                 my $linktemplate=template("feedlink.tmpl", blind_cache => 1);
271                 $linktemplate->param(rssurl => $rssurl) if $rss;
272                 $linktemplate->param(atomurl => $atomurl) if $atom;
273                 $ret.=$linktemplate->output;
274         }
275         
276         if (! $feedonly) {
277                 require HTML::Template;
278                 my @params=IkiWiki::template_params($params{template}.".tmpl", blind_cache => 1);
279                 if (! @params) {
280                         return sprintf(gettext("nonexistant template %s"), $params{template});
281                 }
282                 my $template=HTML::Template->new(@params) unless $raw;
283         
284                 foreach my $page (@list) {
285                         my $file = $pagesources{$page};
286                         my $type = pagetype($file);
287                         if (! $raw || ($raw && ! defined $type)) {
288                                 unless ($archive && $quick) {
289                                         # Get the content before populating the
290                                         # template, since getting the content uses
291                                         # the same template if inlines are nested.
292                                         my $content=get_inline_content($page, $params{destpage});
293                                         $template->param(content => $content);
294                                 }
295                                 $template->param(pageurl => urlto(bestlink($params{page}, $page), $params{destpage}));
296                                 $template->param(title => pagetitle(basename($page)));
297                                 $template->param(ctime => displaytime($pagectime{$page}, $params{timeformat}));
298                                 $template->param(first => 1) if $page eq $list[0];
299                                 $template->param(last => 1) if $page eq $list[$#list];
300         
301                                 if ($actions) {
302                                         my $file = $pagesources{$page};
303                                         my $type = pagetype($file);
304                                         if ($config{discussion}) {
305                                                 my $discussionlink=gettext("discussion");
306                                                 if ($page !~ /.*\/\Q$discussionlink\E$/ &&
307                                                     (length $config{cgiurl} ||
308                                                      exists $links{$page."/".$discussionlink})) {
309                                                         $template->param(have_actions => 1);
310                                                         $template->param(discussionlink =>
311                                                                 htmllink($page,
312                                                                         $params{destpage},
313                                                                         gettext("Discussion"),
314                                                                         noimageinline => 1,
315                                                                         forcesubpage => 1));
316                                                 }
317                                         }
318                                         if (length $config{cgiurl} && defined $type) {
319                                                 $template->param(have_actions => 1);
320                                                 $template->param(editurl => cgiurl(do => "edit", page => $page));
321                                         }
322                                 }
323         
324                                 run_hooks(pagetemplate => sub {
325                                         shift->(page => $page, destpage => $params{destpage},
326                                                 template => $template,);
327                                 });
328         
329                                 $ret.=$template->output;
330                                 $template->clear_params;
331                         }
332                         else {
333                                 if (defined $type) {
334                                         $ret.="\n".
335                                               linkify($page, $params{destpage},
336                                               preprocess($page, $params{destpage},
337                                               filter($page, $params{destpage},
338                                               readfile(srcfile($file)))));
339                                 }
340                         }
341                 }
342         }
343         
344         if ($feeds) {
345                 if (exists $params{feedpages}) {
346                         @feedlist=grep { pagespec_match($_, $params{feedpages}, location => $params{page}) } @feedlist;
347                 }
348         
349                 if ($rss) {
350                         my $rssp=rsspage($params{destpage}).$feednum;
351                         will_render($params{destpage}, $rssp);
352                         if (! $params{preview}) {
353                                 writefile($rssp, $config{destdir},
354                                         genfeed("rss",
355                                                 $config{url}."/".rsspage($params{destpage}).$feednum, $desc, $params{guid}, $params{destpage}, @feedlist));
356                                 $toping{$params{destpage}}=1 unless $config{rebuild};
357                                 $feedlinks{$params{destpage}}=qq{<link rel="alternate" type="application/rss+xml" title="RSS" href="$rssurl" />};
358                         }
359                 }
360                 if ($atom) {
361                         my $atomp=atompage($params{destpage}).$feednum;
362                         will_render($params{destpage}, $atomp);
363                         if (! $params{preview}) {
364                                 writefile($atomp, $config{destdir},
365                                         genfeed("atom", $config{url}."/".atompage($params{destpage}).$feednum, $desc, $params{guid}, $params{destpage}, @feedlist));
366                                 $toping{$params{destpage}}=1 unless $config{rebuild};
367                                 $feedlinks{$params{destpage}}=qq{<link rel="alternate" type="application/atom+xml" title="Atom" href="$atomurl" />};
368                         }
369                 }
370         }
371         
372         return $ret if $raw || $nested;
373         push @inline, $ret;
374         return "<div class=\"inline\" id=\"$#inline\"></div>\n\n";
375 } #}}}
376
377 sub pagetemplate_inline (@) { #{{{
378         my %params=@_;
379         my $page=$params{page};
380         my $template=$params{template};
381
382         $template->param(feedlinks => $feedlinks{$page})
383                 if exists $feedlinks{$page} && $template->query(name => "feedlinks");
384 } #}}}
385
386 sub get_inline_content ($$) { #{{{
387         my $page=shift;
388         my $destpage=shift;
389         
390         my $file=$pagesources{$page};
391         my $type=pagetype($file);
392         if (defined $type) {
393                 $nested++;
394                 my $ret=htmlize($page, $destpage, $type,
395                        linkify($page, $destpage,
396                        preprocess($page, $destpage,
397                        filter($page, $destpage,
398                        readfile(srcfile($file))))));
399                 $nested--;
400                 return $ret;
401         }
402         else {
403                 return "";
404         }
405 } #}}}
406
407 sub date_822 ($) { #{{{
408         my $time=shift;
409
410         my $lc_time=POSIX::setlocale(&POSIX::LC_TIME);
411         POSIX::setlocale(&POSIX::LC_TIME, "C");
412         my $ret=POSIX::strftime("%a, %d %b %Y %H:%M:%S %z", localtime($time));
413         POSIX::setlocale(&POSIX::LC_TIME, $lc_time);
414         return $ret;
415 } #}}}
416
417 sub date_3339 ($) { #{{{
418         my $time=shift;
419
420         my $lc_time=POSIX::setlocale(&POSIX::LC_TIME);
421         POSIX::setlocale(&POSIX::LC_TIME, "C");
422         my $ret=POSIX::strftime("%Y-%m-%dT%H:%M:%SZ", gmtime($time));
423         POSIX::setlocale(&POSIX::LC_TIME, $lc_time);
424         return $ret;
425 } #}}}
426
427 sub absolute_urls ($$) { #{{{
428         # sucky sub because rss sucks
429         my $content=shift;
430         my $baseurl=shift;
431
432         my $url=$baseurl;
433         $url=~s/[^\/]+$//;
434
435         # what is the non path part of the url?
436         my $top_uri = URI->new($url);
437         $top_uri->path_query(""); # reset the path
438         my $urltop = $top_uri->as_string;
439
440         $content=~s/(<a(?:\s+(?:class|id)\s*="?\w+"?)?)\s+href=\s*"(#[^"]+)"/$1 href="$baseurl$2"/mig;
441         # relative to another wiki page
442         $content=~s/(<a(?:\s+(?:class|id)\s*="?\w+"?)?)\s+href=\s*"(?!\w+:)([^\/][^"]*)"/$1 href="$url$2"/mig;
443         $content=~s/(<img(?:\s+(?:class|id|width|height)\s*="?\w+"?)*)\s+src=\s*"(?!\w+:)([^\/][^"]*)"/$1 src="$url$2"/mig;
444         # relative to the top of the site
445         $content=~s/(<a(?:\s+(?:class|id)\s*="?\w+"?)?)\s+href=\s*"(?!\w+:)(\/[^"]*)"/$1 href="$urltop$2"/mig;
446         $content=~s/(<img(?:\s+(?:class|id|width|height)\s*="?\w+"?)*)\s+src=\s*"(?!\w+:)(\/[^"]*)"/$1 src="$urltop$2"/mig;
447         return $content;
448 } #}}}
449
450 sub rsspage ($) { #{{{
451         return targetpage(shift, "rss");
452 } #}}}
453
454 sub atompage ($) { #{{{
455         return targetpage(shift, "atom");
456 } #}}}
457
458 sub genfeed ($$$$$@) { #{{{
459         my $feedtype=shift;
460         my $feedurl=shift;
461         my $feeddesc=shift;
462         my $guid=shift;
463         my $page=shift;
464         my @pages=@_;
465         
466         my $url=URI->new(encode_utf8(urlto($page,"",1)));
467         
468         my $itemtemplate=template($feedtype."item.tmpl", blind_cache => 1);
469         my $content="";
470         my $lasttime = 0;
471         foreach my $p (@pages) {
472                 my $u=URI->new(encode_utf8(urlto($p, "", 1)));
473                 my $pcontent = absolute_urls(get_inline_content($p, $page), $url);
474
475                 $itemtemplate->param(
476                         title => pagetitle(basename($p)),
477                         url => $u,
478                         permalink => $u,
479                         cdate_822 => date_822($pagectime{$p}),
480                         mdate_822 => date_822($pagemtime{$p}),
481                         cdate_3339 => date_3339($pagectime{$p}),
482                         mdate_3339 => date_3339($pagemtime{$p}),
483                 );
484
485                 if (exists $pagestate{$p} &&
486                     exists $pagestate{$p}{meta}{guid}) {
487                         $itemtemplate->param(guid => $pagestate{$p}{meta}{guid});
488                 }
489
490                 if ($itemtemplate->query(name => "enclosure")) {
491                         my $file=$pagesources{$p};
492                         my $type=pagetype($file);
493                         if (defined $type) {
494                                 $itemtemplate->param(content => $pcontent);
495                         }
496                         else {
497                                 my $size=(srcfile_stat($file))[8];
498                                 my $mime="unknown";
499                                 eval q{use File::MimeInfo};
500                                 if (! $@) {
501                                         $mime = mimetype($file);
502                                 }
503                                 $itemtemplate->param(
504                                         enclosure => $u,
505                                         type => $mime,
506                                         length => $size,
507                                 );
508                         }
509                 }
510                 else {
511                         $itemtemplate->param(content => $pcontent);
512                 }
513
514                 run_hooks(pagetemplate => sub {
515                         shift->(page => $p, destpage => $page,
516                                 template => $itemtemplate);
517                 });
518
519                 $content.=$itemtemplate->output;
520                 $itemtemplate->clear_params;
521
522                 $lasttime = $pagemtime{$p} if $pagemtime{$p} > $lasttime;
523         }
524
525         my $template=template($feedtype."page.tmpl", blind_cache => 1);
526         $template->param(
527                 title => $page ne "index" ? pagetitle($page) : $config{wikiname},
528                 wikiname => $config{wikiname},
529                 pageurl => $url,
530                 content => $content,
531                 feeddesc => $feeddesc,
532                 guid => $guid,
533                 feeddate => date_3339($lasttime),
534                 feedurl => $feedurl,
535                 version => $IkiWiki::version,
536         );
537         run_hooks(pagetemplate => sub {
538                 shift->(page => $page, destpage => $page,
539                         template => $template);
540         });
541         
542         return $template->output;
543 } #}}}
544
545 sub pingurl (@) { #{{{
546         return unless @{$config{pingurl}} && %toping;
547
548         eval q{require RPC::XML::Client};
549         if ($@) {
550                 debug(gettext("RPC::XML::Client not found, not pinging"));
551                 return;
552         }
553
554         # daemonize here so slow pings don't slow down wiki updates
555         defined(my $pid = fork) or error("Can't fork: $!");
556         return if $pid;
557         chdir '/';
558         POSIX::setsid() or error("Can't start a new session: $!");
559         open STDIN, '/dev/null';
560         open STDOUT, '>/dev/null';
561         open STDERR, '>&STDOUT' or error("Can't dup stdout: $!");
562
563         # Don't need to keep a lock on the wiki as a daemon.
564         IkiWiki::unlockwiki();
565
566         foreach my $page (keys %toping) {
567                 my $title=pagetitle(basename($page), 0);
568                 my $url=urlto($page, "", 1);
569                 foreach my $pingurl (@{$config{pingurl}}) {
570                         debug("Pinging $pingurl for $page");
571                         eval {
572                                 my $client = RPC::XML::Client->new($pingurl);
573                                 my $req = RPC::XML::request->new('weblogUpdates.ping',
574                                         $title, $url);
575                                 my $res = $client->send_request($req);
576                                 if (! ref $res) {
577                                         debug("Did not receive response to ping");
578                                 }
579                                 my $r=$res->value;
580                                 if (! exists $r->{flerror} || $r->{flerror}) {
581                                         debug("Ping rejected: ".(exists $r->{message} ? $r->{message} : "[unknown reason]"));
582                                 }
583                         };
584                         if ($@) {
585                                 debug "Ping failed: $@";
586                         }
587                 }
588         }
589
590         exit 0; # daemon done
591 } #}}}
592
593 1