# ================================================================== # File manager - enhanced web based file management system # # Website : http://gossamer-threads.com/ # Support : http://gossamer-threads.com/scripts/support/ # CVS Info : 087,071,086,086,085 # CVS Info : 087,071,086,086,085 # Revision : $Id: FileMan.pm,v 1.160 2008/11/21 21:01:09 brewt Exp $ # # Copyright (c) 2001 Gossamer Threads Inc. All Rights Reserved. # Redistribution in part or in whole strictly prohibited. Please # see LICENSE file for full details. # ================================================================== package GT::FileMan; use strict; use vars qw/$MSWIN $DEBUG $HAVE_GZIP $HAVE_AZIP $LANGUAGE $LANG_TPL/; use GT::Base qw/:persist/; use GT::Template; use GT::File::Tools qw/:all/; use GT::FileMan::Session; use GT::FileMan::Commands; use GT::MD5; use GT::Config; $DEBUG = 0; our @ISA = qw/GT::FileMan::Commands GT::FileMan::Session GT::Base/; # Check if Compress::Zlib and Archive::Zip are available $HAVE_GZIP = eval { local $SIG{__DIE__}; require Compress::Zlib; 1; } ? 1 : 0; $HAVE_AZIP = eval { local $SIG{__DIE__}; require Archive::Zip; 1; } ? 1 : 0; $MSWIN = $^O =~ /mswin/i ? 1 : 0; sub new { my ($class, %args) = @_; my $self = bless {%args}, ref $class || $class; # Upload progress $self->{in} = GT::CGI->new(); unless ($self->{cfg}) { $self->{cfg} = $self->load_config(); } # This applies for GT products version else { $self->{cfg}{template} ||= 'luna'; $self->{cfg}{template_path} ||= $self->{cfg}{template_root}; $self->{cfg}{root_path} ||= $self->{cfg}{root_dir}; $self->{cfg}{tmp_path} ||= '/tmp'; $self->{cfg}{static_url} ||= $self->{cfg}{html_root_url} . '/static'; $self->{cfg}{cgi_url} ||= $self->{in}->url(absolute => 0, query_string => 0); $self->{cfg}{command_timeout} ||= $self->{cfg}{command_time_out}; $self->{cfg}{path_to_perl} ||= '/usr/bin/perl'; $self->{cfg}{default} ||= { allowed_space => 0, upload_mode => '644' }; $self->{cfg}{date} = { display => '%dd%-%mmm%-%yy% %hh%:%MM%:%ss%', input => '%yyyy%-%mm%-%dd%' }; } # Set tmp_path and verify to see if it's writeable $self->{cfg}{tmp_path} ||= '/tmp'; die "$self->{cfg}{tmp_path} is not writeable" unless -w $self->{cfg}{tmp_path}; my $query_string = $ENV{QUERY_STRING}; if ($query_string =~ /^serial=/) { my ($read_file, $read_size) = ('', 0); my $uploaded_size = 0; my $started_time = time; my $total_size = $ENV{CONTENT_LENGTH}; my ($serial) = $query_string =~ /\=([^=]+)$/; $serial =~ m|^(\w+\d*)$|i or die "Invalid serial: $serial"; $self->{serial} = $serial; $self->{in}->upload_hook( sub { my ($filename, $buffer, $bytes) = @_; my $new_progress; if ($read_file ne $filename) { $read_file = $filename; $read_size = $uploaded_size; } if ($read_size) { $new_progress = $read_size + $bytes; } else { my $old_progress = $uploaded_size; $new_progress = $bytes >= $old_progress ? $bytes : $old_progress; } $uploaded_size = $new_progress; my $time = time; my $max_length = 50; $filename = substr($filename, 0, $max_length) if length($filename) > $max_length; open FILE, "> $self->{cfg}{tmp_path}/$serial"; flock FILE, 1; print FILE "$new_progress:|:$total_size:|:$started_time:|:$time:|:$filename:|:$self->{diskspace}{allowed}:|:$self->{diskspace}{free}\n"; # print the close FILE; # select undef, undef, undef, 0.50; } ); } $self->{cgi} = $self->{in}->get_hash(); $DEBUG = $self->{cfg}{debug}; # Load access paths $self->{cfg}{template_path} or die('You must pass in your template root !'); $self->{cfg}{root_path} or die('You must set your root dir !'); $self->{default} = $self->default(); # Cleanup the tmp directory $self->cleanup(); return $self; } sub process { my $self = shift; my $action = $self->{cgi}{cmd} || 'home'; # Avoid same name as GT::File::Tools::move/copy my $command = $action =~ /^(?:copy|move|print)$/ ? "cmd$action" : $action; # Load authentication info if ($self->{cfg}{login}) { $self->auth(); unless ($self->{session}) { return $self->{cgi}{ajax} ? $self->print_json({ html => $self->print('login.html', { json => 1, error => $self->language('ERR_NOAUTH') }) }, 1, undef, 'ERR_NOAUTH') : $self->login(); } } $self->{diskspace} = $self->check_space($self->{cfg}{root_path}, $self->{cfg}{allowed_space}); # Verify action to see if it's permitted return $self->home(error => $self->language('ERR_POST_REQUEST', $action)) unless $self->verify_request($action); return $self->home(error => $self->language('ERR_INVALID_ACTION', $action)) unless exists $GT::FileMan::Commands::COMPILE{$command}; return $self->home(error => $self->language('ERR_NO_PERM', $action)) unless $self->check_action($action); # Checking free space $self->{diskspace} = $self->check_space(($self->{cfg}{root_path}), $self->{cfg}{allowed_space}); $self->$command(); } sub verify_request { my ($self, $action) = @_; return 1 if lc $ENV{REQUEST_METHOD} eq 'post' or $action =~ /^(?:home|print|fdownload|preview)$/; return 1 if $action =~ /^(?:command|upload)$/ and $self->{cgi}{serial} and -e "$self->{cfg}{tmp_path}/$self->{cgi}{serial}"; return; } sub auth { my $self = shift; $self->{session} = $self->session_valid(); return unless $self->{session}; $self->{session}{user} = { username => $self->{cfg}{login}{username}, permission => $self->{cfg}{permission} }; } sub print { my ($self, $page, $args) = @_; $page = 'home.html' if !$page or $page !~ /^[\w\-]+\.\w+$/; my $template = $self->{cgi}{t} ? $self->{cgi}{t} : $self->{cfg}{template}; $template = 'luna' if $template !~ /^[\w-]+$/; my $fullpath = "$self->{cfg}{template_path}/$template/$page"; # Untaint the path ($fullpath) = $fullpath =~ /^(.*)$/; my $globals = $self->globals(); my %browser = $self->{in}->browser_info; $args->{have_gzip} = $HAVE_GZIP; $args->{have_azip} = $HAVE_AZIP; $args->{browser} = \%browser; $args->{apache_server} = 1 if $ENV{SERVER_SOFTWARE} =~ /apache/i; $args->{mswin} = $MSWIN; $args->{noauth} = 1 unless $self->{cfg}{login} or $self->{cfg}{fversion} eq 'multiple'; my $form = GT::Template->parse($fullpath, { %$globals, %$args }, { escape => 1 }); return $form if $args->{json}; print $self->{in}->header; print $form; } sub print_json_error { # -------------------------------------------------- # shorthand to send an error message in json # # * If the first parameter is a hash, we assume it's a data # and the second parameter is the error message # # * If it's a scalar, we assume that it's the error message. # my $self = shift; my $data = ref $_[0] eq 'HASH' ? shift : {}; my $message = shift; my $status = shift; return $self->print_json($data, 0, $message, $status); } sub print_json { # -------------------------------------------------- # Dumps the passed data object to STDOUT # by default, we assume that the request was a # success. If not, status should be set to "fail" # my ($self, $data, $success, $message, $status) = @_; require GT::JSON; # If success is defined, pass it through if (defined $success) { $success = $success ? $GT::JSON::true : $GT::JSON::false; } # Otherwise, lets just default the success status to true else { $success = $GT::JSON::true; } # If there are any special messages $message ||= ''; my $json_str = GT::JSON::to_json({ message => $message, success => $success, status => $status, data => ( $data || {} ), }, { utf8 => 0 }); print $self->{in}->header({ 'no-cache' => 1 }); print $json_str; } sub load_config { # Load the config file into a hash. # my $self = shift; my $file = $self->{cfg_path} || 'fileman.conf'; my $header = <load($file, { inheritance => 0, cache => 1, header => $header }); $cfg->{template_path} = "$cfg->{private_path}/templates"; $cfg->{date} ||= { display => '%dd%-%mmm%-%yy% %hh%:%MM%:%ss%', input => '%yyyy%-%mm%-%dd%' }; $cfg->{default} ||= { allowed_space => 0, upload_mode => '644' }; $cfg->{tmp_path} ||= '/tmp'; $cfg->{filename_check} = 0 if $MSWIN; # Create tmp directory if it doesn't exist rmkdir($cfg->{tmp_path}, 0755) unless -e $cfg->{tmp_path}; return $cfg; } sub default { # Load the default values from cookie # my ($self, %default) = @_; # Loading defaults from fileman_defaults cookie unless (%default) { my $defaults = $self->{in}->cookie('fileman_defaults'); my @defaults = split(/;/, $defaults); foreach my $d (@defaults) { if ($d =~ /^(\w+)=(.*\/?\w+)/) { $default{$1} = $2; } } } return \%default unless $self->{cfg}{root_path}; if ($default{pwd_path} and $default{pwd_path} !~ /^$self->{cfg}{root_path}/) { $default{pwd_path} = '' ; } elsif ($default{pwd_path}) { $default{pwd_path} =~ s/^$self->{cfg}{root_path}//; } if ($default{path} and $default{path} !~ /^$self->{cfg}{root_path}/) { $default{path} = ''; } elsif ($default{path}) { $default{path} =~ s/^$self->{cfg}{root_path}//; } $default{readme} ||= 2; $self->{cfg}{work_path} = $self->{cgi}{work_path} eq '/' ? '' : $self->{cgi}{work_path}; if ($default{path} and $self->{cgi}{load_default} and !$self->{cfg}{work_path}) { $self->{cfg}{work_path} = $default{path}; } return \%default; } sub cleanup { # Clean up xx hour old files in the tmp directory # my $self = shift; return unless -e $self->{cfg}{tmp_path}; opendir (DIR, $self->{cfg}{tmp_path}) or return; my @files = readdir(DIR); close DIR; my $expiry = $self->{session}{expiry} || 5; foreach my $f (@files) { next if $f eq '.' or $f eq '..' or !-f "$self->{cfg}{tmp_file}/$f"; my @stat = stat("$self->{cfg}{tmp_file}/$f"); next if time - $stat[9] < 3600 * $expiry; del("$self->{cfg}{tmp_file}/$f", { untaint => 1 }); } } sub language { # ------------------------------------------------------------------ # Process a language request, it's only loaded once, and saved in # $LANGUAGE. # my $self = shift; my $code = shift; require GT::Config; my $lang = "$self->{cfg}{template_path}/$self->{cfg}{template}/language.txt"; $LANGUAGE = undef unless $LANG_TPL; $LANGUAGE ||= GT::Config->load($lang, { create_ok => 1, inheritance => 1, local => 1, header => <
{cfg}{template}' template set. # Generated on: [localtime] HEADER $LANG_TPL = $self->{cfg}{template}; if (exists $LANGUAGE->{$code}) { return @_ ? sprintf($LANGUAGE->{$code}, @_) : $LANGUAGE->{$code}; } else { return $code; } } sub fatal { # Return a fatal error message to the browser. # die @_ if (GT::Base->in_eval()); # Don't do anything if we are in eval. my $in = new GT::CGI; my $msg = $in->html_escape(shift); my $font = "Tahoma,Arial,Helvetica"; print $in->header; print qq! A fatal error has occurred:
$msg
Please enable debugging in setup for more details.
\n !; print base_env($in) if $DEBUG; } sub base_env { my ($in, $version, $commands) = @_; my $info = '
';
    my ($oserr, $evalerr) = ($@, $!);

# Stack trace.
    $info .= "Stack Trace\n======================================\n";
    $info .= GT::Base::stack_trace('FileMan', 1);
    $info .= "\n";

# Print GT::SQL error if it exists.
    $info .= "System Information\n======================================\n";
    if (my @user = eval { getpwuid($>) }) {
        $info .= "Current user: $user[0] ($>)\n";
    }
    $info .= "Perl version: " . ($^V ? sprintf("%vd", $^V) : $]) . "\n";
    $info .= "Gossamer FileMan Version: $version\n" if $version;
    $info .= "GT::Template version: $GT::Template::VERSION\n" if $GT::Template::VERSION;
    $info .= "Running under mod_perl: " . (MOD_PERL ? "Yes (version " . MOD_PERL . ")" . (MOD_PERL >= 1.99 ? ', mod_perl 2 detected' : '') : "No") . "\n";
    $info .= "Running under SpeedyCGI: " . (SPEEDY ? "Yes (version " . SPEEDY . ")" : "No") . "\n";
    $info .= "\@INC = \n\t" . join("\n\t", @INC) . "\n";
    $info .= "\$\@: " . $in->html_escape($oserr) . "\n" if $oserr;
    $info .= "\$!: " . $in->html_escape($evalerr) . "\n" if $evalerr;
    $info .= "\n";

    if ($commands) {
        $info .= 'Commands: ';
        foreach (keys %$commands) {
            $info .= qq||;
        }
        $info .= '
$_:| . ($commands->{$_} ? 'Enabled' : 'Disabled') . qq|

'; $info .= "\n"; } # CGI Parameters and Cookies. if (ref $in eq 'GT::CGI') { if ($in->param) { $info .= "CGI Input\n======================================\n"; foreach (sort $in->param) { $info .= $in->html_escape($_) . " => " . $in->html_escape($in->param($_)) . "\n"; } $info .= "\n"; } if ($in->cookie) { $info .= "CGI Cookies\n======================================\n"; foreach (sort $in->cookie) { $info .= $in->html_escape($_) . " => " . $in->html_escape($in->cookie($_)) . "\n"; } $info .= "\n"; } } # Environement info. $info .= "Environment\n======================================\n"; foreach (sort keys %ENV) { $info .= $in->html_escape($_) . " => " . $in->html_escape($ENV{$_}) . "\n"; } $info .= "
"; return $info; } sub globals { my $self = shift; # Create css and js url $self->{cfg}{template} = $self->{cgi}{t} if $self->{cgi}{t}; my $date_input = $self->{cfg}{date}{input}; $date_input =~ s/%//g; $self->{cfg}{date_input} = $date_input; my %g = (cfg => $self->{cfg}, in => $self->{cgi}, default => $self->{default}, session => $self->{session}); my $hiddens = $self->hiddens(); foreach (keys %$hiddens) { $g{$_} = \$hiddens->{$_}; } # Reload user's diskspace. This applies for multiple users version only if ($self->{cfg}{fversion} eq 'multiple' and !$self->{session}{user}{type}) { my @paths = map $_->{name}, @{$self->{session}{user}{accesses_loop}}; $self->{diskspace} = $self->check_space(\@paths, $self->{session}{user}{allowed_space}); # Load free space $g{space} = $self->{diskspace}; } \%g; } sub hiddens { my ($self, $no_workpath) = @_; my @items = qw/sid t/; my ($query, $html) = ('', ''); foreach (@items) { next unless $self->{cgi}{$_}; $query .= ";" . $self->{in}->escape($_) . "=" . $self->{in}->escape($self->{cgi}{$_}) if exists $self->{cgi}{$_}; $html .= qq||; } if ($self->{url_opts}) { my @opts = split(/;|&/, $self->{url_opts}); foreach (@opts) { if ($_ =~ /^(\w+)=(.*\/?\w+)/) { $query .= ";$1=$2"; $html .= qq||; } } } my $subquery = $query; unless ($no_workpath) { $query .= ";work_path=" . $self->{in}->escape($self->{cfg}{work_path}) if $self->{cfg}{work_path}; $html .= qq||; } return { hidden_query => $query, hidden_subquery => $subquery, hidden_objects => $html }; } sub check_space { my ($self, $path, $allowed_space) = @_; return undef unless $allowed_space and $path; my @paths = ref $path eq 'ARRAY' ? @$path : [$path]; my ($used_space, $free_space, $usage) = (0, 0, 0); foreach my $p (@paths) { find($p, sub { $used_space += -s shift }, { untaint => 1 } ); } # Size in kb $used_space /= 1024; $free_space = $allowed_space < $used_space ? 0 : $allowed_space - $used_space; $usage = $used_space / $allowed_space * 100 if $allowed_space > 0; return { free => int($free_space * 1024), allowed => int($allowed_space * 1024), used => int($used_space * 1024), usage => int($usage) }; } sub image_url { # Takes an filename and using the current template set and theme, returns # the url of the image. It first checks if the file exists in the theme's # image directory, checks the template's image directory, and then tries # to check the template inheritance tree for more image directories. # my $image = shift; my $tags = GT::Template->tags; if (-e "$tags->{cfg}{static_path}/$tags->{cfg}{template}/images/$image") { return "$tags->{cfg}{static_url}/$tags->{cfg}{template}/images/$image"; } # The image doesn't exist here, but return it anyway return "$tags->{cfg}{static_url}/$tags->{cfg}{template}/images/$image"; } sub encrypt { #-------------------------------------------------------------------- # Encrypt password # my ($clear_pass, $salt) = @_; $salt ||= join '', map +('a' .. 'z', 'A' .. 'Z', 0 .. 9, '.', '/')[rand 64], 1 .. 8; require GT::MD5::Crypt; return GT::MD5::Crypt::gt_md5_crypt($clear_pass, $salt); } sub check_action { my ($self, $action) = @_; my $perm = $self->{cfg}{fversion} eq 'multiple' ? $self->{session}{user}{permission} : $self->{cfg}{permission}; return exists $perm->{$action} ? $perm->{$action} : 1; } 1;