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
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,
safe => 0, # path
rebuild => 1,
},
- adminuser => {
- type => "string",
- default => [],
- description => "user names of wiki admins",
- safe => 1,
- rebuild => 0,
- },
- adminemail => {
- type => "string",
- default => undef,
- example => 'me@example.com',
- description => "contact email for wiki",
- safe => 1,
- rebuild => 0,
- },
url => {
type => "string",
default => '',
type => "string",
default => '',
example => "/var/www/wiki/ikiwiki.cgi",
- description => "cgi executable to generate",
+ description => "cgi wrapper to generate",
safe => 0, # file
rebuild => 0,
},
},
default_plugins => {
type => "internal",
- default => [qw{mdwn link inline htmlscrubber passwordauth
+ default => [qw{mdwn link inline meta htmlscrubber passwordauth
openid signinedit lockedit conditional
- recentchanges parentlinks}],
+ recentchanges parentlinks editpage}],
description => "plugins to enable by default",
- safe => 1,
+ safe => 0,
rebuild => 1,
},
add_plugins => {
type => "string",
default => "$installdir/share/ikiwiki/templates",
description => "location of template files",
+ advanced => 1,
safe => 0, # path
rebuild => 1,
},
type => "string",
default => "$installdir/share/ikiwiki/basewiki",
description => "base wiki source location",
+ advanced => 1,
safe => 0, # path
rebuild => 0,
},
},
verbose => {
type => "boolean",
- default => 0,
+ example => 1,
description => "display verbose messages when building?",
safe => 1,
rebuild => 0,
},
syslog => {
type => "boolean",
- default => 0,
+ example => 1,
description => "log to syslog?",
safe => 1,
rebuild => 0,
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,
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",
type => "string",
default => '%c',
description => "strftime format string to display date",
+ advanced => 1,
safe => 1,
rebuild => 1,
},
default => undef,
example => "en_US.UTF-8",
description => "UTF-8 locale to use",
+ advanced => 1,
safe => 0,
rebuild => 1,
},
- sslcookie => {
- type => "boolean",
- default => 0,
- description => "only send cookies over SSL connections?",
- safe => 1,
- rebuild => 0,
- },
userdir => {
type => "string",
default => "",
type => "boolean",
default => 0,
description => "attempt to hardlink source files? (optimisation for large files)",
+ advanced => 1,
safe => 0, # paranoia
rebuild => 0,
},
description => "",
example => "022",
description => "force ikiwiki to use a particular umask",
+ advanced => 1,
safe => 0, # paranoia
rebuild => 0,
},
default => "",
example => "$ENV{HOME}/.ikiwiki/",
description => "extra library and plugin directory",
+ advanced => 1,
safe => 0, # directory
rebuild => 0,
},
default => undef,
example => '\.wav$',
description => "regexp of source files to ignore",
+ advanced => 1,
safe => 0, # regexp
rebuild => 1,
},
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",
- default => qr/(^[-[:alnum:]_.:\/+]+$)/,
description => "regexp of legal source files",
safe => 0,
rebuild => 1,
safe => 0,
rebuild => 0,
},
+ setup => {
+ type => "internal",
+ default => undef,
+ description => "running in setup mode",
+ safe => 0,
+ rebuild => 0,
+ },
refresh => {
type => "internal",
default => 0,
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,
safe => 0,
rebuild => 0,
},
- setup => {
+ wikistatedir => {
+ type => "internal",
+ default => undef,
+ description => "path to the .ikiwiki directory holding ikiwiki state",
+ safe => 0,
+ rebuild => 0,
+ },
+ setupfile => {
type => "internal",
default => undef,
- description => "setup file to read",
+ 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,
},
$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}}) {
}
$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 (defined $config{umask}) {
umask(possibly_foolish_untaint($config{umask}));
my %ret;
foreach my $dir (@INC, $config{libdir}) {
- next unless defined $dir;
+ 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;
+ next unless defined $dir && length $dir;
foreach my $file (glob("$dir/plugins/*")) {
$ret{basename($file)}=1 if -x $file;
}
} #}}}
sub loadplugins () { #{{{
- if (defined $config{libdir}) {
+ if (defined $config{libdir} && length $config{libdir}) {
unshift @INC, possibly_foolish_untaint($config{libdir});
}
- loadplugin($_) foreach @{$config{default_plugins}}, @{$config{add_plugins}};
+ 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) {
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;
}
}
if ($@) {
error("Failed to load plugin $mod: $@");
}
+ $loaded_plugins{$plugin}=1;
return 1;
} #}}}
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;
}
} #}}}
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;
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;
} #}}}
# 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");
}
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);
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;
} #}}}
} #}}}
sub displaytime ($;$) { #{{{
+ # Plugins can override this function to mark up the time to
+ # display.
+ return '<span class="date">'.formattime(@_).'</span>';
+} #}}}
+
+sub formattime ($;$) { #{{{
+ # Plugins can override this function to format the time.
my $time=shift;
my $format=shift;
if (! defined $format) {
my $prefix=shift;
my $command=shift;
my $params=shift;
+ $params="" if ! defined $params;
+
if (length $escape) {
return "[[$prefix$command $params]]";
}
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}) {
}
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;
} #}}}
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;
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},
};
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);
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);
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;
my $sub=pagespec_translate($spec);
return ! $@;
} #}}}
-
+
sub glob2re ($) { #{{{
my $re=quotemeta(shift);
$re=~s/\\\*/.*/g;
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");
}
} #}}}
+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