# Module of TWiki Enterprise Collaboration Platform, http://TWiki.org/
#
# Copyright (C) 2000-2018 Peter Thoeny, peter[at]thoeny.org
# and TWiki Contributors. All Rights Reserved. TWiki Contributors
# are listed in the AUTHORS file in the root of this distribution.
# NOTE: Please extend that file, not this notice.
#
# Additional copyrights apply to some or all of the code in this
# file as follows:
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version. For
# more details read LICENSE in the root of this distribution.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# As per the GPL, removal of this notice is prohibited.
=pod
---+ package TWiki::Configure::UIs::EXTEND
=cut
package TWiki::Configure::UIs::EXTEND;
use base 'TWiki::Configure::UI';
use strict;
use File::Temp;
use File::Copy;
use File::Spec;
use Cwd;
sub ui {
    my $this = shift;
    my $query = $TWiki::query;
    my $ar;
    my $extension = $query->param('extension');
    $extension =~ /(\w+)/; # filter-in and untaint
    $extension = $1;
    die "Bad extension name" unless $extension;
    my $ext = '.tgz';
    $this->findRepositories();
    my $repository = $this->getRepository($query->param('repository'));
    if (!defined($repository)) {
        return $this->ERROR("Repository not found. 
 ".$query->param('repository')." ");
    }
    my $arf = $repository->{pub}.$extension.'/'.$extension.$ext;
    print "
Fetching $arf...
\n";
    my $response = $this->getUrl($arf);
    if (!$response->is_error()) {
        eval { $ar = $response->content(); };
    } else {
        $@ = $response->message();
    }
    if ($@) {
        print $this->WARN(<$@
HERE
        undef $ar;
    }
    if (!defined($ar)) {
        print $this->WARN(<{pub}.$extension.'/'.$extension.$ext;
        print "
Fetching $arf...
\n";
        $response = $this->getUrl($arf);
        if (!$response->is_error()) {
            eval { $ar = $response->content(); };
        } else {
            $@ = $response->message();
        }
        if ($@) {
            print $this->WARN(<$@
HERE
            undef $ar;
        }
    }
    unless ($ar) {
        return $this->ERROR(< $ext, UNLINK=>1);
    binmode($tmp);
    print $tmp $ar;
    $tmp->close();
    print "Unpacking...
\n";
    my $dir = _unpackArchive($tmpfilename);
    my @names = _listDir($dir);
    # install the contents
    my $installScript = undef;
    unless ($query->param('confirm')) {
        foreach my $file (@names) {
            my $ef = $this->_findTarget($file);
            if (-e $ef && !-d $ef) {
                my $mess = "Note: Existing $file overwritten.";
                if (File::Copy::move($ef, "$ef.bak")) {
                    $mess .= " Backup saved in $ef.bak";
                }
                print $this->NOTE("$mess
");
            } else {
                print "$file
";
            }
            if( $file =~ /^${extension}_installer(\.pl)?$/) {
                $installScript = $this->_findTarget($file);
            }
        }
        unless ($installScript) {
            print $this->WARN(
                "No installer script found in archive");
        }
    }
    # foreach file in archive, move it to the correct place
    foreach my $file (@names) {
        # The file may already have been moved along with its directory
        next unless -e "$dir/$file";
        # Find where it is meant to go
        my $ef = $this->_findTarget($file);
        if (-e $ef && !-d $ef && !-w $ef) {
            print $this->ERROR("No permission to write to $ef");
            die "Installation terminated";
        } elsif (!-d $ef) {
            if (-d "$dir/$file") {
                unless (mkdir($ef)) {
                    print $this->ERROR(
                        "Cannot create directory $ef: $!");
                    die "Installation terminated";
                }
            } elsif (!File::Copy::move("$dir/$file", $ef)) {
                print $this->ERROR("Failed to move file '$file' to $ef: $!");
                die "Installation terminated";
            };
        }
    }
    if ($installScript && -e $installScript) {
        # clean and untaint installer script
        $installScript =~ s/[^a-zA-Z0-9\_\-\:\/\\\.]//g;
        $installScript =~ /^(.*)$/;
        $installScript = $1;
        # invoke the installer script.
        # SMELL: Not sure yet how to handle
        # interaction if the script ignores -a. At the moment it
        # will just hang :-(
        chdir($this->{root});
        unshift(@ARGV, '-a');
        print "\n";
        eval {
            no warnings 'redefine';
            do $installScript;
            use warnings 'redefine';
            die $@ if $@; # propagate
        };
        print "\n";
        if ($@) {
            print $this->ERROR(<$@
You may be able to resolve these errors and complete the installation
from the command line, so I will leave the installed files where they are.
HERE
        } else {
            print $this->NOTE("Installer ran without errors");
        }
        chdir($this->{bin});
    }
    if ($this->{warnings}) {
        print $this->NOTE(
            "Installation finished with $this->{errors} error".
              ($this->{errors}==1?'':'s').
                " and $this->{warnings} warning".
                  ($this->{warnings}==1?'':'s'));
    } else {
        print 'Installation finished.';
    }
    unless ($installScript) {
        print $this->WARN(<NOTE(<{root}$1$TWiki::cfg{ScriptSuffix}#) {
        #This makes a couple of bad assumptions
        #1. that the twiki's bin dir _is_ called bin
        #2. that any file going into there _is_ a script - making installing the 
        #   .htaccess file via this machanism impossible
        #3. that softlinks are not in use (same issue below)
    } else {
        $file = File::Spec->catfile($this->{root}, $file);
    }
    return $file;
}
# Recursively list a directory
sub _listDir {
    my ($dir, $path) = @_;
    $path ||= '';
    $dir .= '/' unless $dir =~ /\/$/;
    my $d;
    my @names = ();
    if (opendir($d, "$dir$path")) {
        foreach my $f ( grep { !/^\.*$/ } readdir $d ) {
            # Someone might upload a package to twiki.org that contains
            # a filename which, when passed to File::Copy, does something
            # evil. Check and untaint the filenames here.
            # SMELL: potential problem with unicode chars in file names?
            $f =~ /([\w\-\.]+)/; $f = $1;
            if (-d "$dir$path/$f") {
                push(@names, "$path$f/");
                push(@names, _listDir($dir, "$path$f/"));
            } else {
                push(@names, "$path$f");
            }
        }
        closedir($d);
    }
    return @names;
}
=pod
---++ StaticMethod _unpackArchive($archive [,$dir] )
Unpack an archive. The unpacking method is determined from the file
extension e.g. .zip, .tgz. .tar, etc. If $dir is not given, unpack
to a temporary directory, the name of which is returned.
=cut
sub _unpackArchive {
    my ($name, $dir) = @_;
    $dir ||= File::Temp::tempdir(CLEANUP=>1);
    my $here = Cwd::getcwd();
    $here =~ /(.*)/; $here = $1; # untaint current dir name
    chdir( $dir );
    unless( $name =~ /\.zip/i && _unzip( $name ) ||
              $name =~ /(\.tar\.gz|\.tgz|\.tar)/ && _untar( $name )) {
        $dir = undef;
        print "Failed to unpack archive $name
\n";
    }
    chdir( $1 );
    return $dir;
}
sub _unzip {
    my $archive = shift;
    eval 'use Archive::Zip';
    unless ( $@ ) {
        my $zip = Archive::Zip->new( $archive );
        unless ( $zip ) {
            print "Could not open zip file $archive
\n";
            return 0;
        }
        my @members = $zip->members();
        foreach my $member ( @members ) {
            my $file = $member->fileName();
            my $target = $file ;
            my $err = $zip->extractMember( $file, $target );
            if ( $err ) {
                print "Failed to extract '$file' from zip file ",
                  $zip,". Archive may be corrupt.
\n";
                return 0;
            }
        }
    } else {
        print "Archive::Zip is not installed; trying unzip on the command line
\n";
        print `unzip $archive`;
        # On certain older versions of perl / unzip it seems the unzip results
        # in an illegal seek error. But running the same command again often
        # goes well. Seems like the 2nd pass works because the subdirectories
        # are then created. A hack but it seems to work.
        if ( $! ) {
            print `unzip $archive`;
            if ( $! ) {
                print "unzip failed: $!\n";
                return 0;
            }
        }
    }
    return 1;
}
sub _untar {
    my $archive = shift;
    my $compressed = ( $archive =~ /z$/i ) ? 'z' : '';
    eval 'use Archive::Tar';
    unless ( $@ ) {
        my $tar = Archive::Tar->new( $archive, $compressed );
        unless ( $tar ) {
            print "Could not open tar file $archive
\n";
            return 0;
        }
        my @members = $tar->list_files();
        foreach my $file ( @members ) {
            my $err = $tar->extract( $file );
            unless ( $err ) {
                print 'Failed to extract ',$file,' from tar file ',
                  $tar,". Archive may be corrupt.
\n";
                return 0;
            }
        }
    } else {
        print "Archive::Tar is not installed; trying tar on the command-line
\n";
        print `tar xvf$compressed $archive`;
        if ( $! ) {
            print "tar failed: $!\n";
            return 0;
        }
    }
    return 1;
}
1;