X-Git-Url: https://sipb.mit.edu/gitweb.cgi/ikiwiki.git/blobdiff_plain/39d4bfde6b9c49f0a8b13699be701bda11eb851a..d3a9cb4b126571f6342c3747df40ffd0b8f078b0:/IkiWiki.pm diff --git a/IkiWiki.pm b/IkiWiki.pm index 7382f11e4..0570c8bc5 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,15 +12,15 @@ 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}; use Exporter q{import}; 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 %config %links %pagestate %wikistate %renderedfiles @@ -119,6 +118,22 @@ 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, + }, rcs => { type => "string", default => '', @@ -152,7 +167,7 @@ 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, @@ -235,6 +250,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, @@ -298,9 +321,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, @@ -329,6 +352,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, @@ -434,6 +465,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", @@ -463,7 +501,7 @@ sub getsetup () { }, setuptype => { type => "internal", - default => "Standard", + default => "Yaml", description => "perl class to use to dump setup file", safe => 0, rebuild => 0, @@ -483,10 +521,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}) { @@ -509,6 +552,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}}; @@ -523,12 +572,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->() }); @@ -585,10 +676,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") { @@ -702,7 +794,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$/) { @@ -763,17 +855,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; } @@ -808,6 +906,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); } @@ -861,10 +970,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}) { @@ -923,7 +1058,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 ($;$) { @@ -958,19 +1093,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/[^\/]+$//; @@ -978,6 +1124,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 @@ -991,10 +1147,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 ($;$) { @@ -1005,9 +1169,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 ($) { @@ -1026,13 +1210,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}) { @@ -1043,11 +1227,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) @@ -1073,7 +1272,7 @@ sub htmllink ($$$;@) { } return "$linktext" - if length $bestlink && $page eq $bestlink && + if length $bestlink && isselflink($page, $bestlink) && ! defined $opts{anchor}; if (! $destsources{$bestlink}) { @@ -1085,7 +1284,7 @@ sub htmllink ($$$;@) { $cgilink = " "create", - page => lc($link), + page => $link, from => $lpage )."\" rel=\"nofollow\">?"; } @@ -1122,7 +1321,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; @@ -1234,16 +1433,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) { @@ -1258,6 +1462,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; @@ -1320,12 +1530,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 @@ -1343,12 +1558,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 @@ -1376,10 +1596,6 @@ sub filter ($$$) { return $content; } -sub indexlink () { - return "$config{wikiname}"; -} - sub check_canedit ($$$;$) { my $page=shift; my $q=shift; @@ -1444,6 +1660,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 () { @@ -1521,6 +1800,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; @@ -1531,8 +1816,8 @@ sub loadindex () { my $d=$pages->{$src}; my $page=pagename($src); $pagectime{$page}=$d->{ctime}; + $pagesources{$page}=$src; if (! $config{rebuild}) { - $pagesources{$page}=$src; $pagemtime{$page}=$d->{mtime}; $renderedfiles{$page}=$d->{dest}; if (exists $d->{links} && ref $d->{links}) { @@ -1588,11 +1873,7 @@ sub loadindex () { 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}); @@ -1626,7 +1907,7 @@ sub saveindex () { } if (exists $pagestate{$page}) { - foreach my $id (@hookids) { + foreach my $id (@plugins) { foreach my $key (keys %{$pagestate{$page}{$id}}) { $index{page}{$src}{state}{$id}{$key}=$pagestate{$page}{$id}{$key}; } @@ -1635,7 +1916,8 @@ sub saveindex () { } $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}; } @@ -1654,14 +1936,21 @@ sub saveindex () { sub template_file ($) { my $name=shift; - my $tpage=($name =~ /^\//) ? $name : "templates/$name"; + my $tpage=($name =~ s/^\///) ? $name : "templates/$name"; + my $template; if ($name !~ /\.tmpl$/ && exists $pagesources{$tpage}) { - $tpage=$pagesources{$tpage}; + $template=srcfile($pagesources{$tpage}, 1); $name.=".tmpl"; } + else { + $template=srcfile($tpage, 1); + } - my $template=srcfile($tpage, 1); - if (! defined $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") { @@ -1670,12 +1959,12 @@ sub template_file ($) { last; } } + if (defined $template) { + return $template, $tpage if wantarray; + return $template; + } } - if (defined $template) { - return $template, $tpage if wantarray; - return $template; - } return; } @@ -1683,47 +1972,57 @@ sub template_depends ($$;@) { my $name=shift; my $page=shift; - my ($filename, $tpage)=template_file($name); + my ($filename, $tpage, $untrusted)=template_file($name); + if (! defined $filename) { + error(sprintf(gettext("template %s not found"), $name)) + } + if (defined $page && defined $tpage) { add_depends($page, $tpage); } - - return unless defined $filename; - - require HTML::Template; - return HTML::Template->new( + + my @opts=( filter => sub { my $text_ref = shift; ${$text_ref} = decode_utf8(${$text_ref}); }, loop_context_vars => 1, die_on_bad_params => 0, + parent_global_vars => 1, filename => $filename, @_, - no_includes => 1, + ($untrusted ? (no_includes => 1) : ()), ); + return @opts if wantarray; + + require HTML::Template; + return HTML::Template->new(@opts); } sub template ($;@) { template_depends(shift, undef, @_); } -sub misctemplate ($$;@) { - my $title=shift; - my $pagebody=shift; - - my $template=template("misc.tmpl", - 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 (@) { @@ -1774,11 +2073,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}->(@_); } @@ -1798,7 +2097,7 @@ sub rcs_recentchanges ($) { $hooks{rcs}{rcs_recentchanges}{call}->(@_); } -sub rcs_diff ($) { +sub rcs_diff ($;$) { $hooks{rcs}{rcs_diff}{call}->(@_); } @@ -2198,7 +2497,7 @@ sub glob2re ($) { my $re=quotemeta(shift); $re=~s/\\\*/.*/g; $re=~s/\\\?/./g; - return $re; + return qr/^$re$/i; } package IkiWiki::FailReason; @@ -2277,15 +2576,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; @@ -2293,8 +2600,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"); } @@ -2308,7 +2620,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 ($$;@) { @@ -2329,18 +2656,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) } } @@ -2349,8 +2678,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; } @@ -2395,7 +2730,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 { @@ -2404,7 +2744,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 { @@ -2413,7 +2758,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 { @@ -2432,7 +2782,7 @@ sub match_user ($$;@) { return IkiWiki::ErrorReason->new("no user specified"); } - if (defined $params{user} && $params{user}=~/^$regexp$/i) { + if (defined $params{user} && $params{user}=~$regexp) { return IkiWiki::SuccessReason->new("user is $user"); } elsif (! defined $params{user}) { @@ -2495,6 +2845,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} }