X-Git-Url: http://git.vanrenterghem.biz/git.ikiwiki.info.git/blobdiff_plain/135e5fc63a47d198e6ab6b0ebf30c87087d3f5d5..ae0e6e816cd15dcadb88a8cf816d76b74a744ff3:/IkiWiki.pm?ds=inline diff --git a/IkiWiki.pm b/IkiWiki.pm index 40622a5be..0af4a4fe9 100644 --- a/IkiWiki.pm +++ b/IkiWiki.pm @@ -5,18 +5,20 @@ use warnings; use strict; use Encode; use HTML::Entities; +use URI::Escape q{uri_escape_utf8}; +use POSIX; use open qw{:utf8 :std}; -use vars qw{%config %links %oldlinks %oldpagemtime %pagectime %pagecase - %renderedfiles %oldrenderedfiles %pagesources %depends %hooks - %forcerebuild $gettext_obj}; +use vars qw{%config %links %oldlinks %pagemtime %pagectime %pagecase + %renderedfiles %oldrenderedfiles %pagesources %destsources + %depends %hooks %forcerebuild $gettext_obj}; use Exporter q{import}; our @EXPORT = qw(hook debug error template htmlpage add_depends pagespec_match bestlink htmllink readfile writefile pagetype srcfile pagename - displaytime will_render gettext - %config %links %renderedfiles %pagesources); -our $VERSION = 1.02; # plugin interface version, next is ikiwiki version + displaytime will_render gettext urlto targetpage + %config %links %renderedfiles %pagesources %destsources); +our $VERSION = 2.00; # plugin interface version, next is ikiwiki version our $version='unknown'; # VERSION_AUTOREPLACE done by Makefile, DNE my $installdir=''; # INSTALLDIR_AUTOREPLACE done by Makefile, DNE @@ -29,15 +31,32 @@ memoize("file_pruned"); sub defaultconfig () { #{{{ wiki_file_prune_regexps => [qr/\.\./, qr/^\./, qr/\/\./, qr/\.x?html?$/, qr/\.ikiwiki-new$/, - qr/(^|\/).svn\//, qr/.arch-ids\//, qr/{arch}\//], - wiki_link_regexp => qr/\[\[(?:([^\]\|]+)\|)?([^\s\]#]+)(?:#([^\s\]]+))?\]\]/, + qr/(^|\/).svn\//, qr/.arch-ids\//, qr/{arch}\//, + qr/\.dpkg-tmp$/], + wiki_link_regexp => qr{ + \[\[ # beginning of link + (?: + ([^\]\|]+) # 1: link text + \| # followed by '|' + )? # optional + + ([^\s\]#]+) # 2: page to link to + (?: + \# # '#', beginning of anchor + ([^\s\]]+) # 3: anchor text + )? # optional + + \]\] # end of link + }x, wiki_file_regexp => qr/(^[-[:alnum:]_.:\/+]+$)/, web_commit_regexp => qr/^web commit (by (.*?(?=: |$))|from (\d+\.\d+\.\d+\.\d+)):?(.*)/, verbose => 0, syslog => 0, wikiname => "wiki", default_pageext => "mdwn", + htmlext => "html", cgi => 0, + post_commit => 0, rcs => '', notify => 0, url => '', @@ -65,13 +84,17 @@ sub defaultconfig () { #{{{ setup => undef, adminuser => undef, adminemail => undef, - plugin => [qw{mdwn inline htmlscrubber passwordauth signinedit + plugin => [qw{mdwn inline htmlscrubber passwordauth openid signinedit lockedit conditional}], + libdir => undef, timeformat => '%c', locale => undef, sslcookie => 0, httpauth => 0, userdir => "", + usedirs => 1, + numbacklinks => 10, + account_creation_password => "", } #}}} sub checkconfig () { #{{{ @@ -81,8 +104,6 @@ sub checkconfig () { #{{{ delete $ENV{LC_ALL}; } if (defined $config{locale}) { - eval q{use POSIX}; - error($@) if $@; if (POSIX::setlocale(&POSIX::LC_ALL, $config{locale})) { $ENV{LANG}=$config{locale}; $gettext_obj=undef; @@ -107,7 +128,7 @@ sub checkconfig () { #{{{ unless exists $config{wikistatedir}; if ($config{rcs}) { - eval qq{require IkiWiki::Rcs::$config{rcs}}; + eval qq{use IkiWiki::Rcs::$config{rcs}}; if ($@) { error("Failed to load RCS module IkiWiki::Rcs::$config{rcs}: $@"); } @@ -120,8 +141,12 @@ sub checkconfig () { #{{{ } #}}} sub loadplugins () { #{{{ + if (defined $config{libdir}) { + unshift @INC, $config{libdir}; + } + loadplugin($_) foreach @{$config{plugin}}; - + run_hooks(getopt => sub { shift->() }); if (grep /^-/, @ARGV) { print STDERR "Unknown option: $_\n" @@ -150,7 +175,7 @@ sub error ($;$) { #{{{ print misctemplate(gettext("Error"), "
".gettext("Error").": $message
"); } - log_message(debug => $message) if $config{syslog}; + log_message('err' => $message) if $config{syslog}; if (defined $cleaner) { $cleaner->(); } @@ -174,8 +199,8 @@ sub log_message ($$) { #{{{ $log_open=1; } eval { - Sys::Syslog::syslog($type, "%s", join(" ", @_)); - } + Sys::Syslog::syslog($type, "[$config{wikiname}] %s", join(" ", @_)); + }; } elsif (! $config{cgi}) { print "@_\n"; @@ -187,7 +212,7 @@ sub log_message ($$) { #{{{ sub possibly_foolish_untaint ($) { #{{{ my $tainted=shift; - my ($untainted)=$tainted=~/(.*)/; + my ($untainted)=$tainted=~/(.*)/s; return $untainted; } #}}} @@ -223,10 +248,21 @@ sub pagename ($) { #{{{ return $page; } #}}} -sub htmlpage ($) { #{{{ +sub targetpage ($$) { #{{{ my $page=shift; + my $ext=shift; + + if (! $config{usedirs} || $page =~ /^index$/ ) { + return $page.".".$ext; + } else { + return $page."/index.".$ext; + } +} #}}} - return $page.".html"; +sub htmlpage ($) { #{{{ + my $page=shift; + + return targetpage($page, $config{htmlext}); } #}}} sub srcfile ($) { #{{{ @@ -234,7 +270,7 @@ sub srcfile ($) { #{{{ return "$config{srcdir}/$file" if -e "$config{srcdir}/$file"; return "$config{underlaydir}/$file" if -e "$config{underlaydir}/$file"; - error("internal error: $file cannot be found"); + error("internal error: $file cannot be found in $config{srcdir} or $config{underlaydir}"); } #}}} sub readfile ($;$$) { #{{{ @@ -292,9 +328,7 @@ sub writefile ($$$;$$) { #{{{ $writer->(\*OUT, $cleanup); } else { - if (length $content) { - print OUT $content || error("failed writing to $newfile: $!", $cleanup); - } + print OUT $content or error("failed writing to $newfile: $!", $cleanup); } close OUT || error("failed saving $newfile: $!", $cleanup); rename($newfile, "$destdir/$file") || @@ -317,9 +351,13 @@ sub will_render ($$;$) { #{{{ $renderedfiles{$page}=[$dest, grep { $_ ne $dest } @{$renderedfiles{$page}}]; } else { + foreach my $old (@{$renderedfiles{$page}}) { + delete $destsources{$old}; + } $renderedfiles{$page}=[$dest]; $cleared{$page}=1; } + $destsources{$dest}=$page; } #}}} sub bestlink ($$) { #{{{ @@ -345,8 +383,14 @@ sub bestlink ($$) { #{{{ } } while $cwd=~s!/?[^/]+$!!; - if (length $config{userdir} && exists $links{"$config{userdir}/".lc($link)}) { - return "$config{userdir}/".lc($link); + if (length $config{userdir}) { + my $l = "$config{userdir}/".lc($link); + if (exists $links{$l}) { + return $l; + } + elsif (exists $pagecase{lc $l}) { + return $pagecase{lc $l}; + } } #print STDERR "warning: page $page, broken link: $link\n"; @@ -364,27 +408,32 @@ sub pagetitle ($;$) { #{{{ my $unescaped=shift; if ($unescaped) { - $page=~s/__(\d+)__/chr($1)/eg; + $page=~s/(__(\d+)__|_)/$1 eq '_' ? ' ' : chr($2)/eg; } else { - $page=~s/__(\d+)__/$1;/g; + $page=~s/(__(\d+)__|_)/$1 eq '_' ? ' ' : "$2;"/eg; } - $page=~y/_/ /; return $page; } #}}} sub titlepage ($) { #{{{ my $title=shift; - $title=~y/ /_/; - $title=~s/([^-[:alnum:]_:+\/.])/"__".ord($1)."__"/eg; + $title=~s/([^-[:alnum:]:+\/.])/$1 eq ' ' ? '_' : "__".ord($1)."__"/eg; return $title; } #}}} +sub linkpage ($) { #{{{ + my $link=shift; + $link=~s/([^-[:alnum:]:+\/._])/$1 eq ' ' ? '_' : "__".ord($1)."__"/eg; + return $link; +} #}}} + sub cgiurl (@) { #{{{ my %params=@_; - return $config{cgiurl}."?".join("&", map "$_=$params{$_}", keys %params); + return $config{cgiurl}."?". + join("&", map $_."=".uri_escape_utf8($params{$_}), keys %params); } #}}} sub baseurl (;$) { #{{{ @@ -392,6 +441,7 @@ sub baseurl (;$) { #{{{ return "$config{url}/" if ! defined $page; + $page=htmlpage($page); $page=~s/[^\/]+$//; $page=~s/[^\/]+\//..\//g; return $page; @@ -413,14 +463,38 @@ sub abs2rel ($$) { #{{{ sub displaytime ($) { #{{{ my $time=shift; - eval q{use POSIX}; - error($@) if $@; # strftime doesn't know about encodings, so make sure # its output is properly treated as utf8 return decode_utf8(POSIX::strftime( $config{timeformat}, localtime($time))); } #}}} +sub beautify_url ($) { #{{{ + my $url=shift; + + $url =~ s!/index.$config{htmlext}$!/!; + $url =~ s!^$!./!; # Browsers don't like empty links... + + return $url; +} #}}} + +sub urlto ($$) { #{{{ + my $to=shift; + my $from=shift; + + if (! length $to) { + return beautify_url(baseurl($from)); + } + + if (! $destsources{$to}) { + $to=htmlpage($to); + } + + my $link = abs2rel($to, dirname(htmlpage($from))); + + return beautify_url($link); +} #}}} + sub htmllink ($$$;@) { #{{{ my $lpage=shift; # the page doing the linking my $page=shift; # the page that will contain the link (different for inline) @@ -446,17 +520,23 @@ sub htmllink ($$$;@) { #{{{ return "$linktext" if length $bestlink && $page eq $bestlink; - if (! grep { $_ eq $bestlink } map { @{$_} } values %renderedfiles) { + if (! $destsources{$bestlink}) { $bestlink=htmlpage($bestlink); - } - if (! grep { $_ eq $bestlink } map { @{$_} } values %renderedfiles) { - return $linktext unless length $config{cgiurl}; - return " "create", page => lc($link), from => $page). - "\">?$linktext" + + if (! $destsources{$bestlink}) { + return $linktext unless length $config{cgiurl}; + return " "create", + page => pagetitle(lc($link), 1), + from => $lpage + ). + "\">?$linktext" + } } - $bestlink=abs2rel($bestlink, dirname($page)); + $bestlink=abs2rel($bestlink, dirname(htmlpage($page))); + $bestlink=beautify_url($bestlink); if (! $opts{noimageinline} && isinlinableimage($bestlink)) { return ""; @@ -466,7 +546,12 @@ sub htmllink ($$$;@) { #{{{ $bestlink.="#".$opts{anchor}; } - return "$linktext"; + my @attrs; + if (defined $opts{rel}) { + push @attrs, ' rel="'.$opts{rel}.'"'; + } + + return "$linktext"; } #}}} sub htmlize ($$$) { #{{{ @@ -501,33 +586,55 @@ sub linkify ($$$) { #{{{ $content =~ s{(\\?)$config{wiki_link_regexp}}{ defined $2 - ? ( $1 ? "[[$2|$3]]" : htmllink($lpage, $page, titlepage($3), anchor => $4, linktext => pagetitle($2))) - : ( $1 ? "[[$3]]" : htmllink($lpage, $page, titlepage($3), anchor => $4)) + ? ( $1 + ? "[[$2|$3".($4 ? "#$4" : "")."]]" + : htmllink($lpage, $page, linkpage($3), + anchor => $4, linktext => pagetitle($2))) + : ( $1 + ? "[[$3".($4 ? "#$4" : "")."]]" + : htmllink($lpage, $page, linkpage($3), + anchor => $4)) }eg; return $content; } #}}} my %preprocessing; -sub preprocess ($$$;$) { #{{{ +our $preprocess_preview=0; +sub preprocess ($$$;$$) { #{{{ my $page=shift; # the page the data comes from my $destpage=shift; # the page the data will appear in (different for inline) my $content=shift; my $scan=shift; + my $preview=shift; + + # Using local because it needs to be set within any nested calls + # of this function. + local $preprocess_preview=$preview if defined $preview; my $handle=sub { my $escape=shift; my $command=shift; my $params=shift; if (length $escape) { - return "[[$command $params]]"; + return "\\[[$command $params]]"; } elsif (exists $hooks{preprocess}{$command}) { return "" if $scan && ! $hooks{preprocess}{$command}{scan}; # Note: preserve order of params, some plugins may # consider it significant. my @params; - while ($params =~ /(?:(\w+)=)?(?:"""(.*?)"""|"([^"]+)"|(\S+))(?:\s+|$)/sg) { + while ($params =~ m{ + (?:(\w+)=)? # 1: named parameter key? + (?: + """(.*?)""" # 2: triple-quoted value + | + "([^"]+)" # 3: single-quoted value + | + (\S+) # 4: unquoted value + ) + (?:\s+|$) # delimiter to next param + }sgx) { my $key=$1; my $val; if (defined $2) { @@ -565,25 +672,48 @@ sub preprocess ($$$;$) { #{{{ @params, page => $page, destpage => $destpage, + preview => $preprocess_preview, ); $preprocessing{$page}--; return $ret; } else { - return "[[$command $params]]"; + return "\\[[$command $params]]"; } }; - $content =~ s{(\\?)\[\[(\w+)\s+((?:(?:\w+=)?(?:""".*?"""|"[^"]+"|[^\s\]]+)\s*)*)\]\]}{$handle->($1, $2, $3)}seg; + $content =~ s{ + (\\?) # 1: escape? + \[\[ # directive open + (\w+) # 2: command + \s+ + ( # 3: the parameters.. + (?: + (?:\w+=)? # named parameter key? + (?: + """.*?""" # triple-quoted value + | + "[^"]+" # single-quoted value + | + [^\s\]]+ # unquoted value + ) + \s* # whitespace or end + # of directive + ) + *) # 0 or more parameters + \]\] # directive closed + }{$handle->($1, $2, $3)}sexg; return $content; } #}}} -sub filter ($$) { #{{{ +sub filter ($$$) { #{{{ my $page=shift; + my $destpage=shift; my $content=shift; run_hooks(filter => sub { - $content=shift->(page => $page, content => $content); + $content=shift->(page => $page, destpage => $destpage, + content => $content); }); return $content; @@ -593,7 +723,8 @@ sub indexlink () { #{{{ return "$config{wikiname}"; } #}}} -sub lockwiki () { #{{{ +sub lockwiki (;$) { #{{{ + my $wait=@_ ? shift : 1; # Take an exclusive lock on the wiki to prevent multiple concurrent # run issues. The lock will be dropped on program exit. if (! -d $config{wikistatedir}) { @@ -601,22 +732,51 @@ sub lockwiki () { #{{{ } open(WIKILOCK, ">$config{wikistatedir}/lockfile") || error ("cannot write to $config{wikistatedir}/lockfile: $!"); - if (! flock(WIKILOCK, 2 | 4)) { - debug("wiki seems to be locked, waiting for lock"); - my $wait=600; # arbitrary, but don't hang forever to - # prevent process pileup - for (1..$wait) { - return if flock(WIKILOCK, 2 | 4); - sleep 1; + if (! flock(WIKILOCK, 2 | 4)) { # LOCK_EX | LOCK_NB + if ($wait) { + debug("wiki seems to be locked, waiting for lock"); + my $wait=600; # arbitrary, but don't hang forever to + # prevent process pileup + for (1..$wait) { + return if flock(WIKILOCK, 2 | 4); + sleep 1; + } + error("wiki is locked; waited $wait seconds without lock being freed (possible stuck process or stale lock?)"); + } + else { + return 0; } - error("wiki is locked; waited $wait seconds without lock being freed (possible stuck process or stale lock?)"); } + return 1; } #}}} sub unlockwiki () { #{{{ close WIKILOCK; } #}}} +sub commit_hook_enabled () { #{{{ + open(COMMITLOCK, "+>$config{wikistatedir}/commitlock") || + error ("cannot write to $config{wikistatedir}/commitlock: $!"); + if (! flock(COMMITLOCK, 1 | 4)) { # LOCK_SH | LOCK_NB to test + close COMMITLOCK; + return 0; + } + close COMMITLOCK; + return 1; +} #}}} + +sub disable_commit_hook () { #{{{ + open(COMMITLOCK, ">$config{wikistatedir}/commitlock") || + error ("cannot write to $config{wikistatedir}/commitlock: $!"); + if (! flock(COMMITLOCK, 2)) { # LOCK_EX + error("failed to get commit lock"); + } +} #}}} + +sub enable_commit_hook () { #{{{ + close COMMITLOCK; +} #}}} + sub loadindex () { #{{{ open (IN, "$config{wikistatedir}/index") || return; while (