X-Git-Url: http://git.vanrenterghem.biz/git.ikiwiki.info.git/blobdiff_plain/d4c61b72813b880d86b316770f2e3819a6428202..b72780e70b54bef4fd7fa52a3d2f221b9b4a46d4:/IkiWiki.pm?ds=sidebyside diff --git a/IkiWiki.pm b/IkiWiki.pm index 8f037ed04..0af4a4fe9 100644 --- a/IkiWiki.pm +++ b/IkiWiki.pm @@ -5,18 +5,22 @@ 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.01; # plugin interface 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 # Optimisation. use Memoize; @@ -24,21 +28,35 @@ memoize("abs2rel"); memoize("pagespec_translate"); memoize("file_pruned"); -my $installdir=''; # INSTALLDIR_AUTOREPLACE done by Makefile, DNE -our $version='unknown'; # VERSION_AUTOREPLACE done by Makefile, DNE - 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\]]+)\]\]/, + 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 => '', @@ -66,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 () { #{{{ @@ -82,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; @@ -108,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}: $@"); } @@ -121,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" @@ -151,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->(); } @@ -175,8 +199,8 @@ sub log_message ($$) { #{{{ $log_open=1; } eval { - Sys::Syslog::syslog($type, join(" ", @_)); - } + Sys::Syslog::syslog($type, "[$config{wikiname}] %s", join(" ", @_)); + }; } elsif (! $config{cgi}) { print "@_\n"; @@ -188,7 +212,7 @@ sub log_message ($$) { #{{{ sub possibly_foolish_untaint ($) { #{{{ my $tainted=shift; - my ($untainted)=$tainted=~/(.*)/; + my ($untainted)=$tainted=~/(.*)/s; return $untainted; } #}}} @@ -224,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 ($) { #{{{ @@ -235,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 ($;$$) { #{{{ @@ -293,7 +328,7 @@ sub writefile ($$$;$$) { #{{{ $writer->(\*OUT, $cleanup); } else { - 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") || @@ -316,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 ($$) { #{{{ @@ -344,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"; @@ -363,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 (;$) { #{{{ @@ -391,6 +441,7 @@ sub baseurl (;$) { #{{{ return "$config{url}/" if ! defined $page; + $page=htmlpage($page); $page=~s/[^\/]+$//; $page=~s/[^\/]+\//..\//g; return $page; @@ -412,51 +463,95 @@ 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 htmllink ($$$;$$$) { #{{{ +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) my $link=shift; - my $noimageinline=shift; # don't turn links into inline html images - my $forcesubpage=shift; # force a link to a subpage - my $linktext=shift; # set to force the link text to something + my %opts=@_; my $bestlink; - if (! $forcesubpage) { + if (! $opts{forcesubpage}) { $bestlink=bestlink($lpage, $link); } else { $bestlink="$lpage/".lc($link); } - $linktext=pagetitle(basename($link)) unless defined $linktext; + my $linktext; + if (defined $opts{linktext}) { + $linktext=$opts{linktext}; + } + else { + $linktext=pagetitle(basename($link)); + } 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 (! $noimageinline && isinlinableimage($bestlink)) { + if (! $opts{noimageinline} && isinlinableimage($bestlink)) { return ""; } - return "$linktext"; + + if (defined $opts{anchor}) { + $bestlink.="#".$opts{anchor}; + } + + my @attrs; + if (defined $opts{rel}) { + push @attrs, ' rel="'.$opts{rel}.'"'; + } + + return "$linktext"; } #}}} sub htmlize ($$$) { #{{{ @@ -490,33 +585,56 @@ sub linkify ($$$) { #{{{ my $content=shift; $content =~ s{(\\?)$config{wiki_link_regexp}}{ - $2 ? ( $1 ? "[[$2|$3]]" : htmllink($lpage, $page, titlepage($3), 0, 0, pagetitle($2))) - : ( $1 ? "[[$3]]" : htmllink($lpage, $page, titlepage($3))) + defined $2 + ? ( $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) { @@ -554,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; @@ -582,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}) { @@ -590,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 (