Re: ikiwiki / CVE-2019-9187
Chris Lamb <lamby@debian.org> writes:
> It appears you generated your debdiff "backwards" - just in case this
> is part of larger problem, could you re-post it? Thanks.
As attached.
--
Brian May <bam@debian.org>
diff -Nru ikiwiki-3.20141016.4/CHANGELOG ikiwiki-3.20141016.4+deb8u1/CHANGELOG
--- ikiwiki-3.20141016.4/CHANGELOG 2017-01-12 05:18:52.000000000 +1100
+++ ikiwiki-3.20141016.4+deb8u1/CHANGELOG 2019-03-07 17:35:55.000000000 +1100
@@ -1,3 +1,10 @@
+ikiwiki (3.20141016.4+deb8u1) jessie-security; urgency=high
+
+ * Non-maintainer upload by the LTS Team.
+ * CVE-2019-9187: Fix server-side request forgery via aggregate plugin.
+
+ -- Brian May <bam@debian.org> Thu, 07 Mar 2019 17:35:55 +1100
+
ikiwiki (3.20141016.4) jessie-security; urgency=high
* Reference CVE-2016-4561 in 3.20141016.3 changelog
diff -Nru ikiwiki-3.20141016.4/CVE-2019-9187-1.patch ikiwiki-3.20141016.4+deb8u1/CVE-2019-9187-1.patch
--- ikiwiki-3.20141016.4/CVE-2019-9187-1.patch 1970-01-01 10:00:00.000000000 +1000
+++ ikiwiki-3.20141016.4+deb8u1/CVE-2019-9187-1.patch 2019-03-07 17:25:37.000000000 +1100
@@ -0,0 +1,28 @@
+From e7b0d4a0fff8ed45a90c2efe8ef294bdf7c9bdac Mon Sep 17 00:00:00 2001
+From: Simon McVittie <smcv@debian.org>
+Date: Sun, 10 Feb 2019 16:29:19 +0000
+Subject: [PATCH] useragent: Raise an exception if the LWP module can't be
+ loaded
+
+Signed-off-by: Simon McVittie <smcv@debian.org>
+---
+ IkiWiki.pm | 3 +++
+ 1 file changed, 3 insertions(+)
+
+diff --git a/IkiWiki.pm b/IkiWiki.pm
+index 90cb96e58..dc047b08a 100644
+--- a/IkiWiki.pm
++++ b/IkiWiki.pm
+@@ -2470,6 +2470,9 @@ sub add_autofile ($$$) {
+ }
+
+ sub useragent () {
++ eval q{use LWP};
++ error($@) if $@;
++
+ return LWP::UserAgent->new(
+ cookie_jar => $config{cookiejar},
+ env_proxy => 1, # respect proxy env vars
+--
+2.11.0
+
diff -Nru ikiwiki-3.20141016.4/CVE-2019-9187-2.patch ikiwiki-3.20141016.4+deb8u1/CVE-2019-9187-2.patch
--- ikiwiki-3.20141016.4/CVE-2019-9187-2.patch 1970-01-01 10:00:00.000000000 +1000
+++ ikiwiki-3.20141016.4+deb8u1/CVE-2019-9187-2.patch 2019-03-07 17:26:25.000000000 +1100
@@ -0,0 +1,238 @@
+From 67543ce1d62161fdef9dca198289d7dd7dceacc0 Mon Sep 17 00:00:00 2001
+From: Simon McVittie <smcv@debian.org>
+Date: Sun, 10 Feb 2019 16:30:07 +0000
+Subject: [PATCH] useragent: Don't allow non-HTTP protocols to be used
+
+This prevents the aggregate plugin from being used to read the contents
+of local files via file:/// URLs.
+
+Signed-off-by: Simon McVittie <smcv@debian.org>
+---
+ IkiWiki.pm | 1 +
+ t/aggregate-file.t | 173 +++++++++++++++++++++++++++++++++++++
+ t/noparanoia/LWPx/ParanoidAgent.pm | 2 +
+ t/secret.rss | 11 +++
+ 4 files changed, 187 insertions(+)
+ create mode 100755 t/aggregate-file.t
+ create mode 100644 t/noparanoia/LWPx/ParanoidAgent.pm
+ create mode 100644 t/secret.rss
+
+diff --git a/IkiWiki.pm b/IkiWiki.pm
+index dc047b08a..d5d1af56c 100644
+--- a/IkiWiki.pm
++++ b/IkiWiki.pm
+@@ -2477,6 +2477,7 @@ sub useragent () {
+ cookie_jar => $config{cookiejar},
+ env_proxy => 1, # respect proxy env vars
+ agent => $config{useragent},
++ protocols_allowed => [qw(http https)],
+ );
+ }
+
+diff --git a/t/aggregate-file.t b/t/aggregate-file.t
+new file mode 100755
+index 000000000..f00743dac
+--- /dev/null
++++ b/t/aggregate-file.t
+@@ -0,0 +1,173 @@
++#!/usr/bin/perl
++use utf8;
++use warnings;
++use strict;
++
++use Encode;
++use Test::More;
++
++BEGIN {
++ plan(skip_all => "CGI not available")
++ unless eval q{
++ use CGI qw();
++ 1;
++ };
++
++ plan(skip_all => "IPC::Run not available")
++ unless eval q{
++ use IPC::Run qw(run);
++ 1;
++ };
++
++ use_ok('IkiWiki');
++ use_ok('YAML::XS');
++}
++
++# We check for English error messages
++$ENV{LC_ALL} = 'C';
++
++use Cwd qw(getcwd);
++use Errno qw(ENOENT);
++
++my $installed = $ENV{INSTALLED_TESTS};
++
++my @command;
++if ($installed) {
++ @command = qw(ikiwiki --plugin inline);
++}
++else {
++ ok(! system("make -s ikiwiki.out"));
++ @command = ("perl", "-I".getcwd."/blib/lib", './ikiwiki.out',
++ '--underlaydir='.getcwd.'/underlays/basewiki',
++ '--set', 'underlaydirbase='.getcwd.'/underlays',
++ '--templatedir='.getcwd.'/templates');
++}
++
++sub write_old_file {
++ my $name = shift;
++ my $dir = shift;
++ my $content = shift;
++ writefile($name, $dir, $content);
++ ok(utime(333333333, 333333333, "$dir/$name"));
++}
++
++sub write_setup_file {
++ my %params = @_;
++ my %setup = (
++ wikiname => 'this is the name of my wiki',
++ srcdir => getcwd.'/t/tmp/in',
++ destdir => getcwd.'/t/tmp/out',
++ url => 'http://example.com',
++ cgiurl => 'http://example.com/cgi-bin/ikiwiki.cgi',
++ cgi_wrapper => getcwd.'/t/tmp/ikiwiki.cgi',
++ cgi_wrappermode => '0751',
++ add_plugins => [qw(aggregate)],
++ disable_plugins => [qw(emailauth openid passwordauth)],
++ aggregate_webtrigger => 1,
++ );
++ if ($params{without_paranoia}) {
++ $setup{libdirs} = [getcwd.'/t/noparanoia'];
++ }
++ unless ($installed) {
++ $setup{ENV} = { 'PERL5LIB' => getcwd.'/blib/lib' };
++ }
++ writefile("test.setup", "t/tmp",
++ "# IkiWiki::Setup::Yaml - YAML formatted setup file\n" .
++ Dump(\%setup));
++}
++
++sub thoroughly_rebuild {
++ ok(unlink("t/tmp/ikiwiki.cgi") || $!{ENOENT});
++ ok(! system(@command, qw(--setup t/tmp/test.setup --rebuild --wrappers)));
++}
++
++sub run_cgi {
++ my (%args) = @_;
++ my ($in, $out);
++ my $method = $args{method} || 'GET';
++ my $environ = $args{environ} || {};
++ my $params = $args{params} || { do => 'prefs' };
++
++ my %defaults = (
++ SCRIPT_NAME => '/cgi-bin/ikiwiki.cgi',
++ HTTP_HOST => 'example.com',
++ );
++
++ my $cgi = CGI->new($args{params});
++ my $query_string = $cgi->query_string();
++ diag $query_string;
++
++ if ($method eq 'POST') {
++ $defaults{REQUEST_METHOD} = 'POST';
++ $in = $query_string;
++ $defaults{CONTENT_LENGTH} = length $in;
++ } else {
++ $defaults{REQUEST_METHOD} = 'GET';
++ $defaults{QUERY_STRING} = $query_string;
++ }
++
++ my %envvars = (
++ %defaults,
++ %$environ,
++ );
++ run(["./t/tmp/ikiwiki.cgi"], \$in, \$out, init => sub {
++ map {
++ $ENV{$_} = $envvars{$_}
++ } keys(%envvars);
++ });
++
++ return decode_utf8($out);
++}
++
++sub test {
++ my $content;
++
++ ok(! system(qw(rm -rf t/tmp)));
++ ok(! system(qw(mkdir t/tmp)));
++
++ write_old_file('aggregator.mdwn', 't/tmp/in',
++ '[[!aggregate name="ssrf" url="file://'.getcwd.'/t/secret.rss"]]'
++ .'[[!inline pages="internal(aggregator/*)"]]');
++
++ write_setup_file();
++ thoroughly_rebuild();
++
++ $content = run_cgi(
++ method => 'GET',
++ params => {
++ do => 'aggregate_webtrigger',
++ },
++ );
++ unlike($content, qr{creating new page});
++ unlike($content, qr{Secrets});
++ ok(! -e 't/tmp/in/.ikiwiki/transient/aggregator/ssrf');
++ ok(! -e 't/tmp/in/.ikiwiki/transient/aggregator/ssrf/Secrets_go_here._aggregated');
++
++ thoroughly_rebuild();
++ $content = readfile('t/tmp/out/aggregator/index.html');
++ unlike($content, qr{Secrets});
++
++ diag('Trying test again with LWPx::ParanoidAgent disabled');
++
++ write_setup_file(without_paranoia => 1);
++ thoroughly_rebuild();
++
++ $content = run_cgi(
++ method => 'GET',
++ params => {
++ do => 'aggregate_webtrigger',
++ },
++ );
++ unlike($content, qr{creating new page});
++ unlike($content, qr{Secrets});
++ ok(! -e 't/tmp/in/.ikiwiki/transient/aggregator/ssrf');
++ ok(! -e 't/tmp/in/.ikiwiki/transient/aggregator/ssrf/Secrets_go_here._aggregated');
++
++ thoroughly_rebuild();
++ $content = readfile('t/tmp/out/aggregator/index.html');
++ unlike($content, qr{Secrets});
++}
++
++test();
++
++done_testing();
+diff --git a/t/noparanoia/LWPx/ParanoidAgent.pm b/t/noparanoia/LWPx/ParanoidAgent.pm
+new file mode 100644
+index 000000000..751e80ce6
+--- /dev/null
++++ b/t/noparanoia/LWPx/ParanoidAgent.pm
+@@ -0,0 +1,2 @@
++# make import fail
++0;
+diff --git a/t/secret.rss b/t/secret.rss
+new file mode 100644
+index 000000000..11202e9ed
+--- /dev/null
++++ b/t/secret.rss
+@@ -0,0 +1,11 @@
++<?xml version="1.0"?>
++<rss version="2.0">
++<channel>
++<title>Secrets go here</title>
++<description>Secrets go here</description>
++<item>
++ <title>Secrets go here</title>
++ <description>Secrets go here</description>
++</item>
++</channel>
++</rss>
+--
+2.11.0
+
diff -Nru ikiwiki-3.20141016.4/CVE-2019-9187-3.patch ikiwiki-3.20141016.4+deb8u1/CVE-2019-9187-3.patch
--- ikiwiki-3.20141016.4/CVE-2019-9187-3.patch 1970-01-01 10:00:00.000000000 +1000
+++ ikiwiki-3.20141016.4+deb8u1/CVE-2019-9187-3.patch 2019-03-07 17:26:41.000000000 +1100
@@ -0,0 +1,590 @@
+From d283e4ca1aeb6ca8cc0951c8495f778071076013 Mon Sep 17 00:00:00 2001
+From: Simon McVittie <smcv@debian.org>
+Date: Sun, 10 Feb 2019 17:22:06 +0000
+Subject: [PATCH] useragent: Automatically choose whether to use
+ LWPx::ParanoidAgent
+
+The simple implementation of this, which I'd prefer to use, would be:
+if we can import LWPx::ParanoidAgent, use it; otherwise, use
+LWP::UserAgent.
+
+However, aggregate has historically worked with proxies, and
+LWPx::ParanoidAgent quite reasonably refuses to work with proxies
+(because it can't know whether those proxies are going to do the same
+filtering that LWPx::ParanoidAgent would).
+
+Signed-off-by: Simon McVittie <smcv@debian.org>
+---
+ IkiWiki.pm | 123 ++++++++++++++++-
+ IkiWiki/Plugin/aggregate.pm | 5 +-
+ IkiWiki/Plugin/blogspam.pm | 16 +--
+ IkiWiki/Plugin/openid.pm | 12 +-
+ IkiWiki/Plugin/pinger.pm | 21 ++-
+ t/useragent.t | 317 ++++++++++++++++++++++++++++++++++++++++++++
+ 6 files changed, 458 insertions(+), 36 deletions(-)
+ create mode 100755 t/useragent.t
+
+diff --git a/IkiWiki.pm b/IkiWiki.pm
+index d5d1af56c..efb48293a 100644
+--- a/IkiWiki.pm
++++ b/IkiWiki.pm
+@@ -2469,16 +2469,131 @@ sub add_autofile ($$$) {
+ $autofiles{$file}{generator}=$generator;
+ }
+
+-sub useragent () {
++sub useragent (@) {
++ my %params = @_;
++ my $for_url = delete $params{for_url};
++ # Fail safe, in case a plugin calling this function is relying on
++ # a future parameter to make the UA more strict
++ foreach my $key (keys %params) {
++ error "Internal error: useragent(\"$key\" => ...) not understood";
++ }
++
+ eval q{use LWP};
+ error($@) if $@;
+
+- return LWP::UserAgent->new(
+- cookie_jar => $config{cookiejar},
+- env_proxy => 1, # respect proxy env vars
++ my %args = (
+ agent => $config{useragent},
++ cookie_jar => $config{cookiejar},
++ env_proxy => 0,
+ protocols_allowed => [qw(http https)],
+ );
++ my %proxies;
++
++ if (defined $for_url) {
++ # We know which URL we're going to fetch, so we can choose
++ # whether it's going to go through a proxy or not.
++ #
++ # We reimplement http_proxy, https_proxy and no_proxy here, so
++ # that we are not relying on LWP implementing them exactly the
++ # same way we do.
++
++ eval q{use URI};
++ error($@) if $@;
++
++ my $proxy;
++ my $uri = URI->new($for_url);
++
++ if ($uri->scheme eq 'http') {
++ $proxy = $ENV{http_proxy};
++ # HTTP_PROXY is deliberately not implemented
++ # because the HTTP_* namespace is also used by CGI
++ }
++ elsif ($uri->scheme eq 'https') {
++ $proxy = $ENV{https_proxy};
++ $proxy = $ENV{HTTPS_PROXY} unless defined $proxy;
++ }
++ else {
++ $proxy = undef;
++ }
++
++ foreach my $var (qw(no_proxy NO_PROXY)) {
++ my $no_proxy = $ENV{$var};
++ if (defined $no_proxy) {
++ foreach my $domain (split /\s*,\s*/, $no_proxy) {
++ if ($domain =~ s/^\*?\.//) {
++ # no_proxy="*.example.com" or
++ # ".example.com": match suffix
++ # against .example.com
++ if ($uri->host =~ m/(^|\.)\Q$domain\E$/i) {
++ $proxy = undef;
++ }
++ }
++ else {
++ # no_proxy="example.com":
++ # match exactly example.com
++ if (lc $uri->host eq lc $domain) {
++ $proxy = undef;
++ }
++ }
++ }
++ }
++ }
++
++ if (defined $proxy) {
++ $proxies{$uri->scheme} = $proxy;
++ # Paranoia: make sure we can't bypass the proxy
++ $args{protocols_allowed} = [$uri->scheme];
++ }
++ }
++ else {
++ # The plugin doesn't know yet which URL(s) it's going to
++ # fetch, so we have to make some conservative assumptions.
++ my $http_proxy = $ENV{http_proxy};
++ my $https_proxy = $ENV{https_proxy};
++ $https_proxy = $ENV{HTTPS_PROXY} unless defined $https_proxy;
++
++ # We don't respect no_proxy here: if we are not using the
++ # paranoid user-agent, then we need to give the proxy the
++ # opportunity to reject undesirable requests.
++
++ # If we have one, we need the other: otherwise, neither
++ # LWPx::ParanoidAgent nor the proxy would have the
++ # opportunity to filter requests for the other protocol.
++ if (defined $https_proxy && defined $http_proxy) {
++ %proxies = (http => $http_proxy, https => $https_proxy);
++ }
++ elsif (defined $https_proxy) {
++ %proxies = (http => $https_proxy, https => $https_proxy);
++ }
++ elsif (defined $http_proxy) {
++ %proxies = (http => $http_proxy, https => $http_proxy);
++ }
++
++ }
++
++ if (scalar keys %proxies) {
++ # The configured proxy is responsible for deciding which
++ # URLs are acceptable to fetch and which URLs are not.
++ my $ua = LWP::UserAgent->new(%args);
++ foreach my $scheme (@{$ua->protocols_allowed}) {
++ unless ($proxies{$scheme}) {
++ error "internal error: $scheme is allowed but has no proxy";
++ }
++ }
++ # We can't pass the proxies in %args because that only
++ # works since LWP 6.24.
++ foreach my $scheme (keys %proxies) {
++ $ua->proxy($scheme, $proxies{$scheme});
++ }
++ return $ua;
++ }
++
++ eval q{use LWPx::ParanoidAgent};
++ if ($@) {
++ print STDERR "warning: installing LWPx::ParanoidAgent is recommended\n";
++ return LWP::UserAgent->new(%args);
++ }
++ return LWPx::ParanoidAgent->new(%args);
+ }
+
+ sub sortspec_translate ($$) {
+diff --git a/IkiWiki/Plugin/aggregate.pm b/IkiWiki/Plugin/aggregate.pm
+index 05e22a290..8f0870e2e 100644
+--- a/IkiWiki/Plugin/aggregate.pm
++++ b/IkiWiki/Plugin/aggregate.pm
+@@ -513,7 +513,10 @@ sub aggregate (@) {
+ }
+ $feed->{feedurl}=pop @urls;
+ }
+- my $ua=useragent();
++ # Using the for_url parameter makes sure we crash if used
++ # with an older IkiWiki.pm that didn't automatically try
++ # to use LWPx::ParanoidAgent.
++ my $ua=useragent(for_url => $feed->{feedurl});
+ my $res=URI::Fetch->fetch($feed->{feedurl}, UserAgent=>$ua);
+ if (! $res) {
+ $feed->{message}=URI::Fetch->errstr;
+diff --git a/IkiWiki/Plugin/blogspam.pm b/IkiWiki/Plugin/blogspam.pm
+index 3eb4cf8b3..3835f52ca 100644
+--- a/IkiWiki/Plugin/blogspam.pm
++++ b/IkiWiki/Plugin/blogspam.pm
+@@ -57,18 +57,10 @@ sub checkconfig () {
+ };
+ error $@ if $@;
+
+- eval q{use LWPx::ParanoidAgent};
+- if (!$@) {
+- $client=LWPx::ParanoidAgent->new(agent => $config{useragent});
+- }
+- else {
+- eval q{use LWP};
+- if ($@) {
+- error $@;
+- return;
+- }
+- $client=useragent();
+- }
++ # Using the for_url parameter makes sure we crash if used
++ # with an older IkiWiki.pm that didn't automatically try
++ # to use LWPx::ParanoidAgent.
++ $client=useragent(for_url => $config{blogspam_server});
+ }
+
+ sub checkcontent (@) {
+diff --git a/IkiWiki/Plugin/openid.pm b/IkiWiki/Plugin/openid.pm
+index 35ef52a58..eb21955e9 100644
+--- a/IkiWiki/Plugin/openid.pm
++++ b/IkiWiki/Plugin/openid.pm
+@@ -219,14 +219,10 @@ sub getobj ($$) {
+ eval q{use Net::OpenID::Consumer};
+ error($@) if $@;
+
+- my $ua;
+- eval q{use LWPx::ParanoidAgent};
+- if (! $@) {
+- $ua=LWPx::ParanoidAgent->new(agent => $config{useragent});
+- }
+- else {
+- $ua=useragent();
+- }
++ # We pass the for_url parameter, even though it's undef, because
++ # that will make sure we crash if used with an older IkiWiki.pm
++ # that didn't automatically try to use LWPx::ParanoidAgent.
++ my $ua=useragent(for_url => undef);
+
+ # Store the secret in the session.
+ my $secret=$session->param("openid_secret");
+diff --git a/IkiWiki/Plugin/pinger.pm b/IkiWiki/Plugin/pinger.pm
+index b2d54af8a..ec764caee 100644
+--- a/IkiWiki/Plugin/pinger.pm
++++ b/IkiWiki/Plugin/pinger.pm
+@@ -70,17 +70,16 @@ sub ping {
+ eval q{use Net::INET6Glue::INET_is_INET6}; # may not be available
+
+ my $ua;
+- eval q{use LWPx::ParanoidAgent};
+- if (!$@) {
+- $ua=LWPx::ParanoidAgent->new(agent => $config{useragent});
+- }
+- else {
+- eval q{use LWP};
+- if ($@) {
+- debug(gettext("LWP not found, not pinging"));
+- return;
+- }
+- $ua=useragent();
++ eval {
++ # We pass the for_url parameter, even though it's
++ # undef, because that will make sure we crash if used
++ # with an older IkiWiki.pm that didn't automatically
++ # try to use LWPx::ParanoidAgent.
++ $ua=useragent(for_url => undef);
++ };
++ if ($@) {
++ debug(gettext("LWP not found, not pinging").": $@");
++ return;
+ }
+ $ua->timeout($config{pinger_timeout} || 15);
+
+diff --git a/t/useragent.t b/t/useragent.t
+new file mode 100755
+index 000000000..195a86521
+--- /dev/null
++++ b/t/useragent.t
+@@ -0,0 +1,317 @@
++#!/usr/bin/perl
++use warnings;
++use strict;
++use Test::More;
++
++my $have_paranoid_agent;
++BEGIN {
++ plan(skip_all => 'LWP not available')
++ unless eval q{
++ use LWP qw(); 1;
++ };
++ use_ok("IkiWiki");
++ $have_paranoid_agent = eval q{
++ use LWPx::ParanoidAgent qw(); 1;
++ };
++}
++
++eval { useragent(future_feature => 1); };
++ok($@, 'future features should cause useragent to fail');
++
++diag "==== No proxy ====";
++delete $ENV{http_proxy};
++delete $ENV{https_proxy};
++delete $ENV{no_proxy};
++delete $ENV{HTTPS_PROXY};
++delete $ENV{NO_PROXY};
++
++diag "---- Unspecified URL ----";
++my $ua = useragent(for_url => undef);
++SKIP: {
++ skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
++ ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
++}
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
++is($ua->proxy('http'), undef, 'No http proxy');
++is($ua->proxy('https'), undef, 'No https proxy');
++
++diag "---- Specified URL ----";
++$ua = useragent(for_url => 'http://example.com');
++SKIP: {
++ skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
++ ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
++}
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
++is($ua->proxy('http'), undef, 'No http proxy');
++is($ua->proxy('https'), undef, 'No https proxy');
++
++diag "==== Proxy for everything ====";
++$ENV{http_proxy} = 'http://proxy:8080';
++$ENV{https_proxy} = 'http://sproxy:8080';
++delete $ENV{no_proxy};
++delete $ENV{HTTPS_PROXY};
++delete $ENV{NO_PROXY};
++
++diag "---- Unspecified URL ----";
++$ua = useragent(for_url => undef);
++ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
++is($ua->proxy('http'), 'http://proxy:8080', 'should use proxy');
++is($ua->proxy('https'), 'http://sproxy:8080', 'should use CONNECT proxy');
++$ua = useragent(for_url => 'http://example.com');
++ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http)]);
++is($ua->proxy('http'), 'http://proxy:8080', 'should use proxy');
++# We don't care what $ua->proxy('https') is, because it won't be used
++$ua = useragent(for_url => 'https://example.com');
++ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(https)]);
++# We don't care what $ua->proxy('http') is, because it won't be used
++is($ua->proxy('https'), 'http://sproxy:8080', 'should use CONNECT proxy');
++
++diag "==== Selective proxy ====";
++$ENV{http_proxy} = 'http://proxy:8080';
++$ENV{https_proxy} = 'http://sproxy:8080';
++$ENV{no_proxy} = '*.example.net,example.com,.example.org';
++delete $ENV{HTTPS_PROXY};
++delete $ENV{NO_PROXY};
++
++diag "---- Unspecified URL ----";
++$ua = useragent(for_url => undef);
++ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
++is($ua->proxy('http'), 'http://proxy:8080', 'should use proxy');
++is($ua->proxy('https'), 'http://sproxy:8080', 'should use CONNECT proxy');
++
++diag "---- Exact match for no_proxy ----";
++$ua = useragent(for_url => 'http://example.com');
++SKIP: {
++ skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
++ ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
++}
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
++is($ua->proxy('http'), undef);
++is($ua->proxy('https'), undef);
++
++diag "---- Subdomain of exact domain in no_proxy ----";
++$ua = useragent(for_url => 'http://sub.example.com');
++ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http)]);
++is($ua->proxy('http'), 'http://proxy:8080', 'should use proxy');
++
++diag "---- example.net matches *.example.net ----";
++$ua = useragent(for_url => 'https://example.net');
++SKIP: {
++ skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
++ ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
++}
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
++is($ua->proxy('http'), undef);
++is($ua->proxy('https'), undef);
++
++diag "---- sub.example.net matches *.example.net ----";
++$ua = useragent(for_url => 'https://sub.example.net');
++SKIP: {
++ skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
++ ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
++}
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
++is($ua->proxy('http'), undef);
++is($ua->proxy('https'), undef);
++
++diag "---- badexample.net does not match *.example.net ----";
++$ua = useragent(for_url => 'https://badexample.net');
++ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(https)]);
++is($ua->proxy('https'), 'http://sproxy:8080', 'should use proxy');
++
++diag "---- example.org matches .example.org ----";
++$ua = useragent(for_url => 'https://example.org');
++SKIP: {
++ skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
++ ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
++}
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
++is($ua->proxy('http'), undef);
++is($ua->proxy('https'), undef);
++
++diag "---- sub.example.org matches .example.org ----";
++$ua = useragent(for_url => 'https://sub.example.org');
++SKIP: {
++ skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
++ ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
++}
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
++is($ua->proxy('http'), undef);
++is($ua->proxy('https'), undef);
++
++diag "---- badexample.org does not match .example.org ----";
++$ua = useragent(for_url => 'https://badexample.org');
++ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(https)]);
++is($ua->proxy('https'), 'http://sproxy:8080', 'should use proxy');
++
++diag "==== Selective proxy (alternate variables) ====";
++$ENV{http_proxy} = 'http://proxy:8080';
++delete $ENV{https_proxy};
++$ENV{HTTPS_PROXY} = 'http://sproxy:8080';
++delete $ENV{no_proxy};
++$ENV{NO_PROXY} = '*.example.net,example.com,.example.org';
++
++diag "---- Unspecified URL ----";
++$ua = useragent(for_url => undef);
++ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
++is($ua->proxy('http'), 'http://proxy:8080', 'should use proxy');
++is($ua->proxy('https'), 'http://sproxy:8080', 'should use CONNECT proxy');
++
++diag "---- Exact match for no_proxy ----";
++$ua = useragent(for_url => 'http://example.com');
++SKIP: {
++ skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
++ ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
++}
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
++is($ua->proxy('http'), undef);
++is($ua->proxy('https'), undef);
++
++diag "---- Subdomain of exact domain in no_proxy ----";
++$ua = useragent(for_url => 'http://sub.example.com');
++ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http)]);
++is($ua->proxy('http'), 'http://proxy:8080', 'should use proxy');
++
++diag "---- example.net matches *.example.net ----";
++$ua = useragent(for_url => 'https://example.net');
++SKIP: {
++ skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
++ ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
++}
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
++is($ua->proxy('http'), undef);
++is($ua->proxy('https'), undef);
++
++diag "---- sub.example.net matches *.example.net ----";
++$ua = useragent(for_url => 'https://sub.example.net');
++SKIP: {
++ skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
++ ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
++}
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
++is($ua->proxy('http'), undef);
++is($ua->proxy('https'), undef);
++
++diag "---- badexample.net does not match *.example.net ----";
++$ua = useragent(for_url => 'https://badexample.net');
++ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(https)]);
++is($ua->proxy('https'), 'http://sproxy:8080', 'should use proxy');
++
++diag "---- example.org matches .example.org ----";
++$ua = useragent(for_url => 'https://example.org');
++SKIP: {
++ skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
++ ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
++}
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
++is($ua->proxy('http'), undef);
++is($ua->proxy('https'), undef);
++
++diag "---- sub.example.org matches .example.org ----";
++$ua = useragent(for_url => 'https://sub.example.org');
++SKIP: {
++ skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
++ ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
++}
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
++is($ua->proxy('http'), undef);
++is($ua->proxy('https'), undef);
++
++diag "---- badexample.org does not match .example.org ----";
++$ua = useragent(for_url => 'https://badexample.org');
++ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(https)]);
++is($ua->proxy('https'), 'http://sproxy:8080', 'should use proxy');
++
++diag "==== Selective proxy (many variables) ====";
++$ENV{http_proxy} = 'http://proxy:8080';
++$ENV{https_proxy} = 'http://sproxy:8080';
++# This one should be ignored in favour of https_proxy
++$ENV{HTTPS_PROXY} = 'http://not.preferred.proxy:3128';
++# These two should be merged
++$ENV{no_proxy} = '*.example.net,example.com';
++$ENV{NO_PROXY} = '.example.org';
++
++diag "---- Unspecified URL ----";
++$ua = useragent(for_url => undef);
++ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
++is($ua->proxy('http'), 'http://proxy:8080', 'should use proxy');
++is($ua->proxy('https'), 'http://sproxy:8080', 'should use CONNECT proxy');
++
++diag "---- Exact match for no_proxy ----";
++$ua = useragent(for_url => 'http://example.com');
++SKIP: {
++ skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
++ ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
++}
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
++is($ua->proxy('http'), undef);
++is($ua->proxy('https'), undef);
++
++diag "---- Subdomain of exact domain in no_proxy ----";
++$ua = useragent(for_url => 'http://sub.example.com');
++ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http)]);
++is($ua->proxy('http'), 'http://proxy:8080', 'should use proxy');
++
++diag "---- example.net matches *.example.net ----";
++$ua = useragent(for_url => 'https://example.net');
++SKIP: {
++ skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
++ ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
++}
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
++is($ua->proxy('http'), undef);
++is($ua->proxy('https'), undef);
++
++diag "---- sub.example.net matches *.example.net ----";
++$ua = useragent(for_url => 'https://sub.example.net');
++SKIP: {
++ skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
++ ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
++}
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
++is($ua->proxy('http'), undef);
++is($ua->proxy('https'), undef);
++
++diag "---- badexample.net does not match *.example.net ----";
++$ua = useragent(for_url => 'https://badexample.net');
++ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(https)]);
++is($ua->proxy('https'), 'http://sproxy:8080', 'should use proxy');
++
++diag "==== One but not the other ====\n";
++$ENV{http_proxy} = 'http://proxy:8080';
++delete $ENV{https_proxy};
++delete $ENV{HTTPS_PROXY};
++delete $ENV{no_proxy};
++delete $ENV{NO_PROXY};
++$ua = useragent(for_url => undef);
++ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
++is($ua->proxy('http'), 'http://proxy:8080', 'should use proxy');
++is($ua->proxy('https'), 'http://proxy:8080', 'should use proxy');
++
++delete $ENV{http_proxy};
++$ENV{https_proxy} = 'http://sproxy:8080';
++delete $ENV{HTTPS_PROXY};
++delete $ENV{no_proxy};
++delete $ENV{NO_PROXY};
++$ua = useragent(for_url => undef);
++ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
++is($ua->proxy('http'), 'http://sproxy:8080', 'should use proxy');
++is($ua->proxy('https'), 'http://sproxy:8080', 'should use proxy');
++
++done_testing;
+--
+2.11.0
+
diff -Nru ikiwiki-3.20141016.4/CVE-2019-9187-4.patch ikiwiki-3.20141016.4+deb8u1/CVE-2019-9187-4.patch
--- ikiwiki-3.20141016.4/CVE-2019-9187-4.patch 1970-01-01 10:00:00.000000000 +1000
+++ ikiwiki-3.20141016.4+deb8u1/CVE-2019-9187-4.patch 2019-03-07 17:26:55.000000000 +1100
@@ -0,0 +1,175 @@
+From 9a275b2f1846d7268c71a740975447e269383849 Mon Sep 17 00:00:00 2001
+From: Simon McVittie <smcv@debian.org>
+Date: Sun, 10 Feb 2019 16:56:41 +0000
+Subject: [PATCH] doc: Document security issues involving LWP::UserAgent
+
+Recommend the LWPx::ParanoidAgent module where appropriate.
+It is particularly important for openid, since unauthenticated users
+can control which URLs that plugin will contact. Conversely, it is
+non-critical for blogspam, since the URL to be contacted is under
+the wiki administrator's control.
+
+Signed-off-by: Simon McVittie <smcv@debian.org>
+---
+ doc/plugins/aggregate.mdwn | 4 ++++
+ doc/plugins/blogspam.mdwn | 2 ++
+ doc/plugins/openid.mdwn | 7 +++++--
+ doc/plugins/pinger.mdwn | 8 +++++---
+ doc/security.mdwn | 49 +++++++++++++++++++++++++++++++++++++++++++++
+ doc/tips/using_a_proxy.mdwn | 22 ++++++++++++++++++++
+ 6 files changed, 87 insertions(+), 5 deletions(-)
+ create mode 100644 doc/tips/using_a_proxy.mdwn
+
+diff --git a/doc/plugins/aggregate.mdwn b/doc/plugins/aggregate.mdwn
+index 75123d923..b1db828d1 100644
+--- a/doc/plugins/aggregate.mdwn
++++ b/doc/plugins/aggregate.mdwn
+@@ -11,6 +11,10 @@ The [[meta]] and [[tag]] plugins are also recommended to be used with this
+ one. Either the [[htmltidy]] or [[htmlbalance]] plugin is suggested, since
+ feeds can easily contain html problems, some of which these plugins can fix.
+
++Installing the [[!cpan LWPx::ParanoidAgent]] Perl module is strongly
++recommended. The [[!cpan LWP]] module can also be used, but is susceptible
++to server-side request forgery.
++
+ ## triggering aggregation
+
+ You will need to run ikiwiki periodically from a cron job, passing it the
+diff --git a/doc/plugins/blogspam.mdwn b/doc/plugins/blogspam.mdwn
+index 745fc48e2..0ebae7d84 100644
+--- a/doc/plugins/blogspam.mdwn
++++ b/doc/plugins/blogspam.mdwn
+@@ -11,6 +11,8 @@ To check for and moderate comments, log in to the wiki as an admin,
+ go to your Preferences page, and click the "Comment Moderation" button.
+
+ The plugin requires the [[!cpan JSON]] perl module.
++The [[!cpan LWPx::ParanoidAgent]] Perl module is recommended,
++although this plugin can also fall back to [[!cpan LWP]].
+
+ You can control how content is tested via the `blogspam_options` setting.
+ The list of options is [here](http://blogspam.net/api/2.0/testComment.html#options).
+diff --git a/doc/plugins/openid.mdwn b/doc/plugins/openid.mdwn
+index 4c8e0d381..a061cb43f 100644
+--- a/doc/plugins/openid.mdwn
++++ b/doc/plugins/openid.mdwn
+@@ -7,8 +7,11 @@ into the wiki.
+ The plugin needs the [[!cpan Net::OpenID::Consumer]] perl module.
+ Version 1.x is needed in order for OpenID v2 to work.
+
+-The [[!cpan LWPx::ParanoidAgent]] perl module is used if available, for
+-added security. Finally, the [[!cpan Crypt::SSLeay]] perl module is needed
++The [[!cpan LWPx::ParanoidAgent]] Perl module is strongly recommended.
++The [[!cpan LWP]] module can also be used, but is susceptible to
++server-side request forgery.
++
++The [[!cpan Crypt::SSLeay]] Perl module is needed
+ to support users entering "https" OpenID urls.
+
+ This plugin is enabled by default, but can be turned off if you want to
+diff --git a/doc/plugins/pinger.mdwn b/doc/plugins/pinger.mdwn
+index 00d83e1bb..f37979ac6 100644
+--- a/doc/plugins/pinger.mdwn
++++ b/doc/plugins/pinger.mdwn
+@@ -10,9 +10,11 @@ can be kept up-to-date.
+ To configure what URLs to ping, use the [[ikiwiki/directive/ping]]
+ [[ikiwiki/directive]].
+
+-The [[!cpan LWP]] perl module is used for pinging. Or the [[!cpan
+-LWPx::ParanoidAgent]] perl module is used if available, for added security.
+-Finally, the [[!cpan Crypt::SSLeay]] perl module is needed to support pinging
++The [[!cpan LWPx::ParanoidAgent]] Perl module is strongly recommended.
++The [[!cpan LWP]] module can also be used, but is susceptible
++to server-side request forgery.
++
++The [[!cpan Crypt::SSLeay]] perl module is needed to support pinging
+ "https" urls.
+
+ By default the pinger will try to ping a site for 15 seconds before timing
+diff --git a/doc/security.mdwn b/doc/security.mdwn
+index e7770dd27..378a2e4bc 100644
+--- a/doc/security.mdwn
++++ b/doc/security.mdwn
+@@ -611,3 +611,52 @@ This was fixed in ikiwiki 3.20170111, with fixes backported to Debian 8
+ in version 3.20141016.4.
+
+ ([[!debcve CVE-2017-0356]]/OVE-20170111-0001)
++
++## Server-side request forgery via aggregate plugin
++
++The ikiwiki maintainers discovered that the [[plugins/aggregate]] plugin
++did not use [[!cpan LWPx::ParanoidAgent]]. On sites where the
++aggregate plugin is enabled, authorized wiki editors could tell ikiwiki
++to fetch potentially undesired URIs even if LWPx::ParanoidAgent was
++installed:
++
++* local files via `file:` URIs
++* other URI schemes that might be misused by attackers, such as `gopher:`
++* hosts that resolve to loopback IP addresses (127.x.x.x)
++* hosts that resolve to RFC 1918 IP addresses (192.168.x.x etc.)
++
++This could be used by an attacker to publish information that should not have
++been accessible, cause denial of service by requesting "tarpit" URIs that are
++slow to respond, or cause undesired side-effects if local web servers implement
++["unsafe"](https://tools.ietf.org/html/rfc7231#section-4.2.1) GET requests.
++([[!debcve CVE-2019-9187]])
++
++Additionally, if the LWPx::ParanoidAgent module was not installed, the
++[[plugins/blogspam]], [[plugins/openid]] and [[plugins/pinger]] plugins
++would fall back to [[!cpan LWP]], which is susceptible to similar attacks.
++This is unlikely to be a practical problem for the blogspam plugin because
++the URL it requests is under the control of the wiki administrator, but
++the openid plugin can request URLs controlled by unauthenticated remote
++users, and the pinger plugin can request URLs controlled by authorized
++wiki editors.
++
++This is addressed in ikiwiki 3.20190228 as follows, with the same fixes
++backported to Debian 9 in version 3.20170111.1:
++
++* URI schemes other than `http:` and `https:` are not accepted, preventing
++ access to `file:`, `gopher:`, etc.
++
++* If a proxy is [[configured in the ikiwiki setup file|tips/using_a_proxy]],
++ it is used for all outgoing `http:` and `https:` requests. In this case
++ the proxy is responsible for blocking any requests that are undesired,
++ including loopback or RFC 1918 addresses.
++
++* If a proxy is not configured, and LWPx::ParanoidAgent is installed,
++ it will be used. This prevents loopback and RFC 1918 IP addresses, and
++ sets a timeout to avoid denial of service via "tarpit" URIs.
++
++* Otherwise, the ordinary LWP user-agent will be used. This allows requests
++ to loopback and RFC 1918 IP addresses, and has less robust timeout
++ behaviour. We are not treating this as a vulnerability: if this
++ behaviour is not acceptable for your site, please make sure to install
++ LWPx::ParanoidAgent or disable the affected plugins.
+diff --git a/doc/tips/using_a_proxy.mdwn b/doc/tips/using_a_proxy.mdwn
+new file mode 100644
+index 000000000..39df3c42a
+--- /dev/null
++++ b/doc/tips/using_a_proxy.mdwn
+@@ -0,0 +1,22 @@
++Some ikiwiki plugins make outgoing HTTP requests from the web server:
++
++* [[plugins/aggregate]] (to download Atom and RSS feeds)
++* [[plugins/blogspam]] (to check whether a comment or edit is spam)
++* [[plugins/openid]] (to authenticate users)
++* [[plugins/pinger]] (to ping other ikiwiki installations)
++
++If your ikiwiki installation cannot contact the Internet without going
++through a proxy, you can configure this in the [[setup file|setup]] by
++setting environment variables:
++
++ ENV:
++ http_proxy: "http://proxy.example.com:8080"
++ https_proxy: "http://proxy.example.com:8080"
++ # optional
++ no_proxy: ".example.com,www.example.org"
++
++Note that some plugins will use the configured proxy for all destinations,
++even if they are listed in `no_proxy`.
++
++To avoid server-side request forgery attacks, ensure that your proxy does
++not allow requests to addresses that are considered to be internal.
+--
+2.11.0
+
diff -Nru ikiwiki-3.20141016.4/debian/changelog ikiwiki-3.20141016.4+deb8u1/debian/changelog
--- ikiwiki-3.20141016.4/debian/changelog 2017-01-12 05:18:52.000000000 +1100
+++ ikiwiki-3.20141016.4+deb8u1/debian/changelog 2019-03-07 17:35:55.000000000 +1100
@@ -1,3 +1,10 @@
+ikiwiki (3.20141016.4+deb8u1) jessie-security; urgency=high
+
+ * Non-maintainer upload by the LTS Team.
+ * CVE-2019-9187: Fix server-side request forgery via aggregate plugin.
+
+ -- Brian May <bam@debian.org> Thu, 07 Mar 2019 17:35:55 +1100
+
ikiwiki (3.20141016.4) jessie-security; urgency=high
* Reference CVE-2016-4561 in 3.20141016.3 changelog
diff -Nru ikiwiki-3.20141016.4/debian/control ikiwiki-3.20141016.4+deb8u1/debian/control
--- ikiwiki-3.20141016.4/debian/control 2017-01-12 05:18:52.000000000 +1100
+++ ikiwiki-3.20141016.4+deb8u1/debian/control 2019-03-07 17:35:55.000000000 +1100
@@ -17,7 +17,8 @@
libnet-openid-consumer-perl,
libxml-feed-perl,
libxml-parser-perl,
- libxml-twig-perl
+ libxml-twig-perl,
+ liblwpx-paranoidagent-perl,
Maintainer: Simon McVittie <smcv@debian.org>
Uploaders: Josh Triplett <josh@freedesktop.org>
Standards-Version: 3.9.5
diff -Nru ikiwiki-3.20141016.4/debian/patches/CVE-2019-9187-1.patch ikiwiki-3.20141016.4+deb8u1/debian/patches/CVE-2019-9187-1.patch
--- ikiwiki-3.20141016.4/debian/patches/CVE-2019-9187-1.patch 1970-01-01 10:00:00.000000000 +1000
+++ ikiwiki-3.20141016.4+deb8u1/debian/patches/CVE-2019-9187-1.patch 2019-03-07 17:32:31.000000000 +1100
@@ -0,0 +1,23 @@
+From e7b0d4a0fff8ed45a90c2efe8ef294bdf7c9bdac Mon Sep 17 00:00:00 2001
+From: Simon McVittie <smcv@debian.org>
+Date: Sun, 10 Feb 2019 16:29:19 +0000
+Subject: [PATCH] useragent: Raise an exception if the LWP module can't be
+ loaded
+
+Signed-off-by: Simon McVittie <smcv@debian.org>
+---
+ IkiWiki.pm | 3 +++
+ 1 file changed, 3 insertions(+)
+
+--- a/IkiWiki.pm
++++ b/IkiWiki.pm
+@@ -2368,6 +2368,9 @@
+ }
+
+ sub useragent () {
++ eval q{use LWP};
++ error($@) if $@;
++
+ return LWP::UserAgent->new(
+ cookie_jar => $config{cookiejar},
+ env_proxy => 1, # respect proxy env vars
diff -Nru ikiwiki-3.20141016.4/debian/patches/CVE-2019-9187-2.patch ikiwiki-3.20141016.4+deb8u1/debian/patches/CVE-2019-9187-2.patch
--- ikiwiki-3.20141016.4/debian/patches/CVE-2019-9187-2.patch 1970-01-01 10:00:00.000000000 +1000
+++ ikiwiki-3.20141016.4+deb8u1/debian/patches/CVE-2019-9187-2.patch 2019-03-07 17:32:43.000000000 +1100
@@ -0,0 +1,224 @@
+From 67543ce1d62161fdef9dca198289d7dd7dceacc0 Mon Sep 17 00:00:00 2001
+From: Simon McVittie <smcv@debian.org>
+Date: Sun, 10 Feb 2019 16:30:07 +0000
+Subject: [PATCH] useragent: Don't allow non-HTTP protocols to be used
+
+This prevents the aggregate plugin from being used to read the contents
+of local files via file:/// URLs.
+
+Signed-off-by: Simon McVittie <smcv@debian.org>
+---
+ IkiWiki.pm | 1 +
+ t/aggregate-file.t | 173 +++++++++++++++++++++++++++++++++++++
+ t/noparanoia/LWPx/ParanoidAgent.pm | 2 +
+ t/secret.rss | 11 +++
+ 4 files changed, 187 insertions(+)
+ create mode 100755 t/aggregate-file.t
+ create mode 100644 t/noparanoia/LWPx/ParanoidAgent.pm
+ create mode 100644 t/secret.rss
+
+--- a/IkiWiki.pm
++++ b/IkiWiki.pm
+@@ -2375,6 +2375,7 @@
+ cookie_jar => $config{cookiejar},
+ env_proxy => 1, # respect proxy env vars
+ agent => $config{useragent},
++ protocols_allowed => [qw(http https)],
+ );
+ }
+
+--- /dev/null
++++ b/t/aggregate-file.t
+@@ -0,0 +1,173 @@
++#!/usr/bin/perl
++use utf8;
++use warnings;
++use strict;
++
++use Encode;
++use Test::More;
++
++BEGIN {
++ plan(skip_all => "CGI not available")
++ unless eval q{
++ use CGI qw();
++ 1;
++ };
++
++ plan(skip_all => "IPC::Run not available")
++ unless eval q{
++ use IPC::Run qw(run);
++ 1;
++ };
++
++ use_ok('IkiWiki');
++ use_ok('YAML::XS');
++}
++
++# We check for English error messages
++$ENV{LC_ALL} = 'C';
++
++use Cwd qw(getcwd);
++use Errno qw(ENOENT);
++
++my $installed = $ENV{INSTALLED_TESTS};
++
++my @command;
++if ($installed) {
++ @command = qw(ikiwiki --plugin inline);
++}
++else {
++ ok(! system("make -s ikiwiki.out"));
++ @command = ("perl", "-I".getcwd."/blib/lib", './ikiwiki.out',
++ '--underlaydir='.getcwd.'/underlays/basewiki',
++ '--set', 'underlaydirbase='.getcwd.'/underlays',
++ '--templatedir='.getcwd.'/templates');
++}
++
++sub write_old_file {
++ my $name = shift;
++ my $dir = shift;
++ my $content = shift;
++ writefile($name, $dir, $content);
++ ok(utime(333333333, 333333333, "$dir/$name"));
++}
++
++sub write_setup_file {
++ my %params = @_;
++ my %setup = (
++ wikiname => 'this is the name of my wiki',
++ srcdir => getcwd.'/t/tmp/in',
++ destdir => getcwd.'/t/tmp/out',
++ url => 'http://example.com',
++ cgiurl => 'http://example.com/cgi-bin/ikiwiki.cgi',
++ cgi_wrapper => getcwd.'/t/tmp/ikiwiki.cgi',
++ cgi_wrappermode => '0751',
++ add_plugins => [qw(aggregate)],
++ disable_plugins => [qw(emailauth openid passwordauth)],
++ aggregate_webtrigger => 1,
++ );
++ if ($params{without_paranoia}) {
++ $setup{libdirs} = [getcwd.'/t/noparanoia'];
++ }
++ unless ($installed) {
++ $setup{ENV} = { 'PERL5LIB' => getcwd.'/blib/lib' };
++ }
++ writefile("test.setup", "t/tmp",
++ "# IkiWiki::Setup::Yaml - YAML formatted setup file\n" .
++ Dump(\%setup));
++}
++
++sub thoroughly_rebuild {
++ ok(unlink("t/tmp/ikiwiki.cgi") || $!{ENOENT});
++ ok(! system(@command, qw(--setup t/tmp/test.setup --rebuild --wrappers)));
++}
++
++sub run_cgi {
++ my (%args) = @_;
++ my ($in, $out);
++ my $method = $args{method} || 'GET';
++ my $environ = $args{environ} || {};
++ my $params = $args{params} || { do => 'prefs' };
++
++ my %defaults = (
++ SCRIPT_NAME => '/cgi-bin/ikiwiki.cgi',
++ HTTP_HOST => 'example.com',
++ );
++
++ my $cgi = CGI->new($args{params});
++ my $query_string = $cgi->query_string();
++ diag $query_string;
++
++ if ($method eq 'POST') {
++ $defaults{REQUEST_METHOD} = 'POST';
++ $in = $query_string;
++ $defaults{CONTENT_LENGTH} = length $in;
++ } else {
++ $defaults{REQUEST_METHOD} = 'GET';
++ $defaults{QUERY_STRING} = $query_string;
++ }
++
++ my %envvars = (
++ %defaults,
++ %$environ,
++ );
++ run(["./t/tmp/ikiwiki.cgi"], \$in, \$out, init => sub {
++ map {
++ $ENV{$_} = $envvars{$_}
++ } keys(%envvars);
++ });
++
++ return decode_utf8($out);
++}
++
++sub test {
++ my $content;
++
++ ok(! system(qw(rm -rf t/tmp)));
++ ok(! system(qw(mkdir t/tmp)));
++
++ write_old_file('aggregator.mdwn', 't/tmp/in',
++ '[[!aggregate name="ssrf" url="file://'.getcwd.'/t/secret.rss"]]'
++ .'[[!inline pages="internal(aggregator/*)"]]');
++
++ write_setup_file();
++ thoroughly_rebuild();
++
++ $content = run_cgi(
++ method => 'GET',
++ params => {
++ do => 'aggregate_webtrigger',
++ },
++ );
++ unlike($content, qr{creating new page});
++ unlike($content, qr{Secrets});
++ ok(! -e 't/tmp/in/.ikiwiki/transient/aggregator/ssrf');
++ ok(! -e 't/tmp/in/.ikiwiki/transient/aggregator/ssrf/Secrets_go_here._aggregated');
++
++ thoroughly_rebuild();
++ $content = readfile('t/tmp/out/aggregator/index.html');
++ unlike($content, qr{Secrets});
++
++ diag('Trying test again with LWPx::ParanoidAgent disabled');
++
++ write_setup_file(without_paranoia => 1);
++ thoroughly_rebuild();
++
++ $content = run_cgi(
++ method => 'GET',
++ params => {
++ do => 'aggregate_webtrigger',
++ },
++ );
++ unlike($content, qr{creating new page});
++ unlike($content, qr{Secrets});
++ ok(! -e 't/tmp/in/.ikiwiki/transient/aggregator/ssrf');
++ ok(! -e 't/tmp/in/.ikiwiki/transient/aggregator/ssrf/Secrets_go_here._aggregated');
++
++ thoroughly_rebuild();
++ $content = readfile('t/tmp/out/aggregator/index.html');
++ unlike($content, qr{Secrets});
++}
++
++test();
++
++done_testing();
+--- /dev/null
++++ b/t/noparanoia/LWPx/ParanoidAgent.pm
+@@ -0,0 +1,2 @@
++# make import fail
++0;
+--- /dev/null
++++ b/t/secret.rss
+@@ -0,0 +1,11 @@
++<?xml version="1.0"?>
++<rss version="2.0">
++<channel>
++<title>Secrets go here</title>
++<description>Secrets go here</description>
++<item>
++ <title>Secrets go here</title>
++ <description>Secrets go here</description>
++</item>
++</channel>
++</rss>
diff -Nru ikiwiki-3.20141016.4/debian/patches/CVE-2019-9187-3.patch ikiwiki-3.20141016.4+deb8u1/debian/patches/CVE-2019-9187-3.patch
--- ikiwiki-3.20141016.4/debian/patches/CVE-2019-9187-3.patch 1970-01-01 10:00:00.000000000 +1000
+++ ikiwiki-3.20141016.4+deb8u1/debian/patches/CVE-2019-9187-3.patch 2019-03-07 17:32:58.000000000 +1100
@@ -0,0 +1,574 @@
+From d283e4ca1aeb6ca8cc0951c8495f778071076013 Mon Sep 17 00:00:00 2001
+From: Simon McVittie <smcv@debian.org>
+Date: Sun, 10 Feb 2019 17:22:06 +0000
+Subject: [PATCH] useragent: Automatically choose whether to use
+ LWPx::ParanoidAgent
+
+The simple implementation of this, which I'd prefer to use, would be:
+if we can import LWPx::ParanoidAgent, use it; otherwise, use
+LWP::UserAgent.
+
+However, aggregate has historically worked with proxies, and
+LWPx::ParanoidAgent quite reasonably refuses to work with proxies
+(because it can't know whether those proxies are going to do the same
+filtering that LWPx::ParanoidAgent would).
+
+Signed-off-by: Simon McVittie <smcv@debian.org>
+---
+ IkiWiki.pm | 123 ++++++++++++++++-
+ IkiWiki/Plugin/aggregate.pm | 5 +-
+ IkiWiki/Plugin/blogspam.pm | 16 +--
+ IkiWiki/Plugin/openid.pm | 12 +-
+ IkiWiki/Plugin/pinger.pm | 21 ++-
+ t/useragent.t | 317 ++++++++++++++++++++++++++++++++++++++++++++
+ 6 files changed, 458 insertions(+), 36 deletions(-)
+ create mode 100755 t/useragent.t
+
+--- a/IkiWiki.pm
++++ b/IkiWiki.pm
+@@ -2367,16 +2367,131 @@
+ $autofiles{$file}{generator}=$generator;
+ }
+
+-sub useragent () {
++sub useragent (@) {
++ my %params = @_;
++ my $for_url = delete $params{for_url};
++ # Fail safe, in case a plugin calling this function is relying on
++ # a future parameter to make the UA more strict
++ foreach my $key (keys %params) {
++ error "Internal error: useragent(\"$key\" => ...) not understood";
++ }
++
+ eval q{use LWP};
+ error($@) if $@;
+
+- return LWP::UserAgent->new(
+- cookie_jar => $config{cookiejar},
+- env_proxy => 1, # respect proxy env vars
++ my %args = (
+ agent => $config{useragent},
++ cookie_jar => $config{cookiejar},
++ env_proxy => 0,
+ protocols_allowed => [qw(http https)],
+ );
++ my %proxies;
++
++ if (defined $for_url) {
++ # We know which URL we're going to fetch, so we can choose
++ # whether it's going to go through a proxy or not.
++ #
++ # We reimplement http_proxy, https_proxy and no_proxy here, so
++ # that we are not relying on LWP implementing them exactly the
++ # same way we do.
++
++ eval q{use URI};
++ error($@) if $@;
++
++ my $proxy;
++ my $uri = URI->new($for_url);
++
++ if ($uri->scheme eq 'http') {
++ $proxy = $ENV{http_proxy};
++ # HTTP_PROXY is deliberately not implemented
++ # because the HTTP_* namespace is also used by CGI
++ }
++ elsif ($uri->scheme eq 'https') {
++ $proxy = $ENV{https_proxy};
++ $proxy = $ENV{HTTPS_PROXY} unless defined $proxy;
++ }
++ else {
++ $proxy = undef;
++ }
++
++ foreach my $var (qw(no_proxy NO_PROXY)) {
++ my $no_proxy = $ENV{$var};
++ if (defined $no_proxy) {
++ foreach my $domain (split /\s*,\s*/, $no_proxy) {
++ if ($domain =~ s/^\*?\.//) {
++ # no_proxy="*.example.com" or
++ # ".example.com": match suffix
++ # against .example.com
++ if ($uri->host =~ m/(^|\.)\Q$domain\E$/i) {
++ $proxy = undef;
++ }
++ }
++ else {
++ # no_proxy="example.com":
++ # match exactly example.com
++ if (lc $uri->host eq lc $domain) {
++ $proxy = undef;
++ }
++ }
++ }
++ }
++ }
++
++ if (defined $proxy) {
++ $proxies{$uri->scheme} = $proxy;
++ # Paranoia: make sure we can't bypass the proxy
++ $args{protocols_allowed} = [$uri->scheme];
++ }
++ }
++ else {
++ # The plugin doesn't know yet which URL(s) it's going to
++ # fetch, so we have to make some conservative assumptions.
++ my $http_proxy = $ENV{http_proxy};
++ my $https_proxy = $ENV{https_proxy};
++ $https_proxy = $ENV{HTTPS_PROXY} unless defined $https_proxy;
++
++ # We don't respect no_proxy here: if we are not using the
++ # paranoid user-agent, then we need to give the proxy the
++ # opportunity to reject undesirable requests.
++
++ # If we have one, we need the other: otherwise, neither
++ # LWPx::ParanoidAgent nor the proxy would have the
++ # opportunity to filter requests for the other protocol.
++ if (defined $https_proxy && defined $http_proxy) {
++ %proxies = (http => $http_proxy, https => $https_proxy);
++ }
++ elsif (defined $https_proxy) {
++ %proxies = (http => $https_proxy, https => $https_proxy);
++ }
++ elsif (defined $http_proxy) {
++ %proxies = (http => $http_proxy, https => $http_proxy);
++ }
++
++ }
++
++ if (scalar keys %proxies) {
++ # The configured proxy is responsible for deciding which
++ # URLs are acceptable to fetch and which URLs are not.
++ my $ua = LWP::UserAgent->new(%args);
++ foreach my $scheme (@{$ua->protocols_allowed}) {
++ unless ($proxies{$scheme}) {
++ error "internal error: $scheme is allowed but has no proxy";
++ }
++ }
++ # We can't pass the proxies in %args because that only
++ # works since LWP 6.24.
++ foreach my $scheme (keys %proxies) {
++ $ua->proxy($scheme, $proxies{$scheme});
++ }
++ return $ua;
++ }
++
++ eval q{use LWPx::ParanoidAgent};
++ if ($@) {
++ print STDERR "warning: installing LWPx::ParanoidAgent is recommended\n";
++ return LWP::UserAgent->new(%args);
++ }
++ return LWPx::ParanoidAgent->new(%args);
+ }
+
+ sub sortspec_translate ($$) {
+--- a/IkiWiki/Plugin/aggregate.pm
++++ b/IkiWiki/Plugin/aggregate.pm
+@@ -513,7 +513,10 @@
+ }
+ $feed->{feedurl}=pop @urls;
+ }
+- my $ua=useragent();
++ # Using the for_url parameter makes sure we crash if used
++ # with an older IkiWiki.pm that didn't automatically try
++ # to use LWPx::ParanoidAgent.
++ my $ua=useragent(for_url => $feed->{feedurl});
+ my $res=URI::Fetch->fetch($feed->{feedurl}, UserAgent=>$ua);
+ if (! $res) {
+ $feed->{message}=URI::Fetch->errstr;
+--- a/IkiWiki/Plugin/blogspam.pm
++++ b/IkiWiki/Plugin/blogspam.pm
+@@ -57,18 +57,10 @@
+ };
+ error $@ if $@;
+
+- eval q{use LWPx::ParanoidAgent};
+- if (!$@) {
+- $client=LWPx::ParanoidAgent->new(agent => $config{useragent});
+- }
+- else {
+- eval q{use LWP};
+- if ($@) {
+- error $@;
+- return;
+- }
+- $client=useragent();
+- }
++ # Using the for_url parameter makes sure we crash if used
++ # with an older IkiWiki.pm that didn't automatically try
++ # to use LWPx::ParanoidAgent.
++ $client=useragent(for_url => $config{blogspam_server});
+ }
+
+ sub checkcontent (@) {
+--- a/IkiWiki/Plugin/openid.pm
++++ b/IkiWiki/Plugin/openid.pm
+@@ -237,14 +237,10 @@
+ eval q{use Net::OpenID::Consumer};
+ error($@) if $@;
+
+- my $ua;
+- eval q{use LWPx::ParanoidAgent};
+- if (! $@) {
+- $ua=LWPx::ParanoidAgent->new(agent => $config{useragent});
+- }
+- else {
+- $ua=useragent();
+- }
++ # We pass the for_url parameter, even though it's undef, because
++ # that will make sure we crash if used with an older IkiWiki.pm
++ # that didn't automatically try to use LWPx::ParanoidAgent.
++ my $ua=useragent(for_url => undef);
+
+ # Store the secret in the session.
+ my $secret=$session->param("openid_secret");
+--- a/IkiWiki/Plugin/pinger.pm
++++ b/IkiWiki/Plugin/pinger.pm
+@@ -70,17 +70,16 @@
+ eval q{use Net::INET6Glue::INET_is_INET6}; # may not be available
+
+ my $ua;
+- eval q{use LWPx::ParanoidAgent};
+- if (!$@) {
+- $ua=LWPx::ParanoidAgent->new(agent => $config{useragent});
+- }
+- else {
+- eval q{use LWP};
+- if ($@) {
+- debug(gettext("LWP not found, not pinging"));
+- return;
+- }
+- $ua=useragent();
++ eval {
++ # We pass the for_url parameter, even though it's
++ # undef, because that will make sure we crash if used
++ # with an older IkiWiki.pm that didn't automatically
++ # try to use LWPx::ParanoidAgent.
++ $ua=useragent(for_url => undef);
++ };
++ if ($@) {
++ debug(gettext("LWP not found, not pinging").": $@");
++ return;
+ }
+ $ua->timeout($config{pinger_timeout} || 15);
+
+--- /dev/null
++++ b/t/useragent.t
+@@ -0,0 +1,317 @@
++#!/usr/bin/perl
++use warnings;
++use strict;
++use Test::More;
++
++my $have_paranoid_agent;
++BEGIN {
++ plan(skip_all => 'LWP not available')
++ unless eval q{
++ use LWP qw(); 1;
++ };
++ use_ok("IkiWiki");
++ $have_paranoid_agent = eval q{
++ use LWPx::ParanoidAgent qw(); 1;
++ };
++}
++
++eval { useragent(future_feature => 1); };
++ok($@, 'future features should cause useragent to fail');
++
++diag "==== No proxy ====";
++delete $ENV{http_proxy};
++delete $ENV{https_proxy};
++delete $ENV{no_proxy};
++delete $ENV{HTTPS_PROXY};
++delete $ENV{NO_PROXY};
++
++diag "---- Unspecified URL ----";
++my $ua = useragent(for_url => undef);
++SKIP: {
++ skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
++ ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
++}
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
++is($ua->proxy('http'), undef, 'No http proxy');
++is($ua->proxy('https'), undef, 'No https proxy');
++
++diag "---- Specified URL ----";
++$ua = useragent(for_url => 'http://example.com');
++SKIP: {
++ skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
++ ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
++}
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
++is($ua->proxy('http'), undef, 'No http proxy');
++is($ua->proxy('https'), undef, 'No https proxy');
++
++diag "==== Proxy for everything ====";
++$ENV{http_proxy} = 'http://proxy:8080';
++$ENV{https_proxy} = 'http://sproxy:8080';
++delete $ENV{no_proxy};
++delete $ENV{HTTPS_PROXY};
++delete $ENV{NO_PROXY};
++
++diag "---- Unspecified URL ----";
++$ua = useragent(for_url => undef);
++ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
++is($ua->proxy('http'), 'http://proxy:8080', 'should use proxy');
++is($ua->proxy('https'), 'http://sproxy:8080', 'should use CONNECT proxy');
++$ua = useragent(for_url => 'http://example.com');
++ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http)]);
++is($ua->proxy('http'), 'http://proxy:8080', 'should use proxy');
++# We don't care what $ua->proxy('https') is, because it won't be used
++$ua = useragent(for_url => 'https://example.com');
++ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(https)]);
++# We don't care what $ua->proxy('http') is, because it won't be used
++is($ua->proxy('https'), 'http://sproxy:8080', 'should use CONNECT proxy');
++
++diag "==== Selective proxy ====";
++$ENV{http_proxy} = 'http://proxy:8080';
++$ENV{https_proxy} = 'http://sproxy:8080';
++$ENV{no_proxy} = '*.example.net,example.com,.example.org';
++delete $ENV{HTTPS_PROXY};
++delete $ENV{NO_PROXY};
++
++diag "---- Unspecified URL ----";
++$ua = useragent(for_url => undef);
++ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
++is($ua->proxy('http'), 'http://proxy:8080', 'should use proxy');
++is($ua->proxy('https'), 'http://sproxy:8080', 'should use CONNECT proxy');
++
++diag "---- Exact match for no_proxy ----";
++$ua = useragent(for_url => 'http://example.com');
++SKIP: {
++ skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
++ ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
++}
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
++is($ua->proxy('http'), undef);
++is($ua->proxy('https'), undef);
++
++diag "---- Subdomain of exact domain in no_proxy ----";
++$ua = useragent(for_url => 'http://sub.example.com');
++ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http)]);
++is($ua->proxy('http'), 'http://proxy:8080', 'should use proxy');
++
++diag "---- example.net matches *.example.net ----";
++$ua = useragent(for_url => 'https://example.net');
++SKIP: {
++ skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
++ ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
++}
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
++is($ua->proxy('http'), undef);
++is($ua->proxy('https'), undef);
++
++diag "---- sub.example.net matches *.example.net ----";
++$ua = useragent(for_url => 'https://sub.example.net');
++SKIP: {
++ skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
++ ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
++}
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
++is($ua->proxy('http'), undef);
++is($ua->proxy('https'), undef);
++
++diag "---- badexample.net does not match *.example.net ----";
++$ua = useragent(for_url => 'https://badexample.net');
++ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(https)]);
++is($ua->proxy('https'), 'http://sproxy:8080', 'should use proxy');
++
++diag "---- example.org matches .example.org ----";
++$ua = useragent(for_url => 'https://example.org');
++SKIP: {
++ skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
++ ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
++}
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
++is($ua->proxy('http'), undef);
++is($ua->proxy('https'), undef);
++
++diag "---- sub.example.org matches .example.org ----";
++$ua = useragent(for_url => 'https://sub.example.org');
++SKIP: {
++ skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
++ ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
++}
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
++is($ua->proxy('http'), undef);
++is($ua->proxy('https'), undef);
++
++diag "---- badexample.org does not match .example.org ----";
++$ua = useragent(for_url => 'https://badexample.org');
++ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(https)]);
++is($ua->proxy('https'), 'http://sproxy:8080', 'should use proxy');
++
++diag "==== Selective proxy (alternate variables) ====";
++$ENV{http_proxy} = 'http://proxy:8080';
++delete $ENV{https_proxy};
++$ENV{HTTPS_PROXY} = 'http://sproxy:8080';
++delete $ENV{no_proxy};
++$ENV{NO_PROXY} = '*.example.net,example.com,.example.org';
++
++diag "---- Unspecified URL ----";
++$ua = useragent(for_url => undef);
++ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
++is($ua->proxy('http'), 'http://proxy:8080', 'should use proxy');
++is($ua->proxy('https'), 'http://sproxy:8080', 'should use CONNECT proxy');
++
++diag "---- Exact match for no_proxy ----";
++$ua = useragent(for_url => 'http://example.com');
++SKIP: {
++ skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
++ ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
++}
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
++is($ua->proxy('http'), undef);
++is($ua->proxy('https'), undef);
++
++diag "---- Subdomain of exact domain in no_proxy ----";
++$ua = useragent(for_url => 'http://sub.example.com');
++ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http)]);
++is($ua->proxy('http'), 'http://proxy:8080', 'should use proxy');
++
++diag "---- example.net matches *.example.net ----";
++$ua = useragent(for_url => 'https://example.net');
++SKIP: {
++ skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
++ ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
++}
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
++is($ua->proxy('http'), undef);
++is($ua->proxy('https'), undef);
++
++diag "---- sub.example.net matches *.example.net ----";
++$ua = useragent(for_url => 'https://sub.example.net');
++SKIP: {
++ skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
++ ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
++}
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
++is($ua->proxy('http'), undef);
++is($ua->proxy('https'), undef);
++
++diag "---- badexample.net does not match *.example.net ----";
++$ua = useragent(for_url => 'https://badexample.net');
++ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(https)]);
++is($ua->proxy('https'), 'http://sproxy:8080', 'should use proxy');
++
++diag "---- example.org matches .example.org ----";
++$ua = useragent(for_url => 'https://example.org');
++SKIP: {
++ skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
++ ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
++}
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
++is($ua->proxy('http'), undef);
++is($ua->proxy('https'), undef);
++
++diag "---- sub.example.org matches .example.org ----";
++$ua = useragent(for_url => 'https://sub.example.org');
++SKIP: {
++ skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
++ ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
++}
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
++is($ua->proxy('http'), undef);
++is($ua->proxy('https'), undef);
++
++diag "---- badexample.org does not match .example.org ----";
++$ua = useragent(for_url => 'https://badexample.org');
++ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(https)]);
++is($ua->proxy('https'), 'http://sproxy:8080', 'should use proxy');
++
++diag "==== Selective proxy (many variables) ====";
++$ENV{http_proxy} = 'http://proxy:8080';
++$ENV{https_proxy} = 'http://sproxy:8080';
++# This one should be ignored in favour of https_proxy
++$ENV{HTTPS_PROXY} = 'http://not.preferred.proxy:3128';
++# These two should be merged
++$ENV{no_proxy} = '*.example.net,example.com';
++$ENV{NO_PROXY} = '.example.org';
++
++diag "---- Unspecified URL ----";
++$ua = useragent(for_url => undef);
++ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
++is($ua->proxy('http'), 'http://proxy:8080', 'should use proxy');
++is($ua->proxy('https'), 'http://sproxy:8080', 'should use CONNECT proxy');
++
++diag "---- Exact match for no_proxy ----";
++$ua = useragent(for_url => 'http://example.com');
++SKIP: {
++ skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
++ ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
++}
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
++is($ua->proxy('http'), undef);
++is($ua->proxy('https'), undef);
++
++diag "---- Subdomain of exact domain in no_proxy ----";
++$ua = useragent(for_url => 'http://sub.example.com');
++ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http)]);
++is($ua->proxy('http'), 'http://proxy:8080', 'should use proxy');
++
++diag "---- example.net matches *.example.net ----";
++$ua = useragent(for_url => 'https://example.net');
++SKIP: {
++ skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
++ ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
++}
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
++is($ua->proxy('http'), undef);
++is($ua->proxy('https'), undef);
++
++diag "---- sub.example.net matches *.example.net ----";
++$ua = useragent(for_url => 'https://sub.example.net');
++SKIP: {
++ skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
++ ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
++}
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
++is($ua->proxy('http'), undef);
++is($ua->proxy('https'), undef);
++
++diag "---- badexample.net does not match *.example.net ----";
++$ua = useragent(for_url => 'https://badexample.net');
++ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(https)]);
++is($ua->proxy('https'), 'http://sproxy:8080', 'should use proxy');
++
++diag "==== One but not the other ====\n";
++$ENV{http_proxy} = 'http://proxy:8080';
++delete $ENV{https_proxy};
++delete $ENV{HTTPS_PROXY};
++delete $ENV{no_proxy};
++delete $ENV{NO_PROXY};
++$ua = useragent(for_url => undef);
++ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
++is($ua->proxy('http'), 'http://proxy:8080', 'should use proxy');
++is($ua->proxy('https'), 'http://proxy:8080', 'should use proxy');
++
++delete $ENV{http_proxy};
++$ENV{https_proxy} = 'http://sproxy:8080';
++delete $ENV{HTTPS_PROXY};
++delete $ENV{no_proxy};
++delete $ENV{NO_PROXY};
++$ua = useragent(for_url => undef);
++ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
++is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
++is($ua->proxy('http'), 'http://sproxy:8080', 'should use proxy');
++is($ua->proxy('https'), 'http://sproxy:8080', 'should use proxy');
++
++done_testing;
diff -Nru ikiwiki-3.20141016.4/debian/patches/CVE-2019-9187-4.patch ikiwiki-3.20141016.4+deb8u1/debian/patches/CVE-2019-9187-4.patch
--- ikiwiki-3.20141016.4/debian/patches/CVE-2019-9187-4.patch 1970-01-01 10:00:00.000000000 +1000
+++ ikiwiki-3.20141016.4+deb8u1/debian/patches/CVE-2019-9187-4.patch 2019-03-07 17:35:45.000000000 +1100
@@ -0,0 +1,159 @@
+From 9a275b2f1846d7268c71a740975447e269383849 Mon Sep 17 00:00:00 2001
+From: Simon McVittie <smcv@debian.org>
+Date: Sun, 10 Feb 2019 16:56:41 +0000
+Subject: [PATCH] doc: Document security issues involving LWP::UserAgent
+
+Recommend the LWPx::ParanoidAgent module where appropriate.
+It is particularly important for openid, since unauthenticated users
+can control which URLs that plugin will contact. Conversely, it is
+non-critical for blogspam, since the URL to be contacted is under
+the wiki administrator's control.
+
+Signed-off-by: Simon McVittie <smcv@debian.org>
+---
+ doc/plugins/aggregate.mdwn | 4 ++++
+ doc/plugins/blogspam.mdwn | 2 ++
+ doc/plugins/openid.mdwn | 7 +++++--
+ doc/plugins/pinger.mdwn | 8 +++++---
+ doc/security.mdwn | 49 +++++++++++++++++++++++++++++++++++++++++++++
+ doc/tips/using_a_proxy.mdwn | 22 ++++++++++++++++++++
+ 6 files changed, 87 insertions(+), 5 deletions(-)
+ create mode 100644 doc/tips/using_a_proxy.mdwn
+
+--- a/doc/plugins/aggregate.mdwn
++++ b/doc/plugins/aggregate.mdwn
+@@ -11,6 +11,10 @@
+ one. Either the [[htmltidy]] or [[htmlbalance]] plugin is suggested, since
+ feeds can easily contain html problems, some of which these plugins can fix.
+
++Installing the [[!cpan LWPx::ParanoidAgent]] Perl module is strongly
++recommended. The [[!cpan LWP]] module can also be used, but is susceptible
++to server-side request forgery.
++
+ ## triggering aggregation
+
+ You will need to run ikiwiki periodically from a cron job, passing it the
+--- a/doc/plugins/blogspam.mdwn
++++ b/doc/plugins/blogspam.mdwn
+@@ -11,6 +11,8 @@
+ go to your Preferences page, and click the "Comment Moderation" button.
+
+ The plugin requires the [[!cpan JSON]] perl module.
++The [[!cpan LWPx::ParanoidAgent]] Perl module is recommended,
++although this plugin can also fall back to [[!cpan LWP]].
+
+ You can control how content is tested via the `blogspam_options` setting.
+ The list of options is [here](http://blogspam.net/api/testComment.html#options).
+--- a/doc/plugins/openid.mdwn
++++ b/doc/plugins/openid.mdwn
+@@ -7,8 +7,11 @@
+ The plugin needs the [[!cpan Net::OpenID::Consumer]] perl module.
+ Version 1.x is needed in order for OpenID v2 to work.
+
+-The [[!cpan LWPx::ParanoidAgent]] perl module is used if available, for
+-added security. Finally, the [[!cpan Crypt::SSLeay]] perl module is needed
++The [[!cpan LWPx::ParanoidAgent]] Perl module is strongly recommended.
++The [[!cpan LWP]] module can also be used, but is susceptible to
++server-side request forgery.
++
++The [[!cpan Crypt::SSLeay]] Perl module is needed
+ to support users entering "https" OpenID urls.
+
+ This plugin is enabled by default, but can be turned off if you want to
+--- a/doc/plugins/pinger.mdwn
++++ b/doc/plugins/pinger.mdwn
+@@ -10,9 +10,11 @@
+ To configure what URLs to ping, use the [[ikiwiki/directive/ping]]
+ [[ikiwiki/directive]].
+
+-The [[!cpan LWP]] perl module is used for pinging. Or the [[!cpan
+-LWPx::ParanoidAgent]] perl module is used if available, for added security.
+-Finally, the [[!cpan Crypt::SSLeay]] perl module is needed to support pinging
++The [[!cpan LWPx::ParanoidAgent]] Perl module is strongly recommended.
++The [[!cpan LWP]] module can also be used, but is susceptible
++to server-side request forgery.
++
++The [[!cpan Crypt::SSLeay]] perl module is needed to support pinging
+ "https" urls.
+
+ By default the pinger will try to ping a site for 15 seconds before timing
+--- a/doc/security.mdwn
++++ b/doc/security.mdwn
+@@ -526,3 +526,52 @@
+ able to attach images. Upgrading ImageMagick to a version where
+ CVE-2016-3714 has been fixed is also recommended, but at the time of
+ writing no such version is available.
++
++## Server-side request forgery via aggregate plugin
++
++The ikiwiki maintainers discovered that the [[plugins/aggregate]] plugin
++did not use [[!cpan LWPx::ParanoidAgent]]. On sites where the
++aggregate plugin is enabled, authorized wiki editors could tell ikiwiki
++to fetch potentially undesired URIs even if LWPx::ParanoidAgent was
++installed:
++
++* local files via `file:` URIs
++* other URI schemes that might be misused by attackers, such as `gopher:`
++* hosts that resolve to loopback IP addresses (127.x.x.x)
++* hosts that resolve to RFC 1918 IP addresses (192.168.x.x etc.)
++
++This could be used by an attacker to publish information that should not have
++been accessible, cause denial of service by requesting "tarpit" URIs that are
++slow to respond, or cause undesired side-effects if local web servers implement
++["unsafe"](https://tools.ietf.org/html/rfc7231#section-4.2.1) GET requests.
++([[!debcve CVE-2019-9187]])
++
++Additionally, if the LWPx::ParanoidAgent module was not installed, the
++[[plugins/blogspam]], [[plugins/openid]] and [[plugins/pinger]] plugins
++would fall back to [[!cpan LWP]], which is susceptible to similar attacks.
++This is unlikely to be a practical problem for the blogspam plugin because
++the URL it requests is under the control of the wiki administrator, but
++the openid plugin can request URLs controlled by unauthenticated remote
++users, and the pinger plugin can request URLs controlled by authorized
++wiki editors.
++
++This is addressed in ikiwiki 3.20190228 as follows, with the same fixes
++backported to Debian 9 in version 3.20170111.1:
++
++* URI schemes other than `http:` and `https:` are not accepted, preventing
++ access to `file:`, `gopher:`, etc.
++
++* If a proxy is [[configured in the ikiwiki setup file|tips/using_a_proxy]],
++ it is used for all outgoing `http:` and `https:` requests. In this case
++ the proxy is responsible for blocking any requests that are undesired,
++ including loopback or RFC 1918 addresses.
++
++* If a proxy is not configured, and LWPx::ParanoidAgent is installed,
++ it will be used. This prevents loopback and RFC 1918 IP addresses, and
++ sets a timeout to avoid denial of service via "tarpit" URIs.
++
++* Otherwise, the ordinary LWP user-agent will be used. This allows requests
++ to loopback and RFC 1918 IP addresses, and has less robust timeout
++ behaviour. We are not treating this as a vulnerability: if this
++ behaviour is not acceptable for your site, please make sure to install
++ LWPx::ParanoidAgent or disable the affected plugins.
+--- /dev/null
++++ b/doc/tips/using_a_proxy.mdwn
+@@ -0,0 +1,22 @@
++Some ikiwiki plugins make outgoing HTTP requests from the web server:
++
++* [[plugins/aggregate]] (to download Atom and RSS feeds)
++* [[plugins/blogspam]] (to check whether a comment or edit is spam)
++* [[plugins/openid]] (to authenticate users)
++* [[plugins/pinger]] (to ping other ikiwiki installations)
++
++If your ikiwiki installation cannot contact the Internet without going
++through a proxy, you can configure this in the [[setup file|setup]] by
++setting environment variables:
++
++ ENV:
++ http_proxy: "http://proxy.example.com:8080"
++ https_proxy: "http://proxy.example.com:8080"
++ # optional
++ no_proxy: ".example.com,www.example.org"
++
++Note that some plugins will use the configured proxy for all destinations,
++even if they are listed in `no_proxy`.
++
++To avoid server-side request forgery attacks, ensure that your proxy does
++not allow requests to addresses that are considered to be internal.
diff -Nru ikiwiki-3.20141016.4/debian/patches/series ikiwiki-3.20141016.4+deb8u1/debian/patches/series
--- ikiwiki-3.20141016.4/debian/patches/series 1970-01-01 10:00:00.000000000 +1000
+++ ikiwiki-3.20141016.4+deb8u1/debian/patches/series 2019-03-07 17:33:02.000000000 +1100
@@ -0,0 +1,5 @@
+CVE-2019-9187-1.patch
+CVE-2019-9187-2.patch
+CVE-2019-9187-3.patch
+CVE-2019-9187-4.patch
+abc
diff -Nru ikiwiki-3.20141016.4/doc/plugins/aggregate.mdwn ikiwiki-3.20141016.4+deb8u1/doc/plugins/aggregate.mdwn
--- ikiwiki-3.20141016.4/doc/plugins/aggregate.mdwn 2017-01-12 05:18:52.000000000 +1100
+++ ikiwiki-3.20141016.4+deb8u1/doc/plugins/aggregate.mdwn 2019-03-07 17:33:15.000000000 +1100
@@ -11,6 +11,10 @@
one. Either the [[htmltidy]] or [[htmlbalance]] plugin is suggested, since
feeds can easily contain html problems, some of which these plugins can fix.
+Installing the [[!cpan LWPx::ParanoidAgent]] Perl module is strongly
+recommended. The [[!cpan LWP]] module can also be used, but is susceptible
+to server-side request forgery.
+
## triggering aggregation
You will need to run ikiwiki periodically from a cron job, passing it the
diff -Nru ikiwiki-3.20141016.4/doc/plugins/blogspam.mdwn ikiwiki-3.20141016.4+deb8u1/doc/plugins/blogspam.mdwn
--- ikiwiki-3.20141016.4/doc/plugins/blogspam.mdwn 2017-01-12 05:18:52.000000000 +1100
+++ ikiwiki-3.20141016.4+deb8u1/doc/plugins/blogspam.mdwn 2019-03-07 17:33:15.000000000 +1100
@@ -11,6 +11,8 @@
go to your Preferences page, and click the "Comment Moderation" button.
The plugin requires the [[!cpan JSON]] perl module.
+The [[!cpan LWPx::ParanoidAgent]] Perl module is recommended,
+although this plugin can also fall back to [[!cpan LWP]].
You can control how content is tested via the `blogspam_options` setting.
The list of options is [here](http://blogspam.net/api/testComment.html#options).
diff -Nru ikiwiki-3.20141016.4/doc/plugins/openid.mdwn ikiwiki-3.20141016.4+deb8u1/doc/plugins/openid.mdwn
--- ikiwiki-3.20141016.4/doc/plugins/openid.mdwn 2017-01-12 05:18:52.000000000 +1100
+++ ikiwiki-3.20141016.4+deb8u1/doc/plugins/openid.mdwn 2019-03-07 17:33:15.000000000 +1100
@@ -7,8 +7,11 @@
The plugin needs the [[!cpan Net::OpenID::Consumer]] perl module.
Version 1.x is needed in order for OpenID v2 to work.
-The [[!cpan LWPx::ParanoidAgent]] perl module is used if available, for
-added security. Finally, the [[!cpan Crypt::SSLeay]] perl module is needed
+The [[!cpan LWPx::ParanoidAgent]] Perl module is strongly recommended.
+The [[!cpan LWP]] module can also be used, but is susceptible to
+server-side request forgery.
+
+The [[!cpan Crypt::SSLeay]] Perl module is needed
to support users entering "https" OpenID urls.
This plugin is enabled by default, but can be turned off if you want to
diff -Nru ikiwiki-3.20141016.4/doc/plugins/pinger.mdwn ikiwiki-3.20141016.4+deb8u1/doc/plugins/pinger.mdwn
--- ikiwiki-3.20141016.4/doc/plugins/pinger.mdwn 2017-01-12 05:18:52.000000000 +1100
+++ ikiwiki-3.20141016.4+deb8u1/doc/plugins/pinger.mdwn 2019-03-07 17:33:15.000000000 +1100
@@ -10,9 +10,11 @@
To configure what URLs to ping, use the [[ikiwiki/directive/ping]]
[[ikiwiki/directive]].
-The [[!cpan LWP]] perl module is used for pinging. Or the [[!cpan
-LWPx::ParanoidAgent]] perl module is used if available, for added security.
-Finally, the [[!cpan Crypt::SSLeay]] perl module is needed to support pinging
+The [[!cpan LWPx::ParanoidAgent]] Perl module is strongly recommended.
+The [[!cpan LWP]] module can also be used, but is susceptible
+to server-side request forgery.
+
+The [[!cpan Crypt::SSLeay]] perl module is needed to support pinging
"https" urls.
By default the pinger will try to ping a site for 15 seconds before timing
diff -Nru ikiwiki-3.20141016.4/doc/security.mdwn ikiwiki-3.20141016.4+deb8u1/doc/security.mdwn
--- ikiwiki-3.20141016.4/doc/security.mdwn 2017-01-12 05:18:52.000000000 +1100
+++ ikiwiki-3.20141016.4+deb8u1/doc/security.mdwn 2019-03-07 17:35:19.000000000 +1100
@@ -526,3 +526,52 @@
able to attach images. Upgrading ImageMagick to a version where
CVE-2016-3714 has been fixed is also recommended, but at the time of
writing no such version is available.
+
+## Server-side request forgery via aggregate plugin
+
+The ikiwiki maintainers discovered that the [[plugins/aggregate]] plugin
+did not use [[!cpan LWPx::ParanoidAgent]]. On sites where the
+aggregate plugin is enabled, authorized wiki editors could tell ikiwiki
+to fetch potentially undesired URIs even if LWPx::ParanoidAgent was
+installed:
+
+* local files via `file:` URIs
+* other URI schemes that might be misused by attackers, such as `gopher:`
+* hosts that resolve to loopback IP addresses (127.x.x.x)
+* hosts that resolve to RFC 1918 IP addresses (192.168.x.x etc.)
+
+This could be used by an attacker to publish information that should not have
+been accessible, cause denial of service by requesting "tarpit" URIs that are
+slow to respond, or cause undesired side-effects if local web servers implement
+["unsafe"](https://tools.ietf.org/html/rfc7231#section-4.2.1) GET requests.
+([[!debcve CVE-2019-9187]])
+
+Additionally, if the LWPx::ParanoidAgent module was not installed, the
+[[plugins/blogspam]], [[plugins/openid]] and [[plugins/pinger]] plugins
+would fall back to [[!cpan LWP]], which is susceptible to similar attacks.
+This is unlikely to be a practical problem for the blogspam plugin because
+the URL it requests is under the control of the wiki administrator, but
+the openid plugin can request URLs controlled by unauthenticated remote
+users, and the pinger plugin can request URLs controlled by authorized
+wiki editors.
+
+This is addressed in ikiwiki 3.20190228 as follows, with the same fixes
+backported to Debian 9 in version 3.20170111.1:
+
+* URI schemes other than `http:` and `https:` are not accepted, preventing
+ access to `file:`, `gopher:`, etc.
+
+* If a proxy is [[configured in the ikiwiki setup file|tips/using_a_proxy]],
+ it is used for all outgoing `http:` and `https:` requests. In this case
+ the proxy is responsible for blocking any requests that are undesired,
+ including loopback or RFC 1918 addresses.
+
+* If a proxy is not configured, and LWPx::ParanoidAgent is installed,
+ it will be used. This prevents loopback and RFC 1918 IP addresses, and
+ sets a timeout to avoid denial of service via "tarpit" URIs.
+
+* Otherwise, the ordinary LWP user-agent will be used. This allows requests
+ to loopback and RFC 1918 IP addresses, and has less robust timeout
+ behaviour. We are not treating this as a vulnerability: if this
+ behaviour is not acceptable for your site, please make sure to install
+ LWPx::ParanoidAgent or disable the affected plugins.
diff -Nru ikiwiki-3.20141016.4/doc/security.mdwn.rej ikiwiki-3.20141016.4+deb8u1/doc/security.mdwn.rej
--- ikiwiki-3.20141016.4/doc/security.mdwn.rej 1970-01-01 10:00:00.000000000 +1000
+++ ikiwiki-3.20141016.4+deb8u1/doc/security.mdwn.rej 2019-03-07 17:33:15.000000000 +1100
@@ -0,0 +1,55 @@
+--- doc/security.mdwn
++++ doc/security.mdwn
+@@ -611,3 +611,52 @@ This was fixed in ikiwiki 3.20170111, with fixes backported to Debian 8
+ in version 3.20141016.4.
+
+ ([[!debcve CVE-2017-0356]]/OVE-20170111-0001)
++
++## Server-side request forgery via aggregate plugin
++
++The ikiwiki maintainers discovered that the [[plugins/aggregate]] plugin
++did not use [[!cpan LWPx::ParanoidAgent]]. On sites where the
++aggregate plugin is enabled, authorized wiki editors could tell ikiwiki
++to fetch potentially undesired URIs even if LWPx::ParanoidAgent was
++installed:
++
++* local files via `file:` URIs
++* other URI schemes that might be misused by attackers, such as `gopher:`
++* hosts that resolve to loopback IP addresses (127.x.x.x)
++* hosts that resolve to RFC 1918 IP addresses (192.168.x.x etc.)
++
++This could be used by an attacker to publish information that should not have
++been accessible, cause denial of service by requesting "tarpit" URIs that are
++slow to respond, or cause undesired side-effects if local web servers implement
++["unsafe"](https://tools.ietf.org/html/rfc7231#section-4.2.1) GET requests.
++([[!debcve CVE-2019-9187]])
++
++Additionally, if the LWPx::ParanoidAgent module was not installed, the
++[[plugins/blogspam]], [[plugins/openid]] and [[plugins/pinger]] plugins
++would fall back to [[!cpan LWP]], which is susceptible to similar attacks.
++This is unlikely to be a practical problem for the blogspam plugin because
++the URL it requests is under the control of the wiki administrator, but
++the openid plugin can request URLs controlled by unauthenticated remote
++users, and the pinger plugin can request URLs controlled by authorized
++wiki editors.
++
++This is addressed in ikiwiki 3.20190228 as follows, with the same fixes
++backported to Debian 9 in version 3.20170111.1:
++
++* URI schemes other than `http:` and `https:` are not accepted, preventing
++ access to `file:`, `gopher:`, etc.
++
++* If a proxy is [[configured in the ikiwiki setup file|tips/using_a_proxy]],
++ it is used for all outgoing `http:` and `https:` requests. In this case
++ the proxy is responsible for blocking any requests that are undesired,
++ including loopback or RFC 1918 addresses.
++
++* If a proxy is not configured, and LWPx::ParanoidAgent is installed,
++ it will be used. This prevents loopback and RFC 1918 IP addresses, and
++ sets a timeout to avoid denial of service via "tarpit" URIs.
++
++* Otherwise, the ordinary LWP user-agent will be used. This allows requests
++ to loopback and RFC 1918 IP addresses, and has less robust timeout
++ behaviour. We are not treating this as a vulnerability: if this
++ behaviour is not acceptable for your site, please make sure to install
++ LWPx::ParanoidAgent or disable the affected plugins.
diff -Nru ikiwiki-3.20141016.4/doc/tips/using_a_proxy.mdwn ikiwiki-3.20141016.4+deb8u1/doc/tips/using_a_proxy.mdwn
--- ikiwiki-3.20141016.4/doc/tips/using_a_proxy.mdwn 1970-01-01 10:00:00.000000000 +1000
+++ ikiwiki-3.20141016.4+deb8u1/doc/tips/using_a_proxy.mdwn 2019-03-07 17:33:15.000000000 +1100
@@ -0,0 +1,22 @@
+Some ikiwiki plugins make outgoing HTTP requests from the web server:
+
+* [[plugins/aggregate]] (to download Atom and RSS feeds)
+* [[plugins/blogspam]] (to check whether a comment or edit is spam)
+* [[plugins/openid]] (to authenticate users)
+* [[plugins/pinger]] (to ping other ikiwiki installations)
+
+If your ikiwiki installation cannot contact the Internet without going
+through a proxy, you can configure this in the [[setup file|setup]] by
+setting environment variables:
+
+ ENV:
+ http_proxy: "http://proxy.example.com:8080"
+ https_proxy: "http://proxy.example.com:8080"
+ # optional
+ no_proxy: ".example.com,www.example.org"
+
+Note that some plugins will use the configured proxy for all destinations,
+even if they are listed in `no_proxy`.
+
+To avoid server-side request forgery attacks, ensure that your proxy does
+not allow requests to addresses that are considered to be internal.
diff -Nru ikiwiki-3.20141016.4/IkiWiki/Plugin/aggregate.pm ikiwiki-3.20141016.4+deb8u1/IkiWiki/Plugin/aggregate.pm
--- ikiwiki-3.20141016.4/IkiWiki/Plugin/aggregate.pm 2017-01-12 05:18:52.000000000 +1100
+++ ikiwiki-3.20141016.4+deb8u1/IkiWiki/Plugin/aggregate.pm 2019-03-07 17:32:54.000000000 +1100
@@ -513,7 +513,10 @@
}
$feed->{feedurl}=pop @urls;
}
- my $ua=useragent();
+ # Using the for_url parameter makes sure we crash if used
+ # with an older IkiWiki.pm that didn't automatically try
+ # to use LWPx::ParanoidAgent.
+ my $ua=useragent(for_url => $feed->{feedurl});
my $res=URI::Fetch->fetch($feed->{feedurl}, UserAgent=>$ua);
if (! $res) {
$feed->{message}=URI::Fetch->errstr;
diff -Nru ikiwiki-3.20141016.4/IkiWiki/Plugin/blogspam.pm ikiwiki-3.20141016.4+deb8u1/IkiWiki/Plugin/blogspam.pm
--- ikiwiki-3.20141016.4/IkiWiki/Plugin/blogspam.pm 2017-01-12 05:18:52.000000000 +1100
+++ ikiwiki-3.20141016.4+deb8u1/IkiWiki/Plugin/blogspam.pm 2019-03-07 17:32:54.000000000 +1100
@@ -57,18 +57,10 @@
};
error $@ if $@;
- eval q{use LWPx::ParanoidAgent};
- if (!$@) {
- $client=LWPx::ParanoidAgent->new(agent => $config{useragent});
- }
- else {
- eval q{use LWP};
- if ($@) {
- error $@;
- return;
- }
- $client=useragent();
- }
+ # Using the for_url parameter makes sure we crash if used
+ # with an older IkiWiki.pm that didn't automatically try
+ # to use LWPx::ParanoidAgent.
+ $client=useragent(for_url => $config{blogspam_server});
}
sub checkcontent (@) {
diff -Nru ikiwiki-3.20141016.4/IkiWiki/Plugin/openid.pm ikiwiki-3.20141016.4+deb8u1/IkiWiki/Plugin/openid.pm
--- ikiwiki-3.20141016.4/IkiWiki/Plugin/openid.pm 2017-01-12 05:18:52.000000000 +1100
+++ ikiwiki-3.20141016.4+deb8u1/IkiWiki/Plugin/openid.pm 2019-03-07 17:32:54.000000000 +1100
@@ -237,14 +237,10 @@
eval q{use Net::OpenID::Consumer};
error($@) if $@;
- my $ua;
- eval q{use LWPx::ParanoidAgent};
- if (! $@) {
- $ua=LWPx::ParanoidAgent->new(agent => $config{useragent});
- }
- else {
- $ua=useragent();
- }
+ # We pass the for_url parameter, even though it's undef, because
+ # that will make sure we crash if used with an older IkiWiki.pm
+ # that didn't automatically try to use LWPx::ParanoidAgent.
+ my $ua=useragent(for_url => undef);
# Store the secret in the session.
my $secret=$session->param("openid_secret");
diff -Nru ikiwiki-3.20141016.4/IkiWiki/Plugin/pinger.pm ikiwiki-3.20141016.4+deb8u1/IkiWiki/Plugin/pinger.pm
--- ikiwiki-3.20141016.4/IkiWiki/Plugin/pinger.pm 2017-01-12 05:18:52.000000000 +1100
+++ ikiwiki-3.20141016.4+deb8u1/IkiWiki/Plugin/pinger.pm 2019-03-07 17:32:54.000000000 +1100
@@ -70,17 +70,16 @@
eval q{use Net::INET6Glue::INET_is_INET6}; # may not be available
my $ua;
- eval q{use LWPx::ParanoidAgent};
- if (!$@) {
- $ua=LWPx::ParanoidAgent->new(agent => $config{useragent});
- }
- else {
- eval q{use LWP};
- if ($@) {
- debug(gettext("LWP not found, not pinging"));
- return;
- }
- $ua=useragent();
+ eval {
+ # We pass the for_url parameter, even though it's
+ # undef, because that will make sure we crash if used
+ # with an older IkiWiki.pm that didn't automatically
+ # try to use LWPx::ParanoidAgent.
+ $ua=useragent(for_url => undef);
+ };
+ if ($@) {
+ debug(gettext("LWP not found, not pinging").": $@");
+ return;
}
$ua->timeout($config{pinger_timeout} || 15);
diff -Nru ikiwiki-3.20141016.4/IkiWiki.pm ikiwiki-3.20141016.4+deb8u1/IkiWiki.pm
--- ikiwiki-3.20141016.4/IkiWiki.pm 2017-01-12 05:18:52.000000000 +1100
+++ ikiwiki-3.20141016.4+deb8u1/IkiWiki.pm 2019-03-07 17:32:54.000000000 +1100
@@ -2367,12 +2367,131 @@
$autofiles{$file}{generator}=$generator;
}
-sub useragent () {
- return LWP::UserAgent->new(
- cookie_jar => $config{cookiejar},
- env_proxy => 1, # respect proxy env vars
+sub useragent (@) {
+ my %params = @_;
+ my $for_url = delete $params{for_url};
+ # Fail safe, in case a plugin calling this function is relying on
+ # a future parameter to make the UA more strict
+ foreach my $key (keys %params) {
+ error "Internal error: useragent(\"$key\" => ...) not understood";
+ }
+
+ eval q{use LWP};
+ error($@) if $@;
+
+ my %args = (
agent => $config{useragent},
+ cookie_jar => $config{cookiejar},
+ env_proxy => 0,
+ protocols_allowed => [qw(http https)],
);
+ my %proxies;
+
+ if (defined $for_url) {
+ # We know which URL we're going to fetch, so we can choose
+ # whether it's going to go through a proxy or not.
+ #
+ # We reimplement http_proxy, https_proxy and no_proxy here, so
+ # that we are not relying on LWP implementing them exactly the
+ # same way we do.
+
+ eval q{use URI};
+ error($@) if $@;
+
+ my $proxy;
+ my $uri = URI->new($for_url);
+
+ if ($uri->scheme eq 'http') {
+ $proxy = $ENV{http_proxy};
+ # HTTP_PROXY is deliberately not implemented
+ # because the HTTP_* namespace is also used by CGI
+ }
+ elsif ($uri->scheme eq 'https') {
+ $proxy = $ENV{https_proxy};
+ $proxy = $ENV{HTTPS_PROXY} unless defined $proxy;
+ }
+ else {
+ $proxy = undef;
+ }
+
+ foreach my $var (qw(no_proxy NO_PROXY)) {
+ my $no_proxy = $ENV{$var};
+ if (defined $no_proxy) {
+ foreach my $domain (split /\s*,\s*/, $no_proxy) {
+ if ($domain =~ s/^\*?\.//) {
+ # no_proxy="*.example.com" or
+ # ".example.com": match suffix
+ # against .example.com
+ if ($uri->host =~ m/(^|\.)\Q$domain\E$/i) {
+ $proxy = undef;
+ }
+ }
+ else {
+ # no_proxy="example.com":
+ # match exactly example.com
+ if (lc $uri->host eq lc $domain) {
+ $proxy = undef;
+ }
+ }
+ }
+ }
+ }
+
+ if (defined $proxy) {
+ $proxies{$uri->scheme} = $proxy;
+ # Paranoia: make sure we can't bypass the proxy
+ $args{protocols_allowed} = [$uri->scheme];
+ }
+ }
+ else {
+ # The plugin doesn't know yet which URL(s) it's going to
+ # fetch, so we have to make some conservative assumptions.
+ my $http_proxy = $ENV{http_proxy};
+ my $https_proxy = $ENV{https_proxy};
+ $https_proxy = $ENV{HTTPS_PROXY} unless defined $https_proxy;
+
+ # We don't respect no_proxy here: if we are not using the
+ # paranoid user-agent, then we need to give the proxy the
+ # opportunity to reject undesirable requests.
+
+ # If we have one, we need the other: otherwise, neither
+ # LWPx::ParanoidAgent nor the proxy would have the
+ # opportunity to filter requests for the other protocol.
+ if (defined $https_proxy && defined $http_proxy) {
+ %proxies = (http => $http_proxy, https => $https_proxy);
+ }
+ elsif (defined $https_proxy) {
+ %proxies = (http => $https_proxy, https => $https_proxy);
+ }
+ elsif (defined $http_proxy) {
+ %proxies = (http => $http_proxy, https => $http_proxy);
+ }
+
+ }
+
+ if (scalar keys %proxies) {
+ # The configured proxy is responsible for deciding which
+ # URLs are acceptable to fetch and which URLs are not.
+ my $ua = LWP::UserAgent->new(%args);
+ foreach my $scheme (@{$ua->protocols_allowed}) {
+ unless ($proxies{$scheme}) {
+ error "internal error: $scheme is allowed but has no proxy";
+ }
+ }
+ # We can't pass the proxies in %args because that only
+ # works since LWP 6.24.
+ foreach my $scheme (keys %proxies) {
+ $ua->proxy($scheme, $proxies{$scheme});
+ }
+ return $ua;
+ }
+
+ eval q{use LWPx::ParanoidAgent};
+ if ($@) {
+ print STDERR "warning: installing LWPx::ParanoidAgent is recommended\n";
+ return LWP::UserAgent->new(%args);
+ }
+ return LWPx::ParanoidAgent->new(%args);
}
sub sortspec_translate ($$) {
diff -Nru ikiwiki-3.20141016.4/.pc/applied-patches ikiwiki-3.20141016.4+deb8u1/.pc/applied-patches
--- ikiwiki-3.20141016.4/.pc/applied-patches 1970-01-01 10:00:00.000000000 +1000
+++ ikiwiki-3.20141016.4+deb8u1/.pc/applied-patches 2019-03-07 17:33:15.000000000 +1100
@@ -0,0 +1,4 @@
+CVE-2019-9187-1.patch
+CVE-2019-9187-2.patch
+CVE-2019-9187-3.patch
+CVE-2019-9187-4.patch
diff -Nru ikiwiki-3.20141016.4/.pc/CVE-2019-9187-1.patch/IkiWiki.pm ikiwiki-3.20141016.4+deb8u1/.pc/CVE-2019-9187-1.patch/IkiWiki.pm
--- ikiwiki-3.20141016.4/.pc/CVE-2019-9187-1.patch/IkiWiki.pm 1970-01-01 10:00:00.000000000 +1000
+++ ikiwiki-3.20141016.4+deb8u1/.pc/CVE-2019-9187-1.patch/IkiWiki.pm 2019-03-07 17:32:28.000000000 +1100
@@ -0,0 +1,3028 @@
+#!/usr/bin/perl
+
+package IkiWiki;
+
+use warnings;
+use strict;
+use Encode;
+use URI::Escape q{uri_escape_utf8};
+use POSIX ();
+use Storable;
+use open qw{:utf8 :std};
+
+use vars qw{%config %links %oldlinks %pagemtime %pagectime %pagecase
+ %pagestate %wikistate %renderedfiles %oldrenderedfiles
+ %pagesources %delpagesources %destsources %depends %depends_simple
+ @mass_depends %hooks %forcerebuild %loaded_plugins %typedlinks
+ %oldtypedlinks %autofiles @underlayfiles $lastrev $phase};
+
+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 strftime_utf8 will_render gettext ngettext urlto targetpage
+ add_underlay pagetitle titlepage linkpage newpagefile
+ 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
+our $version='unknown'; # VERSION_AUTOREPLACE done by Makefile, DNE
+our $installdir='/usr'; # INSTALLDIR_AUTOREPLACE done by Makefile, DNE
+
+# Page dependency types.
+our $DEPEND_CONTENT=1;
+our $DEPEND_PRESENCE=2;
+our $DEPEND_LINKS=4;
+
+# Phases of processing.
+sub PHASE_SCAN () { 0 }
+sub PHASE_RENDER () { 1 }
+$phase = PHASE_SCAN;
+
+# Optimisation.
+use Memoize;
+memoize("abs2rel");
+memoize("sortspec_translate");
+memoize("pagespec_translate");
+memoize("template_file");
+
+sub getsetup () {
+ wikiname => {
+ type => "string",
+ default => "wiki",
+ description => "name of the wiki",
+ safe => 1,
+ rebuild => 1,
+ },
+ adminemail => {
+ type => "string",
+ default => undef,
+ example => 'me@example.com',
+ description => "contact email for wiki",
+ safe => 1,
+ rebuild => 0,
+ },
+ adminuser => {
+ type => "string",
+ default => [],
+ description => "users who are wiki admins",
+ safe => 1,
+ rebuild => 0,
+ },
+ banned_users => {
+ type => "string",
+ default => [],
+ description => "users who are banned from the wiki",
+ safe => 1,
+ rebuild => 0,
+ },
+ srcdir => {
+ type => "string",
+ default => undef,
+ example => "$ENV{HOME}/wiki",
+ description => "where the source of the wiki is located",
+ safe => 0, # path
+ rebuild => 1,
+ },
+ destdir => {
+ type => "string",
+ default => undef,
+ example => "/var/www/wiki",
+ description => "where to build the wiki",
+ safe => 0, # path
+ rebuild => 1,
+ },
+ url => {
+ type => "string",
+ default => '',
+ example => "http://example.com/wiki",
+ description => "base url to the wiki",
+ safe => 1,
+ rebuild => 1,
+ },
+ cgiurl => {
+ type => "string",
+ default => '',
+ example => "http://example.com/wiki/ikiwiki.cgi",
+ description => "url to the ikiwiki.cgi",
+ safe => 1,
+ rebuild => 1,
+ },
+ reverse_proxy => {
+ type => "boolean",
+ default => 0,
+ description => "do not adjust cgiurl if CGI is accessed via different URL",
+ advanced => 0,
+ safe => 1,
+ rebuild => 0, # only affects CGI requests
+ },
+ cgi_wrapper => {
+ type => "string",
+ default => '',
+ example => "/var/www/wiki/ikiwiki.cgi",
+ description => "filename of cgi wrapper to generate",
+ safe => 0, # file
+ rebuild => 0,
+ },
+ cgi_wrappermode => {
+ type => "string",
+ default => '06755',
+ description => "mode for cgi_wrapper (can safely be made suid)",
+ safe => 0,
+ rebuild => 0,
+ },
+ 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 => '',
+ description => "rcs backend to use",
+ safe => 0, # don't allow overriding
+ rebuild => 0,
+ },
+ default_plugins => {
+ type => "internal",
+ default => [qw{mdwn link inline meta htmlscrubber passwordauth
+ openid signinedit lockedit conditional
+ recentchanges parentlinks editpage
+ templatebody}],
+ description => "plugins to enable by default",
+ safe => 0,
+ rebuild => 1,
+ },
+ add_plugins => {
+ type => "string",
+ default => [],
+ description => "plugins to add to the default configuration",
+ safe => 1,
+ rebuild => 1,
+ },
+ disable_plugins => {
+ type => "string",
+ default => [],
+ description => "plugins to disable",
+ safe => 1,
+ rebuild => 1,
+ },
+ templatedir => {
+ type => "string",
+ default => "$installdir/share/ikiwiki/templates",
+ description => "additional directory to search for template files",
+ advanced => 1,
+ safe => 0, # path
+ rebuild => 1,
+ },
+ underlaydir => {
+ type => "string",
+ default => "$installdir/share/ikiwiki/basewiki",
+ description => "base wiki source location",
+ advanced => 1,
+ safe => 0, # path
+ rebuild => 0,
+ },
+ underlaydirbase => {
+ type => "internal",
+ default => "$installdir/share/ikiwiki",
+ description => "parent directory containing additional underlays",
+ safe => 0,
+ rebuild => 0,
+ },
+ wrappers => {
+ type => "internal",
+ default => [],
+ description => "wrappers to generate",
+ safe => 0,
+ rebuild => 0,
+ },
+ underlaydirs => {
+ type => "internal",
+ default => [],
+ description => "additional underlays to use",
+ safe => 0,
+ rebuild => 0,
+ },
+ verbose => {
+ type => "boolean",
+ example => 1,
+ description => "display verbose messages?",
+ safe => 1,
+ rebuild => 0,
+ },
+ syslog => {
+ type => "boolean",
+ example => 1,
+ description => "log to syslog?",
+ safe => 1,
+ rebuild => 0,
+ },
+ usedirs => {
+ type => "boolean",
+ default => 1,
+ description => "create output files named page/index.html?",
+ safe => 0, # changing requires manual transition
+ rebuild => 1,
+ },
+ prefix_directives => {
+ type => "boolean",
+ default => 1,
+ description => "use '!'-prefixed preprocessor directives?",
+ safe => 0, # changing requires manual transition
+ rebuild => 1,
+ },
+ indexpages => {
+ type => "boolean",
+ default => 0,
+ description => "use page/index.mdwn source files",
+ safe => 1,
+ rebuild => 1,
+ },
+ discussion => {
+ type => "boolean",
+ default => 1,
+ description => "enable Discussion pages?",
+ safe => 1,
+ rebuild => 1,
+ },
+ discussionpage => {
+ type => "string",
+ default => gettext("Discussion"),
+ description => "name of Discussion pages",
+ safe => 1,
+ rebuild => 1,
+ },
+ html5 => {
+ type => "boolean",
+ default => 0,
+ description => "generate HTML5?",
+ advanced => 0,
+ safe => 1,
+ rebuild => 1,
+ },
+ sslcookie => {
+ type => "boolean",
+ default => 0,
+ description => "only send cookies over SSL connections?",
+ advanced => 1,
+ safe => 1,
+ rebuild => 0,
+ },
+ default_pageext => {
+ type => "string",
+ default => "mdwn",
+ description => "extension to use for new pages",
+ safe => 0, # not sanitized
+ rebuild => 0,
+ },
+ htmlext => {
+ type => "string",
+ default => "html",
+ description => "extension to use for html files",
+ safe => 0, # not sanitized
+ rebuild => 1,
+ },
+ timeformat => {
+ type => "string",
+ default => '%c',
+ description => "strftime format string to display date",
+ advanced => 1,
+ safe => 1,
+ rebuild => 1,
+ },
+ locale => {
+ type => "string",
+ default => undef,
+ example => "en_US.UTF-8",
+ description => "UTF-8 locale to use",
+ advanced => 1,
+ safe => 0,
+ rebuild => 1,
+ },
+ userdir => {
+ type => "string",
+ default => "",
+ example => "users",
+ description => "put user pages below specified page",
+ safe => 1,
+ rebuild => 1,
+ },
+ numbacklinks => {
+ type => "integer",
+ default => 10,
+ description => "how many backlinks to show before hiding excess (0 to show all)",
+ safe => 1,
+ rebuild => 1,
+ },
+ hardlink => {
+ type => "boolean",
+ default => 0,
+ description => "attempt to hardlink source files? (optimisation for large files)",
+ advanced => 1,
+ safe => 0, # paranoia
+ rebuild => 0,
+ },
+ umask => {
+ type => "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,
+ },
+ wrappergroup => {
+ type => "string",
+ example => "ikiwiki",
+ description => "group for wrappers to run in",
+ advanced => 1,
+ safe => 0, # paranoia
+ rebuild => 0,
+ },
+ libdir => {
+ type => "string",
+ default => "",
+ example => "$ENV{HOME}/.ikiwiki/",
+ description => "extra library and plugin directory",
+ advanced => 1,
+ safe => 0, # directory
+ rebuild => 0,
+ },
+ ENV => {
+ type => "string",
+ default => {},
+ description => "environment variables",
+ safe => 0, # paranoia
+ rebuild => 0,
+ },
+ timezone => {
+ type => "string",
+ default => "",
+ example => "US/Eastern",
+ description => "time zone name",
+ safe => 1,
+ rebuild => 1,
+ },
+ include => {
+ type => "string",
+ default => undef,
+ example => '^\.htaccess$',
+ description => "regexp of normally excluded files to include",
+ advanced => 1,
+ safe => 0, # regexp
+ rebuild => 1,
+ },
+ exclude => {
+ type => "string",
+ default => undef,
+ example => '^(*\.private|Makefile)$',
+ description => "regexp of files that should be skipped",
+ advanced => 1,
+ safe => 0, # regexp
+ rebuild => 1,
+ },
+ wiki_file_prune_regexps => {
+ type => "internal",
+ default => [qr/(^|\/)\.\.(\/|$)/, qr/^\//, qr/^\./, qr/\/\./,
+ qr/\.x?html?$/, qr/\.ikiwiki-new$/,
+ qr/(^|\/).svn\//, qr/.arch-ids\//, qr/{arch}\//,
+ qr/(^|\/)_MTN\//, qr/(^|\/)_darcs\//,
+ qr/(^|\/)CVS\//, qr/\.dpkg-tmp$/],
+ description => "regexps of source files to ignore",
+ safe => 0,
+ rebuild => 1,
+ },
+ wiki_file_chars => {
+ type => "string",
+ description => "specifies the characters that are allowed in source filenames",
+ default => "-[:alnum:]+/.:_",
+ safe => 0,
+ rebuild => 1,
+ },
+ wiki_file_regexp => {
+ type => "internal",
+ description => "regexp of legal source files",
+ safe => 0,
+ rebuild => 1,
+ },
+ web_commit_regexp => {
+ type => "internal",
+ default => qr/^web commit (by (.*?(?=: |$))|from ([0-9a-fA-F:.]+[0-9a-fA-F])):?(.*)/,
+ description => "regexp to parse web commits from logs",
+ safe => 0,
+ rebuild => 0,
+ },
+ cgi => {
+ type => "internal",
+ default => 0,
+ description => "run as a cgi",
+ safe => 0,
+ rebuild => 0,
+ },
+ cgi_disable_uploads => {
+ type => "internal",
+ default => 1,
+ description => "whether CGI should accept file uploads",
+ safe => 0,
+ rebuild => 0,
+ },
+ post_commit => {
+ type => "internal",
+ default => 0,
+ description => "run as a post-commit hook",
+ safe => 0,
+ rebuild => 0,
+ },
+ rebuild => {
+ type => "internal",
+ default => 0,
+ description => "running in rebuild mode",
+ safe => 0,
+ rebuild => 0,
+ },
+ setup => {
+ type => "internal",
+ default => undef,
+ description => "running in setup mode",
+ safe => 0,
+ rebuild => 0,
+ },
+ clean => {
+ type => "internal",
+ default => 0,
+ description => "running in clean mode",
+ safe => 0,
+ rebuild => 0,
+ },
+ refresh => {
+ type => "internal",
+ default => 0,
+ description => "running in refresh mode",
+ safe => 0,
+ rebuild => 0,
+ },
+ test_receive => {
+ type => "internal",
+ default => 0,
+ description => "running in receive test mode",
+ safe => 0,
+ rebuild => 0,
+ },
+ wrapper_background_command => {
+ type => "internal",
+ default => '',
+ description => "background shell command to run",
+ safe => 0,
+ rebuild => 0,
+ },
+ gettime => {
+ type => "internal",
+ description => "running in gettime mode",
+ safe => 0,
+ rebuild => 0,
+ },
+ w3mmode => {
+ type => "internal",
+ default => 0,
+ description => "running in w3mmode",
+ safe => 0,
+ rebuild => 0,
+ },
+ wikistatedir => {
+ type => "internal",
+ default => undef,
+ description => "path to the .ikiwiki directory holding ikiwiki state",
+ safe => 0,
+ rebuild => 0,
+ },
+ setupfile => {
+ type => "internal",
+ default => undef,
+ description => "path to setup file",
+ safe => 0,
+ rebuild => 0,
+ },
+ setuptype => {
+ type => "internal",
+ default => "Yaml",
+ description => "perl class to use to dump 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,
+ },
+ 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 => "ikiwiki/$version",
+ 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 () {
+ my %s=getsetup();
+ my @ret;
+ foreach my $key (keys %s) {
+ push @ret, $key, $s{$key}->{default};
+ }
+ 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}) {
+ $ENV{LANG} = $ENV{LC_ALL};
+ delete $ENV{LC_ALL};
+ }
+ if (defined $config{locale}) {
+ if (POSIX::setlocale(&POSIX::LC_ALL, $config{locale})) {
+ $ENV{LANG}=$config{locale};
+ define_gettext();
+ }
+ }
+
+ 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}}) {
+ $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}};
+ error($@) if $@;
+ $config{srcdir}=possibly_foolish_untaint(abs_path($config{srcdir}));
+ $config{destdir}=possibly_foolish_untaint(abs_path($config{destdir}));
+ $config{cgiurl}="file:///\$LIB/ikiwiki-w3m.cgi/".$config{cgiurl}
+ unless $config{cgiurl} =~ m!file:///!;
+ $config{url}="file://".$config{destdir};
+ }
+
+ 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 eq 'https' &&
+ $baseurl->scheme eq 'http') {
+ # We assume that the same content is available
+ # over both http and https, because if it
+ # wasn't, accessing the static content
+ # from the CGI would be mixed-content,
+ # which would be a security flaw.
+
+ if ($cgiurl->authority ne $baseurl->authority) {
+ # use protocol-relative URL for
+ # static content
+ $local_url = "$config{url}/";
+ $local_url =~ s{^http://}{//};
+ }
+ # else use host-relative URL for static content
+
+ # either way, CGI needs to be absolute
+ $local_cgiurl = $config{cgiurl};
+ }
+ elsif ($cgiurl->scheme ne $baseurl->scheme) {
+ # too far apart, fall back to absolute URLs
+ $local_url = "$config{url}/";
+ $local_cgiurl = $config{cgiurl};
+ }
+ elsif ($cgiurl->authority ne $baseurl->authority) {
+ # slightly too far apart, fall back to
+ # protocol-relative URLs
+ $local_url = "$config{url}/";
+ $local_url =~ s{^https?://}{//};
+ $local_cgiurl = $config{cgiurl};
+ $local_cgiurl =~ s{^https?://}{//};
+ }
+ # else keep host-relative URLs
+ }
+
+ $local_url =~ s{//$}{/};
+ }
+ else {
+ $local_cgiurl = $config{cgiurl};
+ }
+
+ $config{wikistatedir}="$config{srcdir}/.ikiwiki"
+ unless exists $config{wikistatedir} && defined $config{wikistatedir};
+
+ if (defined $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->() });
+
+ return 1;
+}
+
+sub listplugins () {
+ my %ret;
+
+ foreach my $dir (@INC, $config{libdir}) {
+ next unless defined $dir && length $dir;
+ foreach my $file (glob("$dir/IkiWiki/Plugin/*.pm")) {
+ my ($plugin)=$file=~/.*\/(.*)\.pm$/;
+ $ret{$plugin}=1;
+ }
+ }
+ foreach my $dir ($config{libdir}, "$installdir/lib/ikiwiki") {
+ next unless defined $dir && length $dir;
+ foreach my $file (glob("$dir/plugins/*")) {
+ $ret{basename($file)}=1 if -x $file;
+ }
+ }
+
+ return keys %ret;
+}
+
+sub loadplugins () {
+ if (defined $config{libdir} && length $config{libdir}) {
+ unshift @INC, possibly_foolish_untaint($config{libdir});
+ }
+
+ foreach my $plugin (@{$config{default_plugins}}, @{$config{add_plugins}}) {
+ loadplugin($plugin);
+ }
+
+ if ($config{rcs}) {
+ if (exists $hooks{rcs}) {
+ error(gettext("cannot use multiple rcs plugins"));
+ }
+ loadplugin($config{rcs});
+ }
+ if (! exists $hooks{rcs}) {
+ loadplugin("norcs");
+ }
+
+ run_hooks(getopt => sub { shift->() });
+ if (grep /^-/, @ARGV) {
+ print STDERR "Unknown option (or missing parameter): $_\n"
+ foreach grep /^-/, @ARGV;
+ usage();
+ }
+
+ return 1;
+}
+
+sub loadplugin ($;$) {
+ my $plugin=shift;
+ my $force=shift;
+
+ return if ! $force && grep { $_ eq $plugin} @{$config{disable_plugins}};
+
+ foreach my $dir (defined $config{libdir} ? possibly_foolish_untaint($config{libdir}) : undef,
+ "$installdir/lib/ikiwiki") {
+ if (defined $dir && -x "$dir/plugins/$plugin") {
+ 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;
+ }
+ }
+
+ my $mod="IkiWiki::Plugin::".possibly_foolish_untaint($plugin);
+ eval qq{use $mod};
+ if ($@) {
+ error("Failed to load plugin $mod: $@");
+ }
+ $loaded_plugins{$plugin}=1;
+ return 1;
+}
+
+sub error ($;$) {
+ my $message=shift;
+ my $cleaner=shift;
+ log_message('err' => $message) if $config{syslog};
+ if (defined $cleaner) {
+ $cleaner->();
+ }
+ die $message."\n";
+}
+
+sub debug ($) {
+ return unless $config{verbose};
+ return log_message(debug => @_);
+}
+
+my $log_open=0;
+my $log_failed=0;
+sub log_message ($$) {
+ my $type=shift;
+
+ if ($config{syslog}) {
+ require Sys::Syslog;
+ if (! $log_open) {
+ Sys::Syslog::setlogsock('unix');
+ Sys::Syslog::openlog('ikiwiki', '', 'user');
+ $log_open=1;
+ }
+ 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";
+ }
+ else {
+ return print STDERR "@_\n";
+ }
+}
+
+sub possibly_foolish_untaint ($) {
+ my $tainted=shift;
+ my ($untainted)=$tainted=~/(.*)/s;
+ return $untainted;
+}
+
+sub basename ($) {
+ my $file=shift;
+
+ $file=~s!.*/+!!;
+ return $file;
+}
+
+sub dirname ($) {
+ my $file=shift;
+
+ $file=~s!/*[^/]+$!!;
+ return $file;
+}
+
+sub isinternal ($) {
+ my $page=shift;
+ return exists $pagesources{$page} &&
+ $pagesources{$page} =~ /\._([^.]+)$/;
+}
+
+sub pagetype ($) {
+ my $file=shift;
+
+ if ($file =~ /\.([^.]+)$/) {
+ return $1 if exists $hooks{htmlize}{$1};
+ }
+ my $base=basename($file);
+ if (exists $hooks{htmlize}{$base} &&
+ $hooks{htmlize}{$base}{noextension}) {
+ return $base;
+ }
+ return;
+}
+
+my %pagename_cache;
+
+sub pagename ($) {
+ my $file=shift;
+
+ if (exists $pagename_cache{$file}) {
+ return $pagename_cache{$file};
+ }
+
+ my $type=pagetype($file);
+ my $page=$file;
+ $page=~s/\Q.$type\E*$//
+ if defined $type && !$hooks{htmlize}{$type}{keepextension}
+ && !$hooks{htmlize}{$type}{noextension};
+ if ($config{indexpages} && $page=~/(.*)\/index$/) {
+ $page=$1;
+ }
+
+ $pagename_cache{$file} = $page;
+ 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;
+ my $filename=shift;
+
+ if (defined $filename) {
+ return $page."/".$filename.".".$ext;
+ }
+ elsif (! $config{usedirs} || $page eq 'index') {
+ return $page.".".$ext;
+ }
+ else {
+ return $page."/index.".$ext;
+ }
+}
+
+sub htmlpage ($) {
+ my $page=shift;
+
+ return targetpage($page, $config{htmlext});
+}
+
+sub srcfile_stat {
+ my $file=shift;
+ my $nothrow=shift;
+
+ return "$config{srcdir}/$file", stat(_) if -e "$config{srcdir}/$file";
+ foreach my $dir (@{$config{underlaydirs}}, $config{underlaydir}) {
+ return "$dir/$file", stat(_) if -e "$dir/$file";
+ }
+ error("internal error: $file cannot be found in $config{srcdir} or underlay") unless $nothrow;
+ return;
+}
+
+sub srcfile ($;$) {
+ return (srcfile_stat(@_))[0];
+}
+
+sub add_literal_underlay ($) {
+ my $dir=shift;
+
+ 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;
+}
+
+sub readfile ($;$$) {
+ my $file=shift;
+ my $binary=shift;
+ my $wantfd=shift;
+
+ if (-l $file) {
+ error("cannot read a symlink ($file)");
+ }
+
+ local $/=undef;
+ open (my $in, "<", $file) || error("failed to read $file: $!");
+ 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;
+}
+
+sub prep_writefile ($$) {
+ my $file=shift;
+ my $destdir=shift;
+
+ my $test=$file;
+ while (length $test) {
+ 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);
+ }
+
+ my $dir=dirname("$destdir/$file");
+ if (! -d $dir) {
+ my $d="";
+ foreach my $s (split(m!/+!, $dir)) {
+ $d.="$s/";
+ if (! -d $d) {
+ mkdir($d) || error("failed to create directory $d: $!");
+ }
+ }
+ }
+
+ return 1;
+}
+
+sub writefile ($$$;$$) {
+ my $file=shift; # can include subdirs
+ my $destdir=shift; # directory to put file in
+ my $content=shift;
+ my $binary=shift;
+ my $writer=shift;
+
+ prep_writefile($file, $destdir);
+
+ my $newfile="$destdir/$file.ikiwiki-new";
+ if (-l $newfile) {
+ error("cannot write to a symlink ($newfile)");
+ }
+
+ my $cleanup = sub { unlink($newfile) };
+ open (my $out, '>', $newfile) || error("failed to write $newfile: $!", $cleanup);
+ binmode($out) if ($binary);
+ if ($writer) {
+ $writer->(\*$out, $cleanup);
+ }
+ else {
+ print $out $content or error("failed writing to $newfile: $!", $cleanup);
+ }
+ close $out || error("failed saving $newfile: $!", $cleanup);
+ rename($newfile, "$destdir/$file") ||
+ error("failed renaming $newfile to $destdir/$file: $!", $cleanup);
+
+ return 1;
+}
+
+my %cleared;
+sub will_render ($$;$) {
+ my $page=shift;
+ my $dest=shift;
+ my $clear=shift;
+
+ # Important security check for independently created files.
+ if (-e "$config{destdir}/$dest" && ! $config{rebuild} &&
+ ! grep { $_ eq $dest } (@{$renderedfiles{$page}}, @{$oldrenderedfiles{$page}}, @{$wikistate{editpage}{previews}})) {
+ 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}) {
+ $renderedfiles{$page}=[$dest, grep { $_ ne $dest } @{$renderedfiles{$page}}];
+ }
+ else {
+ foreach my $old (@{$renderedfiles{$page}}) {
+ delete $destsources{$old};
+ }
+ $renderedfiles{$page}=[$dest];
+ $cleared{$page}=1;
+ }
+ $destsources{$dest}=$page;
+
+ return 1;
+}
+
+sub bestlink ($$) {
+ my $page=shift;
+ my $link=shift;
+
+ my $cwd=$page;
+ if ($link=~s/^\/+//) {
+ # absolute links
+ $cwd="";
+ }
+ $link=~s/\/$//;
+
+ do {
+ my $l=$cwd;
+ $l.="/" if length $l;
+ $l.=$link;
+
+ if (exists $pagesources{$l}) {
+ return $l;
+ }
+ elsif (exists $pagecase{lc $l}) {
+ return $pagecase{lc $l};
+ }
+ } while $cwd=~s{/?[^/]+$}{};
+
+ if (length $config{userdir}) {
+ my $l = "$config{userdir}/".lc($link);
+ if (exists $pagesources{$l}) {
+ return $l;
+ }
+ elsif (exists $pagecase{lc $l}) {
+ return $pagecase{lc $l};
+ }
+ }
+
+ #print STDERR "warning: page $page, broken link: $link\n";
+ return "";
+}
+
+sub isinlinableimage ($) {
+ my $file=shift;
+
+ return $file =~ /\.(png|gif|jpg|jpeg|svg)$/i;
+}
+
+sub pagetitle ($;$) {
+ my $page=shift;
+ my $unescaped=shift;
+
+ if ($unescaped) {
+ $page=~s/(__(\d+)__|_)/$1 eq '_' ? ' ' : chr($2)/eg;
+ }
+ else {
+ $page=~s/(__(\d+)__|_)/$1 eq '_' ? ' ' : "&#$2;"/eg;
+ }
+
+ return $page;
+}
+
+sub titlepage ($) {
+ my $title=shift;
+ # 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;
+ my $chars = defined $config{wiki_file_chars} ? $config{wiki_file_chars} : "-[:alnum:]+/.:_";
+ $link=~s/([^$chars])/$1 eq ' ' ? '_' : "__".ord($1)."__"/eg;
+ return $link;
+}
+
+sub cgiurl (@) {
+ my %params=@_;
+
+ 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 $local_url if ! defined $page;
+
+ $page=htmlpage($page);
+ $page=~s/[^\/]+$//;
+ $page=~s/[^\/]+\//..\//g;
+ 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
+ # absolute! (Debian bug #376658; fixed in debian unstable now)
+ my $path="/".shift;
+ my $base="/".shift;
+
+ require File::Spec;
+ my $ret=File::Spec->abs2rel($path, $base);
+ $ret=~s/^// if defined $ret;
+ return $ret;
+}
+
+sub displaytime ($;$$) {
+ # Plugins can override this function to mark up the time to
+ # display.
+ my $time=formattime($_[0], $_[1]);
+ if ($config{html5}) {
+ return '<time datetime="'.date_3339($_[0]).'"'.
+ ($_[2] ? ' pubdate="pubdate"' : '').
+ '>'.$time.'</time>';
+ }
+ else {
+ return '<span class="date">'.$time.'</span>';
+ }
+}
+
+sub formattime ($;$) {
+ # Plugins can override this function to format the time.
+ my $time=shift;
+ my $format=shift;
+ if (! defined $format) {
+ $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.
+ # 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 ($) {
+ my $url=shift;
+
+ # Ensure url is not an empty link, and if necessary,
+ # add ./ to avoid colon confusion.
+ if ($url !~ /^\// && $url !~ /^\.\.?\//) {
+ $url="./$url";
+ }
+
+ if ($config{usedirs}) {
+ $url =~ s!/index.$config{htmlext}$!/!;
+ }
+
+ return $url;
+}
+
+sub urlto ($;$$) {
+ my $to=shift;
+ my $from=shift;
+ my $absolute=shift;
+
+ if (! length $to) {
+ $to = 'index';
+ }
+
+ if (! $destsources{$to}) {
+ $to=htmlpage($to);
+ }
+
+ if ($absolute) {
+ 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)
+ my $link=shift;
+ my %opts=@_;
+
+ $link=~s/\/$//;
+
+ my $bestlink;
+ if (! $opts{forcesubpage}) {
+ $bestlink=bestlink($lpage, $link);
+ }
+ else {
+ $bestlink="$lpage/".lc($link);
+ }
+
+ my $linktext;
+ if (defined $opts{linktext}) {
+ $linktext=$opts{linktext};
+ }
+ else {
+ $linktext=pagetitle(basename($link));
+ }
+
+ return "<span class=\"selflink\">$linktext</span>"
+ if length $bestlink && isselflink($page, $bestlink) &&
+ ! defined $opts{anchor};
+
+ if (! $destsources{$bestlink}) {
+ $bestlink=htmlpage($bestlink);
+
+ if (! $destsources{$bestlink}) {
+ my $cgilink = "";
+ if (length $config{cgiurl}) {
+ $cgilink = "<a href=\"".
+ cgiurl(
+ do => "create",
+ page => $link,
+ from => $lpage
+ )."\" rel=\"nofollow\">?</a>";
+ }
+ return "<span class=\"createlink\">$cgilink$linktext</span>"
+ }
+ }
+
+ $bestlink=abs2rel($bestlink, dirname(htmlpage($page)));
+ $bestlink=beautify_urlpath($bestlink);
+
+ if (! $opts{noimageinline} && isinlinableimage($bestlink)) {
+ return "<img src=\"$bestlink\" alt=\"$linktext\" />";
+ }
+
+ if (defined $opts{anchor}) {
+ $bestlink.="#".$opts{anchor};
+ }
+
+ my @attrs;
+ foreach my $attr (qw{rel class title}) {
+ if (defined $opts{$attr}) {
+ push @attrs, " $attr=\"$opts{$attr}\"";
+ }
+ }
+
+ return "<a href=\"$bestlink\"@attrs>$linktext</a>";
+}
+
+sub userpage ($) {
+ my $user=shift;
+ return length $config{userdir} ? "$config{userdir}/$user" : $user;
+}
+
+sub openiduser ($) {
+ my $user=shift;
+
+ if (defined $user && $user =~ m!^https?://! &&
+ eval q{use Net::OpenID::VerifiedIdentity; 1} && !$@) {
+ my $display;
+
+ if (Net::OpenID::VerifiedIdentity->can("DisplayOfURL")) {
+ $display = Net::OpenID::VerifiedIdentity::DisplayOfURL($user);
+ }
+ else {
+ # backcompat with old version
+ my $oid=Net::OpenID::VerifiedIdentity->new(identity => $user);
+ $display=$oid->display;
+ }
+
+ # Convert "user.somehost.com" to "user [somehost.com]"
+ # (also "user.somehost.co.uk")
+ if ($display !~ /\[/) {
+ $display=~s/^([-a-zA-Z0-9]+?)\.([-.a-zA-Z0-9]+\.[a-z]+)$/$1 [$2]/;
+ }
+ # Convert "http://somehost.com/user" to "user [somehost.com]".
+ # (also "https://somehost.com/user/")
+ if ($display !~ /\[/) {
+ $display=~s/^https?:\/\/(.+)\/([^\/#?]+)\/?(?:[#?].*)?$/$2 [$1]/;
+ }
+ $display=~s!^https?://!!; # make sure this is removed
+ eval q{use CGI 'escapeHTML'};
+ error($@) if $@;
+ return escapeHTML($display);
+ }
+ return;
+}
+
+sub htmlize ($$$$) {
+ my $page=shift;
+ my $destpage=shift;
+ my $type=shift;
+ my $content=shift;
+
+ my $oneline = $content !~ /\n/;
+
+ if (exists $hooks{htmlize}{$type}) {
+ $content=$hooks{htmlize}{$type}{call}->(
+ page => $page,
+ content => $content,
+ );
+ }
+ else {
+ error("htmlization of $type not supported");
+ }
+
+ run_hooks(sanitize => sub {
+ $content=shift->(
+ page => $page,
+ destpage => $destpage,
+ content => $content,
+ );
+ });
+
+ if ($oneline) {
+ # hack to get rid of enclosing junk added by markdown
+ # and other htmlizers/sanitizers
+ $content=~s/^<p>//i;
+ $content=~s/<\/p>\n*$//i;
+ }
+
+ return $content;
+}
+
+sub linkify ($$$) {
+ my $page=shift;
+ my $destpage=shift;
+ my $content=shift;
+
+ run_hooks(linkify => sub {
+ $content=shift->(
+ page => $page,
+ destpage => $destpage,
+ content => $content,
+ );
+ });
+
+ return $content;
+}
+
+our %preprocessing;
+our $preprocess_preview=0;
+sub preprocess ($$$;$$) {
+ my $page=shift; # the page the data comes from
+ my $destpage=shift; # the page the data will appear in (different for inline)
+ my $content=shift;
+ my $scan=shift;
+ my $preview=shift;
+
+ # Using local because it needs to be set within any nested calls
+ # of this function.
+ local $preprocess_preview=$preview if defined $preview;
+
+ my $handle=sub {
+ my $escape=shift;
+ my $prefix=shift;
+ my $command=shift;
+ my $params=shift;
+ $params="" if ! defined $params;
+
+ if (length $escape) {
+ return "[[$prefix$command $params]]";
+ }
+ elsif (exists $hooks{preprocess}{$command}) {
+ return "" if $scan && ! $hooks{preprocess}{$command}{scan};
+ # Note: preserve order of params, some plugins may
+ # consider it significant.
+ my @params;
+ while ($params =~ m{
+ (?:([-.\w]+)=)? # 1: named parameter key?
+ (?:
+ """(.*?)""" # 2: triple-quoted value
+ |
+ "([^"]*?)" # 3: single-quoted 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
+ }msgx) {
+ my $key=$1;
+ my $val;
+ if (defined $2) {
+ $val=$2;
+ $val=~s/\r\n/\n/mg;
+ $val=~s/^\n+//g;
+ $val=~s/\n+$//g;
+ }
+ elsif (defined $3) {
+ $val=$3;
+ }
+ elsif (defined $4) {
+ $val=$4;
+ }
+ elsif (defined $7) {
+ $val=$7;
+ }
+ elsif (defined $6) {
+ $val=$6;
+ }
+
+ if (defined $key) {
+ push @params, $key, $val;
+ }
+ else {
+ push @params, $val, '';
+ }
+ }
+ if ($preprocessing{$page}++ > 8) {
+ # Avoid loops of preprocessed pages preprocessing
+ # other pages that preprocess them, etc.
+ return "[[!$command <span class=\"error\">".
+ sprintf(gettext("preprocessing loop detected on %s at depth %i"),
+ $page, $preprocessing{$page}).
+ "</span>]]";
+ }
+ my $ret;
+ if (! $scan) {
+ $ret=eval {
+ $hooks{preprocess}{$command}{call}->(
+ @params,
+ page => $page,
+ destpage => $destpage,
+ preview => $preprocess_preview,
+ );
+ };
+ if ($@) {
+ my $error=$@;
+ chomp $error;
+ eval q{use HTML::Entities};
+ $error = encode_entities($error);
+ $ret="[[!$command <span class=\"error\">".
+ gettext("Error").": $error"."</span>]]";
+ }
+ }
+ else {
+ # use void context during scan pass
+ eval {
+ $hooks{preprocess}{$command}{call}->(
+ @params,
+ page => $page,
+ destpage => $destpage,
+ preview => $preprocess_preview,
+ );
+ };
+ $ret="";
+ }
+ $preprocessing{$page}--;
+ return $ret;
+ }
+ else {
+ return "[[$prefix$command $params]]";
+ }
+ };
+
+ my $regex;
+ if ($config{prefix_directives}) {
+ $regex = qr{
+ (\\?) # 1: escape?
+ \[\[(!) # directive open; 2: prefix
+ ([-\w]+) # 3: command
+ ( # 4: the parameters..
+ \s+ # Must have space if parameters present
+ (?:
+ (?:[-.\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
+ # of directive
+ )
+ *)? # 0 or more parameters
+ \]\] # directive closed
+ }sx;
+ }
+ else {
+ $regex = qr{
+ (\\?) # 1: escape?
+ \[\[(!?) # directive open; 2: optional prefix
+ ([-\w]+) # 3: command
+ \s+
+ ( # 4: the parameters..
+ (?:
+ (?:[-.\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
+ # of directive
+ )
+ *) # 0 or more parameters
+ \]\] # directive closed
+ }sx;
+ }
+
+ $content =~ s{$regex}{$handle->($1, $2, $3, $4)}eg;
+ return $content;
+}
+
+sub filter ($$$) {
+ my $page=shift;
+ my $destpage=shift;
+ my $content=shift;
+
+ run_hooks(filter => sub {
+ $content=shift->(page => $page, destpage => $destpage,
+ content => $content);
+ });
+
+ return $content;
+}
+
+sub check_canedit ($$$;$) {
+ my $page=shift;
+ my $q=shift;
+ my $session=shift;
+ my $nonfatal=shift;
+
+ my $canedit;
+ run_hooks(canedit => sub {
+ return if defined $canedit;
+ my $ret=shift->($page, $q, $session);
+ if (defined $ret) {
+ if ($ret eq "") {
+ $canedit=1;
+ }
+ elsif (ref $ret eq 'CODE') {
+ $ret->() unless $nonfatal;
+ $canedit=0;
+ }
+ elsif (defined $ret) {
+ error($ret) unless $nonfatal;
+ $canedit=0;
+ }
+ }
+ });
+ return defined $canedit ? $canedit : 1;
+}
+
+sub check_content (@) {
+ my %params=@_;
+
+ return 1 if ! exists $hooks{checkcontent}; # optimisation
+
+ if (exists $pagesources{$params{page}}) {
+ my @diff;
+ my %old=map { $_ => 1 }
+ split("\n", readfile(srcfile($pagesources{$params{page}})));
+ foreach my $line (split("\n", $params{content})) {
+ push @diff, $line if ! exists $old{$line};
+ }
+ $params{diff}=join("\n", @diff);
+ }
+
+ my $ok;
+ run_hooks(checkcontent => sub {
+ return if defined $ok;
+ my $ret=shift->(%params);
+ if (defined $ret) {
+ if ($ret eq "") {
+ $ok=1;
+ }
+ elsif (ref $ret eq 'CODE') {
+ $ret->() unless $params{nonfatal};
+ $ok=0;
+ }
+ elsif (defined $ret) {
+ error($ret) unless $params{nonfatal};
+ $ok=0;
+ }
+ }
+
+ });
+ 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 () {
+ # 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}) {
+ mkdir($config{wikistatedir});
+ }
+ open($wikilock, '>', "$config{wikistatedir}/lockfile") ||
+ error ("cannot write to $config{wikistatedir}/lockfile: $!");
+ 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;
+}
+
+my $commitlock;
+
+sub commit_hook_enabled () {
+ open($commitlock, '+>', "$config{wikistatedir}/commitlock") ||
+ error("cannot write to $config{wikistatedir}/commitlock: $!");
+ if (! flock($commitlock, 1 | 4)) { # LOCK_SH | LOCK_NB to test
+ close($commitlock) || error("failed closing commitlock: $!");
+ return 0;
+ }
+ close($commitlock) || error("failed closing commitlock: $!");
+ return 1;
+}
+
+sub disable_commit_hook () {
+ open($commitlock, '>', "$config{wikistatedir}/commitlock") ||
+ error("cannot write to $config{wikistatedir}/commitlock: $!");
+ if (! flock($commitlock, 2)) { # LOCK_EX
+ error("failed to get commit lock");
+ }
+ return 1;
+}
+
+sub enable_commit_hook () {
+ return close($commitlock) if $commitlock;
+ return;
+}
+
+sub loadindex () {
+ %oldrenderedfiles=%pagectime=();
+ my $rebuild=$config{rebuild};
+ if (! $rebuild) {
+ %pagesources=%pagemtime=%oldlinks=%links=%depends=
+ %destsources=%renderedfiles=%pagecase=%pagestate=
+ %depends_simple=%typedlinks=%oldtypedlinks=();
+ }
+ my $in;
+ if (! open ($in, "<", "$config{wikistatedir}/indexdb")) {
+ if (-e "$config{wikistatedir}/index") {
+ system("ikiwiki-transition", "indexdb", $config{srcdir});
+ open ($in, "<", "$config{wikistatedir}/indexdb") || return;
+ }
+ else {
+ # gettime on first build
+ $config{gettime}=1 unless defined $config{gettime};
+ return;
+ }
+ }
+
+ my $index=Storable::fd_retrieve($in);
+ if (! defined $index) {
+ return 0;
+ }
+
+ my $pages;
+ 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;
+ %wikistate=();
+ }
+
+ foreach my $src (keys %$pages) {
+ my $d=$pages->{$src};
+ my $page;
+ if (exists $d->{page} && ! $rebuild) {
+ $page=$d->{page};
+ }
+ else {
+ $page=pagename($src);
+ }
+ $pagectime{$page}=$d->{ctime};
+ $pagesources{$page}=$src;
+ if (! $rebuild) {
+ $pagemtime{$page}=$d->{mtime};
+ $renderedfiles{$page}=$d->{dest};
+ if (exists $d->{links} && ref $d->{links}) {
+ $links{$page}=$d->{links};
+ $oldlinks{$page}=[@{$d->{links}}];
+ }
+ if (ref $d->{depends_simple} eq 'ARRAY') {
+ # old format
+ $depends_simple{$page}={
+ map { $_ => 1 } @{$d->{depends_simple}}
+ };
+ }
+ elsif (exists $d->{depends_simple}) {
+ $depends_simple{$page}=$d->{depends_simple};
+ }
+ if (exists $d->{dependslist}) {
+ # old format
+ $depends{$page}={
+ map { $_ => $DEPEND_CONTENT }
+ @{$d->{dependslist}}
+ };
+ }
+ elsif (exists $d->{depends} && ! ref $d->{depends}) {
+ # old format
+ $depends{$page}={$d->{depends} => $DEPEND_CONTENT };
+ }
+ elsif (exists $d->{depends}) {
+ $depends{$page}=$d->{depends};
+ }
+ if (exists $d->{state}) {
+ $pagestate{$page}=$d->{state};
+ }
+ if (exists $d->{typedlinks}) {
+ $typedlinks{$page}=$d->{typedlinks};
+
+ while (my ($type, $links) = each %{$typedlinks{$page}}) {
+ next unless %$links;
+ $oldtypedlinks{$page}{$type} = {%$links};
+ }
+ }
+ }
+ $oldrenderedfiles{$page}=[@{$d->{dest}}];
+ }
+ foreach my $page (keys %pagesources) {
+ $pagecase{lc $page}=$page;
+ }
+ 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 @plugins=keys %loaded_plugins;
+
+ if (! -d $config{wikistatedir}) {
+ mkdir($config{wikistatedir});
+ }
+ 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{page}{$src}={
+ page => $page,
+ ctime => $pagectime{$page},
+ mtime => $pagemtime{$page},
+ dest => $renderedfiles{$page},
+ links => $links{$page},
+ };
+
+ if (exists $depends{$page}) {
+ $index{page}{$src}{depends} = $depends{$page};
+ }
+
+ if (exists $depends_simple{$page}) {
+ $index{page}{$src}{depends_simple} = $depends_simple{$page};
+ }
+
+ if (exists $typedlinks{$page} && %{$typedlinks{$page}}) {
+ $index{page}{$src}{typedlinks} = $typedlinks{$page};
+ }
+
+ if (exists $pagestate{$page}) {
+ $index{page}{$src}{state}=$pagestate{$page};
+ }
+ }
+
+ $index{state}={};
+ 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;
+ close $out || error("failed saving to $newfile: $!", $cleanup);
+ rename($newfile, "$config{wikistatedir}/indexdb") ||
+ error("failed renaming $newfile to $config{wikistatedir}/indexdb", $cleanup);
+
+ return 1;
+}
+
+sub template_file ($) {
+ 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);
+ }
+
+ 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_depends ($$;@) {
+ my $name=shift;
+ my $page=shift;
+
+ 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);
+ }
+
+ my @opts=(
+ filter => sub {
+ my $text_ref = shift;
+ ${$text_ref} = decode_utf8(${$text_ref});
+ run_hooks(readtemplate => sub {
+ ${$text_ref} = shift->(
+ id => $name,
+ page => $tpage,
+ content => ${$text_ref},
+ untrusted => $untrusted,
+ );
+ });
+ },
+ loop_context_vars => 1,
+ die_on_bad_params => 0,
+ parent_global_vars => 1,
+ filename => $filename,
+ @_,
+ ($untrusted ? (no_includes => 1) : ()),
+ );
+ return @opts if wantarray;
+
+ require HTML::Template;
+ return HTML::Template->new(@opts);
+}
+
+sub template ($;@) {
+ template_depends(shift, undef, @_);
+}
+
+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);
+ });
+ $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 (@) {
+ my %param=@_;
+
+ if (! exists $param{type} || ! ref $param{call} || ! exists $param{id}) {
+ error 'hook requires type, call, and id parameters';
+ }
+
+ return if $param{no_override} && exists $hooks{$param{type}}{$param{id}};
+
+ $hooks{$param{type}}{$param{id}}=\%param;
+ return 1;
+}
+
+sub run_hooks ($$) {
+ # Calls the given sub for each hook of the given type,
+ # passing it the hook function to call.
+ my $type=shift;
+ my $sub=shift;
+
+ if (exists $hooks{$type}) {
+ my (@first, @middle, @last);
+ foreach my $id (keys %{$hooks{$type}}) {
+ if ($hooks{$type}{$id}{first}) {
+ push @first, $id;
+ }
+ elsif ($hooks{$type}{$id}{last}) {
+ push @last, $id;
+ }
+ else {
+ push @middle, $id;
+ }
+ }
+ foreach my $id (@first, @middle, @last) {
+ $sub->($hooks{$type}{$id}{call});
+ }
+ }
+
+ 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_getmtime ($) {
+ $hooks{rcs}{rcs_getmtime}{call}->(@_);
+}
+
+sub rcs_receive () {
+ $hooks{rcs}{rcs_receive}{call}->();
+}
+
+sub add_depends ($$;$) {
+ my $page=shift;
+ my $pagespec=shift;
+ my $deptype=shift || $DEPEND_CONTENT;
+
+ # Is the pagespec a simple page name?
+ if ($pagespec =~ /$config{wiki_file_regexp}/ &&
+ $pagespec !~ /[\s*?()!]/) {
+ $depends_simple{$page}{lc $pagespec} |= $deptype;
+ return 1;
+ }
+
+ # Add explicit dependencies for influences.
+ my $sub=pagespec_translate($pagespec);
+ return unless defined $sub;
+ foreach my $p (keys %pagesources) {
+ my $r=$sub->($p, location => $page);
+ my $i=$r->influences;
+ my $static=$r->influences_static;
+ foreach my $k (keys %$i) {
+ next unless $r || $static || $k eq $page;
+ $depends_simple{$page}{lc $k} |= $i->{$k};
+ }
+ last if $static;
+ }
+
+ $depends{$page}{$pagespec} |= $deptype;
+ return 1;
+}
+
+sub deptype (@) {
+ my $deptype=0;
+ foreach my $type (@_) {
+ if ($type eq 'presence') {
+ $deptype |= $DEPEND_PRESENCE;
+ }
+ elsif ($type eq 'links') {
+ $deptype |= $DEPEND_LINKS;
+ }
+ elsif ($type eq 'content') {
+ $deptype |= $DEPEND_CONTENT;
+ }
+ }
+ return $deptype;
+}
+
+my $file_prune_regexp;
+sub file_pruned ($) {
+ my $file=shift;
+
+ if (defined $config{include} && length $config{include}) {
+ return 0 if $file =~ m/$config{include}/;
+ }
+
+ if (! defined $file_prune_regexp) {
+ $file_prune_regexp='('.join('|', @{$config{wiki_file_prune_regexps}}).')';
+ $file_prune_regexp=qr/$file_prune_regexp/;
+ }
+ return $file =~ m/$file_prune_regexp/;
+}
+
+sub define_gettext () {
+ # If translation is needed, redefine the gettext function to do it.
+ # Otherwise, it becomes a quick no-op.
+ my $gettext_obj;
+ my $getobj;
+ if ((exists $ENV{LANG} && length $ENV{LANG}) ||
+ (exists $ENV{LC_ALL} && length $ENV{LC_ALL}) ||
+ (exists $ENV{LC_MESSAGES} && length $ENV{LC_MESSAGES})) {
+ $getobj=sub {
+ $gettext_obj=eval q{
+ use Locale::gettext q{textdomain};
+ Locale::gettext->domain('ikiwiki')
+ };
+ };
+ }
+
+ no warnings 'redefine';
+ *gettext=sub {
+ $getobj->() if $getobj;
+ if ($gettext_obj) {
+ $gettext_obj->get(shift);
+ }
+ else {
+ return shift;
+ }
+ };
+ *ngettext=sub {
+ $getobj->() if $getobj;
+ if ($gettext_obj) {
+ $gettext_obj->nget(@_);
+ }
+ else {
+ return ($_[2] == 1 ? $_[0] : $_[1])
+ }
+ };
+}
+
+sub gettext {
+ define_gettext();
+ gettext(@_);
+}
+
+sub ngettext {
+ define_gettext();
+ ngettext(@_);
+}
+
+sub yesno ($) {
+ my $val=shift;
+
+ return (defined $val && (lc($val) eq gettext("yes") || lc($val) eq "yes" || $val eq "1"));
+}
+
+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 add_link ($$;$) {
+ my $page=shift;
+ my $link=shift;
+ my $type=shift;
+
+ push @{$links{$page}}, $link
+ unless grep { $_ eq $link } @{$links{$page}};
+
+ if (defined $type) {
+ $typedlinks{$page}{$type}{$link} = 1;
+ }
+}
+
+sub add_autofile ($$$) {
+ my $file=shift;
+ my $plugin=shift;
+ my $generator=shift;
+
+ $autofiles{$file}{plugin}=$plugin;
+ $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;
+
+ my $code = "";
+ my @data;
+ while ($spec =~ m{
+ \s*
+ (-?) # group 1: perhaps negated
+ \s*
+ ( # group 2: a word
+ \w+\([^\)]*\) # command(params)
+ |
+ [^\s]+ # or anything else
+ )
+ \s*
+ }gx) {
+ my $negated = $1;
+ my $word = $2;
+ my $params = undef;
+
+ if ($word =~ m/^(\w+)\((.*)\)$/) {
+ # command with parameters
+ $params = $2;
+ $word = $1;
+ }
+ elsif ($word !~ m/^\w+$/) {
+ error(sprintf(gettext("invalid sort type %s"), $word));
+ }
+
+ if (length $code) {
+ $code .= " || ";
+ }
+
+ if ($negated) {
+ $code .= "-";
+ }
+
+ if (exists $IkiWiki::SortSpec::{"cmp_$word"}) {
+ if (defined $params) {
+ push @data, $params;
+ $code .= "IkiWiki::SortSpec::cmp_$word(\$data[$#data])";
+ }
+ else {
+ $code .= "IkiWiki::SortSpec::cmp_$word(undef)";
+ }
+ }
+ else {
+ error(sprintf(gettext("unknown sort type %s"), $word));
+ }
+ }
+
+ if (! length $code) {
+ # undefined sorting method... sort arbitrarily
+ return sub { 0 };
+ }
+
+ if ($reverse) {
+ $code="-($code)";
+ }
+
+ no warnings;
+ return eval 'sub { '.$code.' }';
+}
+
+sub pagespec_translate ($) {
+ my $spec=shift;
+
+ # Convert spec to perl code.
+ my $code="";
+ my @data;
+ while ($spec=~m{
+ \s* # ignore whitespace
+ ( # 1: match a single word
+ \! # !
+ |
+ \( # (
+ |
+ \) # )
+ |
+ \w+\([^\)]*\) # command(params)
+ |
+ [^\s()]+ # any other text
+ )
+ \s* # ignore whitespace
+ }gx) {
+ my $word=$1;
+ if (lc $word eq 'and') {
+ $code.=' &';
+ }
+ elsif (lc $word eq 'or') {
+ $code.=' |';
+ }
+ elsif ($word eq "(" || $word eq ")" || $word eq "!") {
+ $code.=' '.$word;
+ }
+ elsif ($word =~ /^(\w+)\((.*)\)$/) {
+ if (exists $IkiWiki::PageSpec::{"match_$1"}) {
+ push @data, $2;
+ $code.="IkiWiki::PageSpec::match_$1(\$page, \$data[$#data], \@_)";
+ }
+ else {
+ push @data, qq{unknown function in pagespec "$word"};
+ $code.="IkiWiki::ErrorReason->new(\$data[$#data])";
+ }
+ }
+ else {
+ push @data, $word;
+ $code.=" IkiWiki::PageSpec::match_glob(\$page, \$data[$#data], \@_)";
+ }
+ }
+
+ if (! length $code) {
+ $code="IkiWiki::FailReason->new('empty pagespec')";
+ }
+
+ no warnings;
+ return eval 'sub { my $page=shift; '.$code.' }';
+}
+
+sub pagespec_match ($$;@) {
+ my $page=shift;
+ my $spec=shift;
+ my @params=@_;
+
+ # Backwards compatability with old calling convention.
+ if (@params == 1) {
+ unshift @params, 'location';
+ }
+
+ my $sub=pagespec_translate($spec);
+ return IkiWiki::ErrorReason->new("syntax error in pagespec \"$spec\"")
+ if ! defined $sub;
+ return $sub->($page, @params);
+}
+
+# e.g. @pages = sort_pages("title", \@pages, reverse => "yes")
+#
+# Not exported yet, but could be in future if it is generally useful.
+# Note that this signature is not the same as IkiWiki::SortSpec::sort_pages,
+# which is "more internal".
+sub sort_pages ($$;@) {
+ my $sort = shift;
+ my $list = shift;
+ my %params = @_;
+ $sort = sortspec_translate($sort, $params{reverse});
+ return IkiWiki::SortSpec::sort_pages($sort, @$list);
+}
+
+sub pagespec_match_list ($$;@) {
+ my $page=shift;
+ my $pagespec=shift;
+ my %params=@_;
+
+ # Backwards compatability with old calling convention.
+ if (ref $page) {
+ print STDERR "warning: a plugin (".caller().") is using pagespec_match_list in an obsolete way, and needs to be updated\n";
+ $params{list}=$page;
+ $page=$params{location}; # ugh!
+ }
+
+ my $sub=pagespec_translate($pagespec);
+ error "syntax error in pagespec \"$pagespec\""
+ if ! defined $sub;
+ my $sort=sortspec_translate($params{sort}, $params{reverse})
+ if defined $params{sort};
+
+ my @candidates;
+ if (exists $params{list}) {
+ @candidates=exists $params{filter}
+ ? grep { ! $params{filter}->($_) } @{$params{list}}
+ : @{$params{list}};
+ }
+ else {
+ @candidates=exists $params{filter}
+ ? grep { ! $params{filter}->($_) } keys %pagesources
+ : keys %pagesources;
+ }
+
+ # clear params, remainder is passed to pagespec
+ $depends{$page}{$pagespec} |= ($params{deptype} || $DEPEND_CONTENT);
+ my $num=$params{num};
+ delete @params{qw{num deptype reverse sort filter list}};
+
+ # when only the top matches will be returned, it's efficient to
+ # sort before matching to pagespec,
+ if (defined $num && defined $sort) {
+ @candidates=IkiWiki::SortSpec::sort_pages(
+ $sort, @candidates);
+ }
+
+ my @matches;
+ my $firstfail;
+ my $count=0;
+ my $accum=IkiWiki::SuccessReason->new();
+ foreach my $p (@candidates) {
+ my $r=$sub->($p, %params, location => $page);
+ error(sprintf(gettext("cannot match pages: %s"), $r))
+ if $r->isa("IkiWiki::ErrorReason");
+ unless ($r || $r->influences_static) {
+ $r->remove_influence($p);
+ }
+ $accum |= $r;
+ if ($r) {
+ push @matches, $p;
+ last if defined $num && ++$count == $num;
+ }
+ }
+
+ # Add simple dependencies for accumulated influences.
+ my $i=$accum->influences;
+ foreach my $k (keys %$i) {
+ $depends_simple{$page}{lc $k} |= $i->{$k};
+ }
+
+ # when all matches will be returned, it's efficient to
+ # sort after matching
+ if (! defined $num && defined $sort) {
+ return IkiWiki::SortSpec::sort_pages(
+ $sort, @matches);
+ }
+ else {
+ return @matches;
+ }
+}
+
+sub pagespec_valid ($) {
+ my $spec=shift;
+
+ return defined pagespec_translate($spec);
+}
+
+sub glob2re ($) {
+ my $re=quotemeta(shift);
+ $re=~s/\\\*/.*/g;
+ $re=~s/\\\?/./g;
+ return qr/^$re$/i;
+}
+
+package IkiWiki::FailReason;
+
+use overload (
+ '""' => sub { $_[0][0] },
+ '0+' => sub { 0 },
+ '!' => sub { bless $_[0], 'IkiWiki::SuccessReason'},
+ '&' => sub { $_[0]->merge_influences($_[1], 1); $_[0] },
+ '|' => sub { $_[1]->merge_influences($_[0]); $_[1] },
+ fallback => 1,
+);
+
+our @ISA = 'IkiWiki::SuccessReason';
+
+package IkiWiki::SuccessReason;
+
+# A blessed array-ref:
+#
+# [0]: human-readable reason for success (or, in FailReason subclass, failure)
+# [1]{""}:
+# - if absent or false, the influences of this evaluation are "static",
+# see the influences_static method
+# - if true, they are dynamic (not static)
+# [1]{any other key}:
+# the dependency types of influences, as returned by the influences method
+
+use overload (
+ # in string context, it's the human-readable reason
+ '""' => sub { $_[0][0] },
+ # in boolean context, SuccessReason is 1 and FailReason is 0
+ '0+' => sub { 1 },
+ # negating a result gives the opposite result with the same influences
+ '!' => sub { bless $_[0], 'IkiWiki::FailReason'},
+ # A & B = (A ? B : A) with the influences of both
+ '&' => sub { $_[1]->merge_influences($_[0], 1); $_[1] },
+ # A | B = (A ? A : B) with the influences of both
+ '|' => sub { $_[0]->merge_influences($_[1]); $_[0] },
+ fallback => 1,
+);
+
+# SuccessReason->new("human-readable reason", page => deptype, ...)
+
+sub new {
+ my $class = shift;
+ my $value = shift;
+ return bless [$value, {@_}], $class;
+}
+
+# influences(): return a reference to a copy of the hash
+# { page => dependency type } describing the pages that indirectly influenced
+# this result, but would not cause a dependency through ikiwiki's core
+# dependency logic.
+#
+# See [[todo/dependency_types]] for extensive discussion of what this means.
+#
+# influences(page => deptype, ...): remove all influences, replace them
+# with the arguments, and return a reference to a copy of the new influences.
+
+sub influences {
+ my $this=shift;
+ $this->[1]={@_} if @_;
+ my %i=%{$this->[1]};
+ delete $i{""};
+ return \%i;
+}
+
+# True if this result has the same influences whichever page it matches,
+# For instance, whether bar matches backlink(foo) is influenced only by
+# the set of links in foo, so its only influence is { foo => DEPEND_LINKS },
+# which does not mention bar anywhere.
+#
+# False if this result would have different influences when matching
+# different pages. For instance, when testing whether link(foo) matches bar,
+# { bar => DEPEND_LINKS } is an influence on that result, because changing
+# bar's links could change the outcome; so its influences are not the same
+# as when testing whether link(foo) matches baz.
+#
+# Static influences are one of the things that make pagespec_match_list
+# more efficient than repeated calls to pagespec_match.
+
+sub influences_static {
+ return ! $_[0][1]->{""};
+}
+
+# Change the influences of $this to be the influences of "$this & $other"
+# or "$this | $other".
+#
+# If both $this and $other are either successful or have influences,
+# or this is an "or" operation, the result has all the influences from
+# either of the arguments. It has dynamic influences if either argument
+# has dynamic influences.
+#
+# If this is an "and" operation, and at least one argument is a
+# FailReason with no influences, the result has no influences, and they
+# are not dynamic. For instance, link(foo) matching bar is influenced
+# by bar, but enabled(ddate) has no influences. Suppose ddate is disabled;
+# then (link(foo) and enabled(ddate)) not matching bar is not influenced by
+# bar, because it would be false however often you edit bar.
+
+sub merge_influences {
+ my $this=shift;
+ my $other=shift;
+ my $anded=shift;
+
+ # This "if" is odd because it needs to avoid negating $this
+ # or $other, which would alter the objects in-place. Be careful.
+ if (! $anded || (($this || %{$this->[1]}) &&
+ ($other || %{$other->[1]}))) {
+ foreach my $influence (keys %{$other->[1]}) {
+ $this->[1]{$influence} |= $other->[1]{$influence};
+ }
+ }
+ else {
+ # influence blocker
+ $this->[1]={};
+ }
+}
+
+# Change $this so it is not considered to be influenced by $torm.
+
+sub remove_influence {
+ my $this=shift;
+ my $torm=shift;
+
+ delete $this->[1]{$torm};
+}
+
+package IkiWiki::ErrorReason;
+
+our @ISA = 'IkiWiki::FailReason';
+
+package IkiWiki::PageSpec;
+
+sub derel ($$) {
+ my $path=shift;
+ my $from=shift;
+
+ 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;
+ my %params=@_;
+
+ $glob=derel($glob, $params{location});
+
+ # 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");
+ }
+ else {
+ return IkiWiki::FailReason->new("$glob matches $page, but the page is an internal page");
+ }
+ }
+ else {
+ return IkiWiki::FailReason->new("$glob does not match $page");
+ }
+}
+
+sub match_internal ($$;@) {
+ 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 ($$;@) {
+ my $page=shift;
+ my $link=lc(shift);
+ my %params=@_;
+
+ $link=derel($link, $params{location});
+ my $from=exists $params{location} ? $params{location} : '';
+ my $linktype=$params{linktype};
+ my $qualifier='';
+ if (defined $linktype) {
+ $qualifier=" with type $linktype";
+ }
+
+ my $links = $IkiWiki::links{$page};
+ return IkiWiki::FailReason->new("$page has no links", $page => $IkiWiki::DEPEND_LINKS, "" => 1)
+ 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 ($bestlink eq IkiWiki::bestlink($page, $p)) {
+ return IkiWiki::SuccessReason->new("$page links to $link$qualifier", $page => $IkiWiki::DEPEND_LINKS, "" => 1)
+ }
+ }
+ else {
+ 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 (match_glob($p_rel, $link, %params)) {
+ return IkiWiki::SuccessReason->new("$page links to page $p_rel$qualifier, matching $link", $page => $IkiWiki::DEPEND_LINKS, "" => 1)
+ }
+ }
+ }
+ return IkiWiki::FailReason->new("$page does not link to $link$qualifier", $page => $IkiWiki::DEPEND_LINKS, "" => 1);
+}
+
+sub match_backlink ($$;@) {
+ 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;
+}
+
+sub match_created_before ($$;@) {
+ my $page=shift;
+ my $testpage=shift;
+ my %params=@_;
+
+ $testpage=derel($testpage, $params{location});
+
+ if (exists $IkiWiki::pagectime{$testpage}) {
+ if ($IkiWiki::pagectime{$page} < $IkiWiki::pagectime{$testpage}) {
+ return IkiWiki::SuccessReason->new("$page created before $testpage", $testpage => $IkiWiki::DEPEND_PRESENCE);
+ }
+ else {
+ return IkiWiki::FailReason->new("$page not created before $testpage", $testpage => $IkiWiki::DEPEND_PRESENCE);
+ }
+ }
+ else {
+ return IkiWiki::ErrorReason->new("$testpage does not exist", $testpage => $IkiWiki::DEPEND_PRESENCE);
+ }
+}
+
+sub match_created_after ($$;@) {
+ my $page=shift;
+ my $testpage=shift;
+ my %params=@_;
+
+ $testpage=derel($testpage, $params{location});
+
+ if (exists $IkiWiki::pagectime{$testpage}) {
+ if ($IkiWiki::pagectime{$page} > $IkiWiki::pagectime{$testpage}) {
+ return IkiWiki::SuccessReason->new("$page created after $testpage", $testpage => $IkiWiki::DEPEND_PRESENCE);
+ }
+ else {
+ return IkiWiki::FailReason->new("$page not created after $testpage", $testpage => $IkiWiki::DEPEND_PRESENCE);
+ }
+ }
+ else {
+ return IkiWiki::ErrorReason->new("$testpage does not exist", $testpage => $IkiWiki::DEPEND_PRESENCE);
+ }
+}
+
+sub match_creation_day ($$;@) {
+ 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 {
+ return IkiWiki::FailReason->new('creation_day did not match');
+ }
+}
+
+sub match_creation_month ($$;@) {
+ 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 {
+ return IkiWiki::FailReason->new('creation_month did not match');
+ }
+}
+
+sub match_creation_year ($$;@) {
+ 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 {
+ return IkiWiki::FailReason->new('creation_year did not match');
+ }
+}
+
+sub match_user ($$;@) {
+ shift;
+ my $user=shift;
+ my %params=@_;
+
+ if (! exists $params{user}) {
+ return IkiWiki::ErrorReason->new("no user specified");
+ }
+
+ my $regexp=IkiWiki::glob2re($user);
+
+ if (defined $params{user} && $params{user}=~$regexp) {
+ 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::ErrorReason->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::ErrorReason->new("no IP specified");
+ }
+
+ my $regexp=IkiWiki::glob2re(lc $ip);
+
+ if (defined $params{ip} && lc $params{ip}=~$regexp) {
+ return IkiWiki::SuccessReason->new("IP is $ip");
+ }
+ else {
+ return IkiWiki::FailReason->new("IP is $params{ip}, not $ip");
+ }
+}
+
+package IkiWiki::SortSpec;
+
+# This is in the SortSpec namespace so that the $a and $b that sort() uses
+# are easily available in this namespace, for cmp functions to use them.
+sub sort_pages {
+ my $f=shift;
+ sort $f @_
+}
+
+sub cmp_title {
+ IkiWiki::pagetitle(IkiWiki::basename($a))
+ cmp
+ 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} }
+
+1
diff -Nru ikiwiki-3.20141016.4/.pc/CVE-2019-9187-2.patch/IkiWiki.pm ikiwiki-3.20141016.4+deb8u1/.pc/CVE-2019-9187-2.patch/IkiWiki.pm
--- ikiwiki-3.20141016.4/.pc/CVE-2019-9187-2.patch/IkiWiki.pm 1970-01-01 10:00:00.000000000 +1000
+++ ikiwiki-3.20141016.4+deb8u1/.pc/CVE-2019-9187-2.patch/IkiWiki.pm 2019-03-07 17:32:38.000000000 +1100
@@ -0,0 +1,3031 @@
+#!/usr/bin/perl
+
+package IkiWiki;
+
+use warnings;
+use strict;
+use Encode;
+use URI::Escape q{uri_escape_utf8};
+use POSIX ();
+use Storable;
+use open qw{:utf8 :std};
+
+use vars qw{%config %links %oldlinks %pagemtime %pagectime %pagecase
+ %pagestate %wikistate %renderedfiles %oldrenderedfiles
+ %pagesources %delpagesources %destsources %depends %depends_simple
+ @mass_depends %hooks %forcerebuild %loaded_plugins %typedlinks
+ %oldtypedlinks %autofiles @underlayfiles $lastrev $phase};
+
+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 strftime_utf8 will_render gettext ngettext urlto targetpage
+ add_underlay pagetitle titlepage linkpage newpagefile
+ 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
+our $version='unknown'; # VERSION_AUTOREPLACE done by Makefile, DNE
+our $installdir='/usr'; # INSTALLDIR_AUTOREPLACE done by Makefile, DNE
+
+# Page dependency types.
+our $DEPEND_CONTENT=1;
+our $DEPEND_PRESENCE=2;
+our $DEPEND_LINKS=4;
+
+# Phases of processing.
+sub PHASE_SCAN () { 0 }
+sub PHASE_RENDER () { 1 }
+$phase = PHASE_SCAN;
+
+# Optimisation.
+use Memoize;
+memoize("abs2rel");
+memoize("sortspec_translate");
+memoize("pagespec_translate");
+memoize("template_file");
+
+sub getsetup () {
+ wikiname => {
+ type => "string",
+ default => "wiki",
+ description => "name of the wiki",
+ safe => 1,
+ rebuild => 1,
+ },
+ adminemail => {
+ type => "string",
+ default => undef,
+ example => 'me@example.com',
+ description => "contact email for wiki",
+ safe => 1,
+ rebuild => 0,
+ },
+ adminuser => {
+ type => "string",
+ default => [],
+ description => "users who are wiki admins",
+ safe => 1,
+ rebuild => 0,
+ },
+ banned_users => {
+ type => "string",
+ default => [],
+ description => "users who are banned from the wiki",
+ safe => 1,
+ rebuild => 0,
+ },
+ srcdir => {
+ type => "string",
+ default => undef,
+ example => "$ENV{HOME}/wiki",
+ description => "where the source of the wiki is located",
+ safe => 0, # path
+ rebuild => 1,
+ },
+ destdir => {
+ type => "string",
+ default => undef,
+ example => "/var/www/wiki",
+ description => "where to build the wiki",
+ safe => 0, # path
+ rebuild => 1,
+ },
+ url => {
+ type => "string",
+ default => '',
+ example => "http://example.com/wiki",
+ description => "base url to the wiki",
+ safe => 1,
+ rebuild => 1,
+ },
+ cgiurl => {
+ type => "string",
+ default => '',
+ example => "http://example.com/wiki/ikiwiki.cgi",
+ description => "url to the ikiwiki.cgi",
+ safe => 1,
+ rebuild => 1,
+ },
+ reverse_proxy => {
+ type => "boolean",
+ default => 0,
+ description => "do not adjust cgiurl if CGI is accessed via different URL",
+ advanced => 0,
+ safe => 1,
+ rebuild => 0, # only affects CGI requests
+ },
+ cgi_wrapper => {
+ type => "string",
+ default => '',
+ example => "/var/www/wiki/ikiwiki.cgi",
+ description => "filename of cgi wrapper to generate",
+ safe => 0, # file
+ rebuild => 0,
+ },
+ cgi_wrappermode => {
+ type => "string",
+ default => '06755',
+ description => "mode for cgi_wrapper (can safely be made suid)",
+ safe => 0,
+ rebuild => 0,
+ },
+ 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 => '',
+ description => "rcs backend to use",
+ safe => 0, # don't allow overriding
+ rebuild => 0,
+ },
+ default_plugins => {
+ type => "internal",
+ default => [qw{mdwn link inline meta htmlscrubber passwordauth
+ openid signinedit lockedit conditional
+ recentchanges parentlinks editpage
+ templatebody}],
+ description => "plugins to enable by default",
+ safe => 0,
+ rebuild => 1,
+ },
+ add_plugins => {
+ type => "string",
+ default => [],
+ description => "plugins to add to the default configuration",
+ safe => 1,
+ rebuild => 1,
+ },
+ disable_plugins => {
+ type => "string",
+ default => [],
+ description => "plugins to disable",
+ safe => 1,
+ rebuild => 1,
+ },
+ templatedir => {
+ type => "string",
+ default => "$installdir/share/ikiwiki/templates",
+ description => "additional directory to search for template files",
+ advanced => 1,
+ safe => 0, # path
+ rebuild => 1,
+ },
+ underlaydir => {
+ type => "string",
+ default => "$installdir/share/ikiwiki/basewiki",
+ description => "base wiki source location",
+ advanced => 1,
+ safe => 0, # path
+ rebuild => 0,
+ },
+ underlaydirbase => {
+ type => "internal",
+ default => "$installdir/share/ikiwiki",
+ description => "parent directory containing additional underlays",
+ safe => 0,
+ rebuild => 0,
+ },
+ wrappers => {
+ type => "internal",
+ default => [],
+ description => "wrappers to generate",
+ safe => 0,
+ rebuild => 0,
+ },
+ underlaydirs => {
+ type => "internal",
+ default => [],
+ description => "additional underlays to use",
+ safe => 0,
+ rebuild => 0,
+ },
+ verbose => {
+ type => "boolean",
+ example => 1,
+ description => "display verbose messages?",
+ safe => 1,
+ rebuild => 0,
+ },
+ syslog => {
+ type => "boolean",
+ example => 1,
+ description => "log to syslog?",
+ safe => 1,
+ rebuild => 0,
+ },
+ usedirs => {
+ type => "boolean",
+ default => 1,
+ description => "create output files named page/index.html?",
+ safe => 0, # changing requires manual transition
+ rebuild => 1,
+ },
+ prefix_directives => {
+ type => "boolean",
+ default => 1,
+ description => "use '!'-prefixed preprocessor directives?",
+ safe => 0, # changing requires manual transition
+ rebuild => 1,
+ },
+ indexpages => {
+ type => "boolean",
+ default => 0,
+ description => "use page/index.mdwn source files",
+ safe => 1,
+ rebuild => 1,
+ },
+ discussion => {
+ type => "boolean",
+ default => 1,
+ description => "enable Discussion pages?",
+ safe => 1,
+ rebuild => 1,
+ },
+ discussionpage => {
+ type => "string",
+ default => gettext("Discussion"),
+ description => "name of Discussion pages",
+ safe => 1,
+ rebuild => 1,
+ },
+ html5 => {
+ type => "boolean",
+ default => 0,
+ description => "generate HTML5?",
+ advanced => 0,
+ safe => 1,
+ rebuild => 1,
+ },
+ sslcookie => {
+ type => "boolean",
+ default => 0,
+ description => "only send cookies over SSL connections?",
+ advanced => 1,
+ safe => 1,
+ rebuild => 0,
+ },
+ default_pageext => {
+ type => "string",
+ default => "mdwn",
+ description => "extension to use for new pages",
+ safe => 0, # not sanitized
+ rebuild => 0,
+ },
+ htmlext => {
+ type => "string",
+ default => "html",
+ description => "extension to use for html files",
+ safe => 0, # not sanitized
+ rebuild => 1,
+ },
+ timeformat => {
+ type => "string",
+ default => '%c',
+ description => "strftime format string to display date",
+ advanced => 1,
+ safe => 1,
+ rebuild => 1,
+ },
+ locale => {
+ type => "string",
+ default => undef,
+ example => "en_US.UTF-8",
+ description => "UTF-8 locale to use",
+ advanced => 1,
+ safe => 0,
+ rebuild => 1,
+ },
+ userdir => {
+ type => "string",
+ default => "",
+ example => "users",
+ description => "put user pages below specified page",
+ safe => 1,
+ rebuild => 1,
+ },
+ numbacklinks => {
+ type => "integer",
+ default => 10,
+ description => "how many backlinks to show before hiding excess (0 to show all)",
+ safe => 1,
+ rebuild => 1,
+ },
+ hardlink => {
+ type => "boolean",
+ default => 0,
+ description => "attempt to hardlink source files? (optimisation for large files)",
+ advanced => 1,
+ safe => 0, # paranoia
+ rebuild => 0,
+ },
+ umask => {
+ type => "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,
+ },
+ wrappergroup => {
+ type => "string",
+ example => "ikiwiki",
+ description => "group for wrappers to run in",
+ advanced => 1,
+ safe => 0, # paranoia
+ rebuild => 0,
+ },
+ libdir => {
+ type => "string",
+ default => "",
+ example => "$ENV{HOME}/.ikiwiki/",
+ description => "extra library and plugin directory",
+ advanced => 1,
+ safe => 0, # directory
+ rebuild => 0,
+ },
+ ENV => {
+ type => "string",
+ default => {},
+ description => "environment variables",
+ safe => 0, # paranoia
+ rebuild => 0,
+ },
+ timezone => {
+ type => "string",
+ default => "",
+ example => "US/Eastern",
+ description => "time zone name",
+ safe => 1,
+ rebuild => 1,
+ },
+ include => {
+ type => "string",
+ default => undef,
+ example => '^\.htaccess$',
+ description => "regexp of normally excluded files to include",
+ advanced => 1,
+ safe => 0, # regexp
+ rebuild => 1,
+ },
+ exclude => {
+ type => "string",
+ default => undef,
+ example => '^(*\.private|Makefile)$',
+ description => "regexp of files that should be skipped",
+ advanced => 1,
+ safe => 0, # regexp
+ rebuild => 1,
+ },
+ wiki_file_prune_regexps => {
+ type => "internal",
+ default => [qr/(^|\/)\.\.(\/|$)/, qr/^\//, qr/^\./, qr/\/\./,
+ qr/\.x?html?$/, qr/\.ikiwiki-new$/,
+ qr/(^|\/).svn\//, qr/.arch-ids\//, qr/{arch}\//,
+ qr/(^|\/)_MTN\//, qr/(^|\/)_darcs\//,
+ qr/(^|\/)CVS\//, qr/\.dpkg-tmp$/],
+ description => "regexps of source files to ignore",
+ safe => 0,
+ rebuild => 1,
+ },
+ wiki_file_chars => {
+ type => "string",
+ description => "specifies the characters that are allowed in source filenames",
+ default => "-[:alnum:]+/.:_",
+ safe => 0,
+ rebuild => 1,
+ },
+ wiki_file_regexp => {
+ type => "internal",
+ description => "regexp of legal source files",
+ safe => 0,
+ rebuild => 1,
+ },
+ web_commit_regexp => {
+ type => "internal",
+ default => qr/^web commit (by (.*?(?=: |$))|from ([0-9a-fA-F:.]+[0-9a-fA-F])):?(.*)/,
+ description => "regexp to parse web commits from logs",
+ safe => 0,
+ rebuild => 0,
+ },
+ cgi => {
+ type => "internal",
+ default => 0,
+ description => "run as a cgi",
+ safe => 0,
+ rebuild => 0,
+ },
+ cgi_disable_uploads => {
+ type => "internal",
+ default => 1,
+ description => "whether CGI should accept file uploads",
+ safe => 0,
+ rebuild => 0,
+ },
+ post_commit => {
+ type => "internal",
+ default => 0,
+ description => "run as a post-commit hook",
+ safe => 0,
+ rebuild => 0,
+ },
+ rebuild => {
+ type => "internal",
+ default => 0,
+ description => "running in rebuild mode",
+ safe => 0,
+ rebuild => 0,
+ },
+ setup => {
+ type => "internal",
+ default => undef,
+ description => "running in setup mode",
+ safe => 0,
+ rebuild => 0,
+ },
+ clean => {
+ type => "internal",
+ default => 0,
+ description => "running in clean mode",
+ safe => 0,
+ rebuild => 0,
+ },
+ refresh => {
+ type => "internal",
+ default => 0,
+ description => "running in refresh mode",
+ safe => 0,
+ rebuild => 0,
+ },
+ test_receive => {
+ type => "internal",
+ default => 0,
+ description => "running in receive test mode",
+ safe => 0,
+ rebuild => 0,
+ },
+ wrapper_background_command => {
+ type => "internal",
+ default => '',
+ description => "background shell command to run",
+ safe => 0,
+ rebuild => 0,
+ },
+ gettime => {
+ type => "internal",
+ description => "running in gettime mode",
+ safe => 0,
+ rebuild => 0,
+ },
+ w3mmode => {
+ type => "internal",
+ default => 0,
+ description => "running in w3mmode",
+ safe => 0,
+ rebuild => 0,
+ },
+ wikistatedir => {
+ type => "internal",
+ default => undef,
+ description => "path to the .ikiwiki directory holding ikiwiki state",
+ safe => 0,
+ rebuild => 0,
+ },
+ setupfile => {
+ type => "internal",
+ default => undef,
+ description => "path to setup file",
+ safe => 0,
+ rebuild => 0,
+ },
+ setuptype => {
+ type => "internal",
+ default => "Yaml",
+ description => "perl class to use to dump 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,
+ },
+ 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 => "ikiwiki/$version",
+ 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 () {
+ my %s=getsetup();
+ my @ret;
+ foreach my $key (keys %s) {
+ push @ret, $key, $s{$key}->{default};
+ }
+ 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}) {
+ $ENV{LANG} = $ENV{LC_ALL};
+ delete $ENV{LC_ALL};
+ }
+ if (defined $config{locale}) {
+ if (POSIX::setlocale(&POSIX::LC_ALL, $config{locale})) {
+ $ENV{LANG}=$config{locale};
+ define_gettext();
+ }
+ }
+
+ 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}}) {
+ $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}};
+ error($@) if $@;
+ $config{srcdir}=possibly_foolish_untaint(abs_path($config{srcdir}));
+ $config{destdir}=possibly_foolish_untaint(abs_path($config{destdir}));
+ $config{cgiurl}="file:///\$LIB/ikiwiki-w3m.cgi/".$config{cgiurl}
+ unless $config{cgiurl} =~ m!file:///!;
+ $config{url}="file://".$config{destdir};
+ }
+
+ 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 eq 'https' &&
+ $baseurl->scheme eq 'http') {
+ # We assume that the same content is available
+ # over both http and https, because if it
+ # wasn't, accessing the static content
+ # from the CGI would be mixed-content,
+ # which would be a security flaw.
+
+ if ($cgiurl->authority ne $baseurl->authority) {
+ # use protocol-relative URL for
+ # static content
+ $local_url = "$config{url}/";
+ $local_url =~ s{^http://}{//};
+ }
+ # else use host-relative URL for static content
+
+ # either way, CGI needs to be absolute
+ $local_cgiurl = $config{cgiurl};
+ }
+ elsif ($cgiurl->scheme ne $baseurl->scheme) {
+ # too far apart, fall back to absolute URLs
+ $local_url = "$config{url}/";
+ $local_cgiurl = $config{cgiurl};
+ }
+ elsif ($cgiurl->authority ne $baseurl->authority) {
+ # slightly too far apart, fall back to
+ # protocol-relative URLs
+ $local_url = "$config{url}/";
+ $local_url =~ s{^https?://}{//};
+ $local_cgiurl = $config{cgiurl};
+ $local_cgiurl =~ s{^https?://}{//};
+ }
+ # else keep host-relative URLs
+ }
+
+ $local_url =~ s{//$}{/};
+ }
+ else {
+ $local_cgiurl = $config{cgiurl};
+ }
+
+ $config{wikistatedir}="$config{srcdir}/.ikiwiki"
+ unless exists $config{wikistatedir} && defined $config{wikistatedir};
+
+ if (defined $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->() });
+
+ return 1;
+}
+
+sub listplugins () {
+ my %ret;
+
+ foreach my $dir (@INC, $config{libdir}) {
+ next unless defined $dir && length $dir;
+ foreach my $file (glob("$dir/IkiWiki/Plugin/*.pm")) {
+ my ($plugin)=$file=~/.*\/(.*)\.pm$/;
+ $ret{$plugin}=1;
+ }
+ }
+ foreach my $dir ($config{libdir}, "$installdir/lib/ikiwiki") {
+ next unless defined $dir && length $dir;
+ foreach my $file (glob("$dir/plugins/*")) {
+ $ret{basename($file)}=1 if -x $file;
+ }
+ }
+
+ return keys %ret;
+}
+
+sub loadplugins () {
+ if (defined $config{libdir} && length $config{libdir}) {
+ unshift @INC, possibly_foolish_untaint($config{libdir});
+ }
+
+ foreach my $plugin (@{$config{default_plugins}}, @{$config{add_plugins}}) {
+ loadplugin($plugin);
+ }
+
+ if ($config{rcs}) {
+ if (exists $hooks{rcs}) {
+ error(gettext("cannot use multiple rcs plugins"));
+ }
+ loadplugin($config{rcs});
+ }
+ if (! exists $hooks{rcs}) {
+ loadplugin("norcs");
+ }
+
+ run_hooks(getopt => sub { shift->() });
+ if (grep /^-/, @ARGV) {
+ print STDERR "Unknown option (or missing parameter): $_\n"
+ foreach grep /^-/, @ARGV;
+ usage();
+ }
+
+ return 1;
+}
+
+sub loadplugin ($;$) {
+ my $plugin=shift;
+ my $force=shift;
+
+ return if ! $force && grep { $_ eq $plugin} @{$config{disable_plugins}};
+
+ foreach my $dir (defined $config{libdir} ? possibly_foolish_untaint($config{libdir}) : undef,
+ "$installdir/lib/ikiwiki") {
+ if (defined $dir && -x "$dir/plugins/$plugin") {
+ 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;
+ }
+ }
+
+ my $mod="IkiWiki::Plugin::".possibly_foolish_untaint($plugin);
+ eval qq{use $mod};
+ if ($@) {
+ error("Failed to load plugin $mod: $@");
+ }
+ $loaded_plugins{$plugin}=1;
+ return 1;
+}
+
+sub error ($;$) {
+ my $message=shift;
+ my $cleaner=shift;
+ log_message('err' => $message) if $config{syslog};
+ if (defined $cleaner) {
+ $cleaner->();
+ }
+ die $message."\n";
+}
+
+sub debug ($) {
+ return unless $config{verbose};
+ return log_message(debug => @_);
+}
+
+my $log_open=0;
+my $log_failed=0;
+sub log_message ($$) {
+ my $type=shift;
+
+ if ($config{syslog}) {
+ require Sys::Syslog;
+ if (! $log_open) {
+ Sys::Syslog::setlogsock('unix');
+ Sys::Syslog::openlog('ikiwiki', '', 'user');
+ $log_open=1;
+ }
+ 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";
+ }
+ else {
+ return print STDERR "@_\n";
+ }
+}
+
+sub possibly_foolish_untaint ($) {
+ my $tainted=shift;
+ my ($untainted)=$tainted=~/(.*)/s;
+ return $untainted;
+}
+
+sub basename ($) {
+ my $file=shift;
+
+ $file=~s!.*/+!!;
+ return $file;
+}
+
+sub dirname ($) {
+ my $file=shift;
+
+ $file=~s!/*[^/]+$!!;
+ return $file;
+}
+
+sub isinternal ($) {
+ my $page=shift;
+ return exists $pagesources{$page} &&
+ $pagesources{$page} =~ /\._([^.]+)$/;
+}
+
+sub pagetype ($) {
+ my $file=shift;
+
+ if ($file =~ /\.([^.]+)$/) {
+ return $1 if exists $hooks{htmlize}{$1};
+ }
+ my $base=basename($file);
+ if (exists $hooks{htmlize}{$base} &&
+ $hooks{htmlize}{$base}{noextension}) {
+ return $base;
+ }
+ return;
+}
+
+my %pagename_cache;
+
+sub pagename ($) {
+ my $file=shift;
+
+ if (exists $pagename_cache{$file}) {
+ return $pagename_cache{$file};
+ }
+
+ my $type=pagetype($file);
+ my $page=$file;
+ $page=~s/\Q.$type\E*$//
+ if defined $type && !$hooks{htmlize}{$type}{keepextension}
+ && !$hooks{htmlize}{$type}{noextension};
+ if ($config{indexpages} && $page=~/(.*)\/index$/) {
+ $page=$1;
+ }
+
+ $pagename_cache{$file} = $page;
+ 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;
+ my $filename=shift;
+
+ if (defined $filename) {
+ return $page."/".$filename.".".$ext;
+ }
+ elsif (! $config{usedirs} || $page eq 'index') {
+ return $page.".".$ext;
+ }
+ else {
+ return $page."/index.".$ext;
+ }
+}
+
+sub htmlpage ($) {
+ my $page=shift;
+
+ return targetpage($page, $config{htmlext});
+}
+
+sub srcfile_stat {
+ my $file=shift;
+ my $nothrow=shift;
+
+ return "$config{srcdir}/$file", stat(_) if -e "$config{srcdir}/$file";
+ foreach my $dir (@{$config{underlaydirs}}, $config{underlaydir}) {
+ return "$dir/$file", stat(_) if -e "$dir/$file";
+ }
+ error("internal error: $file cannot be found in $config{srcdir} or underlay") unless $nothrow;
+ return;
+}
+
+sub srcfile ($;$) {
+ return (srcfile_stat(@_))[0];
+}
+
+sub add_literal_underlay ($) {
+ my $dir=shift;
+
+ 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;
+}
+
+sub readfile ($;$$) {
+ my $file=shift;
+ my $binary=shift;
+ my $wantfd=shift;
+
+ if (-l $file) {
+ error("cannot read a symlink ($file)");
+ }
+
+ local $/=undef;
+ open (my $in, "<", $file) || error("failed to read $file: $!");
+ 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;
+}
+
+sub prep_writefile ($$) {
+ my $file=shift;
+ my $destdir=shift;
+
+ my $test=$file;
+ while (length $test) {
+ 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);
+ }
+
+ my $dir=dirname("$destdir/$file");
+ if (! -d $dir) {
+ my $d="";
+ foreach my $s (split(m!/+!, $dir)) {
+ $d.="$s/";
+ if (! -d $d) {
+ mkdir($d) || error("failed to create directory $d: $!");
+ }
+ }
+ }
+
+ return 1;
+}
+
+sub writefile ($$$;$$) {
+ my $file=shift; # can include subdirs
+ my $destdir=shift; # directory to put file in
+ my $content=shift;
+ my $binary=shift;
+ my $writer=shift;
+
+ prep_writefile($file, $destdir);
+
+ my $newfile="$destdir/$file.ikiwiki-new";
+ if (-l $newfile) {
+ error("cannot write to a symlink ($newfile)");
+ }
+
+ my $cleanup = sub { unlink($newfile) };
+ open (my $out, '>', $newfile) || error("failed to write $newfile: $!", $cleanup);
+ binmode($out) if ($binary);
+ if ($writer) {
+ $writer->(\*$out, $cleanup);
+ }
+ else {
+ print $out $content or error("failed writing to $newfile: $!", $cleanup);
+ }
+ close $out || error("failed saving $newfile: $!", $cleanup);
+ rename($newfile, "$destdir/$file") ||
+ error("failed renaming $newfile to $destdir/$file: $!", $cleanup);
+
+ return 1;
+}
+
+my %cleared;
+sub will_render ($$;$) {
+ my $page=shift;
+ my $dest=shift;
+ my $clear=shift;
+
+ # Important security check for independently created files.
+ if (-e "$config{destdir}/$dest" && ! $config{rebuild} &&
+ ! grep { $_ eq $dest } (@{$renderedfiles{$page}}, @{$oldrenderedfiles{$page}}, @{$wikistate{editpage}{previews}})) {
+ 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}) {
+ $renderedfiles{$page}=[$dest, grep { $_ ne $dest } @{$renderedfiles{$page}}];
+ }
+ else {
+ foreach my $old (@{$renderedfiles{$page}}) {
+ delete $destsources{$old};
+ }
+ $renderedfiles{$page}=[$dest];
+ $cleared{$page}=1;
+ }
+ $destsources{$dest}=$page;
+
+ return 1;
+}
+
+sub bestlink ($$) {
+ my $page=shift;
+ my $link=shift;
+
+ my $cwd=$page;
+ if ($link=~s/^\/+//) {
+ # absolute links
+ $cwd="";
+ }
+ $link=~s/\/$//;
+
+ do {
+ my $l=$cwd;
+ $l.="/" if length $l;
+ $l.=$link;
+
+ if (exists $pagesources{$l}) {
+ return $l;
+ }
+ elsif (exists $pagecase{lc $l}) {
+ return $pagecase{lc $l};
+ }
+ } while $cwd=~s{/?[^/]+$}{};
+
+ if (length $config{userdir}) {
+ my $l = "$config{userdir}/".lc($link);
+ if (exists $pagesources{$l}) {
+ return $l;
+ }
+ elsif (exists $pagecase{lc $l}) {
+ return $pagecase{lc $l};
+ }
+ }
+
+ #print STDERR "warning: page $page, broken link: $link\n";
+ return "";
+}
+
+sub isinlinableimage ($) {
+ my $file=shift;
+
+ return $file =~ /\.(png|gif|jpg|jpeg|svg)$/i;
+}
+
+sub pagetitle ($;$) {
+ my $page=shift;
+ my $unescaped=shift;
+
+ if ($unescaped) {
+ $page=~s/(__(\d+)__|_)/$1 eq '_' ? ' ' : chr($2)/eg;
+ }
+ else {
+ $page=~s/(__(\d+)__|_)/$1 eq '_' ? ' ' : "&#$2;"/eg;
+ }
+
+ return $page;
+}
+
+sub titlepage ($) {
+ my $title=shift;
+ # 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;
+ my $chars = defined $config{wiki_file_chars} ? $config{wiki_file_chars} : "-[:alnum:]+/.:_";
+ $link=~s/([^$chars])/$1 eq ' ' ? '_' : "__".ord($1)."__"/eg;
+ return $link;
+}
+
+sub cgiurl (@) {
+ my %params=@_;
+
+ 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 $local_url if ! defined $page;
+
+ $page=htmlpage($page);
+ $page=~s/[^\/]+$//;
+ $page=~s/[^\/]+\//..\//g;
+ 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
+ # absolute! (Debian bug #376658; fixed in debian unstable now)
+ my $path="/".shift;
+ my $base="/".shift;
+
+ require File::Spec;
+ my $ret=File::Spec->abs2rel($path, $base);
+ $ret=~s/^// if defined $ret;
+ return $ret;
+}
+
+sub displaytime ($;$$) {
+ # Plugins can override this function to mark up the time to
+ # display.
+ my $time=formattime($_[0], $_[1]);
+ if ($config{html5}) {
+ return '<time datetime="'.date_3339($_[0]).'"'.
+ ($_[2] ? ' pubdate="pubdate"' : '').
+ '>'.$time.'</time>';
+ }
+ else {
+ return '<span class="date">'.$time.'</span>';
+ }
+}
+
+sub formattime ($;$) {
+ # Plugins can override this function to format the time.
+ my $time=shift;
+ my $format=shift;
+ if (! defined $format) {
+ $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.
+ # 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 ($) {
+ my $url=shift;
+
+ # Ensure url is not an empty link, and if necessary,
+ # add ./ to avoid colon confusion.
+ if ($url !~ /^\// && $url !~ /^\.\.?\//) {
+ $url="./$url";
+ }
+
+ if ($config{usedirs}) {
+ $url =~ s!/index.$config{htmlext}$!/!;
+ }
+
+ return $url;
+}
+
+sub urlto ($;$$) {
+ my $to=shift;
+ my $from=shift;
+ my $absolute=shift;
+
+ if (! length $to) {
+ $to = 'index';
+ }
+
+ if (! $destsources{$to}) {
+ $to=htmlpage($to);
+ }
+
+ if ($absolute) {
+ 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)
+ my $link=shift;
+ my %opts=@_;
+
+ $link=~s/\/$//;
+
+ my $bestlink;
+ if (! $opts{forcesubpage}) {
+ $bestlink=bestlink($lpage, $link);
+ }
+ else {
+ $bestlink="$lpage/".lc($link);
+ }
+
+ my $linktext;
+ if (defined $opts{linktext}) {
+ $linktext=$opts{linktext};
+ }
+ else {
+ $linktext=pagetitle(basename($link));
+ }
+
+ return "<span class=\"selflink\">$linktext</span>"
+ if length $bestlink && isselflink($page, $bestlink) &&
+ ! defined $opts{anchor};
+
+ if (! $destsources{$bestlink}) {
+ $bestlink=htmlpage($bestlink);
+
+ if (! $destsources{$bestlink}) {
+ my $cgilink = "";
+ if (length $config{cgiurl}) {
+ $cgilink = "<a href=\"".
+ cgiurl(
+ do => "create",
+ page => $link,
+ from => $lpage
+ )."\" rel=\"nofollow\">?</a>";
+ }
+ return "<span class=\"createlink\">$cgilink$linktext</span>"
+ }
+ }
+
+ $bestlink=abs2rel($bestlink, dirname(htmlpage($page)));
+ $bestlink=beautify_urlpath($bestlink);
+
+ if (! $opts{noimageinline} && isinlinableimage($bestlink)) {
+ return "<img src=\"$bestlink\" alt=\"$linktext\" />";
+ }
+
+ if (defined $opts{anchor}) {
+ $bestlink.="#".$opts{anchor};
+ }
+
+ my @attrs;
+ foreach my $attr (qw{rel class title}) {
+ if (defined $opts{$attr}) {
+ push @attrs, " $attr=\"$opts{$attr}\"";
+ }
+ }
+
+ return "<a href=\"$bestlink\"@attrs>$linktext</a>";
+}
+
+sub userpage ($) {
+ my $user=shift;
+ return length $config{userdir} ? "$config{userdir}/$user" : $user;
+}
+
+sub openiduser ($) {
+ my $user=shift;
+
+ if (defined $user && $user =~ m!^https?://! &&
+ eval q{use Net::OpenID::VerifiedIdentity; 1} && !$@) {
+ my $display;
+
+ if (Net::OpenID::VerifiedIdentity->can("DisplayOfURL")) {
+ $display = Net::OpenID::VerifiedIdentity::DisplayOfURL($user);
+ }
+ else {
+ # backcompat with old version
+ my $oid=Net::OpenID::VerifiedIdentity->new(identity => $user);
+ $display=$oid->display;
+ }
+
+ # Convert "user.somehost.com" to "user [somehost.com]"
+ # (also "user.somehost.co.uk")
+ if ($display !~ /\[/) {
+ $display=~s/^([-a-zA-Z0-9]+?)\.([-.a-zA-Z0-9]+\.[a-z]+)$/$1 [$2]/;
+ }
+ # Convert "http://somehost.com/user" to "user [somehost.com]".
+ # (also "https://somehost.com/user/")
+ if ($display !~ /\[/) {
+ $display=~s/^https?:\/\/(.+)\/([^\/#?]+)\/?(?:[#?].*)?$/$2 [$1]/;
+ }
+ $display=~s!^https?://!!; # make sure this is removed
+ eval q{use CGI 'escapeHTML'};
+ error($@) if $@;
+ return escapeHTML($display);
+ }
+ return;
+}
+
+sub htmlize ($$$$) {
+ my $page=shift;
+ my $destpage=shift;
+ my $type=shift;
+ my $content=shift;
+
+ my $oneline = $content !~ /\n/;
+
+ if (exists $hooks{htmlize}{$type}) {
+ $content=$hooks{htmlize}{$type}{call}->(
+ page => $page,
+ content => $content,
+ );
+ }
+ else {
+ error("htmlization of $type not supported");
+ }
+
+ run_hooks(sanitize => sub {
+ $content=shift->(
+ page => $page,
+ destpage => $destpage,
+ content => $content,
+ );
+ });
+
+ if ($oneline) {
+ # hack to get rid of enclosing junk added by markdown
+ # and other htmlizers/sanitizers
+ $content=~s/^<p>//i;
+ $content=~s/<\/p>\n*$//i;
+ }
+
+ return $content;
+}
+
+sub linkify ($$$) {
+ my $page=shift;
+ my $destpage=shift;
+ my $content=shift;
+
+ run_hooks(linkify => sub {
+ $content=shift->(
+ page => $page,
+ destpage => $destpage,
+ content => $content,
+ );
+ });
+
+ return $content;
+}
+
+our %preprocessing;
+our $preprocess_preview=0;
+sub preprocess ($$$;$$) {
+ my $page=shift; # the page the data comes from
+ my $destpage=shift; # the page the data will appear in (different for inline)
+ my $content=shift;
+ my $scan=shift;
+ my $preview=shift;
+
+ # Using local because it needs to be set within any nested calls
+ # of this function.
+ local $preprocess_preview=$preview if defined $preview;
+
+ my $handle=sub {
+ my $escape=shift;
+ my $prefix=shift;
+ my $command=shift;
+ my $params=shift;
+ $params="" if ! defined $params;
+
+ if (length $escape) {
+ return "[[$prefix$command $params]]";
+ }
+ elsif (exists $hooks{preprocess}{$command}) {
+ return "" if $scan && ! $hooks{preprocess}{$command}{scan};
+ # Note: preserve order of params, some plugins may
+ # consider it significant.
+ my @params;
+ while ($params =~ m{
+ (?:([-.\w]+)=)? # 1: named parameter key?
+ (?:
+ """(.*?)""" # 2: triple-quoted value
+ |
+ "([^"]*?)" # 3: single-quoted 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
+ }msgx) {
+ my $key=$1;
+ my $val;
+ if (defined $2) {
+ $val=$2;
+ $val=~s/\r\n/\n/mg;
+ $val=~s/^\n+//g;
+ $val=~s/\n+$//g;
+ }
+ elsif (defined $3) {
+ $val=$3;
+ }
+ elsif (defined $4) {
+ $val=$4;
+ }
+ elsif (defined $7) {
+ $val=$7;
+ }
+ elsif (defined $6) {
+ $val=$6;
+ }
+
+ if (defined $key) {
+ push @params, $key, $val;
+ }
+ else {
+ push @params, $val, '';
+ }
+ }
+ if ($preprocessing{$page}++ > 8) {
+ # Avoid loops of preprocessed pages preprocessing
+ # other pages that preprocess them, etc.
+ return "[[!$command <span class=\"error\">".
+ sprintf(gettext("preprocessing loop detected on %s at depth %i"),
+ $page, $preprocessing{$page}).
+ "</span>]]";
+ }
+ my $ret;
+ if (! $scan) {
+ $ret=eval {
+ $hooks{preprocess}{$command}{call}->(
+ @params,
+ page => $page,
+ destpage => $destpage,
+ preview => $preprocess_preview,
+ );
+ };
+ if ($@) {
+ my $error=$@;
+ chomp $error;
+ eval q{use HTML::Entities};
+ $error = encode_entities($error);
+ $ret="[[!$command <span class=\"error\">".
+ gettext("Error").": $error"."</span>]]";
+ }
+ }
+ else {
+ # use void context during scan pass
+ eval {
+ $hooks{preprocess}{$command}{call}->(
+ @params,
+ page => $page,
+ destpage => $destpage,
+ preview => $preprocess_preview,
+ );
+ };
+ $ret="";
+ }
+ $preprocessing{$page}--;
+ return $ret;
+ }
+ else {
+ return "[[$prefix$command $params]]";
+ }
+ };
+
+ my $regex;
+ if ($config{prefix_directives}) {
+ $regex = qr{
+ (\\?) # 1: escape?
+ \[\[(!) # directive open; 2: prefix
+ ([-\w]+) # 3: command
+ ( # 4: the parameters..
+ \s+ # Must have space if parameters present
+ (?:
+ (?:[-.\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
+ # of directive
+ )
+ *)? # 0 or more parameters
+ \]\] # directive closed
+ }sx;
+ }
+ else {
+ $regex = qr{
+ (\\?) # 1: escape?
+ \[\[(!?) # directive open; 2: optional prefix
+ ([-\w]+) # 3: command
+ \s+
+ ( # 4: the parameters..
+ (?:
+ (?:[-.\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
+ # of directive
+ )
+ *) # 0 or more parameters
+ \]\] # directive closed
+ }sx;
+ }
+
+ $content =~ s{$regex}{$handle->($1, $2, $3, $4)}eg;
+ return $content;
+}
+
+sub filter ($$$) {
+ my $page=shift;
+ my $destpage=shift;
+ my $content=shift;
+
+ run_hooks(filter => sub {
+ $content=shift->(page => $page, destpage => $destpage,
+ content => $content);
+ });
+
+ return $content;
+}
+
+sub check_canedit ($$$;$) {
+ my $page=shift;
+ my $q=shift;
+ my $session=shift;
+ my $nonfatal=shift;
+
+ my $canedit;
+ run_hooks(canedit => sub {
+ return if defined $canedit;
+ my $ret=shift->($page, $q, $session);
+ if (defined $ret) {
+ if ($ret eq "") {
+ $canedit=1;
+ }
+ elsif (ref $ret eq 'CODE') {
+ $ret->() unless $nonfatal;
+ $canedit=0;
+ }
+ elsif (defined $ret) {
+ error($ret) unless $nonfatal;
+ $canedit=0;
+ }
+ }
+ });
+ return defined $canedit ? $canedit : 1;
+}
+
+sub check_content (@) {
+ my %params=@_;
+
+ return 1 if ! exists $hooks{checkcontent}; # optimisation
+
+ if (exists $pagesources{$params{page}}) {
+ my @diff;
+ my %old=map { $_ => 1 }
+ split("\n", readfile(srcfile($pagesources{$params{page}})));
+ foreach my $line (split("\n", $params{content})) {
+ push @diff, $line if ! exists $old{$line};
+ }
+ $params{diff}=join("\n", @diff);
+ }
+
+ my $ok;
+ run_hooks(checkcontent => sub {
+ return if defined $ok;
+ my $ret=shift->(%params);
+ if (defined $ret) {
+ if ($ret eq "") {
+ $ok=1;
+ }
+ elsif (ref $ret eq 'CODE') {
+ $ret->() unless $params{nonfatal};
+ $ok=0;
+ }
+ elsif (defined $ret) {
+ error($ret) unless $params{nonfatal};
+ $ok=0;
+ }
+ }
+
+ });
+ 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 () {
+ # 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}) {
+ mkdir($config{wikistatedir});
+ }
+ open($wikilock, '>', "$config{wikistatedir}/lockfile") ||
+ error ("cannot write to $config{wikistatedir}/lockfile: $!");
+ 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;
+}
+
+my $commitlock;
+
+sub commit_hook_enabled () {
+ open($commitlock, '+>', "$config{wikistatedir}/commitlock") ||
+ error("cannot write to $config{wikistatedir}/commitlock: $!");
+ if (! flock($commitlock, 1 | 4)) { # LOCK_SH | LOCK_NB to test
+ close($commitlock) || error("failed closing commitlock: $!");
+ return 0;
+ }
+ close($commitlock) || error("failed closing commitlock: $!");
+ return 1;
+}
+
+sub disable_commit_hook () {
+ open($commitlock, '>', "$config{wikistatedir}/commitlock") ||
+ error("cannot write to $config{wikistatedir}/commitlock: $!");
+ if (! flock($commitlock, 2)) { # LOCK_EX
+ error("failed to get commit lock");
+ }
+ return 1;
+}
+
+sub enable_commit_hook () {
+ return close($commitlock) if $commitlock;
+ return;
+}
+
+sub loadindex () {
+ %oldrenderedfiles=%pagectime=();
+ my $rebuild=$config{rebuild};
+ if (! $rebuild) {
+ %pagesources=%pagemtime=%oldlinks=%links=%depends=
+ %destsources=%renderedfiles=%pagecase=%pagestate=
+ %depends_simple=%typedlinks=%oldtypedlinks=();
+ }
+ my $in;
+ if (! open ($in, "<", "$config{wikistatedir}/indexdb")) {
+ if (-e "$config{wikistatedir}/index") {
+ system("ikiwiki-transition", "indexdb", $config{srcdir});
+ open ($in, "<", "$config{wikistatedir}/indexdb") || return;
+ }
+ else {
+ # gettime on first build
+ $config{gettime}=1 unless defined $config{gettime};
+ return;
+ }
+ }
+
+ my $index=Storable::fd_retrieve($in);
+ if (! defined $index) {
+ return 0;
+ }
+
+ my $pages;
+ 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;
+ %wikistate=();
+ }
+
+ foreach my $src (keys %$pages) {
+ my $d=$pages->{$src};
+ my $page;
+ if (exists $d->{page} && ! $rebuild) {
+ $page=$d->{page};
+ }
+ else {
+ $page=pagename($src);
+ }
+ $pagectime{$page}=$d->{ctime};
+ $pagesources{$page}=$src;
+ if (! $rebuild) {
+ $pagemtime{$page}=$d->{mtime};
+ $renderedfiles{$page}=$d->{dest};
+ if (exists $d->{links} && ref $d->{links}) {
+ $links{$page}=$d->{links};
+ $oldlinks{$page}=[@{$d->{links}}];
+ }
+ if (ref $d->{depends_simple} eq 'ARRAY') {
+ # old format
+ $depends_simple{$page}={
+ map { $_ => 1 } @{$d->{depends_simple}}
+ };
+ }
+ elsif (exists $d->{depends_simple}) {
+ $depends_simple{$page}=$d->{depends_simple};
+ }
+ if (exists $d->{dependslist}) {
+ # old format
+ $depends{$page}={
+ map { $_ => $DEPEND_CONTENT }
+ @{$d->{dependslist}}
+ };
+ }
+ elsif (exists $d->{depends} && ! ref $d->{depends}) {
+ # old format
+ $depends{$page}={$d->{depends} => $DEPEND_CONTENT };
+ }
+ elsif (exists $d->{depends}) {
+ $depends{$page}=$d->{depends};
+ }
+ if (exists $d->{state}) {
+ $pagestate{$page}=$d->{state};
+ }
+ if (exists $d->{typedlinks}) {
+ $typedlinks{$page}=$d->{typedlinks};
+
+ while (my ($type, $links) = each %{$typedlinks{$page}}) {
+ next unless %$links;
+ $oldtypedlinks{$page}{$type} = {%$links};
+ }
+ }
+ }
+ $oldrenderedfiles{$page}=[@{$d->{dest}}];
+ }
+ foreach my $page (keys %pagesources) {
+ $pagecase{lc $page}=$page;
+ }
+ 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 @plugins=keys %loaded_plugins;
+
+ if (! -d $config{wikistatedir}) {
+ mkdir($config{wikistatedir});
+ }
+ 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{page}{$src}={
+ page => $page,
+ ctime => $pagectime{$page},
+ mtime => $pagemtime{$page},
+ dest => $renderedfiles{$page},
+ links => $links{$page},
+ };
+
+ if (exists $depends{$page}) {
+ $index{page}{$src}{depends} = $depends{$page};
+ }
+
+ if (exists $depends_simple{$page}) {
+ $index{page}{$src}{depends_simple} = $depends_simple{$page};
+ }
+
+ if (exists $typedlinks{$page} && %{$typedlinks{$page}}) {
+ $index{page}{$src}{typedlinks} = $typedlinks{$page};
+ }
+
+ if (exists $pagestate{$page}) {
+ $index{page}{$src}{state}=$pagestate{$page};
+ }
+ }
+
+ $index{state}={};
+ 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;
+ close $out || error("failed saving to $newfile: $!", $cleanup);
+ rename($newfile, "$config{wikistatedir}/indexdb") ||
+ error("failed renaming $newfile to $config{wikistatedir}/indexdb", $cleanup);
+
+ return 1;
+}
+
+sub template_file ($) {
+ 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);
+ }
+
+ 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_depends ($$;@) {
+ my $name=shift;
+ my $page=shift;
+
+ 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);
+ }
+
+ my @opts=(
+ filter => sub {
+ my $text_ref = shift;
+ ${$text_ref} = decode_utf8(${$text_ref});
+ run_hooks(readtemplate => sub {
+ ${$text_ref} = shift->(
+ id => $name,
+ page => $tpage,
+ content => ${$text_ref},
+ untrusted => $untrusted,
+ );
+ });
+ },
+ loop_context_vars => 1,
+ die_on_bad_params => 0,
+ parent_global_vars => 1,
+ filename => $filename,
+ @_,
+ ($untrusted ? (no_includes => 1) : ()),
+ );
+ return @opts if wantarray;
+
+ require HTML::Template;
+ return HTML::Template->new(@opts);
+}
+
+sub template ($;@) {
+ template_depends(shift, undef, @_);
+}
+
+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);
+ });
+ $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 (@) {
+ my %param=@_;
+
+ if (! exists $param{type} || ! ref $param{call} || ! exists $param{id}) {
+ error 'hook requires type, call, and id parameters';
+ }
+
+ return if $param{no_override} && exists $hooks{$param{type}}{$param{id}};
+
+ $hooks{$param{type}}{$param{id}}=\%param;
+ return 1;
+}
+
+sub run_hooks ($$) {
+ # Calls the given sub for each hook of the given type,
+ # passing it the hook function to call.
+ my $type=shift;
+ my $sub=shift;
+
+ if (exists $hooks{$type}) {
+ my (@first, @middle, @last);
+ foreach my $id (keys %{$hooks{$type}}) {
+ if ($hooks{$type}{$id}{first}) {
+ push @first, $id;
+ }
+ elsif ($hooks{$type}{$id}{last}) {
+ push @last, $id;
+ }
+ else {
+ push @middle, $id;
+ }
+ }
+ foreach my $id (@first, @middle, @last) {
+ $sub->($hooks{$type}{$id}{call});
+ }
+ }
+
+ 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_getmtime ($) {
+ $hooks{rcs}{rcs_getmtime}{call}->(@_);
+}
+
+sub rcs_receive () {
+ $hooks{rcs}{rcs_receive}{call}->();
+}
+
+sub add_depends ($$;$) {
+ my $page=shift;
+ my $pagespec=shift;
+ my $deptype=shift || $DEPEND_CONTENT;
+
+ # Is the pagespec a simple page name?
+ if ($pagespec =~ /$config{wiki_file_regexp}/ &&
+ $pagespec !~ /[\s*?()!]/) {
+ $depends_simple{$page}{lc $pagespec} |= $deptype;
+ return 1;
+ }
+
+ # Add explicit dependencies for influences.
+ my $sub=pagespec_translate($pagespec);
+ return unless defined $sub;
+ foreach my $p (keys %pagesources) {
+ my $r=$sub->($p, location => $page);
+ my $i=$r->influences;
+ my $static=$r->influences_static;
+ foreach my $k (keys %$i) {
+ next unless $r || $static || $k eq $page;
+ $depends_simple{$page}{lc $k} |= $i->{$k};
+ }
+ last if $static;
+ }
+
+ $depends{$page}{$pagespec} |= $deptype;
+ return 1;
+}
+
+sub deptype (@) {
+ my $deptype=0;
+ foreach my $type (@_) {
+ if ($type eq 'presence') {
+ $deptype |= $DEPEND_PRESENCE;
+ }
+ elsif ($type eq 'links') {
+ $deptype |= $DEPEND_LINKS;
+ }
+ elsif ($type eq 'content') {
+ $deptype |= $DEPEND_CONTENT;
+ }
+ }
+ return $deptype;
+}
+
+my $file_prune_regexp;
+sub file_pruned ($) {
+ my $file=shift;
+
+ if (defined $config{include} && length $config{include}) {
+ return 0 if $file =~ m/$config{include}/;
+ }
+
+ if (! defined $file_prune_regexp) {
+ $file_prune_regexp='('.join('|', @{$config{wiki_file_prune_regexps}}).')';
+ $file_prune_regexp=qr/$file_prune_regexp/;
+ }
+ return $file =~ m/$file_prune_regexp/;
+}
+
+sub define_gettext () {
+ # If translation is needed, redefine the gettext function to do it.
+ # Otherwise, it becomes a quick no-op.
+ my $gettext_obj;
+ my $getobj;
+ if ((exists $ENV{LANG} && length $ENV{LANG}) ||
+ (exists $ENV{LC_ALL} && length $ENV{LC_ALL}) ||
+ (exists $ENV{LC_MESSAGES} && length $ENV{LC_MESSAGES})) {
+ $getobj=sub {
+ $gettext_obj=eval q{
+ use Locale::gettext q{textdomain};
+ Locale::gettext->domain('ikiwiki')
+ };
+ };
+ }
+
+ no warnings 'redefine';
+ *gettext=sub {
+ $getobj->() if $getobj;
+ if ($gettext_obj) {
+ $gettext_obj->get(shift);
+ }
+ else {
+ return shift;
+ }
+ };
+ *ngettext=sub {
+ $getobj->() if $getobj;
+ if ($gettext_obj) {
+ $gettext_obj->nget(@_);
+ }
+ else {
+ return ($_[2] == 1 ? $_[0] : $_[1])
+ }
+ };
+}
+
+sub gettext {
+ define_gettext();
+ gettext(@_);
+}
+
+sub ngettext {
+ define_gettext();
+ ngettext(@_);
+}
+
+sub yesno ($) {
+ my $val=shift;
+
+ return (defined $val && (lc($val) eq gettext("yes") || lc($val) eq "yes" || $val eq "1"));
+}
+
+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 add_link ($$;$) {
+ my $page=shift;
+ my $link=shift;
+ my $type=shift;
+
+ push @{$links{$page}}, $link
+ unless grep { $_ eq $link } @{$links{$page}};
+
+ if (defined $type) {
+ $typedlinks{$page}{$type}{$link} = 1;
+ }
+}
+
+sub add_autofile ($$$) {
+ my $file=shift;
+ my $plugin=shift;
+ my $generator=shift;
+
+ $autofiles{$file}{plugin}=$plugin;
+ $autofiles{$file}{generator}=$generator;
+}
+
+sub useragent () {
+ eval q{use LWP};
+ error($@) if $@;
+
+ 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;
+
+ my $code = "";
+ my @data;
+ while ($spec =~ m{
+ \s*
+ (-?) # group 1: perhaps negated
+ \s*
+ ( # group 2: a word
+ \w+\([^\)]*\) # command(params)
+ |
+ [^\s]+ # or anything else
+ )
+ \s*
+ }gx) {
+ my $negated = $1;
+ my $word = $2;
+ my $params = undef;
+
+ if ($word =~ m/^(\w+)\((.*)\)$/) {
+ # command with parameters
+ $params = $2;
+ $word = $1;
+ }
+ elsif ($word !~ m/^\w+$/) {
+ error(sprintf(gettext("invalid sort type %s"), $word));
+ }
+
+ if (length $code) {
+ $code .= " || ";
+ }
+
+ if ($negated) {
+ $code .= "-";
+ }
+
+ if (exists $IkiWiki::SortSpec::{"cmp_$word"}) {
+ if (defined $params) {
+ push @data, $params;
+ $code .= "IkiWiki::SortSpec::cmp_$word(\$data[$#data])";
+ }
+ else {
+ $code .= "IkiWiki::SortSpec::cmp_$word(undef)";
+ }
+ }
+ else {
+ error(sprintf(gettext("unknown sort type %s"), $word));
+ }
+ }
+
+ if (! length $code) {
+ # undefined sorting method... sort arbitrarily
+ return sub { 0 };
+ }
+
+ if ($reverse) {
+ $code="-($code)";
+ }
+
+ no warnings;
+ return eval 'sub { '.$code.' }';
+}
+
+sub pagespec_translate ($) {
+ my $spec=shift;
+
+ # Convert spec to perl code.
+ my $code="";
+ my @data;
+ while ($spec=~m{
+ \s* # ignore whitespace
+ ( # 1: match a single word
+ \! # !
+ |
+ \( # (
+ |
+ \) # )
+ |
+ \w+\([^\)]*\) # command(params)
+ |
+ [^\s()]+ # any other text
+ )
+ \s* # ignore whitespace
+ }gx) {
+ my $word=$1;
+ if (lc $word eq 'and') {
+ $code.=' &';
+ }
+ elsif (lc $word eq 'or') {
+ $code.=' |';
+ }
+ elsif ($word eq "(" || $word eq ")" || $word eq "!") {
+ $code.=' '.$word;
+ }
+ elsif ($word =~ /^(\w+)\((.*)\)$/) {
+ if (exists $IkiWiki::PageSpec::{"match_$1"}) {
+ push @data, $2;
+ $code.="IkiWiki::PageSpec::match_$1(\$page, \$data[$#data], \@_)";
+ }
+ else {
+ push @data, qq{unknown function in pagespec "$word"};
+ $code.="IkiWiki::ErrorReason->new(\$data[$#data])";
+ }
+ }
+ else {
+ push @data, $word;
+ $code.=" IkiWiki::PageSpec::match_glob(\$page, \$data[$#data], \@_)";
+ }
+ }
+
+ if (! length $code) {
+ $code="IkiWiki::FailReason->new('empty pagespec')";
+ }
+
+ no warnings;
+ return eval 'sub { my $page=shift; '.$code.' }';
+}
+
+sub pagespec_match ($$;@) {
+ my $page=shift;
+ my $spec=shift;
+ my @params=@_;
+
+ # Backwards compatability with old calling convention.
+ if (@params == 1) {
+ unshift @params, 'location';
+ }
+
+ my $sub=pagespec_translate($spec);
+ return IkiWiki::ErrorReason->new("syntax error in pagespec \"$spec\"")
+ if ! defined $sub;
+ return $sub->($page, @params);
+}
+
+# e.g. @pages = sort_pages("title", \@pages, reverse => "yes")
+#
+# Not exported yet, but could be in future if it is generally useful.
+# Note that this signature is not the same as IkiWiki::SortSpec::sort_pages,
+# which is "more internal".
+sub sort_pages ($$;@) {
+ my $sort = shift;
+ my $list = shift;
+ my %params = @_;
+ $sort = sortspec_translate($sort, $params{reverse});
+ return IkiWiki::SortSpec::sort_pages($sort, @$list);
+}
+
+sub pagespec_match_list ($$;@) {
+ my $page=shift;
+ my $pagespec=shift;
+ my %params=@_;
+
+ # Backwards compatability with old calling convention.
+ if (ref $page) {
+ print STDERR "warning: a plugin (".caller().") is using pagespec_match_list in an obsolete way, and needs to be updated\n";
+ $params{list}=$page;
+ $page=$params{location}; # ugh!
+ }
+
+ my $sub=pagespec_translate($pagespec);
+ error "syntax error in pagespec \"$pagespec\""
+ if ! defined $sub;
+ my $sort=sortspec_translate($params{sort}, $params{reverse})
+ if defined $params{sort};
+
+ my @candidates;
+ if (exists $params{list}) {
+ @candidates=exists $params{filter}
+ ? grep { ! $params{filter}->($_) } @{$params{list}}
+ : @{$params{list}};
+ }
+ else {
+ @candidates=exists $params{filter}
+ ? grep { ! $params{filter}->($_) } keys %pagesources
+ : keys %pagesources;
+ }
+
+ # clear params, remainder is passed to pagespec
+ $depends{$page}{$pagespec} |= ($params{deptype} || $DEPEND_CONTENT);
+ my $num=$params{num};
+ delete @params{qw{num deptype reverse sort filter list}};
+
+ # when only the top matches will be returned, it's efficient to
+ # sort before matching to pagespec,
+ if (defined $num && defined $sort) {
+ @candidates=IkiWiki::SortSpec::sort_pages(
+ $sort, @candidates);
+ }
+
+ my @matches;
+ my $firstfail;
+ my $count=0;
+ my $accum=IkiWiki::SuccessReason->new();
+ foreach my $p (@candidates) {
+ my $r=$sub->($p, %params, location => $page);
+ error(sprintf(gettext("cannot match pages: %s"), $r))
+ if $r->isa("IkiWiki::ErrorReason");
+ unless ($r || $r->influences_static) {
+ $r->remove_influence($p);
+ }
+ $accum |= $r;
+ if ($r) {
+ push @matches, $p;
+ last if defined $num && ++$count == $num;
+ }
+ }
+
+ # Add simple dependencies for accumulated influences.
+ my $i=$accum->influences;
+ foreach my $k (keys %$i) {
+ $depends_simple{$page}{lc $k} |= $i->{$k};
+ }
+
+ # when all matches will be returned, it's efficient to
+ # sort after matching
+ if (! defined $num && defined $sort) {
+ return IkiWiki::SortSpec::sort_pages(
+ $sort, @matches);
+ }
+ else {
+ return @matches;
+ }
+}
+
+sub pagespec_valid ($) {
+ my $spec=shift;
+
+ return defined pagespec_translate($spec);
+}
+
+sub glob2re ($) {
+ my $re=quotemeta(shift);
+ $re=~s/\\\*/.*/g;
+ $re=~s/\\\?/./g;
+ return qr/^$re$/i;
+}
+
+package IkiWiki::FailReason;
+
+use overload (
+ '""' => sub { $_[0][0] },
+ '0+' => sub { 0 },
+ '!' => sub { bless $_[0], 'IkiWiki::SuccessReason'},
+ '&' => sub { $_[0]->merge_influences($_[1], 1); $_[0] },
+ '|' => sub { $_[1]->merge_influences($_[0]); $_[1] },
+ fallback => 1,
+);
+
+our @ISA = 'IkiWiki::SuccessReason';
+
+package IkiWiki::SuccessReason;
+
+# A blessed array-ref:
+#
+# [0]: human-readable reason for success (or, in FailReason subclass, failure)
+# [1]{""}:
+# - if absent or false, the influences of this evaluation are "static",
+# see the influences_static method
+# - if true, they are dynamic (not static)
+# [1]{any other key}:
+# the dependency types of influences, as returned by the influences method
+
+use overload (
+ # in string context, it's the human-readable reason
+ '""' => sub { $_[0][0] },
+ # in boolean context, SuccessReason is 1 and FailReason is 0
+ '0+' => sub { 1 },
+ # negating a result gives the opposite result with the same influences
+ '!' => sub { bless $_[0], 'IkiWiki::FailReason'},
+ # A & B = (A ? B : A) with the influences of both
+ '&' => sub { $_[1]->merge_influences($_[0], 1); $_[1] },
+ # A | B = (A ? A : B) with the influences of both
+ '|' => sub { $_[0]->merge_influences($_[1]); $_[0] },
+ fallback => 1,
+);
+
+# SuccessReason->new("human-readable reason", page => deptype, ...)
+
+sub new {
+ my $class = shift;
+ my $value = shift;
+ return bless [$value, {@_}], $class;
+}
+
+# influences(): return a reference to a copy of the hash
+# { page => dependency type } describing the pages that indirectly influenced
+# this result, but would not cause a dependency through ikiwiki's core
+# dependency logic.
+#
+# See [[todo/dependency_types]] for extensive discussion of what this means.
+#
+# influences(page => deptype, ...): remove all influences, replace them
+# with the arguments, and return a reference to a copy of the new influences.
+
+sub influences {
+ my $this=shift;
+ $this->[1]={@_} if @_;
+ my %i=%{$this->[1]};
+ delete $i{""};
+ return \%i;
+}
+
+# True if this result has the same influences whichever page it matches,
+# For instance, whether bar matches backlink(foo) is influenced only by
+# the set of links in foo, so its only influence is { foo => DEPEND_LINKS },
+# which does not mention bar anywhere.
+#
+# False if this result would have different influences when matching
+# different pages. For instance, when testing whether link(foo) matches bar,
+# { bar => DEPEND_LINKS } is an influence on that result, because changing
+# bar's links could change the outcome; so its influences are not the same
+# as when testing whether link(foo) matches baz.
+#
+# Static influences are one of the things that make pagespec_match_list
+# more efficient than repeated calls to pagespec_match.
+
+sub influences_static {
+ return ! $_[0][1]->{""};
+}
+
+# Change the influences of $this to be the influences of "$this & $other"
+# or "$this | $other".
+#
+# If both $this and $other are either successful or have influences,
+# or this is an "or" operation, the result has all the influences from
+# either of the arguments. It has dynamic influences if either argument
+# has dynamic influences.
+#
+# If this is an "and" operation, and at least one argument is a
+# FailReason with no influences, the result has no influences, and they
+# are not dynamic. For instance, link(foo) matching bar is influenced
+# by bar, but enabled(ddate) has no influences. Suppose ddate is disabled;
+# then (link(foo) and enabled(ddate)) not matching bar is not influenced by
+# bar, because it would be false however often you edit bar.
+
+sub merge_influences {
+ my $this=shift;
+ my $other=shift;
+ my $anded=shift;
+
+ # This "if" is odd because it needs to avoid negating $this
+ # or $other, which would alter the objects in-place. Be careful.
+ if (! $anded || (($this || %{$this->[1]}) &&
+ ($other || %{$other->[1]}))) {
+ foreach my $influence (keys %{$other->[1]}) {
+ $this->[1]{$influence} |= $other->[1]{$influence};
+ }
+ }
+ else {
+ # influence blocker
+ $this->[1]={};
+ }
+}
+
+# Change $this so it is not considered to be influenced by $torm.
+
+sub remove_influence {
+ my $this=shift;
+ my $torm=shift;
+
+ delete $this->[1]{$torm};
+}
+
+package IkiWiki::ErrorReason;
+
+our @ISA = 'IkiWiki::FailReason';
+
+package IkiWiki::PageSpec;
+
+sub derel ($$) {
+ my $path=shift;
+ my $from=shift;
+
+ 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;
+ my %params=@_;
+
+ $glob=derel($glob, $params{location});
+
+ # 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");
+ }
+ else {
+ return IkiWiki::FailReason->new("$glob matches $page, but the page is an internal page");
+ }
+ }
+ else {
+ return IkiWiki::FailReason->new("$glob does not match $page");
+ }
+}
+
+sub match_internal ($$;@) {
+ 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 ($$;@) {
+ my $page=shift;
+ my $link=lc(shift);
+ my %params=@_;
+
+ $link=derel($link, $params{location});
+ my $from=exists $params{location} ? $params{location} : '';
+ my $linktype=$params{linktype};
+ my $qualifier='';
+ if (defined $linktype) {
+ $qualifier=" with type $linktype";
+ }
+
+ my $links = $IkiWiki::links{$page};
+ return IkiWiki::FailReason->new("$page has no links", $page => $IkiWiki::DEPEND_LINKS, "" => 1)
+ 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 ($bestlink eq IkiWiki::bestlink($page, $p)) {
+ return IkiWiki::SuccessReason->new("$page links to $link$qualifier", $page => $IkiWiki::DEPEND_LINKS, "" => 1)
+ }
+ }
+ else {
+ 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 (match_glob($p_rel, $link, %params)) {
+ return IkiWiki::SuccessReason->new("$page links to page $p_rel$qualifier, matching $link", $page => $IkiWiki::DEPEND_LINKS, "" => 1)
+ }
+ }
+ }
+ return IkiWiki::FailReason->new("$page does not link to $link$qualifier", $page => $IkiWiki::DEPEND_LINKS, "" => 1);
+}
+
+sub match_backlink ($$;@) {
+ 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;
+}
+
+sub match_created_before ($$;@) {
+ my $page=shift;
+ my $testpage=shift;
+ my %params=@_;
+
+ $testpage=derel($testpage, $params{location});
+
+ if (exists $IkiWiki::pagectime{$testpage}) {
+ if ($IkiWiki::pagectime{$page} < $IkiWiki::pagectime{$testpage}) {
+ return IkiWiki::SuccessReason->new("$page created before $testpage", $testpage => $IkiWiki::DEPEND_PRESENCE);
+ }
+ else {
+ return IkiWiki::FailReason->new("$page not created before $testpage", $testpage => $IkiWiki::DEPEND_PRESENCE);
+ }
+ }
+ else {
+ return IkiWiki::ErrorReason->new("$testpage does not exist", $testpage => $IkiWiki::DEPEND_PRESENCE);
+ }
+}
+
+sub match_created_after ($$;@) {
+ my $page=shift;
+ my $testpage=shift;
+ my %params=@_;
+
+ $testpage=derel($testpage, $params{location});
+
+ if (exists $IkiWiki::pagectime{$testpage}) {
+ if ($IkiWiki::pagectime{$page} > $IkiWiki::pagectime{$testpage}) {
+ return IkiWiki::SuccessReason->new("$page created after $testpage", $testpage => $IkiWiki::DEPEND_PRESENCE);
+ }
+ else {
+ return IkiWiki::FailReason->new("$page not created after $testpage", $testpage => $IkiWiki::DEPEND_PRESENCE);
+ }
+ }
+ else {
+ return IkiWiki::ErrorReason->new("$testpage does not exist", $testpage => $IkiWiki::DEPEND_PRESENCE);
+ }
+}
+
+sub match_creation_day ($$;@) {
+ 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 {
+ return IkiWiki::FailReason->new('creation_day did not match');
+ }
+}
+
+sub match_creation_month ($$;@) {
+ 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 {
+ return IkiWiki::FailReason->new('creation_month did not match');
+ }
+}
+
+sub match_creation_year ($$;@) {
+ 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 {
+ return IkiWiki::FailReason->new('creation_year did not match');
+ }
+}
+
+sub match_user ($$;@) {
+ shift;
+ my $user=shift;
+ my %params=@_;
+
+ if (! exists $params{user}) {
+ return IkiWiki::ErrorReason->new("no user specified");
+ }
+
+ my $regexp=IkiWiki::glob2re($user);
+
+ if (defined $params{user} && $params{user}=~$regexp) {
+ 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::ErrorReason->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::ErrorReason->new("no IP specified");
+ }
+
+ my $regexp=IkiWiki::glob2re(lc $ip);
+
+ if (defined $params{ip} && lc $params{ip}=~$regexp) {
+ return IkiWiki::SuccessReason->new("IP is $ip");
+ }
+ else {
+ return IkiWiki::FailReason->new("IP is $params{ip}, not $ip");
+ }
+}
+
+package IkiWiki::SortSpec;
+
+# This is in the SortSpec namespace so that the $a and $b that sort() uses
+# are easily available in this namespace, for cmp functions to use them.
+sub sort_pages {
+ my $f=shift;
+ sort $f @_
+}
+
+sub cmp_title {
+ IkiWiki::pagetitle(IkiWiki::basename($a))
+ cmp
+ 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} }
+
+1
diff -Nru ikiwiki-3.20141016.4/.pc/CVE-2019-9187-3.patch/IkiWiki/Plugin/aggregate.pm ikiwiki-3.20141016.4+deb8u1/.pc/CVE-2019-9187-3.patch/IkiWiki/Plugin/aggregate.pm
--- ikiwiki-3.20141016.4/.pc/CVE-2019-9187-3.patch/IkiWiki/Plugin/aggregate.pm 1970-01-01 10:00:00.000000000 +1000
+++ ikiwiki-3.20141016.4+deb8u1/.pc/CVE-2019-9187-3.patch/IkiWiki/Plugin/aggregate.pm 2019-03-07 17:32:54.000000000 +1100
@@ -0,0 +1,789 @@
+#!/usr/bin/perl
+# Feed aggregation plugin.
+package IkiWiki::Plugin::aggregate;
+
+use warnings;
+use strict;
+use IkiWiki 3.00;
+use HTML::Parser;
+use HTML::Tagset;
+use HTML::Entities;
+use open qw{:utf8 :std};
+
+my %feeds;
+my %guids;
+
+sub import {
+ hook(type => "getopt", id => "aggregate", call => \&getopt);
+ hook(type => "getsetup", id => "aggregate", call => \&getsetup);
+ hook(type => "checkconfig", id => "aggregate", call => \&checkconfig,
+ last => 1);
+ hook(type => "needsbuild", id => "aggregate", call => \&needsbuild);
+ hook(type => "preprocess", id => "aggregate", call => \&preprocess);
+ hook(type => "delete", id => "aggregate", call => \&delete);
+ hook(type => "savestate", id => "aggregate", call => \&savestate);
+ hook(type => "htmlize", id => "_aggregated", call => \&htmlize);
+ if (exists $config{aggregate_webtrigger} && $config{aggregate_webtrigger}) {
+ hook(type => "cgi", id => "aggregate", call => \&cgi);
+ }
+}
+
+sub getopt () {
+ eval q{use Getopt::Long};
+ error($@) if $@;
+ Getopt::Long::Configure('pass_through');
+ GetOptions(
+ "aggregate" => \$config{aggregate},
+ "aggregateinternal!" => \$config{aggregateinternal},
+ );
+}
+
+sub getsetup () {
+ return
+ plugin => {
+ safe => 1,
+ rebuild => undef,
+ },
+ aggregateinternal => {
+ type => "boolean",
+ example => 1,
+ description => "enable aggregation to internal pages?",
+ safe => 0, # enabling needs manual transition
+ rebuild => 0,
+ },
+ aggregate_webtrigger => {
+ type => "boolean",
+ example => 0,
+ description => "allow aggregation to be triggered via the web?",
+ safe => 1,
+ rebuild => 0,
+ },
+}
+
+sub checkconfig () {
+ if (! defined $config{aggregateinternal}) {
+ $config{aggregateinternal}=1;
+ }
+
+ # This is done here rather than in a refresh hook because it
+ # needs to run before the wiki is locked.
+ if ($config{aggregate} && ! ($config{post_commit} &&
+ IkiWiki::commit_hook_enabled())) {
+ launchaggregation();
+ }
+}
+
+sub cgi ($) {
+ my $cgi=shift;
+
+ if (defined $cgi->param('do') &&
+ $cgi->param("do") eq "aggregate_webtrigger") {
+ $|=1;
+ print "Content-Type: text/plain\n\n";
+ $config{cgi}=0;
+ $config{verbose}=1;
+ $config{syslog}=0;
+ print gettext("Aggregation triggered via web.")."\n\n";
+ if (launchaggregation()) {
+ IkiWiki::lockwiki();
+ IkiWiki::loadindex();
+ require IkiWiki::Render;
+ IkiWiki::refresh();
+ IkiWiki::saveindex();
+ }
+ else {
+ print gettext("Nothing to do right now, all feeds are up-to-date!")."\n";
+ }
+ exit 0;
+ }
+}
+
+sub launchaggregation () {
+ # See if any feeds need aggregation.
+ loadstate();
+ my @feeds=needsaggregate();
+ return unless @feeds;
+ if (! lockaggregate()) {
+ error("an aggregation process is already running");
+ }
+ # force a later rebuild of source pages
+ $IkiWiki::forcerebuild{$_->{sourcepage}}=1
+ foreach @feeds;
+
+ # Fork a child process to handle the aggregation.
+ # The parent process will then handle building the
+ # result. This avoids messy code to clear state
+ # accumulated while aggregating.
+ defined(my $pid = fork) or error("Can't fork: $!");
+ if (! $pid) {
+ IkiWiki::loadindex();
+ # Aggregation happens without the main wiki lock
+ # being held. This allows editing pages etc while
+ # aggregation is running.
+ aggregate(@feeds);
+
+ IkiWiki::lockwiki;
+ # Merge changes, since aggregation state may have
+ # changed on disk while the aggregation was happening.
+ mergestate();
+ expire();
+ savestate();
+ IkiWiki::unlockwiki;
+ exit 0;
+ }
+ waitpid($pid,0);
+ if ($?) {
+ error "aggregation failed with code $?";
+ }
+
+ clearstate();
+ unlockaggregate();
+
+ return 1;
+}
+
+# Pages with extension _aggregated have plain html markup, pass through.
+sub htmlize (@) {
+ my %params=@_;
+ return $params{content};
+}
+
+# Used by ikiwiki-transition aggregateinternal.
+sub migrate_to_internal {
+ if (! lockaggregate()) {
+ error("an aggregation process is currently running");
+ }
+
+ IkiWiki::lockwiki();
+ loadstate();
+ $config{verbose}=1;
+
+ foreach my $data (values %guids) {
+ next unless $data->{page};
+ next if $data->{expired};
+
+ $config{aggregateinternal} = 0;
+ my $oldname = "$config{srcdir}/".htmlfn($data->{page});
+ if (! -e $oldname) {
+ $oldname = $IkiWiki::Plugin::transient::transientdir."/".htmlfn($data->{page});
+ }
+
+ my $oldoutput = $config{destdir}."/".IkiWiki::htmlpage($data->{page});
+
+ $config{aggregateinternal} = 1;
+ my $newname = $IkiWiki::Plugin::transient::transientdir."/".htmlfn($data->{page});
+
+ debug "moving $oldname -> $newname";
+ if (-e $newname) {
+ if (-e $oldname) {
+ error("$newname already exists");
+ }
+ else {
+ debug("already renamed to $newname?");
+ }
+ }
+ elsif (-e $oldname) {
+ rename($oldname, $newname) || error("$!");
+ }
+ else {
+ debug("$oldname not found");
+ }
+ if (-e $oldoutput) {
+ require IkiWiki::Render;
+ debug("removing output file $oldoutput");
+ IkiWiki::prune($oldoutput, $config{destdir});
+ }
+ }
+
+ savestate();
+ IkiWiki::unlockwiki;
+
+ unlockaggregate();
+}
+
+sub needsbuild (@) {
+ my $needsbuild=shift;
+
+ loadstate();
+
+ foreach my $feed (values %feeds) {
+ if (exists $pagesources{$feed->{sourcepage}} &&
+ grep { $_ eq $pagesources{$feed->{sourcepage}} } @$needsbuild) {
+ # Mark all feeds originating on this page as
+ # not yet seen; preprocess will unmark those that
+ # still exist.
+ markunseen($feed->{sourcepage});
+ }
+ }
+
+ return $needsbuild;
+}
+
+sub preprocess (@) {
+ my %params=@_;
+
+ foreach my $required (qw{name url}) {
+ if (! exists $params{$required}) {
+ error sprintf(gettext("missing %s parameter"), $required)
+ }
+ }
+
+ my $feed={};
+ my $name=$params{name};
+ if (exists $feeds{$name}) {
+ $feed=$feeds{$name};
+ }
+ else {
+ $feeds{$name}=$feed;
+ }
+ $feed->{name}=$name;
+ $feed->{sourcepage}=$params{page};
+ $feed->{url}=$params{url};
+ my $dir=exists $params{dir} ? $params{dir} : $params{page}."/".titlepage($params{name});
+ $dir=~s/^\/+//;
+ ($dir)=$dir=~/$config{wiki_file_regexp}/;
+ $feed->{dir}=$dir;
+ $feed->{feedurl}=defined $params{feedurl} ? $params{feedurl} : "";
+ $feed->{updateinterval}=defined $params{updateinterval} ? $params{updateinterval} * 60 : 15 * 60;
+ $feed->{expireage}=defined $params{expireage} ? $params{expireage} : 0;
+ $feed->{expirecount}=defined $params{expirecount} ? $params{expirecount} : 0;
+ if (exists $params{template}) {
+ $params{template}=~s/[^-_a-zA-Z0-9]+//g;
+ }
+ else {
+ $params{template} = "aggregatepost"
+ }
+ $feed->{template}=$params{template} . ".tmpl";
+ delete $feed->{unseen};
+ $feed->{lastupdate}=0 unless defined $feed->{lastupdate};
+ $feed->{lasttry}=$feed->{lastupdate} unless defined $feed->{lasttry};
+ $feed->{numposts}=0 unless defined $feed->{numposts};
+ $feed->{newposts}=0 unless defined $feed->{newposts};
+ $feed->{message}=gettext("new feed") unless defined $feed->{message};
+ $feed->{error}=0 unless defined $feed->{error};
+ $feed->{tags}=[];
+ while (@_) {
+ my $key=shift;
+ my $value=shift;
+ if ($key eq 'tag') {
+ push @{$feed->{tags}}, $value;
+ }
+ }
+
+ return "<a href=\"".$feed->{url}."\">".$feed->{name}."</a>: ".
+ ($feed->{error} ? "<em>" : "").$feed->{message}.
+ ($feed->{error} ? "</em>" : "").
+ " (".$feed->{numposts}." ".gettext("posts").
+ ($feed->{newposts} ? "; ".$feed->{newposts}.
+ " ".gettext("new") : "").
+ ")";
+}
+
+sub delete (@) {
+ my @files=@_;
+
+ # Remove feed data for removed pages.
+ foreach my $file (@files) {
+ my $page=pagename($file);
+ markunseen($page);
+ }
+}
+
+sub markunseen ($) {
+ my $page=shift;
+
+ foreach my $id (keys %feeds) {
+ if ($feeds{$id}->{sourcepage} eq $page) {
+ $feeds{$id}->{unseen}=1;
+ }
+ }
+}
+
+my $state_loaded=0;
+
+sub loadstate () {
+ return if $state_loaded;
+ $state_loaded=1;
+ if (-e "$config{wikistatedir}/aggregate") {
+ open(IN, "<", "$config{wikistatedir}/aggregate") ||
+ die "$config{wikistatedir}/aggregate: $!";
+ while (<IN>) {
+ $_=IkiWiki::possibly_foolish_untaint($_);
+ chomp;
+ my $data={};
+ foreach my $i (split(/ /, $_)) {
+ my ($field, $val)=split(/=/, $i, 2);
+ if ($field eq "name" || $field eq "feed" ||
+ $field eq "guid" || $field eq "message") {
+ $data->{$field}=decode_entities($val, " \t\n");
+ }
+ elsif ($field eq "tag") {
+ push @{$data->{tags}}, $val;
+ }
+ else {
+ $data->{$field}=$val;
+ }
+ }
+
+ if (exists $data->{name}) {
+ $feeds{$data->{name}}=$data;
+ }
+ elsif (exists $data->{guid}) {
+ $guids{$data->{guid}}=$data;
+ }
+ }
+
+ close IN;
+ }
+}
+
+sub savestate () {
+ return unless $state_loaded;
+ garbage_collect();
+ my $newfile="$config{wikistatedir}/aggregate.new";
+ my $cleanup = sub { unlink($newfile) };
+ open (OUT, ">", $newfile) || error("open $newfile: $!", $cleanup);
+ foreach my $data (values %feeds, values %guids) {
+ my @line;
+ foreach my $field (keys %$data) {
+ if ($field eq "name" || $field eq "feed" ||
+ $field eq "guid" || $field eq "message") {
+ push @line, "$field=".encode_entities($data->{$field}, " \t\n");
+ }
+ elsif ($field eq "tags") {
+ push @line, "tag=$_" foreach @{$data->{tags}};
+ }
+ else {
+ push @line, "$field=".$data->{$field}
+ if defined $data->{$field};
+ }
+ }
+ print OUT join(" ", @line)."\n" || error("write $newfile: $!", $cleanup);
+ }
+ close OUT || error("save $newfile: $!", $cleanup);
+ rename($newfile, "$config{wikistatedir}/aggregate") ||
+ error("rename $newfile: $!", $cleanup);
+
+ my $timestamp=undef;
+ foreach my $feed (keys %feeds) {
+ my $t=$feeds{$feed}->{lastupdate}+$feeds{$feed}->{updateinterval};
+ if (! defined $timestamp || $timestamp > $t) {
+ $timestamp=$t;
+ }
+ }
+ $newfile=~s/\.new$/time/;
+ open (OUT, ">", $newfile) || error("open $newfile: $!", $cleanup);
+ if (defined $timestamp) {
+ print OUT $timestamp."\n";
+ }
+ close OUT || error("save $newfile: $!", $cleanup);
+}
+
+sub garbage_collect () {
+ foreach my $name (keys %feeds) {
+ # remove any feeds that were not seen while building the pages
+ # that used to contain them
+ if ($feeds{$name}->{unseen}) {
+ delete $feeds{$name};
+ }
+ }
+
+ foreach my $guid (values %guids) {
+ # any guid whose feed is gone should be removed
+ if (! exists $feeds{$guid->{feed}}) {
+ if (exists $guid->{page}) {
+ unlink $IkiWiki::Plugin::transient::transientdir."/".htmlfn($guid->{page})
+ || unlink "$config{srcdir}/".htmlfn($guid->{page});
+ }
+ delete $guids{$guid->{guid}};
+ }
+ # handle expired guids
+ elsif ($guid->{expired} && exists $guid->{page}) {
+ unlink "$config{srcdir}/".htmlfn($guid->{page});
+ unlink $IkiWiki::Plugin::transient::transientdir."/".htmlfn($guid->{page});
+ delete $guid->{page};
+ delete $guid->{md5};
+ }
+ }
+}
+
+sub mergestate () {
+ # Load the current state in from disk, and merge into it
+ # values from the state in memory that might have changed
+ # during aggregation.
+ my %myfeeds=%feeds;
+ my %myguids=%guids;
+ clearstate();
+ loadstate();
+
+ # All that can change in feed state during aggregation is a few
+ # fields.
+ foreach my $name (keys %myfeeds) {
+ if (exists $feeds{$name}) {
+ foreach my $field (qw{message lastupdate lasttry
+ numposts newposts error}) {
+ $feeds{$name}->{$field}=$myfeeds{$name}->{$field};
+ }
+ }
+ }
+
+ # New guids can be created during aggregation.
+ # Guids have a few fields that may be updated during aggregation.
+ # It's also possible that guids were removed from the on-disk state
+ # while the aggregation was in process. That would only happen if
+ # their feed was also removed, so any removed guids added back here
+ # will be garbage collected later.
+ foreach my $guid (keys %myguids) {
+ if (! exists $guids{$guid}) {
+ $guids{$guid}=$myguids{$guid};
+ }
+ else {
+ foreach my $field (qw{md5}) {
+ $guids{$guid}->{$field}=$myguids{$guid}->{$field};
+ }
+ }
+ }
+}
+
+sub clearstate () {
+ %feeds=();
+ %guids=();
+ $state_loaded=0;
+}
+
+sub expire () {
+ foreach my $feed (values %feeds) {
+ next unless $feed->{expireage} || $feed->{expirecount};
+ my $count=0;
+ my %seen;
+ foreach my $item (sort { ($IkiWiki::pagectime{$b->{page}} || 0) <=> ($IkiWiki::pagectime{$a->{page}} || 0) }
+ grep { exists $_->{page} && $_->{feed} eq $feed->{name} }
+ values %guids) {
+ if ($feed->{expireage}) {
+ my $days_old = (time - ($IkiWiki::pagectime{$item->{page}} || 0)) / 60 / 60 / 24;
+ if ($days_old > $feed->{expireage}) {
+ debug(sprintf(gettext("expiring %s (%s days old)"),
+ $item->{page}, int($days_old)));
+ $item->{expired}=1;
+ }
+ }
+ elsif ($feed->{expirecount} &&
+ $count >= $feed->{expirecount}) {
+ debug(sprintf(gettext("expiring %s"), $item->{page}));
+ $item->{expired}=1;
+ }
+ else {
+ if (! $seen{$item->{page}}) {
+ $seen{$item->{page}}=1;
+ $count++;
+ }
+ }
+ }
+ }
+}
+
+sub needsaggregate () {
+ return values %feeds if $config{rebuild};
+ return grep { time - $_->{lastupdate} >= $_->{updateinterval} } values %feeds;
+}
+
+sub aggregate (@) {
+ eval q{use Net::INET6Glue::INET_is_INET6}; # may not be available
+ eval q{use XML::Feed};
+ error($@) if $@;
+ eval q{use URI::Fetch};
+ error($@) if $@;
+
+ foreach my $feed (@_) {
+ $feed->{lasttry}=time;
+ $feed->{newposts}=0;
+ $feed->{message}=sprintf(gettext("last checked %s"),
+ displaytime($feed->{lasttry}));
+ $feed->{error}=0;
+
+ debug(sprintf(gettext("checking feed %s ..."), $feed->{name}));
+
+ if (! length $feed->{feedurl}) {
+ my @urls=XML::Feed->find_feeds($feed->{url});
+ if (! @urls) {
+ $feed->{message}=sprintf(gettext("could not find feed at %s"), $feed->{url});
+ $feed->{error}=1;
+ debug($feed->{message});
+ next;
+ }
+ $feed->{feedurl}=pop @urls;
+ }
+ my $ua=useragent();
+ my $res=URI::Fetch->fetch($feed->{feedurl}, UserAgent=>$ua);
+ if (! $res) {
+ $feed->{message}=URI::Fetch->errstr;
+ $feed->{error}=1;
+ debug($feed->{message});
+ next;
+ }
+
+ # lastupdate is only set if we were able to contact the server
+ $feed->{lastupdate}=$feed->{lasttry};
+
+ if ($res->status == URI::Fetch::URI_GONE()) {
+ $feed->{message}=gettext("feed not found");
+ $feed->{error}=1;
+ debug($feed->{message});
+ next;
+ }
+ my $content=$res->content;
+ my $f=eval{XML::Feed->parse(\$content)};
+ if ($@) {
+ # One common cause of XML::Feed crashing is a feed
+ # that contains invalid UTF-8 sequences. Convert
+ # feed to ascii to try to work around.
+ $feed->{message}.=" ".sprintf(gettext("(invalid UTF-8 stripped from feed)"));
+ $f=eval {
+ $content=Encode::decode_utf8($content, 0);
+ XML::Feed->parse(\$content)
+ };
+ }
+ if ($@) {
+ # Another possibility is badly escaped entities.
+ $feed->{message}.=" ".sprintf(gettext("(feed entities escaped)"));
+ $content=~s/\&(?!amp)(\w+);/&$1;/g;
+ $f=eval {
+ $content=Encode::decode_utf8($content, 0);
+ XML::Feed->parse(\$content)
+ };
+ }
+ if ($@) {
+ # gettext can clobber $@
+ my $error = $@;
+ $feed->{message}=gettext("feed crashed XML::Feed!")." ($error)";
+ $feed->{error}=1;
+ debug($feed->{message});
+ next;
+ }
+ if (! $f) {
+ $feed->{message}=XML::Feed->errstr;
+ $feed->{error}=1;
+ debug($feed->{message});
+ next;
+ }
+
+ foreach my $entry ($f->entries) {
+ # XML::Feed doesn't work around XML::Atom's bizarre
+ # API, so we will. Real unicode strings? Yes please.
+ # See [[bugs/Aggregated_Atom_feeds_are_double-encoded]]
+ local $XML::Atom::ForceUnicode = 1;
+
+ my $c=$entry->content;
+ # atom feeds may have no content, only a summary
+ if (! defined $c && ref $entry->summary) {
+ $c=$entry->summary;
+ }
+
+ add_page(
+ feed => $feed,
+ copyright => $f->copyright,
+ title => defined $entry->title ? decode_entities($entry->title) : "untitled",
+ author => defined $entry->author ? decode_entities($entry->author) : "",
+ link => $entry->link,
+ content => (defined $c && defined $c->body) ? $c->body : "",
+ guid => defined $entry->id ? $entry->id : time."_".$feed->{name},
+ ctime => $entry->issued ? ($entry->issued->epoch || time) : time,
+ base => (defined $c && $c->can("base")) ? $c->base : undef,
+ );
+ }
+ }
+}
+
+sub add_page (@) {
+ my %params=@_;
+
+ my $feed=$params{feed};
+ my $guid={};
+ my $mtime;
+ if (exists $guids{$params{guid}}) {
+ # updating an existing post
+ $guid=$guids{$params{guid}};
+ return if $guid->{expired};
+ write_page($feed, $guid, $mtime, \%params);
+ }
+ else {
+ # new post
+ $guid->{guid}=$params{guid};
+ $guids{$params{guid}}=$guid;
+ $mtime=$params{ctime};
+ $feed->{numposts}++;
+ $feed->{newposts}++;
+
+ # assign it an unused page
+ my $page=titlepage($params{title});
+ # escape slashes and periods in title so it doesn't specify
+ # directory name or trigger ".." disallowing code.
+ $page=~s!([/.])!"__".ord($1)."__"!eg;
+ $page=$feed->{dir}."/".$page;
+ ($page)=$page=~/$config{wiki_file_regexp}/;
+ if (! defined $page || ! length $page) {
+ $page=$feed->{dir}."/item";
+ }
+ my $c="";
+ while (exists $IkiWiki::pagecase{lc $page.$c} ||
+ -e $IkiWiki::Plugin::transient::transientdir."/".htmlfn($page.$c) ||
+ -e "$config{srcdir}/".htmlfn($page.$c)) {
+ $c++
+ }
+ $page=$page.$c;
+
+ $guid->{page}=$page;
+ eval { write_page($feed, $guid, $mtime, \%params) };
+ if ($@) {
+ # assume failure was due to a too long filename
+ $c="";
+ $page=$feed->{dir}."/item";
+ while (exists $IkiWiki::pagecase{lc $page.$c} ||
+ -e $IkiWiki::Plugin::transient::transientdir."/".htmlfn($page.$c) ||
+ -e "$config{srcdir}/".htmlfn($page.$c)) {
+ $c++
+ }
+ $page=$page.$c;
+
+ $guid->{page}=$page;
+ write_page($feed, $guid, $mtime, \%params);
+ }
+
+ debug(sprintf(gettext("creating new page %s"), $page));
+ }
+}
+
+sub write_page ($$$$$) {
+ my $feed=shift;
+ my $guid=shift;
+ my $mtime=shift;
+ my %params=%{shift()};
+
+ $guid->{feed}=$feed->{name};
+
+ # To write or not to write? Need to avoid writing unchanged pages
+ # to avoid unneccessary rebuilding. The mtime from rss cannot be
+ # trusted; let's use a digest.
+ eval q{use Digest::MD5 'md5_hex'};
+ error($@) if $@;
+ require Encode;
+ my $digest=md5_hex(Encode::encode_utf8($params{content}));
+ return unless ! exists $guid->{md5} || $guid->{md5} ne $digest || $config{rebuild};
+ $guid->{md5}=$digest;
+
+ # Create the page.
+ my $template;
+ eval {
+ $template=template($feed->{template}, blind_cache => 1);
+ };
+ if ($@) {
+ # gettext can clobber $@
+ my $error = $@;
+ print STDERR gettext("failed to process template:")." $error";
+ return;
+ }
+ $template->param(title => $params{title})
+ if defined $params{title} && length($params{title});
+ $template->param(author => $params{author})
+ if defined $params{author} && length($params{author}
+ && $params{author} ne $feed->{name});
+ $template->param(content => wikiescape(htmlabs($params{content},
+ defined $params{base} ? $params{base} : $feed->{feedurl})));
+ $template->param(name => $feed->{name});
+ $template->param(url => $feed->{url});
+ $template->param(copyright => $params{copyright})
+ if defined $params{copyright} && length $params{copyright};
+ $template->param(permalink => IkiWiki::urlabs($params{link}, $feed->{feedurl}))
+ if defined $params{link};
+ if (ref $feed->{tags}) {
+ $template->param(tags => [map { tag => $_ }, @{$feed->{tags}}]);
+ }
+ writefile(htmlfn($guid->{page}),
+ $IkiWiki::Plugin::transient::transientdir, $template->output);
+
+ if (defined $mtime && $mtime <= time) {
+ # Set the mtime, this lets the build process get the right
+ # creation time on record for the new page.
+ utime $mtime, $mtime,
+ $IkiWiki::Plugin::transient::transientdir."/".htmlfn($guid->{page});
+ # Store it in pagectime for expiry code to use also.
+ $IkiWiki::pagectime{$guid->{page}}=$mtime
+ unless exists $IkiWiki::pagectime{$guid->{page}};
+ }
+ else {
+ # Dummy value for expiry code.
+ $IkiWiki::pagectime{$guid->{page}}=time
+ unless exists $IkiWiki::pagectime{$guid->{page}};
+ }
+}
+
+sub wikiescape ($) {
+ # escape accidental wikilinks and preprocessor stuff
+ return encode_entities(shift, '\[\]');
+}
+
+sub htmlabs ($$) {
+ # Convert links in html from relative to absolute.
+ # Note that this is a heuristic, which is not specified by the rss
+ # spec and may not be right for all feeds. Also, see Debian
+ # bug #381359.
+ my $html=shift;
+ my $urlbase=shift;
+
+ my $ret="";
+ my $p = HTML::Parser->new(api_version => 3);
+ $p->handler(default => sub { $ret.=join("", @_) }, "text");
+ $p->handler(start => sub {
+ my ($tagname, $pos, $text) = @_;
+ if (ref $HTML::Tagset::linkElements{$tagname}) {
+ while (4 <= @$pos) {
+ # use attribute sets from right to left
+ # to avoid invalidating the offsets
+ # when replacing the values
+ my($k_offset, $k_len, $v_offset, $v_len) =
+ splice(@$pos, -4);
+ my $attrname = lc(substr($text, $k_offset, $k_len));
+ next unless grep { $_ eq $attrname } @{$HTML::Tagset::linkElements{$tagname}};
+ next unless $v_offset; # 0 v_offset means no value
+ my $v = substr($text, $v_offset, $v_len);
+ $v =~ s/^([\'\"])(.*)\1$/$2/;
+ my $new_v=IkiWiki::urlabs($v, $urlbase);
+ $new_v =~ s/\"/"/g; # since we quote with ""
+ substr($text, $v_offset, $v_len) = qq("$new_v");
+ }
+ }
+ $ret.=$text;
+ }, "tagname, tokenpos, text");
+ $p->parse($html);
+ $p->eof;
+
+ return $ret;
+}
+
+sub htmlfn ($) {
+ return shift().".".($config{aggregateinternal} ? "_aggregated" : $config{htmlext});
+}
+
+my $aggregatelock;
+
+sub lockaggregate () {
+ # Take an exclusive lock to prevent multiple concurrent aggregators.
+ # Returns true if the lock was aquired.
+ if (! -d $config{wikistatedir}) {
+ mkdir($config{wikistatedir});
+ }
+ open($aggregatelock, '>', "$config{wikistatedir}/aggregatelock") ||
+ error ("cannot open to $config{wikistatedir}/aggregatelock: $!");
+ if (! flock($aggregatelock, 2 | 4)) { # LOCK_EX | LOCK_NB
+ close($aggregatelock) || error("failed closing aggregatelock: $!");
+ return 0;
+ }
+ return 1;
+}
+
+sub unlockaggregate () {
+ return close($aggregatelock) if $aggregatelock;
+ return;
+}
+
+1
diff -Nru ikiwiki-3.20141016.4/.pc/CVE-2019-9187-3.patch/IkiWiki/Plugin/blogspam.pm ikiwiki-3.20141016.4+deb8u1/.pc/CVE-2019-9187-3.patch/IkiWiki/Plugin/blogspam.pm
--- ikiwiki-3.20141016.4/.pc/CVE-2019-9187-3.patch/IkiWiki/Plugin/blogspam.pm 1970-01-01 10:00:00.000000000 +1000
+++ ikiwiki-3.20141016.4+deb8u1/.pc/CVE-2019-9187-3.patch/IkiWiki/Plugin/blogspam.pm 2019-03-07 17:32:54.000000000 +1100
@@ -0,0 +1,150 @@
+#!/usr/bin/perl
+package IkiWiki::Plugin::blogspam;
+
+use warnings;
+use strict;
+use IkiWiki 3.00;
+use Encode;
+
+my $defaulturl='http://test.blogspam.net:9999/';
+my $client;
+
+sub import {
+ hook(type => "getsetup", id => "blogspam", call => \&getsetup);
+ hook(type => "checkconfig", id => "blogspam", call => \&checkconfig);
+ hook(type => "checkcontent", id => "blogspam", call => \&checkcontent);
+}
+
+sub getsetup () {
+ return
+ plugin => {
+ safe => 1,
+ rebuild => 0,
+ section => "auth",
+ },
+ blogspam_pagespec => {
+ type => 'pagespec',
+ example => 'postcomment(*)',
+ description => 'PageSpec of pages to check for spam',
+ link => 'ikiwiki/PageSpec',
+ safe => 1,
+ rebuild => 0,
+ },
+ blogspam_options => {
+ type => "string",
+ example => "blacklist=1.2.3.4,blacklist=8.7.6.5,max-links=10",
+ description => "options to send to blogspam server",
+ link => "http://blogspam.net/api/2.0/testComment.html#options",
+ safe => 1,
+ rebuild => 0,
+ },
+ blogspam_server => {
+ type => "string",
+ default => $defaulturl,
+ description => "blogspam server JSON url",
+ safe => 1,
+ rebuild => 0,
+ },
+}
+
+sub checkconfig () {
+ # This is done at checkconfig time because printing an error
+ # if the module is missing when a spam is posted would not
+ # let the admin know about the problem.
+ eval q{
+ use JSON;
+ use HTTP::Request;
+ };
+ error $@ if $@;
+
+ eval q{use LWPx::ParanoidAgent};
+ if (!$@) {
+ $client=LWPx::ParanoidAgent->new(agent => $config{useragent});
+ }
+ else {
+ eval q{use LWP};
+ if ($@) {
+ error $@;
+ return;
+ }
+ $client=useragent();
+ }
+}
+
+sub checkcontent (@) {
+ my %params=@_;
+ my $session=$params{session};
+
+ my $spec='!admin()';
+ if (exists $config{blogspam_pagespec} &&
+ length $config{blogspam_pagespec}) {
+ $spec.=" and (".$config{blogspam_pagespec}.")";
+ }
+
+ my $user=$session->param("name");
+ return undef unless pagespec_match($params{page}, $spec,
+ (defined $user ? (user => $user) : ()),
+ (defined $session->remote_addr() ? (ip => $session->remote_addr()) : ()),
+ location => $params{page});
+
+ my $url=$defaulturl;
+ $url = $config{blogspam_server} if exists $config{blogspam_server};
+
+ my @options = split(",", $config{blogspam_options})
+ if exists $config{blogspam_options};
+
+ # Allow short comments and whitespace-only edits, unless the user
+ # has overridden min-words themselves.
+ push @options, "min-words=0"
+ unless grep /^min-words=/i, @options;
+ # Wiki pages can have a lot of urls, unless the user specifically
+ # wants to limit them.
+ push @options, "exclude=lotsaurls"
+ unless grep /^max-links/i, @options;
+ # Unless the user specified a size check, disable such checking.
+ push @options, "exclude=size"
+ unless grep /^(?:max|min)-size/i, @options;
+ # This test has absurd false positives on words like "alpha"
+ # and "buy".
+ push @options, "exclude=stopwords";
+
+ my %req=(
+ ip => $session->remote_addr(),
+ comment => encode_utf8(defined $params{diff} ? $params{diff} : $params{content}),
+ subject => encode_utf8(defined $params{subject} ? $params{subject} : ""),
+ name => encode_utf8(defined $params{author} ? $params{author} : ""),
+ link => encode_utf8(exists $params{url} ? $params{url} : ""),
+ options => join(",", @options),
+ site => encode_utf8($config{url}),
+ version => "ikiwiki ".$IkiWiki::version,
+ );
+ eval q{use JSON; use HTTP::Request}; # errors handled in checkconfig()
+ my $res = $client->request(
+ HTTP::Request->new(
+ 'POST',
+ $url,
+ [ 'Content-Type' => 'application/json' ],
+ to_json(\%req),
+ ),
+ );
+
+ if (! ref $res || ! $res->is_success()) {
+ debug("failed to get response from blogspam server ($url)");
+ return undef;
+ }
+ my $details = from_json($res->content);
+ if ($details->{result} eq 'SPAM') {
+ eval q{use Data::Dumper};
+ debug("blogspam server reports $details->{reason}: ".Dumper(\%req));
+ return gettext("Sorry, but that looks like spam to <a href=\"http://blogspam.net/\">blogspam</a>: ").$details->{reason};
+ }
+ elsif ($details->{result} ne 'OK') {
+ debug("blogspam server failure: ".$res->content);
+ return undef;
+ }
+ else {
+ return undef;
+ }
+}
+
+1
diff -Nru ikiwiki-3.20141016.4/.pc/CVE-2019-9187-3.patch/IkiWiki/Plugin/openid.pm ikiwiki-3.20141016.4+deb8u1/.pc/CVE-2019-9187-3.patch/IkiWiki/Plugin/openid.pm
--- ikiwiki-3.20141016.4/.pc/CVE-2019-9187-3.patch/IkiWiki/Plugin/openid.pm 1970-01-01 10:00:00.000000000 +1000
+++ ikiwiki-3.20141016.4+deb8u1/.pc/CVE-2019-9187-3.patch/IkiWiki/Plugin/openid.pm 2019-03-07 17:32:54.000000000 +1100
@@ -0,0 +1,286 @@
+#!/usr/bin/perl
+# OpenID support.
+package IkiWiki::Plugin::openid;
+
+use warnings;
+use strict;
+use IkiWiki 3.00;
+
+sub import {
+ add_underlay("openid-selector");
+ add_underlay("jquery");
+ hook(type => "checkconfig", id => "openid", call => \&checkconfig);
+ hook(type => "getsetup", id => "openid", call => \&getsetup);
+ hook(type => "auth", id => "openid", call => \&auth);
+ hook(type => "formbuilder_setup", id => "openid",
+ call => \&formbuilder_setup, last => 1);
+}
+
+sub checkconfig () {
+ if ($config{cgi}) {
+ # Intercept normal signin form, so the openid selector
+ # can be displayed.
+ #
+ # When other auth hooks are registered, give the selector
+ # a reference to the normal signin form.
+ require IkiWiki::CGI;
+ my $real_cgi_signin;
+ if (keys %{$IkiWiki::hooks{auth}} > 1) {
+ $real_cgi_signin=\&IkiWiki::cgi_signin;
+ }
+ inject(name => "IkiWiki::cgi_signin", call => sub ($$) {
+ openid_selector($real_cgi_signin, @_);
+ });
+ }
+}
+
+sub getsetup () {
+ return
+ plugin => {
+ safe => 1,
+ rebuild => 0,
+ section => "auth",
+ },
+ openid_realm => {
+ type => "string",
+ description => "url pattern of openid realm (default is cgiurl)",
+ safe => 0,
+ rebuild => 0,
+ },
+ openid_cgiurl => {
+ type => "string",
+ description => "url to ikiwiki cgi to use for openid authentication (default is cgiurl)",
+ safe => 0,
+ rebuild => 0,
+ },
+}
+
+sub openid_selector {
+ my $real_cgi_signin=shift;
+ my $q=shift;
+ my $session=shift;
+
+ my $openid_url=$q->param('openid_identifier');
+ my $openid_error;
+
+ if (! load_openid_module()) {
+ if ($real_cgi_signin) {
+ $real_cgi_signin->($q, $session);
+ exit;
+ }
+ error(sprintf(gettext("failed to load openid module: "), @_));
+ }
+ elsif (defined $q->param("action") && $q->param("action") eq "verify") {
+ validate($q, $session, $openid_url, sub {
+ $openid_error=shift;
+ });
+ }
+
+ my $template=IkiWiki::template("openid-selector.tmpl");
+ $template->param(
+ cgiurl => IkiWiki::cgiurl(),
+ (defined $openid_error ? (openid_error => $openid_error) : ()),
+ (defined $openid_url ? (openid_url => $openid_url) : ()),
+ ($real_cgi_signin ? (nonopenidform => $real_cgi_signin->($q, $session, 1)) : ()),
+ );
+
+ IkiWiki::printheader($session);
+ print IkiWiki::cgitemplate($q, "signin", $template->output);
+ exit;
+}
+
+sub formbuilder_setup (@) {
+ my %params=@_;
+
+ my $form=$params{form};
+ my $session=$params{session};
+ my $cgi=$params{cgi};
+
+ if ($form->title eq "preferences" &&
+ IkiWiki::openiduser($session->param("name"))) {
+ $form->field(name => "openid_identifier", disabled => 1,
+ label => htmllink("", "", "ikiwiki/OpenID", noimageinline => 1),
+ value => "",
+ size => 1, force => 1,
+ fieldset => "login",
+ comment => $session->param("name"));
+ $form->field(name => "email", type => "hidden");
+ }
+}
+
+sub validate ($$$;$) {
+ my $q=shift;
+ my $session=shift;
+ my $openid_url=shift;
+ my $errhandler=shift;
+
+ my $csr=getobj($q, $session);
+
+ my $claimed_identity = $csr->claimed_identity($openid_url);
+ if (! $claimed_identity) {
+ if ($errhandler) {
+ if (ref($errhandler) eq 'CODE') {
+ $errhandler->($csr->err);
+ }
+ return 0;
+ }
+ else {
+ error($csr->err);
+ }
+ }
+
+ # Ask for client to provide a name and email, if possible.
+ # Try sreg and ax
+ if ($claimed_identity->can("set_extension_args")) {
+ $claimed_identity->set_extension_args(
+ 'http://openid.net/extensions/sreg/1.1',
+ {
+ optional => 'email,fullname,nickname',
+ },
+ );
+ $claimed_identity->set_extension_args(
+ 'http://openid.net/srv/ax/1.0',
+ {
+ mode => 'fetch_request',
+ 'required' => 'email,fullname,nickname,firstname',
+ 'type.email' => "http://schema.openid.net/contact/email",
+ 'type.fullname' => "http://axschema.org/namePerson",
+ 'type.nickname' => "http://axschema.org/namePerson/friendly",
+ 'type.firstname' => "http://axschema.org/namePerson/first",
+ },
+ );
+ }
+
+ my $cgiurl=$config{openid_cgiurl};
+ $cgiurl=$q->url if ! defined $cgiurl;
+
+ my $trust_root=$config{openid_realm};
+ $trust_root=$cgiurl if ! defined $trust_root;
+
+ my $check_url = $claimed_identity->check_url(
+ return_to => auto_upgrade_https($q, "$cgiurl?do=postsignin"),
+ trust_root => auto_upgrade_https($q, $trust_root),
+ delayed_return => 1,
+ );
+ # Redirect the user to the OpenID server, which will
+ # eventually bounce them back to auth()
+ IkiWiki::redirect($q, $check_url);
+ exit 0;
+}
+
+sub auth ($$) {
+ my $q=shift;
+ my $session=shift;
+
+ if (defined $q->param('openid.mode')) {
+ my $csr=getobj($q, $session);
+
+ if (my $setup_url = $csr->user_setup_url) {
+ IkiWiki::redirect($q, $setup_url);
+ }
+ elsif ($csr->user_cancel) {
+ IkiWiki::redirect($q, IkiWiki::baseurl(undef));
+ }
+ elsif (my $vident = $csr->verified_identity) {
+ $session->param(name => $vident->url);
+
+ my @extensions;
+ if ($vident->can("signed_extension_fields")) {
+ @extensions=grep { defined } (
+ $vident->signed_extension_fields('http://openid.net/extensions/sreg/1.1'),
+ $vident->signed_extension_fields('http://openid.net/srv/ax/1.0'),
+ );
+ }
+ my $nickname;
+ foreach my $ext (@extensions) {
+ foreach my $field (qw{value.email email}) {
+ if (exists $ext->{$field} &&
+ defined $ext->{$field} &&
+ length $ext->{$field}) {
+ $session->param(email => $ext->{$field});
+ if (! defined $nickname &&
+ $ext->{$field}=~/(.+)@.+/) {
+ $nickname = $1;
+ }
+ last;
+ }
+ }
+ foreach my $field (qw{value.nickname nickname value.fullname fullname value.firstname}) {
+ if (exists $ext->{$field} &&
+ defined $ext->{$field} &&
+ length $ext->{$field}) {
+ $nickname=$ext->{$field};
+ last;
+ }
+ }
+ }
+ if (defined $nickname) {
+ $session->param(nickname =>
+ Encode::decode_utf8($nickname));
+ }
+ }
+ else {
+ error("OpenID failure: ".$csr->err);
+ }
+ }
+ elsif (defined $q->param('openid_identifier')) {
+ # myopenid.com affiliate support
+ validate($q, $session, scalar $q->param('openid_identifier'));
+ }
+}
+
+sub getobj ($$) {
+ my $q=shift;
+ my $session=shift;
+
+ eval q{use Net::INET6Glue::INET_is_INET6}; # may not be available
+ eval q{use Net::OpenID::Consumer};
+ error($@) if $@;
+
+ my $ua;
+ eval q{use LWPx::ParanoidAgent};
+ if (! $@) {
+ $ua=LWPx::ParanoidAgent->new(agent => $config{useragent});
+ }
+ else {
+ $ua=useragent();
+ }
+
+ # Store the secret in the session.
+ my $secret=$session->param("openid_secret");
+ if (! defined $secret) {
+ $secret=rand;
+ $session->param(openid_secret => $secret);
+ }
+
+ my $cgiurl=$config{openid_cgiurl};
+ $cgiurl=$q->url if ! defined $cgiurl;
+
+ return Net::OpenID::Consumer->new(
+ ua => $ua,
+ args => $q,
+ consumer_secret => sub { return shift()+$secret },
+ required_root => auto_upgrade_https($q, $cgiurl),
+ );
+}
+
+sub auto_upgrade_https {
+ my $q=shift;
+ my $url=shift;
+ if ($q->https()) {
+ $url=~s/^http:/https:/i;
+ }
+ return $url;
+}
+
+sub load_openid_module {
+ # Give up if module is unavailable to avoid needing to depend on it.
+ eval q{use Net::OpenID::Consumer};
+ if ($@) {
+ debug("unable to load Net::OpenID::Consumer, not enabling OpenID login ($@)");
+ return;
+ }
+ return 1;
+}
+
+1
diff -Nru ikiwiki-3.20141016.4/.pc/CVE-2019-9187-3.patch/IkiWiki/Plugin/pinger.pm ikiwiki-3.20141016.4+deb8u1/.pc/CVE-2019-9187-3.patch/IkiWiki/Plugin/pinger.pm
--- ikiwiki-3.20141016.4/.pc/CVE-2019-9187-3.patch/IkiWiki/Plugin/pinger.pm 1970-01-01 10:00:00.000000000 +1000
+++ ikiwiki-3.20141016.4+deb8u1/.pc/CVE-2019-9187-3.patch/IkiWiki/Plugin/pinger.pm 2019-03-07 17:32:54.000000000 +1100
@@ -0,0 +1,121 @@
+#!/usr/bin/perl
+package IkiWiki::Plugin::pinger;
+
+use warnings;
+use strict;
+use IkiWiki 3.00;
+
+my %pages;
+my $pinged=0;
+
+sub import {
+ hook(type => "getsetup", id => "pinger", call => \&getsetup);
+ hook(type => "needsbuild", id => "pinger", call => \&needsbuild);
+ hook(type => "preprocess", id => "ping", call => \&preprocess);
+ hook(type => "delete", id => "pinger", call => \&ping);
+ hook(type => "rendered", id => "pinger", call => \&ping);
+}
+
+sub getsetup () {
+ return
+ plugin => {
+ safe => 1,
+ rebuild => 0,
+ },
+ pinger_timeout => {
+ type => "integer",
+ example => 15,
+ description => "how many seconds to try pinging before timing out",
+ safe => 1,
+ rebuild => 0,
+ },
+}
+
+sub needsbuild (@) {
+ my $needsbuild=shift;
+ foreach my $page (keys %pagestate) {
+ if (exists $pagestate{$page}{pinger}) {
+ $pages{$page}=1;
+ if (exists $pagesources{$page} &&
+ grep { $_ eq $pagesources{$page} } @$needsbuild) {
+ # remove state, will be re-added if
+ # the ping directive is still present
+ # on rebuild.
+ delete $pagestate{$page}{pinger};
+ }
+ }
+ }
+ return $needsbuild;
+}
+
+sub preprocess (@) {
+ my %params=@_;
+ if (! exists $params{from} || ! exists $params{to}) {
+ error gettext("requires 'from' and 'to' parameters");
+ }
+ if ($params{from} eq $config{url}) {
+ $pagestate{$params{destpage}}{pinger}{$params{to}}=1;
+ $pages{$params{destpage}}=1;
+ return sprintf(gettext("Will ping %s"), $params{to});
+ }
+ else {
+ return sprintf(gettext("Ignoring ping directive for wiki %s (this wiki is %s)"), $params{from}, $config{url});
+ }
+}
+
+sub ping {
+ if (! $pinged && %pages) {
+ $pinged=1;
+
+ eval q{use Net::INET6Glue::INET_is_INET6}; # may not be available
+
+ my $ua;
+ eval q{use LWPx::ParanoidAgent};
+ if (!$@) {
+ $ua=LWPx::ParanoidAgent->new(agent => $config{useragent});
+ }
+ else {
+ eval q{use LWP};
+ if ($@) {
+ debug(gettext("LWP not found, not pinging"));
+ return;
+ }
+ $ua=useragent();
+ }
+ $ua->timeout($config{pinger_timeout} || 15);
+
+ # daemonise here so slow pings don't slow down wiki updates
+ defined(my $pid = fork) or error("Can't fork: $!");
+ return if $pid;
+ chdir '/';
+ open STDIN, '/dev/null';
+ open STDOUT, '>/dev/null';
+ POSIX::setsid() or error("Can't start a new session: $!");
+ open STDERR, '>&STDOUT' or error("Can't dup stdout: $!");
+
+ # Don't need to keep a lock on the wiki as a daemon.
+ IkiWiki::unlockwiki();
+
+ my %urls;
+ foreach my $page (%pages) {
+ if (exists $pagestate{$page}{pinger}) {
+ $urls{$_}=1 foreach keys %{$pagestate{$page}{pinger}};
+ }
+ }
+ foreach my $url (keys %urls) {
+ # Try to avoid pinging ourselves. If this check
+ # fails, it's not the end of the world, since we
+ # only ping when a page was changed, so a ping loop
+ # will still be avoided.
+ next if $url=~/^\Q$config{cgiurl}\E/;
+ my $local_cgiurl = IkiWiki::cgiurl();
+ next if $url=~/^\Q$local_cgiurl\E/;
+
+ $ua->get($url);
+ }
+
+ exit 0;
+ }
+}
+
+1
diff -Nru ikiwiki-3.20141016.4/.pc/CVE-2019-9187-3.patch/IkiWiki.pm ikiwiki-3.20141016.4+deb8u1/.pc/CVE-2019-9187-3.patch/IkiWiki.pm
--- ikiwiki-3.20141016.4/.pc/CVE-2019-9187-3.patch/IkiWiki.pm 1970-01-01 10:00:00.000000000 +1000
+++ ikiwiki-3.20141016.4+deb8u1/.pc/CVE-2019-9187-3.patch/IkiWiki.pm 2019-03-07 17:32:54.000000000 +1100
@@ -0,0 +1,3032 @@
+#!/usr/bin/perl
+
+package IkiWiki;
+
+use warnings;
+use strict;
+use Encode;
+use URI::Escape q{uri_escape_utf8};
+use POSIX ();
+use Storable;
+use open qw{:utf8 :std};
+
+use vars qw{%config %links %oldlinks %pagemtime %pagectime %pagecase
+ %pagestate %wikistate %renderedfiles %oldrenderedfiles
+ %pagesources %delpagesources %destsources %depends %depends_simple
+ @mass_depends %hooks %forcerebuild %loaded_plugins %typedlinks
+ %oldtypedlinks %autofiles @underlayfiles $lastrev $phase};
+
+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 strftime_utf8 will_render gettext ngettext urlto targetpage
+ add_underlay pagetitle titlepage linkpage newpagefile
+ 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
+our $version='unknown'; # VERSION_AUTOREPLACE done by Makefile, DNE
+our $installdir='/usr'; # INSTALLDIR_AUTOREPLACE done by Makefile, DNE
+
+# Page dependency types.
+our $DEPEND_CONTENT=1;
+our $DEPEND_PRESENCE=2;
+our $DEPEND_LINKS=4;
+
+# Phases of processing.
+sub PHASE_SCAN () { 0 }
+sub PHASE_RENDER () { 1 }
+$phase = PHASE_SCAN;
+
+# Optimisation.
+use Memoize;
+memoize("abs2rel");
+memoize("sortspec_translate");
+memoize("pagespec_translate");
+memoize("template_file");
+
+sub getsetup () {
+ wikiname => {
+ type => "string",
+ default => "wiki",
+ description => "name of the wiki",
+ safe => 1,
+ rebuild => 1,
+ },
+ adminemail => {
+ type => "string",
+ default => undef,
+ example => 'me@example.com',
+ description => "contact email for wiki",
+ safe => 1,
+ rebuild => 0,
+ },
+ adminuser => {
+ type => "string",
+ default => [],
+ description => "users who are wiki admins",
+ safe => 1,
+ rebuild => 0,
+ },
+ banned_users => {
+ type => "string",
+ default => [],
+ description => "users who are banned from the wiki",
+ safe => 1,
+ rebuild => 0,
+ },
+ srcdir => {
+ type => "string",
+ default => undef,
+ example => "$ENV{HOME}/wiki",
+ description => "where the source of the wiki is located",
+ safe => 0, # path
+ rebuild => 1,
+ },
+ destdir => {
+ type => "string",
+ default => undef,
+ example => "/var/www/wiki",
+ description => "where to build the wiki",
+ safe => 0, # path
+ rebuild => 1,
+ },
+ url => {
+ type => "string",
+ default => '',
+ example => "http://example.com/wiki",
+ description => "base url to the wiki",
+ safe => 1,
+ rebuild => 1,
+ },
+ cgiurl => {
+ type => "string",
+ default => '',
+ example => "http://example.com/wiki/ikiwiki.cgi",
+ description => "url to the ikiwiki.cgi",
+ safe => 1,
+ rebuild => 1,
+ },
+ reverse_proxy => {
+ type => "boolean",
+ default => 0,
+ description => "do not adjust cgiurl if CGI is accessed via different URL",
+ advanced => 0,
+ safe => 1,
+ rebuild => 0, # only affects CGI requests
+ },
+ cgi_wrapper => {
+ type => "string",
+ default => '',
+ example => "/var/www/wiki/ikiwiki.cgi",
+ description => "filename of cgi wrapper to generate",
+ safe => 0, # file
+ rebuild => 0,
+ },
+ cgi_wrappermode => {
+ type => "string",
+ default => '06755',
+ description => "mode for cgi_wrapper (can safely be made suid)",
+ safe => 0,
+ rebuild => 0,
+ },
+ 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 => '',
+ description => "rcs backend to use",
+ safe => 0, # don't allow overriding
+ rebuild => 0,
+ },
+ default_plugins => {
+ type => "internal",
+ default => [qw{mdwn link inline meta htmlscrubber passwordauth
+ openid signinedit lockedit conditional
+ recentchanges parentlinks editpage
+ templatebody}],
+ description => "plugins to enable by default",
+ safe => 0,
+ rebuild => 1,
+ },
+ add_plugins => {
+ type => "string",
+ default => [],
+ description => "plugins to add to the default configuration",
+ safe => 1,
+ rebuild => 1,
+ },
+ disable_plugins => {
+ type => "string",
+ default => [],
+ description => "plugins to disable",
+ safe => 1,
+ rebuild => 1,
+ },
+ templatedir => {
+ type => "string",
+ default => "$installdir/share/ikiwiki/templates",
+ description => "additional directory to search for template files",
+ advanced => 1,
+ safe => 0, # path
+ rebuild => 1,
+ },
+ underlaydir => {
+ type => "string",
+ default => "$installdir/share/ikiwiki/basewiki",
+ description => "base wiki source location",
+ advanced => 1,
+ safe => 0, # path
+ rebuild => 0,
+ },
+ underlaydirbase => {
+ type => "internal",
+ default => "$installdir/share/ikiwiki",
+ description => "parent directory containing additional underlays",
+ safe => 0,
+ rebuild => 0,
+ },
+ wrappers => {
+ type => "internal",
+ default => [],
+ description => "wrappers to generate",
+ safe => 0,
+ rebuild => 0,
+ },
+ underlaydirs => {
+ type => "internal",
+ default => [],
+ description => "additional underlays to use",
+ safe => 0,
+ rebuild => 0,
+ },
+ verbose => {
+ type => "boolean",
+ example => 1,
+ description => "display verbose messages?",
+ safe => 1,
+ rebuild => 0,
+ },
+ syslog => {
+ type => "boolean",
+ example => 1,
+ description => "log to syslog?",
+ safe => 1,
+ rebuild => 0,
+ },
+ usedirs => {
+ type => "boolean",
+ default => 1,
+ description => "create output files named page/index.html?",
+ safe => 0, # changing requires manual transition
+ rebuild => 1,
+ },
+ prefix_directives => {
+ type => "boolean",
+ default => 1,
+ description => "use '!'-prefixed preprocessor directives?",
+ safe => 0, # changing requires manual transition
+ rebuild => 1,
+ },
+ indexpages => {
+ type => "boolean",
+ default => 0,
+ description => "use page/index.mdwn source files",
+ safe => 1,
+ rebuild => 1,
+ },
+ discussion => {
+ type => "boolean",
+ default => 1,
+ description => "enable Discussion pages?",
+ safe => 1,
+ rebuild => 1,
+ },
+ discussionpage => {
+ type => "string",
+ default => gettext("Discussion"),
+ description => "name of Discussion pages",
+ safe => 1,
+ rebuild => 1,
+ },
+ html5 => {
+ type => "boolean",
+ default => 0,
+ description => "generate HTML5?",
+ advanced => 0,
+ safe => 1,
+ rebuild => 1,
+ },
+ sslcookie => {
+ type => "boolean",
+ default => 0,
+ description => "only send cookies over SSL connections?",
+ advanced => 1,
+ safe => 1,
+ rebuild => 0,
+ },
+ default_pageext => {
+ type => "string",
+ default => "mdwn",
+ description => "extension to use for new pages",
+ safe => 0, # not sanitized
+ rebuild => 0,
+ },
+ htmlext => {
+ type => "string",
+ default => "html",
+ description => "extension to use for html files",
+ safe => 0, # not sanitized
+ rebuild => 1,
+ },
+ timeformat => {
+ type => "string",
+ default => '%c',
+ description => "strftime format string to display date",
+ advanced => 1,
+ safe => 1,
+ rebuild => 1,
+ },
+ locale => {
+ type => "string",
+ default => undef,
+ example => "en_US.UTF-8",
+ description => "UTF-8 locale to use",
+ advanced => 1,
+ safe => 0,
+ rebuild => 1,
+ },
+ userdir => {
+ type => "string",
+ default => "",
+ example => "users",
+ description => "put user pages below specified page",
+ safe => 1,
+ rebuild => 1,
+ },
+ numbacklinks => {
+ type => "integer",
+ default => 10,
+ description => "how many backlinks to show before hiding excess (0 to show all)",
+ safe => 1,
+ rebuild => 1,
+ },
+ hardlink => {
+ type => "boolean",
+ default => 0,
+ description => "attempt to hardlink source files? (optimisation for large files)",
+ advanced => 1,
+ safe => 0, # paranoia
+ rebuild => 0,
+ },
+ umask => {
+ type => "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,
+ },
+ wrappergroup => {
+ type => "string",
+ example => "ikiwiki",
+ description => "group for wrappers to run in",
+ advanced => 1,
+ safe => 0, # paranoia
+ rebuild => 0,
+ },
+ libdir => {
+ type => "string",
+ default => "",
+ example => "$ENV{HOME}/.ikiwiki/",
+ description => "extra library and plugin directory",
+ advanced => 1,
+ safe => 0, # directory
+ rebuild => 0,
+ },
+ ENV => {
+ type => "string",
+ default => {},
+ description => "environment variables",
+ safe => 0, # paranoia
+ rebuild => 0,
+ },
+ timezone => {
+ type => "string",
+ default => "",
+ example => "US/Eastern",
+ description => "time zone name",
+ safe => 1,
+ rebuild => 1,
+ },
+ include => {
+ type => "string",
+ default => undef,
+ example => '^\.htaccess$',
+ description => "regexp of normally excluded files to include",
+ advanced => 1,
+ safe => 0, # regexp
+ rebuild => 1,
+ },
+ exclude => {
+ type => "string",
+ default => undef,
+ example => '^(*\.private|Makefile)$',
+ description => "regexp of files that should be skipped",
+ advanced => 1,
+ safe => 0, # regexp
+ rebuild => 1,
+ },
+ wiki_file_prune_regexps => {
+ type => "internal",
+ default => [qr/(^|\/)\.\.(\/|$)/, qr/^\//, qr/^\./, qr/\/\./,
+ qr/\.x?html?$/, qr/\.ikiwiki-new$/,
+ qr/(^|\/).svn\//, qr/.arch-ids\//, qr/{arch}\//,
+ qr/(^|\/)_MTN\//, qr/(^|\/)_darcs\//,
+ qr/(^|\/)CVS\//, qr/\.dpkg-tmp$/],
+ description => "regexps of source files to ignore",
+ safe => 0,
+ rebuild => 1,
+ },
+ wiki_file_chars => {
+ type => "string",
+ description => "specifies the characters that are allowed in source filenames",
+ default => "-[:alnum:]+/.:_",
+ safe => 0,
+ rebuild => 1,
+ },
+ wiki_file_regexp => {
+ type => "internal",
+ description => "regexp of legal source files",
+ safe => 0,
+ rebuild => 1,
+ },
+ web_commit_regexp => {
+ type => "internal",
+ default => qr/^web commit (by (.*?(?=: |$))|from ([0-9a-fA-F:.]+[0-9a-fA-F])):?(.*)/,
+ description => "regexp to parse web commits from logs",
+ safe => 0,
+ rebuild => 0,
+ },
+ cgi => {
+ type => "internal",
+ default => 0,
+ description => "run as a cgi",
+ safe => 0,
+ rebuild => 0,
+ },
+ cgi_disable_uploads => {
+ type => "internal",
+ default => 1,
+ description => "whether CGI should accept file uploads",
+ safe => 0,
+ rebuild => 0,
+ },
+ post_commit => {
+ type => "internal",
+ default => 0,
+ description => "run as a post-commit hook",
+ safe => 0,
+ rebuild => 0,
+ },
+ rebuild => {
+ type => "internal",
+ default => 0,
+ description => "running in rebuild mode",
+ safe => 0,
+ rebuild => 0,
+ },
+ setup => {
+ type => "internal",
+ default => undef,
+ description => "running in setup mode",
+ safe => 0,
+ rebuild => 0,
+ },
+ clean => {
+ type => "internal",
+ default => 0,
+ description => "running in clean mode",
+ safe => 0,
+ rebuild => 0,
+ },
+ refresh => {
+ type => "internal",
+ default => 0,
+ description => "running in refresh mode",
+ safe => 0,
+ rebuild => 0,
+ },
+ test_receive => {
+ type => "internal",
+ default => 0,
+ description => "running in receive test mode",
+ safe => 0,
+ rebuild => 0,
+ },
+ wrapper_background_command => {
+ type => "internal",
+ default => '',
+ description => "background shell command to run",
+ safe => 0,
+ rebuild => 0,
+ },
+ gettime => {
+ type => "internal",
+ description => "running in gettime mode",
+ safe => 0,
+ rebuild => 0,
+ },
+ w3mmode => {
+ type => "internal",
+ default => 0,
+ description => "running in w3mmode",
+ safe => 0,
+ rebuild => 0,
+ },
+ wikistatedir => {
+ type => "internal",
+ default => undef,
+ description => "path to the .ikiwiki directory holding ikiwiki state",
+ safe => 0,
+ rebuild => 0,
+ },
+ setupfile => {
+ type => "internal",
+ default => undef,
+ description => "path to setup file",
+ safe => 0,
+ rebuild => 0,
+ },
+ setuptype => {
+ type => "internal",
+ default => "Yaml",
+ description => "perl class to use to dump 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,
+ },
+ 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 => "ikiwiki/$version",
+ 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 () {
+ my %s=getsetup();
+ my @ret;
+ foreach my $key (keys %s) {
+ push @ret, $key, $s{$key}->{default};
+ }
+ 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}) {
+ $ENV{LANG} = $ENV{LC_ALL};
+ delete $ENV{LC_ALL};
+ }
+ if (defined $config{locale}) {
+ if (POSIX::setlocale(&POSIX::LC_ALL, $config{locale})) {
+ $ENV{LANG}=$config{locale};
+ define_gettext();
+ }
+ }
+
+ 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}}) {
+ $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}};
+ error($@) if $@;
+ $config{srcdir}=possibly_foolish_untaint(abs_path($config{srcdir}));
+ $config{destdir}=possibly_foolish_untaint(abs_path($config{destdir}));
+ $config{cgiurl}="file:///\$LIB/ikiwiki-w3m.cgi/".$config{cgiurl}
+ unless $config{cgiurl} =~ m!file:///!;
+ $config{url}="file://".$config{destdir};
+ }
+
+ 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 eq 'https' &&
+ $baseurl->scheme eq 'http') {
+ # We assume that the same content is available
+ # over both http and https, because if it
+ # wasn't, accessing the static content
+ # from the CGI would be mixed-content,
+ # which would be a security flaw.
+
+ if ($cgiurl->authority ne $baseurl->authority) {
+ # use protocol-relative URL for
+ # static content
+ $local_url = "$config{url}/";
+ $local_url =~ s{^http://}{//};
+ }
+ # else use host-relative URL for static content
+
+ # either way, CGI needs to be absolute
+ $local_cgiurl = $config{cgiurl};
+ }
+ elsif ($cgiurl->scheme ne $baseurl->scheme) {
+ # too far apart, fall back to absolute URLs
+ $local_url = "$config{url}/";
+ $local_cgiurl = $config{cgiurl};
+ }
+ elsif ($cgiurl->authority ne $baseurl->authority) {
+ # slightly too far apart, fall back to
+ # protocol-relative URLs
+ $local_url = "$config{url}/";
+ $local_url =~ s{^https?://}{//};
+ $local_cgiurl = $config{cgiurl};
+ $local_cgiurl =~ s{^https?://}{//};
+ }
+ # else keep host-relative URLs
+ }
+
+ $local_url =~ s{//$}{/};
+ }
+ else {
+ $local_cgiurl = $config{cgiurl};
+ }
+
+ $config{wikistatedir}="$config{srcdir}/.ikiwiki"
+ unless exists $config{wikistatedir} && defined $config{wikistatedir};
+
+ if (defined $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->() });
+
+ return 1;
+}
+
+sub listplugins () {
+ my %ret;
+
+ foreach my $dir (@INC, $config{libdir}) {
+ next unless defined $dir && length $dir;
+ foreach my $file (glob("$dir/IkiWiki/Plugin/*.pm")) {
+ my ($plugin)=$file=~/.*\/(.*)\.pm$/;
+ $ret{$plugin}=1;
+ }
+ }
+ foreach my $dir ($config{libdir}, "$installdir/lib/ikiwiki") {
+ next unless defined $dir && length $dir;
+ foreach my $file (glob("$dir/plugins/*")) {
+ $ret{basename($file)}=1 if -x $file;
+ }
+ }
+
+ return keys %ret;
+}
+
+sub loadplugins () {
+ if (defined $config{libdir} && length $config{libdir}) {
+ unshift @INC, possibly_foolish_untaint($config{libdir});
+ }
+
+ foreach my $plugin (@{$config{default_plugins}}, @{$config{add_plugins}}) {
+ loadplugin($plugin);
+ }
+
+ if ($config{rcs}) {
+ if (exists $hooks{rcs}) {
+ error(gettext("cannot use multiple rcs plugins"));
+ }
+ loadplugin($config{rcs});
+ }
+ if (! exists $hooks{rcs}) {
+ loadplugin("norcs");
+ }
+
+ run_hooks(getopt => sub { shift->() });
+ if (grep /^-/, @ARGV) {
+ print STDERR "Unknown option (or missing parameter): $_\n"
+ foreach grep /^-/, @ARGV;
+ usage();
+ }
+
+ return 1;
+}
+
+sub loadplugin ($;$) {
+ my $plugin=shift;
+ my $force=shift;
+
+ return if ! $force && grep { $_ eq $plugin} @{$config{disable_plugins}};
+
+ foreach my $dir (defined $config{libdir} ? possibly_foolish_untaint($config{libdir}) : undef,
+ "$installdir/lib/ikiwiki") {
+ if (defined $dir && -x "$dir/plugins/$plugin") {
+ 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;
+ }
+ }
+
+ my $mod="IkiWiki::Plugin::".possibly_foolish_untaint($plugin);
+ eval qq{use $mod};
+ if ($@) {
+ error("Failed to load plugin $mod: $@");
+ }
+ $loaded_plugins{$plugin}=1;
+ return 1;
+}
+
+sub error ($;$) {
+ my $message=shift;
+ my $cleaner=shift;
+ log_message('err' => $message) if $config{syslog};
+ if (defined $cleaner) {
+ $cleaner->();
+ }
+ die $message."\n";
+}
+
+sub debug ($) {
+ return unless $config{verbose};
+ return log_message(debug => @_);
+}
+
+my $log_open=0;
+my $log_failed=0;
+sub log_message ($$) {
+ my $type=shift;
+
+ if ($config{syslog}) {
+ require Sys::Syslog;
+ if (! $log_open) {
+ Sys::Syslog::setlogsock('unix');
+ Sys::Syslog::openlog('ikiwiki', '', 'user');
+ $log_open=1;
+ }
+ 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";
+ }
+ else {
+ return print STDERR "@_\n";
+ }
+}
+
+sub possibly_foolish_untaint ($) {
+ my $tainted=shift;
+ my ($untainted)=$tainted=~/(.*)/s;
+ return $untainted;
+}
+
+sub basename ($) {
+ my $file=shift;
+
+ $file=~s!.*/+!!;
+ return $file;
+}
+
+sub dirname ($) {
+ my $file=shift;
+
+ $file=~s!/*[^/]+$!!;
+ return $file;
+}
+
+sub isinternal ($) {
+ my $page=shift;
+ return exists $pagesources{$page} &&
+ $pagesources{$page} =~ /\._([^.]+)$/;
+}
+
+sub pagetype ($) {
+ my $file=shift;
+
+ if ($file =~ /\.([^.]+)$/) {
+ return $1 if exists $hooks{htmlize}{$1};
+ }
+ my $base=basename($file);
+ if (exists $hooks{htmlize}{$base} &&
+ $hooks{htmlize}{$base}{noextension}) {
+ return $base;
+ }
+ return;
+}
+
+my %pagename_cache;
+
+sub pagename ($) {
+ my $file=shift;
+
+ if (exists $pagename_cache{$file}) {
+ return $pagename_cache{$file};
+ }
+
+ my $type=pagetype($file);
+ my $page=$file;
+ $page=~s/\Q.$type\E*$//
+ if defined $type && !$hooks{htmlize}{$type}{keepextension}
+ && !$hooks{htmlize}{$type}{noextension};
+ if ($config{indexpages} && $page=~/(.*)\/index$/) {
+ $page=$1;
+ }
+
+ $pagename_cache{$file} = $page;
+ 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;
+ my $filename=shift;
+
+ if (defined $filename) {
+ return $page."/".$filename.".".$ext;
+ }
+ elsif (! $config{usedirs} || $page eq 'index') {
+ return $page.".".$ext;
+ }
+ else {
+ return $page."/index.".$ext;
+ }
+}
+
+sub htmlpage ($) {
+ my $page=shift;
+
+ return targetpage($page, $config{htmlext});
+}
+
+sub srcfile_stat {
+ my $file=shift;
+ my $nothrow=shift;
+
+ return "$config{srcdir}/$file", stat(_) if -e "$config{srcdir}/$file";
+ foreach my $dir (@{$config{underlaydirs}}, $config{underlaydir}) {
+ return "$dir/$file", stat(_) if -e "$dir/$file";
+ }
+ error("internal error: $file cannot be found in $config{srcdir} or underlay") unless $nothrow;
+ return;
+}
+
+sub srcfile ($;$) {
+ return (srcfile_stat(@_))[0];
+}
+
+sub add_literal_underlay ($) {
+ my $dir=shift;
+
+ 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;
+}
+
+sub readfile ($;$$) {
+ my $file=shift;
+ my $binary=shift;
+ my $wantfd=shift;
+
+ if (-l $file) {
+ error("cannot read a symlink ($file)");
+ }
+
+ local $/=undef;
+ open (my $in, "<", $file) || error("failed to read $file: $!");
+ 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;
+}
+
+sub prep_writefile ($$) {
+ my $file=shift;
+ my $destdir=shift;
+
+ my $test=$file;
+ while (length $test) {
+ 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);
+ }
+
+ my $dir=dirname("$destdir/$file");
+ if (! -d $dir) {
+ my $d="";
+ foreach my $s (split(m!/+!, $dir)) {
+ $d.="$s/";
+ if (! -d $d) {
+ mkdir($d) || error("failed to create directory $d: $!");
+ }
+ }
+ }
+
+ return 1;
+}
+
+sub writefile ($$$;$$) {
+ my $file=shift; # can include subdirs
+ my $destdir=shift; # directory to put file in
+ my $content=shift;
+ my $binary=shift;
+ my $writer=shift;
+
+ prep_writefile($file, $destdir);
+
+ my $newfile="$destdir/$file.ikiwiki-new";
+ if (-l $newfile) {
+ error("cannot write to a symlink ($newfile)");
+ }
+
+ my $cleanup = sub { unlink($newfile) };
+ open (my $out, '>', $newfile) || error("failed to write $newfile: $!", $cleanup);
+ binmode($out) if ($binary);
+ if ($writer) {
+ $writer->(\*$out, $cleanup);
+ }
+ else {
+ print $out $content or error("failed writing to $newfile: $!", $cleanup);
+ }
+ close $out || error("failed saving $newfile: $!", $cleanup);
+ rename($newfile, "$destdir/$file") ||
+ error("failed renaming $newfile to $destdir/$file: $!", $cleanup);
+
+ return 1;
+}
+
+my %cleared;
+sub will_render ($$;$) {
+ my $page=shift;
+ my $dest=shift;
+ my $clear=shift;
+
+ # Important security check for independently created files.
+ if (-e "$config{destdir}/$dest" && ! $config{rebuild} &&
+ ! grep { $_ eq $dest } (@{$renderedfiles{$page}}, @{$oldrenderedfiles{$page}}, @{$wikistate{editpage}{previews}})) {
+ 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}) {
+ $renderedfiles{$page}=[$dest, grep { $_ ne $dest } @{$renderedfiles{$page}}];
+ }
+ else {
+ foreach my $old (@{$renderedfiles{$page}}) {
+ delete $destsources{$old};
+ }
+ $renderedfiles{$page}=[$dest];
+ $cleared{$page}=1;
+ }
+ $destsources{$dest}=$page;
+
+ return 1;
+}
+
+sub bestlink ($$) {
+ my $page=shift;
+ my $link=shift;
+
+ my $cwd=$page;
+ if ($link=~s/^\/+//) {
+ # absolute links
+ $cwd="";
+ }
+ $link=~s/\/$//;
+
+ do {
+ my $l=$cwd;
+ $l.="/" if length $l;
+ $l.=$link;
+
+ if (exists $pagesources{$l}) {
+ return $l;
+ }
+ elsif (exists $pagecase{lc $l}) {
+ return $pagecase{lc $l};
+ }
+ } while $cwd=~s{/?[^/]+$}{};
+
+ if (length $config{userdir}) {
+ my $l = "$config{userdir}/".lc($link);
+ if (exists $pagesources{$l}) {
+ return $l;
+ }
+ elsif (exists $pagecase{lc $l}) {
+ return $pagecase{lc $l};
+ }
+ }
+
+ #print STDERR "warning: page $page, broken link: $link\n";
+ return "";
+}
+
+sub isinlinableimage ($) {
+ my $file=shift;
+
+ return $file =~ /\.(png|gif|jpg|jpeg|svg)$/i;
+}
+
+sub pagetitle ($;$) {
+ my $page=shift;
+ my $unescaped=shift;
+
+ if ($unescaped) {
+ $page=~s/(__(\d+)__|_)/$1 eq '_' ? ' ' : chr($2)/eg;
+ }
+ else {
+ $page=~s/(__(\d+)__|_)/$1 eq '_' ? ' ' : "&#$2;"/eg;
+ }
+
+ return $page;
+}
+
+sub titlepage ($) {
+ my $title=shift;
+ # 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;
+ my $chars = defined $config{wiki_file_chars} ? $config{wiki_file_chars} : "-[:alnum:]+/.:_";
+ $link=~s/([^$chars])/$1 eq ' ' ? '_' : "__".ord($1)."__"/eg;
+ return $link;
+}
+
+sub cgiurl (@) {
+ my %params=@_;
+
+ 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 $local_url if ! defined $page;
+
+ $page=htmlpage($page);
+ $page=~s/[^\/]+$//;
+ $page=~s/[^\/]+\//..\//g;
+ 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
+ # absolute! (Debian bug #376658; fixed in debian unstable now)
+ my $path="/".shift;
+ my $base="/".shift;
+
+ require File::Spec;
+ my $ret=File::Spec->abs2rel($path, $base);
+ $ret=~s/^// if defined $ret;
+ return $ret;
+}
+
+sub displaytime ($;$$) {
+ # Plugins can override this function to mark up the time to
+ # display.
+ my $time=formattime($_[0], $_[1]);
+ if ($config{html5}) {
+ return '<time datetime="'.date_3339($_[0]).'"'.
+ ($_[2] ? ' pubdate="pubdate"' : '').
+ '>'.$time.'</time>';
+ }
+ else {
+ return '<span class="date">'.$time.'</span>';
+ }
+}
+
+sub formattime ($;$) {
+ # Plugins can override this function to format the time.
+ my $time=shift;
+ my $format=shift;
+ if (! defined $format) {
+ $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.
+ # 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 ($) {
+ my $url=shift;
+
+ # Ensure url is not an empty link, and if necessary,
+ # add ./ to avoid colon confusion.
+ if ($url !~ /^\// && $url !~ /^\.\.?\//) {
+ $url="./$url";
+ }
+
+ if ($config{usedirs}) {
+ $url =~ s!/index.$config{htmlext}$!/!;
+ }
+
+ return $url;
+}
+
+sub urlto ($;$$) {
+ my $to=shift;
+ my $from=shift;
+ my $absolute=shift;
+
+ if (! length $to) {
+ $to = 'index';
+ }
+
+ if (! $destsources{$to}) {
+ $to=htmlpage($to);
+ }
+
+ if ($absolute) {
+ 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)
+ my $link=shift;
+ my %opts=@_;
+
+ $link=~s/\/$//;
+
+ my $bestlink;
+ if (! $opts{forcesubpage}) {
+ $bestlink=bestlink($lpage, $link);
+ }
+ else {
+ $bestlink="$lpage/".lc($link);
+ }
+
+ my $linktext;
+ if (defined $opts{linktext}) {
+ $linktext=$opts{linktext};
+ }
+ else {
+ $linktext=pagetitle(basename($link));
+ }
+
+ return "<span class=\"selflink\">$linktext</span>"
+ if length $bestlink && isselflink($page, $bestlink) &&
+ ! defined $opts{anchor};
+
+ if (! $destsources{$bestlink}) {
+ $bestlink=htmlpage($bestlink);
+
+ if (! $destsources{$bestlink}) {
+ my $cgilink = "";
+ if (length $config{cgiurl}) {
+ $cgilink = "<a href=\"".
+ cgiurl(
+ do => "create",
+ page => $link,
+ from => $lpage
+ )."\" rel=\"nofollow\">?</a>";
+ }
+ return "<span class=\"createlink\">$cgilink$linktext</span>"
+ }
+ }
+
+ $bestlink=abs2rel($bestlink, dirname(htmlpage($page)));
+ $bestlink=beautify_urlpath($bestlink);
+
+ if (! $opts{noimageinline} && isinlinableimage($bestlink)) {
+ return "<img src=\"$bestlink\" alt=\"$linktext\" />";
+ }
+
+ if (defined $opts{anchor}) {
+ $bestlink.="#".$opts{anchor};
+ }
+
+ my @attrs;
+ foreach my $attr (qw{rel class title}) {
+ if (defined $opts{$attr}) {
+ push @attrs, " $attr=\"$opts{$attr}\"";
+ }
+ }
+
+ return "<a href=\"$bestlink\"@attrs>$linktext</a>";
+}
+
+sub userpage ($) {
+ my $user=shift;
+ return length $config{userdir} ? "$config{userdir}/$user" : $user;
+}
+
+sub openiduser ($) {
+ my $user=shift;
+
+ if (defined $user && $user =~ m!^https?://! &&
+ eval q{use Net::OpenID::VerifiedIdentity; 1} && !$@) {
+ my $display;
+
+ if (Net::OpenID::VerifiedIdentity->can("DisplayOfURL")) {
+ $display = Net::OpenID::VerifiedIdentity::DisplayOfURL($user);
+ }
+ else {
+ # backcompat with old version
+ my $oid=Net::OpenID::VerifiedIdentity->new(identity => $user);
+ $display=$oid->display;
+ }
+
+ # Convert "user.somehost.com" to "user [somehost.com]"
+ # (also "user.somehost.co.uk")
+ if ($display !~ /\[/) {
+ $display=~s/^([-a-zA-Z0-9]+?)\.([-.a-zA-Z0-9]+\.[a-z]+)$/$1 [$2]/;
+ }
+ # Convert "http://somehost.com/user" to "user [somehost.com]".
+ # (also "https://somehost.com/user/")
+ if ($display !~ /\[/) {
+ $display=~s/^https?:\/\/(.+)\/([^\/#?]+)\/?(?:[#?].*)?$/$2 [$1]/;
+ }
+ $display=~s!^https?://!!; # make sure this is removed
+ eval q{use CGI 'escapeHTML'};
+ error($@) if $@;
+ return escapeHTML($display);
+ }
+ return;
+}
+
+sub htmlize ($$$$) {
+ my $page=shift;
+ my $destpage=shift;
+ my $type=shift;
+ my $content=shift;
+
+ my $oneline = $content !~ /\n/;
+
+ if (exists $hooks{htmlize}{$type}) {
+ $content=$hooks{htmlize}{$type}{call}->(
+ page => $page,
+ content => $content,
+ );
+ }
+ else {
+ error("htmlization of $type not supported");
+ }
+
+ run_hooks(sanitize => sub {
+ $content=shift->(
+ page => $page,
+ destpage => $destpage,
+ content => $content,
+ );
+ });
+
+ if ($oneline) {
+ # hack to get rid of enclosing junk added by markdown
+ # and other htmlizers/sanitizers
+ $content=~s/^<p>//i;
+ $content=~s/<\/p>\n*$//i;
+ }
+
+ return $content;
+}
+
+sub linkify ($$$) {
+ my $page=shift;
+ my $destpage=shift;
+ my $content=shift;
+
+ run_hooks(linkify => sub {
+ $content=shift->(
+ page => $page,
+ destpage => $destpage,
+ content => $content,
+ );
+ });
+
+ return $content;
+}
+
+our %preprocessing;
+our $preprocess_preview=0;
+sub preprocess ($$$;$$) {
+ my $page=shift; # the page the data comes from
+ my $destpage=shift; # the page the data will appear in (different for inline)
+ my $content=shift;
+ my $scan=shift;
+ my $preview=shift;
+
+ # Using local because it needs to be set within any nested calls
+ # of this function.
+ local $preprocess_preview=$preview if defined $preview;
+
+ my $handle=sub {
+ my $escape=shift;
+ my $prefix=shift;
+ my $command=shift;
+ my $params=shift;
+ $params="" if ! defined $params;
+
+ if (length $escape) {
+ return "[[$prefix$command $params]]";
+ }
+ elsif (exists $hooks{preprocess}{$command}) {
+ return "" if $scan && ! $hooks{preprocess}{$command}{scan};
+ # Note: preserve order of params, some plugins may
+ # consider it significant.
+ my @params;
+ while ($params =~ m{
+ (?:([-.\w]+)=)? # 1: named parameter key?
+ (?:
+ """(.*?)""" # 2: triple-quoted value
+ |
+ "([^"]*?)" # 3: single-quoted 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
+ }msgx) {
+ my $key=$1;
+ my $val;
+ if (defined $2) {
+ $val=$2;
+ $val=~s/\r\n/\n/mg;
+ $val=~s/^\n+//g;
+ $val=~s/\n+$//g;
+ }
+ elsif (defined $3) {
+ $val=$3;
+ }
+ elsif (defined $4) {
+ $val=$4;
+ }
+ elsif (defined $7) {
+ $val=$7;
+ }
+ elsif (defined $6) {
+ $val=$6;
+ }
+
+ if (defined $key) {
+ push @params, $key, $val;
+ }
+ else {
+ push @params, $val, '';
+ }
+ }
+ if ($preprocessing{$page}++ > 8) {
+ # Avoid loops of preprocessed pages preprocessing
+ # other pages that preprocess them, etc.
+ return "[[!$command <span class=\"error\">".
+ sprintf(gettext("preprocessing loop detected on %s at depth %i"),
+ $page, $preprocessing{$page}).
+ "</span>]]";
+ }
+ my $ret;
+ if (! $scan) {
+ $ret=eval {
+ $hooks{preprocess}{$command}{call}->(
+ @params,
+ page => $page,
+ destpage => $destpage,
+ preview => $preprocess_preview,
+ );
+ };
+ if ($@) {
+ my $error=$@;
+ chomp $error;
+ eval q{use HTML::Entities};
+ $error = encode_entities($error);
+ $ret="[[!$command <span class=\"error\">".
+ gettext("Error").": $error"."</span>]]";
+ }
+ }
+ else {
+ # use void context during scan pass
+ eval {
+ $hooks{preprocess}{$command}{call}->(
+ @params,
+ page => $page,
+ destpage => $destpage,
+ preview => $preprocess_preview,
+ );
+ };
+ $ret="";
+ }
+ $preprocessing{$page}--;
+ return $ret;
+ }
+ else {
+ return "[[$prefix$command $params]]";
+ }
+ };
+
+ my $regex;
+ if ($config{prefix_directives}) {
+ $regex = qr{
+ (\\?) # 1: escape?
+ \[\[(!) # directive open; 2: prefix
+ ([-\w]+) # 3: command
+ ( # 4: the parameters..
+ \s+ # Must have space if parameters present
+ (?:
+ (?:[-.\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
+ # of directive
+ )
+ *)? # 0 or more parameters
+ \]\] # directive closed
+ }sx;
+ }
+ else {
+ $regex = qr{
+ (\\?) # 1: escape?
+ \[\[(!?) # directive open; 2: optional prefix
+ ([-\w]+) # 3: command
+ \s+
+ ( # 4: the parameters..
+ (?:
+ (?:[-.\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
+ # of directive
+ )
+ *) # 0 or more parameters
+ \]\] # directive closed
+ }sx;
+ }
+
+ $content =~ s{$regex}{$handle->($1, $2, $3, $4)}eg;
+ return $content;
+}
+
+sub filter ($$$) {
+ my $page=shift;
+ my $destpage=shift;
+ my $content=shift;
+
+ run_hooks(filter => sub {
+ $content=shift->(page => $page, destpage => $destpage,
+ content => $content);
+ });
+
+ return $content;
+}
+
+sub check_canedit ($$$;$) {
+ my $page=shift;
+ my $q=shift;
+ my $session=shift;
+ my $nonfatal=shift;
+
+ my $canedit;
+ run_hooks(canedit => sub {
+ return if defined $canedit;
+ my $ret=shift->($page, $q, $session);
+ if (defined $ret) {
+ if ($ret eq "") {
+ $canedit=1;
+ }
+ elsif (ref $ret eq 'CODE') {
+ $ret->() unless $nonfatal;
+ $canedit=0;
+ }
+ elsif (defined $ret) {
+ error($ret) unless $nonfatal;
+ $canedit=0;
+ }
+ }
+ });
+ return defined $canedit ? $canedit : 1;
+}
+
+sub check_content (@) {
+ my %params=@_;
+
+ return 1 if ! exists $hooks{checkcontent}; # optimisation
+
+ if (exists $pagesources{$params{page}}) {
+ my @diff;
+ my %old=map { $_ => 1 }
+ split("\n", readfile(srcfile($pagesources{$params{page}})));
+ foreach my $line (split("\n", $params{content})) {
+ push @diff, $line if ! exists $old{$line};
+ }
+ $params{diff}=join("\n", @diff);
+ }
+
+ my $ok;
+ run_hooks(checkcontent => sub {
+ return if defined $ok;
+ my $ret=shift->(%params);
+ if (defined $ret) {
+ if ($ret eq "") {
+ $ok=1;
+ }
+ elsif (ref $ret eq 'CODE') {
+ $ret->() unless $params{nonfatal};
+ $ok=0;
+ }
+ elsif (defined $ret) {
+ error($ret) unless $params{nonfatal};
+ $ok=0;
+ }
+ }
+
+ });
+ 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 () {
+ # 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}) {
+ mkdir($config{wikistatedir});
+ }
+ open($wikilock, '>', "$config{wikistatedir}/lockfile") ||
+ error ("cannot write to $config{wikistatedir}/lockfile: $!");
+ 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;
+}
+
+my $commitlock;
+
+sub commit_hook_enabled () {
+ open($commitlock, '+>', "$config{wikistatedir}/commitlock") ||
+ error("cannot write to $config{wikistatedir}/commitlock: $!");
+ if (! flock($commitlock, 1 | 4)) { # LOCK_SH | LOCK_NB to test
+ close($commitlock) || error("failed closing commitlock: $!");
+ return 0;
+ }
+ close($commitlock) || error("failed closing commitlock: $!");
+ return 1;
+}
+
+sub disable_commit_hook () {
+ open($commitlock, '>', "$config{wikistatedir}/commitlock") ||
+ error("cannot write to $config{wikistatedir}/commitlock: $!");
+ if (! flock($commitlock, 2)) { # LOCK_EX
+ error("failed to get commit lock");
+ }
+ return 1;
+}
+
+sub enable_commit_hook () {
+ return close($commitlock) if $commitlock;
+ return;
+}
+
+sub loadindex () {
+ %oldrenderedfiles=%pagectime=();
+ my $rebuild=$config{rebuild};
+ if (! $rebuild) {
+ %pagesources=%pagemtime=%oldlinks=%links=%depends=
+ %destsources=%renderedfiles=%pagecase=%pagestate=
+ %depends_simple=%typedlinks=%oldtypedlinks=();
+ }
+ my $in;
+ if (! open ($in, "<", "$config{wikistatedir}/indexdb")) {
+ if (-e "$config{wikistatedir}/index") {
+ system("ikiwiki-transition", "indexdb", $config{srcdir});
+ open ($in, "<", "$config{wikistatedir}/indexdb") || return;
+ }
+ else {
+ # gettime on first build
+ $config{gettime}=1 unless defined $config{gettime};
+ return;
+ }
+ }
+
+ my $index=Storable::fd_retrieve($in);
+ if (! defined $index) {
+ return 0;
+ }
+
+ my $pages;
+ 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;
+ %wikistate=();
+ }
+
+ foreach my $src (keys %$pages) {
+ my $d=$pages->{$src};
+ my $page;
+ if (exists $d->{page} && ! $rebuild) {
+ $page=$d->{page};
+ }
+ else {
+ $page=pagename($src);
+ }
+ $pagectime{$page}=$d->{ctime};
+ $pagesources{$page}=$src;
+ if (! $rebuild) {
+ $pagemtime{$page}=$d->{mtime};
+ $renderedfiles{$page}=$d->{dest};
+ if (exists $d->{links} && ref $d->{links}) {
+ $links{$page}=$d->{links};
+ $oldlinks{$page}=[@{$d->{links}}];
+ }
+ if (ref $d->{depends_simple} eq 'ARRAY') {
+ # old format
+ $depends_simple{$page}={
+ map { $_ => 1 } @{$d->{depends_simple}}
+ };
+ }
+ elsif (exists $d->{depends_simple}) {
+ $depends_simple{$page}=$d->{depends_simple};
+ }
+ if (exists $d->{dependslist}) {
+ # old format
+ $depends{$page}={
+ map { $_ => $DEPEND_CONTENT }
+ @{$d->{dependslist}}
+ };
+ }
+ elsif (exists $d->{depends} && ! ref $d->{depends}) {
+ # old format
+ $depends{$page}={$d->{depends} => $DEPEND_CONTENT };
+ }
+ elsif (exists $d->{depends}) {
+ $depends{$page}=$d->{depends};
+ }
+ if (exists $d->{state}) {
+ $pagestate{$page}=$d->{state};
+ }
+ if (exists $d->{typedlinks}) {
+ $typedlinks{$page}=$d->{typedlinks};
+
+ while (my ($type, $links) = each %{$typedlinks{$page}}) {
+ next unless %$links;
+ $oldtypedlinks{$page}{$type} = {%$links};
+ }
+ }
+ }
+ $oldrenderedfiles{$page}=[@{$d->{dest}}];
+ }
+ foreach my $page (keys %pagesources) {
+ $pagecase{lc $page}=$page;
+ }
+ 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 @plugins=keys %loaded_plugins;
+
+ if (! -d $config{wikistatedir}) {
+ mkdir($config{wikistatedir});
+ }
+ 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{page}{$src}={
+ page => $page,
+ ctime => $pagectime{$page},
+ mtime => $pagemtime{$page},
+ dest => $renderedfiles{$page},
+ links => $links{$page},
+ };
+
+ if (exists $depends{$page}) {
+ $index{page}{$src}{depends} = $depends{$page};
+ }
+
+ if (exists $depends_simple{$page}) {
+ $index{page}{$src}{depends_simple} = $depends_simple{$page};
+ }
+
+ if (exists $typedlinks{$page} && %{$typedlinks{$page}}) {
+ $index{page}{$src}{typedlinks} = $typedlinks{$page};
+ }
+
+ if (exists $pagestate{$page}) {
+ $index{page}{$src}{state}=$pagestate{$page};
+ }
+ }
+
+ $index{state}={};
+ 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;
+ close $out || error("failed saving to $newfile: $!", $cleanup);
+ rename($newfile, "$config{wikistatedir}/indexdb") ||
+ error("failed renaming $newfile to $config{wikistatedir}/indexdb", $cleanup);
+
+ return 1;
+}
+
+sub template_file ($) {
+ 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);
+ }
+
+ 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_depends ($$;@) {
+ my $name=shift;
+ my $page=shift;
+
+ 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);
+ }
+
+ my @opts=(
+ filter => sub {
+ my $text_ref = shift;
+ ${$text_ref} = decode_utf8(${$text_ref});
+ run_hooks(readtemplate => sub {
+ ${$text_ref} = shift->(
+ id => $name,
+ page => $tpage,
+ content => ${$text_ref},
+ untrusted => $untrusted,
+ );
+ });
+ },
+ loop_context_vars => 1,
+ die_on_bad_params => 0,
+ parent_global_vars => 1,
+ filename => $filename,
+ @_,
+ ($untrusted ? (no_includes => 1) : ()),
+ );
+ return @opts if wantarray;
+
+ require HTML::Template;
+ return HTML::Template->new(@opts);
+}
+
+sub template ($;@) {
+ template_depends(shift, undef, @_);
+}
+
+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);
+ });
+ $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 (@) {
+ my %param=@_;
+
+ if (! exists $param{type} || ! ref $param{call} || ! exists $param{id}) {
+ error 'hook requires type, call, and id parameters';
+ }
+
+ return if $param{no_override} && exists $hooks{$param{type}}{$param{id}};
+
+ $hooks{$param{type}}{$param{id}}=\%param;
+ return 1;
+}
+
+sub run_hooks ($$) {
+ # Calls the given sub for each hook of the given type,
+ # passing it the hook function to call.
+ my $type=shift;
+ my $sub=shift;
+
+ if (exists $hooks{$type}) {
+ my (@first, @middle, @last);
+ foreach my $id (keys %{$hooks{$type}}) {
+ if ($hooks{$type}{$id}{first}) {
+ push @first, $id;
+ }
+ elsif ($hooks{$type}{$id}{last}) {
+ push @last, $id;
+ }
+ else {
+ push @middle, $id;
+ }
+ }
+ foreach my $id (@first, @middle, @last) {
+ $sub->($hooks{$type}{$id}{call});
+ }
+ }
+
+ 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_getmtime ($) {
+ $hooks{rcs}{rcs_getmtime}{call}->(@_);
+}
+
+sub rcs_receive () {
+ $hooks{rcs}{rcs_receive}{call}->();
+}
+
+sub add_depends ($$;$) {
+ my $page=shift;
+ my $pagespec=shift;
+ my $deptype=shift || $DEPEND_CONTENT;
+
+ # Is the pagespec a simple page name?
+ if ($pagespec =~ /$config{wiki_file_regexp}/ &&
+ $pagespec !~ /[\s*?()!]/) {
+ $depends_simple{$page}{lc $pagespec} |= $deptype;
+ return 1;
+ }
+
+ # Add explicit dependencies for influences.
+ my $sub=pagespec_translate($pagespec);
+ return unless defined $sub;
+ foreach my $p (keys %pagesources) {
+ my $r=$sub->($p, location => $page);
+ my $i=$r->influences;
+ my $static=$r->influences_static;
+ foreach my $k (keys %$i) {
+ next unless $r || $static || $k eq $page;
+ $depends_simple{$page}{lc $k} |= $i->{$k};
+ }
+ last if $static;
+ }
+
+ $depends{$page}{$pagespec} |= $deptype;
+ return 1;
+}
+
+sub deptype (@) {
+ my $deptype=0;
+ foreach my $type (@_) {
+ if ($type eq 'presence') {
+ $deptype |= $DEPEND_PRESENCE;
+ }
+ elsif ($type eq 'links') {
+ $deptype |= $DEPEND_LINKS;
+ }
+ elsif ($type eq 'content') {
+ $deptype |= $DEPEND_CONTENT;
+ }
+ }
+ return $deptype;
+}
+
+my $file_prune_regexp;
+sub file_pruned ($) {
+ my $file=shift;
+
+ if (defined $config{include} && length $config{include}) {
+ return 0 if $file =~ m/$config{include}/;
+ }
+
+ if (! defined $file_prune_regexp) {
+ $file_prune_regexp='('.join('|', @{$config{wiki_file_prune_regexps}}).')';
+ $file_prune_regexp=qr/$file_prune_regexp/;
+ }
+ return $file =~ m/$file_prune_regexp/;
+}
+
+sub define_gettext () {
+ # If translation is needed, redefine the gettext function to do it.
+ # Otherwise, it becomes a quick no-op.
+ my $gettext_obj;
+ my $getobj;
+ if ((exists $ENV{LANG} && length $ENV{LANG}) ||
+ (exists $ENV{LC_ALL} && length $ENV{LC_ALL}) ||
+ (exists $ENV{LC_MESSAGES} && length $ENV{LC_MESSAGES})) {
+ $getobj=sub {
+ $gettext_obj=eval q{
+ use Locale::gettext q{textdomain};
+ Locale::gettext->domain('ikiwiki')
+ };
+ };
+ }
+
+ no warnings 'redefine';
+ *gettext=sub {
+ $getobj->() if $getobj;
+ if ($gettext_obj) {
+ $gettext_obj->get(shift);
+ }
+ else {
+ return shift;
+ }
+ };
+ *ngettext=sub {
+ $getobj->() if $getobj;
+ if ($gettext_obj) {
+ $gettext_obj->nget(@_);
+ }
+ else {
+ return ($_[2] == 1 ? $_[0] : $_[1])
+ }
+ };
+}
+
+sub gettext {
+ define_gettext();
+ gettext(@_);
+}
+
+sub ngettext {
+ define_gettext();
+ ngettext(@_);
+}
+
+sub yesno ($) {
+ my $val=shift;
+
+ return (defined $val && (lc($val) eq gettext("yes") || lc($val) eq "yes" || $val eq "1"));
+}
+
+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 add_link ($$;$) {
+ my $page=shift;
+ my $link=shift;
+ my $type=shift;
+
+ push @{$links{$page}}, $link
+ unless grep { $_ eq $link } @{$links{$page}};
+
+ if (defined $type) {
+ $typedlinks{$page}{$type}{$link} = 1;
+ }
+}
+
+sub add_autofile ($$$) {
+ my $file=shift;
+ my $plugin=shift;
+ my $generator=shift;
+
+ $autofiles{$file}{plugin}=$plugin;
+ $autofiles{$file}{generator}=$generator;
+}
+
+sub useragent () {
+ eval q{use LWP};
+ error($@) if $@;
+
+ return LWP::UserAgent->new(
+ cookie_jar => $config{cookiejar},
+ env_proxy => 1, # respect proxy env vars
+ agent => $config{useragent},
+ protocols_allowed => [qw(http https)],
+ );
+}
+
+sub sortspec_translate ($$) {
+ my $spec = shift;
+ my $reverse = shift;
+
+ my $code = "";
+ my @data;
+ while ($spec =~ m{
+ \s*
+ (-?) # group 1: perhaps negated
+ \s*
+ ( # group 2: a word
+ \w+\([^\)]*\) # command(params)
+ |
+ [^\s]+ # or anything else
+ )
+ \s*
+ }gx) {
+ my $negated = $1;
+ my $word = $2;
+ my $params = undef;
+
+ if ($word =~ m/^(\w+)\((.*)\)$/) {
+ # command with parameters
+ $params = $2;
+ $word = $1;
+ }
+ elsif ($word !~ m/^\w+$/) {
+ error(sprintf(gettext("invalid sort type %s"), $word));
+ }
+
+ if (length $code) {
+ $code .= " || ";
+ }
+
+ if ($negated) {
+ $code .= "-";
+ }
+
+ if (exists $IkiWiki::SortSpec::{"cmp_$word"}) {
+ if (defined $params) {
+ push @data, $params;
+ $code .= "IkiWiki::SortSpec::cmp_$word(\$data[$#data])";
+ }
+ else {
+ $code .= "IkiWiki::SortSpec::cmp_$word(undef)";
+ }
+ }
+ else {
+ error(sprintf(gettext("unknown sort type %s"), $word));
+ }
+ }
+
+ if (! length $code) {
+ # undefined sorting method... sort arbitrarily
+ return sub { 0 };
+ }
+
+ if ($reverse) {
+ $code="-($code)";
+ }
+
+ no warnings;
+ return eval 'sub { '.$code.' }';
+}
+
+sub pagespec_translate ($) {
+ my $spec=shift;
+
+ # Convert spec to perl code.
+ my $code="";
+ my @data;
+ while ($spec=~m{
+ \s* # ignore whitespace
+ ( # 1: match a single word
+ \! # !
+ |
+ \( # (
+ |
+ \) # )
+ |
+ \w+\([^\)]*\) # command(params)
+ |
+ [^\s()]+ # any other text
+ )
+ \s* # ignore whitespace
+ }gx) {
+ my $word=$1;
+ if (lc $word eq 'and') {
+ $code.=' &';
+ }
+ elsif (lc $word eq 'or') {
+ $code.=' |';
+ }
+ elsif ($word eq "(" || $word eq ")" || $word eq "!") {
+ $code.=' '.$word;
+ }
+ elsif ($word =~ /^(\w+)\((.*)\)$/) {
+ if (exists $IkiWiki::PageSpec::{"match_$1"}) {
+ push @data, $2;
+ $code.="IkiWiki::PageSpec::match_$1(\$page, \$data[$#data], \@_)";
+ }
+ else {
+ push @data, qq{unknown function in pagespec "$word"};
+ $code.="IkiWiki::ErrorReason->new(\$data[$#data])";
+ }
+ }
+ else {
+ push @data, $word;
+ $code.=" IkiWiki::PageSpec::match_glob(\$page, \$data[$#data], \@_)";
+ }
+ }
+
+ if (! length $code) {
+ $code="IkiWiki::FailReason->new('empty pagespec')";
+ }
+
+ no warnings;
+ return eval 'sub { my $page=shift; '.$code.' }';
+}
+
+sub pagespec_match ($$;@) {
+ my $page=shift;
+ my $spec=shift;
+ my @params=@_;
+
+ # Backwards compatability with old calling convention.
+ if (@params == 1) {
+ unshift @params, 'location';
+ }
+
+ my $sub=pagespec_translate($spec);
+ return IkiWiki::ErrorReason->new("syntax error in pagespec \"$spec\"")
+ if ! defined $sub;
+ return $sub->($page, @params);
+}
+
+# e.g. @pages = sort_pages("title", \@pages, reverse => "yes")
+#
+# Not exported yet, but could be in future if it is generally useful.
+# Note that this signature is not the same as IkiWiki::SortSpec::sort_pages,
+# which is "more internal".
+sub sort_pages ($$;@) {
+ my $sort = shift;
+ my $list = shift;
+ my %params = @_;
+ $sort = sortspec_translate($sort, $params{reverse});
+ return IkiWiki::SortSpec::sort_pages($sort, @$list);
+}
+
+sub pagespec_match_list ($$;@) {
+ my $page=shift;
+ my $pagespec=shift;
+ my %params=@_;
+
+ # Backwards compatability with old calling convention.
+ if (ref $page) {
+ print STDERR "warning: a plugin (".caller().") is using pagespec_match_list in an obsolete way, and needs to be updated\n";
+ $params{list}=$page;
+ $page=$params{location}; # ugh!
+ }
+
+ my $sub=pagespec_translate($pagespec);
+ error "syntax error in pagespec \"$pagespec\""
+ if ! defined $sub;
+ my $sort=sortspec_translate($params{sort}, $params{reverse})
+ if defined $params{sort};
+
+ my @candidates;
+ if (exists $params{list}) {
+ @candidates=exists $params{filter}
+ ? grep { ! $params{filter}->($_) } @{$params{list}}
+ : @{$params{list}};
+ }
+ else {
+ @candidates=exists $params{filter}
+ ? grep { ! $params{filter}->($_) } keys %pagesources
+ : keys %pagesources;
+ }
+
+ # clear params, remainder is passed to pagespec
+ $depends{$page}{$pagespec} |= ($params{deptype} || $DEPEND_CONTENT);
+ my $num=$params{num};
+ delete @params{qw{num deptype reverse sort filter list}};
+
+ # when only the top matches will be returned, it's efficient to
+ # sort before matching to pagespec,
+ if (defined $num && defined $sort) {
+ @candidates=IkiWiki::SortSpec::sort_pages(
+ $sort, @candidates);
+ }
+
+ my @matches;
+ my $firstfail;
+ my $count=0;
+ my $accum=IkiWiki::SuccessReason->new();
+ foreach my $p (@candidates) {
+ my $r=$sub->($p, %params, location => $page);
+ error(sprintf(gettext("cannot match pages: %s"), $r))
+ if $r->isa("IkiWiki::ErrorReason");
+ unless ($r || $r->influences_static) {
+ $r->remove_influence($p);
+ }
+ $accum |= $r;
+ if ($r) {
+ push @matches, $p;
+ last if defined $num && ++$count == $num;
+ }
+ }
+
+ # Add simple dependencies for accumulated influences.
+ my $i=$accum->influences;
+ foreach my $k (keys %$i) {
+ $depends_simple{$page}{lc $k} |= $i->{$k};
+ }
+
+ # when all matches will be returned, it's efficient to
+ # sort after matching
+ if (! defined $num && defined $sort) {
+ return IkiWiki::SortSpec::sort_pages(
+ $sort, @matches);
+ }
+ else {
+ return @matches;
+ }
+}
+
+sub pagespec_valid ($) {
+ my $spec=shift;
+
+ return defined pagespec_translate($spec);
+}
+
+sub glob2re ($) {
+ my $re=quotemeta(shift);
+ $re=~s/\\\*/.*/g;
+ $re=~s/\\\?/./g;
+ return qr/^$re$/i;
+}
+
+package IkiWiki::FailReason;
+
+use overload (
+ '""' => sub { $_[0][0] },
+ '0+' => sub { 0 },
+ '!' => sub { bless $_[0], 'IkiWiki::SuccessReason'},
+ '&' => sub { $_[0]->merge_influences($_[1], 1); $_[0] },
+ '|' => sub { $_[1]->merge_influences($_[0]); $_[1] },
+ fallback => 1,
+);
+
+our @ISA = 'IkiWiki::SuccessReason';
+
+package IkiWiki::SuccessReason;
+
+# A blessed array-ref:
+#
+# [0]: human-readable reason for success (or, in FailReason subclass, failure)
+# [1]{""}:
+# - if absent or false, the influences of this evaluation are "static",
+# see the influences_static method
+# - if true, they are dynamic (not static)
+# [1]{any other key}:
+# the dependency types of influences, as returned by the influences method
+
+use overload (
+ # in string context, it's the human-readable reason
+ '""' => sub { $_[0][0] },
+ # in boolean context, SuccessReason is 1 and FailReason is 0
+ '0+' => sub { 1 },
+ # negating a result gives the opposite result with the same influences
+ '!' => sub { bless $_[0], 'IkiWiki::FailReason'},
+ # A & B = (A ? B : A) with the influences of both
+ '&' => sub { $_[1]->merge_influences($_[0], 1); $_[1] },
+ # A | B = (A ? A : B) with the influences of both
+ '|' => sub { $_[0]->merge_influences($_[1]); $_[0] },
+ fallback => 1,
+);
+
+# SuccessReason->new("human-readable reason", page => deptype, ...)
+
+sub new {
+ my $class = shift;
+ my $value = shift;
+ return bless [$value, {@_}], $class;
+}
+
+# influences(): return a reference to a copy of the hash
+# { page => dependency type } describing the pages that indirectly influenced
+# this result, but would not cause a dependency through ikiwiki's core
+# dependency logic.
+#
+# See [[todo/dependency_types]] for extensive discussion of what this means.
+#
+# influences(page => deptype, ...): remove all influences, replace them
+# with the arguments, and return a reference to a copy of the new influences.
+
+sub influences {
+ my $this=shift;
+ $this->[1]={@_} if @_;
+ my %i=%{$this->[1]};
+ delete $i{""};
+ return \%i;
+}
+
+# True if this result has the same influences whichever page it matches,
+# For instance, whether bar matches backlink(foo) is influenced only by
+# the set of links in foo, so its only influence is { foo => DEPEND_LINKS },
+# which does not mention bar anywhere.
+#
+# False if this result would have different influences when matching
+# different pages. For instance, when testing whether link(foo) matches bar,
+# { bar => DEPEND_LINKS } is an influence on that result, because changing
+# bar's links could change the outcome; so its influences are not the same
+# as when testing whether link(foo) matches baz.
+#
+# Static influences are one of the things that make pagespec_match_list
+# more efficient than repeated calls to pagespec_match.
+
+sub influences_static {
+ return ! $_[0][1]->{""};
+}
+
+# Change the influences of $this to be the influences of "$this & $other"
+# or "$this | $other".
+#
+# If both $this and $other are either successful or have influences,
+# or this is an "or" operation, the result has all the influences from
+# either of the arguments. It has dynamic influences if either argument
+# has dynamic influences.
+#
+# If this is an "and" operation, and at least one argument is a
+# FailReason with no influences, the result has no influences, and they
+# are not dynamic. For instance, link(foo) matching bar is influenced
+# by bar, but enabled(ddate) has no influences. Suppose ddate is disabled;
+# then (link(foo) and enabled(ddate)) not matching bar is not influenced by
+# bar, because it would be false however often you edit bar.
+
+sub merge_influences {
+ my $this=shift;
+ my $other=shift;
+ my $anded=shift;
+
+ # This "if" is odd because it needs to avoid negating $this
+ # or $other, which would alter the objects in-place. Be careful.
+ if (! $anded || (($this || %{$this->[1]}) &&
+ ($other || %{$other->[1]}))) {
+ foreach my $influence (keys %{$other->[1]}) {
+ $this->[1]{$influence} |= $other->[1]{$influence};
+ }
+ }
+ else {
+ # influence blocker
+ $this->[1]={};
+ }
+}
+
+# Change $this so it is not considered to be influenced by $torm.
+
+sub remove_influence {
+ my $this=shift;
+ my $torm=shift;
+
+ delete $this->[1]{$torm};
+}
+
+package IkiWiki::ErrorReason;
+
+our @ISA = 'IkiWiki::FailReason';
+
+package IkiWiki::PageSpec;
+
+sub derel ($$) {
+ my $path=shift;
+ my $from=shift;
+
+ 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;
+ my %params=@_;
+
+ $glob=derel($glob, $params{location});
+
+ # 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");
+ }
+ else {
+ return IkiWiki::FailReason->new("$glob matches $page, but the page is an internal page");
+ }
+ }
+ else {
+ return IkiWiki::FailReason->new("$glob does not match $page");
+ }
+}
+
+sub match_internal ($$;@) {
+ 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 ($$;@) {
+ my $page=shift;
+ my $link=lc(shift);
+ my %params=@_;
+
+ $link=derel($link, $params{location});
+ my $from=exists $params{location} ? $params{location} : '';
+ my $linktype=$params{linktype};
+ my $qualifier='';
+ if (defined $linktype) {
+ $qualifier=" with type $linktype";
+ }
+
+ my $links = $IkiWiki::links{$page};
+ return IkiWiki::FailReason->new("$page has no links", $page => $IkiWiki::DEPEND_LINKS, "" => 1)
+ 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 ($bestlink eq IkiWiki::bestlink($page, $p)) {
+ return IkiWiki::SuccessReason->new("$page links to $link$qualifier", $page => $IkiWiki::DEPEND_LINKS, "" => 1)
+ }
+ }
+ else {
+ 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 (match_glob($p_rel, $link, %params)) {
+ return IkiWiki::SuccessReason->new("$page links to page $p_rel$qualifier, matching $link", $page => $IkiWiki::DEPEND_LINKS, "" => 1)
+ }
+ }
+ }
+ return IkiWiki::FailReason->new("$page does not link to $link$qualifier", $page => $IkiWiki::DEPEND_LINKS, "" => 1);
+}
+
+sub match_backlink ($$;@) {
+ 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;
+}
+
+sub match_created_before ($$;@) {
+ my $page=shift;
+ my $testpage=shift;
+ my %params=@_;
+
+ $testpage=derel($testpage, $params{location});
+
+ if (exists $IkiWiki::pagectime{$testpage}) {
+ if ($IkiWiki::pagectime{$page} < $IkiWiki::pagectime{$testpage}) {
+ return IkiWiki::SuccessReason->new("$page created before $testpage", $testpage => $IkiWiki::DEPEND_PRESENCE);
+ }
+ else {
+ return IkiWiki::FailReason->new("$page not created before $testpage", $testpage => $IkiWiki::DEPEND_PRESENCE);
+ }
+ }
+ else {
+ return IkiWiki::ErrorReason->new("$testpage does not exist", $testpage => $IkiWiki::DEPEND_PRESENCE);
+ }
+}
+
+sub match_created_after ($$;@) {
+ my $page=shift;
+ my $testpage=shift;
+ my %params=@_;
+
+ $testpage=derel($testpage, $params{location});
+
+ if (exists $IkiWiki::pagectime{$testpage}) {
+ if ($IkiWiki::pagectime{$page} > $IkiWiki::pagectime{$testpage}) {
+ return IkiWiki::SuccessReason->new("$page created after $testpage", $testpage => $IkiWiki::DEPEND_PRESENCE);
+ }
+ else {
+ return IkiWiki::FailReason->new("$page not created after $testpage", $testpage => $IkiWiki::DEPEND_PRESENCE);
+ }
+ }
+ else {
+ return IkiWiki::ErrorReason->new("$testpage does not exist", $testpage => $IkiWiki::DEPEND_PRESENCE);
+ }
+}
+
+sub match_creation_day ($$;@) {
+ 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 {
+ return IkiWiki::FailReason->new('creation_day did not match');
+ }
+}
+
+sub match_creation_month ($$;@) {
+ 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 {
+ return IkiWiki::FailReason->new('creation_month did not match');
+ }
+}
+
+sub match_creation_year ($$;@) {
+ 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 {
+ return IkiWiki::FailReason->new('creation_year did not match');
+ }
+}
+
+sub match_user ($$;@) {
+ shift;
+ my $user=shift;
+ my %params=@_;
+
+ if (! exists $params{user}) {
+ return IkiWiki::ErrorReason->new("no user specified");
+ }
+
+ my $regexp=IkiWiki::glob2re($user);
+
+ if (defined $params{user} && $params{user}=~$regexp) {
+ 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::ErrorReason->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::ErrorReason->new("no IP specified");
+ }
+
+ my $regexp=IkiWiki::glob2re(lc $ip);
+
+ if (defined $params{ip} && lc $params{ip}=~$regexp) {
+ return IkiWiki::SuccessReason->new("IP is $ip");
+ }
+ else {
+ return IkiWiki::FailReason->new("IP is $params{ip}, not $ip");
+ }
+}
+
+package IkiWiki::SortSpec;
+
+# This is in the SortSpec namespace so that the $a and $b that sort() uses
+# are easily available in this namespace, for cmp functions to use them.
+sub sort_pages {
+ my $f=shift;
+ sort $f @_
+}
+
+sub cmp_title {
+ IkiWiki::pagetitle(IkiWiki::basename($a))
+ cmp
+ 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} }
+
+1
diff -Nru ikiwiki-3.20141016.4/.pc/CVE-2019-9187-4.patch/doc/plugins/aggregate.mdwn ikiwiki-3.20141016.4+deb8u1/.pc/CVE-2019-9187-4.patch/doc/plugins/aggregate.mdwn
--- ikiwiki-3.20141016.4/.pc/CVE-2019-9187-4.patch/doc/plugins/aggregate.mdwn 1970-01-01 10:00:00.000000000 +1000
+++ ikiwiki-3.20141016.4+deb8u1/.pc/CVE-2019-9187-4.patch/doc/plugins/aggregate.mdwn 2019-03-07 17:33:15.000000000 +1100
@@ -0,0 +1,57 @@
+[[!template id=plugin name=aggregate author="[[Joey]]"]]
+[[!tag type/special-purpose]]
+
+This plugin allows content from other feeds to be aggregated into the
+wiki. To specify feeds to aggregate, use the
+[[ikiwiki/directive/aggregate]] [[ikiwiki/directive]].
+
+## requirements
+
+The [[meta]] and [[tag]] plugins are also recommended to be used with this
+one. Either the [[htmltidy]] or [[htmlbalance]] plugin is suggested, since
+feeds can easily contain html problems, some of which these plugins can fix.
+
+## triggering aggregation
+
+You will need to run ikiwiki periodically from a cron job, passing it the
+--aggregate parameter, to make it check for new posts. Here's an example
+crontab entry:
+
+ */15 * * * * ikiwiki --setup my.wiki --aggregate --refresh
+
+The plugin updates a file `.ikiwiki/aggregatetime` with the unix time stamp
+when the next aggregation run could occur. (The file may be empty, if no
+aggregation is required.) This can be integrated into more complex cron
+jobs or systems to trigger aggregation only when needed.
+
+Alternatively, you can allow `ikiwiki.cgi` to trigger the aggregation. You
+should only need this if for some reason you cannot use cron, and instead
+want to use a service such as [WebCron](http://webcron.org). To enable
+this, turn on `aggregate_webtrigger` in your setup file. The url to
+visit is `http://whatever/ikiwiki.cgi?do=aggregate_webtrigger`. Anyone
+can visit the url to trigger an aggregation run, but it will only check
+each feed if its `updateinterval` has passed.
+
+## aggregated pages
+
+This plugin creates a page for each aggregated item.
+
+If the `aggregateinternal` option is enabled in the setup file (which is
+the default), aggregated pages are stored in the source directory with a
+"._aggregated" extension. These pages cannot be edited by web users, and
+do not generate first-class wiki pages. They can still be inlined into a
+blog, but you have to use `internal` in [[PageSpecs|IkiWiki/PageSpec]],
+like `internal(blog/*)`.
+
+If `aggregateinternal` is disabled, you will need to enable the [[html]]
+plugin as well as aggregate itself, since feed entries will be stored as
+HTML, and as first-class wiki pages -- each one generates
+a separate HTML page in the output, and they can even be edited. This
+option is provided only for backwards compatability.
+
+## cookies
+
+The `cookiejar` option can be used to configure how [[!cpan LWP::UserAgent]]
+handles cookies. The default is to read them from a file
+`~/.ikiwiki/cookies`, which can be populated using standard perl cookie
+tools like [[!cpan HTTP::Cookies]].
diff -Nru ikiwiki-3.20141016.4/.pc/CVE-2019-9187-4.patch/doc/plugins/blogspam.mdwn ikiwiki-3.20141016.4+deb8u1/.pc/CVE-2019-9187-4.patch/doc/plugins/blogspam.mdwn
--- ikiwiki-3.20141016.4/.pc/CVE-2019-9187-4.patch/doc/plugins/blogspam.mdwn 1970-01-01 10:00:00.000000000 +1000
+++ ikiwiki-3.20141016.4+deb8u1/.pc/CVE-2019-9187-4.patch/doc/plugins/blogspam.mdwn 2019-03-07 17:33:15.000000000 +1100
@@ -0,0 +1,32 @@
+[[!template id=plugin name=blogspam author="[[Joey]]"]]
+[[!tag type/auth type/comments]]
+
+This plugin adds antispam support to ikiwiki, using the
+[blogspam.net](http://blogspam.net/) API. Both page edits and
+[[comment|comments]] postings can be checked for spam. Page edits that
+appear to contain spam will be rejected; comments that look spammy will be
+stored in a queue for moderation by an admin.
+
+To check for and moderate comments, log in to the wiki as an admin,
+go to your Preferences page, and click the "Comment Moderation" button.
+
+The plugin requires the [[!cpan JSON]] perl module.
+
+You can control how content is tested via the `blogspam_options` setting.
+The list of options is [here](http://blogspam.net/api/testComment.html#options).
+By default, the options are configured in a way that is appropriate for
+wiki content. This includes turning off some of the more problematic tests.
+An interesting option for testing is `fail`, by setting it (e.g.,
+`blogspam_options => 'fail'`), *all* comments will be marked as SPAM, so that
+you can check whether the interaction with blogspam.net works.
+
+The `blogspam_pagespec` setting is a [[ikiwiki/PageSpec]] that can be
+used to configure which pages are checked for spam. The default is to check
+all edits. If you only want to check [[comments]] (not wiki page edits),
+set it to "postcomment(*)". Posts by admins are never checked for spam.
+
+By default, the blogspam.net server is used to do the spam checking. To
+change this, the `blogspam_server` option can be set to the url for a
+different server implementing the same API. Note that content is sent
+unencrypted over the internet to the server, and the server sees
+the full text of the content.
diff -Nru ikiwiki-3.20141016.4/.pc/CVE-2019-9187-4.patch/doc/plugins/openid.mdwn ikiwiki-3.20141016.4+deb8u1/.pc/CVE-2019-9187-4.patch/doc/plugins/openid.mdwn
--- ikiwiki-3.20141016.4/.pc/CVE-2019-9187-4.patch/doc/plugins/openid.mdwn 1970-01-01 10:00:00.000000000 +1000
+++ ikiwiki-3.20141016.4+deb8u1/.pc/CVE-2019-9187-4.patch/doc/plugins/openid.mdwn 2019-03-07 17:33:15.000000000 +1100
@@ -0,0 +1,37 @@
+[[!template id=plugin name=openid core=1 author="[[Joey]]"]]
+[[!tag type/auth]]
+
+This plugin allows users to use their [OpenID](http://openid.net/) to log
+into the wiki.
+
+The plugin needs the [[!cpan Net::OpenID::Consumer]] perl module.
+Version 1.x is needed in order for OpenID v2 to work.
+
+The [[!cpan LWPx::ParanoidAgent]] perl module is used if available, for
+added security. Finally, the [[!cpan Crypt::SSLeay]] perl module is needed
+to support users entering "https" OpenID urls.
+
+This plugin is enabled by default, but can be turned off if you want to
+only use some other form of authentication, such as [[passwordauth]].
+
+## options
+
+These options do not normally need to be set, but can be useful in
+certain setups.
+
+* `openid_realm` can be used to control the scope of the openid request.
+ It defaults to the `cgiurl` (or `openid_cgiurl` if set); only allowing
+ ikiwiki's [[CGI]] to authenticate. If you have multiple ikiwiki instances,
+ or other things using openid on the same site, you may choose to put them
+ all in the same realm to improve the user's openid experience. It is an
+ url pattern, so can be set to eg "http://*.example.com/"
+
+* `openid_cgiurl` can be used to cause a different than usual `cgiurl`
+ to be used when doing openid authentication. The `openid_cgiurl` must
+ point to an ikiwiki [[CGI]], and it will need to match the `openid_realm`
+ to work.
+
+## troubleshooting
+
+See [[plugins/openid/troubleshooting]] for a number of issues that may
+need to be addressed when setting up ikiwiki to accept OpenID logins reliably.
diff -Nru ikiwiki-3.20141016.4/.pc/CVE-2019-9187-4.patch/doc/plugins/pinger.mdwn ikiwiki-3.20141016.4+deb8u1/.pc/CVE-2019-9187-4.patch/doc/plugins/pinger.mdwn
--- ikiwiki-3.20141016.4/.pc/CVE-2019-9187-4.patch/doc/plugins/pinger.mdwn 1970-01-01 10:00:00.000000000 +1000
+++ ikiwiki-3.20141016.4+deb8u1/.pc/CVE-2019-9187-4.patch/doc/plugins/pinger.mdwn 2019-03-07 17:33:15.000000000 +1100
@@ -0,0 +1,20 @@
+[[!template id=plugin name=pinger author="[[Joey]]"]]
+[[!tag type/special-purpose]]
+
+This plugin allows ikiwiki to be configured to hit a URL each time it
+updates the wiki. One way to use this is in conjunction with the [[pingee]]
+plugin to set up a loosely coupled mirror network, or a branched version of
+a wiki. By pinging the mirror or branch each time the main wiki changes, it
+can be kept up-to-date.
+
+To configure what URLs to ping, use the [[ikiwiki/directive/ping]]
+[[ikiwiki/directive]].
+
+The [[!cpan LWP]] perl module is used for pinging. Or the [[!cpan
+LWPx::ParanoidAgent]] perl module is used if available, for added security.
+Finally, the [[!cpan Crypt::SSLeay]] perl module is needed to support pinging
+"https" urls.
+
+By default the pinger will try to ping a site for 15 seconds before timing
+out. This timeout can be changed by setting the `pinger_timeout`
+configuration setting in the setup file.
diff -Nru ikiwiki-3.20141016.4/.pc/CVE-2019-9187-4.patch/doc/security.mdwn ikiwiki-3.20141016.4+deb8u1/.pc/CVE-2019-9187-4.patch/doc/security.mdwn
--- ikiwiki-3.20141016.4/.pc/CVE-2019-9187-4.patch/doc/security.mdwn 1970-01-01 10:00:00.000000000 +1000
+++ ikiwiki-3.20141016.4+deb8u1/.pc/CVE-2019-9187-4.patch/doc/security.mdwn 2019-03-07 17:34:53.000000000 +1100
@@ -0,0 +1,528 @@
+Let's do an ikiwiki security analysis.
+
+If you are using ikiwiki to render pages that only you can edit, do not
+generate any wrappers, and do not use the cgi, then there are no more
+security issues with this program than with cat(1). If, however, you let
+others edit pages in your wiki, then some possible security issues do need
+to be kept in mind.
+
+[[!toc levels=2]]
+
+----
+
+# Probable holes
+
+_(The list of things to fix.)_
+
+## commit spoofing
+
+Anyone with direct commit access can forge "web commit from foo" and
+make it appear on [[RecentChanges]] like foo committed. One way to avoid
+this would be to limit web commits to those done by a certain user.
+
+## other stuff to look at
+
+I have been meaning to see if any CRLF injection type things can be
+done in the CGI code.
+
+----
+
+# Potential gotchas
+
+_(Things not to do.)_
+
+## image file etc attacks
+
+If it enounters a file type it does not understand, ikiwiki just copies it
+into place. So if you let users add any kind of file they like, they can
+upload images, movies, windows executables, css files, etc (though not html
+files). If these files exploit security holes in the browser of someone
+who's viewing the wiki, that can be a security problem.
+
+Of course nobody else seems to worry about this in other wikis, so should we?
+
+People with direct commit access can upload such files
+(and if you wanted to you could block that with a pre-commit hook).
+
+The attachments plugin is not enabled by default. If you choose to
+enable it, you should make use of its powerful abilities to filter allowed
+types of attachments, and only let trusted users upload.
+
+It is possible to embed an image in a page edited over the web, by using
+`img src="data:image/png;"`. Ikiwiki's htmlscrubber only allows `data:`
+urls to be used for `image/*` mime types. It's possible that some broken
+browser might ignore the mime type and if the data provided is not an
+image, instead run it as javascript, or something evil like that. Hopefully
+not many browsers are that broken.
+
+## multiple accessors of wiki directory
+
+If multiple people can directly write to the source directory ikiwiki is
+using, or to the destination directory it writes files to, then one can
+cause trouble for the other when they run ikiwiki through symlink attacks.
+
+So it's best if only one person can ever directly write to those directories.
+
+## setup files
+
+Setup files are not safe to keep in the same revision control repository
+with the rest of the wiki. Just don't do it.
+
+## page locking can be bypassed via direct commits
+
+A locked page can only be edited on the web by an admin, but anyone who is
+allowed to commit directly to the repository can bypass this. This is by
+design, although a pre-commit hook could be used to prevent editing of
+locked pages, if you really need to.
+
+## web server attacks
+
+If your web server does any parsing of special sorts of files (for example,
+server parsed html files), then if you let anyone else add files to the wiki,
+they can try to use this to exploit your web server.
+
+----
+
+# Hopefully non-holes
+
+_(AKA, the assumptions that will be the root of most security holes...)_
+
+## exploiting ikiwiki with bad content
+
+Someone could add bad content to the wiki and hope to exploit ikiwiki.
+Note that ikiwiki runs with perl taint checks on, so this is unlikely.
+
+One fun thing in ikiwiki is its handling of a PageSpec, which involves
+translating it into perl and running the perl. Of course, this is done
+*very* carefully to guard against injecting arbitrary perl code.
+
+## publishing cgi scripts
+
+ikiwiki does not allow cgi scripts to be published as part of the wiki. Or
+rather, the script is published, but it's not marked executable (except in
+the case of "destination directory file replacement" below), so hopefully
+your web server will not run it.
+
+## suid wrappers
+
+`ikiwiki --wrapper` is intended to generate a wrapper program that
+runs ikiwiki to update a given wiki. The wrapper can in turn be made suid,
+for example to be used in a [[post-commit]] hook by people who cannot write
+to the html pages, etc.
+
+If the wrapper program is made suid, then any bugs in this wrapper would be
+security holes. The wrapper is written as securely as I know how, is based
+on code that has a history of security use long before ikiwiki, and there's
+been no problem yet.
+
+## shell exploits
+
+ikiwiki does not expose untrusted data to the shell. In fact it doesn't use
+`system(3)` at all, and the only use of backticks is on data supplied by the
+wiki admin and untainted filenames.
+
+Ikiwiki was developed and used for a long time with perl's taint checking
+turned on as a second layer of defense against shell and other exploits. Due
+to a strange [bug](http://bugs.debian.org/411786) in perl, taint checking
+is currently disabled for production builds of ikiwiki.
+
+## cgi data security
+
+When ikiwiki runs as a cgi to edit a page, it is passed the name of the
+page to edit. It has to make sure to sanitise this page, to prevent eg,
+editing of ../../../foo, or editing of files that are not part of the wiki,
+such as subversion dotfiles. This is done by sanitising the filename
+removing unallowed characters, then making sure it doesn't start with "/"
+or contain ".." or "/.svn/", etc. Annoyingly ad-hoc, this kind of code is
+where security holes breed. It needs a test suite at the very least.
+
+## CGI::Session security
+
+I've audited this module and it is massively insecure by default. ikiwiki
+uses it in one of the few secure ways; by forcing it to write to a
+directory it controls (and not /tmp) and by setting a umask that makes the
+file not be world readable.
+
+## cgi password security
+
+Login to the wiki using [[plugins/passwordauth]] involves sending a password
+in cleartext over the net. Cracking the password only allows editing the wiki
+as that user though. If you care, you can use https, I suppose. If you do use
+https either for all of the wiki, or just the cgi access, then consider using
+the sslcookie option. Using [[plugins/openid]] is a potentially better option.
+
+## XSS holes in CGI output
+
+ikiwiki has been audited to ensure that all cgi script input/output
+is sanitised to prevent XSS attacks. For example, a user can't register
+with a username containing html code (anymore).
+
+It's difficult to know for sure if all such avenues have really been
+closed though.
+
+## HTML::Template security
+
+If the [[plugins/template]] plugin is enabled, all users can modify templates
+like any other part of the wiki. Some trusted users can modify templates
+without it too. This assumes that HTML::Template is secure
+when used with untrusted/malicious templates. (Note that includes are not
+allowed.)
+
+----
+
+# Plugins
+
+The security of [[plugins]] depends on how well they're written and what
+external tools they use. The plugins included in ikiwiki are all held to
+the same standards as the rest of ikiwiki, but with that said, here are
+some security notes for them.
+
+* The [[plugins/img]] plugin assumes that imagemagick/perlmagick are secure
+ from malformed image attacks for at least the formats listed in
+ `img_allowed_formats`. Imagemagick has had security holes in the
+ past. To be able to exploit such a hole, a user would need to be able to
+ upload images to the wiki.
+
+----
+
+# Fixed holes
+
+_(Unless otherwise noted, these were discovered and immediately fixed by the
+ikiwiki developers.)_
+
+## destination directory file replacement
+
+Any file in the destination directory that is a valid page filename can be
+replaced, even if it was not originally rendered from a page. For example,
+ikiwiki.cgi could be edited in the wiki, and it would write out a
+replacement. File permission is preseved. Yipes!
+
+This was fixed by making ikiwiki check if the file it's writing to exists;
+if it does then it has to be a file that it's aware of creating before, or
+it will refuse to create it.
+
+Still, this sort of attack is something to keep in mind.
+
+## symlink attacks
+
+Could a committer trick ikiwiki into following a symlink and operating on
+some other tree that it shouldn't? svn supports symlinks, so one can get
+into the repo. ikiwiki uses File::Find to traverse the repo, and does not
+tell it to follow symlinks, but it might be possible to race replacing a
+directory with a symlink and trick it into following the link.
+
+Also, if someone checks in a symlink to /etc/passwd, ikiwiki would read and
+publish that, which could be used to expose files a committer otherwise
+wouldn't see.
+
+To avoid this, ikiwiki will skip over symlinks when scanning for pages, and
+uses locking to prevent more than one instance running at a time. The lock
+prevents one ikiwiki from running a svn up/git pull/etc at the wrong time
+to race another ikiwiki. So only attackers who can write to the working
+copy on their own can race it.
+
+## symlink + cgi attacks
+
+Similarly, a commit of a symlink could be made, ikiwiki ignores it
+because of the above, but the symlink is still there, and then you edit the
+page from the web, which follows the symlink when reading the page
+(exposing the content), and again when saving the changed page (changing
+the content).
+
+This was fixed for page saving by making ikiwiki refuse to write to files
+that are symlinks, or that are in subdirectories that are symlinks,
+combined with the above locking.
+
+For page editing, it's fixed by ikiwiki checking to make sure that it
+already has found a page by scanning the tree, before loading it for
+editing, which as described above, also is done in a way that avoids
+symlink attacks.
+
+## underlaydir override attacks
+
+ikiwiki also scans an underlaydir for pages, this is used to provide stock
+pages to all wikis w/o needing to copy them into the wiki. Since ikiwiki
+internally stores only the base filename from the underlaydir or srcdir,
+and searches for a file in either directory when reading a page source,
+there is the potential for ikiwiki's scanner to reject a file from the
+srcdir for some reason (such as it being contained in a directory that is
+symlinked in), find a valid copy of the file in the underlaydir, and then
+when loading the file, mistakenly load the bad file from the srcdir.
+
+This attack is avoided by making ikiwiki refuse to add any files from the
+underlaydir if a file also exists in the srcdir with the same name.
+
+## multiple page source issues
+
+Note that I previously worried that underlay override attacks could also be
+accomplished if ikiwiki were extended to support other page markup
+languages besides markdown. However, a closer look indicates that this is
+not a problem: ikiwiki does preserve the file extension when storing the
+source filename of a page, so a file with another extension that renders to
+the same page name can't bypass the check. Ie, ikiwiki won't skip foo.rst
+in the srcdir, find foo.mdwn in the underlay, decide to render page foo and
+then read the bad foo.mdwn. Instead it will remember the .rst extension and
+only render a file with that extension.
+
+## XSS attacks in page content
+
+ikiwiki supports protecting users from their own broken browsers via the
+[[plugins/htmlscrubber]] plugin, which is enabled by default.
+
+## svn commit logs
+
+It's was possible to force a whole series of svn commits to appear to
+have come just before yours, by forging svn log output. This was
+guarded against by using svn log --xml.
+
+ikiwiki escapes any html in svn commit logs to prevent other mischief.
+
+## XML::Parser
+
+XML::Parser is used by the aggregation plugin, and has some security holes.
+Bug #[378411](http://bugs.debian.org/378411) does not
+seem to affect our use, since the data is not encoded as utf-8 at that
+point. #[378412](http://bugs.debian.org/378412) could affect us, although it
+doesn't seem very exploitable. It has a simple fix, and has been fixed in
+Debian unstable.
+
+## include loops
+
+Various directives that cause one page to be included into another could
+be exploited to DOS the wiki, by causing a loop. Ikiwiki has always guarded
+against this one way or another; the current solution should detect all
+types of loops involving preprocessor directives.
+
+## Online editing of existing css and images
+
+A bug in ikiwiki allowed the web-based editor to edit any file that was in
+the wiki, not just files that are page sources. So an attacker (or a
+genuinely helpful user, which is how the hole came to light) could edit
+files like style.css. It is also theoretically possible that an attacker
+could have used this hole to edit images or other files in the wiki, with
+some difficulty, since all editing would happen in a textarea.
+
+This hole was discovered on 10 Feb 2007 and fixed the same day with the
+release of ikiwiki 1.42. A fix was also backported to Debian etch, as
+version 1.33.1. I recommend upgrading to one of these versions if your wiki
+allows web editing.
+
+## html insertion via title
+
+Missing html escaping of the title contents allowed a web-based editor to
+insert arbitrary html inside the title tag of a page. Since that part of
+the page is not processed by the htmlscrubber, evil html could be injected.
+
+This hole was discovered on 21 March 2007 and fixed the same day (er, hour)
+with the release of ikiwiki 1.46. A fix was also backported to Debian etch,
+as version 1.33.2. I recommend upgrading to one of these versions if your
+wiki allows web editing or aggregates feeds.
+
+## javascript insertion via meta tags
+
+It was possible to use the meta plugin's meta tags to insert arbitrary
+url contents, which could be used to insert stylesheet information
+containing javascript. This was fixed by sanitising meta tags.
+
+This hole was discovered on 21 March 2007 and fixed the same day
+with the release of ikiwiki 1.47. A fix was also backported to Debian etch,
+as version 1.33.3. I recommend upgrading to one of these versions if your
+wiki can be edited by third parties.
+
+## insufficient checking for symlinks in srcdir path
+
+Ikiwiki did not check if path to the srcdir to contained a symlink. If an
+attacker had commit access to the directories in the path, they could
+change it to a symlink, causing ikiwiki to read and publish files that were
+not intended to be published. (But not write to them due to other checks.)
+
+In most configurations, this is not exploitable, because the srcdir is
+checked out of revision control, but the directories leading up to it are
+not. Or, the srcdir is a single subdirectory of a project in revision
+control (ie, `ikiwiki/doc`), and if the subdirectory were a symlink,
+ikiwiki would still typically not follow it.
+
+There are at least two configurations where this is exploitable:
+
+* If the srcdir is a deeper subdirectory of a project. For example if it is
+ `project/foo/doc`, an an attacker can replace `foo` with a symlink to a
+ directory containing a `doc` directory (not a symlink), then ikiwiki
+ would follow the symlink.
+* If the path to the srcdir in ikiwiki's configuration ended in "/",
+ and the srcdir is a single subdirectory of a project, (ie,
+ `ikiwiki/doc/`), the srcdir could be a symlink and ikiwiki would not
+ notice.
+
+This security hole was discovered on 26 November 2007 and fixed the same
+day with the release of ikiwiki 2.14. I recommend upgrading to this version
+if your wiki can be committed to by third parties. Alternatively, don't use
+a trailing slash in the srcdir, and avoid the (unusual) configurations that
+allow the security hole to be exploited.
+
+## javascript insertion via uris
+
+The htmlscrubber did not block javascript in uris. This was fixed by adding
+a whitelist of valid uri types, which does not include javascript.
+([[!cve CVE-2008-0809]]) Some urls specifyable by the meta plugin could also
+theoretically have been used to inject javascript; this was also blocked
+([[!cve CVE-2008-0808]]).
+
+This hole was discovered on 10 February 2008 and fixed the same day
+with the release of ikiwiki 2.31.1. (And a few subsequent versions..)
+A fix was also backported to Debian etch, as version 1.33.4. I recommend
+upgrading to one of these versions if your wiki can be edited by third
+parties.
+
+## Cross Site Request Forging
+
+Cross Site Request Forging could be used to constuct a link that would
+change a logged-in user's password or other preferences if they clicked on
+the link. It could also be used to construct a link that would cause a wiki
+page to be modified by a logged-in user. ([[!cve CVE-2008-0165]])
+
+These holes were discovered on 10 April 2008 and fixed the same day with
+the release of ikiwiki 2.42. A fix was also backported to Debian etch, as
+version 1.33.5. I recommend upgrading to one of these versions.
+
+## Cleartext passwords
+
+Until version 2.48, ikiwiki stored passwords in cleartext in the `userdb`.
+That risks exposing all users' passwords if the file is somehow exposed. To
+pre-emtively guard against that, current versions of ikiwiki store password
+hashes (using Eksblowfish).
+
+If you use the [[plugins/passwordauth]] plugin, I recommend upgrading to
+ikiwiki 2.48, installing the [[!cpan Authen::Passphrase]] perl module, and running
+`ikiwiki-transition hashpassword` to replace all existing cleartext passwords
+with strong blowfish hashes.
+
+You might also consider changing to [[plugins/openid]], which does not
+require ikiwiki deal with passwords at all, and does not involve users sending
+passwords in cleartext over the net to log in, either.
+
+## Empty password security hole
+
+This hole allowed ikiwiki to accept logins using empty passwords, to openid
+accounts that didn't use a password. It was introduced in version 1.34, and
+fixed in version 2.48. The [bug](http://bugs.debian.org/483770) was
+discovered on 30 May 2008 and fixed the same day. ([[!cve CVE-2008-0169]])
+
+I recommend upgrading to 2.48 immediatly if your wiki allows both password
+and openid logins.
+
+## Malformed UTF-8 DOS
+
+Feeding ikiwiki page sources containing certian forms of malformed UTF-8
+can cause it to crash. This can potentially be used for a denial of service
+attack.
+
+intrigeri discovered this problem on 12 Nov 2008 and a patch put in place
+later that day, in version 2.70. The fix was backported to testing as version
+2.53.3, and to stable as version 1.33.7.
+
+## Insufficient blacklisting in teximg plugin
+
+Josh Triplett discovered on 28 Aug 2009 that the teximg plugin's
+blacklisting of insecure TeX commands was insufficient; it could be
+bypassed and used to read arbitrary files. This was fixed by
+enabling TeX configuration options that disallow unsafe TeX commands.
+The fix was released on 30 Aug 2009 in version 3.1415926, and was
+backported to stable in version 2.53.4. If you use the teximg plugin,
+I recommend upgrading. ([[!cve CVE-2009-2944]])
+
+## javascript insertion via svg uris
+
+Ivan Shmakov pointed out that the htmlscrubber allowed `data:image/*` urls,
+including `data:image/svg+xml`. But svg can contain javascript, so that is
+unsafe.
+
+This hole was discovered on 12 March 2010 and fixed the same day
+with the release of ikiwiki 3.20100312.
+A fix was also backported to Debian etch, as version 2.53.5. I recommend
+upgrading to one of these versions if your wiki can be edited by third
+parties.
+
+## javascript insertion via insufficient htmlscrubbing of comments
+
+Kevin Riggle noticed that it was not possible to configure
+`htmlscrubber_skip` to scrub comments while leaving unscubbed the text
+of eg, blog posts. Confusingly, setting it to "* and !comment(*)" did not
+scrub comments.
+
+Additionally, it was discovered that comments' html was never scrubbed during
+preview or moderation of comments with such a configuration.
+
+These problems were discovered on 12 November 2010 and fixed the same
+hour with the release of ikiwiki 3.20101112. ([[!cve CVE-2010-1673]])
+
+## javascript insertion via insufficient checking in comments
+
+Dave B noticed that attempting to comment on an illegal page name could be
+used for an XSS attack.
+
+This hole was discovered on 22 Jan 2011 and fixed the same day with
+the release of ikiwiki 3.20110122. A fix was backported to Debian squeeze,
+as version 3.20100815.5. An upgrade is recommended for sites
+with the comments plugin enabled. ([[!cve CVE-2011-0428]])
+
+## possible javascript insertion via insufficient htmlscrubbing of alternate stylesheets
+
+Giuseppe Bilotta noticed that 'meta stylesheet` directives allowed anyone
+who could upload a malicious stylesheet to a site to add it to a
+page as an alternate stylesheet, or replacing the default stylesheet.
+
+This hole was discovered on 28 Mar 2011 and fixed the same hour with
+the release of ikiwiki 3.20110328. A fix was backported to Debian squeeze,
+as version 3.20100815.6. An upgrade is recommended for sites that have
+untrusted committers, or have the attachments plugin enabled.
+([[!cve CVE-2011-1401]])
+
+## tty hijacking via ikiwiki-mass-rebuild
+
+Ludwig Nussel discovered a way for users to hijack root's tty when
+ikiwiki-mass-rebuild was run. Additionally, there was some potential
+for information disclosure via symlinks. ([[!cve CVE-2011-1408]])
+
+This hole was discovered on 8 June 2011 and fixed the same day with
+the release of ikiwiki 3.20110608. Note that the fix is dependant on
+a version of su that has a similar hole fixed. Version 4.1.5 of the shadow
+package contains the fixed su; [[!debbug 628843]] tracks fixing the hole in
+Debian. An upgrade is a must for any sites that have `ikiwiki-update-wikilist`
+installed suid (not the default), and whose admins run `ikiwiki-mass-rebuild`.
+
+## javascript insertion via meta tags
+
+Raúl Benencia discovered an additional XSS exposure in the meta plugin.
+([[!cve CVE-2012-0220]])
+
+This hole was discovered on 16 May 2012 and fixed the same day with
+the release of ikiwiki 3.20120516. A fix was backported to Debian squeeze,
+as version 3.20100815.9. An upgrade is recommended for all sites.
+
+## XSS via openid selector
+
+Raghav Bisht discovered this XSS in the openid selector. ([[!cve CVE-2015-2793]])
+
+The hole was reported on March 24th, a fix was developed on March 27th,
+and the fixed version 3.20150329 was released on the 29th. A fix was backported
+to Debian jessie as version 3.20141016.2 and to Debian wheezy as version
+3.20120629.2. An upgrade is recommended for sites using CGI and openid.
+
+## XSS via error messages
+
+CGI error messages did not escape HTML meta-characters, potentially
+allowing an attacker to carry out cross-site scripting by directing a
+user to a URL that would result in a crafted ikiwiki error message. This
+was discovered on 4 May by the ikiwiki developers, and the fixed version
+3.20160506 was released on 6 May. An upgrade is recommended for sites using
+the CGI.
+
+## ImageMagick CVE-2016–3714 ("ImageTragick")
+
+ikiwiki 3.20160506 attempts to mitigate [[!cve CVE-2016-3714]] and any
+future ImageMagick vulnerabilities that resemble it, by restricting the
+image formats that the [[ikiwiki/directive/img]] directive is willing to
+resize. An upgrade is recommended for sites where an untrusted user is
+able to attach images. Upgrading ImageMagick to a version where
+CVE-2016-3714 has been fixed is also recommended, but at the time of
+writing no such version is available.
diff -Nru ikiwiki-3.20141016.4/.pc/.quilt_patches ikiwiki-3.20141016.4+deb8u1/.pc/.quilt_patches
--- ikiwiki-3.20141016.4/.pc/.quilt_patches 1970-01-01 10:00:00.000000000 +1000
+++ ikiwiki-3.20141016.4+deb8u1/.pc/.quilt_patches 2019-03-07 17:31:01.000000000 +1100
@@ -0,0 +1 @@
+debian/patches
diff -Nru ikiwiki-3.20141016.4/.pc/.quilt_series ikiwiki-3.20141016.4+deb8u1/.pc/.quilt_series
--- ikiwiki-3.20141016.4/.pc/.quilt_series 1970-01-01 10:00:00.000000000 +1000
+++ ikiwiki-3.20141016.4+deb8u1/.pc/.quilt_series 2019-03-07 17:31:01.000000000 +1100
@@ -0,0 +1 @@
+series
diff -Nru ikiwiki-3.20141016.4/.pc/.version ikiwiki-3.20141016.4+deb8u1/.pc/.version
--- ikiwiki-3.20141016.4/.pc/.version 1970-01-01 10:00:00.000000000 +1000
+++ ikiwiki-3.20141016.4+deb8u1/.pc/.version 2019-03-07 17:31:01.000000000 +1100
@@ -0,0 +1 @@
+2
diff -Nru ikiwiki-3.20141016.4/t/aggregate-file.t ikiwiki-3.20141016.4+deb8u1/t/aggregate-file.t
--- ikiwiki-3.20141016.4/t/aggregate-file.t 1970-01-01 10:00:00.000000000 +1000
+++ ikiwiki-3.20141016.4+deb8u1/t/aggregate-file.t 2019-03-07 17:32:38.000000000 +1100
@@ -0,0 +1,173 @@
+#!/usr/bin/perl
+use utf8;
+use warnings;
+use strict;
+
+use Encode;
+use Test::More;
+
+BEGIN {
+ plan(skip_all => "CGI not available")
+ unless eval q{
+ use CGI qw();
+ 1;
+ };
+
+ plan(skip_all => "IPC::Run not available")
+ unless eval q{
+ use IPC::Run qw(run);
+ 1;
+ };
+
+ use_ok('IkiWiki');
+ use_ok('YAML::XS');
+}
+
+# We check for English error messages
+$ENV{LC_ALL} = 'C';
+
+use Cwd qw(getcwd);
+use Errno qw(ENOENT);
+
+my $installed = $ENV{INSTALLED_TESTS};
+
+my @command;
+if ($installed) {
+ @command = qw(ikiwiki --plugin inline);
+}
+else {
+ ok(! system("make -s ikiwiki.out"));
+ @command = ("perl", "-I".getcwd."/blib/lib", './ikiwiki.out',
+ '--underlaydir='.getcwd.'/underlays/basewiki',
+ '--set', 'underlaydirbase='.getcwd.'/underlays',
+ '--templatedir='.getcwd.'/templates');
+}
+
+sub write_old_file {
+ my $name = shift;
+ my $dir = shift;
+ my $content = shift;
+ writefile($name, $dir, $content);
+ ok(utime(333333333, 333333333, "$dir/$name"));
+}
+
+sub write_setup_file {
+ my %params = @_;
+ my %setup = (
+ wikiname => 'this is the name of my wiki',
+ srcdir => getcwd.'/t/tmp/in',
+ destdir => getcwd.'/t/tmp/out',
+ url => 'http://example.com',
+ cgiurl => 'http://example.com/cgi-bin/ikiwiki.cgi',
+ cgi_wrapper => getcwd.'/t/tmp/ikiwiki.cgi',
+ cgi_wrappermode => '0751',
+ add_plugins => [qw(aggregate)],
+ disable_plugins => [qw(emailauth openid passwordauth)],
+ aggregate_webtrigger => 1,
+ );
+ if ($params{without_paranoia}) {
+ $setup{libdirs} = [getcwd.'/t/noparanoia'];
+ }
+ unless ($installed) {
+ $setup{ENV} = { 'PERL5LIB' => getcwd.'/blib/lib' };
+ }
+ writefile("test.setup", "t/tmp",
+ "# IkiWiki::Setup::Yaml - YAML formatted setup file\n" .
+ Dump(\%setup));
+}
+
+sub thoroughly_rebuild {
+ ok(unlink("t/tmp/ikiwiki.cgi") || $!{ENOENT});
+ ok(! system(@command, qw(--setup t/tmp/test.setup --rebuild --wrappers)));
+}
+
+sub run_cgi {
+ my (%args) = @_;
+ my ($in, $out);
+ my $method = $args{method} || 'GET';
+ my $environ = $args{environ} || {};
+ my $params = $args{params} || { do => 'prefs' };
+
+ my %defaults = (
+ SCRIPT_NAME => '/cgi-bin/ikiwiki.cgi',
+ HTTP_HOST => 'example.com',
+ );
+
+ my $cgi = CGI->new($args{params});
+ my $query_string = $cgi->query_string();
+ diag $query_string;
+
+ if ($method eq 'POST') {
+ $defaults{REQUEST_METHOD} = 'POST';
+ $in = $query_string;
+ $defaults{CONTENT_LENGTH} = length $in;
+ } else {
+ $defaults{REQUEST_METHOD} = 'GET';
+ $defaults{QUERY_STRING} = $query_string;
+ }
+
+ my %envvars = (
+ %defaults,
+ %$environ,
+ );
+ run(["./t/tmp/ikiwiki.cgi"], \$in, \$out, init => sub {
+ map {
+ $ENV{$_} = $envvars{$_}
+ } keys(%envvars);
+ });
+
+ return decode_utf8($out);
+}
+
+sub test {
+ my $content;
+
+ ok(! system(qw(rm -rf t/tmp)));
+ ok(! system(qw(mkdir t/tmp)));
+
+ write_old_file('aggregator.mdwn', 't/tmp/in',
+ '[[!aggregate name="ssrf" url="file://'.getcwd.'/t/secret.rss"]]'
+ .'[[!inline pages="internal(aggregator/*)"]]');
+
+ write_setup_file();
+ thoroughly_rebuild();
+
+ $content = run_cgi(
+ method => 'GET',
+ params => {
+ do => 'aggregate_webtrigger',
+ },
+ );
+ unlike($content, qr{creating new page});
+ unlike($content, qr{Secrets});
+ ok(! -e 't/tmp/in/.ikiwiki/transient/aggregator/ssrf');
+ ok(! -e 't/tmp/in/.ikiwiki/transient/aggregator/ssrf/Secrets_go_here._aggregated');
+
+ thoroughly_rebuild();
+ $content = readfile('t/tmp/out/aggregator/index.html');
+ unlike($content, qr{Secrets});
+
+ diag('Trying test again with LWPx::ParanoidAgent disabled');
+
+ write_setup_file(without_paranoia => 1);
+ thoroughly_rebuild();
+
+ $content = run_cgi(
+ method => 'GET',
+ params => {
+ do => 'aggregate_webtrigger',
+ },
+ );
+ unlike($content, qr{creating new page});
+ unlike($content, qr{Secrets});
+ ok(! -e 't/tmp/in/.ikiwiki/transient/aggregator/ssrf');
+ ok(! -e 't/tmp/in/.ikiwiki/transient/aggregator/ssrf/Secrets_go_here._aggregated');
+
+ thoroughly_rebuild();
+ $content = readfile('t/tmp/out/aggregator/index.html');
+ unlike($content, qr{Secrets});
+}
+
+test();
+
+done_testing();
diff -Nru ikiwiki-3.20141016.4/t/noparanoia/LWPx/ParanoidAgent.pm ikiwiki-3.20141016.4+deb8u1/t/noparanoia/LWPx/ParanoidAgent.pm
--- ikiwiki-3.20141016.4/t/noparanoia/LWPx/ParanoidAgent.pm 1970-01-01 10:00:00.000000000 +1000
+++ ikiwiki-3.20141016.4+deb8u1/t/noparanoia/LWPx/ParanoidAgent.pm 2019-03-07 17:32:38.000000000 +1100
@@ -0,0 +1,2 @@
+# make import fail
+0;
diff -Nru ikiwiki-3.20141016.4/t/secret.rss ikiwiki-3.20141016.4+deb8u1/t/secret.rss
--- ikiwiki-3.20141016.4/t/secret.rss 1970-01-01 10:00:00.000000000 +1000
+++ ikiwiki-3.20141016.4+deb8u1/t/secret.rss 2019-03-07 17:32:38.000000000 +1100
@@ -0,0 +1,11 @@
+<?xml version="1.0"?>
+<rss version="2.0">
+<channel>
+<title>Secrets go here</title>
+<description>Secrets go here</description>
+<item>
+ <title>Secrets go here</title>
+ <description>Secrets go here</description>
+</item>
+</channel>
+</rss>
diff -Nru ikiwiki-3.20141016.4/t/useragent.t ikiwiki-3.20141016.4+deb8u1/t/useragent.t
--- ikiwiki-3.20141016.4/t/useragent.t 1970-01-01 10:00:00.000000000 +1000
+++ ikiwiki-3.20141016.4+deb8u1/t/useragent.t 2019-03-07 17:32:54.000000000 +1100
@@ -0,0 +1,317 @@
+#!/usr/bin/perl
+use warnings;
+use strict;
+use Test::More;
+
+my $have_paranoid_agent;
+BEGIN {
+ plan(skip_all => 'LWP not available')
+ unless eval q{
+ use LWP qw(); 1;
+ };
+ use_ok("IkiWiki");
+ $have_paranoid_agent = eval q{
+ use LWPx::ParanoidAgent qw(); 1;
+ };
+}
+
+eval { useragent(future_feature => 1); };
+ok($@, 'future features should cause useragent to fail');
+
+diag "==== No proxy ====";
+delete $ENV{http_proxy};
+delete $ENV{https_proxy};
+delete $ENV{no_proxy};
+delete $ENV{HTTPS_PROXY};
+delete $ENV{NO_PROXY};
+
+diag "---- Unspecified URL ----";
+my $ua = useragent(for_url => undef);
+SKIP: {
+ skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
+ ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
+}
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
+is($ua->proxy('http'), undef, 'No http proxy');
+is($ua->proxy('https'), undef, 'No https proxy');
+
+diag "---- Specified URL ----";
+$ua = useragent(for_url => 'http://example.com');
+SKIP: {
+ skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
+ ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
+}
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
+is($ua->proxy('http'), undef, 'No http proxy');
+is($ua->proxy('https'), undef, 'No https proxy');
+
+diag "==== Proxy for everything ====";
+$ENV{http_proxy} = 'http://proxy:8080';
+$ENV{https_proxy} = 'http://sproxy:8080';
+delete $ENV{no_proxy};
+delete $ENV{HTTPS_PROXY};
+delete $ENV{NO_PROXY};
+
+diag "---- Unspecified URL ----";
+$ua = useragent(for_url => undef);
+ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
+is($ua->proxy('http'), 'http://proxy:8080', 'should use proxy');
+is($ua->proxy('https'), 'http://sproxy:8080', 'should use CONNECT proxy');
+$ua = useragent(for_url => 'http://example.com');
+ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http)]);
+is($ua->proxy('http'), 'http://proxy:8080', 'should use proxy');
+# We don't care what $ua->proxy('https') is, because it won't be used
+$ua = useragent(for_url => 'https://example.com');
+ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(https)]);
+# We don't care what $ua->proxy('http') is, because it won't be used
+is($ua->proxy('https'), 'http://sproxy:8080', 'should use CONNECT proxy');
+
+diag "==== Selective proxy ====";
+$ENV{http_proxy} = 'http://proxy:8080';
+$ENV{https_proxy} = 'http://sproxy:8080';
+$ENV{no_proxy} = '*.example.net,example.com,.example.org';
+delete $ENV{HTTPS_PROXY};
+delete $ENV{NO_PROXY};
+
+diag "---- Unspecified URL ----";
+$ua = useragent(for_url => undef);
+ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
+is($ua->proxy('http'), 'http://proxy:8080', 'should use proxy');
+is($ua->proxy('https'), 'http://sproxy:8080', 'should use CONNECT proxy');
+
+diag "---- Exact match for no_proxy ----";
+$ua = useragent(for_url => 'http://example.com');
+SKIP: {
+ skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
+ ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
+}
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
+is($ua->proxy('http'), undef);
+is($ua->proxy('https'), undef);
+
+diag "---- Subdomain of exact domain in no_proxy ----";
+$ua = useragent(for_url => 'http://sub.example.com');
+ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http)]);
+is($ua->proxy('http'), 'http://proxy:8080', 'should use proxy');
+
+diag "---- example.net matches *.example.net ----";
+$ua = useragent(for_url => 'https://example.net');
+SKIP: {
+ skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
+ ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
+}
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
+is($ua->proxy('http'), undef);
+is($ua->proxy('https'), undef);
+
+diag "---- sub.example.net matches *.example.net ----";
+$ua = useragent(for_url => 'https://sub.example.net');
+SKIP: {
+ skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
+ ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
+}
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
+is($ua->proxy('http'), undef);
+is($ua->proxy('https'), undef);
+
+diag "---- badexample.net does not match *.example.net ----";
+$ua = useragent(for_url => 'https://badexample.net');
+ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(https)]);
+is($ua->proxy('https'), 'http://sproxy:8080', 'should use proxy');
+
+diag "---- example.org matches .example.org ----";
+$ua = useragent(for_url => 'https://example.org');
+SKIP: {
+ skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
+ ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
+}
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
+is($ua->proxy('http'), undef);
+is($ua->proxy('https'), undef);
+
+diag "---- sub.example.org matches .example.org ----";
+$ua = useragent(for_url => 'https://sub.example.org');
+SKIP: {
+ skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
+ ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
+}
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
+is($ua->proxy('http'), undef);
+is($ua->proxy('https'), undef);
+
+diag "---- badexample.org does not match .example.org ----";
+$ua = useragent(for_url => 'https://badexample.org');
+ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(https)]);
+is($ua->proxy('https'), 'http://sproxy:8080', 'should use proxy');
+
+diag "==== Selective proxy (alternate variables) ====";
+$ENV{http_proxy} = 'http://proxy:8080';
+delete $ENV{https_proxy};
+$ENV{HTTPS_PROXY} = 'http://sproxy:8080';
+delete $ENV{no_proxy};
+$ENV{NO_PROXY} = '*.example.net,example.com,.example.org';
+
+diag "---- Unspecified URL ----";
+$ua = useragent(for_url => undef);
+ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
+is($ua->proxy('http'), 'http://proxy:8080', 'should use proxy');
+is($ua->proxy('https'), 'http://sproxy:8080', 'should use CONNECT proxy');
+
+diag "---- Exact match for no_proxy ----";
+$ua = useragent(for_url => 'http://example.com');
+SKIP: {
+ skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
+ ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
+}
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
+is($ua->proxy('http'), undef);
+is($ua->proxy('https'), undef);
+
+diag "---- Subdomain of exact domain in no_proxy ----";
+$ua = useragent(for_url => 'http://sub.example.com');
+ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http)]);
+is($ua->proxy('http'), 'http://proxy:8080', 'should use proxy');
+
+diag "---- example.net matches *.example.net ----";
+$ua = useragent(for_url => 'https://example.net');
+SKIP: {
+ skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
+ ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
+}
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
+is($ua->proxy('http'), undef);
+is($ua->proxy('https'), undef);
+
+diag "---- sub.example.net matches *.example.net ----";
+$ua = useragent(for_url => 'https://sub.example.net');
+SKIP: {
+ skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
+ ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
+}
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
+is($ua->proxy('http'), undef);
+is($ua->proxy('https'), undef);
+
+diag "---- badexample.net does not match *.example.net ----";
+$ua = useragent(for_url => 'https://badexample.net');
+ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(https)]);
+is($ua->proxy('https'), 'http://sproxy:8080', 'should use proxy');
+
+diag "---- example.org matches .example.org ----";
+$ua = useragent(for_url => 'https://example.org');
+SKIP: {
+ skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
+ ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
+}
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
+is($ua->proxy('http'), undef);
+is($ua->proxy('https'), undef);
+
+diag "---- sub.example.org matches .example.org ----";
+$ua = useragent(for_url => 'https://sub.example.org');
+SKIP: {
+ skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
+ ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
+}
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
+is($ua->proxy('http'), undef);
+is($ua->proxy('https'), undef);
+
+diag "---- badexample.org does not match .example.org ----";
+$ua = useragent(for_url => 'https://badexample.org');
+ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(https)]);
+is($ua->proxy('https'), 'http://sproxy:8080', 'should use proxy');
+
+diag "==== Selective proxy (many variables) ====";
+$ENV{http_proxy} = 'http://proxy:8080';
+$ENV{https_proxy} = 'http://sproxy:8080';
+# This one should be ignored in favour of https_proxy
+$ENV{HTTPS_PROXY} = 'http://not.preferred.proxy:3128';
+# These two should be merged
+$ENV{no_proxy} = '*.example.net,example.com';
+$ENV{NO_PROXY} = '.example.org';
+
+diag "---- Unspecified URL ----";
+$ua = useragent(for_url => undef);
+ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
+is($ua->proxy('http'), 'http://proxy:8080', 'should use proxy');
+is($ua->proxy('https'), 'http://sproxy:8080', 'should use CONNECT proxy');
+
+diag "---- Exact match for no_proxy ----";
+$ua = useragent(for_url => 'http://example.com');
+SKIP: {
+ skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
+ ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
+}
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
+is($ua->proxy('http'), undef);
+is($ua->proxy('https'), undef);
+
+diag "---- Subdomain of exact domain in no_proxy ----";
+$ua = useragent(for_url => 'http://sub.example.com');
+ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http)]);
+is($ua->proxy('http'), 'http://proxy:8080', 'should use proxy');
+
+diag "---- example.net matches *.example.net ----";
+$ua = useragent(for_url => 'https://example.net');
+SKIP: {
+ skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
+ ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
+}
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
+is($ua->proxy('http'), undef);
+is($ua->proxy('https'), undef);
+
+diag "---- sub.example.net matches *.example.net ----";
+$ua = useragent(for_url => 'https://sub.example.net');
+SKIP: {
+ skip 'paranoid agent not available', 1 unless $have_paranoid_agent;
+ ok($ua->isa('LWPx::ParanoidAgent'), 'uses ParanoidAgent if possible');
+}
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
+is($ua->proxy('http'), undef);
+is($ua->proxy('https'), undef);
+
+diag "---- badexample.net does not match *.example.net ----";
+$ua = useragent(for_url => 'https://badexample.net');
+ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(https)]);
+is($ua->proxy('https'), 'http://sproxy:8080', 'should use proxy');
+
+diag "==== One but not the other ====\n";
+$ENV{http_proxy} = 'http://proxy:8080';
+delete $ENV{https_proxy};
+delete $ENV{HTTPS_PROXY};
+delete $ENV{no_proxy};
+delete $ENV{NO_PROXY};
+$ua = useragent(for_url => undef);
+ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
+is($ua->proxy('http'), 'http://proxy:8080', 'should use proxy');
+is($ua->proxy('https'), 'http://proxy:8080', 'should use proxy');
+
+delete $ENV{http_proxy};
+$ENV{https_proxy} = 'http://sproxy:8080';
+delete $ENV{HTTPS_PROXY};
+delete $ENV{no_proxy};
+delete $ENV{NO_PROXY};
+$ua = useragent(for_url => undef);
+ok(! $ua->isa('LWPx::ParanoidAgent'), 'should use proxy instead of ParanoidAgent');
+is_deeply([sort @{$ua->protocols_allowed}], [sort qw(http https)]);
+is($ua->proxy('http'), 'http://sproxy:8080', 'should use proxy');
+is($ua->proxy('https'), 'http://sproxy:8080', 'should use proxy');
+
+done_testing;
Reply to: