X-Git-Url: http://git.vanrenterghem.biz/git.ikiwiki.info.git/blobdiff_plain/8a56df576a9d7d184a5233bcde8ea07eee86dd60..bcc209eb5acd26e3751c5869cd5c1ebe66b0bf03:/IkiWiki.pm?ds=sidebyside
diff --git a/IkiWiki.pm b/IkiWiki.pm
index 7382f11e4..e5da04a3b 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 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,7 +174,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 +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,
@@ -298,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,
@@ -329,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,
@@ -434,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",
@@ -463,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,
@@ -475,6 +520,21 @@ 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,
+ },
+ useragent => {
+ type => "string",
+ default => undef,
+ example => "Wget/1.13.4 (linux-gnu)",
+ description => "set custom user agent string for outbound HTTP requests e.g. when fetching aggregated RSS feeds",
+ safe => 0,
+ rebuild => 0,
+ },
}
sub defaultconfig () {
@@ -483,10 +543,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 +574,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 +594,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 +698,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") {
@@ -629,6 +743,7 @@ sub debug ($) {
}
my $log_open=0;
+my $log_failed=0;
sub log_message ($$) {
my $type=shift;
@@ -639,9 +754,18 @@ sub log_message ($$) {
Sys::Syslog::openlog('ikiwiki', '', 'user');
$log_open=1;
}
- return eval {
- Sys::Syslog::syslog($type, "[$config{wikiname}] %s", join(" ", @_));
+ eval {
+ # keep a copy to avoid editing the original config repeatedly
+ my $wikiname = $config{wikiname};
+ utf8::encode($wikiname);
+ Sys::Syslog::syslog($type, "[$wikiname] %s", join(" ", @_));
};
+ if ($@) {
+ print STDERR "failed to syslog: $@" unless $log_failed;
+ $log_failed=1;
+ print STDERR "@_\n";
+ }
+ return $@;
}
elsif (! $config{cgi}) {
return print "@_\n";
@@ -702,7 +826,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 +887,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 +938,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 +1002,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 +1090,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 +1125,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 +1156,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 +1179,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 +1201,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 +1242,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 +1259,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 +1304,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 +1316,7 @@ sub htmllink ($$$;@) {
$cgilink = " "create",
- page => lc($link),
+ page => $link,
from => $lpage
)."\" rel=\"nofollow\">?";
}
@@ -1122,7 +1353,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 +1465,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 +1494,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;
@@ -1266,7 +1508,7 @@ sub preprocess ($$$;$$) {
push @params, $val, '';
}
}
- if ($preprocessing{$page}++ > 3) {
+ if ($preprocessing{$page}++ > 8) {
# Avoid loops of preprocessed pages preprocessing
# other pages that preprocess them, etc.
return "[[!$command ".
@@ -1320,12 +1562,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 +1590,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 +1628,6 @@ sub filter ($$$) {
return $content;
}
-sub indexlink () {
- return "$config{wikiname}";
-}
-
sub check_canedit ($$$;$) {
my $page=shift;
my $q=shift;
@@ -1444,6 +1692,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 () {
@@ -1495,7 +1806,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=();
@@ -1521,6 +1833,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;
@@ -1529,10 +1847,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}) {
@@ -1582,17 +1906,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});
@@ -1607,6 +1929,7 @@ sub saveindex () {
my $src=$pagesources{$page};
$index{page}{$src}={
+ page => $page,
ctime => $pagectime{$page},
mtime => $pagemtime{$page},
dest => $renderedfiles{$page},
@@ -1626,21 +1949,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;
@@ -1654,14 +1977,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 +2000,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 +2013,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 +2114,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 +2138,7 @@ sub rcs_recentchanges ($) {
$hooks{rcs}{rcs_recentchanges}{call}->(@_);
}
-sub rcs_diff ($) {
+sub rcs_diff ($;$) {
$hooks{rcs}{rcs_diff}{call}->(@_);
}
@@ -1975,6 +2315,14 @@ sub add_autofile ($$$) {
$autofiles{$file}{generator}=$generator;
}
+sub useragent () {
+ return LWP::UserAgent->new(
+ cookie_jar => $config{cookiejar},
+ env_proxy => 1, # respect proxy env vars
+ agent => $config{useragent},
+ );
+}
+
sub sortspec_translate ($$) {
my $spec = shift;
my $reverse = shift;
@@ -2198,7 +2546,7 @@ sub glob2re ($) {
my $re=quotemeta(shift);
$re=~s/\\\*/.*/g;
$re=~s/\\\?/./g;
- return $re;
+ return qr/^$re$/i;
}
package IkiWiki::FailReason;
@@ -2277,15 +2625,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 +2649,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 +2669,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 +2705,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 +2727,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 +2779,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 +2793,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 +2807,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 {
@@ -2426,13 +2825,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}) {
@@ -2471,8 +2870,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 {
@@ -2495,6 +2896,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} }