8 use open qw{:utf8 :std};
10 use vars qw{%config %links %oldlinks %oldpagemtime %pagectime %pagecase
11 %renderedfiles %oldrenderedfiles %pagesources %depends %hooks
12 %forcerebuild $gettext_obj};
14 use Exporter q{import};
15 our @EXPORT = qw(hook debug error template htmlpage add_depends pagespec_match
16 bestlink htmllink readfile writefile pagetype srcfile pagename
17 displaytime will_render gettext
18 %config %links %renderedfiles %pagesources);
19 our $VERSION = 1.01; # plugin interface version
24 memoize("pagespec_translate");
25 memoize("file_pruned");
27 my $installdir=''; # INSTALLDIR_AUTOREPLACE done by Makefile, DNE
28 our $version='unknown'; # VERSION_AUTOREPLACE done by Makefile, DNE
30 sub defaultconfig () { #{{{
31 wiki_file_prune_regexps => [qr/\.\./, qr/^\./, qr/\/\./, qr/\.x?html?$/,
32 qr/(^|\/).svn\//, qr/.arch-ids\//, qr/{arch}\//],
33 wiki_link_regexp => qr/\[\[(?:([^\]\|]+)\|)?([^\s\]]+)\]\]/,
34 wiki_file_regexp => qr/(^[-[:alnum:]_.:\/+]+$)/,
35 web_commit_regexp => qr/^web commit (by (.*?(?=: |$))|from (\d+\.\d+\.\d+\.\d+)):?(.*)/,
39 default_pageext => "mdwn",
58 gitorigin_branch => "origin",
59 gitmaster_branch => "master",
63 templatedir => "$installdir/share/ikiwiki/templates",
64 underlaydir => "$installdir/share/ikiwiki/basewiki",
68 plugin => [qw{mdwn inline htmlscrubber passwordauth signinedit lockedit}],
76 sub checkconfig () { #{{{
77 # locale stuff; avoid LC_ALL since it overrides everything
78 if (defined $ENV{LC_ALL}) {
79 $ENV{LANG} = $ENV{LC_ALL};
82 if (defined $config{locale}) {
85 if (POSIX::setlocale(&POSIX::LC_ALL, $config{locale})) {
86 $ENV{LANG}=$config{locale};
91 if ($config{w3mmode}) {
92 eval q{use Cwd q{abs_path}};
94 $config{srcdir}=possibly_foolish_untaint(abs_path($config{srcdir}));
95 $config{destdir}=possibly_foolish_untaint(abs_path($config{destdir}));
96 $config{cgiurl}="file:///\$LIB/ikiwiki-w3m.cgi/".$config{cgiurl}
97 unless $config{cgiurl} =~ m!file:///!;
98 $config{url}="file://".$config{destdir};
101 if ($config{cgi} && ! length $config{url}) {
102 error(gettext("Must specify url to wiki with --url when using --cgi"));
105 $config{wikistatedir}="$config{srcdir}/.ikiwiki"
106 unless exists $config{wikistatedir};
109 eval qq{require IkiWiki::Rcs::$config{rcs}};
111 error("Failed to load RCS module IkiWiki::Rcs::$config{rcs}: $@");
115 require IkiWiki::Rcs::Stub;
118 run_hooks(checkconfig => sub { shift->() });
121 sub loadplugins () { #{{{
122 loadplugin($_) foreach @{$config{plugin}};
124 run_hooks(getopt => sub { shift->() });
125 if (grep /^-/, @ARGV) {
126 print STDERR "Unknown option: $_\n"
127 foreach grep /^-/, @ARGV;
132 sub loadplugin ($) { #{{{
135 return if grep { $_ eq $plugin} @{$config{disable_plugins}};
137 my $mod="IkiWiki::Plugin::".possibly_foolish_untaint($plugin);
140 error("Failed to load plugin $mod: $@");
146 print "Content-type: text/html\n\n";
147 print misctemplate(gettext("Error"),
148 "<p>".gettext("Error").": @_</p>");
150 log_message(error => @_);
155 return unless $config{verbose};
156 log_message(debug => @_);
160 sub log_message ($$) { #{{{
163 if ($config{syslog}) {
166 Sys::Syslog::setlogsock('unix');
167 Sys::Syslog::openlog('ikiwiki', '', 'user');
171 Sys::Syslog::syslog($type, join(" ", @_));
174 elsif (! $config{cgi}) {
182 sub possibly_foolish_untaint ($) { #{{{
184 my ($untainted)=$tainted=~/(.*)/;
188 sub basename ($) { #{{{
195 sub dirname ($) { #{{{
202 sub pagetype ($) { #{{{
205 if ($page =~ /\.([^.]+)$/) {
206 return $1 if exists $hooks{htmlize}{$1};
211 sub pagename ($) { #{{{
214 my $type=pagetype($file);
216 $page=~s/\Q.$type\E*$// if defined $type;
220 sub htmlpage ($) { #{{{
223 return $page.".html";
226 sub srcfile ($) { #{{{
229 return "$config{srcdir}/$file" if -e "$config{srcdir}/$file";
230 return "$config{underlaydir}/$file" if -e "$config{underlaydir}/$file";
231 error("internal error: $file cannot be found");
234 sub readfile ($;$) { #{{{
239 error("cannot read a symlink ($file)");
243 open (IN, $file) || error("failed to read $file: $!");
244 binmode(IN) if ($binary);
250 sub writefile ($$$;$) { #{{{
251 my $file=shift; # can include subdirs
252 my $destdir=shift; # directory to put file in
257 while (length $test) {
258 if (-l "$destdir/$test") {
259 error("cannot write to a symlink ($test)");
261 $test=dirname($test);
264 my $dir=dirname("$destdir/$file");
267 foreach my $s (split(m!/+!, $dir)) {
270 mkdir($d) || error("failed to create directory $d: $!");
275 open (OUT, ">$destdir/$file") || error("failed to write $destdir/$file: $!");
276 binmode(OUT) if ($binary);
282 sub will_render ($$;$) { #{{{
287 # Important security check.
288 if (-e "$config{destdir}/$dest" && ! $config{rebuild} &&
289 ! grep { $_ eq $dest } (@{$renderedfiles{$page}}, @{$oldrenderedfiles{$page}})) {
290 error("$config{destdir}/$dest independently created, not overwriting with version from $page");
293 if (! $clear || $cleared{$page}) {
294 $renderedfiles{$page}=[$dest, grep { $_ ne $dest } @{$renderedfiles{$page}}];
297 $renderedfiles{$page}=[$dest];
302 sub bestlink ($$) { #{{{
307 if ($link=~s/^\/+//) {
314 $l.="/" if length $l;
317 if (exists $links{$l}) {
320 elsif (exists $pagecase{lc $l}) {
321 return $pagecase{lc $l};
323 } while $cwd=~s!/?[^/]+$!!;
325 if (length $config{userdir} && exists $links{"$config{userdir}/".lc($link)}) {
326 return "$config{userdir}/".lc($link);
329 #print STDERR "warning: page $page, broken link: $link\n";
333 sub isinlinableimage ($) { #{{{
336 $file=~/\.(png|gif|jpg|jpeg)$/i;
339 sub pagetitle ($;$) { #{{{
344 $page=~s/__(\d+)__/chr($1)/eg;
347 $page=~s/__(\d+)__/&#$1;/g;
354 sub titlepage ($) { #{{{
357 $title=~s/([^-[:alnum:]_:+\/.])/"__".ord($1)."__"/eg;
361 sub cgiurl (@) { #{{{
364 return $config{cgiurl}."?".join("&", map "$_=$params{$_}", keys %params);
367 sub baseurl (;$) { #{{{
370 return "$config{url}/" if ! defined $page;
373 $page=~s/[^\/]+\//..\//g;
377 sub abs2rel ($$) { #{{{
378 # Work around very innefficient behavior in File::Spec if abs2rel
379 # is passed two relative paths. It's much faster if paths are
380 # absolute! (Debian bug #376658; fixed in debian unstable now)
385 my $ret=File::Spec->abs2rel($path, $base);
386 $ret=~s/^// if defined $ret;
390 sub displaytime ($) { #{{{
395 # strftime doesn't know about encodings, so make sure
396 # its output is properly treated as utf8
397 return decode_utf8(POSIX::strftime(
398 $config{timeformat}, localtime($time)));
401 sub htmllink ($$$;$$$) { #{{{
402 my $lpage=shift; # the page doing the linking
403 my $page=shift; # the page that will contain the link (different for inline)
405 my $noimageinline=shift; # don't turn links into inline html images
406 my $forcesubpage=shift; # force a link to a subpage
407 my $linktext=shift; # set to force the link text to something
410 if (! $forcesubpage) {
411 $bestlink=bestlink($lpage, $link);
414 $bestlink="$lpage/".lc($link);
417 $linktext=pagetitle(basename($link)) unless defined $linktext;
419 return "<span class=\"selflink\">$linktext</span>"
420 if length $bestlink && $page eq $bestlink;
422 if (! grep { $_ eq $bestlink } map { @{$_} } values %renderedfiles) {
423 $bestlink=htmlpage($bestlink);
425 if (! grep { $_ eq $bestlink } map { @{$_} } values %renderedfiles) {
426 return $linktext unless length $config{cgiurl};
427 return "<span><a href=\"".
428 cgiurl(do => "create", page => lc($link), from => $page).
429 "\">?</a>$linktext</span>"
432 $bestlink=abs2rel($bestlink, dirname($page));
434 if (! $noimageinline && isinlinableimage($bestlink)) {
435 return "<img src=\"$bestlink\" alt=\"$linktext\" />";
437 return "<a href=\"$bestlink\">$linktext</a>";
440 sub htmlize ($$$) { #{{{
445 if (exists $hooks{htmlize}{$type}) {
446 $content=$hooks{htmlize}{$type}{call}->(
452 error("htmlization of $type not supported");
455 run_hooks(sanitize => sub {
465 sub linkify ($$$) { #{{{
466 my $lpage=shift; # the page containing the links
467 my $page=shift; # the page the link will end up on (different for inline)
470 $content =~ s{(\\?)$config{wiki_link_regexp}}{
471 $2 ? ( $1 ? "[[$2|$3]]" : htmllink($lpage, $page, titlepage($3), 0, 0, pagetitle($2)))
472 : ( $1 ? "[[$3]]" : htmllink($lpage, $page, titlepage($3)))
479 sub preprocess ($$$;$) { #{{{
480 my $page=shift; # the page the data comes from
481 my $destpage=shift; # the page the data will appear in (different for inline)
489 if (length $escape) {
490 return "[[$command $params]]";
492 elsif (exists $hooks{preprocess}{$command}) {
493 return "" if $scan && ! $hooks{preprocess}{$command}{scan};
494 # Note: preserve order of params, some plugins may
495 # consider it significant.
497 while ($params =~ /(?:(\w+)=)?(?:"""(.*?)"""|"([^"]+)"|(\S+))(?:\s+|$)/sg) {
514 push @params, $key, $val;
517 push @params, $val, '';
520 if ($preprocessing{$page}++ > 3) {
521 # Avoid loops of preprocessed pages preprocessing
522 # other pages that preprocess them, etc.
523 #translators: The first parameter is a
524 #translators: preprocessor directive name,
525 #translators: the second a page name, the
526 #translators: third a number.
527 return "[[".sprintf(gettext("%s preprocessing loop detected on %s at depth %i"),
528 $command, $page, $preprocessing{$page}).
531 my $ret=$hooks{preprocess}{$command}{call}->(
534 destpage => $destpage,
536 $preprocessing{$page}--;
540 return "[[$command $params]]";
544 $content =~ s{(\\?)\[\[(\w+)\s+((?:(?:\w+=)?(?:""".*?"""|"[^"]+"|[^\s\]]+)\s*)*)\]\]}{$handle->($1, $2, $3)}seg;
548 sub filter ($$) { #{{{
552 run_hooks(filter => sub {
553 $content=shift->(page => $page, content => $content);
559 sub indexlink () { #{{{
560 return "<a href=\"$config{url}\">$config{wikiname}</a>";
563 sub lockwiki () { #{{{
564 # Take an exclusive lock on the wiki to prevent multiple concurrent
565 # run issues. The lock will be dropped on program exit.
566 if (! -d $config{wikistatedir}) {
567 mkdir($config{wikistatedir});
569 open(WIKILOCK, ">$config{wikistatedir}/lockfile") ||
570 error ("cannot write to $config{wikistatedir}/lockfile: $!");
571 if (! flock(WIKILOCK, 2 | 4)) {
572 debug("wiki seems to be locked, waiting for lock");
573 my $wait=600; # arbitrary, but don't hang forever to
574 # prevent process pileup
576 return if flock(WIKILOCK, 2 | 4);
579 error("wiki is locked; waited $wait seconds without lock being freed (possible stuck process or stale lock?)");
583 sub unlockwiki () { #{{{
587 sub loadindex () { #{{{
588 open (IN, "$config{wikistatedir}/index") || return;
590 $_=possibly_foolish_untaint($_);
595 foreach my $i (split(/ /, $_)) {
596 my ($item, $val)=split(/=/, $i, 2);
597 push @{$items{$item}}, decode_entities($val);
600 next unless exists $items{src}; # skip bad lines for now
602 my $page=pagename($items{src}[0]);
603 if (! $config{rebuild}) {
604 $pagesources{$page}=$items{src}[0];
605 $oldpagemtime{$page}=$items{mtime}[0];
606 $oldlinks{$page}=[@{$items{link}}];
607 $links{$page}=[@{$items{link}}];
608 $depends{$page}=$items{depends}[0] if exists $items{depends};
609 $renderedfiles{$page}=[@{$items{dest}}];
610 $oldrenderedfiles{$page}=[@{$items{dest}}];
611 $pagecase{lc $page}=$page;
613 $pagectime{$page}=$items{ctime}[0];
618 sub saveindex () { #{{{
619 run_hooks(savestate => sub { shift->() });
621 if (! -d $config{wikistatedir}) {
622 mkdir($config{wikistatedir});
624 open (OUT, ">$config{wikistatedir}/index") ||
625 error("cannot write to $config{wikistatedir}/index: $!");
626 foreach my $page (keys %oldpagemtime) {
627 next unless $oldpagemtime{$page};
628 my $line="mtime=$oldpagemtime{$page} ".
629 "ctime=$pagectime{$page} ".
630 "src=$pagesources{$page}";
631 $line.=" dest=$_" foreach @{$renderedfiles{$page}};
633 $line.=" link=$_" foreach grep { ++$count{$_} == 1 } @{$links{$page}};
634 if (exists $depends{$page}) {
635 $line.=" depends=".encode_entities($depends{$page}, " \t\n");
637 print OUT $line."\n";
642 sub template_file ($) { #{{{
645 foreach my $dir ($config{templatedir}, "$installdir/share/ikiwiki/templates") {
646 return "$dir/$template" if -e "$dir/$template";
651 sub template_params (@) { #{{{
652 my $filename=template_file(shift);
654 if (! defined $filename) {
659 require HTML::Template;
662 my $text_ref = shift;
663 $$text_ref=&Encode::decode_utf8($$text_ref);
665 filename => $filename,
666 loop_context_vars => 1,
667 die_on_bad_params => 0,
670 return wantarray ? @ret : {@ret};
673 sub template ($;@) { #{{{
674 HTML::Template->new(template_params(@_));
677 sub misctemplate ($$;@) { #{{{
681 my $template=template("misc.tmpl");
684 indexlink => indexlink(),
685 wikiname => $config{wikiname},
686 pagebody => $pagebody,
687 baseurl => baseurl(),
690 run_hooks(pagetemplate => sub {
691 shift->(page => "", destpage => "", template => $template);
693 return $template->output;
699 if (! exists $param{type} || ! ref $param{call} || ! exists $param{id}) {
700 error "hook requires type, call, and id parameters";
703 return if $param{no_override} && exists $hooks{$param{type}}{$param{id}};
705 $hooks{$param{type}}{$param{id}}=\%param;
708 sub run_hooks ($$) { # {{{
709 # Calls the given sub for each hook of the given type,
710 # passing it the hook function to call.
714 if (exists $hooks{$type}) {
716 foreach my $id (keys %{$hooks{$type}}) {
717 if ($hooks{$type}{$id}{last}) {
721 $sub->($hooks{$type}{$id}{call});
723 foreach my $id (@deferred) {
724 $sub->($hooks{$type}{$id}{call});
729 sub globlist_to_pagespec ($) { #{{{
730 my @globlist=split(' ', shift);
733 foreach my $glob (@globlist) {
734 if ($glob=~/^!(.*)/) {
742 my $spec=join(" or ", @spec);
744 my $skip=join(" and ", @skip);
746 $spec="$skip and ($spec)";
755 sub is_globlist ($) { #{{{
757 $s=~/[^\s]+\s+([^\s]+)/ && $1 ne "and" && $1 ne "or";
760 sub safequote ($) { #{{{
766 sub add_depends ($$) { #{{{
770 if (! exists $depends{$page}) {
771 $depends{$page}=$pagespec;
774 $depends{$page}=pagespec_merge($depends{$page}, $pagespec);
778 sub file_pruned ($$) { #{{{
780 my $file=File::Spec->canonpath(shift);
781 my $base=File::Spec->canonpath(shift);
782 $file=~s#^\Q$base\E/*##;
784 my $regexp='('.join('|', @{$config{wiki_file_prune_regexps}}).')';
789 # Only use gettext in the rare cases it's needed.
790 if (exists $ENV{LANG} || exists $ENV{LC_ALL} || exists $ENV{LC_MESSAGES}) {
791 if (! $gettext_obj) {
793 use Locale::gettext q{textdomain};
794 Locale::gettext->domain('ikiwiki')
802 return $gettext_obj->get(shift);
809 sub pagespec_merge ($$) { #{{{
813 return $a if $a eq $b;
815 # Support for old-style GlobLists.
816 if (is_globlist($a)) {
817 $a=globlist_to_pagespec($a);
819 if (is_globlist($b)) {
820 $b=globlist_to_pagespec($b);
823 return "($a) or ($b)";
826 sub pagespec_translate ($) { #{{{
827 # This assumes that $page is in scope in the function
828 # that evalulates the translated pagespec code.
831 # Support for old-style GlobLists.
832 if (is_globlist($spec)) {
833 $spec=globlist_to_pagespec($spec);
836 # Convert spec to perl code.
838 while ($spec=~m/\s*(\!|\(|\)|\w+\([^\)]+\)|[^\s()]+)\s*/ig) {
840 if (lc $word eq "and") {
843 elsif (lc $word eq "or") {
846 elsif ($word eq "(" || $word eq ")" || $word eq "!") {
849 elsif ($word =~ /^(link|backlink|created_before|created_after|creation_month|creation_year|creation_day)\((.+)\)$/) {
850 $code.=" match_$1(\$page, ".safequote($2).")";
853 $code.=" match_glob(\$page, ".safequote($word).")";
860 sub pagespec_match ($$) { #{{{
864 return eval pagespec_translate($spec);
867 sub match_glob ($$) { #{{{
871 # turn glob into safe regexp
872 $glob=quotemeta($glob);
876 return $page=~/^$glob$/i;
879 sub match_link ($$) { #{{{
883 my $links = $links{$page} or return undef;
884 foreach my $p (@$links) {
885 return 1 if lc $p eq $link;
890 sub match_backlink ($$) { #{{{
891 match_link(pop, pop);
894 sub match_created_before ($$) { #{{{
898 if (exists $pagectime{$testpage}) {
899 return $pagectime{$page} < $pagectime{$testpage};
906 sub match_created_after ($$) { #{{{
910 if (exists $pagectime{$testpage}) {
911 return $pagectime{$page} > $pagectime{$testpage};
918 sub match_creation_day ($$) { #{{{
919 return ((gmtime($pagectime{shift()}))[3] == shift);
922 sub match_creation_month ($$) { #{{{
923 return ((gmtime($pagectime{shift()}))[4] + 1 == shift);
926 sub match_creation_year ($$) { #{{{
927 return ((gmtime($pagectime{shift()}))[5] + 1900 == shift);