# Gossamer Threads Module Library - http://gossamer-threads.com/
#
#   GT::FileMan::Commands
#   CVS Info : 087,071,086,086,085      
#   $Id: Commands.pm,v 1.383 2008/11/27 07:06:56 brewt Exp $
#
# Copyright (c) 2004 Gossamer Threads Inc.  All Rights Reserved.
# ====================================================================
#

package GT::FileMan::Commands;

use strict;
use vars qw/%ICONS $READ_SIZE/;
use GT::TempFile;
use GT::Base qw/:persist/;
use GT::AutoLoader;
use GT::File::Tools qw/:all/;
use GT::Date;
use Cwd;

use constants KB => 1024, MB => 1024 * 1024, FOLDER => 1, FILE => 2, SYMLINK => 3;

%ICONS = (
    'gif jpg jpeg bmp'      => ['image' => 'Image File'],
    'txt'                   => ['txt' => 'Text File'],
    'cgi pl pm'             => ['perl' => 'Script File'],
    'zip gz tar'            => ['compressed' => 'Compressed File'],
    'htm html shtm shtml'   => ['html' => 'HTML File'],
    'wav au mid mod mp3 wmv'=> ['audio' => 'Sound File'],
    'exe'                   => ['exe' => 'Binary File'],
    'doc'                   => ['doc' => 'MS Word'],
    'xls'                   => ['xls' => 'MS Excel'],
    'pdf'                   => ['pdf' => 'Adobe Acrobat'],
    'unknown'               => ['unknown' => 'Unknown'],
    'folder'                => ['folder' => 'File Folder'],
    'symlink'               => ['symlink' => 'Symlink']
);

# How large a chunk should we read into memory at once.
$READ_SIZE = 500000;

$COMPILE{home} = __LINE__ . <<'END_OF_SUB';
sub home {
# -----------------------------------------------------------------------------
# Print out the home page
#
    my ($self, %args) = @_;

    my $page = $self->{cgi}{page} || '';
    if ($page =~ /^help(_\w*)?.html/) {
        return $self->print($page);
    }
    ($self->{cgi}{ajax} and $args{error} and lc $ENV{REQUEST_METHOD} eq 'post') ? $self->print_json_error($args{error}) : $self->print('home.html', \%args);
}
END_OF_SUB

$COMPILE{password} = __LINE__ . <<'END_OF_SUB';
sub password {
# -----------------------------------------------------------------------------
# Change password feature which is for single user version
#
    my $self = shift;

    return $self->print_json({ html => $self->print('password.html', { json => 1 }) }, 1) if $self->{cgi}{form};

    my $old = $self->{cgi}{old_password};
    my $new = $self->{cgi}{new_password};
    my $confirm = $self->{cgi}{confirm_password};

    return $self->print_json_error($self->language('ERR_PASSWD_INPUT')) unless $old and $new and $confirm;
    return $self->print_json_error($self->language('ERR_INVALID_OLDPASSWD')) if $self->{cfg}{login}{password} ne crypt($old, $self->{cfg}{login}{password}) and $self->{cfg}{login}{password} ne GT::FileMan::encrypt($old, $self->{cfg}{login}{password});
    return $self->print_json_error($self->language('ERR_PASSWD_NOMATCH')) if $new ne $confirm;

    $self->{cfg}{login}{password} = GT::FileMan::encrypt($new);
    $self->{cfg}->save();

    $self->print_json(undef, 1, $self->language('MSG_PASSWD_UPDATED'));
}
END_OF_SUB

$COMPILE{cd} = __LINE__ . <<'END_OF_SUB';
sub cd {
# -----------------------------------------------------------------------------
# CD command. This must be a post request
#
    my $self = shift;

    my $dname = $self->{cgi}{f};
    my $path  = $self->check_path($dname);

    return $self->print_json_error($path->{error}) if $path->{error};
    return $self->print_json_error($self->language('ERR_NOT_FOUND', $dname)) unless $path->{exist};

    $self->{cfg}{work_path} = $path->{work_path};

# Loading files
    my $files = $self->files;
    return $self->print_json_error($files) if ref $files ne 'HASH';

    $self->print_json($files, 1);
}

sub files {
# -----------------------------------------------------------------------------
#
    my $self = shift;

    my $fpath = $self->check_path();
    return $self->language($fpath->{error}) if $fpath->{error};

    my $path = $fpath->{full_path};
    opendir (DIR, $path) or return $self->language('ERR_CANNOT_OPEN', $path, $!);
    my @rows = readdir(DIR);
    close DIR;

    my (@files, $readme);
    my $size = 0;
    foreach my $f (@rows) {
        next if ($f eq '.' or $f eq '..');
        next if ($f =~ /^\./ and !$self->{default}{show_hidden});

        my $file = $self->finfo("$path/$f", $f);
        if ($self->{cgi}{filter} == 1 and $file->[0] != FOLDER) {
            next;
        }
        elsif ($self->{cgi}{filter} == 2 and $file->[0] != FOLDER and !-T "$path/$f") {
            next;
        }

        push @files, $file;
        $size  += $file->[3];
        $readme = $f if lc $f eq 'readme';
    }

# loading current path and hidden objects
    my $hiddens = $self->hiddens();
    my $paths   = $self->current_path();

# Loading .htaccess and .htpassword
    my $htaccess_users = $self->load_htpasswd();
    my $readme_content = '';
    if ($self->{default}{readme} != 3 and $readme) {
        if (open(README, "< $path/$readme")) {
            my $count = 0;
            while (<README>) {
                chomp;
                next unless $_;
                $readme_content .= "$_\n";
                $count++;
                last if $count == 10;
            }
            close README;
        }
    }
    return {
        hits    => scalar @files,
        files   => \@files,
        size    => $size,
        paths   => $paths,
        hiddens => $hiddens,
        root_path      => $self->{session}{user} ? $self->{session}{user}{current}{disp} : undef,
        htaccess_users => $htaccess_users,
        readme_content => $self->{in}->html_escape($readme_content)
    };
}

END_OF_SUB

$COMPILE{search} = __LINE__ . <<'END_OF_SUB';
sub search {
    my $self = shift;

    return $self->print_json({ html => $self->print('search.html', { json => 1 }) }, 1) if $self->{cgi}{form};

    my $result = $self->fsearch();
    return $self->print_json_error($result->{error}) if $result->{error};

    $self->flog("search|Search for: '$self->{cgi}{search_input}'");
    $self->print_json($result, 1, $self->language('MSG_SEARCH', $result->{hits}));
}

sub fsearch {
# -----------------------------------------------------------------------------
#
    my $self = shift;

    my $ftype     = $self->{cgi}{search_type};
    my $fmodified = $self->{cgi}{search_mod};
    my $from_date = $self->{cgi}{fromdate};
    my $to_date   = $self->{cgi}{todate};

    require GT::Date;
    if ($fmodified eq 'past_week') {
        $from_date = GT::Date::date_get(time - (7 * 86400), $self->{cfg}{date}{input});
        $to_date   = GT::Date::date_get(time, $self->{cfg}{date}{input});
    }
    elsif ($fmodified eq 'past_month') {
        $from_date = GT::Date::date_get(time - (30 * 86400), $self->{cfg}{date}{input});
        $to_date   = GT::Date::date_get(time, $self->{cfg}{date}{input});
    }
    elsif ($fmodified eq 'past_year') {
        $from_date = GT::Date::date_get(time - (365 * 86400));
        $to_date   = GT::Date::date_get(time, $self->{cfg}{date}{input});
    }

    return { error => $self->language('ERR_SEARCH_COND') } unless $self->{cgi}{search_input} or $from_date or $to_date or $ftype;

    my (@files, @results);
    unless ($self->{cgi}{search_scope}) {
        my $fpath = $self->check_path();
        unless ($fpath->{error}) {
            find($fpath->{full_path}, sub { push @files, shift }, { untaint => 1 });
        }
    }
    else {
        my @selecteds = $self->{in}->param('cinput');
        foreach (@selecteds) {
            my $fpath = $self->check_name($_);
            next if $fpath->{error};

            if ($fpath->{isfile}) {
                push @files, $fpath->{full_path};
            }
            else {
                find($fpath->{full_path}, sub { push @files, shift }, { untaint => 1 });
            }
        }
    }

    my $src_term = $self->{cgi}{search_input} || '';
    if (not $self->{cgi}{search_exp}) {
        $src_term = quotemeta($src_term);
        $src_term =~ s/\\\*/.*/g;
        $src_term =~ s/\\\?/./g;
    }
    $src_term = "(?i)$src_term" unless $self->{cgi}{search_case};

# Search file and directory names
    my $total_size = 0;
    unless ($self->{cgi}{search_content}) {
        $ftype =~ s/^\s+//;
        $ftype =~ s/\s+$//;
        my %ftype = map { $_ => 1 } split /\s*,\s*/, $ftype;

        foreach my $file (@files) {
            my ($fn) = $file =~ m,/([^/]+)$,;
            next if $fn eq $self->{work_path} or $fn !~ /$src_term/;

            if ($ftype) {
                my ($ext) = $fn =~ /\.([^.]+)$/;
                next unless $ext and $ftype{$ext};
            }

            my $f = $self->finfo($file, $fn);
            next unless scalar @$f;

# Search on modify date
            if ($fmodified) {
                my $modified = GT::Date::date_get($f->[5], $self->{cfg}{date}{input});
                if ($from_date and $to_date) {
                    next if GT::Date::date_is_smaller($modified, $from_date) or GT::Date::date_is_greater($modified, $to_date);
                }
                else {
                    next if ($from_date and $modified ne $from_date) or ($to_date and $modified ne $to_date);
                }
            }

            push @results, $f;
            $total_size += $f->[3];
        }
    }
# Search file contents
    else {
        foreach my $file (@files) {
# Only search the contents of files that are readable, have content and are non-binary
            next unless -f $file and -r _ and -s _ and -T _;
            my ($ext) = $file =~ /\.([^.]+)$/;
            my ($fn)  = $file =~ m,/([^/]+)$,;
# pdf files look like text files
            next if lc $ext eq 'pdf';

            open(DATA, "<$file") or next;
            my ($buffer, $f, $bit);
            while (my $rs = read DATA, $buffer, $READ_SIZE) {
                $buffer = "$bit$buffer" if length $bit;
                $bit = ($rs == $READ_SIZE and $buffer =~ s/(?:\r|\r?\n)([^\r\n]+)$//) ? $1 : '';

                if ($buffer =~ /$src_term/) {
                    $f = $self->finfo($file, $fn);
                    last;
                }
            }
            close DATA;
            next unless $f;

            push @results, $f;
            $total_size += $f->[3];
        }
    }

    return { files => \@results, hits  => scalar @results, size  => $total_size };
}
END_OF_SUB

$COMPILE{replace} = __LINE__ . <<'END_OF_SUB';
sub replace {
# -----------------------------------------------------------------------------
# Replace contents of text files
#
    my $self = shift;

    return $self->print_json_error($self->language('ERR_SEARCH_COND')) unless $self->{cgi}{replace_input};

    my (@files, @results);
    unless ($self->{cgi}{replace_scope}) {
        my $fpath = $self->check_path();
        unless ($fpath->{error}) {
            find($fpath->{full_path}, sub { push @files, shift }, { untaint => 1 });
        }
    }
    else {
        my @selecteds = $self->{in}->param('cinput');
        foreach (@selecteds) {
            my $fpath = $self->check_name($_);

            next if $fpath->{error};
            if ($fpath->{isfile}) {
                push @files, $fpath->{full_path};
            }
            else {
                find($fpath->{full_path}, sub { push @files, shift }, { untaint => 1 });
            }
        }
    }

    my $search_for   = $self->{cgi}{replace_input} || '';
    my $replace_with = $self->{cgi}{replace_with}  || '';

    if (not $self->{cgi}{replace_exp}) {
        $search_for = quotemeta($search_for);
        $search_for =~ s/\\\*/.*/g;
        $search_for =~ s/\\\?/./g;
    }

    $search_for = "\\b$search_for\\b" if $self->{cgi}{replace_wholeword};
    $search_for = "(?i)$search_for"   unless $self->{cgi}{replace_case};

    my $total_size = 0;
    foreach my $file (@files) {
# Only search the contents of files that are readable, have content and are non-binary
        next unless -f $file and -r _ and -s _ and -T _;
        my ($ext) = $file =~ /\.([^.]+)$/;
        my ($fn)  = $file =~ m,/([^/]+)$,;
        next if lc $ext eq 'pdf';

        open(DATA, "<$file") or next;
        my ($buffer, $f, $bit);
        while (my $rs = read DATA, $buffer, $READ_SIZE) {
            $buffer = "$bit$buffer" if length $bit;
            $bit = ($rs == $READ_SIZE and $buffer =~ s/(?:\r|\r?\n)([^\r\n]+)$//) ? $1 : '';

            if ($buffer =~ /$search_for/) {
                $f++;
                last;
            }
        }
        close DATA;
        next unless $f;

        my $tempfile = new GT::TempFile;
        if (fcopy($file, "$$tempfile.tmp", $search_for, $replace_with)) {
            move($file, "$file.bak", { untaint => 1 }) if $self->{cgi}{replace_bak};
            move("$$tempfile.tmp", $file, { untaint => 1 });
        }

        push @results, $self->finfo($file, $fn);
        $total_size += -s $file;

# Log the action
        $self->flog("replace|Replace '$search_for' with '$replace_with'");
    }

# loading current path and hidden objects
    my $hiddens = $self->hiddens();
    my $paths   = $self->current_path();
    my $hits    = scalar @results;

    $self->print_json({
        hits    => $hits,
        files   => \@results,
        size    => $total_size,
        hiddens => $hiddens,
        paths   => $paths
    }, 1, $self->language('MSG_REPLACED', $hits));
}
END_OF_SUB

$COMPILE{command} = __LINE__ . <<'END_OF_SUB';
sub command {
# -----------------------------------------------------------------------------
# Execute a command in shell
#
    my $self = shift;

# Untaint PATH
    $ENV{PATH} = '/bin:/usr/bin:/usr/local/bin';

# Untaint the command
    my $command = $self->{cgi}{command_input} || '';
    ($command) = $command =~ /^(.*)$/;

# Remove serial file which is nolonger used
    my $serial = $self->{cgi}{serial};
    ($serial) = $serial  =~ /^(\w+)$/;

    if ($self->{cgi}{remove} and $serial) {
        unlink "$self->{cfg}{tmp_path}/$serial";
        print $self->{in}->header(-type => 'text/xml; charset=utf8', '-no-cache' => 1);
        print qq!<xml>\n<body>\n<output>done</output></body>\n</xml>!;

# Log the action
        $self->flog("command|$command");
        return;
    }

# Change to the working directory and then get the current path to ensure we
# get a 'clean looking' path
    my $working_dir = $self->{cgi}{working_dir};
    unless ($working_dir) {
        my $fpath = $self->check_path();
        $working_dir = $fpath->{full_path} unless $fpath->{error};
    }

    ($working_dir) = $working_dir =~ /^(.*)$/;
    chdir($working_dir);

    $working_dir = cwd();

    my $prompt = $GT::FileMan::MSWIN ? "$working_dir>" : "[" . eval { getpwuid($<) . '@' } . "$ENV{SERVER_NAME} $working_dir]";
    my $next_prompt = $prompt; 
    return $self->print_json({ prompt => $prompt, working_dir => $working_dir }, 1) if $self->{cgi}{prompt} and !$self->{cgi}{retrieve};
    return $self->print_json_error($self->language('ERR_COMMAND')) unless $command;


    my ($output, $error) = ('', '');
    if ($self->{cgi}{retrieve} and $serial) {
        if (-f "$self->{cfg}{tmp_path}/$serial") {
            open(DATA, "< $self->{cfg}{tmp_path}/$serial");
            read DATA, $output, -s DATA;
            close DATA;
        }

# Getting the working directory        
        if ($command =~ m/^\s*cd\s+(.+)/) {
            chdir($1);
            $working_dir = cwd();
            $next_prompt = $GT::FileMan::MSWIN ? "$working_dir>" : "[" . eval { getpwuid($<) . '@' } . "$ENV{SERVER_NAME} $working_dir]";
        }

        print $self->{in}->header(-type => 'text/xml; charset=utf8', '-no-cache' => 1);
        print qq~<xml>\n<body>\n<prompt><![CDATA[$prompt]]></prompt>\n<next_prompt><![CDATA[$next_prompt]]></next_prompt>\n<command><![CDATA[$command]]></command>\n<output><![CDATA[$output]]></output><working><![CDATA[$working_dir]]></working></body>\n</xml>~;
        return;
    }

# Run the command and send out a temporary file
    my $timeout = $self->{cfg}{command_timeout} || 60;
    my ($pid, $oldfh);
    $SIG{ALRM} = sub {
        kill('INT', $pid);
        die;
    };
    alarm($timeout);

    eval {
        $pid   = open(TMP, "$command |");
        $oldfh = select(TMP); $| = 1; select($oldfh);
        while (<TMP>) {
            s/(\n|\r\n)$//;
            open DATA, ">> $self->{cfg}{tmp_path}/$serial" or die $!;
            print DATA $_. "\n";
            close DATA;
        }
        close TMP;
    };
    open DATA, ">> $self->{cfg}{tmp_path}/$serial" or die $!;
    print DATA ($error ? $error : 'done');
    close DATA;
}
END_OF_SUB

$COMPILE{upload} = __LINE__ . <<'END_OF_SUB';
sub upload {
# -----------------------------------------------------------------------------
# Upload a file
#
    my $self = shift;

# Handle upload progress
    return $self->upload_progress() if $self->{cgi}{upload};

    if ($self->{cgi}{num_files}) {
        my $num_files  = $self->{cgi}{num_files};
        my $uploaded   = 0;
        my $declined   = 0;
        my $total_size = 0;
        for (my $i = 1; $i <= $num_files; $i++) {
            next unless $self->{cgi}{"file-$i"};

            my %result = $self->fupload($self->{cgi}{"file-$i"}, $self->{cgi}{"mode-$i"});
            if ($result{error}) {
                return $self->print_json_error($result{error}) if $num_files == 1;
                $declined++;
                next;
            }
            $total_size += $result{size};
            $uploaded++;

            $self->{diskspace}{free} -= $result{size} if $self->{diskspace};

# Log the action
            $self->flog("upload|$result{name}");
        }

        return $self->print_json(undef, 1, $self->language('MSG_MULT_UPLOADED', $uploaded, friendly_size($total_size), $declined));
    }
    else {
        my %result = $self->fupload($self->{cgi}{upload_input}, $self->{cgi}{upload_mode});
        return $self->print_json_error($result{error}) if $result{error};

# Log the action
        $self->flog("upload|$result{name}");
        return $self->print_json(undef, 1, $self->language('MSG_UPLOADED', $result{name}, friendly_size($result{size})));
    }
}

sub fupload {
# -----------------------------------------------------------------------------
#
    my ($self, $data, $mode) = @_;

    my $fn = $data;
    $fn    =~ s/.*?([^\\\/:]+)$/$1/;
    $fn    =~ s/[\[\]\s\$\#\%'"]/\_/g;

    unlink "$self->{cfg}{tmp_path}/$self->{serial}" if $self->{serial}; # Remove the log file

    return (error => $self->language('ERR_NOUPLOAD')) unless $fn;

# Change the name if format
    if ($self->{cgi}{name_format} eq 'uc') {
        $fn =~ s/(\w+)/\U$1/gi;
    }
    elsif ($self->{cgi}{name_format} eq 'lc') {
        $fn =~ s/(\w+)/\L$1/gi;
    }

    my $file = $self->check_name($fn);
    return (error => $file->{error}) if $file->{error};

    if ($file->{exist}) {
        return (error => $self->language('ERR_EXISTING', $fn))     unless $self->{cgi}{opt_overwrite};
        return (error => $self->language('ERR_NOT_WRITABLE', $fn)) unless $file->{write};
    }
    else {
        my $folder = $self->check_path(undef);
        return (error => $folder->{error}) if $folder->{error};
        return (error => $self->language('ERR_NOT_FOUND', $self->{cfg}{work_path})) if $self->{cfg}{work_path} and !$folder->{exist};
        return (error => $self->language('ERR_NOT_WRITABLE', $self->{cfg}{work_path})) unless $folder->{write};
    }

# Check free space if appicable. If diskspace is null, there is no limit
    return (error => $self->language('ERR_NOSPACE')) if $self->{diskspace} and !$self->{diskspace}{free};

    my $full_path = $file->{full_path};

    $mode ||= 'auto';
    $mode   = lc $mode;
    $mode   = 'ascii' if $mode eq 'auto' and lc substr($full_path, -4, 4) ne '.pdf' and -T $full_path;

    my ($uploaded_size, $bytesread, $buffer) = (0, 0, '');
    my $newlines = $GT::FileMan::MSWIN ? "\r\n" : "\n";
    open(OUTFILE, "> $full_path");
    binmode OUTFILE;
    while ($bytesread = read($data, $buffer, 1024)) {
        $buffer =~ s/$newlines/\n/g if $mode eq 'ascii';
        print OUTFILE $buffer;
        $uploaded_size += $bytesread;
    }
    close OUTFILE;

    my $mod = $self->{cfg}{default}{upload_mode} || '644';
    chmod(oct($mod), $full_path);
    my $fsize = -s $full_path;

# Check free space if appicable. If diskspace is null, there is no limit
    if ($self->{diskspace} and $self->{diskspace}{free} < $fsize) {
        del($full_path, { untaint => 1 });
        return $self->home(error => $self->language('ERR_NOSPACE'));
    }

    return (name => $file->{name}, size => $fsize);
}

END_OF_SUB


$COMPILE{file} = __LINE__ . <<'END_OF_SUB';
sub file {
# Create a new file
#
    my $self = shift;

    if ($self->{cgi}{form}) {
        $self->print_json({ html => $self->print('editor.html', { json => 1, editor_mode => $self->{default}{editor_mode} ? 'html' : 'text' }) }, 1);
    }
    else {
        $self->edit();
    }
}
END_OF_SUB

$COMPILE{edit} = __LINE__ . <<'END_OF_SUB';
sub edit {
# -----------------------------------------------------------------------------
# Edit a file
#
    my $self = shift;

# Print out editor form
    if ($self->{cgi}{form}) {
        my $fname = $self->{cgi}{f};
        return $self->print_json_error($self->language('ERR_CANNOT_OPEN_MULT')) if ref $fname eq 'ARRAY';

        my $file = $self->check_name($fname);
        return $self->print_json_error($file->{error}) if $file->{error};
        return $self->print_json_error($self->language('ERR_NOT_FILE', $file->{name})) unless $file->{isfile};
        return $self->print_json_error($self->language('ERR_NOT_READABLE', $file->{name})) unless $file->{read};
        return $self->open_compressed($file) if $file->{type} eq 'compress';
        return $self->print_json({ file => $file }) unless $file->{type} =~ /text|html/;

# Get content of the file
        open (DATA, "<$file->{full_path}") or return $self->print_json_error($self->language('ERR_CANNOT_OPEN', $file->{name}, $!));
        read (DATA, my $content, -s DATA);
        close DATA;

        if ($file->{type} eq 'html') {
            $file->{content} = $content;
        }
        else {
            $content =~ s,\r\n,\n,g;
            $file->{content} = $content;
        }

        return $self->print_json({
            html => $self->print('editor.html', { file => $file, editor_mode => ($file->{type} eq 'html' and $self->{default}{editor_mode}) ? 'html' : 'text', json => 1 }),
            type => $file->{type},
        }, 1);
    }

# Save file
    my $old     = $self->{cgi}{file};
    my $content = $self->{cgi}{editor};
    my $mode    = $self->{cgi}{editor_mode};

    return $self->print_json_error($self->language('ERR_NOSPACE')) if ($self->{diskspace} and (!$self->{diskspace}{free} or $self->{diskspace}{free} < length $content));

# Save content into a new file
    my ($history, $file);
    my $fname = $self->{cgi}{edit_input} || $self->{cgi}{file_input};
    if ($fname and $fname ne $old) {
        $file = $self->check_name($fname);
        return $self->print_json_error($file->{error}) if $file->{error};
        return $self->print_json_error({ file => $file, confirm => 1 }) if $file->{exist} and !$self->{cgi}{overwrite_confirmed};

        $history = "newfile|$self->{cgi}{name}";
    }

# Update file with new content
    else {
        $file = $self->check_name($old);
        return $self->print_json_error($file->{error}) if $file->{error};
        return $self->print_json_error($self->language('ERR_NOT_WRITEABLE', $file->{name})) unless $file->{write};

        $history = "edit|$old";
    }

    open(DATA, "> $file->{full_path}") or return $self->print_json_error($self->language('ERR_CANNOT_OPEN', $file->{name}, $!));
    print DATA $content;
    close DATA;

# Log the action
    $self->flog($history);
    return $self->print_json(undef, 1, $self->language('MSG_FILE_SAVED', $file->{name}));
}
END_OF_SUB

$COMPILE{makedir} = __LINE__ . <<'END_OF_SUB';
sub makedir {
# -----------------------------------------------------------------------------
# Create a new directory
#
    my $self = shift;

    my $dname = $self->{cgi}{makedir_input};
    return $self->print_json_error($self->language('ERR_INVALID_INPUT')) unless $dname;

    my $new = $self->check_name($dname);

    return $self->print_json_error($new->{error}) if $new->{error};
    return $self->print_json_error($self->language('ERR_EXISTING', $dname)) if $new->{exist};
    return $self->print_json_error($self->language('ERR_NOSPACE')) if $self->{diskspace} and !$self->{diskspace}{free};

    rmkdir($new->{full_path}, 0755, { untaint => 1 }) or return $self->print_json_error($self->language('ERR_MAKDEDIR', $dname, $!));
    $self->flog("makedir|$dname");

    return $self->print_json(undef, 1, $self->language('MSG_MAKEDIR', $dname));
}
END_OF_SUB

$COMPILE{chmod} = __LINE__ . <<'END_OF_SUB';
sub chmod {
# -----------------------------------------------------------------------------
# Change the permissions of a file
#
    my $self = shift;

# Change mod
    my @files = $self->{in}->param('cinput');
    my $mod   = $self->{cgi}{chmod_input};

    return $self->print_json_error($self->language('ERR_NOSELECTED'))  unless scalar @files;
    return $self->print_json_error($self->language('ERR_CHMOD_INPUT')) unless $mod;

# Untaint permission input
    ($mod) = $mod =~ /^([0-7]{3,4})$/;
    return $self->print_json_error($self->language('ERR_INVALID_PERM')) unless $mod;

    my $changed = 0;
    my $oct_mod = oct($mod);

    foreach my $f (@files) {
        my $file = $self->check_name($f);
        if ($file->{error}) {
            return $self->print_json_error($file->{error}) if scalar @files == 1; # Return if error occurs for a single file
            next;
        }

        if (-d $file->{full_path} and $self->{cgi}{chmod_recursive}) {
            find($file->{full_path}, sub { chmod($oct_mod, shift) }, { untaint => 1 });
            $changed++;
        }
        else {
            chmod($oct_mod, $file->{full_path}) and $changed++;
        }
    }
    return $self->print_json_error('cannot_chmod') unless $changed;

# Log the action
    $self->flog("chmod|" . join(', ', @files));
    $self->print_json(undef, 1, scalar @files == 1 ? $self->language('MSG_CHMODED', $files[0]) : $self->language('MSG_MULT_CHMODED', $changed));
}
END_OF_SUB

$COMPILE{protect} = __LINE__ . <<'END_OF_SUB';
sub protect {
# -----------------------------------------------------------------------------
# Protect command: create .htaccess file
#
    my $self = shift;

    my $pwd_path = $self->{default}{pwd_path};
    my ($htpwd, $htacc, $name_pwd, $name_acc);

# Create the .htpasswd
    if ($pwd_path) {
        my $path = $self->check_path($pwd_path);
        if (!$path->{error} and $path->{exist}) {
            my $current = $self->check_path();
            unless ($current->{error}) {
                $current->{full_path} =~ s/[\/ \:]/\_/g;
                $htpwd = "$path->{full_path}/.htpass$current->{full_path}";
                ($htpwd) = $htpwd  =~ /^(.*)$/; # Untaint the path
            }
        }
    }

    unless ($htpwd) {
        my $fpassword = $self->check_name('.htpasswd');
        return $self->print_json_error($fpassword->{error}) if $fpassword->{error};
        $htpwd = $fpassword->{full_path};
    }

    ($name_pwd) = $htpwd =~ /^$self->{cfg}{root_path}\/(.*)/;

    unless (-e $htpwd) {
        open (FILE, "> $htpwd") or return $self->print_json_error($self->language('ERR_CANNOT_OPEN', $name_pwd, $!));
        close FILE;
    }

# Create the .htaccess
    my $faccess = $self->check_name('.htaccess');
    return $self->print_json_error($faccess->{error}) if $faccess->{error};

    $htacc      = $faccess->{full_path};
    ($name_acc) = $htacc =~ /^$self->{cfg}{root_path}\/(.*)/;

    unless ($faccess->{exist} and $faccess->{size}) {
        my $error = $self->create_htaccess($htacc, $htpwd);
        return $self->print_json_error($error) if $error;
    }

    return $self->print_json_error($self->language('ERR_NOT_WRITABLE', $name_pwd)) unless -w $htpwd;
    return $self->print_json_error($self->language('ERR_NOT_WRITABLE', $name_acc)) unless -w $htacc;

# Overwrite existing .htaccess if AuthUserFile isn't matching with htpwd
    open (HTACC, "< $htacc") or return $self->print_json_error($self->language('ERR_CANNOT_OPEN', $name_acc, $!));
    my @info = <HTACC>;
    close HTACC;

    my $found;
    LINE: foreach (@info) {
        if ( $_ =~ /$htpwd/ ) {
            $found = 1;
            last;
        }
    }

    unless ($found) {
        my $error = $self->create_htaccess($htacc, $htpwd);
        return $self->print_json_error($error) if $error;
    }

# Delete all users
    if ($self->{cgi}{'protect-delete-all'}) {
        unlink $htpwd;
        unlink $htacc;
        return $self->print_json(undef, 1, $self->language('MSG_MULTUSER_DELETED', 'All'));
    }

# Create/delete a user
    my (@users, $msg);
    my $username = $self->{cgi}{protect_username};
    my $password = $self->{cgi}{protect_password};
    my $del_user = $self->{cgi}{protect_user};

    if (-s $htpwd) {
        open (HTPWD, "< $htpwd") or return $self->print_json_error($self->language('ERR_CANNOT_OPEN', $name_pwd, $!));
        @users = grep { $_ !~ /^$del_user:/} <HTPWD>;
        close HTPWD;
    }

    my $message;
    if ($username and $password) {
        return $self->print_json_error($self->language('ERR_UID_INVALID', $username)) if $username =~ /:/;

        foreach my $i (0 .. @users) {
            my $u = $users[$i];
            if ($u =~ /^$username:/) {
                return $self->print_json_error($self->language('ERR_UID_EXISTING', $username)) unless $self->{cgi}{opt_protect_overwrite};
                delete $users[$i];
            }
        }

        my $crypted;
        if ($self->{cgi}{opt_protect_apache}) {
            require GT::MD5::Crypt;
            my $salt = join '', ('A' .. 'Z', 0 .. 9, 'a' .. 'z', '.', '/')[map rand 64, 1 .. 8];
            $crypted = GT::MD5::Crypt::apache_md5_crypt($password, $salt);
        }
        else {
            my @salt_chars = ('A' .. 'Z', 0 .. 9, 'a' .. 'z', '.', '/');
            my $salt       = join '', @salt_chars[rand 64, rand 64];
            $crypted  = crypt($password, $salt);
        }
        push @users, "$username:$crypted\n";
        $message = $self->language('MSG_USER_ADDED', $username);
    }
    elsif ($self->{cgi}{protect} and !$del_user) {
        return $self->print_json_error($self->language('ERR_PROTECT_REQUIRED'));
    }
    elsif ($del_user) {
        $message = $self->language('MSG_USER_DELETED', $del_user);
    }

    if (@users) {
        open (HTPWD, "> $htpwd") or return $self->print_json_error($self->language('ERR_CANNOT_OPEN', $name_pwd, $!));
        print HTPWD join("", @users);
        close HTPWD;
        foreach (@users) {
            my ($u, $p) = split(/:/, $_);
        }
    }
    else {
        unlink $htpwd;
        unlink $htacc;
    }
    return $self->print_json(undef, 1, $message);
}
END_OF_SUB

$COMPILE{setup} = __LINE__ . <<'END_OF_SUB';
sub setup {
# -----------------------------------------------------------------------------
# Change configurations
#
    my $self = shift;

    return $self->print_json({ html => $self->print('setup.html', { json => 1 }) }, 1) if $self->{cgi}{form};

    my %required_fields = (cgi_url => 1, static_url => 1, root_path => 1);
    foreach (keys %required_fields) {
        return $self->print_json_error($self->language('MSG_REQUIRED', $_)) unless $self->{cgi}{$_};
    }

    if ($self->{cfg}{fversion} eq 'multiple') {
        return $self->print_json_error($self->language('ERR_MAIL_SERVER'))  unless $self->{cgi}{email_smtp_server} or $self->{cgi}{email_mail_path};
        return $self->print_json_error($self->language('ERR_INVALID_MAIL')) if $self->{cgi}{email_smtp_server} and $self->{cgi}{email_mail_path};
    }

    my $config = $self->{cfg};
    foreach my $k (keys %$config) {
        if (ref $config->{$k} eq 'HASH') {
            my $hsh = $config->{$k};
            $config->{$k} = $self->fsetup($hsh, $k);
        }
        elsif (!defined $self->{in}->param($k)) {
            next;
        }
        else {
            $config->{$k} = $self->{in}->param($k);
        }
    }

# Overwrite current configurations
    $self->{cfg} = $config;
    $self->{cfg}->save();

# Log the action
    $self->flog("setup|Change the configuration");
    $self->print_json(undef, 1, $self->language('MSG_CHANCES_SAVED'));
}

sub fsetup {
# -----------------------------------------------------------------------------
#
    my ($self, $cfg, $key) = @_;

    foreach my $k (keys %$cfg) {
        my $val = $cfg->{$k};
        if (ref $val eq 'HASH') {
            $cfg->{$k} = $self->fsetup($val, "${key}_$k");
        }
        elsif (defined $self->{in}->param("${key}_$k")) {
            $cfg->{$k} = $self->{in}->param("${key}_$k") || '';
        }
    }
    return $cfg;
}

END_OF_SUB

$COMPILE{preferences} = __LINE__ . <<'END_OF_SUB';
sub preferences {
# -----------------------------------------------------------------------------
# Update preferences
#
    my $self = shift;

    return $self->print_json({ html => $self->print('preferences.html', { json => 1 }) }, 1) if $self->{cgi}{form};

    my ($pwd_path, $default_path) = ('', '');
    if ($self->{cgi}{pwd_path}) {
        my $path = $self->check_path($self->{cgi}{pwd_path});
        return $self->print_json_error($path->{error}) if $path->{error};
        return $self->print_json_error($self->language('ERR_NOT_FOUND', $self->{cgi}{pwd_path})) unless $path->{exist};
        return $self->print_json_error($self->lanugage('ERR_NOT_FOLDER', $self->{cgi}{pwd_path})) unless $path->{isdir};
        $pwd_path = $path->{full_path};
    }

    if ($self->{cgi}{path}) {
        my $path = $self->check_path($self->{cgi}{path});
        return $self->print_json_error($path->{error}) if $path->{error};
        return $self->print_json_error($self->language('ERR_NOT_FOUND', $self->{cgi}{path})) unless $path->{exist};
        return $self->print_json_error($self->language('ERR_NOT_FOLDER', $self->{cgi}{path})) unless $path->{isdir};
        $default_path = $path->{full_path};
    }

    my %defaults = (
        pwd_path    => $pwd_path,
        path        => $default_path,
        maxhits     => $self->{cgi}{maxhits}     || 25,
        sb          => $self->{cgi}{sb}          || 'name',
        so          => $self->{cgi}{so}          || 'asc',
        readme      => $self->{cgi}{readme}      || 1,
        show_hidden => $self->{cgi}{show_hidden} || 0,
        editor_mode => $self->{cgi}{editor_mode} || 0,
        effect      => $self->{cgi}{effect}      || 1
    );
    my $defaults = join ';', map "$_=$defaults{$_}" , (keys %defaults);

    print $self->{in}->header(
        -cookie => [
            $self->{in}->cookie(-name => 'fileman_defaults', -value => $defaults, -expires => '+5y')
        ]
    );

    $self->{default} = $self->default(%defaults);
    $self->print_json({ defaults => $self->{default} }, 1, $self->language('MSG_CHANCES_SAVED'));
}
END_OF_SUB

$COMPILE{download} = __LINE__ . <<'END_OF_SUB';
sub download {
# -----------------------------------------------------------------------------
# Download command
#
    my $self = shift;

    my @files    = $self->{in}->param('cinput');
    my $opt_zip  = $self->{cgi}{download_compress};
    my $opt_mode = $self->{cgi}{download_mode};

    return $self->print_json_error($self->language('ERR_NOSELECTED')) unless scalar @files;

# Download a single file
    if (scalar @files == 1 and !$opt_zip) {
        my $file = $self->check_name($files[0]);
        return $self->print_json_error($file->{error}) if $file->{error};
        return $self->print_json_error($self->language('ERR_NOT_READABLE', $file->{name})) unless $file->{read};

        if ($file->{isfile}) {
            $self->flog("download|$file->{name}");
            return $self->print_json({ file => $file, mode => $opt_mode }, 1);
        }
        else {
            $opt_zip = 1;
        }
    }

# Download multiple files
    require GT::TempFile;
    my $tempfile = new GT::TempFile(tmp_dir => $self->{cfg}{tmp_path});
    my $ext      = 'tar';

    if ($opt_zip == 3 and $GT::FileMan::HAVE_AZIP) {
        $ext = 'zip';
        $self->create_zip(\@files, "$$tempfile.$ext");
    }
    else {
        $ext = ($opt_zip == 2 and $GT::FileMan::HAVE_GZIP) ? 'tar.gz' : 'tar';
        $self->create_tar(\@files, "$$tempfile.$ext");
    }

    $self->flog("download|$$tempfile.$ext");
    my $filename = "$$tempfile.$ext";
    $filename =~ s,^$self->{cfg}{tmp_path}/,,;

    return $self->print_json({
        file => {
            name   => $filename,
            source => $ext,
            mode   => 'binary'
        },
    }, 1);
}
END_OF_SUB

$COMPILE{cmdcopy} = __LINE__ . <<'END_OF_SUB';
sub cmdcopy {
# -----------------------------------------------------------------------------
# Copy command
#
    my ($self, $action, $to) = @_;

    $to ||= $self->{cgi}{copy_input};
    my @files  = $self->{in}->param('cinput');
    my $copied = $self->{cgi}{num_done} || 0;

    return $self->print_json_error($self->language('ERR_INVALID_PATH')) unless $to;
    return $self->print_json_error($self->language('ERR_NOSELECTED')) unless scalar @files or $self->{cgi}{'confirm-name'};

    my $path = $self->check_path($to);
    return $self->print_json_error($path->{error}) if $path->{error};

    if ($path->{exist}) {
        return $self->print_json_error($self->language('ERR_NOT_WRITABLE', $to)) unless $path->{write};
    }
    else {
        my ($fname)  = $path->{full_path}  =~ /\/([^\/]+)$/;
        (my $path_to = $path->{full_path}) =~ s/\/$fname$//;
        return $self->print_json_error($self->language('ERR_NOT_FOUND', $to)) unless -e $path_to;
        return $self->print_json_error($self->language('ERR_NOT_WRITABLE', $to)) unless -w $path_to;
    }

    if ($self->{diskspace} and !$action) { # check free space if appicable
        my $need_space = $self->size(\@files);
        return $self->print_json_error($self->language('ERR_NOSPACE')) unless ($self->{diskspace}{free} or $self->{diskspace}{free} > $need_space);
    }

    # Need a confirmation on overwrite existing files
    my @loop_files;
    if ($self->{cgi}{'confirm-name'}) {
        return $self->print_json(undef, 1, $self->language('MSG_COPIED', $copied, $to)) if $self->{cgi}{'button-cancel'};

        if ($self->{cgi}{'confirm-name'} eq 'copy-all') {
            foreach my $f (@files) {
                next unless $f;
                my $file = $self->check_name($f);
                next if $file->{error};

                if ($action eq 'move') {
                    move($file->{full_path}, $path->{full_path}, { untaint => 1 }) and $copied++;
                }
                else {
                    copy($file->{full_path}, $path->{full_path}, { untaint => 1 }) and $copied++;
                }
            }
        }
        elsif ($self->{cgi}{'confirm-name'} !~ /button-cancel|button-skip/) {
            my $fcurrent = $self->{cgi}{'confirm-name'};
            my $file = $self->check_name($fcurrent);
            unless ($file->{error}) {
                if ($action eq 'move') {
                    move($file->{full_path}, $path->{full_path}, { untaint => 1 }) and $copied++;
                }
                else {
                    copy($file->{full_path}, $path->{full_path}, { untaint => 1 }) and $copied++;
                }
            }

            foreach my $f (@files) {
                next unless $f and $f ne $fcurrent;
                push @loop_files, $f;
            }
        }
    }
    else {
        foreach my $f (@files) {
            my $file = $self->check_name($f);
            next if $file->{error} or $file->{full_path} eq $path->{full_path};

            if (-e "$path->{full_path}/$f") {
                push @loop_files, $f;
                next;
            }

            if ($action eq 'move') {
                move($file->{full_path}, $path->{full_path}, { untaint => 1 }) and $copied++;
            }
            else {
                copy($file->{full_path}, $path->{full_path}, { untaint => 1 }) and $copied++;
            }
        }
    }

# Need confirmation on overwrite existing files
    return $self->print_json({ confirms => \@loop_files, num_done => $copied, input => $to }, 1) if scalar @loop_files;

    my $history = $action eq 'move' ? "move" : "copy";
    my $msg     = $action eq 'move' ? $self->language('MSG_MOVED', $copied, $to) : $self->language('MSG_COPIED', $copied, $to);
    $self->flog("$history|To: $path->{full_path} ($copied files)");
    return $self->print_json(undef, 1, $msg);
}
END_OF_SUB

$COMPILE{cmdmove} = __LINE__ . <<'END_OF_SUB';
sub cmdmove {
# -----------------------------------------------------------------------------
# Move command
#
    my $self = shift;
    return $self->cmdcopy('move', $self->{cgi}{'move_input'});
}
END_OF_SUB

$COMPILE{rename} = __LINE__ . <<'END_OF_SUB';
sub rename {
# -----------------------------------------------------------------------------
# Rename command
#
    my $self = shift;

    my $to = $self->{cgi}{rename_input};
    my $f  = $self->{cgi}{cinput};

    return $self->print_json_error($self->language('ERR_RENAME_INPUT')) unless $to;
    return $self->print_json_error($self->language('ERR_NOSELECTED')) unless $f;
    return $self->print_json_error($self->language('ERR_CANNOT_RENAME', $to)) if $to =~ /\//;
    return $self->print_json_error($self->language('ERR_MULT_SELECTED')) if ref $f eq 'ARRAY';

    my $path = $self->check_path($to);
    return $self->print_json_error($path->{error}) if $path->{error};
    return $self->print_json({ file => { name => $to }, confirm => 1 }, 1) if $path->{exist} and !$self->{cgi}{overwrite_confirmed};

    my ($fname) = $path->{full_path}  =~ /\/([^\/]+)$/;
    (my $path_to = $path->{full_path}) =~ s/\/$fname$//;
    return $self->print_json_error($self->language('ERR_NOT_FOUND', $to)) unless -e $path_to;
    return $self->print_json_error($self->language('ERR_NOT_WRITABLE', $self->{cfg}{work_path})) unless -w $path_to;

    my $file = $self->check_name($f);
    return $self->print_json_error($file->{error}) if $file->{error};

    move($file->{full_path}, $path->{full_path}, { untaint => 1 });

    $self->flog("Rename|From: $f - To:$fname");
    $self->print_json(undef, 1, $self->language('MSG_RENAMED', $f, $fname));
}
END_OF_SUB

$COMPILE{delete} = __LINE__ . <<'END_OF_SUB';
sub delete {
# -----------------------------------------------------------------------------
# Delete files & directories
#
    my $self = shift;

    my @files = $self->{in}->param('cinput');
    return $self->print_json_error($self->language('ERR_NOSELECTED')) unless scalar @files or $self->{cgi}{'confirm-name'};


    my $num_done  = $self->{cgi}{num_done} || 0;
    my $confirmed = $self->{cgi}{'confirm-name'} || '';

    my (@deleted, @loop_files);
    foreach my $f (@files) {
        my $file = $self->check_name($f);
        next if $file->{error} or !$file->{exist};

        # Delete confirmed files and folders
        if ($confirmed eq 'delete-all' or $confirmed eq $f) {
            my $deleted = 0;
            if ($file->{isfile}) {
                del($file->{full_path}, { untaint => 1 }) and $deleted = 1;
            }
            else {
                deldir($file->{full_path}, { untaint => 1 }) and $deleted = 1;
            }
            if ($deleted) {
                $num_done++;
                push @deleted, $file->{full_path};
            }
        }
        elsif (!$confirmed or $confirmed !~ /button-skip|button-cancel/) {
            push @loop_files, $f;
        }
    }

# Log deleted files
    $self->flog("delete|".join("; ", @deleted)) if scalar @deleted;

# Need to confirm on deleting selected files
    return $self->print_json({ confirms => \@loop_files, num_done => $num_done }, 1) if scalar @loop_files;
    return $self->print_json(undef, 1, $self->language($num_done > 1 ? 'MSG_MULT_DELETED' : 'MSG_DELETED', $num_done));
}
END_OF_SUB

$COMPILE{symlink} = __LINE__ . <<'END_OF_SUB';
sub symlink {
# -----------------------------------------------------------------------------
# Symlink command
#
    my $self = shift;

    my @files = $self->{in}->param('cinput');
    my $to    = $self->{cgi}{symlink_input};
    return $self->print_json_error($self->language('ERR_SYMLINK_INPUT')) unless $to;
    return $self->print_json_error($self->language('ERR_NOSELECTED')) unless scalar @files;

    my $path = $self->check_path($to);
    return $self->print_json_error($path->{error}) if $path->{error};
    return $self->print_json_error($self->language('ERR_NOT_FOUND', $to)) unless $path->{exist};
    return $self->print_json_error($self->language('ERR_NOT_WRITABLE', $to)) unless $path->{write};

    my $links = 0;
    foreach my $f (@files) {
        my $file = $self->check_name($f, 1);
        next if $file->{error} or $file->{full_path} eq $path->{full_path};

# Untaint the path
        my $p = "$path->{full_path}/$f";
        ($p) = $p  =~ /^(.*)$/;
        symlink($file->{full_path}, $p) and $links++;
    }

    return $self->print_json(undef, 1, $self->language('MSG_LINKED', $links, $to));
}
END_OF_SUB

$COMPILE{compress} = __LINE__ . <<'END_OF_SUB';
sub compress {
# -----------------------------------------------------------------------------
# Compress files and directories
#
    my $self = shift;

    my @files = $self->{in}->param('cinput');
    my $loc   = $self->{cgi}{compress_input};
    my $mode  = $self->{cgi}{compress_mode} || 1;

    return $self->print_json_error($self->language('ERR_GZIP_REQUIRED')) if $mode == 2 and !$GT::FileMan::HAVE_GZIP;
    return $self->print_json_error($self->language('ERR_AZIP_REQUIRED')) if $mode == 3 and !$GT::FileMan::HAVE_AZIP;
    return $self->print_json_error($self->language('ERR_NOSELECTED')) unless scalar @files;

# Verify file name as well as location to store the compressed file
    my $fname;
    unless ($loc) {
        my $path = $self->check_path(undef);
        return $self->print_json_error($path->{error}) if $path->{error};
        return $self->print_json_error($self->language('ERR_NOT_WRITABLE', $path->{full_path})) unless $path->{write};
        return $self->print_json_error($self->language('ERR_COMPRESS_INPUT')) if scalar @files > 1;

        ($fname) = $files[0] =~ /([^.]+)/;
        return $self->print_json_error($self->language('ERR_COMPRESS_INPUT')) unless $fname;
    }
    elsif ($loc =~ /\/$/) {
        return $self->print_json_error($self->language('ERR_COMPRESS_INPUT')) if scalar @files > 1;

        ($fname) = $files[0] =~ /([^.]+)/;
        return $self->print_json_error($self->language('ERR_COMPRESS_INPUT')) unless $fname;
        $fname = "$loc$fname";
    }
    else {
        $fname = $loc;
    }

# Handling compressed file extension
    ($fname) = $fname =~ /([^.]+)/;
    if ($mode > 1) {
        $fname .= $mode == 2 ? '.tar.gz' : '.zip';
    }
    else {
        $fname .= '.tar';
    }

    my $file = $self->check_name($fname);
    return $self->print_json_error($file->{error}) if $file->{error};
    return $self->print_json_error($self->language('ERR_NOSPACE')) if $self->{diskspace} and !$self->{diskspace}{free};
    return $self->print_json_error($self->language('ERR_FILENAME_EXISTING')) if scalar @files == 1 and $files[0] eq $fname;

    return $self->print_json_error({ file => $file, confirm => 1 }) if $file->{exist} and !$self->{cgi}{overwrite_confirmed};

    if ($mode == 3) {
        $self->create_zip(\@files, $file->{full_path});
    }
    else {
        $self->create_tar(\@files, $file->{full_path});
    }

    if ($self->{diskspace} and $self->{diskspace}{free} < -s $file->{full_path}) {
        unlink $file->{full_path};
        return $self->print_json_error('nospace');
    }

    $self->flog("compress|$file->{name}");

    return $self->print_json(undef, 1, $self->language('MSG_COMPRESSED', $file->{name}));
}
END_OF_SUB

$COMPILE{uncompress} = __LINE__ . <<'END_OF_SUB';
sub uncompress {
# -----------------------------------------------------------------------------
# uncompress a file
#
    my $self = shift;

    my @files = $self->{in}->param('compress');
    my $to    = $self->{cgi}{uncompress_input};
    my $scope = $self->{cgi}{uncompress_scope};
    my $fname = $self->{cgi}{compressed_file};

    my $file  = $self->check_name($fname);
    my $path  = $self->check_path($to);

    return $self->print_json_error($file->{error} || $path->{error}) if $file->{error} or $path->{error};
    return $self->print_json_error($self->language('ERR_NOSELECTED')) if $scope and !scalar @files;
    return $self->print_json_error($self->language('ERR_NOT_FOUND', $fname)) unless $file->{exist};
    return $self->print_json_error($self->language('ERR_NOT_READABLE', $fname)) unless $file->{read};
    return $self->print_json_error($self->language('ERR_NOT_WRITABLE', $to)) if $path->{exist} and !$path->{write};
    return $self->print_json_error($self->language('ERR_NOSPACE')) if ($self->{diskspace} and !$self->{diskspace}{free});

# Untaint the path
    ($path->{full_path}) = $path->{full_path}  =~ /^(.*)$/;

# Create the target folder if it does not exist
    unless ($path->{exist}) {
        rmkdir($path->{full_path}, 0755, { untaint => 1 }) or return $self->print_json_error($self->language('ERR_MAKEDIR', $to, $!));
    }

    my ($copied, $total_size, $hits) = (0, 0, 0);
    my %error;
    if ($file->{full_path} =~ /.zip$/i) {
        require Archive::Zip;
        my $zip = Archive::Zip->new($file->{full_path}) or return $self->print_json_error($self->language('ERR_CANNOT_OPEN', $fname, $!));
        my @members = $zip->members();
        $hits       = scalar @members;
        foreach my $f (@members) {
            last if ($self->{diskspace} and $total_size + $f->uncompressedSize > $self->{diskspace}{free});
            my $name  = $f->fileName;
            my $found = ($scope and scalar @files) ? 0 : 1;
            if ($scope) {
                foreach (@files) {
                    next if $_ ne $name;
                    $found = 1;
                    last;
                }
            }
            next unless $found;

            $copied++;
            $total_size += $f->uncompressedSize;

            # Untaint file name
            ($name) = $name  =~ /^(.*)$/;
            $zip->extractMember($name, "$path->{full_path}/$name");
        }
    }
    else {
        require GT::Tar;
        my $tar    = GT::Tar->open($file->{full_path}) or return $self->print_json_error($self->language('ERR_CANNOT_OPEN', $fname, $GT::Tar::error));
        my $cfiles = $tar->files;
        $hits      = scalar @$cfiles;

        foreach my $f (@$cfiles) {
            last if ($self->{diskspace} and $total_size + $f->{size} > $self->{diskspace}{free});
            my $name  = $f->{name};
            my $found = ($scope and scalar @files) ? 0 : 1;
            if ($scope) {
                foreach (@files) {
                    next if $_ ne $name;
                    $found = 1;
                    last;
                }
            }
            next unless $found;
            $total_size += $f->{size};
            $copied++;
            $f->write($path->{full_path}, { untaint => 1 });
        }
    }

    $self->flog("cmd_uncompress|$fname");
    $self->print_json(undef, 1, $self->language('MSG_UNCOMPRESSED', $fname, $hits, $copied));
}
END_OF_SUB

$COMPILE{tail} = __LINE__ . <<'END_OF_SUB';
sub tail {
# -----------------------------------------------------------------------------
# tail command
#
    my $self = shift;

    my $lines  = $self->{cgi}{tail_input} || 25;
    my $retime = $self->{cgi}{tail_retime};
    my $fname  = $self->{cgi}{cinput};

    return $self->print_json_error($self->language('ERR_NOSELECTED')) unless $fname;
    return $self->print_json_error($self->language('ERR_MULT_SELECTED')) if ref $fname eq 'ARRAY';

    my $file = $self->check_name($fname);
    return $self->print_json_error($file->{error}) if $file->{error};
    return $self->print_json_error($self->language('ERR_NOTTEXT_FILE', $fname)) unless $file->{isfile} and $file->{text};
    return $self->print_json_error($self->language('ERR_READABLE', $fname)) unless $file->{read};
    return $self->print_json_error($self->language('ERR_EMPTY_FILE', $fname)) unless $file->{size};

    my ($follow, $buffer, $content);
    @ARGV = grep { if ($_ eq "-f") { $follow++; 0 } else { 1 } } @ARGV;
    open DATA, "<$file->{full_path}" or return $self->print_json_error($self->language('ERR_CANNOT_OPEN', $fname, $!));

    my $read_size = $file->{size} > $READ_SIZE ? $READ_SIZE : $file->{size};
    seek DATA, -$read_size, 2;
    read DATA, $buffer, $read_size;
    my $read  = $read_size;
    $lines--;

    while () {
        if ($buffer =~ /\n(.*(?:\n.*){$lines}\n?$)/) {
            $content .= $1;
            last;
        }
        $read_size = ($file->{size} - $read > $READ_SIZE) ? $READ_SIZE : $file->{size} - $read;
        unless ($read_size == 0) {
            $content .= $buffer;
            last;
        }
        seek DATA, -($read_size + $read), 2;
        $read += $read_size;
        my $new_buffer;
        my $bytes_read = read DATA, $new_buffer, $read_size;
        if ($bytes_read == 0) {
            $content .= $buffer;
            last;
        }
        $buffer = $new_buffer . $buffer;
    }

    my $count = 0;
    if ($follow) {
        seek DATA, 0, 2; # Seek to the end of the file
        while () {
            select undef, undef, undef, 1;
            seek DATA, 0, 1 or last; # Reset eof(FILE)
            print while <DATA>;
            seek DATA, 0, 2;
            last if ($count++ > 60); # Only run for one min max.
        }
    }
    $self->print_json({ output => $self->{in}->html_escape($content), refresh => $retime }, 1);
}
END_OF_SUB

$COMPILE{perl} = __LINE__ . <<'END_OF_SUB';
sub perl {
# -----------------------------------------------------------------------------
# Perl command
#
    my $self = shift;

    my @files = $self->{in}->param('cinput');
    return $self->print_json_error($self->language('ERR_PERL_SELECTED')) unless @files;

    $ENV{'PATH'} = '/bin:/usr/bin:/usr/local/bin'; #for taint mode

    my @output;
    foreach my $f (@files) {
        my $file = $self->check_name($f);
        if ($file->{error}) {
            push @output, { name => $f, error => $file->{error} };
            next;
        }
        elsif (!$file->{read}) {
            push @output, { name => $f, error => $self->language('ERR_NOT_READABLE', $file->{full_path}) };
            next;
        }
        elsif (!$file->{isfile} or !$file->{text}) {
            push @output, { name => $f, error => $self->language('ERR_NOTPERL_FILE', $file->{full_path}) };
            next;
        }
        my ($ext) = $f =~ /\.([^.]+)$/;
        if ($ext !~ /^(?:cgi|pl|pm)$/i) { push @output, { name => $f, error => $self->language('ERR_NOTPERL_FILE', $file->{full_path}) }; next; }

        my ($fname) = $file->{full_path} =~ /^$self->{cfg}{root_path}(.*)/;
        my $result  = fsystem($self->{cfg}{path_to_perl},
                '-cw -I',
                "$self->{cfg}{private_path}/lib",
                $file->{full_path},
        );
        push @output, { name => $f, %$result };
    }
    $self->print_json({ files => \@output }, 1);
}
END_OF_SUB

$COMPILE{diff} = __LINE__ . <<'END_OF_SUB';
sub diff {
# -----------------------------------------------------------------------------
# Diff Command
#
    my $self = shift;

    my $fname1 = $self->{cgi}{cinput};
    my $fname2 = $self->{cgi}{diff_input};
    return $self->print_json_error($self->language('ERR_NOSELECTED')) unless $fname1;
    return $self->print_json_error($self->language('ERR_DIFF_INPUT')) unless $fname2;
    return $self->print_json_error($self->language('ERR_MULT_SELECTED')) if ref $fname1 eq 'ARRAY';

    my $file1 = $self->check_name($fname1);
    my $file2 = $self->check_name($fname2);
    return $self->print_json_error($file1->{error} || $file2->{error}) if $file1->{error} or $file2->{error};
    return $self->print_json_error($self->language('ERR_NOT_FOUND', $fname1)) unless $file1->{exist};
    return $self->print_json_error($self->language('ERR_NOT_FOUND', $fname2)) unless $file2->{exist};
    return $self->print_json_error($self->language('ERR_NOTTEXT_FILE', $fname1)) unless $file1->{text};
    return $self->print_json_error($self->language('ERR_NOTTEXT_FILE', $fname2)) unless $file2->{text};

    require GT::FileMan::Diff;
    my $diff = GT::FileMan::Diff::html_diff($file1->{full_path}, $file2->{full_path}, 3);
    return $self->print_json_error($self->language('ERR_CANNOT_OPEN', ($diff == 1 ? $fname1 : $fname2), $!)) unless ref $diff;

    $self->print_json({ output => $$diff }, 1);
}
END_OF_SUB

$COMPILE{preview} = __LINE__ . <<'END_OF_SUB';
sub preview {
    my $self = shift;

    my $file = $self->check_name($self->{cgi}{f});
    return $self->print_json_error($self->language('ERR_INVALID_FILE')) if $file->{error};
    return $self->print_json_error($self->language('ERR_NOT_FILE', $file->{name})) unless $file->{isfile};
    return $self->print_json_error($self->language('ERR_NOT_READABLE', $file->{name})) unless $file->{read};

    if ($file->{type} =~ /^(?:text|html)$/) {
        open(TEXT, $file->{full_path}) or return $self->print_json_error($self->language('ERR_CANNOT_OPEN', $file->{name}, $!));
        read TEXT, my $content, -s TEXT;
        close TEXT;

        $file->{content} = $self->{in}->html_escape($content);
    }
    elsif ($file->{type} eq 'compress') {
        return $self->open_compressed($file);
    }
    return $self->print_json({ file => $file }, 1);
}
END_OF_SUB

$COMPILE{fdownload} = __LINE__ . <<'END_OF_SUB';
sub fdownload {
# -----------------------------------------------------------------------------
#
    my $self = shift;

    my $fname = $self->{cgi}{f};
    my $mode  = $self->{cgi}{mode} || 'auto';

    return if ref $fname eq 'ARRAY';

    my $file;
    if ($self->{cgi}{type}) {
# Untaint the path
        my $full_path = "$self->{cfg}{tmp_path}/$fname";
        ($full_path) = $full_path  =~ /^(.*)$/;

        $file = {
            full_path => $full_path,
            name      => 'download.' . $self->{cgi}{type},
            size      => -s $full_path
        }
    }
    else {
        $file = $self->check_name($fname);
        return if $file->{error} or !$file->{isfile};
    }

    $mode = 'ascii' if ($mode and lc $mode eq 'auto' and $file->{text});
    open(DATA, $file->{full_path}) or return $self->print_json_error($self->language('ERR_CANNOT_OPEN', $file->{name}, $!));
    if ($file->{name} =~ /.zip$/i) {
        print $self->{in}->header(
            '-type'                      => 'application/octect-stream',
            '-Content-Length'            => $file->{size},
            '-Content-Transfer-Encoding' => 'binary',
            '-Content-Disposition'       =>  \(qq/attachment; filename="$file->{name}"/ . (defined($file->{size}) ? "; size=$file->{size}" : ''))
        );
    }
    else {
        print $self->{in}->header($self->{in}->file_headers(
            filename => $file->{name},
            inline   => 0,
            size     => $file->{size}
        ));
    }

    binmode STDOUT if $GT::FileMan::MSWIN;
    binmode DATA;

    my $newlines = $GT::FileMan::MSWIN ? "\r\n" : "\n";
    my $buffer;
    while (read(DATA, $buffer, $READ_SIZE)){
        $buffer =~ s,$newlines,\n,g if $mode eq 'ascii';
        print $buffer;
    }
    close DATA;
}
END_OF_SUB

$COMPILE{env} = __LINE__ . <<'END_OF_SUB';
sub env {
# -----------------------------------------------------------------------------
# Print out environment
#
    my $self = shift;

    my $env = GT::FileMan::base_env($self->{in}, $self->{cfg}{version}, $self->{commands});
    $self->print_json({ html => $self->print('env.html', { json => 1, env => \$env }) }, 1);
}
END_OF_SUB

$COMPILE{login} = __LINE__ . <<'END_OF_SUB';
sub login {
# -----------------------------------------------------------------------------
#
    my ($self, %args) = @_;

    return $self->print('home.html') unless $self->{cgi}{login};
    return $self->print_json_error($self->language('ERR_INVALID_LOGIN')) unless $self->{cgi}{username} and $self->{cgi}{password};
    return $self->print_json_error($self->language('ERR_INVALID_USERNAME')) if $self->{cfg}{login}{username} ne $self->{cgi}{username};
    return $self->print_json_error($self->language('ERR_INVALID_PASSWORD')) if $self->{cfg}{login}{password} ne GT::FileMan::encrypt($self->{cgi}{password}, $self->{cfg}{login}{password}) and $self->{cfg}{login}{password} ne crypt($self->{cgi}{password}, $self->{cfg}{login}{password});

    my $session = $self->session_create($self->{cfg}{login}, $self->{cgi}{cookie});
    return $self->print_json_error($self->language('ERR_SESSION')) unless $session;

    my $user = $self->{cfg}{login};
    $user->{permission} = $self->{cfg}{permission};
    $self->{session}    = { id => $session->{id}, user => $user };

    $self->flog("login|$user->{username}");
    $self->print_json(undef, 1);
}
END_OF_SUB

$COMPILE{logout} = __LINE__ . <<'END_OF_SUB';
sub logout {
# -----------------------------------------------------------------------------
#
    my ($self, $error) = @_;

    $self->session_delete();
    $self->flog('logout|Logged out');

    $self->{session} = undef;
    my %args = ( json => 1 );
    if ($error) {
        $args{error} = $error
    }
    else {
        $args{message} = $self->language('MSG_LOGOUT');
    }
    $self->print_json({ html => $self->print('login.html', \%args) }, 1, undef, 'LOGGED_OUT');
}
END_OF_SUB

$COMPILE{help} = __LINE__ . <<'END_OF_SUB';
sub help {
# -----------------------------------------------------------------------------
#
    my $self = shift;
    my $page = $self->{cgi}{page} || 'help.html';

    $page = 'help.html' if $page ne 'help.html' and $page ne 'quicktip.html';
    ($page) = $page =~ /^(.*)$/;

    $self->print($page);
}
END_OF_SUB

$COMPILE{cmdprint} = __LINE__ . <<'END_OF_SUB';
sub cmdprint {
# -----------------------------------------------------------------------------
# Print selected file(s)
#
    my $self = shift;

    my @input = $self->{in}->param('cinput');

# Check the selected files
    my @files;
    foreach my $f (@input) {
        my $file = $self->check_name($f);
        next unless !$file->{error} and $file->{isfile} and $file->{text};

        push @files, $file;
    }
    return $self->print_json_error($self->language('ERR_PRINT')) unless scalar @files;
    return $self->print_json({ files => \@files }, 1) unless $self->{cgi}{print};


    my $output = qq~
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    <html><header><title>Gossamer Threads - FileMan</title></header><body onload="window.print()" style="">
~;

    if ($self->{cgi}{all}) { # Print multiple files
        my $flag = 0;
        foreach my $f (@files) {
            open (DATA, "< $f->{full_path}") or next;
            read (DATA, my $data, -s DATA);
            close DATA;


            my $style = $flag ? 'style="page-break-before: always;"' : '';
            $output .= qq|<div $style><hr size=1><pre>$data</pre><br /></div>|;
            $flag++;
        }
    }
    else { # Print single file
        my $file     = pop @files;
        my $next_url = '';
        if (scalar @files) {
            my $hiddens = $self->hiddens();
            $next_url = $self->{in}->url(absolute => 1, query_string => 0) . "?cmd=print$hiddens->{hidden_query};print=1";
            foreach (@files) {
                $next_url .= ";cinput=" . $self->{in}->escape($_->{name});
            }
        }
        open (DATA, "< $file->{full_path}") or return $self->home(error => $self->language('ERR_CANNOT_OPEN', $file->{name}, $!));
        read (DATA, my $data, -s DATA);
        close DATA;

        $output .= qq|<hr size=1><pre>$data</pre><br />|;
        $output = sprintf(qq|<a href="%s"><img src="$self->{cfg}{static_url}/$self->{cfg}{template}/images/paging-next.gif" border="0" /></a>|, $next_url) . $output if $next_url;
    }
    $output .= qq!</body></html>!;

    print $self->{in}->header;
    print $output;
}
END_OF_SUB

sub open_compressed {
# -----------------------------------------------------------------------------
# Open a compressed file
#
    my ($self, $file) = @_;

    my ($ext) = $file->{name} =~ /\.([^.]+)$/;

    my @files;
    my $total_size = 0;
    if (lc $ext eq 'zip') {
        require Archive::Zip;
        my $zip = new Archive::Zip($file->{full_path}) or return $self->print_json_error($self->language('ERR_CANNOT_OPEN', $file->{name}, $!));
        foreach my $f ($zip->members) {
            my $type = $f->isDirectory ? FOLDER : FILE;
            my $name = $f->fileName;
            my $spec = ficon($name, $type);
            my $perm = $f->unixFileAttributes;
            my $date = $f->lastModTime;
            my $size = $f->compressedSize;
            my @info = (
                $type,
                $spec->{icon},
                $name,
                $size,
                $spec->{type},
                $date,
                '',
                $perm,
                permission($perm),
                GT::Date::date_get($date, $self->{cfg}{date_format} || '%yyyy%-%mm%-%dd%'),
            );
            $total_size += $size;
            push @files, \@info;
        }
    }
    else {
        require GT::Tar;
        my $tar   = GT::Tar->open($file->{full_path}) or return $self->print_json_error($self->language('ERR_CANNOT_OPEN', $file->{name}, $!));
        my $files = $tar->files;
        foreach my $f (@$files) {
            my $type  = $f->{type} == 5 ? FOLDER : FILE;
            my $spec  = ficon($f->{name}, $type);
            my $owner = (eval {getpwuid($f->{uid})} || '') . ':' . (eval {getgrgid($f->{gid})} || '');
            my @info = (
                $type,
                $spec->{icon},
                $f->{name},
                $f->{size},
                $spec->{type},
                $f->{mtime},
                $owner,
                $f->{mode},
                permission($f->{mode}),
                GT::Date::date_get($f->{mtime}, $self->{cfg}{date_format} || '%yyyy%-%mm%-%dd%'),
            );

            $total_size += $f->{size};
            push @files, \@info;
        }
    }

    return $self->print_json({ html => $self->print('compressed.html', { file => $file, json => 1 }), files => \@files, compressed => 1 }, 1);
}

sub create_zip {
# -----------------------------------------------------------------------------
#
    my ($self, $files, $to) = @_;

    require Archive::Zip::Tree;
    my $zip = Archive::Zip->new();

    foreach my $f (@$files) {
        my $file = $self->check_name($f);
        next unless !$file->{error} and $file->{read};

# Untaint the path
        ($file->{full_path}) = $file->{full_path}  =~ /^(.*)$/;
        ($f) = $f  =~ /^(.*)$/;
        if (-d $file->{full_path}) {
            my @files;
            find($file->{full_path}, sub {
                my $fp = shift;
                my $fn = $fp;
                $fn =~ s/$self->{cfg}{root_path}\///;
                if (-d $fp) {
                    $zip->addDirectory($fp, $fn) or warn "$!";
                }
                else {
                    $zip->addFile($fp, $fn) or warn "$!";
                }
            }, { untaint => 1 });
        }
        elsif ($file->{isfile}) {
            $zip->addFile($file->{full_path}, $f) or warn "$!";
        }
    }
    my $error = $zip->writeToFileNamed($to) ? $! : '';
    return $self->home({ error => { cannot_zip => 1, message => $error }}) if $error;
}

sub create_tar {
# -----------------------------------------------------------------------------
#
    my ($self, $files, $to) = @_;

    require GT::Tar;
    my $fpath = $self->check_path();
    my $tar   = new GT::Tar($to);

    my $from = $fpath->{error} ? '' : $fpath->{full_path};
    foreach my $f (@$files) {
        my $file = $self->check_name($f);
        next unless !$file->{error} and $file->{read};
        $tar->add_file($file->{full_path});
    }

    my $items = $tar->files;
    foreach my $f (@$items) {
        $f->{name} =~ s,$from/,,;
    }
    $tar->write($to);
}

sub current_path {
# -----------------------------------------------------------------------------
#
    my $self = shift;

    my $work_path = $self->{cfg}{work_path};
    return unless $work_path;

    my (@paths, $parent);
    my $work_folders = [split /\//, $work_path];

    my $spath = '';
    foreach my $f (@$work_folders) {
        next unless $f;
        push @paths, { folder => $f, path => $spath };
        $spath .= $spath ? "/$f" : $f;

        $parent = '/';
        foreach my $i (0..$#$work_folders - 1) {
            $parent .= ($parent =~ /\/$/) ? $work_folders->[$i] : "/$work_folders->[$i]";
        }
    }
    return { parent => $parent, work_path => $work_path, loop => \@paths };
}

sub finfo {
# -----------------------------------------------------------------------------
# Load file information
#
    my ($self, $full_path, $fname) = @_;

    my $path = $self->check_path();
    my $work_path = $path->{error} ? '' : $path->{full_path};
    my ($located) = $full_path =~ /^$work_path\/(.*)/;
    $located      =~ s,/$fname$,,;

    my (@stat, $type, $link);
    if (-l $full_path) {
        @stat = lstat($full_path);
        $type = -d $full_path ? FOLDER : FILE;
        $link = readlink $full_path;
    }
    else {
        @stat = stat($full_path);
        $type = -d $full_path ? FOLDER : FILE;
    }

    my $spec  = ficon($full_path, $type);
    my $owner = (eval {getpwuid($stat[4])} || '') . ':' . (eval {getgrgid($stat[5])} || '');
    my @info  = (
        $type,
        $link ? 'symlink' : $spec->{icon},
        $self->{in}->html_escape($fname),
        $type == FOLDER ? 0 : $stat[7],
        $link ? $ICONS{symlink}->[1] : $spec->{type},
        $stat[9],
        $owner,
        sprintf("%04o", ($stat[2] & 07777)),
        permission($stat[2]),
        GT::Date::date_get($stat[9], $self->{cfg}{date_format} || '%yyyy%-%mm%-%dd%'),
        $located eq $fname ? '' : $located,
        $link || $full_path,
    );
    return \@info;
}

sub friendly_size {
# -----------------------------------------------------------------------------
# Prints out the file size.
#
    my $size = shift;
    return $size <= 100
        ? "$size Bytes"
        : $size < 10 * KB
          ? sprintf("%.2f ", $size / KB) . 'KB'
          : $size < 100 * KB
            ? sprintf("%.2f ", $size / KB) . 'KB'
            : $size < MB
              ? sprintf("%.2f ", $size / KB) . 'KB'
              : $size < 10 * MB
                ? sprintf("%.2f ", $size / MB) . 'MB'
                : $size < 100 * MB
                  ? sprintf("%.2f ", $size / MB) . 'MB'
                  : sprintf("%.2f ", $size / MB) . 'MB';
}

sub permission {
# -----------------------------------------------------------------------------
# Takes permissions supplied from stat() and prints out in ls -al format.
#
    my $octal = shift;

    my $string = sprintf "%04o", ($octal & 07777);
    my @perms = split '', $string;

    my $result = '--- --- ---';
    my @extra_map = (
        { mask => 0x4, char => 's' },
        { mask => 0x2, char => 's' },
        { mask => 0x1, char => 't' },
    );

    for (my $i = 1; $i < @perms; $i++) {
        my $j = $i - 1;

        substr($result, $j * 4 + 0, 1, 'r') if 0x4 & $perms[$i];
        substr($result, $j * 4 + 1, 1, 'w') if 0x2 & $perms[$i];

# The display of execute needs to be handled differently as it also shows the
# additional permissions
        my $exec = 0x1 & $perms[$i];
        my $char;
        $char = 'x' if $exec;
        $char = $extra_map[$j]->{char} if $extra_map[$j]->{mask} & $perms[0];
        $char = uc $char if !$exec and $char ne 'x';
        substr($result, $j * 4 + 2, 1, $char) if $char;
    }
    return $result;
}

sub ficon {
# -----------------------------------------------------------------------------
# Get the associated icon based on a files extension
#
    my ($file, $type) = @_;

    return { icon => $ICONS{folder}[0], type => $ICONS{folder}[1] } if $type == FOLDER;

    my ($ext)  = $file =~ /\.([^.]+)$/;
    return { icon => $ICONS{unknown}[0], type => $ICONS{unknown}[1] } unless $ext;

    foreach (keys %ICONS) {
        next if $_ =~ /^(?:folder|unknown|parent)$/;
        return { icon => $ICONS{$_}[0], type => $ICONS{$_}[1]} if $_ =~ /\b\Q$ext\E\b/i;
    }

    return { icon => $ICONS{unknown}[0], type => $ICONS{unknown}[1] };
}

sub load_htpasswd {
# -----------------------------------------------------------------------------
# Load .htpasswd
#
    my $self = shift;

    my $pwd_path = $self->{default}{pwd_path};
    my $htpwd;
    if ($pwd_path) {
        my $path = $self->check_path($pwd_path);
        if (!$path->{error} and $path->{exist}) {
            my $current = $self->check_path();
            $current->{full_path} =~ s/[\/ \:]/\_/g;
            $htpwd = "$path->{full_path}/.htpass$current->{full_path}";
            ($htpwd) = $htpwd  =~ /^(.*)$/; # Untaint the path
        }
    }

    unless ($htpwd) {
        my $fpassword = $self->check_name('.htpasswd');
        return if $fpassword->{error};
        $htpwd = $fpassword->{full_path};
    }

    my $faccess = $self->check_name(".htaccess");
    return unless !$faccess->{error} and $htpwd;

    my $htacs = $faccess->{full_path};
    if (-e $htpwd and $faccess->{exist}) {
        my ($name_pwd) = $htpwd =~ /^$self->{cfg}{root_path}\/(.*)/;
        open (HTPWD, "< $htpwd") or return;
        my @users = map { /^([^:]+)/ ? qq:$1: : qq:'': } <HTPWD>;
        close HTPWD;
        return \@users;
    }
}

sub create_htaccess {
# -----------------------------------------------------------------------------
# Creates the htaccess file.
#
    my ($self, $htaccess, $htpasswd) = @_;

    my $raq = $ENV{GT_COBALT_RAQ} ? "AuthPAM_Enabled off\n" : '';
    open (HTAC, "> $htaccess") or return "open_file: $htaccess - $!";
    print HTAC <<HTACCESS;
AuthUserFile   $htpasswd
AuthGroupFile  /dev/null
AuthType       Basic
AuthName       Protected
$raq
require valid-user
HTACCESS
    close HTAC;
    return;
}

sub fcopy {
# -----------------------------------------------------------------------------
# Copy and replace a file
#
    my ($from, $to, $replace, $with) = @_;

    open(TARGET, ">$to") or return 0;
    open(SOURCE, "<$from") or return 0;
    binmode SOURCE;
    binmode TARGET;

    my ($buffer, $bit);
    while (my $rs = read SOURCE, $buffer, $READ_SIZE) {
        if ($replace) {
            $buffer = "$bit$buffer" if length $bit;
            $bit = ($rs == $READ_SIZE and $buffer =~ s/(?:\r|\r?\n)([^\r\n]+)$//) ? $1 : '';

            $buffer =~ s/$replace/$with/g;
        }
        print TARGET $buffer;
    }
    close SOURCE;
    close TARGET;
    fchmod($from, $to);
    return 1;
}

sub fchmod {
# -----------------------------------------------------------------------------
# set chmod
#
    my($from, $to) = @_;
    $from =~ m,^([/\w.-]+)$,;
    $from = $1;

    $to =~ m,^([/\w.-]+)$,;
    $to = $1;

    my $stat = [stat($from)];
    chmod(@$stat[2], $to);
}

sub size {
# -----------------------------------------------------------------------------
# Load size of files and directories
#
    my ($self, $files) = @_;

    my $total_size = 0;

    foreach my $f (@$files) {
        next unless $f;
        my $file = $self->check_name($f);
        next if $f->{error};
        if ($file->{isfile}) {
            $total_size += $file->{size};
        }
        else {
            find($file->{full_path}, sub { $total_size += -s shift }, { untaint => 1 });
        }
    }
    return $total_size;
}

sub check_path {
# -----------------------------------------------------------------------------
# Check a directory make sure it safe
# It returns a full path or a hash
#
    my ($self, $dir) = @_;

    my $root = $self->{cfg}{root_path};
    my $work = $self->{cfg}{work_path};
    $work =~ s/^\/// if $work;

    my $full_path;
    unless ($dir) {
        $full_path = $work ? "$root/$work" : $root;
    }
    elsif ($dir eq '/') {
        $full_path = $root;
    }
    else {
        $full_path = $dir =~ m,^/, ? "$root$dir" : "$root/". ($work ? "$work/" : "") . "$dir";
    }

# Untaint the path
    ($full_path) = $full_path  =~ /^(.*)$/;

    if (-e $full_path) {
        my $current_path = cwd();
        chdir($full_path);

        $full_path = cwd();

# Untaint the path
        ($current_path) = $current_path  =~ /^(.*)$/;
        chdir($current_path);

        if ($full_path =~ /^$root(.*)$/) {
            $work = $1;
        }
        else {
            return { error => $self->language('ERR_OUT_BOUNCE', $dir) };
        }
    }

    return { error => $self->language('ERR_INVALID_INPUT', $full_path) } if ($self->{cfg}{filename_check} and $full_path !~ m,^([-\w/. ]+)$,);

    return {
        work_path => $work,
        full_path => $full_path,
        exist     => -e $full_path,
        isdir     => -d $full_path,
        write     => -w $full_path,
        read      => -r $full_path
    };
}

sub check_name {
# -----------------------------------------------------------------------------
# Check a file/directory name make sure
# - Not contain an special characters if the option is on
# - Not out side root directory
#

    my ($self, $file) = @_;

    my ($fname, $fpath);
    if ($file =~ /^(.*)\/([^\/]+)$/) {
        $fpath = $1;
        $fname = $2;
    }
    else {
        $fname = $file;
    }
    return { error => $self->language('ERR_INVALID_INPUT') } unless $fname;

    my $path = $self->check_path($fpath);
    return { error => $path->{error} } if $path->{error};

    return { error => $self->language('ERR_INVALID_INPUT') } if $self->{cfg}{filename_check} and $fname !~ m,^([-\w/. ]+)$,;

    my $full_path = $path->{full_path} . '/' . $fname;

# Untaint the path
    ($full_path) = $full_path =~ /^(.*)$/;

    my ($ext) = $fname =~ /\.([^.]+)$/;
    my $type;
    if ($ext =~ /^(?:bmp|gif|jpg|tif|tiff|ico|png)$/i) {
        $type = 'image';
    }
    elsif (-T $full_path and $ext =~ /^(?:html|htm|shtml|shtm)$/i) {
        $type = 'html';
    }
    elsif (-T $full_path and lc $ext ne 'pdf') { # Open a text file e.g: *.txt, *.html, ect...
        $type = 'text';
    }
    elsif ($ext =~ /^(?:doc|xls|pdf|mp3|mpga|mpg)$/i) {
        $type = 'doc';
    }
    elsif ($ext =~ /^(?:tar|gz|zip)$/i) {
        $type = 'compress';
    }
    else {
        $type = 'unknown';
    }

    return {
        name      => $file,
        type      => $type,
        full_path => $full_path,
        exist     => -e $full_path,
        write     => -w $full_path,
        read      => -R $full_path,
        text      => ($ext !~ /^(?:pdf|doc|xls)$/ and -T $full_path),
        size      => -s $full_path,
        isfile    => -f $full_path,
    };
}

sub flog {
# -----------------------------------------------------------------------------
#
    my ($self, $log) = @_;
    return unless $self->{cfg}{fversion} eq 'multiple';

    $self->history($log);
}

sub upload_progress {
# Upload progress 
# It returns a json object with all progess numbers
#
    my $self = shift;

    my ($serial) = $self->{cgi}{upload};

# Untaint the path
    my $logfile  =  "$self->{cfg}{tmp_path}/$serial";
    ($logfile) = $logfile =~ /^(.*)$/;
    return $self->print_json_error(undef) unless -e $logfile;

    my ($progress, $currentfile, $totalprogress, $totalsize, $end_time, $start_time, $elapsedtime, $filename, $allowed_space, $free_space) = ();

    open(READLOGFILE,"< $logfile") or return $self->print_json_error(undef);
    flock READLOGFILE, 1;
    seek READLOGFILE, 0, 0;
    my $line = <READLOGFILE>;
    chomp $line;
    close READLOGFILE or return $self->print_json_error(undef);
    unlink $logfile;

    ($progress, $totalsize, $start_time, $end_time, $filename, $allowed_space, $free_space) = split(/:\|:/, $line);
    $self->print_json({
        uploaded      => $progress,
        upload_size   => $totalsize,
        elapsed_time  => $end_time - $start_time,
        allowed_space => $allowed_space,
        free_space    => $free_space,
        filename      => $filename,
    }, 1);
}

sub fsystem {
    my (@args) = @_;

    my ($output, $error) = ('', '');
    my $tmp_output = new GT::TempFile;
    my $tmp_errors = new GT::TempFile;

    open(OLDOUT, ">&STDOUT");
    open(OLDERR, ">&STDERR");
    open(STDOUT, ">$$tmp_output") or return { error =>  "Can't redirect STDOUT" };
    open(STDERR, ">$$tmp_errors") or return { error => "Can't redirect STDERR" };

    select(STDERR);
    $| = 1;
    select(STDOUT);
    $| = 1;

    my $snum = system(@args);

# Close and restore STDOUT and STDERR
    close STDOUT;
    close STDERR;
    open(STDOUT, ">&OLDOUT");
    open(STDERR, ">&OLDERR");

    open (TMP, "< $$tmp_output") or return { error => $! };
    read (TMP, $output, -s TMP);
    close TMP;
    open (TMP, "< $$tmp_errors") or return { error => $! };
    read (TMP, $error, -s TMP);
    close TMP;

    unlink $$tmp_output;
    unlink $$tmp_errors;

    return $snum ? { error => $output || $error } : { message => $output || $error };
}
1
