From: Simon McVittie Date: Wed, 11 Jan 2017 18:04:28 +0000 (+0000) Subject: Merge branch 'debian-jessie' into debian-jessie-security X-Git-Tag: debian/3.20141016.4~2 X-Git-Url: http://git.vanrenterghem.biz/git.ikiwiki.info.git/commitdiff_plain/2143033a3c11c3c916023fbc11ee742bb34f3766?hp=f8882de9d1f2b8f05da0af1084693d6e6ba753e2 Merge branch 'debian-jessie' into debian-jessie-security --- diff --git a/IkiWiki/CGI.pm b/IkiWiki/CGI.pm index a6c0c2712..9c80c05c1 100644 --- a/IkiWiki/CGI.pm +++ b/IkiWiki/CGI.pm @@ -307,8 +307,9 @@ sub cgi_prefs ($$) { return; } elsif ($form->submitted eq 'Save Preferences' && $form->validate) { - if (defined $form->field('email')) { - userinfo_set($user_name, 'email', $form->field('email')) || + my $email = $form->field('email'); + if (defined $email) { + userinfo_set($user_name, 'email', $email) || error("failed to set email"); } diff --git a/IkiWiki/Plugin/attachment.pm b/IkiWiki/Plugin/attachment.pm index 9bac96fc6..0d6f81c4f 100644 --- a/IkiWiki/Plugin/attachment.pm +++ b/IkiWiki/Plugin/attachment.pm @@ -156,14 +156,15 @@ sub formbuilder (@) { } $add.="\n"; } + my $content = $form->field('editcontent'); $form->field(name => 'editcontent', - value => $form->field('editcontent')."\n\n".$add, + value => $content."\n\n".$add, force => 1) if length $add; } # Generate the attachment list only after having added any new # attachments. - $form->tmpl_param("attachment_list" => [attachment_list($form->field('page'))]); + $form->tmpl_param("attachment_list" => [attachment_list(scalar $form->field('page'))]); } sub attachment_holding_location { @@ -213,12 +214,12 @@ sub attachment_store { $filename=IkiWiki::basename($filename); $filename=~s/.*\\+(.+)/$1/; # hello, windows $filename=IkiWiki::possibly_foolish_untaint(linkpage($filename)); - my $dest=attachment_holding_location($form->field('page')); + my $dest=attachment_holding_location(scalar $form->field('page')); # Check that the user is allowed to edit the attachment. my $final_filename= linkpage(IkiWiki::possibly_foolish_untaint( - attachment_location($form->field('page')))). + attachment_location(scalar $form->field('page')))). $filename; eval { if (IkiWiki::file_pruned($final_filename)) { @@ -272,12 +273,12 @@ sub attachments_save { # Move attachments out of holding directory. my @attachments; - my $dir=attachment_holding_location($form->field('page')); + my $dir=attachment_holding_location(scalar $form->field('page')); foreach my $filename (glob("$dir/*")) { $filename=Encode::decode_utf8($filename); next unless -f $filename; my $destdir=linkpage(IkiWiki::possibly_foolish_untaint( - attachment_location($form->field('page')))); + attachment_location(scalar $form->field('page')))); my $absdestdir=$config{srcdir}."/".$destdir; my $destfile=IkiWiki::basename($filename); my $dest=$absdestdir.$destfile; diff --git a/IkiWiki/Plugin/comments.pm b/IkiWiki/Plugin/comments.pm index c5177833f..aa9b49c8c 100644 --- a/IkiWiki/Plugin/comments.pm +++ b/IkiWiki/Plugin/comments.pm @@ -555,11 +555,12 @@ sub editcomment ($$) { } $postcomment=1; - my $ok=IkiWiki::check_content(content => $form->field('editcontent'), - subject => $form->field('subject'), + my $ok=IkiWiki::check_content( + content => scalar $form->field('editcontent'), + subject => scalar $form->field('subject'), $config{comments_allowauthor} ? ( - author => $form->field('author'), - url => $form->field('url'), + author => scalar $form->field('author'), + url => scalar $form->field('url'), ) : (), page => $location, cgi => $cgi, @@ -599,7 +600,7 @@ sub editcomment ($$) { length $form->field('subject')) { $message = sprintf( gettext("Added a comment: %s"), - $form->field('subject')); + scalar $form->field('subject')); } IkiWiki::rcs_add($file); diff --git a/IkiWiki/Plugin/editpage.pm b/IkiWiki/Plugin/editpage.pm index 78d0704c7..fad7ecc5a 100644 --- a/IkiWiki/Plugin/editpage.pm +++ b/IkiWiki/Plugin/editpage.pm @@ -430,7 +430,7 @@ sub cgi_editpage ($$) { $conflict=rcs_commit( file => $file, message => $message, - token => $form->field("rcsinfo"), + token => scalar $form->field("rcsinfo"), session => $session, ); enable_commit_hook(); diff --git a/IkiWiki/Plugin/git.pm b/IkiWiki/Plugin/git.pm index 641e397eb..010d6d54c 100644 --- a/IkiWiki/Plugin/git.pm +++ b/IkiWiki/Plugin/git.pm @@ -5,6 +5,7 @@ use warnings; use strict; use IkiWiki; use Encode; +use File::Path qw{remove_tree}; use URI::Escape q{uri_escape_utf8}; use open qw{:utf8 :std}; @@ -153,40 +154,65 @@ sub genwrapper { } } -my $git_dir=undef; -my $prefix=undef; +# Loosely based on git-new-workdir from git contrib. +sub create_temp_working_dir ($$) { + my $rootdir = shift; + my $branch = shift; + my $working = "$rootdir/.git/ikiwiki-temp-working"; + remove_tree($working); -sub in_git_dir ($$) { - $git_dir=shift; - my @ret=shift->(); - $git_dir=undef; - $prefix=undef; - return @ret; + foreach my $dir ("", ".git") { + if (!mkdir("$working/$dir")) { + error("Unable to create $working/$dir: $!"); + } + } + + # Hooks are deliberately not included: we will commit to the temporary + # branch that is used in the temporary working tree, and we don't want + # to run the post-commit hook there. + # + # logs/refs is not included because we don't use the reflog. + # remotes, rr-cache, svn are similarly excluded. + foreach my $link ("config", "refs", "objects", "info", "packed-refs") { + if (!symlink("../../$link", "$working/.git/$link")) { + error("Unable to create symlink $working/.git/$link: $!"); + } + } + + open (my $out, '>', "$working/.git/HEAD") or + error("failed to write $working.git/HEAD: $!"); + print $out "ref: refs/heads/$branch\n" or + error("failed to write $working.git/HEAD: $!"); + close $out or + error("failed to write $working.git/HEAD: $!"); + return $working; } -sub safe_git (&@) { +sub safe_git { # Start a child process safely without resorting to /bin/sh. # Returns command output (in list content) or success state # (in scalar context), or runs the specified data handler. - my ($error_handler, $data_handler, @cmdline) = @_; + my %params = @_; my $pid = open my $OUT, "-|"; + error("Working directory not specified") unless defined $params{chdir}; error("Cannot fork: $!") if !defined $pid; if (!$pid) { # In child. # Git commands want to be in wc. - if (! defined $git_dir) { - chdir $config{srcdir} - or error("cannot chdir to $config{srcdir}: $!"); + if ($params{chdir} ne '.') { + chdir $params{chdir} + or error("cannot chdir to $params{chdir}: $!"); } - else { - chdir $git_dir - or error("cannot chdir to $git_dir: $!"); + + if ($params{stdout}) { + open(STDOUT, '>&', $params{stdout}) or error("Cannot reopen stdout: $!"); } - exec @cmdline or error("Cannot exec '@cmdline': $!"); + + exec @{$params{cmdline}} or error("Cannot exec '@{$params{cmdline}}': $!"); } # In parent. @@ -201,25 +227,51 @@ sub safe_git (&@) { chomp; - if (! defined $data_handler) { + if (! defined $params{data_handler}) { push @lines, $_; } else { - last unless $data_handler->($_); + last unless $params{data_handler}->($_); } } close $OUT; - $error_handler->("'@cmdline' failed: $!") if $? && $error_handler; + $params{error_handler}->("'@{$params{cmdline}}' failed: $!") if $? && $params{error_handler}; return wantarray ? @lines : ($? == 0); } # Convenient wrappers. -sub run_or_die ($@) { safe_git(\&error, undef, @_) } -sub run_or_cry ($@) { safe_git(sub { warn @_ }, undef, @_) } -sub run_or_non ($@) { safe_git(undef, undef, @_) } +sub run_or_die_in ($$@) { + my $dir = shift; + safe_git(chdir => $dir, error_handler => \&error, cmdline => \@_); +} +sub run_or_cry_in ($$@) { + my $dir = shift; + safe_git(chdir => $dir, error_handler => sub { warn @_ }, cmdline => \@_); +} +sub run_or_non_in ($$@) { + my $dir = shift; + safe_git(chdir => $dir, cmdline => \@_); +} + +sub ensure_committer ($) { + my $dir = shift; + + if (! length $ENV{GIT_AUTHOR_NAME} || ! length $ENV{GIT_COMMITTER_NAME}) { + my $name = join('', run_or_non_in($dir, "git", "config", "user.name")); + if (! length $name) { + run_or_die_in($dir, "git", "config", "user.name", "IkiWiki"); + } + } + if (! length $ENV{GIT_AUTHOR_EMAIL} || ! length $ENV{GIT_COMMITTER_EMAIL}) { + my $email = join('', run_or_non_in($dir, "git", "config", "user.email")); + if (! length $email) { + run_or_die_in($dir, "git", "config", "user.email", "ikiwiki.info"); + } + } +} sub merge_past ($$$) { # Unlike with Subversion, Git cannot make a 'svn merge -rN:M file'. @@ -258,6 +310,8 @@ sub merge_past ($$$) { my @undo; # undo stack for cleanup in case of an error my $conflict; # file content with conflict markers + ensure_committer($config{srcdir}); + eval { # Hide local changes from Git by renaming the modified file. # Relative paths must be converted to absolute for renaming. @@ -276,30 +330,30 @@ sub merge_past ($$$) { my $branch = "throw_away_${sha1}"; # supposed to be unique # Create a throw-away branch and rewind backward. - push @undo, sub { run_or_cry('git', 'branch', '-D', $branch) }; - run_or_die('git', 'branch', $branch, $sha1); + push @undo, sub { run_or_cry_in($config{srcdir}, 'git', 'branch', '-D', $branch) }; + run_or_die_in($config{srcdir}, 'git', 'branch', $branch, $sha1); # Switch to throw-away branch for the merge operation. push @undo, sub { - if (!run_or_cry('git', 'checkout', $config{gitmaster_branch})) { - run_or_cry('git', 'checkout','-f',$config{gitmaster_branch}); + if (!run_or_cry_in($config{srcdir}, 'git', 'checkout', $config{gitmaster_branch})) { + run_or_cry_in($config{srcdir}, 'git', 'checkout','-f',$config{gitmaster_branch}); } }; - run_or_die('git', 'checkout', $branch); + run_or_die_in($config{srcdir}, 'git', 'checkout', $branch); # Put the modified file in _this_ branch. rename($hidden, $target) or error("rename '$hidden' to '$target' failed: $!"); # _Silently_ commit all modifications in the current branch. - run_or_non('git', 'commit', '-m', $message, '-a'); + run_or_non_in($config{srcdir}, 'git', 'commit', '-m', $message, '-a'); # ... and re-switch to master. - run_or_die('git', 'checkout', $config{gitmaster_branch}); + run_or_die_in($config{srcdir}, 'git', 'checkout', $config{gitmaster_branch}); # Attempt to merge without complaining. - if (!run_or_non('git', 'pull', '--no-commit', '.', $branch)) { + if (!run_or_non_in($config{srcdir}, 'git', 'pull', '--no-commit', '.', $branch)) { $conflict = readfile($target); - run_or_die('git', 'reset', '--hard'); + run_or_die_in($config{srcdir}, 'git', 'reset', '--hard'); } }; my $failure = $@; @@ -315,7 +369,11 @@ sub merge_past ($$$) { return $conflict; } -sub decode_git_file ($) { +{ +my %prefix_cache; + +sub decode_git_file ($$) { + my $dir=shift; my $file=shift; # git does not output utf-8 filenames, but instead @@ -326,20 +384,22 @@ sub decode_git_file ($) { } # strip prefix if in a subdir - if (! defined $prefix) { - ($prefix) = run_or_die('git', 'rev-parse', '--show-prefix'); - if (! defined $prefix) { - $prefix=""; + if (! defined $prefix_cache{$dir}) { + ($prefix_cache{$dir}) = run_or_die_in($dir, 'git', 'rev-parse', '--show-prefix'); + if (! defined $prefix_cache{$dir}) { + $prefix_cache{$dir}=""; } } - $file =~ s/^\Q$prefix\E//; + $file =~ s/^\Q$prefix_cache{$dir}\E//; return decode("utf8", $file); } +} -sub parse_diff_tree ($) { +sub parse_diff_tree ($$) { # Parse the raw diff tree chunk and return the info hash. # See git-diff-tree(1) for the syntax. + my $dir = shift; my $dt_ref = shift; # End of stream? @@ -408,6 +468,17 @@ sub parse_diff_tree ($) { } shift @{ $dt_ref } if $dt_ref->[0] =~ /^$/; + $ci{details} = [parse_changed_files($dir, $dt_ref)]; + + return \%ci; +} + +sub parse_changed_files ($$) { + my $dir = shift; + my $dt_ref = shift; + + my @files; + # Modified files. while (my $line = shift @{ $dt_ref }) { if ($line =~ m{^ @@ -425,8 +496,8 @@ sub parse_diff_tree ($) { my $status = shift(@tmp); if (length $file) { - push @{ $ci{'details'} }, { - 'file' => decode_git_file($file), + push @files, { + 'file' => decode_git_file($dir, $file), 'sha1_from' => $sha1_from[0], 'sha1_to' => $sha1_to, 'mode_from' => $mode_from[0], @@ -439,23 +510,23 @@ sub parse_diff_tree ($) { last; } - return \%ci; + return @files; } -sub git_commit_info ($;$) { +sub git_commit_info ($$;$) { # Return an array of commit info hashes of num commits # starting from the given sha1sum. - my ($sha1, $num) = @_; + my ($dir, $sha1, $num) = @_; my @opts; push @opts, "--max-count=$num" if defined $num; - my @raw_lines = run_or_die('git', 'log', @opts, + my @raw_lines = run_or_die_in($dir, 'git', 'log', @opts, '--pretty=raw', '--raw', '--abbrev=40', '--always', '-c', - '-r', $sha1, '--', '.'); + '-r', $sha1, '--no-renames', '--', '.'); my @ci; - while (my $parsed = parse_diff_tree(\@raw_lines)) { + while (my $parsed = parse_diff_tree($dir, \@raw_lines)) { push @ci, $parsed; } @@ -472,7 +543,7 @@ sub rcs_find_changes ($) { # merge commit where some files were not really added. # This is why the code below verifies that the files really # exist. - my @raw_lines = run_or_die('git', 'log', + my @raw_lines = run_or_die_in($config{srcdir}, 'git', 'log', '--pretty=raw', '--raw', '--abbrev=40', '--always', '-c', '--no-renames', , '--reverse', '-r', "$oldrev..HEAD", '--', '.'); @@ -482,7 +553,7 @@ sub rcs_find_changes ($) { my %deleted; my $nullsha = 0 x 40; my $newrev=$oldrev; - while (my $ci = parse_diff_tree(\@raw_lines)) { + while (my $ci = parse_diff_tree($config{srcdir}, \@raw_lines)) { $newrev=$ci->{sha1}; foreach my $i (@{$ci->{details}}) { my $file=$i->{file}; @@ -504,14 +575,16 @@ sub rcs_find_changes ($) { return (\%changed, \%deleted, $newrev); } -sub git_sha1_file ($) { +sub git_sha1_file ($$) { + my $dir=shift; my $file=shift; - git_sha1("--", $file); + return git_sha1($dir, $file); } -sub git_sha1 (@) { +sub git_sha1 ($@) { + my $dir = shift; # Ignore error since a non-existing file might be given. - my ($sha1) = run_or_non('git', 'rev-list', '--max-count=1', 'HEAD', + my ($sha1) = run_or_non_in($dir, 'git', 'rev-list', '--max-count=1', 'HEAD', '--', @_); if (defined $sha1) { ($sha1) = $sha1 =~ m/($sha1_pattern)/; # sha1 is untainted now @@ -520,14 +593,15 @@ sub git_sha1 (@) { } sub rcs_get_current_rev () { - git_sha1(); + return git_sha1($config{srcdir}); } sub rcs_update () { # Update working directory. + ensure_committer($config{srcdir}); if (length $config{gitorigin_branch}) { - run_or_cry('git', 'pull', '--prune', $config{gitorigin_branch}); + run_or_cry_in($config{srcdir}, 'git', 'pull', '--prune', $config{gitorigin_branch}); } } @@ -536,7 +610,7 @@ sub rcs_prepedit ($) { # This will be later used in rcs_commit if a merge is required. my ($file) = @_; - return git_sha1_file($file); + return git_sha1_file($config{srcdir}, $file); } sub rcs_commit (@) { @@ -547,8 +621,11 @@ sub rcs_commit (@) { # Check to see if the page has been changed by someone else since # rcs_prepedit was called. - my $cur = git_sha1_file($params{file}); - my ($prev) = $params{token} =~ /^($sha1_pattern)$/; # untaint + my $cur = git_sha1_file($config{srcdir}, $params{file}); + my $prev; + if (defined $params{token}) { + ($prev) = $params{token} =~ /^($sha1_pattern)$/; # untaint + } if (defined $cur && defined $prev && $cur ne $prev) { my $conflict = merge_past($prev, $params{file}, $dummy_commit_msg); @@ -578,20 +655,28 @@ sub rcs_commit_helper (@) { elsif (defined $params{session}->remote_addr()) { $u=$params{session}->remote_addr(); } - if (defined $u) { + if (length $u) { $u=encode_utf8($u); $ENV{GIT_AUTHOR_NAME}=$u; } + else { + $u = 'anonymous'; + } if (defined $params{session}->param("nickname")) { $u=encode_utf8($params{session}->param("nickname")); $u=~s/\s+/_/g; $u=~s/[^-_0-9[:alnum:]]+//g; } - if (defined $u) { + if (length $u) { $ENV{GIT_AUTHOR_EMAIL}="$u\@web"; } + else { + $ENV{GIT_AUTHOR_EMAIL}='anonymous@web'; + } } + ensure_committer($config{srcdir}); + $params{message} = IkiWiki::possibly_foolish_untaint($params{message}); my @opts; if ($params{message} !~ /\S/) { @@ -615,10 +700,10 @@ sub rcs_commit_helper (@) { push @opts, '--', $params{file}; } # git commit returns non-zero if nothing really changed. - # So we should ignore its exit status (hence run_or_non). - if (run_or_non('git', 'commit', '-m', $params{message}, '-q', @opts)) { + # So we should ignore its exit status (hence run_or_non_in). + if (run_or_non_in($config{srcdir}, 'git', 'commit', '-m', $params{message}, '-q', @opts)) { if (length $config{gitorigin_branch}) { - run_or_cry('git', 'push', $config{gitorigin_branch}, $config{gitmaster_branch}); + run_or_cry_in($config{srcdir}, 'git', 'push', $config{gitorigin_branch}, $config{gitmaster_branch}); } } @@ -631,7 +716,8 @@ sub rcs_add ($) { my ($file) = @_; - run_or_cry('git', 'add', $file); + ensure_committer($config{srcdir}); + run_or_cry_in($config{srcdir}, 'git', 'add', '--', $file); } sub rcs_remove ($) { @@ -639,13 +725,15 @@ sub rcs_remove ($) { my ($file) = @_; - run_or_cry('git', 'rm', '-f', $file); + ensure_committer($config{srcdir}); + run_or_cry_in($config{srcdir}, 'git', 'rm', '-f', '--', $file); } sub rcs_rename ($$) { my ($src, $dest) = @_; - run_or_cry('git', 'mv', '-f', $src, $dest); + ensure_committer($config{srcdir}); + run_or_cry_in($config{srcdir}, 'git', 'mv', '-f', '--', $src, $dest); } sub rcs_recentchanges ($) { @@ -657,7 +745,7 @@ sub rcs_recentchanges ($) { error($@) if $@; my @rets; - foreach my $ci (git_commit_info('HEAD', $num || 1)) { + foreach my $ci (git_commit_info($config{srcdir}, 'HEAD', $num || 1)) { # Skip redundant commits. next if ($ci->{'comment'} && @{$ci->{'comment'}}[0] eq $dummy_commit_msg); @@ -743,7 +831,12 @@ sub rcs_diff ($;$) { if (@lines || $line=~/^diff --git/); return 1; }; - safe_git(undef, $addlines, "git", "show", $sha1); + safe_git( + chdir => $config{srcdir}, + error_handler => undef, + data_handler => $addlines, + cmdline => ["git", "show", $sha1], + ); if (wantarray) { return @lines; } @@ -761,7 +854,7 @@ sub findtimes ($$) { if (! keys %time_cache) { my $date; - foreach my $line (run_or_die('git', 'log', + foreach my $line (run_or_die_in($config{srcdir}, 'git', 'log', '--pretty=format:%at', '--name-only', '--relative')) { if (! defined $date && $line =~ /^(\d+)$/) { @@ -771,7 +864,7 @@ sub findtimes ($$) { $date=undef; } else { - my $f=decode_git_file($line); + my $f=decode_git_file($config{srcdir}, $line); if (! $time_cache{$f}) { $time_cache{$f}[0]=$date; # mtime @@ -823,7 +916,8 @@ sub git_find_root { } -sub git_parse_changes { +sub git_parse_changes ($$@) { + my $dir = shift; my $reverted = shift; my @changes = @_; @@ -873,11 +967,12 @@ sub git_parse_changes { die $@ if $@; my $fh; ($fh, $path)=File::Temp::tempfile(undef, UNLINK => 1); - my $cmd = "cd $git_dir && ". - "git show $detail->{sha1_to} > '$path'"; - if (system($cmd) != 0) { - error("failed writing temp file '$path'."); - } + safe_git( + chdir => $dir, + error_handler => sub { error("failed writing temp file '$path': ".shift."."); }, + stdout => $fh, + cmdline => ['git', 'show', $detail->{sha1_to}], + ); } push @rets, { @@ -907,9 +1002,7 @@ sub rcs_receive () { # (Also, if a subdir is involved, we don't want to chdir to # it and only see changes in it.) # The pre-receive hook already puts us in the right place. - in_git_dir(".", sub { - push @rets, git_parse_changes(0, git_commit_info($oldrev."..".$newrev)); - }); + push @rets, git_parse_changes('.', 0, git_commit_info('.', $oldrev."..".$newrev)); } return reverse @rets; @@ -919,12 +1012,17 @@ sub rcs_preprevert ($) { my $rev=shift; my ($sha1) = $rev =~ /^($sha1_pattern)$/; # untaint + my @undo; # undo stack for cleanup in case of an error + # Examine changes from root of git repo, not from any subdir, # in order to see all changes. my ($subdir, $rootdir) = git_find_root(); - in_git_dir($rootdir, sub { - my @commits=git_commit_info($sha1, 1); - + ensure_committer($rootdir); + + # preserve indentation of previous in_git_dir code for now + do { + my @commits=git_commit_info($rootdir, $sha1, 1); + if (! @commits) { error "unknown commit"; # just in case } @@ -935,8 +1033,60 @@ sub rcs_preprevert ($) { error gettext("you are not allowed to revert a merge"); } - git_parse_changes(1, @commits); - }); + # Due to the presence of rename-detection, we cannot actually + # see what will happen in a revert without trying it. + # But we can guess, which is enough to rule out most changes + # that we won't allow reverting. + git_parse_changes($rootdir, 1, @commits); + + my $failure; + my @ret; + eval { + my $branch = "ikiwiki_revert_${sha1}"; # supposed to be unique + + push @undo, sub { + run_or_cry_in($rootdir, 'git', 'branch', '-D', $branch) if $failure; + }; + if (run_or_non_in($rootdir, 'git', 'rev-parse', '--quiet', '--verify', $branch)) { + run_or_non_in($rootdir, 'git', 'branch', '-D', $branch); + } + run_or_die_in($rootdir, 'git', 'branch', $branch, $config{gitmaster_branch}); + + my $working = create_temp_working_dir($rootdir, $branch); + + push @undo, sub { + remove_tree($working); + }; + + run_or_die_in($working, 'git', 'checkout', '--quiet', '--force', $branch); + run_or_die_in($working, 'git', 'revert', '--no-commit', $sha1); + run_or_die_in($working, 'git', 'commit', '-m', "revert $sha1", '-a'); + + my @raw_lines; + @raw_lines = run_or_die_in($rootdir, 'git', 'diff', '--pretty=raw', + '--raw', '--abbrev=40', '--always', '--no-renames', + "..${branch}"); + + my $ci = { + details => [parse_changed_files($rootdir, \@raw_lines)], + }; + + @ret = git_parse_changes($rootdir, 0, $ci); + }; + $failure = $@; + + # Process undo stack (in reverse order). By policy cleanup + # actions should normally print a warning on failure. + while (my $handle = pop @undo) { + $handle->(); + } + + if ($failure) { + my $message = sprintf(gettext("Failed to revert commit %s"), $sha1); + error("$message\n$failure\n"); + } + return @ret; + }; } sub rcs_revert ($) { @@ -944,13 +1094,13 @@ sub rcs_revert ($) { my $rev = shift; my ($sha1) = $rev =~ /^($sha1_pattern)$/; # untaint - if (run_or_non('git', 'revert', '--strategy=recursive', - '--strategy-option=no-renames', - '--no-commit', $sha1)) { + ensure_committer($config{srcdir}); + + if (run_or_non_in($config{srcdir}, 'git', 'cherry-pick', '--no-commit', "ikiwiki_revert_$sha1")) { return undef; } else { - run_or_die('git', 'reset', '--hard'); + run_or_non_in($config{srcdir}, 'git', 'branch', '-D', "ikiwiki_revert_$sha1"); return sprintf(gettext("Failed to revert commit %s"), $sha1); } } diff --git a/IkiWiki/Plugin/notifyemail.pm b/IkiWiki/Plugin/notifyemail.pm index b50a22a00..079bb10d4 100644 --- a/IkiWiki/Plugin/notifyemail.pm +++ b/IkiWiki/Plugin/notifyemail.pm @@ -34,7 +34,7 @@ sub formbuilder (@) { } elsif ($form->submitted eq "Save Preferences" && $form->validate && defined $form->field("subscriptions")) { - setsubscriptions($username, $form->field('subscriptions')); + setsubscriptions($username, scalar $form->field('subscriptions')); } } diff --git a/IkiWiki/Plugin/passwordauth.pm b/IkiWiki/Plugin/passwordauth.pm index 0cf2a26ea..84961c51f 100644 --- a/IkiWiki/Plugin/passwordauth.pm +++ b/IkiWiki/Plugin/passwordauth.pm @@ -231,7 +231,7 @@ sub formbuilder_setup (@) { $form->field( name => "password", validate => sub { - checkpassword($form->field("name"), shift); + checkpassword(scalar $form->field("name"), shift); }, ); } @@ -319,16 +319,20 @@ sub formbuilder (@) { if ($form->title eq "signin" || $form->title eq "register") { if (($form->submitted && $form->validate) || $do_register) { + my $user_name = $form->field('name'); + if ($form->submitted eq 'Login') { - $session->param("name", $form->field("name")); + $session->param("name", $user_name); IkiWiki::cgi_postsignin($cgi, $session); } elsif ($form->submitted eq 'Create Account') { - my $user_name=$form->field('name'); + my $email = $form->field('email'); + my $password = $form->field('password'); + if (IkiWiki::userinfo_setall($user_name, { - 'email' => $form->field('email'), + 'email' => $email, 'regdate' => time})) { - setpassword($user_name, $form->field('password')); + setpassword($user_name, $password); $form->field(name => "confirm_password", type => "hidden"); $form->field(name => "email", type => "hidden"); $form->text(gettext("Account creation successful. Now you can Login.")); @@ -338,7 +342,6 @@ sub formbuilder (@) { } } elsif ($form->submitted eq 'Reset Password') { - my $user_name=$form->field("name"); my $email=IkiWiki::userinfo_get($user_name, "email"); if (! length $email) { error(gettext("No email address, so cannot email password reset instructions.")); @@ -388,8 +391,9 @@ sub formbuilder (@) { elsif ($form->title eq "preferences") { if ($form->submitted eq "Save Preferences" && $form->validate) { my $user_name=$form->field('name'); - if (defined $form->field("password") && length $form->field("password")) { - setpassword($user_name, $form->field('password')); + my $password=$form->field('password'); + if (defined $password && length $password) { + setpassword($user_name, $password); } } } diff --git a/IkiWiki/Plugin/po.pm b/IkiWiki/Plugin/po.pm index 6107a4a22..1528f235f 100644 --- a/IkiWiki/Plugin/po.pm +++ b/IkiWiki/Plugin/po.pm @@ -548,7 +548,7 @@ sub formbuilder_setup (@) { # their buttons, which is why this hook must be run last. # The canrename/canremove hooks already ensure this is forbidden # at the backend level, so this is only UI sugar. - if (istranslation($form->field("page"))) { + if (istranslation(scalar $form->field("page"))) { map { for (my $i = 0; $i < @{$params{buttons}}; $i++) { if (@{$params{buttons}}[$i] eq $_) { diff --git a/IkiWiki/Plugin/rename.pm b/IkiWiki/Plugin/rename.pm index 6d56340b8..2456c22cb 100644 --- a/IkiWiki/Plugin/rename.pm +++ b/IkiWiki/Plugin/rename.pm @@ -258,7 +258,7 @@ sub formbuilder (@) { my $session=$params{session}; if ($form->submitted eq "Rename" && $form->field("do") eq "edit") { - rename_start($q, $session, 0, $form->field("page")); + rename_start($q, $session, 0, scalar $form->field("page")); } elsif ($form->submitted eq "Rename Attachment") { my @selected=map { Encode::decode_utf8($_) } $q->param("attachment_select"); @@ -311,7 +311,7 @@ sub sessioncgi ($$) { # performed in check_canrename later. my $srcfile=IkiWiki::possibly_foolish_untaint($pagesources{$src}) if exists $pagesources{$src}; - my $dest=IkiWiki::possibly_foolish_untaint(titlepage($form->field("new_name"))); + my $dest=IkiWiki::possibly_foolish_untaint(titlepage(scalar $form->field("new_name"))); my $destfile=$dest; if (! $q->param("attachment")) { my $type=$q->param('type'); diff --git a/debian/changelog b/debian/changelog index 3e7c3e917..229d44e27 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,18 +1,57 @@ -ikiwiki (3.20141016.4) UNRELEASED; urgency=medium - - [ Amitai Schlair ] - * img: ignore the case of the extension when detecting image format, - fixing the regression that *.JPG etc. would not be displayed - since 3.20141016.3 - - [ Simon McVittie ] - * Add CVE-2016-4561 reference to 3.20141016.3 changelog - * Security: tell `git revert` not to follow renames. If it does, then - renaming a file can result in a revert writing outside the wiki srcdir - or altering a file that the reverting user should not be able to alter, - an authorization bypass. Thanks, intrigeri. (CVE-2016-10026) - - -- Simon McVittie Mon, 09 May 2016 22:35:16 +0100 +ikiwiki (3.20141016.4) UNRELEASED; urgency=high + + * Reference CVE-2016-4561 in 3.20141016.3 changelog + * Security: force CGI::FormBuilder->field to scalar context where + necessary, avoiding unintended function argument injection + analogous to CVE-2014-1572. + - passwordauth: prevent authentication bypass via multiple name + parameters (OVE-20170111-0001) + - passwordauth: prevent userinfo forgery via repeated email + parameter (OVE-20170111-0001) + - comments, editpage: prevent commit metadata forgery + (CVE-2016-9646, OVE-20161226-0001) + - CGI, attachment, comments, editpage, notifyemail, passwordauth, + po, rename: harden against similar issues that are not believed + to be exploitable + * t/passwordauth.t: new automated test for OVE-20170111-0001 + * Backport IkiWiki::Plugin::git from 3.20170110 to fix the following + bugs, including one minor security vulnerability: + - Security: try revert operations before approving them. Previously, + automatic rename detection could result in a revert writing outside + the wiki srcdir or altering a file that the reverting user should not + be able to alter, an authorization bypass. + (CVE-2016-10026 represents the original vulnerability.) + The incomplete fix released in 3.20161219 was not effective for git + versions prior to 2.8.0rc0. + (CVE-2016-9645 represents that incomplete solution. Debian stable + was never vulnerable to this one.) + - Fix the warnings "cannot chdir to .../ikiwiki-temp-working: No such + file or directory" seen in the initial fixes for those security issues + - If no committer identity is known, set it to + "IkiWiki " in .git/config. This resolves commit errors + in versions of git that require a non-trivial committer identity. + - Use git log --no-renames to generate recentchanges, fixing the git + test-case with git 2.9 (Closes: #835612) + - Don't issue a warning if the rcsinfo CGI parameter is undefined + - Do not fail to commit changes with a recent git version + and an anonymous committer + - Do not fail on filenames starting with a dash + (patch from Florian Wagner) + - Don't add a redundant "--" and run "git rev-list ... -- -- ..." + * Backport t/git-cgi.t from 3.20170110 to have automated test coverage + for using the CGI with git, including tests for CVE-2016-10026 + - Build-depend on libipc-run-perl for better build-time test coverage + * Backport IkiWiki::Plugin::img from 3.20160905 to fix a regression + in 3.20141016.3: + - img: ignore the case of the extension when detecting image format, + fixing the regression that *.JPG etc. would not be displayed + (patch from Amitai Schleier) + * Backport tests' installed-test (autopkgtest) support from 3.20160121, + adjusted for compatibility with the older pkg-perl-autopkgtest in jessie + - d/control: add enough build-dependencies to run all tests, except for + non-git VCSs + + -- Simon McVittie Wed, 11 Jan 2017 15:22:38 +0000 ikiwiki (3.20141016.3) jessie-security; urgency=high diff --git a/debian/control b/debian/control index 68f543a24..d2011bf4e 100644 --- a/debian/control +++ b/debian/control @@ -3,6 +3,7 @@ Section: web Priority: optional Build-Depends: perl, debhelper (>= 9) Build-Depends-Indep: dpkg-dev (>= 1.9.0), libxml-simple-perl, + git (>= 1:1.7), libtext-markdown-discount-perl, libtimedate-perl, libhtml-template-perl, libhtml-scrubber-perl, wdg-html-validator, @@ -10,12 +11,19 @@ Build-Depends-Indep: dpkg-dev (>= 1.9.0), libxml-simple-perl, libfile-chdir-perl, libyaml-libyaml-perl, librpc-xml-perl, libcgi-pm-perl, libcgi-session-perl, ghostscript, libmagickcore-extra, - libcgi-formbuilder-perl + libcgi-formbuilder-perl, + libfile-mimeinfo-perl, + libipc-run-perl, + libnet-openid-consumer-perl, + libxml-feed-perl, + libxml-parser-perl, + libxml-twig-perl Maintainer: Simon McVittie Uploaders: Josh Triplett Standards-Version: 3.9.5 Homepage: http://ikiwiki.info/ Vcs-Git: git://git.ikiwiki.info/ +Testsuite: autopkgtest-pkg-perl Package: ikiwiki Architecture: all diff --git a/debian/tests/control b/debian/tests/control new file mode 100644 index 000000000..8b0ce72f9 --- /dev/null +++ b/debian/tests/control @@ -0,0 +1,12 @@ +Test-Command: env INSTALLED_TESTS=1 /usr/share/pkg-perl-autopkgtest/runner build-deps +Depends: @, @builddeps@, pkg-perl-autopkgtest + +Test-Command: env INSTALLED_TESTS=1 /usr/share/pkg-perl-autopkgtest/runner runtime-deps +Depends: @, pkg-perl-autopkgtest + +Test-Command: env INSTALLED_TESTS=1 /usr/share/pkg-perl-autopkgtest/runner runtime-deps-and-recommends +Depends: @, pkg-perl-autopkgtest +Restrictions: needs-recommends + +Test-Command: env INSTALLED_TESTS=1 /usr/share/pkg-perl-autopkgtest/runner heavy-deps +Depends: @, pkg-perl-autopkgtest, pkg-perl-autopkgtest-heavy diff --git a/debian/tests/pkg-perl/smoke-env b/debian/tests/pkg-perl/smoke-env new file mode 100644 index 000000000..774738148 --- /dev/null +++ b/debian/tests/pkg-perl/smoke-env @@ -0,0 +1 @@ +INSTALLED_TESTS=1 diff --git a/debian/tests/pkg-perl/syntax-skip b/debian/tests/pkg-perl/syntax-skip new file mode 100644 index 000000000..404e431d3 --- /dev/null +++ b/debian/tests/pkg-perl/syntax-skip @@ -0,0 +1,4 @@ +IkiWiki/Plugin/amazon_s3.pm +IkiWiki/Plugin/cvs.pm +IkiWiki/Plugin/monotone.pm +IkiWiki/Plugin/po.pm diff --git a/debian/tests/pkg-perl/use-name b/debian/tests/pkg-perl/use-name new file mode 100644 index 000000000..3d60011b3 --- /dev/null +++ b/debian/tests/pkg-perl/use-name @@ -0,0 +1 @@ +IkiWiki diff --git a/ikiwiki-makerepo b/ikiwiki-makerepo index c3a13c214..f1c44067e 100755 --- a/ikiwiki-makerepo +++ b/ikiwiki-makerepo @@ -85,6 +85,12 @@ git) cd "$srcdir" git init + if [ -z "$(git config user.name)" ]; then + git config user.name IkiWiki + fi + if [ -z "$(git config user.email)" ]; then + git config user.email ikiwiki.info + fi echo /.ikiwiki > .gitignore git add . git commit -m "initial commit" diff --git a/t/basewiki_brokenlinks.t b/t/basewiki_brokenlinks.t index 74ddc61c5..26e3859ab 100755 --- a/t/basewiki_brokenlinks.t +++ b/t/basewiki_brokenlinks.t @@ -1,21 +1,32 @@ #!/usr/bin/perl use warnings; use strict; -use Test::More 'no_plan'; +use Test::More; + +my $installed = $ENV{INSTALLED_TESTS}; ok(! system("rm -rf t/tmp; mkdir t/tmp")); -ok(! system("make -s ikiwiki.out")); -ok(! system("make underlay_install DESTDIR=`pwd`/t/tmp/install PREFIX=/usr >/dev/null")); + +my @command; +if ($installed) { + @command = qw(env LC_ALL=C ikiwiki); +} +else { + ok(! system("make -s ikiwiki.out")); + ok(! system("make underlay_install DESTDIR=`pwd`/t/tmp/install PREFIX=/usr >/dev/null")); + @command = qw(env LC_ALL=C perl -I. ./ikiwiki.out + --underlaydir=t/tmp/install/usr/share/ikiwiki/basewiki + --set underlaydirbase=t/tmp/install/usr/share/ikiwiki + --templatedir=templates); +} foreach my $plugin ("", "listdirectives") { - ok(! system("LC_ALL=C perl -I. ./ikiwiki.out -rebuild -plugin brokenlinks ". + ok(! system(@command, qw(--rebuild --plugin brokenlinks), # always enabled because pages link to it conditionally, # which brokenlinks cannot handle properly - "-plugin smiley ". - ($plugin ? "-plugin $plugin " : ""). - "-underlaydir=t/tmp/install/usr/share/ikiwiki/basewiki ". - "-set underlaydirbase=t/tmp/install/usr/share/ikiwiki ". - "-templatedir=templates t/basewiki_brokenlinks t/tmp/out")); + qw(--plugin smiley), + ($plugin ? ("--plugin", $plugin) : ()), + qw(t/basewiki_brokenlinks t/tmp/out))); my $result=`grep 'no broken links' t/tmp/out/index.html`; ok(length($result)); if (! length $result) { @@ -27,3 +38,5 @@ foreach my $plugin ("", "listdirectives") { ok(! system("rm -rf t/tmp/out t/basewiki_brokenlinks/.ikiwiki")); } ok(! system("rm -rf t/tmp")); + +done_testing(); diff --git a/t/comments.t b/t/comments.t index da2148b6b..a5add9701 100755 --- a/t/comments.t +++ b/t/comments.t @@ -1,7 +1,7 @@ #!/usr/bin/perl use warnings; use strict; -use Test::More 'no_plan'; +use Test::More; use IkiWiki; ok(! system("rm -rf t/tmp")); @@ -9,6 +9,20 @@ ok(mkdir "t/tmp"); ok(! system("cp -R t/tinyblog t/tmp/in")); ok(mkdir "t/tmp/in/post" or -d "t/tmp/in/post"); +my $installed = $ENV{INSTALLED_TESTS}; + +my @command; +if ($installed) { + @command = qw(ikiwiki); +} +else { + ok(! system("make -s ikiwiki.out")); + @command = qw(perl -I. ./ikiwiki.out + --underlaydir=underlays/basewiki + --set underlaydirbase=underlays + --templatedir=templates); +} + my $comment; $comment = < 106; +my $installed = $ENV{INSTALLED_TESTS}; + +my @command; +if ($installed) { + ok(1, "running installed"); + @command = qw(ikiwiki); +} +else { + ok(! system("make -s ikiwiki.out")); + @command = qw(perl -I. ./ikiwiki.out + --underlaydir=underlays/basewiki + --set underlaydirbase=underlays + --templatedir=templates); +} + # setup my $srcdir="t/tmp/src"; my $destdir="t/tmp/dest"; -ok(! system("make -s ikiwiki.out")); # runs ikiwiki to build test site sub runiki { my $testdesc=shift; - ok((! system("perl -I. ./ikiwiki.out -plugin txt -plugin rawhtml -underlaydir=underlays/basewiki -set underlaydirbase=underlays -templatedir=templates $srcdir $destdir @_")), + ok((! system(@command, qw(--plugin txt --plugin rawhtml), + $srcdir, $destdir, @_)), $testdesc); } sub refreshiki { diff --git a/t/cvs.t b/t/cvs.t index cbac43252..43a2ca3a8 100755 --- a/t/cvs.t +++ b/t/cvs.t @@ -4,6 +4,8 @@ use strict; use Test::More; my $total_tests = 72; use IkiWiki; +my $installed = $ENV{INSTALLED_TESTS}; + my $default_test_methods = '^test_*'; my @required_programs = qw( cvs @@ -606,12 +608,14 @@ sub _generate_and_configure_post_commit_hook { $config{wrapper} = $config{cvs_wrapper}; require IkiWiki::Wrapper; - { - no warnings 'once'; + if ($installed) { $IkiWiki::program_to_wrap = 'ikiwiki.out'; - # XXX substitute its interpreter to Makefile's $(PERL) - # XXX best solution: do this to all scripts during build } + else { + $IkiWiki::program_to_wrap = `which ikiwiki`; + } + # XXX substitute its interpreter to Makefile's $(PERL) + # XXX best solution: do this to all scripts during build IkiWiki::gen_wrapper(); my $cvs = "cvs -d $config{cvsrepo}"; diff --git a/t/git-cgi.t b/t/git-cgi.t new file mode 100755 index 000000000..ee77257b9 --- /dev/null +++ b/t/git-cgi.t @@ -0,0 +1,317 @@ +#!/usr/bin/perl +use warnings; +use strict; + +use Test::More; + +BEGIN { + my $git = `which git`; + chomp $git; + plan(skip_all => 'git not available') unless -x $git; + + plan(skip_all => "CGI not available") + unless eval q{ + use CGI qw(); + 1; + }; + + plan(skip_all => "IPC::Run not available") + unless eval q{ + use IPC::Run qw(run); + 1; + }; + + use_ok('IkiWiki'); + use_ok('YAML::XS'); +} + +# We check for English error messages +$ENV{LC_ALL} = 'C'; + +use Cwd qw(getcwd); +use Errno qw(ENOENT); + +my $installed = $ENV{INSTALLED_TESTS}; + +my @command; +if ($installed) { + @command = qw(ikiwiki); +} +else { + ok(! system("make -s ikiwiki.out")); + @command = ("perl", "-I".getcwd."/blib/lib", './ikiwiki.out', + '--underlaydir='.getcwd.'/underlays/basewiki', + '--set', 'underlaydirbase='.getcwd.'/underlays', + '--templatedir='.getcwd.'/templates'); +} + +sub write_old_file { + my $name = shift; + my $dir = shift; + my $content = shift; + writefile($name, $dir, $content); + ok(utime(333333333, 333333333, "$dir/$name")); +} + +sub write_setup_file { + my %setup = ( + wikiname => 'this is the name of my wiki', + srcdir => getcwd.'/t/tmp/in/doc', + destdir => getcwd.'/t/tmp/out', + url => 'http://example.com', + cgiurl => 'http://example.com/cgi-bin/ikiwiki.cgi', + cgi_wrapper => getcwd.'/t/tmp/ikiwiki.cgi', + cgi_wrappermode => '0751', + add_plugins => [qw(anonok attachment lockedit recentchanges)], + disable_plugins => [qw(emailauth openid passwordauth)], + anonok_pagespec => 'writable/*', + locked_pages => '!writable/*', + rcs => 'git', + git_wrapper => getcwd.'/t/tmp/in/.git/hooks/post-commit', + git_wrappermode => '0754', + gitorigin_branch => '', + ); + unless ($installed) { + $setup{ENV} = { 'PERL5LIB' => getcwd.'/blib/lib' }; + } + writefile("test.setup", "t/tmp", + "# IkiWiki::Setup::Yaml - YAML formatted setup file\n" . + Dump(\%setup)); +} + +sub thoroughly_rebuild { + ok(unlink("t/tmp/ikiwiki.cgi") || $!{ENOENT}); + ok(unlink("t/tmp/in/.git/hooks/post-commit") || $!{ENOENT}); + ok(! system(@command, qw(--setup t/tmp/test.setup --rebuild --wrappers))); +} + +sub check_cgi_mode_bits { + my $mode; + + (undef, undef, $mode, undef, undef, + undef, undef, undef, undef, undef, + undef, undef, undef) = stat('t/tmp/ikiwiki.cgi'); + is ($mode & 07777, 0751); + (undef, undef, $mode, undef, undef, + undef, undef, undef, undef, undef, + undef, undef, undef) = stat('t/tmp/in/.git/hooks/post-commit'); + is ($mode & 07777, 0754); +} + +sub run_cgi { + my (%args) = @_; + my ($in, $out); + my $method = $args{method} || 'GET'; + my $environ = $args{environ} || {}; + my $params = $args{params} || { do => 'prefs' }; + + my %defaults = ( + SCRIPT_NAME => '/cgi-bin/ikiwiki.cgi', + HTTP_HOST => 'example.com', + ); + + my $cgi = CGI->new($args{params}); + my $query_string = $cgi->query_string(); + + if ($method eq 'POST') { + $defaults{REQUEST_METHOD} = 'POST'; + $in = $query_string; + $defaults{CONTENT_LENGTH} = length $in; + } else { + $defaults{REQUEST_METHOD} = 'GET'; + $defaults{QUERY_STRING} = $query_string; + } + + my %envvars = ( + %defaults, + %$environ, + ); + run(["./t/tmp/ikiwiki.cgi"], \$in, \$out, init => sub { + map { + $ENV{$_} = $envvars{$_} + } keys(%envvars); + }); + + return $out; +} + +sub run_git { + my (undef, $filename, $line) = caller; + my $args = shift; + my $desc = shift || join(' ', 'git', @$args); + my ($in, $out); + ok(run(['git', @$args], \$in, \$out, init => sub { + chdir 't/tmp/in' or die $!; + my $name = 'The IkiWiki Tests'; + my $email = 'nobody@ikiwiki-tests.invalid'; + if ($args->[0] eq 'commit') { + $ENV{GIT_AUTHOR_NAME} = $ENV{GIT_COMMITTER_NAME} = $name; + $ENV{GIT_AUTHOR_EMAIL} = $ENV{GIT_COMMITTER_EMAIL} = $email; + } + }), "$desc at $filename:$line"); + return $out; +} + +sub test { + my $content; + my $status; + + ok(! system(qw(rm -rf t/tmp))); + ok(! system(qw(mkdir t/tmp))); + + write_old_file('.gitignore', 't/tmp/in', "/doc/.ikiwiki/\n"); + write_old_file('doc/writable/one.mdwn', 't/tmp/in', 'This is the first test page'); + write_old_file('doc/writable/two.mdwn', 't/tmp/in', 'This is the second test page'); + write_old_file('doc/writable/three.mdwn', 't/tmp/in', 'This is the third test page'); + write_old_file('doc/writable/three.bin', 't/tmp/in', 'An attachment'); + + unless ($installed) { + ok(! system(qw(cp -pRL doc/wikiicons t/tmp/in/doc/))); + ok(! system(qw(cp -pRL doc/recentchanges.mdwn t/tmp/in/doc/))); + } + + run_git(['init']); + run_git(['add', '.']); + run_git(['commit', '-m', 'Initial commit']); + + write_setup_file(); + thoroughly_rebuild(); + check_cgi_mode_bits(); + + ok(-e 't/tmp/out/writable/one/index.html'); + $content = readfile('t/tmp/out/writable/one/index.html'); + like($content, qr{This is the first test page}); + my $orig_sha1 = run_git(['rev-list', '--max-count=1', 'HEAD']); + + # We have to wait 1 second here so that new writes are guaranteed + # to have a strictly larger mtime. + sleep 1; + + # Test the git hook, which accepts git commits + writefile('doc/writable/one.mdwn', 't/tmp/in', + 'This is new content for the first test page'); + run_git(['add', '.']); + run_git(['commit', '-m', 'Git commit']); + my $first_revertable_sha1 = run_git(['rev-list', '--max-count=1', 'HEAD']); + isnt($orig_sha1, $first_revertable_sha1); + + ok(-e 't/tmp/out/writable/one/index.html'); + $content = readfile('t/tmp/out/writable/one/index.html'); + like($content, qr{This is new content for the first test page}); + + # Test a web commit + $content = run_cgi(method => 'POST', + params => { + do => 'edit', + page => 'writable/two', + type => 'mdwn', + editmessage => 'Web commit', + editcontent => 'Here is new content for the second page', + _submit => 'Save Page', + _submitted => '1', + }, + ); + like($content, qr{^Status:\s*302\s}m); + like($content, qr{^Location:\s*http://example\.com/writable/two/\?updated}m); + my $second_revertable_sha1 = run_git(['rev-list', '--max-count=1', 'HEAD']); + isnt($orig_sha1, $second_revertable_sha1); + isnt($first_revertable_sha1, $second_revertable_sha1); + + ok(-e 't/tmp/out/writable/two/index.html'); + $content = readfile('t/tmp/out/writable/two/index.html'); + like($content, qr{Here is new content for the second page}); + + # Another edit + writefile('doc/writable/three.mdwn', 't/tmp/in', + 'Also new content for the third page'); + unlink('t/tmp/in/doc/writable/three.bin'); + writefile('doc/writable/three.bin', 't/tmp/in', + 'Changed attachment'); + run_git(['add', '.']); + run_git(['commit', '-m', 'Git commit']); + ok(-e 't/tmp/out/writable/three/index.html'); + $content = readfile('t/tmp/out/writable/three/index.html'); + like($content, qr{Also new content for the third page}); + $content = readfile('t/tmp/out/writable/three.bin'); + like($content, qr{Changed attachment}); + my $third_revertable_sha1 = run_git(['rev-list', '--max-count=1', 'HEAD']); + isnt($orig_sha1, $third_revertable_sha1); + isnt($second_revertable_sha1, $third_revertable_sha1); + + run_git(['mv', 'doc/writable/one.mdwn', 'doc/one.mdwn']); + run_git(['mv', 'doc/writable/two.mdwn', 'two.mdwn']); + run_git(['commit', '-m', 'Rename files to test CVE-2016-10026']); + ok(! -e 't/tmp/out/writable/two/index.html'); + ok(! -e 't/tmp/out/writable/one/index.html'); + ok(-e 't/tmp/out/one/index.html'); + my $sha1_before_revert = run_git(['rev-list', '--max-count=1', 'HEAD']); + isnt($sha1_before_revert, $third_revertable_sha1); + + $content = run_cgi(method => 'post', + params => { + do => 'revert', + revertmessage => 'CVE-2016-10026', + rev => $first_revertable_sha1, + _submit => 'Revert', + _submitted_revert => '1', + }, + ); + like($content, qr{is locked and cannot be edited}); + # The tree is left clean + run_git(['diff', '--exit-code']); + run_git(['diff', '--cached', '--exit-code']); + my $sha1 = run_git(['rev-list', '--max-count=1', 'HEAD']); + is($sha1, $sha1_before_revert); + + ok(-e 't/tmp/out/one/index.html'); + ok(! -e 't/tmp/in/doc/writable/one.mdwn'); + ok(-e 't/tmp/in/doc/one.mdwn'); + $content = readfile('t/tmp/out/one/index.html'); + like($content, qr{This is new content for the first test page}); + + $content = run_cgi(method => 'post', + params => { + do => 'revert', + revertmessage => 'CVE-2016-10026', + rev => $second_revertable_sha1, + _submit => 'Revert', + _submitted_revert => '1', + }, + ); + like($content, qr{you are not allowed to change two\.mdwn}); + run_git(['diff', '--exit-code']); + run_git(['diff', '--cached', '--exit-code']); + $sha1 = run_git(['rev-list', '--max-count=1', 'HEAD']); + is($sha1, $sha1_before_revert); + + ok(! -e 't/tmp/out/writable/two/index.html'); + ok(! -e 't/tmp/out/two/index.html'); + ok(! -e 't/tmp/in/doc/writable/two.mdwn'); + ok(-e 't/tmp/in/two.mdwn'); + $content = readfile('t/tmp/in/two.mdwn'); + like($content, qr{Here is new content for the second page}); + + # This one can legitimately be reverted + $content = run_cgi(method => 'post', + params => { + do => 'revert', + revertmessage => 'not CVE-2016-10026', + rev => $third_revertable_sha1, + _submit => 'Revert', + _submitted_revert => '1', + }, + ); + like($content, qr{^Status:\s*302\s}m); + like($content, qr{^Location:\s*http://example\.com/recentchanges/}m); + run_git(['diff', '--exit-code']); + run_git(['diff', '--cached', '--exit-code']); + ok(-e 't/tmp/out/writable/three/index.html'); + $content = readfile('t/tmp/out/writable/three/index.html'); + like($content, qr{This is the third test page}); + $content = readfile('t/tmp/out/writable/three.bin'); + like($content, qr{An attachment}); +} + +test(); + +done_testing(); diff --git a/t/git.t b/t/git.t index 0396ae065..8990a554e 100755 --- a/t/git.t +++ b/t/git.t @@ -27,8 +27,16 @@ $config{diffurl} = '/nonexistent/cgit/plain/[[file]]'; IkiWiki::loadplugins(); IkiWiki::checkconfig(); +my $makerepo; +if ($ENV{INSTALLED_TESTS}) { + $makerepo = "ikiwiki-makerepo"; +} +else { + $makerepo = "./ikiwiki-makerepo"; +} + ok (mkdir($config{srcdir})); -is (system("./ikiwiki-makerepo git $config{srcdir} $dir/repo"), 0); +is (system("$makerepo git $config{srcdir} $dir/repo"), 0); my @changes; @changes = IkiWiki::rcs_recentchanges(3); diff --git a/t/html.t b/t/html.t index 84c561fa8..3933ab7ea 100755 --- a/t/html.t +++ b/t/html.t @@ -6,6 +6,8 @@ use Test::More; my @pages; BEGIN { + plan(skip_all => 'running installed') if $ENV{INSTALLED_TESTS}; + @pages=qw(index features news plugins/map security); if (system("command -v validate >/dev/null") != 0) { plan skip_all => "html validator not present"; diff --git a/t/img.t b/t/img.t index 968200b3e..5e92f1aff 100755 --- a/t/img.t +++ b/t/img.t @@ -34,6 +34,22 @@ else { push @command, qw(--set usedirs=0 --plugin img t/tmp/in t/tmp/out --verbose); +my $installed = $ENV{INSTALLED_TESTS}; + +my @command; +if ($installed) { + @command = qw(ikiwiki); +} +else { + ok(! system("make -s ikiwiki.out")); + @command = qw(perl -I. ./ikiwiki.out + --underlaydir=underlays/basewiki + --set underlaydirbase=underlays + --templatedir=templates); +} + +push @command, qw(--set usedirs=0 --plugin img t/tmp/in t/tmp/out --verbose); + my $magick = new Image::Magick; $magick->Read("t/img/twopages.pdf"); diff --git a/t/inline.t b/t/inline.t index 726227b8f..536bd6d67 100755 --- a/t/inline.t +++ b/t/inline.t @@ -4,6 +4,24 @@ use strict; use Test::More; use IkiWiki; +my $installed = $ENV{INSTALLED_TESTS}; + +my @command; +if ($installed) { + @command = qw(ikiwiki); +} +else { + ok(! system("make -s ikiwiki.out")); + @command = qw(perl -I. ./ikiwiki.out + --underlaydir=underlays/basewiki + --set underlaydirbase=underlays + --templatedir=templates); +} + +push @command, qw(--set usedirs=0 --plugin inline + --url=http://example.com --cgiurl=http://example.com/ikiwiki.cgi + --rss --atom t/tmp/in t/tmp/out --verbose); + my $blob; ok(! system("rm -rf t/tmp")); @@ -33,13 +51,8 @@ foreach my $page (qw(protagonists/shepard protagonists/link write_old_file("$page.mdwn", "this page is {$page}"); } -ok(! system("make -s ikiwiki.out")); - -my $command = "perl -I. ./ikiwiki.out -set usedirs=0 -plugin inline -url=http://example.com -cgiurl=http://example.com/ikiwiki.cgi -rss -atom -underlaydir=underlays/basewiki -set underlaydirbase=underlays -templatedir=templates t/tmp/in t/tmp/out -verbose"; - -ok(! system($command)); - -ok(! system("$command -refresh")); +ok(! system(@command)); +ok(! system(@command, "--refresh")); $blob = readfile("t/tmp/out/protagonists.html"); like($blob, qr{Add a new post}, 'rootpage=yes gives postform'); diff --git a/t/passwordauth.t b/t/passwordauth.t new file mode 100755 index 000000000..26d6c2717 --- /dev/null +++ b/t/passwordauth.t @@ -0,0 +1,225 @@ +#!/usr/bin/perl +use warnings; +use strict; + +use Cwd qw(getcwd); +use Test::More; + +BEGIN { + plan(skip_all => "Authen::Passphrase not available") + unless eval q{ + use Authen::Passphrase qw(); + 1; + }; + + plan(skip_all => "CGI not available") + unless eval q{ + use CGI qw(); + 1; + }; + + plan(skip_all => "IPC::Run not available") + unless eval q{ + use IPC::Run qw(run); + 1; + }; + + use_ok('IkiWiki'); + use_ok('IkiWiki::Plugin::passwordauth'); + use_ok('IkiWiki::Setup'); + use_ok('IkiWiki::UserInfo'); + use_ok('YAML::XS'); +} + +# We check for English messages +$ENV{LC_ALL} = 'C'; + +my $installed = $ENV{INSTALLED_TESTS}; + +my @command; +if ($installed) { + @command = qw(ikiwiki); +} +else { + ok(! system("make -s ikiwiki.out")); + @command = ("perl", "-I".getcwd."/blib/lib", './ikiwiki.out', + '--underlaydir='.getcwd.'/underlays/basewiki', + '--set', 'underlaydirbase='.getcwd.'/underlays', + '--templatedir='.getcwd.'/templates'); +} + +sub write_setup_file { + my %setup = ( + wikiname => 'this is the name of my wiki', + srcdir => getcwd.'/t/tmp/in', + destdir => getcwd.'/t/tmp/out', + url => 'http://example.com', + cgiurl => 'http://example.com/cgi-bin/ikiwiki.cgi', + cgi_wrapper => getcwd.'/t/tmp/ikiwiki.cgi', + cgi_wrappermode => '0751', + add_plugins => [qw(anonok attachment lockedit passwordauth recentchanges)], + adminuser => [qw(alice)], + disable_plugins => [qw(emailauth openid)], + locked_pages => '*', + ); + unless ($installed) { + $setup{ENV} = { 'PERL5LIB' => getcwd.'/blib/lib' }; + } + writefile("test.setup", "t/tmp", + "# IkiWiki::Setup::Yaml - YAML formatted setup file\n" . + Dump(\%setup)); + %IkiWiki::config = IkiWiki::defaultconfig(); + IkiWiki::Setup::load("t/tmp/test.setup"); + IkiWiki::loadplugins(); + IkiWiki::checkconfig(); +} + +sub thoroughly_rebuild { + ok(unlink("t/tmp/ikiwiki.cgi") || $!{ENOENT}); + ok(unlink("t/tmp/in/.git/hooks/post-commit") || $!{ENOENT}); + ok(! system(@command, qw(--setup t/tmp/test.setup --rebuild --wrappers))); +} + +sub run_cgi { + my (%args) = @_; + my ($in, $out); + my $method = $args{method} || 'GET'; + my $environ = $args{environ} || {}; + my $params = $args{params} || { do => 'prefs' }; + + my %defaults = ( + SCRIPT_NAME => '/cgi-bin/ikiwiki.cgi', + HTTP_HOST => 'example.com', + ); + + my $cgi = CGI->new($args{params}); + my $query_string = $cgi->query_string(); + + if ($method eq 'POST') { + $defaults{REQUEST_METHOD} = 'POST'; + $in = $query_string; + $defaults{CONTENT_LENGTH} = length $in; + } else { + $defaults{REQUEST_METHOD} = 'GET'; + $defaults{QUERY_STRING} = $query_string; + } + + my %envvars = ( + %defaults, + %$environ, + ); + print("# $query_string\n"); + run(["./t/tmp/ikiwiki.cgi"], \$in, \$out, init => sub { + map { + $ENV{$_} = $envvars{$_} + } keys(%envvars); + }); + + return $out; +} + +sub test_prefs { + my $content; + my $status; + + IkiWiki::userinfo_setall('alice', {regdate => time, email => 'alice@example.com'}); + IkiWiki::userinfo_setall('bob', {regdate => time, email => 'bob@example.com'}); + IkiWiki::userinfo_setall('name', {regdate => time, email => 'nobody@example.com'}); + IkiWiki::Plugin::passwordauth::setpassword('alice', "Alice's password"); + IkiWiki::Plugin::passwordauth::setpassword('bob', "Bob's password"); + + $content = run_cgi( + params => { + do => 'prefs', + }, + ); + + # prefs requires signing in so we are redirected, with the postsignin + # action saved in the session + like($content, qr/
{ + HTTP_COOKIE => $cookie, + }, + params => { + do => 'signin', + name => 'bob', + password => "Bob's password", + _submit => 'Login', + _submitted_signin => '1', + }, + ); + + # We are signed-in as bob now + like($content, qr{page=bob.*Create your user page}); + like($content, qr{ time, email => 'alice@example.com'}); + IkiWiki::userinfo_setall('bob', {regdate => time, email => 'bob@example.com'}); + IkiWiki::userinfo_setall('name', {regdate => time, email => 'nobody@example.com'}); + IkiWiki::Plugin::passwordauth::setpassword('alice', "Alice's password"); + IkiWiki::Plugin::passwordauth::setpassword('bob', "Bob's password"); + + $content = run_cgi( + params => { + do => 'prefs', + }, + ); + + # prefs requires signing in so we are redirected, with the postsignin + # action saved in the session + like($content, qr/ { + HTTP_COOKIE => $cookie, + }, + params => { + do => 'signin', + name => ['bob', 'name', 'alice'], + password => "Bob's password", + _submit => 'Login', + _submitted_signin => '1', + }, + ); + + like($content, qr{page=bob.*Create your user page}); + like($content, qr{$guid' t/tmp/out/index.rss`); ok(length `egrep '$guid' t/tmp/out/index.atom`); ok(! system("rm -rf t/tmp t/tinyblog/.ikiwiki")); + +done_testing(); diff --git a/t/podcast.t b/t/podcast.t index 94505a05e..77c905bde 100755 --- a/t/podcast.t +++ b/t/podcast.t @@ -9,13 +9,28 @@ BEGIN { "XML::Feed and/or HTML::Parser or File::MimeInfo not available"}; } else { - eval q{use Test::More tests => 136}; + eval q{use Test::More tests => 137}; } } use Cwd; use File::Basename; +my $installed = $ENV{INSTALLED_TESTS}; + +my @base_command; +if ($installed) { + ok(1, "running installed"); + @base_command = qw(ikiwiki); +} +else { + ok(! system("make -s ikiwiki.out")); + @base_command = qw(perl -I. ./ikiwiki.out + --underlaydir=underlays/basewiki + --set underlaydirbase=underlays + --templatedir=templates); +} + my $tmp = 't/tmp'; my $statedir = 't/tinypodcast/.ikiwiki'; @@ -23,10 +38,8 @@ sub podcast { my $podcast_style = shift; my $baseurl = 'http://example.com'; - my @command = (qw(./ikiwiki.out -plugin inline -rss -atom)); - push @command, qw(-underlaydir=underlays/basewiki); - push @command, qw(-set underlaydirbase=underlays -templatedir=templates); - push @command, "-url=$baseurl", qw(t/tinypodcast), "$tmp/out"; + my @command = (@base_command, qw(--plugin inline --rss --atom)); + push @command, "--url=$baseurl", qw(t/tinypodcast), "$tmp/out"; ok(! system("mkdir $tmp"), q{setup}); @@ -113,9 +126,7 @@ sub podcast { } sub single_page_html { - my @command = (qw(./ikiwiki.out)); - push @command, qw(-underlaydir=underlays/basewiki); - push @command, qw(-set underlaydirbase=underlays -templatedir=templates); + my @command = @base_command; push @command, qw(t/tinypodcast), "$tmp/out"; ok(! system("mkdir $tmp"), @@ -158,9 +169,7 @@ sub single_page_html { } sub inlined_pages_html { - my @command = (qw(./ikiwiki.out -plugin inline)); - push @command, qw(-underlaydir=underlays/basewiki); - push @command, qw(-set underlaydirbase=underlays -templatedir=templates); + my @command = (@base_command, qw(--plugin inline)); push @command, qw(t/tinypodcast), "$tmp/out"; ok(! system("mkdir $tmp"), diff --git a/t/relativity.t b/t/relativity.t index 054f8f664..73145dfd7 100755 --- a/t/relativity.t +++ b/t/relativity.t @@ -16,6 +16,20 @@ use Errno qw(ENOENT); # Black-box (ish) test for relative linking between CGI and static content +my $installed = $ENV{INSTALLED_TESTS}; + +my @command; +if ($installed) { + @command = qw(ikiwiki); +} +else { + ok(! system("make -s ikiwiki.out")); + @command = qw(perl -I. ./ikiwiki.out + --underlaydir=underlays/basewiki + --set underlaydirbase=underlays + --templatedir=templates); +} + sub parse_cgi_content { my $content = shift; my %bits; @@ -53,7 +67,6 @@ sub write_setup_file { wikiname: this is the name of my wiki srcdir: t/tmp/in destdir: t/tmp/out -templatedir: templates $urlline cgiurl: $args{cgiurl} $w3mmodeline @@ -72,7 +85,7 @@ EOF sub thoroughly_rebuild { ok(unlink("t/tmp/ikiwiki.cgi") || $!{ENOENT}); - ok(! system("./ikiwiki.out --setup t/tmp/test.setup --rebuild --wrappers")); + ok(! system(@command, qw(--setup t/tmp/test.setup --rebuild --wrappers))); } sub check_cgi_mode_bits { @@ -132,7 +145,6 @@ sub run_cgi { } sub test_startup { - ok(! system("make -s ikiwiki.out")); ok(! system("rm -rf t/tmp")); ok(! system("mkdir t/tmp")); diff --git a/t/syntax.t b/t/syntax.t index b7c6efd58..1d496be2d 100755 --- a/t/syntax.t +++ b/t/syntax.t @@ -3,6 +3,8 @@ use warnings; use strict; use Test::More; +plan(skip_all => 'running installed') if $ENV{INSTALLED_TESTS}; + my @progs="ikiwiki.in"; my @libs="IkiWiki.pm"; # monotone, external, amazon_s3, po, and cvs diff --git a/t/template_syntax.t b/t/template_syntax.t index e3d1feca9..3e6509f35 100755 --- a/t/template_syntax.t +++ b/t/template_syntax.t @@ -3,6 +3,8 @@ use warnings; use strict; use Test::More; +plan(skip_all => 'running installed') if $ENV{INSTALLED_TESTS}; + my @templates=(glob("templates/*.tmpl"), glob("doc/templates/*.mdwn")); plan(tests => 2*@templates); diff --git a/t/templates_documented.t b/t/templates_documented.t index 826c51d36..4991e4521 100755 --- a/t/templates_documented.t +++ b/t/templates_documented.t @@ -1,7 +1,9 @@ #!/usr/bin/perl use warnings; use strict; -use Test::More 'no_plan'; +use Test::More; + +plan(skip_all => 'running installed') if $ENV{INSTALLED_TESTS}; $/=undef; open(IN, "doc/templates.mdwn") || die "doc/templates.mdwn: $!"; @@ -12,3 +14,5 @@ foreach my $file (glob("templates/*.tmpl")) { $file=~s/templates\///; ok($page =~ /\Q$file\E/, "$file documented on doc/templates.mdwn"); } + +done_testing(); diff --git a/t/trail.t b/t/trail.t index dce3b3c7e..cac64c366 100755 --- a/t/trail.t +++ b/t/trail.t @@ -1,7 +1,7 @@ #!/usr/bin/perl use warnings; use strict; -use Test::More 'no_plan'; +use Test::More; use IkiWiki; sub check_trail { @@ -27,6 +27,24 @@ my $blob; ok(! system("rm -rf t/tmp")); ok(! system("mkdir t/tmp")); +my $installed = $ENV{INSTALLED_TESTS}; + +my @command; +if ($installed) { + @command = qw(ikiwiki); +} +else { + ok(! system("make -s ikiwiki.out")); + @command = qw(perl -I. ./ikiwiki.out + --underlaydir=underlays/basewiki + --set underlaydirbase=underlays + --templatedir=templates); +} + +push @command, qw(--set usedirs=0 --plugin trail --plugin inline + --url=http://example.com --cgiurl=http://example.com/ikiwiki.cgi + --rss --atom t/tmp/in t/tmp/out --verbose); + # Write files with a date in the past, so that when we refresh, # the update is detected. sub write_old_file { @@ -129,13 +147,8 @@ write_old_file("wind_in_the_willows.mdwn", <badger<\/a>/m); @@ -232,7 +245,7 @@ writefile("limited/c.mdwn", "t/tmp/in", '[[!meta title="New C page"]]c'); writefile("untrail.mdwn", "t/tmp/in", "no longer a trail"); -ok(! system("$command -refresh")); +ok(! system(@command, "--refresh")); check_trail("add/a.html", "n=add/b p="); check_trail("add/b.html", "n=add/c p=add/a"); @@ -290,3 +303,5 @@ check_no_trail("untrail/a.html"); check_no_trail("untrail/b.html"); ok(! system("rm -rf t/tmp")); + +done_testing();