diff --git a/IkiWiki.pm b/IkiWiki.pm index 67cd2147d..bab7b707a 100644 --- a/IkiWiki.pm +++ b/IkiWiki.pm @@ -21,6 +21,7 @@ our @EXPORT = qw(hook debug error template htmlpage add_depends pagespec_match bestlink htmllink readfile writefile pagetype srcfile pagename displaytime will_render gettext urlto targetpage add_underlay pagetitle titlepage linkpage newpagefile + inject %config %links %pagestate %wikistate %renderedfiles %pagesources %destsources); our $VERSION = 2.00; # plugin interface version, next is ikiwiki version @@ -381,6 +382,13 @@ sub getsetup () { #{{{ safe => 0, rebuild => 0, }, + test_receive => { + type => "internal", + default => 0, + description => "running in receive test mode", + safe => 0, + rebuild => 0, + }, getctime => { type => "internal", default => 0, @@ -403,7 +411,7 @@ sub getsetup () { #{{{ rebuild => 0, }, allow_symlinks_before_srcdir => { - type => "string", + type => "boolean", default => 0, description => "allow symlinks in the path leading to the srcdir (potentially insecure)", safe => 0, @@ -647,19 +655,8 @@ sub newpagefile ($$) { #{{{ sub targetpage ($$) { #{{{ my $page=shift; my $ext=shift; - - my $targetpage=''; - run_hooks(targetpage => sub { - $targetpage=shift->( - page => $page, - ext => $ext, - ); - }); - - if (defined $targetpage && (length($targetpage) > 0)) { - return $targetpage; - } - elsif (! $config{usedirs} || $page eq 'index') { + + if (! $config{usedirs} || $page eq 'index') { return $page.".".$ext; } else { @@ -807,7 +804,6 @@ sub will_render ($$;$) { #{{{ sub bestlink ($$) { #{{{ my $page=shift; my $link=shift; - my $res=undef; my $cwd=$page; if ($link=~s/^\/+//) { @@ -822,35 +818,25 @@ sub bestlink ($$) { #{{{ $l.=$link; if (exists $links{$l}) { - $res=$l; + return $l; } elsif (exists $pagecase{lc $l}) { - $res=$pagecase{lc $l}; + return $pagecase{lc $l}; } - } while ($cwd=~s{/?[^/]+$}{} && ! defined $res); + } while $cwd=~s{/?[^/]+$}{}; - if (! defined $res && length $config{userdir}) { + if (length $config{userdir}) { my $l = "$config{userdir}/".lc($link); if (exists $links{$l}) { - $res=$l; + return $l; } elsif (exists $pagecase{lc $l}) { - $res=$pagecase{lc $l}; + return $pagecase{lc $l}; } } - if (defined $res) { - run_hooks(tweakbestlink => sub { - $res=shift->( - page => $page, - link => $res); - }); - return $res; - } - else { - #print STDERR "warning: page $page, broken link: $link\n"; - return ""; - } + #print STDERR "warning: page $page, broken link: $link\n"; + return ""; } #}}} sub isinlinableimage ($) { #{{{ @@ -920,23 +906,13 @@ sub abs2rel ($$) { #{{{ } #}}} sub displaytime ($;$) { #{{{ - my $time=shift; - my $format=shift; - if (exists $hooks{displaytime}) { - my $ret; - run_hooks(displaytime => sub { - $ret=shift->($time, $format) - }); - return $ret; - } - else { - return formattime($time, $format); - } + # Plugins can override this function to mark up the time to + # display. + return ''.formattime(@_).''; } #}}} sub formattime ($;$) { #{{{ - # Plugins can override this function to mark up the time for - # display. + # Plugins can override this function to format the time. my $time=shift; my $format=shift; if (! defined $format) { @@ -955,10 +931,6 @@ sub beautify_urlpath ($) { #{{{ $url =~ s!/index.$config{htmlext}$!/!; } - run_hooks(tweakurlpath => sub { - $url=shift->(url => $url); - }); - # Ensure url is not an empty link, and # if it's relative, make that explicit to avoid colon confusion. if ($url !~ /^\//) { @@ -1610,6 +1582,10 @@ sub rcs_getctime ($) { #{{{ $hooks{rcs}{rcs_getctime}{call}->(@_); } #}}} +sub rcs_receive () { #{{{ + $hooks{rcs}{rcs_receive}{call}->(); +} #}}} + sub globlist_to_pagespec ($) { #{{{ my @globlist=split(' ', shift); @@ -1702,6 +1678,31 @@ sub yesno ($) { #{{{ return (defined $val && lc($val) eq gettext("yes")); } #}}} +sub inject { #{{{ + # Injects a new function into the symbol table to replace an + # exported function. + my %params=@_; + + # This is deep ugly perl foo, beware. + no strict; + no warnings; + if (! defined $params{parent}) { + $params{parent}='::'; + $params{old}=\&{$params{name}}; + $params{name}=~s/.*:://; + } + my $parent=$params{parent}; + foreach my $ns (grep /^\w+::/, keys %{$parent}) { + $ns = $params{parent} . $ns; + inject(%params, parent => $ns) unless $ns eq '::main::'; + *{$ns . $params{name}} = $params{call} + if exists ${$ns}{$params{name}} && + \&{${$ns}{$params{name}}} == $params{old}; + } + use strict; + use warnings; +} #}}} + sub pagespec_merge ($$) { #{{{ my $a=shift; my $b=shift; @@ -1796,7 +1797,7 @@ sub pagespec_valid ($) { #{{{ my $sub=pagespec_translate($spec); return ! $@; } #}}} - + sub glob2re ($) { #{{{ my $re=quotemeta(shift); $re=~s/\\\*/.*/g; diff --git a/IkiWiki/Plugin/editpage.pm b/IkiWiki/Plugin/editpage.pm index 30c93df20..fe2864bac 100644 --- a/IkiWiki/Plugin/editpage.pm +++ b/IkiWiki/Plugin/editpage.pm @@ -122,7 +122,7 @@ sub cgi_editpage ($$) { #{{{ my $absolute=($page =~ s#^/+##); if (! defined $page || ! length $page || file_pruned($page, $config{srcdir})) { - error("bad page name"); + error(gettext("bad page name")); } my $baseurl = urlto($page, undef, 1); diff --git a/IkiWiki/Plugin/external.pm b/IkiWiki/Plugin/external.pm index ba6c7d8b9..4ce9c8bab 100644 --- a/IkiWiki/Plugin/external.pm +++ b/IkiWiki/Plugin/external.pm @@ -202,8 +202,16 @@ sub inject ($@) { #{{{ my $sub = sub { IkiWiki::Plugin::external::rpc_call($plugin, $params{call}, @_) }; + $sub=memoize($sub) if $params{memoize}; + + # This will add it to the symbol table even if not present. + no warnings; eval qq{*$params{name}=\$sub}; - memoize($params{name}) if $params{memoize}; + use warnings; + + # This will ensure that everywhere it was exported to sees + # the injected version. + IkiWiki::inject(name => $params{name}, call => $sub); return 1; } #}}} diff --git a/IkiWiki/Plugin/format.pm b/IkiWiki/Plugin/format.pm new file mode 100644 index 000000000..a219190e8 --- /dev/null +++ b/IkiWiki/Plugin/format.pm @@ -0,0 +1,29 @@ +#!/usr/bin/perl +package IkiWiki::Plugin::format; + +use warnings; +use strict; +use IkiWiki 2.00; + +sub import { #{{{ + hook(type => "preprocess", id => "format", call => \&preprocess); +} #}}} + +sub preprocess (@) { #{{{ + my $format=$_[0]; + shift; shift; + my $text=$_[0]; + shift; shift; + my %params=@_; + + if (! defined $format || ! defined $text) { + error(gettext("must specify format and text")); + } + elsif (! exists $IkiWiki::hooks{htmlize}{$format}) { + error(sprintf(gettext("unsupported page format %s"), $format)); + } + + return IkiWiki::htmlize($params{page}, $params{destpage}, $format, $text); +} #}}} + +1 diff --git a/IkiWiki/Plugin/git.pm b/IkiWiki/Plugin/git.pm index 14b0ab285..1a39d87e5 100644 --- a/IkiWiki/Plugin/git.pm +++ b/IkiWiki/Plugin/git.pm @@ -9,6 +9,7 @@ use open qw{:utf8 :std}; my $sha1_pattern = qr/[0-9a-fA-F]{40}/; # pattern to validate Git sha1sums my $dummy_commit_msg = 'dummy commit'; # message to skip in recent changes +my $no_chdir=0; sub import { #{{{ hook(type => "checkconfig", id => "git", call => \&checkconfig); @@ -23,6 +24,7 @@ sub import { #{{{ hook(type => "rcs", id => "rcs_recentchanges", call => \&rcs_recentchanges); hook(type => "rcs", id => "rcs_diff", call => \&rcs_diff); hook(type => "rcs", id => "rcs_getctime", call => \&rcs_getctime); + hook(type => "rcs", id => "rcs_receive", call => \&rcs_receive); } #}}} sub checkconfig () { #{{{ @@ -32,12 +34,21 @@ sub checkconfig () { #{{{ if (! defined $config{gitmaster_branch}) { $config{gitmaster_branch}="master"; } - if (defined $config{git_wrapper} && length $config{git_wrapper}) { + if (defined $config{git_wrapper} && + length $config{git_wrapper}) { push @{$config{wrappers}}, { wrapper => $config{git_wrapper}, wrappermode => (defined $config{git_wrappermode} ? $config{git_wrappermode} : "06755"), }; } + if (defined $config{git_test_receive_wrapper} && + length $config{git_test_receive_wrapper}) { + push @{$config{wrappers}}, { + test_receive => 1, + wrapper => $config{git_test_receive_wrapper}, + wrappermode => (defined $config{git_wrappermode} ? $config{git_wrappermode} : "06755"), + }; + } } #}}} sub getsetup () { #{{{ @@ -60,6 +71,20 @@ sub getsetup () { #{{{ safe => 0, rebuild => 0, }, + git_test_receive_wrapper => { + type => "string", + example => "/git/wiki.git/hooks/pre-receive", + description => "git pre-receive hook to generate", + safe => 0, # file + rebuild => 0, + }, + untrusted_committers => { + type => "string", + example => [], + description => "unix users whose commits should be checked by the pre-receive hook", + safe => 0, + rebuild => 0, + }, historyurl => { type => "string", example => "http://git.example.com/gitweb.cgi?p=wiki.git;a=history;f=[[file]]", @@ -70,7 +95,7 @@ sub getsetup () { #{{{ diffurl => { type => "string", example => "http://git.example.com/gitweb.cgi?p=wiki.git;a=blobdiff;h=[[sha1_to]];hp=[[sha1_from]];hb=[[sha1_parent]];f=[[file]]", - description => "gitweb url to show a diff ([[sha1_to]], [[sha1_from]], [[sha1_parent]], and [[file]] substituted)", + description => "gitweb url to show a diff ([[sha1_to]], [[sha1_from]], [[sha1_parent]], [[sha1_commit]] and [[file]] substituted)", safe => 1, rebuild => 1, }, @@ -103,8 +128,10 @@ sub safe_git (&@) { #{{{ if (!$pid) { # In child. # Git commands want to be in wc. - chdir $config{srcdir} - or error("Cannot chdir to $config{srcdir}: $!"); + if (! $no_chdir) { + chdir $config{srcdir} + or error("Cannot chdir to $config{srcdir}: $!"); + } exec @cmdline or error("Cannot exec '@cmdline': $!"); } # In parent. @@ -320,6 +347,9 @@ sub parse_diff_tree ($@) { #{{{ 'file' => decode("utf8", $file), 'sha1_from' => $sha1_from[0], 'sha1_to' => $sha1_to, + 'mode_from' => $mode_from[0], + 'mode_to' => $mode_to, + 'status' => $status, }; } next; @@ -331,14 +361,14 @@ sub parse_diff_tree ($@) { #{{{ } #}}} sub git_commit_info ($;$) { #{{{ - # Return an array of commit info hashes of num commits (default: 1) + # Return an array of commit info hashes of num commits # starting from the given sha1sum. - my ($sha1, $num) = @_; - $num ||= 1; + my @opts; + push @opts, "--max-count=$num" if defined $num; - my @raw_lines = run_or_die('git', 'log', "--max-count=$num", + my @raw_lines = run_or_die('git', 'log', @opts, '--pretty=raw', '--raw', '--abbrev=40', '--always', '-c', '-r', $sha1, '--', '.'); my ($prefix) = run_or_die('git', 'rev-parse', '--show-prefix'); @@ -355,7 +385,6 @@ sub git_commit_info ($;$) { #{{{ sub git_sha1 (;$) { #{{{ # Return head sha1sum (of given file). - my $file = shift || q{--}; # Ignore error since a non-existing file might be given. @@ -378,7 +407,6 @@ sub rcs_update () { #{{{ sub rcs_prepedit ($) { #{{{ # Return the commit sha1sum of the file when editing begins. # This will be later used in rcs_commit if a merge is required. - my ($file) = @_; return git_sha1($file); @@ -475,7 +503,7 @@ sub rcs_recentchanges ($) { #{{{ error($@) if $@; my @rets; - foreach my $ci (git_commit_info('HEAD', $num)) { + foreach my $ci (git_commit_info('HEAD', $num || 1)) { # Skip redundant commits. next if ($ci->{'comment'} && @{$ci->{'comment'}}[0] eq $dummy_commit_msg); @@ -493,6 +521,7 @@ sub rcs_recentchanges ($) { #{{{ $diffurl =~ s/\[\[sha1_parent\]\]/$ci->{'parent'}/go; $diffurl =~ s/\[\[sha1_from\]\]/$detail->{'sha1_from'}/go; $diffurl =~ s/\[\[sha1_to\]\]/$detail->{'sha1_to'}/go; + $diffurl =~ s/\[\[sha1_commit\]\]/$sha1/go; push @pages, { page => pagename($file), @@ -558,11 +587,104 @@ sub rcs_getctime ($) { #{{{ $file =~ s/^\Q$config{srcdir}\E\/?//; my $sha1 = git_sha1($file); - my $ci = git_commit_info($sha1); + my $ci = git_commit_info($sha1, 1); my $ctime = $ci->{'author_epoch'}; debug("ctime for '$file': ". localtime($ctime)); return $ctime; } #}}} +sub rcs_receive () { #{{{ + # The wiki may not be the only thing in the git repo. + # Determine if it is in a subdirectory by examining the srcdir, + # and its parents, looking for the .git directory. + my $subdir=""; + my $dir=$config{srcdir}; + while (! -d "$dir/.git") { + $subdir=IkiWiki::basename($dir)."/".$subdir; + $dir=IkiWiki::dirname($dir); + if (! length $dir) { + error("cannot determine root of git repo"); + } + } + + my @rets; + while (<>) { + chomp; + my ($oldrev, $newrev, $refname) = split(' ', $_, 3); + + # only allow changes to gitmaster_branch + if ($refname !~ /^refs\/heads\/\Q$config{gitmaster_branch}\E$/) { + error sprintf(gettext("you are not allowed to change %s"), $refname); + } + + # Avoid chdir when running git here, because the changes + # are in the master git repo, not the srcdir repo. + # The pre-recieve hook already puts us in the right place. + $no_chdir=1; + my @changes=git_commit_info($oldrev."..".$newrev); + $no_chdir=0; + + foreach my $ci (@changes) { + foreach my $detail (@{ $ci->{'details'} }) { + my $file = $detail->{'file'}; + + # check that all changed files are in the + # subdir + if (length $subdir && + ! ($file =~ s/^\Q$subdir\E//)) { + error sprintf(gettext("you are not allowed to change %s"), $file); + } + + my ($action, $mode, $path); + if ($detail->{'status'} =~ /^[M]+\d*$/) { + $action="change"; + $mode=$detail->{'mode_to'}; + } + elsif ($detail->{'status'} =~ /^[AM]+\d*$/) { + $action="add"; + $mode=$detail->{'mode_to'}; + } + elsif ($detail->{'status'} =~ /^[DAM]+\d*/) { + $action="remove"; + $mode=$detail->{'mode_from'}; + } + else { + error "unknown status ".$detail->{'status'}; + } + + # test that the file mode is ok + if ($mode !~ /^100[64][64][64]$/) { + error sprintf(gettext("you cannot act on a file with mode %s"), $mode); + } + if ($action eq "change") { + if ($detail->{'mode_from'} ne $detail->{'mode_to'}) { + error gettext("you are not allowed to change file modes"); + } + } + + # extract attachment to temp file + if (($action eq 'add' || $action eq 'change') && + ! pagetype($file)) { + eval q{use File::Temp}; + die $@ if $@; + my $fh; + ($fh, $path)=File::Temp::tempfile("XXXXXXXXXX", UNLINK => 1); + if (system("git show ".$detail->{sha1_to}." > '$path'") != 0) { + error("failed writing temp file"); + } + } + + push @rets, { + file => $file, + action => $action, + path => $path, + }; + } + } + } + + return reverse @rets; +} #}}} + 1 diff --git a/IkiWiki/Plugin/inline.pm b/IkiWiki/Plugin/inline.pm index 6d88c2f15..1b1ca2ce2 100644 --- a/IkiWiki/Plugin/inline.pm +++ b/IkiWiki/Plugin/inline.pm @@ -376,7 +376,7 @@ sub preprocess_inline (@) { #{{{ genfeed("rss", $config{url}."/".$rssp, $desc, $params{guid}, $params{destpage}, @feedlist)); $toping{$params{destpage}}=1 unless $config{rebuild}; - $feedlinks{$params{destpage}}=qq{}; + $feedlinks{$params{destpage}}.=qq{}; } } if ($atom) { @@ -386,7 +386,7 @@ sub preprocess_inline (@) { #{{{ writefile($atomp, $config{destdir}, genfeed("atom", $config{url}."/".$atomp, $desc, $params{guid}, $params{destpage}, @feedlist)); $toping{$params{destpage}}=1 unless $config{rebuild}; - $feedlinks{$params{destpage}}=qq{}; + $feedlinks{$params{destpage}}.=qq{}; } } } diff --git a/IkiWiki/Plugin/listdirectives.pm b/IkiWiki/Plugin/listdirectives.pm index fc8927ccb..2ab3e4665 100644 --- a/IkiWiki/Plugin/listdirectives.pm +++ b/IkiWiki/Plugin/listdirectives.pm @@ -30,7 +30,7 @@ sub getsetup () { #{{{ } #}}} my @fulllist; -my @earlylist; +my @shortlist; my $pluginstring; sub checkconfig () { #{{{ @@ -40,15 +40,14 @@ sub checkconfig () { #{{{ else { $config{directive_description_dir} =~ s/\/+$//; } - - @earlylist = sort keys %{$IkiWiki::hooks{preprocess}}; } #}}} sub needsbuild (@) { #{{{ my $needsbuild=shift; @fulllist = sort keys %{$IkiWiki::hooks{preprocess}}; - $pluginstring = join(' ', @earlylist) . " : " . join(' ', @fulllist); + @shortlist = grep { ! $IkiWiki::hooks{preprocess}{$_}{shortcut} } @fulllist; + $pluginstring = join(' ', @shortlist) . " : " . join(' ', @fulllist); foreach my $page (keys %pagestate) { if (exists $pagestate{$page}{listdirectives}{shown}) { @@ -77,7 +76,7 @@ sub preprocess (@) { #{{{ @pluginlist = @fulllist; } else { - @pluginlist = @earlylist; + @pluginlist = @shortlist; } my $result = '