X-Git-Url: http://git.vanrenterghem.biz/git.ikiwiki.info.git/blobdiff_plain/135e5fc63a47d198e6ab6b0ebf30c87087d3f5d5..8ded9f57a12a27fa5fc1e272ec7b2d2a5aa28419:/IkiWiki.pm diff --git a/IkiWiki.pm b/IkiWiki.pm index 40622a5be..bab7b707a 100644 --- a/IkiWiki.pm +++ b/IkiWiki.pm @@ -1,22 +1,30 @@ #!/usr/bin/perl package IkiWiki; + use warnings; use strict; use Encode; use HTML::Entities; +use URI::Escape q{uri_escape_utf8}; +use POSIX; +use Storable; 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 + %pagestate %wikistate %renderedfiles %oldrenderedfiles + %pagesources %destsources %depends %hooks %forcerebuild + $gettext_obj %loaded_plugins}; 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 + add_underlay pagetitle titlepage linkpage newpagefile + inject + %config %links %pagestate %wikistate %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 @@ -26,54 +34,401 @@ memoize("abs2rel"); memoize("pagespec_translate"); memoize("file_pruned"); +sub getsetup () { #{{{ + wikiname => { + type => "string", + default => "wiki", + description => "name of the wiki", + safe => 1, + rebuild => 1, + }, + adminemail => { + type => "string", + default => undef, + example => 'me@example.com', + description => "contact email for wiki", + safe => 1, + rebuild => 0, + }, + adminuser => { + type => "string", + default => [], + description => "users who are wiki admins", + safe => 1, + rebuild => 0, + }, + banned_users => { + type => "string", + default => [], + description => "users who are banned from the wiki", + safe => 1, + rebuild => 0, + }, + srcdir => { + type => "string", + default => undef, + example => "$ENV{HOME}/wiki", + description => "where the source of the wiki is located", + safe => 0, # path + rebuild => 1, + }, + destdir => { + type => "string", + default => undef, + example => "/var/www/wiki", + description => "where to build the wiki", + safe => 0, # path + rebuild => 1, + }, + url => { + type => "string", + default => '', + example => "http://example.com/wiki", + description => "base url to the wiki", + safe => 1, + rebuild => 1, + }, + cgiurl => { + type => "string", + default => '', + example => "http://example.com/wiki/ikiwiki.cgi", + description => "url to the ikiwiki.cgi", + safe => 1, + rebuild => 1, + }, + cgi_wrapper => { + type => "string", + default => '', + example => "/var/www/wiki/ikiwiki.cgi", + description => "cgi wrapper to generate", + safe => 0, # file + rebuild => 0, + }, + cgi_wrappermode => { + type => "string", + default => '06755', + description => "mode for cgi_wrapper (can safely be made suid)", + safe => 0, + rebuild => 0, + }, + rcs => { + type => "string", + default => '', + description => "rcs backend to use", + safe => 0, # don't allow overriding + rebuild => 0, + }, + default_plugins => { + type => "internal", + default => [qw{mdwn link inline htmlscrubber passwordauth + openid signinedit lockedit conditional + recentchanges parentlinks editpage}], + description => "plugins to enable by default", + safe => 0, + rebuild => 1, + }, + add_plugins => { + type => "string", + default => [], + description => "plugins to add to the default configuration", + safe => 1, + rebuild => 1, + }, + disable_plugins => { + type => "string", + default => [], + description => "plugins to disable", + safe => 1, + rebuild => 1, + }, + templatedir => { + type => "string", + default => "$installdir/share/ikiwiki/templates", + description => "location of template files", + advanced => 1, + safe => 0, # path + rebuild => 1, + }, + underlaydir => { + type => "string", + default => "$installdir/share/ikiwiki/basewiki", + description => "base wiki source location", + advanced => 1, + safe => 0, # path + rebuild => 0, + }, + wrappers => { + type => "internal", + default => [], + description => "wrappers to generate", + safe => 0, + rebuild => 0, + }, + underlaydirs => { + type => "internal", + default => [], + description => "additional underlays to use", + safe => 0, + rebuild => 0, + }, + verbose => { + type => "boolean", + example => 1, + description => "display verbose messages when building?", + safe => 1, + rebuild => 0, + }, + syslog => { + type => "boolean", + example => 1, + description => "log to syslog?", + safe => 1, + rebuild => 0, + }, + usedirs => { + type => "boolean", + default => 1, + description => "create output files named page/index.html?", + safe => 0, # changing requires manual transition + rebuild => 1, + }, + prefix_directives => { + type => "boolean", + default => 0, + description => "use '!'-prefixed preprocessor directives?", + safe => 0, # changing requires manual transition + rebuild => 1, + }, + indexpages => { + type => "boolean", + default => 0, + description => "use page/index.mdwn source files", + safe => 1, + rebuild => 1, + }, + discussion => { + type => "boolean", + default => 1, + description => "enable Discussion pages?", + safe => 1, + rebuild => 1, + }, + sslcookie => { + type => "boolean", + default => 0, + description => "only send cookies over SSL connections?", + advanced => 1, + safe => 1, + rebuild => 0, + }, + default_pageext => { + type => "string", + default => "mdwn", + description => "extension to use for new pages", + safe => 0, # not sanitized + rebuild => 0, + }, + htmlext => { + type => "string", + default => "html", + description => "extension to use for html files", + safe => 0, # not sanitized + rebuild => 1, + }, + timeformat => { + type => "string", + default => '%c', + description => "strftime format string to display date", + advanced => 1, + safe => 1, + rebuild => 1, + }, + locale => { + type => "string", + default => undef, + example => "en_US.UTF-8", + description => "UTF-8 locale to use", + advanced => 1, + safe => 0, + rebuild => 1, + }, + userdir => { + type => "string", + default => "", + example => "users", + description => "put user pages below specified page", + safe => 1, + rebuild => 1, + }, + numbacklinks => { + type => "integer", + default => 10, + description => "how many backlinks to show before hiding excess (0 to show all)", + safe => 1, + rebuild => 1, + }, + hardlink => { + type => "boolean", + default => 0, + description => "attempt to hardlink source files? (optimisation for large files)", + advanced => 1, + safe => 0, # paranoia + rebuild => 0, + }, + umask => { + type => "integer", + description => "", + example => "022", + description => "force ikiwiki to use a particular umask", + advanced => 1, + safe => 0, # paranoia + rebuild => 0, + }, + libdir => { + type => "string", + default => "", + example => "$ENV{HOME}/.ikiwiki/", + description => "extra library and plugin directory", + advanced => 1, + safe => 0, # directory + rebuild => 0, + }, + ENV => { + type => "string", + default => {}, + description => "environment variables", + safe => 0, # paranoia + rebuild => 0, + }, + exclude => { + type => "string", + default => undef, + example => '\.wav$', + description => "regexp of source files to ignore", + advanced => 1, + safe => 0, # regexp + rebuild => 1, + }, + wiki_file_prune_regexps => { + type => "internal", + default => [qr/(^|\/)\.\.(\/|$)/, qr/^\./, qr/\/\./, + qr/\.x?html?$/, qr/\.ikiwiki-new$/, + qr/(^|\/).svn\//, qr/.arch-ids\//, qr/{arch}\//, + qr/(^|\/)_MTN\//, + qr/\.dpkg-tmp$/], + description => "regexps of source files to ignore", + safe => 0, + rebuild => 1, + }, + wiki_file_chars => { + type => "string", + description => "specifies the characters that are allowed in source filenames", + default => "-[:alnum:]+/.:_", + safe => 0, + rebuild => 1, + }, + wiki_file_regexp => { + type => "internal", + description => "regexp of legal source files", + safe => 0, + rebuild => 1, + }, + web_commit_regexp => { + type => "internal", + default => qr/^web commit (by (.*?(?=: |$))|from (\d+\.\d+\.\d+\.\d+)):?(.*)/, + description => "regexp to parse web commits from logs", + safe => 0, + rebuild => 0, + }, + cgi => { + type => "internal", + default => 0, + description => "run as a cgi", + safe => 0, + rebuild => 0, + }, + cgi_disable_uploads => { + type => "internal", + default => 1, + description => "whether CGI should accept file uploads", + safe => 0, + rebuild => 0, + }, + post_commit => { + type => "internal", + default => 0, + description => "run as a post-commit hook", + safe => 0, + rebuild => 0, + }, + rebuild => { + type => "internal", + default => 0, + description => "running in rebuild mode", + safe => 0, + rebuild => 0, + }, + setup => { + type => "internal", + default => undef, + description => "running in setup mode", + safe => 0, + rebuild => 0, + }, + refresh => { + type => "internal", + default => 0, + description => "running in refresh mode", + safe => 0, + rebuild => 0, + }, + test_receive => { + type => "internal", + default => 0, + description => "running in receive test mode", + safe => 0, + rebuild => 0, + }, + getctime => { + type => "internal", + default => 0, + description => "running in getctime mode", + safe => 0, + rebuild => 0, + }, + w3mmode => { + type => "internal", + default => 0, + description => "running in w3mmode", + safe => 0, + rebuild => 0, + }, + setupfile => { + type => "internal", + default => undef, + description => "path to setup file", + safe => 0, + rebuild => 0, + }, + allow_symlinks_before_srcdir => { + type => "boolean", + default => 0, + description => "allow symlinks in the path leading to the srcdir (potentially insecure)", + safe => 0, + rebuild => 0, + }, +} #}}} + 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\]]+))?\]\]/, - 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", - cgi => 0, - rcs => '', - notify => 0, - url => '', - cgiurl => '', - historyurl => '', - diffurl => '', - rss => 0, - atom => 0, - discussion => 1, - rebuild => 0, - refresh => 0, - getctime => 0, - w3mmode => 0, - wrapper => undef, - wrappermode => undef, - svnrepo => undef, - svnpath => "trunk", - gitorigin_branch => "origin", - gitmaster_branch => "master", - srcdir => undef, - destdir => undef, - pingurl => [], - templatedir => "$installdir/share/ikiwiki/templates", - underlaydir => "$installdir/share/ikiwiki/basewiki", - setup => undef, - adminuser => undef, - adminemail => undef, - plugin => [qw{mdwn inline htmlscrubber passwordauth signinedit - lockedit conditional}], - timeformat => '%c', - locale => undef, - sslcookie => 0, - httpauth => 0, - userdir => "", -} #}}} - + my %s=getsetup(); + my @ret; + foreach my $key (keys %s) { + push @ret, $key, $s{$key}->{default}; + } + use Data::Dumper; + return @ret; +} #}}} + sub checkconfig () { #{{{ # locale stuff; avoid LC_ALL since it overrides everything if (defined $ENV{LC_ALL}) { @@ -81,13 +436,21 @@ 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; } } + + if (! defined $config{wiki_file_regexp}) { + $config{wiki_file_regexp}=qr/(^[$config{wiki_file_chars}]+$)/; + } + + if (ref $config{ENV} eq 'HASH') { + foreach my $val (keys %{$config{ENV}}) { + $ENV{$val}=$config{ENV}{$val}; + } + } if ($config{w3mmode}) { eval q{use Cwd q{abs_path}}; @@ -105,29 +468,63 @@ sub checkconfig () { #{{{ $config{wikistatedir}="$config{srcdir}/.ikiwiki" unless exists $config{wikistatedir}; - - if ($config{rcs}) { - eval qq{require IkiWiki::Rcs::$config{rcs}}; - if ($@) { - error("Failed to load RCS module IkiWiki::Rcs::$config{rcs}: $@"); + + if (defined $config{umask}) { + umask(possibly_foolish_untaint($config{umask})); + } + + run_hooks(checkconfig => sub { shift->() }); + + return 1; +} #}}} + +sub listplugins () { #{{{ + my %ret; + + foreach my $dir (@INC, $config{libdir}) { + next unless defined $dir && length $dir; + foreach my $file (glob("$dir/IkiWiki/Plugin/*.pm")) { + my ($plugin)=$file=~/.*\/(.*)\.pm$/; + $ret{$plugin}=1; } } - else { - require IkiWiki::Rcs::Stub; + foreach my $dir ($config{libdir}, "$installdir/lib/ikiwiki") { + next unless defined $dir && length $dir; + foreach my $file (glob("$dir/plugins/*")) { + $ret{basename($file)}=1 if -x $file; + } } - run_hooks(checkconfig => sub { shift->() }); + return keys %ret; } #}}} sub loadplugins () { #{{{ - loadplugin($_) foreach @{$config{plugin}}; + if (defined $config{libdir} && length $config{libdir}) { + unshift @INC, possibly_foolish_untaint($config{libdir}); + } + + foreach my $plugin (@{$config{default_plugins}}, @{$config{add_plugins}}) { + loadplugin($plugin); + } + if ($config{rcs}) { + if (exists $IkiWiki::hooks{rcs}) { + error(gettext("cannot use multiple rcs plugins")); + } + loadplugin($config{rcs}); + } + if (! exists $IkiWiki::hooks{rcs}) { + loadplugin("norcs"); + } + run_hooks(getopt => sub { shift->() }); if (grep /^-/, @ARGV) { print STDERR "Unknown option: $_\n" foreach grep /^-/, @ARGV; usage(); } + + return 1; } #}}} sub loadplugin ($) { #{{{ @@ -135,22 +532,33 @@ sub loadplugin ($) { #{{{ return if grep { $_ eq $plugin} @{$config{disable_plugins}}; + foreach my $dir (defined $config{libdir} ? possibly_foolish_untaint($config{libdir}) : undef, + "$installdir/lib/ikiwiki") { + if (defined $dir && -x "$dir/plugins/$plugin") { + eval { require IkiWiki::Plugin::external }; + if ($@) { + my $reason=$@; + error(sprintf(gettext("failed to load external plugin needed for %s plugin: %s"), $plugin, $reason)); + } + import IkiWiki::Plugin::external "$dir/plugins/$plugin"; + $loaded_plugins{$plugin}=1; + return 1; + } + } + my $mod="IkiWiki::Plugin::".possibly_foolish_untaint($plugin); eval qq{use $mod}; if ($@) { error("Failed to load plugin $mod: $@"); } + $loaded_plugins{$plugin}=1; + return 1; } #}}} sub error ($;$) { #{{{ my $message=shift; my $cleaner=shift; - if ($config{cgi}) { - print "Content-type: text/html\n\n"; - 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->(); } @@ -159,7 +567,7 @@ sub error ($;$) { #{{{ sub debug ($) { #{{{ return unless $config{verbose}; - log_message(debug => @_); + return log_message(debug => @_); } #}}} my $log_open=0; @@ -168,26 +576,26 @@ sub log_message ($$) { #{{{ if ($config{syslog}) { require Sys::Syslog; - unless ($log_open) { + if (! $log_open) { Sys::Syslog::setlogsock('unix'); Sys::Syslog::openlog('ikiwiki', '', 'user'); $log_open=1; } - eval { - Sys::Syslog::syslog($type, "%s", join(" ", @_)); - } + return eval { + Sys::Syslog::syslog($type, "[$config{wikiname}] %s", join(" ", @_)); + }; } elsif (! $config{cgi}) { - print "@_\n"; + return print "@_\n"; } else { - print STDERR "@_\n"; + return print STDERR "@_\n"; } } #}}} sub possibly_foolish_untaint ($) { #{{{ my $tainted=shift; - my ($untainted)=$tainted=~/(.*)/; + my ($untainted)=$tainted=~/(.*)/s; return $untainted; } #}}} @@ -211,7 +619,13 @@ sub pagetype ($) { #{{{ if ($page =~ /\.([^.]+)$/) { return $1 if exists $hooks{htmlize}{$1}; } - return undef; + return; +} #}}} + +sub isinternal ($) { #{{{ + my $page=shift; + return exists $pagesources{$page} && + $pagesources{$page} =~ /\._([^.]+)$/; } #}}} sub pagename ($) { #{{{ @@ -219,22 +633,71 @@ sub pagename ($) { #{{{ my $type=pagetype($file); my $page=$file; - $page=~s/\Q.$type\E*$// if defined $type; + $page=~s/\Q.$type\E*$// if defined $type && !$hooks{htmlize}{$type}{keepextension}; + if ($config{indexpages} && $page=~/(.*)\/index$/) { + $page=$1; + } return $page; } #}}} -sub htmlpage ($) { #{{{ +sub newpagefile ($$) { #{{{ my $page=shift; + my $type=shift; - return $page.".html"; + if (! $config{indexpages} || $page eq 'index') { + return $page.".".$type; + } + else { + return $page."/index.".$type; + } } #}}} -sub srcfile ($) { #{{{ +sub targetpage ($$) { #{{{ + my $page=shift; + my $ext=shift; + + if (! $config{usedirs} || $page eq 'index') { + return $page.".".$ext; + } + else { + return $page."/index.".$ext; + } +} #}}} + +sub htmlpage ($) { #{{{ + my $page=shift; + + return targetpage($page, $config{htmlext}); +} #}}} + +sub srcfile_stat { #{{{ my $file=shift; + my $nothrow=shift; - 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"); + return "$config{srcdir}/$file", stat(_) if -e "$config{srcdir}/$file"; + foreach my $dir (@{$config{underlaydirs}}, $config{underlaydir}) { + return "$dir/$file", stat(_) if -e "$dir/$file"; + } + error("internal error: $file cannot be found in $config{srcdir} or underlay") unless $nothrow; + return; +} #}}} + +sub srcfile ($;$) { #{{{ + return (srcfile_stat(@_))[0]; +} #}}} + +sub add_underlay ($) { #{{{ + my $dir=shift; + + if ($dir !~ /^\//) { + $dir="$config{underlaydir}/../$dir"; + } + + if (! grep { $_ eq $dir } @{$config{underlaydirs}}) { + unshift @{$config{underlaydirs}}, $dir; + } + + return 1; } #}}} sub readfile ($;$$) { #{{{ @@ -247,20 +710,17 @@ sub readfile ($;$$) { #{{{ } local $/=undef; - open (IN, $file) || error("failed to read $file: $!"); - binmode(IN) if ($binary); - return \*IN if $wantfd; - my $ret=//i;
+ $content=~s/<\/p>$//i;
+ chomp $content;
+ }
return $content;
} #}}}
sub linkify ($$$) { #{{{
- my $lpage=shift; # the page containing the links
- my $page=shift; # the page the link will end up on (different for inline)
+ my $page=shift;
+ my $destpage=shift;
my $content=shift;
- $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))
- }eg;
+ run_hooks(linkify => sub {
+ $content=shift->(
+ page => $page,
+ destpage => $destpage,
+ content => $content,
+ );
+ });
return $content;
} #}}}
-my %preprocessing;
-sub preprocess ($$$;$) { #{{{
+our %preprocessing;
+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 $prefix=shift;
my $command=shift;
my $params=shift;
+ $params="" if ! defined $params;
+
if (length $escape) {
- return "[[$command $params]]";
+ return "[[$prefix$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) {
@@ -553,37 +1161,107 @@ sub preprocess ($$$;$) { #{{{
if ($preprocessing{$page}++ > 3) {
# Avoid loops of preprocessed pages preprocessing
# other pages that preprocess them, etc.
- #translators: The first parameter is a
- #translators: preprocessor directive name,
- #translators: the second a page name, the
- #translators: third a number.
- return "[[".sprintf(gettext("%s preprocessing loop detected on %s at depth %i"),
- $command, $page, $preprocessing{$page}).
- "]]";
+ return "[[!$command ".
+ sprintf(gettext("preprocessing loop detected on %s at depth %i"),
+ $page, $preprocessing{$page}).
+ "]]";
+ }
+ my $ret;
+ if (! $scan) {
+ $ret=eval {
+ $hooks{preprocess}{$command}{call}->(
+ @params,
+ page => $page,
+ destpage => $destpage,
+ preview => $preprocess_preview,
+ );
+ };
+ if ($@) {
+ chomp $@;
+ $ret="[[!$command ".
+ gettext("Error").": $@"."]]";
+ }
+ }
+ else {
+ # use void context during scan pass
+ eval {
+ $hooks{preprocess}{$command}{call}->(
+ @params,
+ page => $page,
+ destpage => $destpage,
+ preview => $preprocess_preview,
+ );
+ };
+ $ret="";
}
- my $ret=$hooks{preprocess}{$command}{call}->(
- @params,
- page => $page,
- destpage => $destpage,
- );
$preprocessing{$page}--;
return $ret;
}
else {
- return "[[$command $params]]";
+ return "[[$prefix$command $params]]";
}
};
- $content =~ s{(\\?)\[\[(\w+)\s+((?:(?:\w+=)?(?:""".*?"""|"[^"]+"|[^\s\]]+)\s*)*)\]\]}{$handle->($1, $2, $3)}seg;
+ my $regex;
+ if ($config{prefix_directives}) {
+ $regex = qr{
+ (\\?) # 1: escape?
+ \[\[(!) # directive open; 2: prefix
+ ([-\w]+) # 3: command
+ ( # 4: the parameters..
+ \s+ # Must have space if parameters present
+ (?:
+ (?:[-\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
+ }sx;
+ }
+ else {
+ $regex = qr{
+ (\\?) # 1: escape?
+ \[\[(!?) # directive open; 2: optional prefix
+ ([-\w]+) # 3: command
+ \s+
+ ( # 4: 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
+ }sx;
+ }
+
+ $content =~ s{$regex}{$handle->($1, $2, $3, $4)}eg;
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,86 +1271,185 @@ sub indexlink () { #{{{
return "$config{wikiname}";
} #}}}
-sub lockwiki () { #{{{
+my $wikilock;
+
+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}) {
mkdir($config{wikistatedir});
}
- open(WIKILOCK, ">$config{wikistatedir}/lockfile") ||
+ 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;
+ return close($wikilock) if $wikilock;
+ return;
+} #}}}
+
+my $commitlock;
+
+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) || error("failed closing commitlock: $!");
+ return 0;
+ }
+ close($commitlock) || error("failed closing 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");
+ }
+ return 1;
+} #}}}
+
+sub enable_commit_hook () { #{{{
+ return close($commitlock) if $commitlock;
+ return;
} #}}}
sub loadindex () { #{{{
- open (IN, "$config{wikistatedir}/index") || return;
- while (