X-Git-Url: http://git.vanrenterghem.biz/git.ikiwiki.info.git/blobdiff_plain/8dc7f3005ebf36d5b126867e7ca0440d495d8ee3..ce8b34f8d2e122249da465c3750818cc35972c77:/IkiWiki.pm diff --git a/IkiWiki.pm b/IkiWiki.pm index ec8b32a63..b7080bb0b 100644 --- a/IkiWiki.pm +++ b/IkiWiki.pm @@ -5,7 +5,6 @@ package IkiWiki; use warnings; use strict; use Encode; -use HTML::Entities; use URI::Escape q{uri_escape_utf8}; use POSIX (); use Storable; @@ -13,17 +12,17 @@ use open qw{:utf8 :std}; use vars qw{%config %links %oldlinks %pagemtime %pagectime %pagecase %pagestate %wikistate %renderedfiles %oldrenderedfiles - %pagesources %destsources %depends %depends_simple %hooks - %forcerebuild %loaded_plugins %typedlinks %oldtypedlinks - %autofiles}; + %pagesources %delpagesources %destsources %depends %depends_simple + @mass_depends %hooks %forcerebuild %loaded_plugins %typedlinks + %oldtypedlinks %autofiles @underlayfiles $lastrev}; use Exporter q{import}; -our @EXPORT = qw(hook debug error template htmlpage deptype - add_depends pagespec_match pagespec_match_list bestlink +our @EXPORT = qw(hook debug error htmlpage template template_depends + deptype add_depends pagespec_match pagespec_match_list bestlink htmllink readfile writefile pagetype srcfile pagename - displaytime will_render gettext ngettext urlto targetpage + displaytime strftime_utf8 will_render gettext ngettext urlto targetpage add_underlay pagetitle titlepage linkpage newpagefile - inject add_link add_autofile + inject add_link add_autofile useragent %config %links %pagestate %wikistate %renderedfiles %pagesources %destsources %typedlinks); our $VERSION = 3.00; # plugin interface version, next is ikiwiki version @@ -119,6 +118,29 @@ sub getsetup () { safe => 0, rebuild => 0, }, + cgi_overload_delay => { + type => "string", + default => '', + example => "10", + description => "number of seconds to delay CGI requests when overloaded", + safe => 1, + rebuild => 0, + }, + cgi_overload_message => { + type => "string", + default => '', + example => "Please wait", + description => "message to display when overloaded (may contain html)", + safe => 1, + rebuild => 0, + }, + only_committed_changes => { + type => "boolean", + default => 0, + description => "enable optimization of only refreshing committed changes?", + safe => 1, + rebuild => 0, + }, rcs => { type => "string", default => '', @@ -152,18 +174,11 @@ sub getsetup () { templatedir => { type => "string", default => "$installdir/share/ikiwiki/templates", - description => "location of template files", + description => "additional directory to search for template files", advanced => 1, safe => 0, # path rebuild => 1, }, - templatedirs => { - type => "internal", - default => [], - description => "additional directories containing template files", - safe => 0, - rebuild => 0, - }, underlaydir => { type => "string", default => "$installdir/share/ikiwiki/basewiki", @@ -242,6 +257,14 @@ sub getsetup () { safe => 1, rebuild => 1, }, + html5 => { + type => "boolean", + default => 0, + description => "generate HTML5?", + advanced => 0, + safe => 1, + rebuild => 1, + }, sslcookie => { type => "boolean", default => 0, @@ -305,9 +328,9 @@ sub getsetup () { rebuild => 0, }, umask => { - type => "integer", - example => "022", - description => "force ikiwiki to use a particular umask", + type => "string", + example => "public", + description => "force ikiwiki to use a particular umask (keywords public, group or private, or a number)", advanced => 1, safe => 0, # paranoia rebuild => 0, @@ -336,6 +359,14 @@ sub getsetup () { safe => 0, # paranoia rebuild => 0, }, + timezone => { + type => "string", + default => "", + example => "US/Eastern", + description => "time zone name", + safe => 1, + rebuild => 1, + }, include => { type => "string", default => undef, @@ -441,6 +472,13 @@ sub getsetup () { safe => 0, rebuild => 0, }, + wrapper_background_command => { + type => "internal", + default => '', + description => "background shell command to run", + safe => 0, + rebuild => 0, + }, gettime => { type => "internal", description => "running in gettime mode", @@ -470,7 +508,7 @@ sub getsetup () { }, setuptype => { type => "internal", - default => "Standard", + default => "Yaml", description => "perl class to use to dump setup file", safe => 0, rebuild => 0, @@ -482,6 +520,13 @@ sub getsetup () { safe => 0, rebuild => 0, }, + cookiejar => { + type => "string", + default => { file => "$ENV{HOME}/.ikiwiki/cookies" }, + description => "cookie control", + safe => 0, # hooks into perl module internals + rebuild => 0, + }, } sub defaultconfig () { @@ -490,10 +535,15 @@ sub defaultconfig () { foreach my $key (keys %s) { push @ret, $key, $s{$key}->{default}; } - use Data::Dumper; return @ret; } +# URL to top of wiki as a path starting with /, valid from any wiki page or +# the CGI; if that's not possible, an absolute URL. Either way, it ends with / +my $local_url; +# URL to CGI script, similar to $local_url +my $local_cgiurl; + sub checkconfig () { # locale stuff; avoid LC_ALL since it overrides everything if (defined $ENV{LC_ALL}) { @@ -516,6 +566,12 @@ sub checkconfig () { $ENV{$val}=$config{ENV}{$val}; } } + if (defined $config{timezone} && length $config{timezone}) { + $ENV{TZ}=$config{timezone}; + } + else { + $config{timezone}=$ENV{TZ}; + } if ($config{w3mmode}) { eval q{use Cwd q{abs_path}}; @@ -530,12 +586,54 @@ sub checkconfig () { if ($config{cgi} && ! length $config{url}) { error(gettext("Must specify url to wiki with --url when using --cgi")); } - + + if (defined $config{url} && length $config{url}) { + eval q{use URI}; + my $baseurl = URI->new($config{url}); + + $local_url = $baseurl->path . "/"; + $local_cgiurl = undef; + + if (length $config{cgiurl}) { + my $cgiurl = URI->new($config{cgiurl}); + + $local_cgiurl = $cgiurl->path; + + if ($cgiurl->scheme ne $baseurl->scheme or + $cgiurl->authority ne $baseurl->authority) { + # too far apart, fall back to absolute URLs + $local_url = "$config{url}/"; + $local_cgiurl = $config{cgiurl}; + } + } + + $local_url =~ s{//$}{/}; + } + else { + $local_cgiurl = $config{cgiurl}; + } + $config{wikistatedir}="$config{srcdir}/.ikiwiki" unless exists $config{wikistatedir} && defined $config{wikistatedir}; if (defined $config{umask}) { - umask(possibly_foolish_untaint($config{umask})); + my $u = possibly_foolish_untaint($config{umask}); + + if ($u =~ m/^\d+$/) { + umask($u); + } + elsif ($u eq 'private') { + umask(077); + } + elsif ($u eq 'group') { + umask(027); + } + elsif ($u eq 'public') { + umask(022); + } + else { + error(sprintf(gettext("unsupported umask setting %s"), $u)); + } } run_hooks(checkconfig => sub { shift->() }); @@ -592,10 +690,11 @@ sub loadplugins () { return 1; } -sub loadplugin ($) { +sub loadplugin ($;$) { my $plugin=shift; + my $force=shift; - return if grep { $_ eq $plugin} @{$config{disable_plugins}}; + return if ! $force && grep { $_ eq $plugin} @{$config{disable_plugins}}; foreach my $dir (defined $config{libdir} ? possibly_foolish_untaint($config{libdir}) : undef, "$installdir/lib/ikiwiki") { @@ -709,7 +808,7 @@ sub pagename ($) { my $type=pagetype($file); my $page=$file; - $page=~s/\Q.$type\E*$// + $page=~s/\Q.$type\E*$// if defined $type && !$hooks{htmlize}{$type}{keepextension} && !$hooks{htmlize}{$type}{noextension}; if ($config{indexpages} && $page=~/(.*)\/index$/) { @@ -770,17 +869,23 @@ sub srcfile ($;$) { return (srcfile_stat(@_))[0]; } -sub add_underlay ($) { +sub add_literal_underlay ($) { my $dir=shift; - if ($dir !~ /^\//) { - $dir="$config{underlaydirbase}/$dir"; - } - if (! grep { $_ eq $dir } @{$config{underlaydirs}}) { unshift @{$config{underlaydirs}}, $dir; } +} + +sub add_underlay ($) { + my $dir = shift; + if ($dir !~ /^\//) { + $dir="$config{underlaydirbase}/$dir"; + } + + add_literal_underlay($dir); + # why does it return 1? we just don't know return 1; } @@ -815,6 +920,17 @@ sub prep_writefile ($$) { if (-l "$destdir/$test") { error("cannot write to a symlink ($test)"); } + if (-f _ && $test ne $file) { + # Remove conflicting file. + foreach my $p (keys %renderedfiles, keys %oldrenderedfiles) { + foreach my $f (@{$renderedfiles{$p}}, @{$oldrenderedfiles{$p}}) { + if ($f eq $test) { + unlink("$destdir/$test"); + last; + } + } + } + } $test=dirname($test); } @@ -868,10 +984,36 @@ sub will_render ($$;$) { my $dest=shift; my $clear=shift; - # Important security check. + # Important security check for independently created files. if (-e "$config{destdir}/$dest" && ! $config{rebuild} && ! grep { $_ eq $dest } (@{$renderedfiles{$page}}, @{$oldrenderedfiles{$page}}, @{$wikistate{editpage}{previews}})) { - error("$config{destdir}/$dest independently created, not overwriting with version from $page"); + my $from_other_page=0; + # Expensive, but rarely runs. + foreach my $p (keys %renderedfiles, keys %oldrenderedfiles) { + if (grep { + $_ eq $dest || + dirname($_) eq $dest + } @{$renderedfiles{$p}}, @{$oldrenderedfiles{$p}}) { + $from_other_page=1; + last; + } + } + + error("$config{destdir}/$dest independently created, not overwriting with version from $page") + unless $from_other_page; + } + + # If $dest exists as a directory, remove conflicting files in it + # rendered from other pages. + if (-d _) { + foreach my $p (keys %renderedfiles, keys %oldrenderedfiles) { + foreach my $f (@{$renderedfiles{$p}}, @{$oldrenderedfiles{$p}}) { + if (dirname($f) eq $dest) { + unlink("$config{destdir}/$f"); + rmdir(dirname("$config{destdir}/$f")); + } + } + } } if (! $clear || $cleared{$page}) { @@ -930,7 +1072,7 @@ sub bestlink ($$) { sub isinlinableimage ($) { my $file=shift; - return $file =~ /\.(png|gif|jpg|jpeg)$/i; + return $file =~ /\.(png|gif|jpg|jpeg|svg)$/i; } sub pagetitle ($;$) { @@ -965,19 +1107,30 @@ sub linkpage ($) { sub cgiurl (@) { my %params=@_; - my $cgiurl=$config{cgiurl}; + my $cgiurl=$local_cgiurl; + if (exists $params{cgiurl}) { $cgiurl=$params{cgiurl}; delete $params{cgiurl}; } + + unless (%params) { + return $cgiurl; + } + return $cgiurl."?". join("&", map $_."=".uri_escape_utf8($params{$_}), keys %params); } +sub cgiurl_abs (@) { + eval q{use URI}; + URI->new_abs(cgiurl(@_), $config{cgiurl}); +} + sub baseurl (;$) { my $page=shift; - return "$config{url}/" if ! defined $page; + return $local_url if ! defined $page; $page=htmlpage($page); $page=~s/[^\/]+$//; @@ -985,6 +1138,16 @@ sub baseurl (;$) { return $page; } +sub urlabs ($$) { + my $url=shift; + my $urlbase=shift; + + return $url unless defined $urlbase && length $urlbase; + + eval q{use URI}; + URI->new_abs($url, $urlbase)->as_string; +} + sub abs2rel ($$) { # Work around very innefficient behavior in File::Spec if abs2rel # is passed two relative paths. It's much faster if paths are @@ -998,10 +1161,18 @@ sub abs2rel ($$) { return $ret; } -sub displaytime ($;$) { +sub displaytime ($;$$) { # Plugins can override this function to mark up the time to # display. - return ''.formattime(@_).''; + my $time=formattime($_[0], $_[1]); + if ($config{html5}) { + return ''; + } + else { + return ''.$time.''; + } } sub formattime ($;$) { @@ -1012,9 +1183,29 @@ sub formattime ($;$) { $format=$config{timeformat}; } + return strftime_utf8($format, localtime($time)); +} + +my $strftime_encoding; +sub strftime_utf8 { # strftime doesn't know about encodings, so make sure - # its output is properly treated as utf8 - return decode_utf8(POSIX::strftime($format, localtime($time))); + # its output is properly treated as utf8. + # Note that this does not handle utf-8 in the format string. + ($strftime_encoding) = POSIX::setlocale(&POSIX::LC_TIME) =~ m#\.([^@]+)# + unless defined $strftime_encoding; + $strftime_encoding + ? Encode::decode($strftime_encoding, POSIX::strftime(@_)) + : POSIX::strftime(@_); +} + +sub date_3339 ($) { + my $time=shift; + + my $lc_time=POSIX::setlocale(&POSIX::LC_TIME); + POSIX::setlocale(&POSIX::LC_TIME, "C"); + my $ret=POSIX::strftime("%Y-%m-%dT%H:%M:%SZ", gmtime($time)); + POSIX::setlocale(&POSIX::LC_TIME, $lc_time); + return $ret; } sub beautify_urlpath ($) { @@ -1033,13 +1224,13 @@ sub beautify_urlpath ($) { 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}"); + $to = 'index'; } if (! $destsources{$to}) { @@ -1050,11 +1241,26 @@ sub urlto ($$;$) { return $config{url}.beautify_urlpath("/".$to); } + if (! defined $from) { + my $u = $local_url || ''; + $u =~ s{/$}{}; + return $u.beautify_urlpath("/".$to); + } + my $link = abs2rel($to, dirname(htmlpage($from))); return beautify_urlpath($link); } +sub isselflink ($$) { + # Plugins can override this function to support special types + # of selflinks. + my $page=shift; + my $link=shift; + + return $page eq $link; +} + sub htmllink ($$$;@) { my $lpage=shift; # the page doing the linking my $page=shift; # the page that will contain the link (different for inline) @@ -1080,7 +1286,7 @@ sub htmllink ($$$;@) { } return "$linktext" - if length $bestlink && $page eq $bestlink && + if length $bestlink && isselflink($page, $bestlink) && ! defined $opts{anchor}; if (! $destsources{$bestlink}) { @@ -1092,7 +1298,7 @@ sub htmllink ($$$;@) { $cgilink = " "create", - page => lc($link), + page => $link, from => $lpage )."\" rel=\"nofollow\">?"; } @@ -1129,7 +1335,7 @@ sub userpage ($) { sub openiduser ($) { my $user=shift; - if ($user =~ m!^https?://! && + if (defined $user && $user =~ m!^https?://! && eval q{use Net::OpenID::VerifiedIdentity; 1} && !$@) { my $display; @@ -1241,16 +1447,21 @@ sub preprocess ($$$;$$) { # consider it significant. my @params; while ($params =~ m{ - (?:([-\w]+)=)? # 1: named parameter key? + (?:([-.\w]+)=)? # 1: named parameter key? (?: """(.*?)""" # 2: triple-quoted value | "([^"]*?)" # 3: single-quoted value | - (\S+) # 4: unquoted value + '''(.*?)''' # 4: triple-single-quote + | + <<([a-zA-Z]+)\n # 5: heredoc start + (.*?)\n\5 # 6: heredoc value + | + (\S+) # 7: unquoted value ) (?:\s+|$) # delimiter to next param - }sgx) { + }msgx) { my $key=$1; my $val; if (defined $2) { @@ -1265,6 +1476,12 @@ sub preprocess ($$$;$$) { elsif (defined $4) { $val=$4; } + elsif (defined $7) { + $val=$7; + } + elsif (defined $6) { + $val=$6; + } if (defined $key) { push @params, $key, $val; @@ -1327,12 +1544,17 @@ sub preprocess ($$$;$$) { ( # 4: the parameters.. \s+ # Must have space if parameters present (?: - (?:[-\w]+=)? # named parameter key? + (?:[-.\w]+=)? # named parameter key? (?: """.*?""" # triple-quoted value | "[^"]*?" # single-quoted value | + '''.*?''' # triple-single-quote + | + <<([a-zA-Z]+)\n # 5: heredoc start + (?:.*?)\n\5 # heredoc value + | [^"\s\]]+ # unquoted value ) \s* # whitespace or end @@ -1350,12 +1572,17 @@ sub preprocess ($$$;$$) { \s+ ( # 4: the parameters.. (?: - (?:[-\w]+=)? # named parameter key? + (?:[-.\w]+=)? # named parameter key? (?: """.*?""" # triple-quoted value | "[^"]*?" # single-quoted value | + '''.*?''' # triple-single-quote + | + <<([a-zA-Z]+)\n # 5: heredoc start + (?:.*?)\n\5 # heredoc value + | [^"\s\]]+ # unquoted value ) \s* # whitespace or end @@ -1383,10 +1610,6 @@ sub filter ($$$) { return $content; } -sub indexlink () { - return "$config{wikiname}"; -} - sub check_canedit ($$$;$) { my $page=shift; my $q=shift; @@ -1451,6 +1674,69 @@ sub check_content (@) { return defined $ok ? $ok : 1; } +sub check_canchange (@) { + my %params = @_; + my $cgi = $params{cgi}; + my $session = $params{session}; + my @changes = @{$params{changes}}; + + my %newfiles; + foreach my $change (@changes) { + # This untaint is safe because we check file_pruned and + # wiki_file_regexp. + my ($file)=$change->{file}=~/$config{wiki_file_regexp}/; + $file=possibly_foolish_untaint($file); + if (! defined $file || ! length $file || + file_pruned($file)) { + error(gettext("bad file name %s"), $file); + } + + my $type=pagetype($file); + my $page=pagename($file) if defined $type; + + if ($change->{action} eq 'add') { + $newfiles{$file}=1; + } + + if ($change->{action} eq 'change' || + $change->{action} eq 'add') { + if (defined $page) { + check_canedit($page, $cgi, $session); + next; + } + else { + if (IkiWiki::Plugin::attachment->can("check_canattach")) { + IkiWiki::Plugin::attachment::check_canattach($session, $file, $change->{path}); + check_canedit($file, $cgi, $session); + next; + } + } + } + elsif ($change->{action} eq 'remove') { + # check_canremove tests to see if the file is present + # on disk. This will fail when a single commit adds a + # file and then removes it again. Avoid the problem + # by not testing the removal in such pairs of changes. + # (The add is still tested, just to make sure that + # no data is added to the repo that a web edit + # could not add.) + next if $newfiles{$file}; + + if (IkiWiki::Plugin::remove->can("check_canremove")) { + IkiWiki::Plugin::remove::check_canremove(defined $page ? $page : $file, $cgi, $session); + check_canedit(defined $page ? $page : $file, $cgi, $session); + next; + } + } + else { + error "unknown action ".$change->{action}; + } + + error sprintf(gettext("you are not allowed to change %s"), $file); + } +} + + my $wikilock; sub lockwiki () { @@ -1502,7 +1788,8 @@ sub enable_commit_hook () { sub loadindex () { %oldrenderedfiles=%pagectime=(); - if (! $config{rebuild}) { + my $rebuild=$config{rebuild}; + if (! $rebuild) { %pagesources=%pagemtime=%oldlinks=%links=%depends= %destsources=%renderedfiles=%pagecase=%pagestate= %depends_simple=%typedlinks=%oldtypedlinks=(); @@ -1528,6 +1815,12 @@ sub loadindex () { if (exists $index->{version} && ! ref $index->{version}) { $pages=$index->{page}; %wikistate=%{$index->{state}}; + # Handle plugins that got disabled by loading a new setup. + if (exists $config{setupfile}) { + require IkiWiki::Setup; + IkiWiki::Setup::disabled_plugins( + grep { ! $loaded_plugins{$_} } keys %wikistate); + } } else { $pages=$index; @@ -1536,10 +1829,16 @@ sub loadindex () { foreach my $src (keys %$pages) { my $d=$pages->{$src}; - my $page=pagename($src); + my $page; + if (exists $d->{page} && ! $rebuild) { + $page=$d->{page}; + } + else { + $page=pagename($src); + } $pagectime{$page}=$d->{ctime}; - if (! $config{rebuild}) { - $pagesources{$page}=$src; + $pagesources{$page}=$src; + if (! $rebuild) { $pagemtime{$page}=$d->{mtime}; $renderedfiles{$page}=$d->{dest}; if (exists $d->{links} && ref $d->{links}) { @@ -1589,17 +1888,15 @@ sub loadindex () { foreach my $page (keys %renderedfiles) { $destsources{$_}=$page foreach @{$renderedfiles{$page}}; } + $lastrev=$index->{lastrev}; + @underlayfiles=@{$index->{underlayfiles}} if ref $index->{underlayfiles}; return close($in); } sub saveindex () { run_hooks(savestate => sub { shift->() }); - my %hookids; - foreach my $type (keys %hooks) { - $hookids{$_}=1 foreach keys %{$hooks{$type}}; - } - my @hookids=keys %hookids; + my @plugins=keys %loaded_plugins; if (! -d $config{wikistatedir}) { mkdir($config{wikistatedir}); @@ -1614,6 +1911,7 @@ sub saveindex () { my $src=$pagesources{$page}; $index{page}{$src}={ + page => $page, ctime => $pagectime{$page}, mtime => $pagemtime{$page}, dest => $renderedfiles{$page}, @@ -1633,21 +1931,21 @@ sub saveindex () { } if (exists $pagestate{$page}) { - foreach my $id (@hookids) { - foreach my $key (keys %{$pagestate{$page}{$id}}) { - $index{page}{$src}{state}{$id}{$key}=$pagestate{$page}{$id}{$key}; - } - } + $index{page}{$src}{state}=$pagestate{$page}; } } $index{state}={}; - foreach my $id (@hookids) { + foreach my $id (@plugins) { + $index{state}{$id}={}; # used to detect disabled plugins foreach my $key (keys %{$wikistate{$id}}) { $index{state}{$id}{$key}=$wikistate{$id}{$key}; } } + $index{lastrev}=$lastrev; + $index{underlayfiles}=\@underlayfiles; + $index{version}="3"; my $ret=Storable::nstore_fd(\%index, $out); return if ! defined $ret || ! $ret; @@ -1659,58 +1957,95 @@ sub saveindex () { } sub template_file ($) { - my $template=shift; + my $name=shift; + + my $tpage=($name =~ s/^\///) ? $name : "templates/$name"; + my $template; + if ($name !~ /\.tmpl$/ && exists $pagesources{$tpage}) { + $template=srcfile($pagesources{$tpage}, 1); + $name.=".tmpl"; + } + else { + $template=srcfile($tpage, 1); + } - foreach my $dir ($config{templatedir}, @{$config{templatedirs}}, - "$installdir/share/ikiwiki/templates") { - return "$dir/$template" if -e "$dir/$template"; + if (defined $template) { + return $template, $tpage, 1 if wantarray; + return $template; + } + else { + $name=~s:/::; # avoid path traversal + foreach my $dir ($config{templatedir}, + "$installdir/share/ikiwiki/templates") { + if (-e "$dir/$name") { + $template="$dir/$name"; + last; + } + } + if (defined $template) { + return $template, $tpage if wantarray; + return $template; + } } + return; } -sub template_params (@) { - my $filename=template_file(shift); - +sub template_depends ($$;@) { + my $name=shift; + my $page=shift; + + my ($filename, $tpage, $untrusted)=template_file($name); if (! defined $filename) { - return if wantarray; - return ""; + error(sprintf(gettext("template %s not found"), $name)) } - my @ret=( + if (defined $page && defined $tpage) { + add_depends($page, $tpage); + } + + my @opts=( filter => sub { my $text_ref = shift; ${$text_ref} = decode_utf8(${$text_ref}); }, - filename => $filename, loop_context_vars => 1, die_on_bad_params => 0, - @_ + parent_global_vars => 1, + filename => $filename, + @_, + ($untrusted ? (no_includes => 1) : ()), ); - return wantarray ? @ret : {@ret}; + return @opts if wantarray; + + require HTML::Template; + return HTML::Template->new(@opts); } sub template ($;@) { - require HTML::Template; - return HTML::Template->new(template_params(@_)); + template_depends(shift, undef, @_); } -sub misctemplate ($$;@) { - my $title=shift; - my $pagebody=shift; - - my $template=template("misc.tmpl"); - $template->param( - title => $title, - indexlink => indexlink(), - wikiname => $config{wikiname}, - pagebody => $pagebody, - baseurl => baseurl(), - @_, - ); - run_hooks(pagetemplate => sub { - shift->(page => "", destpage => "", template => $template); +sub templateactions ($$) { + my $template=shift; + my $page=shift; + + my $have_actions=0; + my @actions; + run_hooks(pageactions => sub { + push @actions, map { { action => $_ } } + grep { defined } shift->(page => $page); }); - return $template->output; + $template->param(actions => \@actions); + + if ($config{cgiurl} && exists $hooks{auth}) { + $template->param(prefsurl => cgiurl(do => "prefs")); + $have_actions=1; + } + + if ($have_actions || @actions) { + $template->param(have_actions => 1); + } } sub hook (@) { @@ -1761,11 +2096,11 @@ sub rcs_prepedit ($) { $hooks{rcs}{rcs_prepedit}{call}->(@_); } -sub rcs_commit ($$$;$$) { +sub rcs_commit (@) { $hooks{rcs}{rcs_commit}{call}->(@_); } -sub rcs_commit_staged ($$$) { +sub rcs_commit_staged (@) { $hooks{rcs}{rcs_commit_staged}{call}->(@_); } @@ -1785,7 +2120,7 @@ sub rcs_recentchanges ($) { $hooks{rcs}{rcs_recentchanges}{call}->(@_); } -sub rcs_diff ($) { +sub rcs_diff ($;$) { $hooks{rcs}{rcs_diff}{call}->(@_); } @@ -1962,6 +2297,13 @@ sub add_autofile ($$$) { $autofiles{$file}{generator}=$generator; } +sub useragent () { + return LWP::UserAgent->new( + cookie_jar => $config{cookiejar}, + env_proxy => 1, # respect proxy env vars + ); +} + sub sortspec_translate ($$) { my $spec = shift; my $reverse = shift; @@ -2185,7 +2527,7 @@ sub glob2re ($) { my $re=quotemeta(shift); $re=~s/\\\*/.*/g; $re=~s/\\\?/./g; - return $re; + return qr/^$re$/i; } package IkiWiki::FailReason; @@ -2264,15 +2606,23 @@ sub derel ($$) { my $path=shift; my $from=shift; - if ($path =~ m!^\./!) { - $from=~s#/?[^/]+$## if defined $from; - $path=~s#^\./##; - $path="$from/$path" if defined $from && length $from; + if ($path =~ m!^\.(/|$)!) { + if ($1) { + $from=~s#/?[^/]+$## if defined $from; + $path=~s#^\./##; + $path="$from/$path" if defined $from && length $from; + } + else { + $path = $from; + $path = "" unless defined $path; + } } return $path; } +my %glob_cache; + sub match_glob ($$;@) { my $page=shift; my $glob=shift; @@ -2280,8 +2630,13 @@ sub match_glob ($$;@) { $glob=derel($glob, $params{location}); - my $regexp=IkiWiki::glob2re($glob); - if ($page=~/^$regexp$/i) { + # Instead of converting the glob to a regex every time, + # cache the compiled regex to save time. + my $re=$glob_cache{$glob}; + unless (defined $re) { + $glob_cache{$glob} = $re = IkiWiki::glob2re($glob); + } + if ($page =~ $re) { if (! IkiWiki::isinternal($page) || $params{internal}) { return IkiWiki::SuccessReason->new("$glob matches $page"); } @@ -2295,7 +2650,22 @@ sub match_glob ($$;@) { } sub match_internal ($$;@) { - return match_glob($_[0], $_[1], @_, internal => 1) + return match_glob(shift, shift, @_, internal => 1) +} + +sub match_page ($$;@) { + my $page=shift; + my $match=match_glob($page, shift, @_); + if ($match) { + my $source=exists $IkiWiki::pagesources{$page} ? + $IkiWiki::pagesources{$page} : + $IkiWiki::delpagesources{$page}; + my $type=defined $source ? IkiWiki::pagetype($source) : undef; + if (! defined $type) { + return IkiWiki::FailReason->new("$page is not a page"); + } + } + return $match; } sub match_link ($$;@) { @@ -2316,18 +2686,20 @@ sub match_link ($$;@) { unless $links && @{$links}; my $bestlink = IkiWiki::bestlink($from, $link); foreach my $p (@{$links}) { + next unless (! defined $linktype || exists $IkiWiki::typedlinks{$page}{$linktype}{$p}); + if (length $bestlink) { - if ((!defined $linktype || exists $IkiWiki::typedlinks{$page}{$linktype}{$p}) && $bestlink eq IkiWiki::bestlink($page, $p)) { + if ($bestlink eq IkiWiki::bestlink($page, $p)) { return IkiWiki::SuccessReason->new("$page links to $link$qualifier", $page => $IkiWiki::DEPEND_LINKS, "" => 1) } } else { - if ((!defined $linktype || exists $IkiWiki::typedlinks{$page}{$linktype}{$p}) && match_glob($p, $link, %params)) { + if (match_glob($p, $link, %params)) { return IkiWiki::SuccessReason->new("$page links to page $p$qualifier, matching $link", $page => $IkiWiki::DEPEND_LINKS, "" => 1) } my ($p_rel)=$p=~/^\/?(.*)/; $link=~s/^\///; - if ((!defined $linktype || exists $IkiWiki::typedlinks{$page}{$linktype}{$p_rel}) && match_glob($p_rel, $link, %params)) { + if (match_glob($p_rel, $link, %params)) { return IkiWiki::SuccessReason->new("$page links to page $p_rel$qualifier, matching $link", $page => $IkiWiki::DEPEND_LINKS, "" => 1) } } @@ -2336,8 +2708,14 @@ sub match_link ($$;@) { } sub match_backlink ($$;@) { - my $ret=match_link($_[1], $_[0], @_); - $ret->influences($_[1] => $IkiWiki::DEPEND_LINKS); + my $page=shift; + my $testpage=shift; + my %params=@_; + if ($testpage eq '.') { + $testpage = $params{'location'} + } + my $ret=match_link($testpage, $page, @_); + $ret->influences($testpage => $IkiWiki::DEPEND_LINKS); return $ret; } @@ -2382,7 +2760,12 @@ sub match_created_after ($$;@) { } sub match_creation_day ($$;@) { - if ((gmtime($IkiWiki::pagectime{shift()}))[3] == shift) { + my $page=shift; + my $d=shift; + if ($d !~ /^\d+$/) { + return IkiWiki::ErrorReason->new("invalid day $d"); + } + if ((localtime($IkiWiki::pagectime{$page}))[3] == $d) { return IkiWiki::SuccessReason->new('creation_day matched'); } else { @@ -2391,7 +2774,12 @@ sub match_creation_day ($$;@) { } sub match_creation_month ($$;@) { - if ((gmtime($IkiWiki::pagectime{shift()}))[4] + 1 == shift) { + my $page=shift; + my $m=shift; + if ($m !~ /^\d+$/) { + return IkiWiki::ErrorReason->new("invalid month $m"); + } + if ((localtime($IkiWiki::pagectime{$page}))[4] + 1 == $m) { return IkiWiki::SuccessReason->new('creation_month matched'); } else { @@ -2400,7 +2788,12 @@ sub match_creation_month ($$;@) { } sub match_creation_year ($$;@) { - if ((gmtime($IkiWiki::pagectime{shift()}))[5] + 1900 == shift) { + my $page=shift; + my $y=shift; + if ($y !~ /^\d+$/) { + return IkiWiki::ErrorReason->new("invalid year $y"); + } + if ((localtime($IkiWiki::pagectime{$page}))[5] + 1900 == $y) { return IkiWiki::SuccessReason->new('creation_year matched'); } else { @@ -2413,13 +2806,13 @@ sub match_user ($$;@) { my $user=shift; my %params=@_; - my $regexp=IkiWiki::glob2re($user); - if (! exists $params{user}) { return IkiWiki::ErrorReason->new("no user specified"); } - if (defined $params{user} && $params{user}=~/^$regexp$/i) { + my $regexp=IkiWiki::glob2re($user); + + if (defined $params{user} && $params{user}=~$regexp) { return IkiWiki::SuccessReason->new("user is $user"); } elsif (! defined $params{user}) { @@ -2458,8 +2851,10 @@ sub match_ip ($$;@) { if (! exists $params{ip}) { return IkiWiki::ErrorReason->new("no IP specified"); } + + my $regexp=IkiWiki::glob2re(lc $ip); - if (defined $params{ip} && lc $params{ip} eq lc $ip) { + if (defined $params{ip} && lc $params{ip}=~$regexp) { return IkiWiki::SuccessReason->new("IP is $ip"); } else { @@ -2482,6 +2877,7 @@ sub cmp_title { IkiWiki::pagetitle(IkiWiki::basename($b)) } +sub cmp_path { IkiWiki::pagetitle($a) cmp IkiWiki::pagetitle($b) } sub cmp_mtime { $IkiWiki::pagemtime{$b} <=> $IkiWiki::pagemtime{$a} } sub cmp_age { $IkiWiki::pagectime{$b} <=> $IkiWiki::pagectime{$a} }