diff options
author | Amitai Schlair <schmonz-web-ikiwiki@schmonz.com> | 2013-01-25 08:47:17 -0500 |
---|---|---|
committer | Amitai Schlair <schmonz-web-ikiwiki@schmonz.com> | 2013-01-25 08:47:17 -0500 |
commit | 64370885cca3a37ee1f4a9e96673aca7ba5daae4 (patch) | |
tree | 70cbd95c9b9cfc684c0fc2bfa70af296df411dfb /IkiWiki | |
parent | 9faa0f3c6560be7b5e3aea0f8ca12e04a8c85a32 (diff) | |
parent | ea21db6b71f02ebf9b7105452326142f1e1b84b0 (diff) | |
download | ikiwiki-64370885cca3a37ee1f4a9e96673aca7ba5daae4.tar ikiwiki-64370885cca3a37ee1f4a9e96673aca7ba5daae4.tar.gz |
Merge branch 'master' into cvs
Diffstat (limited to 'IkiWiki')
44 files changed, 1721 insertions, 132 deletions
diff --git a/IkiWiki/CGI.pm b/IkiWiki/CGI.pm index 62383b6fd..5baa6c179 100644 --- a/IkiWiki/CGI.pm +++ b/IkiWiki/CGI.pm @@ -131,7 +131,7 @@ sub needsignin ($$) { if (! defined $session->param("name") || ! userinfo_get($session->param("name"), "regdate")) { - $session->param(postsignin => $ENV{QUERY_STRING}); + $session->param(postsignin => $q->query_string); cgi_signin($q, $session); cgi_savesession($session); exit; diff --git a/IkiWiki/Plugin/aggregate.pm b/IkiWiki/Plugin/aggregate.pm index 5e22609c9..89da5c453 100644 --- a/IkiWiki/Plugin/aggregate.pm +++ b/IkiWiki/Plugin/aggregate.pm @@ -113,8 +113,7 @@ sub launchaggregation () { my @feeds=needsaggregate(); return unless @feeds; if (! lockaggregate()) { - debug("an aggregation process is already running"); - return; + error("an aggregation process is already running"); } # force a later rebuild of source pages $IkiWiki::forcerebuild{$_->{sourcepage}}=1 @@ -201,7 +200,7 @@ sub migrate_to_internal { if (-e $oldoutput) { require IkiWiki::Render; debug("removing output file $oldoutput"); - IkiWiki::prune($oldoutput); + IkiWiki::prune($oldoutput, $config{destdir}); } } diff --git a/IkiWiki/Plugin/amazon_s3.pm b/IkiWiki/Plugin/amazon_s3.pm index cfd8cd347..a9da6bf12 100644 --- a/IkiWiki/Plugin/amazon_s3.pm +++ b/IkiWiki/Plugin/amazon_s3.pm @@ -232,8 +232,9 @@ sub writefile ($$$;$$) { } # This is a wrapper around the real prune. -sub prune ($) { +sub prune ($;$) { my $file=shift; + my $up_to=shift; my @keys=IkiWiki::Plugin::amazon_s3::file2keys($file); @@ -250,7 +251,7 @@ sub prune ($) { } } - return $IkiWiki::Plugin::amazon_s3::subs{'IkiWiki::prune'}->($file); + return $IkiWiki::Plugin::amazon_s3::subs{'IkiWiki::prune'}->($file, $up_to); } 1 diff --git a/IkiWiki/Plugin/attachment.pm b/IkiWiki/Plugin/attachment.pm index 5a180cd5c..aea70429d 100644 --- a/IkiWiki/Plugin/attachment.pm +++ b/IkiWiki/Plugin/attachment.pm @@ -148,7 +148,7 @@ sub formbuilder (@) { $f=Encode::decode_utf8($f); $f=~s/^$page\///; if (IkiWiki::isinlinableimage($f) && - UNIVERSAL::can("IkiWiki::Plugin::img", "import")) { + IkiWiki::Plugin::img->can("import")) { $add.='[[!img '.$f.' align="right" size="" alt=""]]'; } else { @@ -286,7 +286,7 @@ sub attachments_save { } return unless @attachments; require IkiWiki::Render; - IkiWiki::prune($dir); + IkiWiki::prune($dir, $config{wikistatedir}."/attachments"); # Check the attachments in and trigger a wiki refresh. if ($config{rcs}) { diff --git a/IkiWiki/Plugin/bzr.pm b/IkiWiki/Plugin/bzr.pm index 3bc4ea8dd..72552abcc 100644 --- a/IkiWiki/Plugin/bzr.pm +++ b/IkiWiki/Plugin/bzr.pm @@ -5,6 +5,7 @@ use warnings; use strict; use IkiWiki; use Encode; +use URI::Escape q{uri_escape_utf8}; use open qw{:utf8 :std}; sub import { @@ -242,8 +243,10 @@ sub rcs_recentchanges ($) { # Skip source name in renames $filename =~ s/^.* => //; + my $efilename = uri_escape_utf8($filename); + my $diffurl = defined $config{'diffurl'} ? $config{'diffurl'} : ""; - $diffurl =~ s/\[\[file\]\]/$filename/go; + $diffurl =~ s/\[\[file\]\]/$efilename/go; $diffurl =~ s/\[\[file-id\]\]/$fileid/go; $diffurl =~ s/\[\[r2\]\]/$info->{revno}/go; diff --git a/IkiWiki/Plugin/calendar.pm b/IkiWiki/Plugin/calendar.pm index fc497b3c7..d443198f6 100644 --- a/IkiWiki/Plugin/calendar.pm +++ b/IkiWiki/Plugin/calendar.pm @@ -13,7 +13,7 @@ # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. require 5.002; package IkiWiki::Plugin::calendar; diff --git a/IkiWiki/Plugin/comments.pm b/IkiWiki/Plugin/comments.pm index 91a482ed6..c00bf5275 100644 --- a/IkiWiki/Plugin/comments.pm +++ b/IkiWiki/Plugin/comments.pm @@ -301,7 +301,8 @@ sub editcomment ($$) { my @buttons = (POST_COMMENT, PREVIEW, CANCEL); my $form = CGI::FormBuilder->new( - fields => [qw{do sid page subject editcontent type author url}], + fields => [qw{do sid page subject editcontent type author + email url subscribe anonsubscribe}], charset => 'utf-8', method => 'POST', required => [qw{editcontent}], @@ -346,18 +347,35 @@ sub editcomment ($$) { $form->field(name => "type", value => $type, force => 1, type => 'select', options => \@page_types); - $form->tmpl_param(username => $session->param('name')); + my $username=$session->param('name'); + $form->tmpl_param(username => $username); + + $form->field(name => "subscribe", type => 'hidden'); + $form->field(name => "anonsubscribe", type => 'hidden'); + if (IkiWiki::Plugin::notifyemail->can("subscribe")) { + if (defined $username) { + $form->field(name => "subscribe", type => "checkbox", + options => [gettext("email replies to me")]); + } + elsif (IkiWiki::Plugin::passwordauth->can("anonuser")) { + $form->field(name => "anonsubscribe", type => "checkbox", + options => [gettext("email replies to me")]); + } + } if ($config{comments_allowauthor} and ! defined $session->param('name')) { $form->tmpl_param(allowauthor => 1); $form->field(name => 'author', type => 'text', size => '40'); + $form->field(name => 'email', type => 'text', size => '40'); $form->field(name => 'url', type => 'text', size => '40'); } else { $form->tmpl_param(allowauthor => 0); $form->field(name => 'author', type => 'hidden', value => '', force => 1); + $form->field(name => 'email', type => 'hidden', value => '', + force => 1); $form->field(name => 'url', type => 'hidden', value => '', force => 1); } @@ -425,10 +443,7 @@ sub editcomment ($$) { $content .= " nickname=\"$nickname\"\n"; } elsif (defined $session->remote_addr()) { - my $ip = $session->remote_addr(); - if ($ip =~ m/^([.0-9]+)$/) { - $content .= " ip=\"$1\"\n"; - } + $content .= " ip=\"".$session->remote_addr()."\"\n"; } if ($config{comments_allowauthor}) { @@ -490,6 +505,20 @@ sub editcomment ($$) { if ($form->submitted eq POST_COMMENT && $form->validate) { IkiWiki::checksessionexpiry($cgi, $session); + + if (IkiWiki::Plugin::notifyemail->can("subscribe")) { + my $subspec="comment($page)"; + if (defined $username && + length $form->field("subscribe")) { + IkiWiki::Plugin::notifyemail::subscribe( + $username, $subspec); + } + elsif (length $form->field("email") && + length $form->field("anonsubscribe")) { + IkiWiki::Plugin::notifyemail::anonsubscribe( + $form->field("email"), $subspec); + } + } $postcomment=1; my $ok=IkiWiki::check_content(content => $form->field('editcontent'), @@ -575,7 +604,8 @@ sub editcomment ($$) { sub getavatar ($) { my $user=shift; - + return undef unless defined $user; + my $avatar; eval q{use Libravatar::URL}; if (! $@) { @@ -632,9 +662,11 @@ sub commentmoderation ($$) { my $page=IkiWiki::dirname($f); my $file="$config{srcdir}/$f"; + my $filedir=$config{srcdir}; if (! -e $file) { # old location $file="$config{wikistatedir}/comments_pending/".$f; + $filedir="$config{wikistatedir}/comments_pending"; } if ($action eq 'Accept') { @@ -649,7 +681,7 @@ sub commentmoderation ($$) { } require IkiWiki::Render; - IkiWiki::prune($file); + IkiWiki::prune($file, $filedir); } } diff --git a/IkiWiki/Plugin/conditional.pm b/IkiWiki/Plugin/conditional.pm index 026078b3c..0a3d7fb4c 100644 --- a/IkiWiki/Plugin/conditional.pm +++ b/IkiWiki/Plugin/conditional.pm @@ -4,7 +4,6 @@ package IkiWiki::Plugin::conditional; use warnings; use strict; use IkiWiki 3.00; -use UNIVERSAL; sub import { hook(type => "getsetup", id => "conditional", call => \&getsetup); diff --git a/IkiWiki/Plugin/cvs.pm b/IkiWiki/Plugin/cvs.pm index 42812ddef..759ea1c23 100644 --- a/IkiWiki/Plugin/cvs.pm +++ b/IkiWiki/Plugin/cvs.pm @@ -33,6 +33,7 @@ use warnings; use strict; use IkiWiki; +use URI::Escape q{uri_escape_utf8}; use File::chdir; @@ -313,7 +314,8 @@ sub rcs_recentchanges ($) { $oldrev =~ s/INITIAL/0/; $newrev =~ s/\(DEAD\)//; my $diffurl = defined $config{diffurl} ? $config{diffurl} : ""; - $diffurl=~s/\[\[file\]\]/$page/g; + my $epage = uri_escape_utf8($page); + $diffurl=~s/\[\[file\]\]/$epage/g; $diffurl=~s/\[\[r1\]\]/$oldrev/g; $diffurl=~s/\[\[r2\]\]/$newrev/g; unshift @pages, { diff --git a/IkiWiki/Plugin/darcs.pm b/IkiWiki/Plugin/darcs.pm index 1313041e7..646f65df1 100644 --- a/IkiWiki/Plugin/darcs.pm +++ b/IkiWiki/Plugin/darcs.pm @@ -3,6 +3,7 @@ package IkiWiki::Plugin::darcs; use warnings; use strict; +use URI::Escape q{uri_escape_utf8}; use IkiWiki; sub import { @@ -336,7 +337,8 @@ sub rcs_recentchanges ($) { foreach my $f (@files) { my $d = defined $config{'diffurl'} ? $config{'diffurl'} : ""; - $d =~ s/\[\[file\]\]/$f/go; + my $ef = uri_escape_utf8($f); + $d =~ s/\[\[file\]\]/$ef/go; $d =~ s/\[\[hash\]\]/$hash/go; push @pg, { diff --git a/IkiWiki/Plugin/editpage.pm b/IkiWiki/Plugin/editpage.pm index 54051c58c..d15607990 100644 --- a/IkiWiki/Plugin/editpage.pm +++ b/IkiWiki/Plugin/editpage.pm @@ -39,7 +39,7 @@ sub refresh () { } if ($delete) { debug(sprintf(gettext("removing old preview %s"), $file)); - IkiWiki::prune("$config{destdir}/$file"); + IkiWiki::prune("$config{destdir}/$file", $config{destdir}); } } elsif (defined $mtime) { @@ -64,7 +64,8 @@ sub cgi_editpage ($$) { decode_cgi_utf8($q); - my @fields=qw(do rcsinfo subpage from page type editcontent editmessage); + my @fields=qw(do rcsinfo subpage from page type editcontent + editmessage subscribe); my @buttons=("Save Page", "Preview", "Cancel"); eval q{use CGI::FormBuilder}; error($@) if $@; @@ -157,6 +158,17 @@ sub cgi_editpage ($$) { noimageinline => 1, linktext => "FormattingHelp")); + my $cansubscribe=IkiWiki::Plugin::notifyemail->can("subscribe") + && IkiWiki::Plugin::comments->can("import") + && defined $session->param('name'); + if ($cansubscribe) { + $form->field(name => "subscribe", type => "checkbox", + options => [gettext("email comments to me")]); + } + else { + $form->field(name => "subscribe", type => 'hidden'); + } + my $previewing=0; if ($form->submitted eq "Cancel") { if ($form->field("do") eq "create" && defined $from) { @@ -448,6 +460,12 @@ sub cgi_editpage ($$) { # caches and get the most recent version of the page. redirect($q, $baseurl."?updated"); } + + if ($cansubscribe && length $form->field("subscribe")) { + my $subspec="comment($page)"; + IkiWiki::Plugin::notifyemail::subscribe( + $session->param('name'), $subspec); + } } exit; diff --git a/IkiWiki/Plugin/edittemplate.pm b/IkiWiki/Plugin/edittemplate.pm index 061242fd8..c7f1e4fa7 100644 --- a/IkiWiki/Plugin/edittemplate.pm +++ b/IkiWiki/Plugin/edittemplate.pm @@ -132,7 +132,7 @@ sub filltemplate ($$) { if ($@) { # Indicate that the earlier preprocessor directive set # up a template that doesn't work. - return "[[!pagetemplate ".gettext("failed to process template:")." $@]]"; + return "[[!edittemplate ".gettext("failed to process template:")." $@]]"; } $template->param(name => $page); diff --git a/IkiWiki/Plugin/filecheck.pm b/IkiWiki/Plugin/filecheck.pm index 4f4e67489..cdea5c706 100644 --- a/IkiWiki/Plugin/filecheck.pm +++ b/IkiWiki/Plugin/filecheck.pm @@ -48,7 +48,6 @@ sub getsetup () { plugin => { safe => 1, rebuild => undef, - section => "misc", }, } @@ -140,7 +139,7 @@ sub match_mimetype ($$;@) { my $mimeinfo_ok=! $@; my $mimetype; if ($mimeinfo_ok) { - my $mimetype=File::MimeInfo::Magic::magic($file); + $mimetype=File::MimeInfo::Magic::magic($file); } # Fall back to using file, which has a more complete diff --git a/IkiWiki/Plugin/git.pm b/IkiWiki/Plugin/git.pm index 3dd910cd5..3879abeae 100644 --- a/IkiWiki/Plugin/git.pm +++ b/IkiWiki/Plugin/git.pm @@ -5,6 +5,7 @@ use warnings; use strict; use IkiWiki; use Encode; +use URI::Escape q{uri_escape_utf8}; use open qw{:utf8 :std}; my $sha1_pattern = qr/[0-9a-fA-F]{40}/; # pattern to validate Git sha1sums @@ -340,8 +341,8 @@ sub parse_diff_tree ($) { my $dt_ref = shift; # End of stream? - return if !defined @{ $dt_ref } || - !defined @{ $dt_ref }[0] || !length @{ $dt_ref }[0]; + return if ! @{ $dt_ref } || + !defined $dt_ref->[0] || !length $dt_ref->[0]; my %ci; # Header line. @@ -468,13 +469,10 @@ sub git_sha1 (;$) { # Ignore error since a non-existing file might be given. my ($sha1) = run_or_non('git', 'rev-list', '--max-count=1', 'HEAD', '--', $file); - if ($sha1) { + if (defined $sha1) { ($sha1) = $sha1 =~ m/($sha1_pattern)/; # sha1 is untainted now } - else { - debug("Empty sha1sum for '$file'."); - } - return defined $sha1 ? $sha1 : q{}; + return defined $sha1 ? $sha1 : ''; } sub rcs_update () { @@ -617,9 +615,10 @@ sub rcs_recentchanges ($) { my @pages; foreach my $detail (@{ $ci->{'details'} }) { my $file = $detail->{'file'}; + my $efile = uri_escape_utf8($file); my $diffurl = defined $config{'diffurl'} ? $config{'diffurl'} : ""; - $diffurl =~ s/\[\[file\]\]/$file/go; + $diffurl =~ s/\[\[file\]\]/$efile/go; $diffurl =~ s/\[\[sha1_parent\]\]/$ci->{'parent'}/go; $diffurl =~ s/\[\[sha1_from\]\]/$detail->{'sha1_from'}/go; $diffurl =~ s/\[\[sha1_to\]\]/$detail->{'sha1_to'}/go; diff --git a/IkiWiki/Plugin/graphviz.pm b/IkiWiki/Plugin/graphviz.pm index b9f997e04..d4018edaa 100644 --- a/IkiWiki/Plugin/graphviz.pm +++ b/IkiWiki/Plugin/graphviz.pm @@ -132,6 +132,7 @@ sub graph (@) { }, "text"); $p->parse($src); $p->eof; + $s=~s/\[ href= \]//g; # handle self-links $params{src}=$s; } else { diff --git a/IkiWiki/Plugin/htmlscrubber.pm b/IkiWiki/Plugin/htmlscrubber.pm index a58a27d52..36c012c73 100644 --- a/IkiWiki/Plugin/htmlscrubber.pm +++ b/IkiWiki/Plugin/htmlscrubber.pm @@ -29,6 +29,7 @@ sub import { "irc", "ircs", "lastfm", "ldaps", "magnet", "mms", "msnim", "notes", "rsync", "secondlife", "skype", "ssh", "sftp", "smb", "sms", "snews", "webcal", "ymsgr", + "bitcoin", "git", "svn", "bzr", "darcs", "hg" ); # data is a special case. Allow a few data:image/ types, # but disallow data:text/javascript and everything else. diff --git a/IkiWiki/Plugin/httpauth.pm b/IkiWiki/Plugin/httpauth.pm index cb488449d..76d574b2a 100644 --- a/IkiWiki/Plugin/httpauth.pm +++ b/IkiWiki/Plugin/httpauth.pm @@ -7,6 +7,7 @@ use strict; use IkiWiki 3.00; sub import { + hook(type => "checkconfig", id => "httpauth", call => \&checkconfig); hook(type => "getsetup", id => "httpauth", call => \&getsetup); hook(type => "auth", id => "httpauth", call => \&auth); hook(type => "formbuilder_setup", id => "httpauth", @@ -37,6 +38,19 @@ sub getsetup () { rebuild => 0, }, } + +sub checkconfig () { + if ($config{cgi} && defined $config{cgiauthurl} && + keys %{$IkiWiki::hooks{auth}} < 2) { + # There are no other auth hooks registered, so avoid + # the normal signin form, and jump right to httpauth. + require IkiWiki::CGI; + inject(name => "IkiWiki::cgi_signin", call => sub ($$) { + my $cgi=shift; + redir_cgiauthurl($cgi, $cgi->query_string()); + }); + } +} sub redir_cgiauthurl ($;@) { my $cgi=shift; diff --git a/IkiWiki/Plugin/inline.pm b/IkiWiki/Plugin/inline.pm index 159cc5def..8eb033951 100644 --- a/IkiWiki/Plugin/inline.pm +++ b/IkiWiki/Plugin/inline.pm @@ -19,14 +19,14 @@ sub import { hook(type => "checkconfig", id => "inline", call => \&checkconfig); hook(type => "sessioncgi", id => "inline", call => \&sessioncgi); hook(type => "preprocess", id => "inline", - call => \&IkiWiki::preprocess_inline); + call => \&IkiWiki::preprocess_inline, scan => 1); hook(type => "pagetemplate", id => "inline", call => \&IkiWiki::pagetemplate_inline); hook(type => "format", id => "inline", call => \&format, first => 1); # Hook to change to do pinging since it's called late. # This ensures each page only pings once and prevents slow # pings interrupting page builds. - hook(type => "change", id => "inline", call => \&IkiWiki::pingurl); + hook(type => "rendered", id => "inline", call => \&IkiWiki::pingurl); } sub getopt () { @@ -155,6 +155,23 @@ sub preprocess_inline (@) { if (! exists $params{pages} && ! exists $params{pagenames}) { error gettext("missing pages parameter"); } + + if (! defined wantarray) { + # Running in scan mode: only do the essentials + + if (yesno($params{trail}) && IkiWiki::Plugin::trail->can("preprocess_trailitems")) { + # default to sorting age, the same as inline itself, + # but let the params override that + IkiWiki::Plugin::trail::preprocess_trailitems(sort => 'age', %params); + } + + return; + } + + if (yesno($params{trail}) && IkiWiki::Plugin::trail->can("preprocess_trailitems")) { + scalar IkiWiki::Plugin::trail::preprocess_trailitems(sort => 'age', %params); + } + my $raw=yesno($params{raw}); my $archive=yesno($params{archive}); my $rss=(($config{rss} || $config{allowrss}) && exists $params{rss}) ? yesno($params{rss}) : $config{rss}; @@ -194,8 +211,7 @@ sub preprocess_inline (@) { } } - @list = map { bestlink($params{page}, $_) } - split ' ', $params{pagenames}; + @list = split ' ', $params{pagenames}; if (yesno($params{reverse})) { @list=reverse(@list); @@ -204,6 +220,8 @@ sub preprocess_inline (@) { foreach my $p (@list) { add_depends($params{page}, $p, deptype($quick ? "presence" : "content")); } + + @list = grep { exists $pagesources{$_} } @list; } else { my $num=0; @@ -677,7 +695,6 @@ sub genfeed ($$$$$@) { guid => $guid, feeddate => date_3339($lasttime), feedurl => $feedurl, - version => $IkiWiki::version, ); run_hooks(pagetemplate => sub { shift->(page => $page, destpage => $page, diff --git a/IkiWiki/Plugin/link.pm b/IkiWiki/Plugin/link.pm index ef01f1107..1ba28eafd 100644 --- a/IkiWiki/Plugin/link.pm +++ b/IkiWiki/Plugin/link.pm @@ -144,9 +144,9 @@ sub renamepage (@) { my $old=$params{oldpage}; my $new=$params{newpage}; - $params{content} =~ s{(?<!\\)$link_regexp}{ - if (! is_externallink($page, $2, $3)) { - my $linktext=$2; + $params{content} =~ s{(?<!\\)($link_regexp)}{ + if (! is_externallink($page, $3, $4)) { + my $linktext=$3; my $link=$linktext; if (bestlink($page, linkpage($linktext)) eq $old) { $link=pagetitle($new, 1); @@ -161,9 +161,12 @@ sub renamepage (@) { $link="/$link"; } } - defined $1 - ? ( "[[$1|$link".($3 ? "#$3" : "")."]]" ) - : ( "[[$link". ($3 ? "#$3" : "")."]]" ) + defined $2 + ? ( "[[$2|$link".($4 ? "#$4" : "")."]]" ) + : ( "[[$link". ($4 ? "#$4" : "")."]]" ) + } + else { + $1 } }eg; diff --git a/IkiWiki/Plugin/mercurial.pm b/IkiWiki/Plugin/mercurial.pm index b7fe01485..8da4ceb07 100644 --- a/IkiWiki/Plugin/mercurial.pm +++ b/IkiWiki/Plugin/mercurial.pm @@ -5,6 +5,7 @@ use warnings; use strict; use IkiWiki; use Encode; +use URI::Escape q{uri_escape_utf8}; use open qw{:utf8 :std}; sub import { @@ -265,7 +266,8 @@ sub rcs_recentchanges ($) { foreach my $file (split / /,$info->{files}) { my $diffurl = defined $config{diffurl} ? $config{'diffurl'} : ""; - $diffurl =~ s/\[\[file\]\]/$file/go; + my $efile = uri_escape_utf8($file); + $diffurl =~ s/\[\[file\]\]/$efile/go; $diffurl =~ s/\[\[r2\]\]/$info->{changeset}/go; push @pages, { diff --git a/IkiWiki/Plugin/meta.pm b/IkiWiki/Plugin/meta.pm index 220fff9dc..421f1dc86 100644 --- a/IkiWiki/Plugin/meta.pm +++ b/IkiWiki/Plugin/meta.pm @@ -275,17 +275,23 @@ sub preprocess (@) { push @{$metaheaders{$page}}, '<meta name="robots"'. ' content="'.encode_entities($value).'" />'; } - elsif ($key eq 'description') { - push @{$metaheaders{$page}}, '<meta name="'. - encode_entities($key). + elsif ($key eq 'description' || $key eq 'author') { + push @{$metaheaders{$page}}, '<meta name="'.$key. '" content="'.encode_entities($value).'" />'; } elsif ($key eq 'name') { - push @{$metaheaders{$page}}, scrub('<meta '.$key.'="'. + push @{$metaheaders{$page}}, scrub('<meta name="'. encode_entities($value). join(' ', map { "$_=\"$params{$_}\"" } keys %params). ' />', $page, $destpage); } + elsif ($key eq 'keywords') { + # Make sure the keyword string is safe: only allow alphanumeric + # characters, space and comma and strip the rest. + $value =~ s/[^[:alnum:], ]+//g; + push @{$metaheaders{$page}}, '<meta name="keywords"'. + ' content="'.encode_entities($value).'" />'; + } else { push @{$metaheaders{$page}}, scrub('<meta name="'. encode_entities($key).'" content="'. @@ -312,8 +318,9 @@ sub pagetemplate (@) { $template->param(title_overridden => 1); } - foreach my $field (qw{author authorurl}) { - $template->param($field => $pagestate{$page}{meta}{$field}) + foreach my $field (qw{authorurl}) { + eval q{use HTML::Entities}; + $template->param($field => HTML::Entities::encode_entities($pagestate{$page}{meta}{$field})) if exists $pagestate{$page}{meta}{$field} && $template->query(name => $field); } @@ -324,7 +331,7 @@ sub pagetemplate (@) { } } - foreach my $field (qw{description}) { + foreach my $field (qw{description author}) { eval q{use HTML::Entities}; $template->param($field => HTML::Entities::encode_numeric($pagestate{$page}{meta}{$field})) if exists $pagestate{$page}{meta}{$field} && $template->query(name => $field); diff --git a/IkiWiki/Plugin/mirrorlist.pm b/IkiWiki/Plugin/mirrorlist.pm index f54d94ad5..b7e532485 100644 --- a/IkiWiki/Plugin/mirrorlist.pm +++ b/IkiWiki/Plugin/mirrorlist.pm @@ -24,6 +24,19 @@ sub getsetup () { safe => 1, rebuild => 1, }, + mirrorlist_use_cgi => { + type => 'boolean', + example => 1, + description => "generate links that point to the mirrors' ikiwiki CGI", + safe => 1, + rebuild => 1, + }, +} + +sub checkconfig () { + if (! defined $config{mirrorlist_use_cgi}) { + $config{mirrorlist_use_cgi}=0; + } } sub pagetemplate (@) { @@ -46,7 +59,9 @@ sub mirrorlist ($) { join(", ", map { qq{<a href="}. - $config{mirrorlist}->{$_}."/".urlto($page, ""). + ( $config{mirrorlist_use_cgi} ? + $config{mirrorlist}->{$_}."?do=goto&page=$page" : + $config{mirrorlist}->{$_}."/".urlto($page, "") ). qq{">$_</a>} } keys %{$config{mirrorlist}} ). diff --git a/IkiWiki/Plugin/monotone.pm b/IkiWiki/Plugin/monotone.pm index 1d89e3f6b..105627814 100644 --- a/IkiWiki/Plugin/monotone.pm +++ b/IkiWiki/Plugin/monotone.pm @@ -7,6 +7,7 @@ use IkiWiki; use Monotone; use Date::Parse qw(str2time); use Date::Format qw(time2str); +use URI::Escape q{uri_escape_utf8}; my $sha1_pattern = qr/[0-9a-fA-F]{40}/; # pattern to validate sha1sums my $mtn_version = undef; @@ -593,7 +594,8 @@ sub rcs_recentchanges ($) { my $diffurl=$config{diffurl}; $diffurl=~s/\[\[r1\]\]/$parent/g; $diffurl=~s/\[\[r2\]\]/$rev/g; - $diffurl=~s/\[\[file\]\]/$file/g; + my $efile = uri_escape_utf8($file); + $diffurl=~s/\[\[file\]\]/$efile/g; push @pages, { page => pagename($file), diffurl => $diffurl, diff --git a/IkiWiki/Plugin/notifyemail.pm b/IkiWiki/Plugin/notifyemail.pm new file mode 100644 index 000000000..2c1775f2e --- /dev/null +++ b/IkiWiki/Plugin/notifyemail.pm @@ -0,0 +1,168 @@ +#!/usr/bin/perl +package IkiWiki::Plugin::notifyemail; + +use warnings; +use strict; +use IkiWiki 3.00; + +sub import { + hook(type => "formbuilder", id => "notifyemail", call => \&formbuilder); + hook(type => "getsetup", id => "notifyemail", call => \&getsetup); + hook(type => "changes", id => "notifyemail", call => \¬ify); +} + +sub getsetup () { + return + plugin => { + safe => 1, + rebuild => 0, + }, +} + +sub formbuilder (@) { + my %params=@_; + my $form=$params{form}; + return unless $form->title eq "preferences"; + my $session=$params{session}; + my $username=$session->param("name"); + $form->field(name => "subscriptions", size => 50, + fieldset => "preferences", + comment => "(".htmllink("", "", "ikiwiki/PageSpec", noimageinline => 1).")"); + if (! $form->submitted) { + $form->field(name => "subscriptions", force => 1, + value => getsubscriptions($username)); + } + elsif ($form->submitted eq "Save Preferences" && $form->validate && + defined $form->field("subscriptions")) { + setsubscriptions($username, $form->field('subscriptions')); + } +} + +sub getsubscriptions ($) { + my $user=shift; + eval q{use IkiWiki::UserInfo}; + error $@ if $@; + IkiWiki::userinfo_get($user, "subscriptions"); +} + +sub setsubscriptions ($$) { + my $user=shift; + my $subscriptions=shift; + eval q{use IkiWiki::UserInfo}; + error $@ if $@; + IkiWiki::userinfo_set($user, "subscriptions", $subscriptions); +} + +# Called by other plugins to subscribe the user to a pagespec. +sub subscribe ($$) { + my $user=shift; + my $addpagespec=shift; + my $pagespec=getsubscriptions($user); + setsubscriptions($user, + length $pagespec ? $pagespec." or ".$addpagespec : $addpagespec); +} + +# Called by other plugins to subscribe an email to a pagespec. +sub anonsubscribe ($$) { + my $email=shift; + my $addpagespec=shift; + if (IkiWiki::Plugin::passwordauth->can("anonuser")) { + my $user=IkiWiki::Plugin::passwordauth::anonuser($email); + if (! defined $user) { + error(gettext("Cannot subscribe your email address without logging in.")); + } + subscribe($user, $addpagespec); + } +} + +sub notify (@) { + my @files=@_; + return unless @files; + + eval q{use Mail::Sendmail}; + error $@ if $@; + eval q{use IkiWiki::UserInfo}; + error $@ if $@; + eval q{use URI}; + error($@) if $@; + + # Daemonize, in case the mail sending takes a while. + defined(my $pid = fork) or error("Can't fork: $!"); + return if $pid; # parent + 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 $userinfo=IkiWiki::userinfo_retrieve(); + exit 0 unless defined $userinfo; + + foreach my $user (keys %$userinfo) { + my $pagespec=$userinfo->{$user}->{"subscriptions"}; + next unless defined $pagespec && length $pagespec; + my $email=$userinfo->{$user}->{email}; + next unless defined $email && length $email; + + foreach my $file (@files) { + my $page=pagename($file); + next unless pagespec_match($page, $pagespec); + my $content=""; + my $showcontent=defined pagetype($file); + if ($showcontent) { + $content=eval { readfile(srcfile($file)) }; + $showcontent=0 if $@; + } + my $url; + if (! IkiWiki::isinternal($page)) { + $url=urlto($page, undef, 1); + } + elsif (defined $pagestate{$page}{meta}{permalink}) { + # need to use permalink for an internal page + $url=URI->new_abs($pagestate{$page}{meta}{permalink}, $config{url}); + } + else { + $url=$config{url}; # crummy fallback url + } + my $pagedesc=$page; + if (defined $pagestate{$page}{meta}{title} && + length $pagestate{$page}{meta}{title}) { + $pagedesc=qq{"$pagestate{$page}{meta}{title}"}; + } + my $subject=gettext("change notification:")." ".$pagedesc; + if (pagetype($file) eq '_comment') { + $subject=gettext("comment notification:")." ".$pagedesc; + } + my $prefsurl=IkiWiki::cgiurl_abs(do => 'prefs'); + if (IkiWiki::Plugin::passwordauth->can("anonusertoken")) { + my $token=IkiWiki::Plugin::passwordauth::anonusertoken($userinfo->{$user}); + $prefsurl=IkiWiki::cgiurl_abs( + do => 'tokenauth', + name => $user, + token => $token, + ) if defined $token; + } + my $template=template("notifyemail.tmpl"); + $template->param( + wikiname => $config{wikiname}, + url => $url, + prefsurl => $prefsurl, + showcontent => $showcontent, + content => $content, + ); + sendmail( + To => $email, + From => "$config{wikiname} <$config{adminemail}>", + Subject => $subject, + Message => $template->output, + ); + } + } + + exit 0; # daemon child +} + +1 diff --git a/IkiWiki/Plugin/opendiscussion.pm b/IkiWiki/Plugin/opendiscussion.pm index 2805f60ef..808d3cd2b 100644 --- a/IkiWiki/Plugin/opendiscussion.pm +++ b/IkiWiki/Plugin/opendiscussion.pm @@ -25,7 +25,7 @@ sub canedit ($$) { my $cgi=shift; my $session=shift; - return "" if $page=~/(\/|^)\Q$config{discussionpage}\E$/i; + return "" if $config{discussion} && $page=~/(\/|^)\Q$config{discussionpage}\E$/i; return "" if pagespec_match($page, "postcomment(*)"); return undef; } diff --git a/IkiWiki/Plugin/openid.pm b/IkiWiki/Plugin/openid.pm index b6642619a..40a956849 100644 --- a/IkiWiki/Plugin/openid.pm +++ b/IkiWiki/Plugin/openid.pm @@ -100,9 +100,10 @@ sub formbuilder_setup (@) { IkiWiki::openiduser($session->param("name"))) { $form->field(name => "openid_identifier", disabled => 1, label => htmllink("", "", "ikiwiki/OpenID", noimageinline => 1), - value => $session->param("name"), - size => length($session->param("name")), force => 1, - fieldset => "login"); + value => "", + size => 1, force => 1, + fieldset => "login", + comment => $session->param("name")); $form->field(name => "email", type => "hidden"); } } diff --git a/IkiWiki/Plugin/osm.pm b/IkiWiki/Plugin/osm.pm new file mode 100644 index 000000000..a7baa5f2b --- /dev/null +++ b/IkiWiki/Plugin/osm.pm @@ -0,0 +1,594 @@ +#!/usr/bin/perl +# Copyright 2011 Blars Blarson +# Released under GPL version 2 + +package IkiWiki::Plugin::osm; +use utf8; +use strict; +use warnings; +use IkiWiki 3.0; + +sub import { + add_underlay("osm"); + hook(type => "getsetup", id => "osm", call => \&getsetup); + hook(type => "format", id => "osm", call => \&format); + hook(type => "preprocess", id => "osm", call => \&preprocess); + hook(type => "preprocess", id => "waypoint", call => \&process_waypoint); + hook(type => "savestate", id => "waypoint", call => \&savestate); + hook(type => "cgi", id => "osm", call => \&cgi); +} + +sub getsetup () { + return + plugin => { + safe => 1, + rebuild => 1, + section => "special-purpose", + }, + osm_default_zoom => { + type => "integer", + example => "15", + description => "the default zoom when you click on the map link", + safe => 1, + rebuild => 1, + }, + osm_default_icon => { + type => "string", + example => "ikiwiki/images/osm.png", + description => "the icon shown on links and on the main map", + safe => 0, + rebuild => 1, + }, + osm_alt => { + type => "string", + example => "", + description => "the alt tag of links, defaults to empty", + safe => 0, + rebuild => 1, + }, + osm_format => { + type => "string", + example => "KML", + description => "the output format for waypoints, can be KML, GeoJSON or CSV (one or many, comma-separated)", + safe => 1, + rebuild => 1, + }, + osm_tag_default_icon => { + type => "string", + example => "icon.png", + description => "the icon attached to a tag, displayed on the map for tagged pages", + safe => 0, + rebuild => 1, + }, + osm_openlayers_url => { + type => "string", + example => "http://www.openlayers.org/api/OpenLayers.js", + description => "Url for the OpenLayers.js file", + safe => 0, + rebuild => 1, + }, + osm_layers => { + type => "string", + example => { 'OSM', 'GoogleSatellite' }, + description => "Layers to use in the map. Can be either the 'OSM' string or a type option for Google maps (GoogleNormal, GoogleSatellite, GoogleHybrid or GooglePhysical). It can also be an arbitrary URL in a syntax acceptable for OpenLayers.Layer.OSM.url parameter.", + safe => 0, + rebuild => 1, + }, + osm_google_apikey => { + type => "string", + example => "", + description => "Google maps API key, Google layer not used if missing, see https://code.google.com/apis/console/ to get an API key", + safe => 1, + rebuild => 1, + }, +} + +sub register_rendered_files { + my $map = shift; + my $page = shift; + my $dest = shift; + + if ($page eq $dest) { + my %formats = get_formats(); + if ($formats{'GeoJSON'}) { + will_render($page, "$map/pois.json"); + } + if ($formats{'CSV'}) { + will_render($page, "$map/pois.txt"); + } + if ($formats{'KML'}) { + will_render($page, "$map/pois.kml"); + } + } +} + +sub preprocess { + my %params=@_; + my $page = $params{page}; + my $dest = $params{destpage}; + my $loc = $params{loc}; # sanitized below + my $lat = $params{lat}; # sanitized below + my $lon = $params{lon}; # sanitized below + my $href = $params{href}; + + my ($width, $height, $float); + $height = scrub($params{'height'} || "300px", $page, $dest); # sanitized here + $width = scrub($params{'width'} || "500px", $page, $dest); # sanitized here + $float = (defined($params{'right'}) && 'right') || (defined($params{'left'}) && 'left'); # sanitized here + + my $zoom = scrub($params{'zoom'} // $config{'osm_default_zoom'} // 15, $page, $dest); # sanitized below + my $map; + $map = $params{'map'} || 'map'; + + $map = scrub($map, $page, $dest); # sanitized here + my $name = scrub($params{'name'} || $map, $page, $dest); + + if (defined($lon) || defined($lat) || defined($loc)) { + ($lon, $lat) = scrub_lonlat($loc, $lon, $lat); + } + + if ($zoom !~ /^\d\d?$/ || $zoom < 2 || $zoom > 18) { + error("Bad zoom"); + } + + if (! defined $href || ! length $href) { + $href=IkiWiki::cgiurl( + do => "osm", + map => $map, + ); + } + + register_rendered_files($map, $page, $dest); + + $pagestate{$page}{'osm'}{$map}{'displays'}{$name} = { + height => $height, + width => $width, + float => $float, + zoom => $zoom, + fullscreen => 0, + editable => defined($params{'editable'}), + lat => $lat, + lon => $lon, + href => $href, + google_apikey => $config{'osm_google_apikey'}, + }; + return "<div id=\"mapdiv-$name\"></div>"; +} + +sub process_waypoint { + my %params=@_; + my $loc = $params{'loc'}; # sanitized below + my $lat = $params{'lat'}; # sanitized below + my $lon = $params{'lon'}; # sanitized below + my $page = $params{'page'}; # not sanitized? + my $dest = $params{'destpage'}; # not sanitized? + my $hidden = defined($params{'hidden'}); # sanitized here + my ($p) = $page =~ /(?:^|\/)([^\/]+)\/?$/; # shorter page name + my $name = scrub($params{'name'} || $p, $page, $dest); # sanitized here + my $desc = scrub($params{'desc'} || '', $page, $dest); # sanitized here + my $zoom = scrub($params{'zoom'} // $config{'osm_default_zoom'} // 15, $page, $dest); # sanitized below + my $icon = $config{'osm_default_icon'} || "ikiwiki/images/osm.png"; # sanitized: we trust $config + my $map = scrub($params{'map'} || 'map', $page, $dest); # sanitized here + my $alt = $config{'osm_alt'} ? "alt=\"$config{'osm_alt'}\"" : ''; # sanitized: we trust $config + if ($zoom !~ /^\d\d?$/ || $zoom < 2 || $zoom > 18) { + error("Bad zoom"); + } + + ($lon, $lat) = scrub_lonlat($loc, $lon, $lat); + if (!defined($lat) || !defined($lon)) { + error("Must specify lat and lon"); + } + + my $tag = $params{'tag'}; + foreach my $t (keys %{$typedlinks{$page}{'tag'}}) { + if ($icon = get_tag_icon($t)) { + $tag = $t; + last; + } + $t =~ s!/$config{'tagbase'}/!!; + if ($icon = get_tag_icon($t)) { + $tag = $t; + last; + } + } + $icon = urlto($icon, $dest, 1); + $tag = '' unless $tag; + register_rendered_files($map, $page, $dest); + $pagestate{$page}{'osm'}{$map}{'waypoints'}{$name} = { + page => $page, + desc => $desc, + icon => $icon, + tag => $tag, + lat => $lat, + lon => $lon, + # How to link back to the page from the map, not to be + # confused with the URL of the map itself sent to the + # embeded map below. Note: used in generated KML etc file, + # so must be absolute. + href => urlto($page), + }; + + my $mapurl = IkiWiki::cgiurl( + do => "osm", + map => $map, + lat => $lat, + lon => $lon, + zoom => $zoom, + ); + my $output = ''; + if (defined($params{'embed'})) { + $output .= preprocess(%params, + href => $mapurl, + ); + } + if (!$hidden) { + $output .= "<a href=\"$mapurl\"><img class=\"img\" src=\"$icon\" $alt /></a>"; + } + return $output; +} + +# get the icon from the given tag +sub get_tag_icon($) { + my $tag = shift; + # look for an icon attached to the tag + my $attached = $tag . '/' . $config{'osm_tag_default_icon'}; + if (srcfile($attached)) { + return $attached; + } + else { + return undef; + } +} + +sub scrub_lonlat($$$) { + my ($loc, $lon, $lat) = @_; + if ($loc) { + if ($loc =~ /^\s*(\-?\d+(?:\.\d*°?|(?:°?|\s)\s*\d+(?:\.\d*\'?|(?:\'|\s)\s*\d+(?:\.\d*)?\"?|\'?)°?)[NS]?)\s*\,?\;?\s*(\-?\d+(?:\.\d*°?|(?:°?|\s)\s*\d+(?:\.\d*\'?|(?:\'|\s)\s*\d+(?:\.\d*)?\"?|\'?)°?)[EW]?)\s*$/) { + $lat = $1; + $lon = $2; + } + else { + error("Bad loc"); + } + } + if (defined($lat)) { + if ($lat =~ /^(\-?)(\d+)(?:(\.\d*)°?|(?:°|\s)\s*(\d+)(?:(\.\d*)\'?|(?:\'|\s)\s*(\d+(?:\.\d*)?\"?)|\'?)|°?)\s*([NS])?\s*$/) { + $lat = $2 + ($3//0) + ((($4//0) + (($5//0) + (($6//0)/60.)))/60.); + if (($1 eq '-') || (($7//'') eq 'S')) { + $lat = - $lat; + } + } + else { + error("Bad lat"); + } + } + if (defined($lon)) { + if ($lon =~ /^(\-?)(\d+)(?:(\.\d*)°?|(?:°|\s)\s*(\d+)(?:(\.\d*)\'?|(?:\'|\s)\s*(\d+(?:\.\d*)?\"?)|\'?)|°?)\s*([EW])?$/) { + $lon = $2 + ($3//0) + ((($4//0) + (($5//0) + (($6//0)/60.)))/60.); + if (($1 eq '-') || (($7//'') eq 'W')) { + $lon = - $lon; + } + } + else { + error("Bad lon"); + } + } + if ($lat < -90 || $lat > 90 || $lon < -180 || $lon > 180) { + error("Location out of range"); + } + return ($lon, $lat); +} + +sub savestate { + my %waypoints = (); + my %linestrings = (); + + foreach my $page (keys %pagestate) { + if (exists $pagestate{$page}{'osm'}) { + foreach my $map (keys %{$pagestate{$page}{'osm'}}) { + foreach my $name (keys %{$pagestate{$page}{'osm'}{$map}{'waypoints'}}) { + debug("found waypoint $name"); + $waypoints{$map}{$name} = $pagestate{$page}{'osm'}{$map}{'waypoints'}{$name}; + } + } + } + } + + foreach my $page (keys %pagestate) { + if (exists $pagestate{$page}{'osm'}) { + foreach my $map (keys %{$pagestate{$page}{'osm'}}) { + # examine the links on this page + foreach my $name (keys %{$pagestate{$page}{'osm'}{$map}{'waypoints'}}) { + if (exists $links{$page}) { + foreach my $otherpage (@{$links{$page}}) { + if (exists $waypoints{$map}{$otherpage}) { + push(@{$linestrings{$map}}, [ + [ $waypoints{$map}{$name}{'lon'}, $waypoints{$map}{$name}{'lat'} ], + [ $waypoints{$map}{$otherpage}{'lon'}, $waypoints{$map}{$otherpage}{'lat'} ] + ]); + } + } + } + } + } + # clear the state, it will be regenerated on the next parse + # the idea here is to clear up removed waypoints... + $pagestate{$page}{'osm'} = (); + } + } + + my %formats = get_formats(); + if ($formats{'GeoJSON'}) { + writejson(\%waypoints, \%linestrings); + } + if ($formats{'CSV'}) { + writecsvs(\%waypoints, \%linestrings); + } + if ($formats{'KML'}) { + writekml(\%waypoints, \%linestrings); + } +} + +sub writejson($;$) { + my %waypoints = %{$_[0]}; + my %linestrings = %{$_[1]}; + eval q{use JSON}; + error $@ if $@; + foreach my $map (keys %waypoints) { + my %geojson = ( "type" => "FeatureCollection", "features" => []); + foreach my $name (keys %{$waypoints{$map}}) { + my %marker = ( "type" => "Feature", + "geometry" => { "type" => "Point", "coordinates" => [ $waypoints{$map}{$name}{'lon'}, $waypoints{$map}{$name}{'lat'} ] }, + "properties" => $waypoints{$map}{$name} ); + push @{$geojson{'features'}}, \%marker; + } + foreach my $linestring (@{$linestrings{$map}}) { + my %json = ( "type" => "Feature", + "geometry" => { "type" => "LineString", "coordinates" => $linestring }); + push @{$geojson{'features'}}, \%json; + } + writefile("pois.json", $config{destdir} . "/$map", to_json(\%geojson)); + } +} + +sub writekml($;$) { + my %waypoints = %{$_[0]}; + my %linestrings = %{$_[1]}; + eval q{use XML::Writer}; + error $@ if $@; + foreach my $map (keys %waypoints) { + my $output; + my $writer = XML::Writer->new( OUTPUT => \$output, + DATA_MODE => 1, DATA_INDENT => ' ', ENCODING => 'UTF-8'); + $writer->xmlDecl(); + $writer->startTag("kml", "xmlns" => "http://www.opengis.net/kml/2.2"); + $writer->startTag("Document"); + + # first pass: get the icons + my %tags_map = (); # keep track of tags seen + foreach my $name (keys %{$waypoints{$map}}) { + my %options = %{$waypoints{$map}{$name}}; + if (!$tags_map{$options{tag}}) { + debug("found new style " . $options{tag}); + $tags_map{$options{tag}} = (); + $writer->startTag("Style", id => $options{tag}); + $writer->startTag("IconStyle"); + $writer->startTag("Icon"); + $writer->startTag("href"); + $writer->characters($options{icon}); + $writer->endTag(); + $writer->endTag(); + $writer->endTag(); + $writer->endTag(); + } + $tags_map{$options{tag}}{$name} = \%options; + } + + foreach my $name (keys %{$waypoints{$map}}) { + my %options = %{$waypoints{$map}{$name}}; + $writer->startTag("Placemark"); + $writer->startTag("name"); + $writer->characters($name); + $writer->endTag(); + $writer->startTag("styleUrl"); + $writer->characters('#' . $options{tag}); + $writer->endTag(); + #$writer->emptyTag('atom:link', href => $options{href}); + # to make it easier for us as the atom:link parameter is + # hard to access from javascript + $writer->startTag('href'); + $writer->characters($options{href}); + $writer->endTag(); + $writer->startTag("description"); + $writer->characters($options{desc}); + $writer->endTag(); + $writer->startTag("Point"); + $writer->startTag("coordinates"); + $writer->characters($options{lon} . "," . $options{lat}); + $writer->endTag(); + $writer->endTag(); + $writer->endTag(); + } + + my $i = 0; + foreach my $linestring (@{$linestrings{$map}}) { + $writer->startTag("Placemark"); + $writer->startTag("name"); + $writer->characters("linestring " . $i++); + $writer->endTag(); + $writer->startTag("LineString"); + $writer->startTag("coordinates"); + my $str = ''; + foreach my $coord (@{$linestring}) { + $str .= join(',', @{$coord}) . " \n"; + } + $writer->characters($str); + $writer->endTag(); + $writer->endTag(); + $writer->endTag(); + } + $writer->endTag(); + $writer->endTag(); + $writer->end(); + + writefile("pois.kml", $config{destdir} . "/$map", $output); + } +} + +sub writecsvs($;$) { + my %waypoints = %{$_[0]}; + foreach my $map (keys %waypoints) { + my $poisf = "lat\tlon\ttitle\tdescription\ticon\ticonSize\ticonOffset\n"; + foreach my $name (keys %{$waypoints{$map}}) { + my %options = %{$waypoints{$map}{$name}}; + my $line = + $options{'lat'} . "\t" . + $options{'lon'} . "\t" . + $name . "\t" . + $options{'desc'} . '<br /><a href="' . $options{'page'} . '">' . $name . "</a>\t" . + $options{'icon'} . "\n"; + $poisf .= $line; + } + writefile("pois.txt", $config{destdir} . "/$map", $poisf); + } +} + +# pipe some data through the HTML scrubber +# +# code taken from the meta.pm plugin +sub scrub($$$) { + if (IkiWiki::Plugin::htmlscrubber->can("sanitize")) { + return IkiWiki::Plugin::htmlscrubber::sanitize( + content => shift, page => shift, destpage => shift); + } + else { + return shift; + } +} + +# taken from toggle.pm +sub format (@) { + my %params=@_; + + if ($params{content}=~m!<div[^>]*id="mapdiv-[^"]*"[^>]*>!g) { + if (! ($params{content}=~s!</body>!include_javascript($params{page})."</body>"!em)) { + # no <body> tag, probably in preview mode + $params{content}=$params{content} . include_javascript($params{page}); + } + } + return $params{content}; +} + +sub preferred_format() { + if (!defined($config{'osm_format'}) || !$config{'osm_format'}) { + $config{'osm_format'} = 'KML'; + } + my @spl = split(/, */, $config{'osm_format'}); + return shift @spl; +} + +sub get_formats() { + if (!defined($config{'osm_format'}) || !$config{'osm_format'}) { + $config{'osm_format'} = 'KML'; + } + map { $_ => 1 } split(/, */, $config{'osm_format'}); +} + +sub include_javascript ($) { + my $page=shift; + my $loader; + + if (exists $pagestate{$page}{'osm'}) { + foreach my $map (keys %{$pagestate{$page}{'osm'}}) { + foreach my $name (keys %{$pagestate{$page}{'osm'}{$map}{'displays'}}) { + $loader .= map_setup_code($map, $name, %{$pagestate{$page}{'osm'}{$map}{'displays'}{$name}}); + } + } + } + if ($loader) { + return embed_map_code($page) . "<script type=\"text/javascript\" charset=\"utf-8\">$loader</script>"; + } + else { + return ''; + } +} + +sub cgi($) { + my $cgi=shift; + + return unless defined $cgi->param('do') && + $cgi->param("do") eq "osm"; + + IkiWiki::loadindex(); + + IkiWiki::decode_cgi_utf8($cgi); + + my $map = $cgi->param('map'); + if (!defined $map || $map !~ /^[a-z]*$/) { + error("invalid map parameter"); + } + + print "Content-Type: text/html\r\n"; + print ("\r\n"); + print "<html><body>"; + print "<div id=\"mapdiv-$map\"></div>"; + print embed_map_code(); + print "<script type=\"text/javascript\" charset=\"utf-8\">"; + print map_setup_code($map, $map, + lat => "urlParams['lat']", + lon => "urlParams['lon']", + zoom => "urlParams['zoom']", + fullscreen => 1, + editable => 1, + google_apikey => $config{'osm_google_apikey'}, + ); + print "</script>"; + print "</body></html>"; + + exit 0; +} + +sub embed_map_code(;$) { + my $page=shift; + my $olurl = $config{osm_openlayers_url} || "http://www.openlayers.org/api/OpenLayers.js"; + my $code = '<script src="'.$olurl.'" type="text/javascript" charset="utf-8"></script>'."\n". + '<script src="'.urlto("ikiwiki/osm.js", $page). + '" type="text/javascript" charset="utf-8"></script>'."\n"; + if ($config{'osm_google_apikey'}) { + $code .= '<script src="http://maps.google.com/maps?file=api&v=2&key='.$config{'osm_google_apikey'}.'&sensor=false" type="text/javascript" charset="utf-8"></script>'; + } + return $code; +} + +sub map_setup_code($;@) { + my $map=shift; + my $name=shift; + my %options=@_; + + my $mapurl = $config{osm_map_url}; + + eval q{use JSON}; + error $@ if $@; + + $options{'format'} = preferred_format(); + + my %formats = get_formats(); + if ($formats{'GeoJSON'}) { + $options{'jsonurl'} = urlto($map."/pois.json"); + } + if ($formats{'CSV'}) { + $options{'csvurl'} = urlto($map."/pois.txt"); + } + if ($formats{'KML'}) { + $options{'kmlurl'} = urlto($map."/pois.kml"); + } + + if ($mapurl) { + $options{'mapurl'} = $mapurl; + } + $options{'layers'} = $config{osm_layers}; + + return "mapsetup('mapdiv-$name', " . to_json(\%options) . ");"; +} + +1; diff --git a/IkiWiki/Plugin/passwordauth.pm b/IkiWiki/Plugin/passwordauth.pm index 35ebd961f..0cf2a26ea 100644 --- a/IkiWiki/Plugin/passwordauth.pm +++ b/IkiWiki/Plugin/passwordauth.pm @@ -96,6 +96,72 @@ sub setpassword ($$;$) { else { IkiWiki::userinfo_set($user, $field, $password); } + + # Setting the password clears any passwordless login token. + if ($field ne 'passwordless') { + IkiWiki::userinfo_set($user, "passwordless", ""); + } +} + +# Generates a token that can be used to log the user in. +# This needs to be hard to guess. Generating a cgi session id will +# make it as hard to guess as any cgi session. +sub gentoken ($$;$) { + my $user=shift; + my $tokenfield=shift; + my $reversable=shift; + + eval q{use CGI::Session}; + error($@) if $@; + my $token = CGI::Session->new->id; + if (! $reversable) { + setpassword($user, $token, $tokenfield); + } + else { + IkiWiki::userinfo_set($user, $tokenfield, $token); + } + return $token; +} + +# An anonymous user has no normal password, only a passwordless login +# token. Given an email address, this sets up such a user for that email, +# unless one already exists, and returns the username. +sub anonuser ($) { + my $email=shift; + + # Want a username for this email that won't overlap with any other. + my $user=$email; + $user=~s/@/_/g; + + my $userinfo=IkiWiki::userinfo_retrieve(); + if (! exists $userinfo->{$user} || ! ref $userinfo->{$user}) { + if (IkiWiki::userinfo_setall($user, { + 'email' => $email, + 'regdate' => time})) { + gentoken($user, "passwordless", 1); + return $user; + } + else { + error(gettext("Error creating account.")); + } + } + elsif (defined anonusertoken($userinfo->{$user})) { + return $user; + } + else { + return undef; + } +} + +sub anonusertoken ($) { + my $userhash=shift; + if (exists $userhash->{passwordless} && + length $userhash->{passwordless}) { + return $userhash->{passwordless}; + } + else { + return undef; + } } sub formbuilder_setup (@) { @@ -277,20 +343,13 @@ sub formbuilder (@) { if (! length $email) { error(gettext("No email address, so cannot email password reset instructions.")); } - - # Store a token that can be used once - # to log the user in. This needs to be hard - # to guess. Generating a cgi session id will - # make it as hard to guess as any cgi session. - eval q{use CGI::Session}; - error($@) if $@; - my $token = CGI::Session->new->id; - setpassword($user_name, $token, "resettoken"); + + my $token=gentoken($user_name, "resettoken"); my $template=template("passwordmail.tmpl"); $template->param( user_name => $user_name, - passwordurl => IkiWiki::cgiurl( + passwordurl => IkiWiki::cgiurl_abs( 'do' => "reset", 'name' => $user_name, 'token' => $token, @@ -329,7 +388,7 @@ sub formbuilder (@) { elsif ($form->title eq "preferences") { if ($form->submitted eq "Save Preferences" && $form->validate) { my $user_name=$form->field('name'); - if ($form->field("password") && length $form->field("password")) { + if (defined $form->field("password") && length $form->field("password")) { setpassword($user_name, $form->field('password')); } } @@ -356,6 +415,22 @@ sub sessioncgi ($$) { IkiWiki::cgi_prefs($q, $session); exit; } + elsif ($q->param('do') eq 'tokenauth') { + my $name=$q->param("name"); + my $token=$q->param("token"); + + if (! defined $name || ! defined $token || + ! length $name || ! length $token) { + error(gettext("incorrect url")); + } + if (! checkpassword($name, $token, "passwordless")) { + error(gettext("access denied")); + } + + $session->param("name", $name); + IkiWiki::cgi_prefs($q, $session); + exit; + } elsif ($q->param("do") eq "register") { # After registration, need to go somewhere, so show prefs page. $session->param(postsignin => "do=prefs"); diff --git a/IkiWiki/Plugin/pinger.pm b/IkiWiki/Plugin/pinger.pm index ea4f3e0dc..588f7a42a 100644 --- a/IkiWiki/Plugin/pinger.pm +++ b/IkiWiki/Plugin/pinger.pm @@ -13,7 +13,7 @@ sub import { hook(type => "needsbuild", id => "pinger", call => \&needsbuild); hook(type => "preprocess", id => "ping", call => \&preprocess); hook(type => "delete", id => "pinger", call => \&ping); - hook(type => "change", id => "pinger", call => \&ping); + hook(type => "rendered", id => "pinger", call => \&ping); } sub getsetup () { diff --git a/IkiWiki/Plugin/po.pm b/IkiWiki/Plugin/po.pm index 6410a1c66..53e6af92f 100644 --- a/IkiWiki/Plugin/po.pm +++ b/IkiWiki/Plugin/po.pm @@ -23,7 +23,6 @@ use File::Copy; use File::Spec; use File::Temp; use Memoize; -use UNIVERSAL; my ($master_language_code, $master_language_name); my %translations; @@ -48,7 +47,7 @@ sub import { hook(type => "pagetemplate", id => "po", call => \&pagetemplate, last => 1); hook(type => "rename", id => "po", call => \&renamepages, first => 1); hook(type => "delete", id => "po", call => \&mydelete); - hook(type => "change", id => "po", call => \&change); + hook(type => "rendered", id => "po", call => \&rendered); hook(type => "checkcontent", id => "po", call => \&checkcontent); hook(type => "canremove", id => "po", call => \&canremove); hook(type => "canrename", id => "po", call => \&canrename); @@ -428,7 +427,7 @@ sub mydelete (@) { map { deletetranslations($_) } grep istranslatablefile($_), @deleted; } -sub change (@) { +sub rendered (@) { my @rendered=@_; my $updated_po_files=0; @@ -1103,7 +1102,7 @@ sub deletetranslations ($) { IkiWiki::rcs_remove($_); } else { - IkiWiki::prune("$config{srcdir}/$_"); + IkiWiki::prune("$config{srcdir}/$_", $config{srcdir}); } } @todelete; diff --git a/IkiWiki/Plugin/poll.pm b/IkiWiki/Plugin/poll.pm index 2773486a6..32756a571 100644 --- a/IkiWiki/Plugin/poll.pm +++ b/IkiWiki/Plugin/poll.pm @@ -23,11 +23,13 @@ sub getsetup () { my %pagenum; sub preprocess (@) { - my %params=(open => "yes", total => "yes", percent => "yes", @_); + my %params=(open => "yes", total => "yes", percent => "yes", + expandable => "no", @_); my $open=IkiWiki::yesno($params{open}); my $showtotal=IkiWiki::yesno($params{total}); my $showpercent=IkiWiki::yesno($params{percent}); + my $expandable=IkiWiki::yesno($params{expandable}); $pagenum{$params{page}}++; my %choices; @@ -74,6 +76,19 @@ sub preprocess (@) { $ret.="</form>\n"; } } + + if ($expandable && $open && exists $config{cgiurl}) { + $ret.="<p>\n"; + $ret.="<form method=\"POST\" action=\"".IkiWiki::cgiurl()."\">\n"; + $ret.="<input type=\"hidden\" name=\"do\" value=\"poll\" />\n"; + $ret.="<input type=\"hidden\" name=\"num\" value=\"$pagenum{$params{page}}\" />\n"; + $ret.="<input type=\"hidden\" name=\"page\" value=\"$params{page}\" />\n"; + $ret.=gettext("Write in").": <input name=\"choice\" size=50 />\n"; + $ret.="<input type=\"submit\" value=\"".gettext("vote")."\" />\n"; + $ret.="</form>\n"; + $ret.="</p>\n"; + } + if ($showtotal) { $ret.="<span>".gettext("Total votes:")." $total</span>\n"; } @@ -85,7 +100,7 @@ sub sessioncgi ($$) { my $session=shift; if (defined $cgi->param('do') && $cgi->param('do') eq "poll") { my $choice=decode_utf8($cgi->param('choice')); - if (! defined $choice) { + if (! defined $choice || not length $choice) { error("no choice specified"); } my $num=$cgi->param('num'); @@ -118,7 +133,14 @@ sub sessioncgi ($$) { my $params=shift; return "\\[[$prefix $params]]" if $escape; if (--$num == 0) { - $params=~s/(^|\s+)(\d+)\s+"?\Q$choice\E"?(\s+|$)/$1.($2+1)." \"$choice\"".$3/se; + if ($params=~s/(^|\s+)(\d+)\s+"?\Q$choice\E"?(\s+|$)/$1.($2+1)." \"$choice\"".$3/se) { + } + elsif ($params=~/expandable=(\w+)/ + & &IkiWiki::yesno($1)) { + $choice=~s/["\]\n\r]//g; + $params.=" 1 \"$choice\"" + if length $choice; + } if (defined $oldchoice) { $params=~s/(^|\s+)(\d+)\s+"?\Q$oldchoice\E"?(\s+|$)/$1.($2-1 >=0 ? $2-1 : 0)." \"$oldchoice\"".$3/se; } diff --git a/IkiWiki/Plugin/recentchanges.pm b/IkiWiki/Plugin/recentchanges.pm index 8ce9474be..4c1863255 100644 --- a/IkiWiki/Plugin/recentchanges.pm +++ b/IkiWiki/Plugin/recentchanges.pm @@ -165,6 +165,7 @@ sub store ($$$) { # Limit pages to first 10, and add links to the changed pages. my $is_excess = exists $change->{pages}[10]; delete @{$change->{pages}}[10 .. @{$change->{pages}}] if $is_excess; + my $has_diffurl=0; $change->{pages} = [ map { if (length $config{cgiurl}) { @@ -180,6 +181,9 @@ sub store ($$$) { else { $_->{link} = pagetitle($_->{page}); } + if (defined $_->{diffurl}) { + $has_diffurl=1; + } $_; } @{$change->{pages}} @@ -227,6 +231,8 @@ sub store ($$$) { wikiname => $config{wikiname}, ); + $template->param(has_diffurl => 1) if $has_diffurl; + $template->param(permalink => urlto($config{recentchangespage})."#change-".titlepage($change->{rev})) if exists $config{url}; diff --git a/IkiWiki/Plugin/recentchangesdiff.pm b/IkiWiki/Plugin/recentchangesdiff.pm index 418822793..eb358be67 100644 --- a/IkiWiki/Plugin/recentchangesdiff.pm +++ b/IkiWiki/Plugin/recentchangesdiff.pm @@ -9,10 +9,12 @@ use HTML::Entities; my $maxlines=200; sub import { + add_underlay("javascript"); hook(type => "getsetup", id => "recentchangesdiff", call => \&getsetup); hook(type => "pagetemplate", id => "recentchangesdiff", call => \&pagetemplate); + hook(type => "format", id => "recentchangesdiff.pm", call => \&format); } sub getsetup () { @@ -55,4 +57,24 @@ sub pagetemplate (@) { } } +sub format (@) { + my %params=@_; + + if (! ($params{content}=~s!^(<body[^>]*>)!$1.include_javascript($params{page})!em)) { + # no <body> tag, probably in preview mode + $params{content}=include_javascript(undef).$params{content}; + } + return $params{content}; +} + +# taken verbatim from toggle.pm +sub include_javascript ($) { + my $from=shift; + + return '<script src="'.urlto("ikiwiki/ikiwiki.js", $from). + '" type="text/javascript" charset="utf-8"></script>'."\n". + '<script src="'.urlto("ikiwiki/toggle.js", $from). + '" type="text/javascript" charset="utf-8"></script>'; +} + 1 diff --git a/IkiWiki/Plugin/remove.pm b/IkiWiki/Plugin/remove.pm index 14ac01c9b..d48b28f95 100644 --- a/IkiWiki/Plugin/remove.pm +++ b/IkiWiki/Plugin/remove.pm @@ -22,6 +22,13 @@ sub getsetup () { }, } +sub allowed_dirs { + return grep { defined $_ } ( + $config{srcdir}, + $IkiWiki::Plugin::transient::transientdir, + ); +} + sub check_canremove ($$$) { my $page=shift; my $q=shift; @@ -33,12 +40,22 @@ sub check_canremove ($$$) { htmllink("", "", $page, noimageinline => 1))); } - # Must exist on disk, and be a regular file. + # Must exist in either the srcdir or a suitable underlay (e.g. + # transient underlay), and be a regular file. my $file=$pagesources{$page}; - if (! -e "$config{srcdir}/$file") { + my $dir; + + foreach my $srcdir (allowed_dirs()) { + if (-e "$srcdir/$file") { + $dir = $srcdir; + last; + } + } + + if (! defined $dir) { error(sprintf(gettext("%s is not in the srcdir, so it cannot be deleted"), $file)); } - elsif (-l "$config{srcdir}/$file" && ! -f _) { + elsif (-l "$dir/$file" && ! -f _) { error(sprintf(gettext("%s is not a file"), $file)); } @@ -46,7 +63,7 @@ sub check_canremove ($$$) { # This is sorta overkill, but better safe than sorry. if (! defined pagetype($pagesources{$page})) { if (IkiWiki::Plugin::attachment->can("check_canattach")) { - IkiWiki::Plugin::attachment::check_canattach($session, $page, "$config{srcdir}/$file"); + IkiWiki::Plugin::attachment::check_canattach($session, $page, "$dir/$file"); } else { error("removal of attachments is not allowed"); @@ -124,7 +141,7 @@ sub removal_confirm ($$@) { my $f=IkiWiki::Plugin::attachment::is_held_attachment($page); if (defined $f) { require IkiWiki::Render; - IkiWiki::prune($f); + IkiWiki::prune($f, "$config{wikistatedir}/attachments"); } } } @@ -223,21 +240,34 @@ sub sessioncgi ($$) { require IkiWiki::Render; if ($config{rcs}) { IkiWiki::disable_commit_hook(); - foreach my $file (@files) { - IkiWiki::rcs_remove($file); + } + my $rcs_removed = 1; + + foreach my $file (@files) { + foreach my $srcdir (allowed_dirs()) { + if (-e "$srcdir/$file") { + if ($srcdir eq $config{srcdir} && $config{rcs}) { + IkiWiki::rcs_remove($file); + $rcs_removed = 1; + } + else { + IkiWiki::prune("$srcdir/$file", $srcdir); + } + } } - IkiWiki::rcs_commit_staged( - message => gettext("removed"), - session => $session, - ); - IkiWiki::enable_commit_hook(); - IkiWiki::rcs_update(); } - else { - foreach my $file (@files) { - IkiWiki::prune("$config{srcdir}/$file"); + + if ($config{rcs}) { + if ($rcs_removed) { + IkiWiki::rcs_commit_staged( + message => gettext("removed"), + session => $session, + ); } + IkiWiki::enable_commit_hook(); + IkiWiki::rcs_update(); } + IkiWiki::refresh(); IkiWiki::saveindex(); diff --git a/IkiWiki/Plugin/rename.pm b/IkiWiki/Plugin/rename.pm index 8e32d41ae..8387a1e32 100644 --- a/IkiWiki/Plugin/rename.pm +++ b/IkiWiki/Plugin/rename.pm @@ -206,14 +206,22 @@ sub rename_start ($$$$) { exit 0; } -sub postrename ($;$$$) { +sub postrename ($$$;$$) { + my $cgi=shift; my $session=shift; my $src=shift; my $dest=shift; my $attachment=shift; - # Load saved form state and return to edit page. - my $postrename=CGI->new($session->param("postrename")); + # Load saved form state and return to edit page, using stored old + # cgi state. Or, if the rename was not started on the edit page, + # return to the renamed page. + my $postrename=$session->param("postrename"); + if (! defined $postrename) { + IkiWiki::redirect($cgi, urlto(defined $dest ? $dest : $src)); + exit; + } + my $oldcgi=CGI->new($postrename); $session->clear("postrename"); IkiWiki::cgi_savesession($session); @@ -222,21 +230,21 @@ sub postrename ($;$$$) { # They renamed the page they were editing. This requires # fixups to the edit form state. # Tweak the edit form to be editing the new page. - $postrename->param("page", $dest); + $oldcgi->param("page", $dest); } # Update edit form content to fix any links present # on it. - $postrename->param("editcontent", + $oldcgi->param("editcontent", renamepage_hook($dest, $src, $dest, - $postrename->param("editcontent"))); + $oldcgi->param("editcontent"))); # Get a new edit token; old was likely invalidated. - $postrename->param("rcsinfo", + $oldcgi->param("rcsinfo", IkiWiki::rcs_prepedit($pagesources{$dest})); } - IkiWiki::cgi_editpage($postrename, $session); + IkiWiki::cgi_editpage($oldcgi, $session); } sub formbuilder (@) { @@ -291,16 +299,16 @@ sub sessioncgi ($$) { my $session=shift; my ($form, $buttons)=rename_form($q, $session, Encode::decode_utf8($q->param("page"))); IkiWiki::decode_form_utf8($form); + my $src=$form->field("page"); if ($form->submitted eq 'Cancel') { - postrename($session); + postrename($q, $session, $src); } elsif ($form->submitted eq 'Rename' && $form->validate) { IkiWiki::checksessionexpiry($q, $session, $q->param('sid')); # These untaints are safe because of the checks # performed in check_canrename later. - my $src=$form->field("page"); my $srcfile=IkiWiki::possibly_foolish_untaint($pagesources{$src}) if exists $pagesources{$src}; my $dest=IkiWiki::possibly_foolish_untaint(titlepage($form->field("new_name"))); @@ -324,7 +332,7 @@ sub sessioncgi ($$) { IkiWiki::Plugin::attachment::is_held_attachment($src); if ($held) { rename($held, IkiWiki::Plugin::attachment::attachment_holding_location($dest)); - postrename($session, $src, $dest, $q->param("attachment")) + postrename($q, $session, $src, $dest, $q->param("attachment")) unless defined $srcfile; } @@ -430,7 +438,7 @@ sub sessioncgi ($$) { $renamesummary.=$template->output; } - postrename($session, $src, $dest, $q->param("attachment")); + postrename($q, $session, $src, $dest, $q->param("attachment")); } else { IkiWiki::showform($form, $buttons, $session, $q); diff --git a/IkiWiki/Plugin/rsync.pm b/IkiWiki/Plugin/rsync.pm index e38801e4a..1b85ea000 100644 --- a/IkiWiki/Plugin/rsync.pm +++ b/IkiWiki/Plugin/rsync.pm @@ -7,7 +7,7 @@ use IkiWiki 3.00; sub import { hook(type => "getsetup", id => "rsync", call => \&getsetup); - hook(type => "change", id => "rsync", call => \&postrefresh); + hook(type => "rendered", id => "rsync", call => \&postrefresh); hook(type => "delete", id => "rsync", call => \&postrefresh); } diff --git a/IkiWiki/Plugin/shortcut.pm b/IkiWiki/Plugin/shortcut.pm index 0cedbe447..98df143ab 100644 --- a/IkiWiki/Plugin/shortcut.pm +++ b/IkiWiki/Plugin/shortcut.pm @@ -73,11 +73,21 @@ sub shortcut_expand ($$@) { add_depends($params{destpage}, "shortcuts"); my $text=join(" ", @params); - my $encoded_text=$text; - $encoded_text=~s/([^A-Za-z0-9])/sprintf("%%%02X", ord($1))/seg; - $url=~s{\%([sS])}{ - $1 eq 's' ? $encoded_text : $text + $url=~s{\%([sSW])}{ + if ($1 eq 's') { + my $t=$text; + $t=~s/([^A-Za-z0-9])/sprintf("%%%02X", ord($1))/seg; + $t; + } + elsif ($1 eq 'S') { + $text; + } + elsif ($1 eq 'W') { + my $t=Encode::encode_utf8($text); + $t=~s/([^A-Za-z0-9])/sprintf("%%%02X", ord($1))/seg; + $t; + } }eg; $text=~s/_/ /g; diff --git a/IkiWiki/Plugin/skeleton.pm.example b/IkiWiki/Plugin/skeleton.pm.example index 7974d5e53..f9caef40c 100644 --- a/IkiWiki/Plugin/skeleton.pm.example +++ b/IkiWiki/Plugin/skeleton.pm.example @@ -26,7 +26,8 @@ sub import { hook(type => "templatefile", id => "skeleton", call => \&templatefile); hook(type => "pageactions", id => "skeleton", call => \&pageactions); hook(type => "delete", id => "skeleton", call => \&delete); - hook(type => "change", id => "skeleton", call => \&change); + hook(type => "rendered", id => "skeleton", call => \&rendered); + hook(type => "changes", id => "skeleton", call => \&changes); hook(type => "cgi", id => "skeleton", call => \&cgi); hook(type => "auth", id => "skeleton", call => \&auth); hook(type => "sessioncgi", id => "skeleton", call => \&sessioncgi); @@ -53,7 +54,6 @@ sub getsetup () { plugin => { safe => 1, rebuild => undef, - section => "misc", }, skeleton => { type => "boolean", @@ -167,10 +167,16 @@ sub delete (@) { debug("skeleton plugin told that files were deleted: @files"); } -sub change (@) { +sub rendered (@) { my @files=@_; - debug("skeleton plugin told that changed files were rendered: @files"); + debug("skeleton plugin told that files were rendered: @files"); +} + +sub changes (@) { + my @files=@_; + + debug("skeleton plugin told that files were changed: @files"); } sub cgi ($) { diff --git a/IkiWiki/Plugin/svn.pm b/IkiWiki/Plugin/svn.pm index 8824a6ce0..fd11f2c63 100644 --- a/IkiWiki/Plugin/svn.pm +++ b/IkiWiki/Plugin/svn.pm @@ -5,6 +5,7 @@ use warnings; use strict; use IkiWiki; use POSIX qw(setlocale LC_CTYPE); +use URI::Escape q{uri_escape_utf8}; sub import { hook(type => "checkconfig", id => "svn", call => \&checkconfig); @@ -292,7 +293,8 @@ sub rcs_recentchanges ($) { } my $diffurl=defined $config{diffurl} ? $config{diffurl} : ""; - $diffurl=~s/\[\[file\]\]/$file/g; + my $efile = uri_escape_utf8($file); + $diffurl=~s/\[\[file\]\]/$efile/g; $diffurl=~s/\[\[r1\]\]/$rev - 1/eg; $diffurl=~s/\[\[r2\]\]/$rev/g; diff --git a/IkiWiki/Plugin/tla.pm b/IkiWiki/Plugin/tla.pm index da4385446..11be248e8 100644 --- a/IkiWiki/Plugin/tla.pm +++ b/IkiWiki/Plugin/tla.pm @@ -4,6 +4,7 @@ package IkiWiki::Plugin::tla; use warnings; use strict; use IkiWiki; +use URI::Escape q{uri_escape_utf8}; sub import { hook(type => "checkconfig", id => "tla", call => \&checkconfig); @@ -224,7 +225,8 @@ sub rcs_recentchanges ($) { foreach my $file (@paths) { my $diffurl=defined $config{diffurl} ? $config{diffurl} : ""; - $diffurl=~s/\[\[file\]\]/$file/g; + my $efile = uri_escape_utf8($file); + $diffurl=~s/\[\[file\]\]/$efile/g; $diffurl=~s/\[\[rev\]\]/$change/g; push @pages, { page => pagename($file), diff --git a/IkiWiki/Plugin/trail.pm b/IkiWiki/Plugin/trail.pm new file mode 100644 index 000000000..d5fb2b5d6 --- /dev/null +++ b/IkiWiki/Plugin/trail.pm @@ -0,0 +1,467 @@ +#!/usr/bin/perl +# Copyright © 2008-2011 Joey Hess +# Copyright © 2009-2012 Simon McVittie <http://smcv.pseudorandom.co.uk/> +# Licensed under the GNU GPL, version 2, or any later version published by the +# Free Software Foundation +package IkiWiki::Plugin::trail; + +use warnings; +use strict; +use IkiWiki 3.00; + +sub import { + hook(type => "getsetup", id => "trail", call => \&getsetup); + hook(type => "needsbuild", id => "trail", call => \&needsbuild); + hook(type => "preprocess", id => "trailoptions", call => \&preprocess_trailoptions, scan => 1); + hook(type => "preprocess", id => "trailitem", call => \&preprocess_trailitem, scan => 1); + hook(type => "preprocess", id => "trailitems", call => \&preprocess_trailitems, scan => 1); + hook(type => "preprocess", id => "traillink", call => \&preprocess_traillink, scan => 1); + hook(type => "pagetemplate", id => "trail", call => \&pagetemplate); + hook(type => "build_affected", id => "trail", call => \&build_affected); +} + +# Page state +# +# If a page $T is a trail, then it can have +# +# * $pagestate{$T}{trail}{contents} +# Reference to an array of lists each containing either: +# - [pagenames => "page1", "page2"] +# Those literal pages +# - [link => "link"] +# A link specification, pointing to the same page that [[link]] +# would select +# - [pagespec => "posts/*", "age", 0] +# A match by pagespec; the third array element is the sort order +# and the fourth is whether to reverse sorting +# +# * $pagestate{$T}{trail}{sort} +# A sorting order; if absent or undef, the trail is in the order given +# by the links that form it +# +# * $pagestate{$T}{trail}{circular} +# True if this trail is circular (i.e. going "next" from the last item is +# allowed, and takes you back to the first) +# +# * $pagestate{$T}{trail}{reverse} +# True if C<sort> is to be reversed. +# +# If a page $M is a member of a trail $T, then it has +# +# * $pagestate{$M}{trail}{item}{$T}[0] +# The page before this one in C<$T> at the last rebuild, or undef. +# +# * $pagestate{$M}{trail}{item}{$T}[1] +# The page after this one in C<$T> at the last refresh, or undef. + +sub getsetup () { + return + plugin => { + safe => 1, + rebuild => undef, + }, +} + +# Cache of pages' old titles, so we can tell whether they changed +my %old_trail_titles; + +sub needsbuild (@) { + my $needsbuild=shift; + + foreach my $page (keys %pagestate) { + if (exists $pagestate{$page}{trail}) { + if (exists $pagesources{$page} && + grep { $_ eq $pagesources{$page} } @$needsbuild) { + # Remember its title, so we can know whether + # it changed. + $old_trail_titles{$page} = title_of($page); + + # Remove state, it will be re-added + # if the preprocessor directive is still + # there during the rebuild. {item} is the + # only thing that's added for items, not + # trails, and it's harmless to delete that - + # the item is being rebuilt anyway. + delete $pagestate{$page}{trail}; + } + } + } + + return $needsbuild; +} + +my $scanned = 0; + +sub preprocess_trailoptions (@) { + my %params = @_; + + if (exists $params{circular}) { + $pagestate{$params{page}}{trail}{circular} = + IkiWiki::yesno($params{circular}); + } + + if (exists $params{sort}) { + $pagestate{$params{page}}{trail}{sort} = $params{sort}; + } + + if (exists $params{reverse}) { + $pagestate{$params{page}}{trail}{reverse} = $params{reverse}; + } + + return ""; +} + +sub preprocess_trailitem (@) { + my $link = shift; + shift; + + # avoid collecting everything in the preprocess stage if we already + # did in the scan stage + if (defined wantarray) { + return "" if $scanned; + } + else { + $scanned = 1; + } + + my %params = @_; + my $trail = $params{page}; + + $link = linkpage($link); + + add_link($params{page}, $link, 'trail'); + push @{$pagestate{$params{page}}{trail}{contents}}, [link => $link]; + + return ""; +} + +sub preprocess_trailitems (@) { + my %params = @_; + + # avoid collecting everything in the preprocess stage if we already + # did in the scan stage + if (defined wantarray) { + return "" if $scanned; + } + else { + $scanned = 1; + } + + # trail members from a pagespec ought to be in some sort of order, + # and path is a nice obvious default + $params{sort} = 'path' unless exists $params{sort}; + $params{reverse} = 'no' unless exists $params{reverse}; + + if (exists $params{pages}) { + push @{$pagestate{$params{page}}{trail}{contents}}, + ["pagespec" => $params{pages}, $params{sort}, + IkiWiki::yesno($params{reverse})]; + } + + if (exists $params{pagenames}) { + push @{$pagestate{$params{page}}{trail}{contents}}, + [pagenames => (split ' ', $params{pagenames})]; + } + + return ""; +} + +sub preprocess_traillink (@) { + my $link = shift; + shift; + + my %params = @_; + my $trail = $params{page}; + + $link =~ qr{ + (?: + ([^\|]+) # 1: link text + \| # followed by | + )? # optional + + (.+) # 2: page to link to + }x; + + my $linktext = $1; + $link = linkpage($2); + + add_link($params{page}, $link, 'trail'); + + # avoid collecting everything in the preprocess stage if we already + # did in the scan stage + my $already; + if (defined wantarray) { + $already = $scanned; + } + else { + $scanned = 1; + } + + push @{$pagestate{$params{page}}{trail}{contents}}, [link => $link] unless $already; + + if (defined $linktext) { + $linktext = pagetitle($linktext); + } + + if (exists $params{text}) { + $linktext = $params{text}; + } + + if (defined $linktext) { + return htmllink($trail, $params{destpage}, + $link, linktext => $linktext); + } + + return htmllink($trail, $params{destpage}, $link); +} + +# trail => [member1, member2] +my %trail_to_members; +# member => { trail => [prev, next] } +# e.g. if %trail_to_members = ( +# trail1 => ["member1", "member2"], +# trail2 => ["member0", "member1"], +# ) +# +# then $member_to_trails{member1} = { +# trail1 => [undef, "member2"], +# trail2 => ["member0", undef], +# } +my %member_to_trails; + +# member => 1 +my %rebuild_trail_members; + +sub trails_differ { + my ($old, $new) = @_; + + foreach my $trail (keys %$old) { + if (! exists $new->{$trail}) { + return 1; + } + + if (exists $old_trail_titles{$trail} && + title_of($trail) ne $old_trail_titles{$trail}) { + return 1; + } + + my ($old_p, $old_n) = @{$old->{$trail}}; + my ($new_p, $new_n) = @{$new->{$trail}}; + $old_p = "" unless defined $old_p; + $old_n = "" unless defined $old_n; + $new_p = "" unless defined $new_p; + $new_n = "" unless defined $new_n; + if ($old_p ne $new_p) { + return 1; + } + + if (exists $old_trail_titles{$old_p} && + title_of($old_p) ne $old_trail_titles{$old_p}) { + return 1; + } + + if ($old_n ne $new_n) { + return 1; + } + + if (exists $old_trail_titles{$old_n} && + title_of($old_n) ne $old_trail_titles{$old_n}) { + return 1; + } + } + + foreach my $trail (keys %$new) { + if (! exists $old->{$trail}) { + return 1; + } + } + + return 0; +} + +my $done_prerender = 0; + +sub prerender { + return if $done_prerender; + + %trail_to_members = (); + %member_to_trails = (); + + foreach my $trail (keys %pagestate) { + next unless exists $pagestate{$trail}{trail}{contents}; + + my $members = []; + my @contents = @{$pagestate{$trail}{trail}{contents}}; + + foreach my $c (@contents) { + if ($c->[0] eq 'pagespec') { + push @$members, pagespec_match_list($trail, + $c->[1], sort => $c->[2], + reverse => $c->[3]); + } + elsif ($c->[0] eq 'pagenames') { + my @pagenames = @$c; + shift @pagenames; + foreach my $page (@pagenames) { + if (exists $pagesources{$page}) { + push @$members, $page; + } + else { + # rebuild trail if it turns up + add_depends($trail, $page, deptype("presence")); + } + } + } + elsif ($c->[0] eq 'link') { + my $best = bestlink($trail, $c->[1]); + push @$members, $best if length $best; + } + } + + if (defined $pagestate{$trail}{trail}{sort}) { + # re-sort + @$members = pagespec_match_list($trail, 'internal(*)', + list => $members, + sort => $pagestate{$trail}{trail}{sort}); + } + + if (IkiWiki::yesno $pagestate{$trail}{trail}{reverse}) { + @$members = reverse @$members; + } + + # uniquify + my %seen; + my @tmp; + foreach my $member (@$members) { + push @tmp, $member unless $seen{$member}; + $seen{$member} = 1; + } + $members = [@tmp]; + + for (my $i = 0; $i <= $#$members; $i++) { + my $member = $members->[$i]; + my $prev; + $prev = $members->[$i - 1] if $i > 0; + my $next = $members->[$i + 1]; + + $member_to_trails{$member}{$trail} = [$prev, $next]; + } + + if ((scalar @$members) > 1 && $pagestate{$trail}{trail}{circular}) { + $member_to_trails{$members->[0]}{$trail}[0] = $members->[$#$members]; + $member_to_trails{$members->[$#$members]}{$trail}[1] = $members->[0]; + } + + $trail_to_members{$trail} = $members; + } + + foreach my $member (keys %pagestate) { + if (exists $pagestate{$member}{trail}{item} && + ! exists $member_to_trails{$member}) { + $rebuild_trail_members{$member} = 1; + delete $pagestate{$member}{trail}{item}; + } + } + + foreach my $member (keys %member_to_trails) { + if (! exists $pagestate{$member}{trail}{item}) { + $rebuild_trail_members{$member} = 1; + } + else { + if (trails_differ($pagestate{$member}{trail}{item}, + $member_to_trails{$member})) { + $rebuild_trail_members{$member} = 1; + } + } + + $pagestate{$member}{trail}{item} = $member_to_trails{$member}; + } + + $done_prerender = 1; +} + +sub build_affected { + my %affected; + + # In principle we might not have done this yet, although in practice + # at least the trail itself has probably changed, and its template + # almost certainly contains TRAILS or TRAILLOOP, triggering our + # prerender as a side-effect. + prerender(); + + foreach my $member (keys %rebuild_trail_members) { + $affected{$member} = sprintf(gettext("building %s, its previous or next page has changed"), $member); + } + + return %affected; +} + +sub title_of ($) { + my $page = shift; + if (defined ($pagestate{$page}{meta}{title})) { + return $pagestate{$page}{meta}{title}; + } + return pagetitle(IkiWiki::basename($page)); +} + +my $recursive = 0; + +sub pagetemplate (@) { + my %params = @_; + my $page = $params{page}; + my $template = $params{template}; + + return unless length $page; + + if ($template->query(name => 'trails') && ! $recursive) { + prerender(); + + $recursive = 1; + my $inner = template("trails.tmpl", blind_cache => 1); + IkiWiki::run_hooks(pagetemplate => sub { + shift->(%params, template => $inner) + }); + $template->param(trails => $inner->output); + $recursive = 0; + } + + if ($template->query(name => 'trailloop')) { + prerender(); + + my @trails; + + # sort backlinks by page name to have a consistent order + foreach my $trail (sort keys %{$member_to_trails{$page}}) { + + my $members = $trail_to_members{$trail}; + my ($prev, $next) = @{$member_to_trails{$page}{$trail}}; + my ($prevurl, $nexturl, $prevtitle, $nexttitle); + + if (defined $prev) { + $prevurl = urlto($prev, $page); + $prevtitle = title_of($prev); + } + + if (defined $next) { + $nexturl = urlto($next, $page); + $nexttitle = title_of($next); + } + + push @trails, { + prevpage => $prev, + prevtitle => $prevtitle, + prevurl => $prevurl, + nextpage => $next, + nexttitle => $nexttitle, + nexturl => $nexturl, + trailpage => $trail, + trailtitle => title_of($trail), + trailurl => urlto($trail, $page), + }; + } + + $template->param(trailloop => \@trails); + } +} + +1; diff --git a/IkiWiki/Plugin/transient.pm b/IkiWiki/Plugin/transient.pm index c0ad5fc11..d4eb005ea 100644 --- a/IkiWiki/Plugin/transient.pm +++ b/IkiWiki/Plugin/transient.pm @@ -8,7 +8,7 @@ use IkiWiki 3.00; sub import { hook(type => "getsetup", id => "transient", call => \&getsetup); hook(type => "checkconfig", id => "transient", call => \&checkconfig); - hook(type => "change", id => "transient", call => \&change); + hook(type => "rendered", id => "transient", call => \&rendered); } sub getsetup () { @@ -33,7 +33,7 @@ sub checkconfig () { } } -sub change (@) { +sub rendered (@) { foreach my $file (@_) { # If the corresponding file exists in the transient underlay # and isn't actually being used, we can get rid of it. @@ -43,7 +43,7 @@ sub change (@) { my $casualty = "$transientdir/$file"; if (srcfile($file) ne $casualty && -e $casualty) { debug(sprintf(gettext("removing transient version of %s"), $file)); - IkiWiki::prune($casualty); + IkiWiki::prune($casualty, $transientdir); } } } diff --git a/IkiWiki/Render.pm b/IkiWiki/Render.pm index 05132a8a8..a90d202ee 100644 --- a/IkiWiki/Render.pm +++ b/IkiWiki/Render.pm @@ -262,12 +262,13 @@ sub render ($$) { } } -sub prune ($) { +sub prune ($;$) { my $file=shift; + my $up_to=shift; unlink($file); my $dir=dirname($file); - while (rmdir($dir)) { + while ((! defined $up_to || $dir =~ m{^\Q$up_to\E\/}) && rmdir($dir)) { $dir=dirname($dir); } } @@ -447,7 +448,7 @@ sub remove_del (@) { } foreach my $old (@{$oldrenderedfiles{$page}}) { - prune($config{destdir}."/".$old); + prune($config{destdir}."/".$old, $config{destdir}); } foreach my $source (keys %destsources) { @@ -537,7 +538,7 @@ sub remove_unrendered () { foreach my $file (@{$oldrenderedfiles{$page}}) { if (! grep { $_ eq $file } @{$renderedfiles{$page}}) { debug(sprintf(gettext("removing %s, no longer built by %s"), $file, $page)); - prune($config{destdir}."/".$file); + prune($config{destdir}."/".$file, $config{destdir}); } } } @@ -800,6 +801,14 @@ sub refresh () { derender_internal($file); } + run_hooks(build_affected => sub { + my %affected = shift->(); + while (my ($page, $message) = each %affected) { + next unless exists $pagesources{$page}; + render($pagesources{$page}, $message); + } + }); + my ($backlinkchanged, $linkchangers)=calculate_changed_links($changed, $del, $oldlink_targets); @@ -821,8 +830,13 @@ sub refresh () { run_hooks(delete => sub { shift->(@$del, @$internal_del) }); } if (%rendered) { - run_hooks(change => sub { shift->(keys %rendered) }); + run_hooks(rendered => sub { shift->(keys %rendered) }); + run_hooks(change => sub { shift->(keys %rendered) }); # back-compat } + my %all_changed = map { $_ => 1 } + @$new, @$changed, @$del, + @$internal_new, @$internal_changed, @$internal_del; + run_hooks(changes => sub { shift->(keys %all_changed) }); } sub clean_rendered { @@ -831,7 +845,7 @@ sub clean_rendered { remove_unrendered(); foreach my $page (keys %oldrenderedfiles) { foreach my $file (@{$oldrenderedfiles{$page}}) { - prune($config{destdir}."/".$file); + prune($config{destdir}."/".$file, $config{destdir}); } } } diff --git a/IkiWiki/Wrapper.pm b/IkiWiki/Wrapper.pm index c39aa2ef7..06be36dfc 100644 --- a/IkiWiki/Wrapper.pm +++ b/IkiWiki/Wrapper.pm @@ -93,12 +93,53 @@ EOF # memory, a pile up of processes could cause thrashing # otherwise. The fd of the lock is stored in # IKIWIKI_CGILOCK_FD so unlockwiki can close it. - $pre_exec=<<"EOF"; + # + # A lot of cgi wrapper processes can potentially build + # up and clog an otherwise unloaded web server. To + # partially avoid this, when a GET comes in and the lock + # is already held, rather than blocking a html page is + # constructed that retries. This is enabled by setting + # cgi_overload_delay. + if (defined $config{cgi_overload_delay} && + $config{cgi_overload_delay} =~/^[0-9]+/) { + my $i=int($config{cgi_overload_delay}); + $pre_exec.="#define CGI_OVERLOAD_DELAY $i\n" + if $i > 0; + my $msg=gettext("Please wait"); + $msg=~s/"/\\"/g; + $pre_exec.='#define CGI_PLEASE_WAIT_TITLE "'.$msg."\"\n"; + if (defined $config{cgi_overload_message} && length $config{cgi_overload_message}) { + $msg=$config{cgi_overload_message}; + $msg=~s/"/\\"/g; + } + $pre_exec.='#define CGI_PLEASE_WAIT_BODY "'.$msg."\"\n"; + } + $pre_exec.=<<"EOF"; lockfd=open("$config{wikistatedir}/cgilock", O_CREAT | O_RDWR, 0666); - if (lockfd != -1 && lockf(lockfd, F_LOCK, 0) == 0) { - char *fd_s=malloc(8); - sprintf(fd_s, "%i", lockfd); - setenv("IKIWIKI_CGILOCK_FD", fd_s, 1); + if (lockfd != -1) { +#ifdef CGI_OVERLOAD_DELAY + char *request_method = getenv("REQUEST_METHOD"); + if (request_method && strcmp(request_method, "GET") == 0) { + if (lockf(lockfd, F_TLOCK, 0) == 0) { + set_cgilock_fd(lockfd); + } + else { + printf("Content-Type: text/html\\nRefresh: %i; URL=%s\\n\\n<html><head><title>%s</title><head><body><p>%s</p></body></html>", + CGI_OVERLOAD_DELAY, + getenv("REQUEST_URI"), + CGI_PLEASE_WAIT_TITLE, + CGI_PLEASE_WAIT_BODY); + exit(0); + } + } + else if (lockf(lockfd, F_LOCK, 0) == 0) { + set_cgilock_fd(lockfd); + } +#else + if (lockf(lockfd, F_LOCK, 0) == 0) { + set_cgilock_fd(lockfd); + } +#endif } EOF } @@ -140,6 +181,12 @@ void addenv(char *var, char *val) { newenviron[i++]=s; } +set_cgilock_fd (int lockfd) { + char *fd_s=malloc(8); + sprintf(fd_s, "%i", lockfd); + setenv("IKIWIKI_CGILOCK_FD", fd_s, 1); +} + int main (int argc, char **argv) { int lockfd=-1; char *s; @@ -214,7 +261,7 @@ $set_background_command EOF my @cc=exists $ENV{CC} ? possibly_foolish_untaint($ENV{CC}) : 'cc'; - push @cc, possibly_foolish_untaint($ENV{CFLAGS}) if exists $ENV{CFLAGS}; + push @cc, split(' ', possibly_foolish_untaint($ENV{CFLAGS})) if exists $ENV{CFLAGS}; if (system(@cc, "$wrapper.c", "-o", "$wrapper.new") != 0) { #translators: The parameter is a C filename. error(sprintf(gettext("failed to compile %s"), "$wrapper.c")); |