use warnings;
use strict;
use Encode;
+use Fcntl q{:flock};
use URI::Escape q{uri_escape_utf8};
use POSIX ();
use Storable;
%pagestate %wikistate %renderedfiles %oldrenderedfiles
%pagesources %delpagesources %destsources %depends %depends_simple
@mass_depends %hooks %forcerebuild %loaded_plugins %typedlinks
- %oldtypedlinks %autofiles};
+ %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 will_render gettext ngettext urlto targetpage
+ displaytime strftime_utf8 will_render gettext ngettext urlto targetpage
add_underlay pagetitle titlepage linkpage newpagefile
- inject add_link add_autofile
+ inject add_link add_autofile useragent
%config %links %pagestate %wikistate %renderedfiles
%pagesources %destsources %typedlinks);
our $VERSION = 3.00; # plugin interface version, next is ikiwiki version
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");
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 => '',
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 => '',
type => "internal",
default => [qw{mdwn link inline meta htmlscrubber passwordauth
openid signinedit lockedit conditional
- recentchanges parentlinks editpage}],
+ recentchanges parentlinks editpage
+ templatebody}],
description => "plugins to enable by default",
safe => 0,
rebuild => 1,
html5 => {
type => "boolean",
default => 0,
- description => "generate HTML5? (experimental)",
- advanced => 1,
+ description => "use elements new in HTML5 like <section>?",
+ advanced => 0,
safe => 1,
rebuild => 1,
},
rebuild => 0,
},
umask => {
- type => "integer",
- example => "022",
- description => "force ikiwiki to use a particular umask",
+ type => "string",
+ example => "public",
+ description => "force ikiwiki to use a particular umask (keywords public, group or private, or a number)",
advanced => 1,
safe => 0, # paranoia
rebuild => 0,
safe => 0, # paranoia
rebuild => 0,
},
+ libdirs => {
+ type => "string",
+ default => [],
+ example => ["$ENV{HOME}/.local/share/ikiwiki"],
+ description => "extra library and plugin directories",
+ advanced => 1,
+ safe => 0, # directory
+ rebuild => 0,
+ },
libdir => {
type => "string",
default => "",
example => "$ENV{HOME}/.ikiwiki/",
- description => "extra library and plugin directory",
+ description => "extra library and plugin directory (searched after libdirs)",
advanced => 1,
safe => 0, # directory
rebuild => 0,
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,
},
setuptype => {
type => "internal",
- default => "Standard",
+ default => "Yaml",
description => "perl class to use to dump setup file",
safe => 0,
rebuild => 0,
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,
+ },
+ responsive_layout => {
+ type => "boolean",
+ default => 1,
+ description => "theme has a responsive layout? (mobile-optimized)",
+ safe => 1,
+ rebuild => 1,
+ },
+ deterministic => {
+ type => "boolean",
+ default => 0,
+ description => "try harder to produce deterministic output",
+ safe => 1,
+ rebuild => 1,
+ advanced => 1,
+ },
+}
+
+sub getlibdirs () {
+ my @libdirs;
+ if ($config{libdirs}) {
+ @libdirs = @{$config{libdirs}};
+ }
+ if (length $config{libdir}) {
+ push @libdirs, $config{libdir};
+ }
+ return @libdirs;
}
sub defaultconfig () {
foreach my $key (keys %s) {
push @ret, $key, $s{$key}->{default};
}
- use Data::Dumper;
return @ret;
}
$ENV{$val}=$config{ENV}{$val};
}
}
+ if (defined $config{timezone} && length $config{timezone}) {
+ $ENV{TZ}=$config{timezone};
+ }
+ elsif (defined $ENV{TZ} && length $ENV{TZ}) {
+ $config{timezone}=$ENV{TZ};
+ }
+ else {
+ eval q{use Config qw()};
+ error($@) if $@;
+
+ if ($Config::Config{d_gnulibc} && -e '/etc/localtime') {
+ $config{timezone}=$ENV{TZ}=':/etc/localtime';
+ }
+ else {
+ $config{timezone}=$ENV{TZ}='GMT';
+ }
+ }
if ($config{w3mmode}) {
eval q{use Cwd q{abs_path}};
error(gettext("Must specify url to wiki with --url when using --cgi"));
}
- if (length $config{url}) {
+ if (defined $config{url} && length $config{url}) {
eval q{use URI};
my $baseurl = URI->new($config{url});
$local_cgiurl = $cgiurl->path;
- if ($cgiurl->scheme ne $baseurl->scheme or
- $cgiurl->authority ne $baseurl->authority) {
+ 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{//$}{/};
unless exists $config{wikistatedir} && defined $config{wikistatedir};
if (defined $config{umask}) {
- umask(possibly_foolish_untaint($config{umask}));
+ my $u = possibly_foolish_untaint($config{umask});
+
+ if ($u =~ m/^\d+$/) {
+ umask($u);
+ }
+ elsif ($u eq 'private') {
+ umask(077);
+ }
+ elsif ($u eq 'group') {
+ umask(027);
+ }
+ elsif ($u eq 'public') {
+ umask(022);
+ }
+ else {
+ error(sprintf(gettext("unsupported umask setting %s"), $u));
+ }
}
run_hooks(checkconfig => sub { shift->() });
sub listplugins () {
my %ret;
- foreach my $dir (@INC, $config{libdir}) {
+ foreach my $dir (@INC, getlibdirs()) {
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") {
+ foreach my $dir (getlibdirs(), "$installdir/lib/ikiwiki") {
next unless defined $dir && length $dir;
foreach my $file (glob("$dir/plugins/*")) {
$ret{basename($file)}=1 if -x $file;
}
sub loadplugins () {
- if (defined $config{libdir} && length $config{libdir}) {
- unshift @INC, possibly_foolish_untaint($config{libdir});
+ foreach my $dir (getlibdirs()) {
+ unshift @INC, possibly_foolish_untaint($dir);
}
foreach my $plugin (@{$config{default_plugins}}, @{$config{add_plugins}}) {
return if ! $force && grep { $_ eq $plugin} @{$config{disable_plugins}};
- foreach my $dir (defined $config{libdir} ? possibly_foolish_untaint($config{libdir}) : undef,
- "$installdir/lib/ikiwiki") {
+ foreach my $possiblytainteddir (getlibdirs(), "$installdir/lib/ikiwiki") {
+ my $dir = possibly_foolish_untaint($possiblytainteddir);
if (defined $dir && -x "$dir/plugins/$plugin") {
eval { require IkiWiki::Plugin::external };
if ($@) {
}
my $log_open=0;
+my $log_failed=0;
sub log_message ($$) {
my $type=shift;
Sys::Syslog::openlog('ikiwiki', '', 'user');
$log_open=1;
}
- return eval {
- Sys::Syslog::syslog($type, "[$config{wikiname}] %s", join(" ", @_));
+ eval {
+ my $message = "[$config{wikiname}] ".join(" ", @_);
+ utf8::encode($message);
+ Sys::Syslog::syslog($type, "%s", $message);
};
+ if ($@) {
+ print STDERR "failed to syslog: $@" unless $log_failed;
+ $log_failed=1;
+ print STDERR "@_\n";
+ }
+ return $@;
}
elsif (! $config{cgi}) {
return print "@_\n";
return (srcfile_stat(@_))[0];
}
-sub add_underlay ($) {
+sub add_literal_underlay ($) {
my $dir=shift;
- if ($dir !~ /^\//) {
- $dir="$config{underlaydirbase}/$dir";
- }
-
if (! grep { $_ eq $dir } @{$config{underlaydirs}}) {
unshift @{$config{underlaydirs}}, $dir;
}
+}
+
+sub add_underlay ($) {
+ my $dir = shift;
+
+ if ($dir !~ /^\//) {
+ $dir="$config{underlaydirbase}/$dir";
+ }
+ add_literal_underlay($dir);
+ # why does it return 1? we just don't know
return 1;
}
sub isinlinableimage ($) {
my $file=shift;
- return $file =~ /\.(png|gif|jpg|jpeg)$/i;
+ return $file =~ /\.(png|gif|jpg|jpeg|svg)$/i;
}
sub pagetitle ($;$) {
}
return $cgiurl."?".
- join("&", map $_."=".uri_escape_utf8($params{$_}), keys %params);
+ join("&", map $_."=".uri_escape_utf8($params{$_}), sort(keys %params));
+}
+
+sub cgiurl_abs (@) {
+ eval q{use URI};
+ URI->new_abs(cgiurl(@_), $config{cgiurl});
+}
+
+# Same as cgiurl_abs, but when the user connected using https,
+# will be a https url even if the cgiurl is normally a http url.
+#
+# This should be used for anything involving emailing a login link,
+# because a https session cookie will not be sent over http.
+sub cgiurl_abs_samescheme (@) {
+ my $u=cgiurl_abs(@_);
+ if (($ENV{HTTPS} && lc $ENV{HTTPS} ne "off")) {
+ $u=~s/^http:/https:/i;
+ }
+ return $u
}
sub baseurl (;$) {
return $page;
}
+sub urlabs ($$) {
+ my $url=shift;
+ my $urlbase=shift;
+
+ return $url unless defined $urlbase && length $urlbase;
+
+ eval q{use URI};
+ URI->new_abs($url, $urlbase)->as_string;
+}
+
sub abs2rel ($$) {
# Work around very innefficient behavior in File::Spec if abs2rel
# is passed two relative paths. It's much faster if paths are
$format=$config{timeformat};
}
- # strftime doesn't know about encodings, so make sure
- # its output is properly treated as utf8
- return decode_utf8(POSIX::strftime($format, localtime($time)));
+ return strftime_utf8($format, localtime($time));
+}
+
+my $strftime_encoding;
+sub strftime_utf8 {
+ # strftime didn't know about encodings in older Perl, so make sure
+ # its output is properly treated as utf8.
+ # Note that this does not handle utf-8 in the format string.
+ my $result = POSIX::strftime(@_);
+
+ if (Encode::is_utf8($result)) {
+ return $result;
+ }
+
+ ($strftime_encoding) = POSIX::setlocale(&POSIX::LC_TIME) =~ m#\.([^@]+)#
+ unless defined $strftime_encoding;
+ $strftime_encoding
+ ? Encode::decode($strftime_encoding, $result)
+ : $result;
}
sub date_3339 ($) {
my $absolute=shift;
if (! length $to) {
- return beautify_urlpath(baseurl($from)."index.$config{htmlext}");
+ $to = 'index';
}
if (! $destsources{$to}) {
}
if (! defined $from) {
- my $u = $local_url;
+ my $u = $local_url || '';
$u =~ s{/$}{};
return $u.beautify_urlpath("/".$to);
}
$cgilink = "<a href=\"".
cgiurl(
do => "create",
- page => lc($link),
+ page => $link,
from => $lpage
)."\" rel=\"nofollow\">?</a>";
}
return length $config{userdir} ? "$config{userdir}/$user" : $user;
}
+# Username to display for openid accounts.
sub openiduser ($) {
my $user=shift;
return;
}
+# Username to display for emailauth accounts.
+sub emailuser ($) {
+ my $user=shift;
+ if (defined $user && $user =~ m/(.+)@/) {
+ my $nick=$1;
+ # remove any characters from not allowed in wiki files
+ # support use w/o %config set
+ my $chars = defined $config{wiki_file_chars} ? $config{wiki_file_chars} : "-[:alnum:]+/.:_";
+ $nick=~s/[^$chars]/_/g;
+ return $nick;
+ }
+ return;
+}
+
+# Some user information should not be exposed in commit metadata, etc.
+# This generates a cloaked form of such information.
+sub cloak ($) {
+ my $user=shift;
+ # cloak email address using http://xmlns.com/foaf/spec/#term_mbox_sha1sum
+ if ($user=~m/(.+)@/) {
+ my $nick=$1;
+ eval q{use Digest::SHA};
+ return $user if $@;
+ return $nick.'@'.Digest::SHA::sha1_hex("mailto:$user");
+ }
+ else {
+ return $user;
+ }
+}
+
sub htmlize ($$$$) {
my $page=shift;
my $destpage=shift;
# consider it significant.
my @params;
while ($params =~ m{
- (?:([-\w]+)=)? # 1: named parameter key?
+ (?:([-.\w]+)=)? # 1: named parameter key?
(?:
"""(.*?)""" # 2: triple-quoted value
|
"([^"]*?)" # 3: single-quoted value
|
- (\S+) # 4: unquoted value
+ '''(.*?)''' # 4: triple-single-quote
+ |
+ <<([a-zA-Z]+)\n # 5: heredoc start
+ (.*?)\n\5 # 6: heredoc value
+ |
+ (\S+) # 7: unquoted value
)
(?:\s+|$) # delimiter to next param
- }sgx) {
+ }msgx) {
my $key=$1;
my $val;
if (defined $2) {
elsif (defined $4) {
$val=$4;
}
+ elsif (defined $7) {
+ $val=$7;
+ }
+ elsif (defined $6) {
+ $val=$6;
+ }
if (defined $key) {
push @params, $key, $val;
push @params, $val, '';
}
}
- if ($preprocessing{$page}++ > 3) {
+ if ($preprocessing{$page}++ > 8) {
# Avoid loops of preprocessed pages preprocessing
# other pages that preprocess them, etc.
return "[[!$command <span class=\"error\">".
if ($@) {
my $error=$@;
chomp $error;
+ eval q{use HTML::Entities};
+ # Also encode most ASCII punctuation
+ # as entities so that error messages
+ # are not interpreted as Markdown etc.
+ $error = encode_entities($error, '^-A-Za-z0-9+_,./:;= '."'");
$ret="[[!$command <span class=\"error\">".
gettext("Error").": $error"."</span>]]";
}
( # 4: the parameters..
\s+ # Must have space if parameters present
(?:
- (?:[-\w]+=)? # named parameter key?
+ (?:[-.\w]+=)? # named parameter key?
(?:
""".*?""" # triple-quoted value
|
"[^"]*?" # single-quoted value
|
+ '''.*?''' # triple-single-quote
+ |
+ <<([a-zA-Z]+)\n # 5: heredoc start
+ (?:.*?)\n\5 # heredoc value
+ |
[^"\s\]]+ # unquoted value
)
\s* # whitespace or end
\s+
( # 4: the parameters..
(?:
- (?:[-\w]+=)? # named parameter key?
+ (?:[-.\w]+=)? # named parameter key?
(?:
""".*?""" # triple-quoted value
|
"[^"]*?" # single-quoted value
|
+ '''.*?''' # triple-single-quote
+ |
+ <<([a-zA-Z]+)\n # 5: heredoc start
+ (?:.*?)\n\5 # heredoc value
+ |
[^"\s\]]+ # unquoted value
)
\s* # whitespace or end
$file=possibly_foolish_untaint($file);
if (! defined $file || ! length $file ||
file_pruned($file)) {
- error(gettext("bad file name %s"), $file);
+ error(sprintf(gettext("bad file name %s"), $file));
}
my $type=pagetype($file);
}
open($wikilock, '>', "$config{wikistatedir}/lockfile") ||
error ("cannot write to $config{wikistatedir}/lockfile: $!");
- if (! flock($wikilock, 2)) { # LOCK_EX
- error("failed to get lock");
+ if (! flock($wikilock, LOCK_EX | LOCK_NB)) {
+ debug("failed to get lock; waiting...");
+ if (! flock($wikilock, LOCK_EX)) {
+ error("failed to get lock");
+ }
}
return 1;
}
sub loadindex () {
%oldrenderedfiles=%pagectime=();
- if (! $config{rebuild}) {
+ my $rebuild=$config{rebuild};
+ if (! $rebuild) {
%pagesources=%pagemtime=%oldlinks=%links=%depends=
%destsources=%renderedfiles=%pagecase=%pagestate=
%depends_simple=%typedlinks=%oldtypedlinks=();
open ($in, "<", "$config{wikistatedir}/indexdb") || return;
}
else {
- $config{gettime}=1; # first build
+ # gettime on first build
+ $config{gettime}=1 unless defined $config{gettime};
return;
}
}
foreach my $src (keys %$pages) {
my $d=$pages->{$src};
- my $page=pagename($src);
+ my $page;
+ if (exists $d->{page} && ! $rebuild) {
+ $page=$d->{page};
+ }
+ else {
+ $page=pagename($src);
+ }
$pagectime{$page}=$d->{ctime};
$pagesources{$page}=$src;
- if (! $config{rebuild}) {
+ if (! $rebuild) {
$pagemtime{$page}=$d->{mtime};
$renderedfiles{$page}=$d->{dest};
if (exists $d->{links} && ref $d->{links}) {
foreach my $page (keys %renderedfiles) {
$destsources{$_}=$page foreach @{$renderedfiles{$page}};
}
+ $lastrev=$index->{lastrev};
+ @underlayfiles=@{$index->{underlayfiles}} if ref $index->{underlayfiles};
return close($in);
}
my $src=$pagesources{$page};
$index{page}{$src}={
+ page => $page,
ctime => $pagectime{$page},
mtime => $pagemtime{$page},
dest => $renderedfiles{$page},
}
if (exists $pagestate{$page}) {
- foreach my $id (@plugins) {
- foreach my $key (keys %{$pagestate{$page}{$id}}) {
- $index{page}{$src}{state}{$id}{$key}=$pagestate{$page}{$id}{$key};
- }
- }
+ $index{page}{$src}{state}=$pagestate{$page};
}
}
}
}
+ $index{lastrev}=$lastrev;
+ $index{underlayfiles}=\@underlayfiles;
+
$index{version}="3";
my $ret=Storable::nstore_fd(\%index, $out);
return if ! defined $ret || ! $ret;
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) : ()),
template_depends(shift, undef, @_);
}
-sub misctemplate ($$;@) {
- my $title=shift;
- my $content=shift;
- my %params=@_;
-
- my $template=template("page.tmpl");
-
- my $page="";
- if (exists $params{page}) {
- $page=delete $params{page};
- }
- run_hooks(pagetemplate => sub {
- shift->(
- page => $page,
- destpage => $page,
- template => $template,
- );
- });
- templateactions($template, "");
-
- $template->param(
- dynamic => 1,
- title => $title,
- wikiname => $config{wikiname},
- content => $content,
- baseurl => baseurl(),
- html5 => $config{html5},
- %params,
- );
-
- return $template->output;
-}
-
sub templateactions ($$) {
my $template=shift;
my $page=shift;
$hooks{rcs}{rcs_recentchanges}{call}->(@_);
}
-sub rcs_diff ($) {
+sub rcs_diff ($;$) {
$hooks{rcs}{rcs_diff}{call}->(@_);
}
$autofiles{$file}{generator}=$generator;
}
+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 ($$) {
my $spec = shift;
my $reverse = shift;
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;
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 @_;
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]}) {
}
}
+# Change $this so it is not considered to be influenced by $torm.
+
sub remove_influence {
my $this=shift;
my $torm=shift;
}
sub match_backlink ($$;@) {
- my $ret=match_link($_[1], $_[0], @_);
- $ret->influences($_[1] => $IkiWiki::DEPEND_LINKS);
+ my $page=shift;
+ my $testpage=shift;
+ my %params=@_;
+ if ($testpage eq '.') {
+ $testpage = $params{'location'}
+ }
+ my $ret=match_link($testpage, $page, @_);
+ $ret->influences($testpage => $IkiWiki::DEPEND_LINKS);
return $ret;
}
my $user=shift;
my %params=@_;
- my $regexp=IkiWiki::glob2re($user);
-
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");
}
if (! exists $params{ip}) {
return IkiWiki::ErrorReason->new("no IP specified");
}
+
+ my $regexp=IkiWiki::glob2re(lc $ip);
- if (defined $params{ip} && lc $params{ip} eq lc $ip) {
+ if (defined $params{ip} && lc $params{ip}=~$regexp) {
return IkiWiki::SuccessReason->new("IP is $ip");
}
else {
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} }