X-Git-Url: https://sipb.mit.edu/gitweb.cgi/ikiwiki.git/blobdiff_plain/c2ffd205f3301a9fe6b5949a88da770899642589..1506638f15797cc13155abfd8adb6ee34c0697bd:/IkiWiki.pm diff --git a/IkiWiki.pm b/IkiWiki.pm index c14124f79..2ad2f792d 100644 --- a/IkiWiki.pm +++ b/IkiWiki.pm @@ -12,15 +12,17 @@ use Storable; use open qw{:utf8 :std}; use vars qw{%config %links %oldlinks %pagemtime %pagectime %pagecase - %pagestate %renderedfiles %oldrenderedfiles %pagesources - %destsources %depends %hooks %forcerebuild $gettext_obj}; + %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 urlto targetpage - add_underlay - %config %links %pagestate %renderedfiles + 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 @@ -32,65 +34,413 @@ 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 meta 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", + example => "022", + description => "force ikiwiki to use a particular umask", + advanced => 1, + safe => 0, # paranoia + rebuild => 0, + }, + wrappergroup => { + type => "string", + example => "ikiwiki", + description => "group for wrappers to run in", + 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, + }, + wikistatedir => { + type => "internal", + default => undef, + description => "path to the .ikiwiki directory holding ikiwiki state", + 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 () { #{{{ - return - wiki_file_prune_regexps => [qr/(^|\/)\.\.(\/|$)/, qr/^\./, qr/\/\./, - qr/\.x?html?$/, qr/\.ikiwiki-new$/, - qr/(^|\/).svn\//, qr/.arch-ids\//, qr/{arch}\//, - qr/(^|\/)_MTN\//, - qr/\.dpkg-tmp$/], - 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 => '', - url => '', - cgiurl => '', - historyurl => '', - diffurl => '', - rss => 0, - atom => 0, - allowrss => 0, - allowatom => 0, - discussion => 1, - rebuild => 0, - refresh => 0, - getctime => 0, - w3mmode => 0, - wrapper => undef, - wrappermode => undef, - svnpath => "trunk", - gitorigin_branch => "origin", - gitmaster_branch => "master", - srcdir => undef, - destdir => undef, - pingurl => [], - templatedir => "$installdir/share/ikiwiki/templates", - underlaydir => "$installdir/share/ikiwiki/basewiki", - underlaydirs => [], - setup => undef, - adminuser => undef, - adminemail => undef, - plugin => [qw{mdwn link inline htmlscrubber passwordauth openid - signinedit lockedit conditional recentchanges - parentlinks}], - libdir => undef, - timeformat => '%c', - locale => undef, - sslcookie => 0, - httpauth => 0, - userdir => "", - usedirs => 1, - numbacklinks => 10, - account_creation_password => "", - prefix_directives => 0, - hardlink => 0, - cgi_disable_uploads => 1, + my %s=getsetup(); + my @ret; + foreach my $key (keys %s) { + push @ret, $key, $s{$key}->{default}; + } + use Data::Dumper; + return @ret; } #}}} sub checkconfig () { #{{{ @@ -105,6 +455,10 @@ sub checkconfig () { #{{{ $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}}) { @@ -127,19 +481,9 @@ sub checkconfig () { #{{{ } $config{wikistatedir}="$config{srcdir}/.ikiwiki" - unless exists $config{wikistatedir}; - - if ($config{rcs}) { - eval qq{use IkiWiki::Rcs::$config{rcs}}; - if ($@) { - error("Failed to load RCS module IkiWiki::Rcs::$config{rcs}: $@"); - } - } - else { - require IkiWiki::Rcs::Stub; - } + unless exists $config{wikistatedir} && defined $config{wikistatedir}; - if (exists $config{umask}) { + if (defined $config{umask}) { umask(possibly_foolish_untaint($config{umask})); } @@ -148,12 +492,44 @@ sub checkconfig () { #{{{ 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; + } + } + 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; + } + } + + return keys %ret; +} #}}} + sub loadplugins () { #{{{ - if (defined $config{libdir}) { + if (defined $config{libdir} && length $config{libdir}) { unshift @INC, possibly_foolish_untaint($config{libdir}); } - loadplugin($_) foreach @{$config{plugin}}; + 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) { @@ -173,8 +549,13 @@ sub loadplugin ($) { #{{{ foreach my $dir (defined $config{libdir} ? possibly_foolish_untaint($config{libdir}) : undef, "$installdir/lib/ikiwiki") { if (defined $dir && -x "$dir/plugins/$plugin") { - require IkiWiki::Plugin::external; + 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; } } @@ -184,6 +565,7 @@ sub loadplugin ($) { #{{{ if ($@) { error("Failed to load plugin $mod: $@"); } + $loaded_plugins{$plugin}=1; return 1; } #}}} @@ -265,17 +647,33 @@ 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 newpagefile ($$) { #{{{ + my $page=shift; + my $type=shift; + + if (! $config{indexpages} || $page eq 'index') { + return $page.".".$type; + } + else { + return $page."/index.".$type; + } +} #}}} + sub targetpage ($$) { #{{{ my $page=shift; my $ext=shift; - if (! $config{usedirs} || $page =~ /^index$/ ) { + if (! $config{usedirs} || $page eq 'index') { return $page.".".$ext; - } else { + } + else { return $page."/index.".$ext; } } #}}} @@ -305,11 +703,12 @@ sub srcfile ($;$) { #{{{ sub add_underlay ($) { #{{{ my $dir=shift; - if ($dir=~/^\//) { - unshift @{$config{underlaydirs}}, $dir; + if ($dir !~ /^\//) { + $dir="$config{underlaydir}/../$dir"; } - else { - unshift @{$config{underlaydirs}}, "$config{underlaydir}/../$dir"; + + if (! grep { $_ eq $dir } @{$config{underlaydirs}}) { + unshift @{$config{underlaydirs}}, $dir; } return 1; @@ -329,6 +728,10 @@ sub readfile ($;$$) { #{{{ binmode($in) if ($binary); return \*$in if $wantfd; my $ret=<$in>; + # check for invalid utf-8, and toss it back to avoid crashes + if (! utf8::valid($ret)) { + $ret=encode_utf8($ret); + } close $in || error("failed to read $file: $!"); return $ret; } #}}} @@ -397,7 +800,7 @@ sub will_render ($$;$) { #{{{ # Important security check. if (-e "$config{destdir}/$dest" && ! $config{rebuild} && - ! grep { $_ eq $dest } (@{$renderedfiles{$page}}, @{$oldrenderedfiles{$page}})) { + ! grep { $_ eq $dest } (@{$renderedfiles{$page}}, @{$oldrenderedfiles{$page}}, @{$wikistate{editpage}{previews}})) { error("$config{destdir}/$dest independently created, not overwriting with version from $page"); } @@ -438,7 +841,7 @@ sub bestlink ($$) { #{{{ elsif (exists $pagecase{lc $l}) { return $pagecase{lc $l}; } - } while $cwd=~s!/?[^/]+$!!; + } while $cwd=~s{/?[^/]+$}{}; if (length $config{userdir}) { my $l = "$config{userdir}/".lc($link); @@ -476,13 +879,16 @@ sub pagetitle ($;$) { #{{{ sub titlepage ($) { #{{{ my $title=shift; - $title=~s/([^-[:alnum:]:+\/.])/$1 eq ' ' ? '_' : "__".ord($1)."__"/eg; + # support use w/o %config set + my $chars = defined $config{wiki_file_chars} ? $config{wiki_file_chars} : "-[:alnum:]+/.:_"; + $title=~s/([^$chars]|_)/$1 eq ' ' ? '_' : "__".ord($1)."__"/eg; return $title; } #}}} sub linkpage ($) { #{{{ my $link=shift; - $link=~s/([^-[:alnum:]:+\/._])/$1 eq ' ' ? '_' : "__".ord($1)."__"/eg; + my $chars = defined $config{wiki_file_chars} ? $config{wiki_file_chars} : "-[:alnum:]+/.:_"; + $link=~s/([^$chars])/$1 eq ' ' ? '_' : "__".ord($1)."__"/eg; return $link; } #}}} @@ -518,6 +924,13 @@ sub abs2rel ($$) { #{{{ } #}}} sub displaytime ($;$) { #{{{ + # Plugins can override this function to mark up the time to + # display. + return ''.formattime(@_).''; +} #}}} + +sub formattime ($;$) { #{{{ + # Plugins can override this function to format the time. my $time=shift; my $format=shift; if (! defined $format) { @@ -536,19 +949,20 @@ sub beautify_urlpath ($) { #{{{ $url =~ s!/index.$config{htmlext}$!/!; } - # Ensure url is not an empty link, and - # if it's relative, make that explicit to avoid colon confusion. - if ($url !~ /^\//) { + # Ensure url is not an empty link, and if necessary, + # add ./ to avoid colon confusion. + if ($url !~ /^\// && $url !~ /^\.\.\//) { $url="./$url"; } return $url; } #}}} -sub urlto ($$) { #{{{ +sub urlto ($$;$) { #{{{ my $to=shift; my $from=shift; - + my $absolute=shift; + if (! length $to) { return beautify_urlpath(baseurl($from)."index.$config{htmlext}"); } @@ -557,6 +971,10 @@ sub urlto ($$) { #{{{ $to=htmlpage($to); } + if ($absolute) { + return $config{url}.beautify_urlpath("/".$to); + } + my $link = abs2rel($to, dirname(htmlpage($from))); return beautify_urlpath($link); @@ -715,6 +1133,8 @@ sub preprocess ($$$;$$) { #{{{ my $prefix=shift; my $command=shift; my $params=shift; + $params="" if ! defined $params; + if (length $escape) { return "[[$prefix$command $params]]"; } @@ -759,13 +1179,10 @@ 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) { @@ -874,8 +1291,7 @@ sub indexlink () { #{{{ my $wikilock; -sub lockwiki (;$) { #{{{ - my $wait=@_ ? shift : 1; +sub lockwiki () { #{{{ # 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}) { @@ -883,25 +1299,14 @@ sub lockwiki (;$) { #{{{ } open($wikilock, '>', "$config{wikistatedir}/lockfile") || error ("cannot write to $config{wikistatedir}/lockfile: $!"); - 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; - } + if (! flock($wikilock, 2)) { # LOCK_EX + error("failed to get lock"); } return 1; } #}}} sub unlockwiki () { #{{{ + POSIX::close($ENV{IKIWIKI_CGILOCK_FD}) if exists $ENV{IKIWIKI_CGILOCK_FD}; return close($wikilock) if $wikilock; return; } #}}} @@ -949,31 +1354,42 @@ sub loadindex () { #{{{ return; } } - my $ret=Storable::fd_retrieve($in); - if (! defined $ret) { + + my $index=Storable::fd_retrieve($in); + if (! defined $index) { return 0; } - my %index=%$ret; - foreach my $src (keys %index) { - my %d=%{$index{$src}}; + + my $pages; + if (exists $index->{version} && ! ref $index->{version}) { + $pages=$index->{page}; + %wikistate=%{$index->{state}}; + } + else { + $pages=$index; + %wikistate=(); + } + + foreach my $src (keys %$pages) { + my $d=$pages->{$src}; my $page=pagename($src); - $pagectime{$page}=$d{ctime}; + $pagectime{$page}=$d->{ctime}; if (! $config{rebuild}) { $pagesources{$page}=$src; - $pagemtime{$page}=$d{mtime}; - $renderedfiles{$page}=$d{dest}; - if (exists $d{links} && ref $d{links}) { - $links{$page}=$d{links}; - $oldlinks{$page}=[@{$d{links}}]; + $pagemtime{$page}=$d->{mtime}; + $renderedfiles{$page}=$d->{dest}; + if (exists $d->{links} && ref $d->{links}) { + $links{$page}=$d->{links}; + $oldlinks{$page}=[@{$d->{links}}]; } - if (exists $d{depends}) { - $depends{$page}=$d{depends}; + if (exists $d->{depends}) { + $depends{$page}=$d->{depends}; } - if (exists $d{state}) { - $pagestate{$page}=$d{state}; + if (exists $d->{state}) { + $pagestate{$page}=$d->{state}; } } - $oldrenderedfiles{$page}=[@{$d{dest}}]; + $oldrenderedfiles{$page}=[@{$d->{dest}}]; } foreach my $page (keys %pagesources) { $pagecase{lc $page}=$page; @@ -999,12 +1415,13 @@ sub saveindex () { #{{{ my $newfile="$config{wikistatedir}/indexdb.new"; my $cleanup = sub { unlink($newfile) }; open (my $out, '>', $newfile) || error("cannot write to $newfile: $!", $cleanup); + my %index; foreach my $page (keys %pagemtime) { next unless $pagemtime{$page}; my $src=$pagesources{$page}; - $index{$src}={ + $index{page}{$src}={ ctime => $pagectime{$page}, mtime => $pagemtime{$page}, dest => $renderedfiles{$page}, @@ -1012,17 +1429,26 @@ sub saveindex () { #{{{ }; if (exists $depends{$page}) { - $index{$src}{depends} = $depends{$page}; + $index{page}{$src}{depends} = $depends{$page}; } if (exists $pagestate{$page}) { foreach my $id (@hookids) { foreach my $key (keys %{$pagestate{$page}{$id}}) { - $index{$src}{state}{$id}{$key}=$pagestate{$page}{$id}{$key}; + $index{page}{$src}{state}{$id}{$key}=$pagestate{$page}{$id}{$key}; } } } } + + $index{state}={}; + foreach my $id (@hookids) { + foreach my $key (keys %{$wikistate{$id}}) { + $index{state}{$id}{$key}=$wikistate{$id}{$key}; + } + } + + $index{version}="3"; my $ret=Storable::nstore_fd(\%index, $out); return if ! defined $ret || ! $ret; close $out || error("failed saving to $newfile: $!", $cleanup); @@ -1122,6 +1548,50 @@ sub run_hooks ($$) { # {{{ return 1; } #}}} +sub rcs_update () { #{{{ + $hooks{rcs}{rcs_update}{call}->(@_); +} #}}} + +sub rcs_prepedit ($) { #{{{ + $hooks{rcs}{rcs_prepedit}{call}->(@_); +} #}}} + +sub rcs_commit ($$$;$$) { #{{{ + $hooks{rcs}{rcs_commit}{call}->(@_); +} #}}} + +sub rcs_commit_staged ($$$) { #{{{ + $hooks{rcs}{rcs_commit_staged}{call}->(@_); +} #}}} + +sub rcs_add ($) { #{{{ + $hooks{rcs}{rcs_add}{call}->(@_); +} #}}} + +sub rcs_remove ($) { #{{{ + $hooks{rcs}{rcs_remove}{call}->(@_); +} #}}} + +sub rcs_rename ($$) { #{{{ + $hooks{rcs}{rcs_rename}{call}->(@_); +} #}}} + +sub rcs_recentchanges ($) { #{{{ + $hooks{rcs}{rcs_recentchanges}{call}->(@_); +} #}}} + +sub rcs_diff ($) { #{{{ + $hooks{rcs}{rcs_diff}{call}->(@_); +} #}}} + +sub rcs_getctime ($) { #{{{ + $hooks{rcs}{rcs_getctime}{call}->(@_); +} #}}} + +sub rcs_receive () { #{{{ + $hooks{rcs}{rcs_receive}{call}->(); +} #}}} + sub globlist_to_pagespec ($) { #{{{ my @globlist=split(' ', shift); @@ -1214,6 +1684,31 @@ sub yesno ($) { #{{{ return (defined $val && lc($val) eq gettext("yes")); } #}}} +sub inject { #{{{ + # Injects a new function into the symbol table to replace an + # exported function. + my %params=@_; + + # This is deep ugly perl foo, beware. + no strict; + no warnings; + if (! defined $params{parent}) { + $params{parent}='::'; + $params{old}=\&{$params{name}}; + $params{name}=~s/.*:://; + } + my $parent=$params{parent}; + foreach my $ns (grep /^\w+::/, keys %{$parent}) { + $ns = $params{parent} . $ns; + inject(%params, parent => $ns) unless $ns eq '::main::'; + *{$ns . $params{name}} = $params{call} + if exists ${$ns}{$params{name}} && + \&{${$ns}{$params{name}}} == $params{old}; + } + use strict; + use warnings; +} #}}} + sub pagespec_merge ($$) { #{{{ my $a=shift; my $b=shift; @@ -1308,7 +1803,7 @@ sub pagespec_valid ($) { #{{{ my $sub=pagespec_translate($spec); return ! $@; } #}}} - + sub glob2re ($) { #{{{ my $re=quotemeta(shift); $re=~s/\\\*/.*/g; @@ -1405,6 +1900,10 @@ sub match_link ($$;@) { #{{{ else { return IkiWiki::SuccessReason->new("$page links to page $p matching $link") if match_glob($p, $link, %params); + $p=~s/^\///; + $link=~s/^\///; + return IkiWiki::SuccessReason->new("$page links to page $p matching $link") + if match_glob($p, $link, %params); } } return IkiWiki::FailReason->new("$page does not link to $link"); @@ -1475,4 +1974,61 @@ sub match_creation_year ($$;@) { #{{{ } } #}}} +sub match_user ($$;@) { #{{{ + shift; + my $user=shift; + my %params=@_; + + if (! exists $params{user}) { + return IkiWiki::FailReason->new("no user specified"); + } + + if (defined $params{user} && lc $params{user} eq lc $user) { + return IkiWiki::SuccessReason->new("user is $user"); + } + elsif (! defined $params{user}) { + return IkiWiki::FailReason->new("not logged in"); + } + else { + return IkiWiki::FailReason->new("user is $params{user}, not $user"); + } +} #}}} + +sub match_admin ($$;@) { #{{{ + shift; + shift; + my %params=@_; + + if (! exists $params{user}) { + return IkiWiki::FailReason->new("no user specified"); + } + + if (defined $params{user} && IkiWiki::is_admin($params{user})) { + return IkiWiki::SuccessReason->new("user is an admin"); + } + elsif (! defined $params{user}) { + return IkiWiki::FailReason->new("not logged in"); + } + else { + return IkiWiki::FailReason->new("user is not an admin"); + } +} #}}} + +sub match_ip ($$;@) { #{{{ + shift; + my $ip=shift; + my %params=@_; + + if (! exists $params{ip}) { + return IkiWiki::FailReason->new("no IP specified"); + } + + if (defined $params{ip} && lc $params{ip} eq lc $ip) { + return IkiWiki::SuccessReason->new("IP is $ip"); + } + else { + return IkiWiki::FailReason->new("IP is $params{ip}, not $ip"); + } +} #}}} + 1