#!/www/perl/bin/perl.exe
#
# ajaxupload.cgi
#
# This program is the copyrighted work of Encodable Industries.
# You may not redistribute it; instead simply refer people to
# the homepage to get their own free copy. Â You are free to
# modify the program for your own use, but you may not distribute
# any modified copies of it.
#
# Homepage: http://encodable.com/tech/ajaxupload/
# Contact: http://encodable.com/contact/
my %PREF = ();
###
### User preferences section: adjust these variables
### to suit your own server/setup/tastes.
###
# Title appearing at the top of the page.
$PREF{title} = 'Poster Uploader';
# Choose whether the script should display a link to the list of
# uploaded files.
$PREF{show_link_to_uploads} = 'no';
# THIS IS A SECURITY HAZARD. Â ONLY ENABLE IT WHILE DEBUGGING.
$PREF{show_errors_in_browser} = 'yes';
# Set the maximum size file that can be uploaded. Â One megabyte
# is 1024*1024*1; 5 MB is 1024*1024*5, etc.
$PREF{sizelimit} = 1024*1024*150;
# By default, if you upload a file that's 1 megabyte or bigger,
# the file sizes and upload rate will be in MB and MB/s. Â If
# you want to force them to always be in KB instead, set this.
$PREF{force_KB_sizes} = 'no';
# Password hashes (optional): if you want to require a password
# for access to the uploader and/or the list of uploaded files,
# you need to set these. Â Go to:
#
# Â Â yoursite.com/cgi-bin/upload/ajaxupload.cgi?makePasswordHash
#
# ...enter the password you want to use into that page, and it
# will generate a "hash" of the password, which is a string that
# looks something like this:
#
# Â Â cdfc81932491375c34c842bcebc7dc15
#
# Copy and paste the hash into one of the following preferences.
# Then when you want to log in, enter the password, not the hash.
# (This is so that we don't store the actual password on disk, which
# would be very insecure.)
#
# We specify two possible user-levels: member and admin. Â If you
# want, you can use just one of them, and have a single password
# for both uploading and viewing the file-list. Â Or you can specify
# both, and set the "must_be_" preferences accordingly, so that only
# the admin can view the uploaded files. Â Or vice-versa. Â Or you
# could require no password to view the file-list, but require one
# to upload. Â Etc, etc. Â Just set the prefs accordingly.
#
# Note that the admin is automatically a "member" too, so someone
# with the admin password automatically has access to anything that
# requires the member password.
#
# Finally, note that to delete uploaded files, you must be logged
# in as admin. Â So you probably at least want to create the admin
# password hash, even if you don't set any of the upload/list prefs
# to yes.
#
$PREF{member_password_hash} = '';
$PREF{admin_password_hash} = '';
$PREF{must_be_member_to_upload} = 'no';
$PREF{must_be_admin_to_upload} = 'no';
$PREF{must_be_member_to_list_files} = 'no';
$PREF{must_be_admin_to_list_files} = 'no';
# Once you allow someone to download a file from your uploads area,
# they will know the path to all your uploads. Â If you don't want
# them to be able to see all the other files by just visiting that
# directory's address, you'll need to put a .htaccess file in that
# directory with the line "Options -Indexes" (without quotes).
# However, as long as they know the address, they can still try to
# guess filenames that might be in there. Â As an extra security
# precaution, you can set serialize_all_uploads, which adds a long
# pseudo-random number to each filename, making it virtually
# impossible that someone could guess the name of a file in the
# directory.
$PREF{serialize_all_uploads} = 'yes';
# This is where the uploaded files will go. Â It must be world-readable
# and world-writable, aka "chmod a+rwx" or "chmod 0777". Â Set this to
# "/dev/null" if you want the files to not be saved at all. Â Note that
# this is relative to your server's $ENV{DOCUMENT_ROOT}, so if you set
# it to '/uploads' then it will be at mysite.com/uploads.
$PREF{uploaded_files_dir} = '/upload/posters';
# If you're using SSI ()
# or a PHP include ()
# to display this script at a shorter URL (like mysite.com/upload/ instead
# of mysite.com/cgi-bin/upload/ajaxupload.cgi) then enter that shorter URL here.
# Otherwise leave it set to $ENV{SCRIPT_NAME}.
$PREF{here} = $ENV{SCRIPT_NAME};
# This is where logfiles are stored. Â (The logs are crucial to the functioning
# of this program.) Â It's relative to the value of $ENV{SCRIPT_NAME}; i.e. if
# the script name is /cgi-bin/upload/ajaxupload.cgi and you set logpath to 'logs'
# then it'll be at /cgi-bin/upload/logs/. Â This must be world-readable and
# world-writable too.
$PREF{logpath} = 'logs';
###
### End of user preferences section. Â You probably don't want to mess with
### anything below here unless you really know what you're doing.
###
# Error logger
open STDERR, ">>/www/cgi-bin/error.log" or die $!;
my $version = 20051103;
if($ENV{QUERY_STRING} eq 'version') { print "Content-type: text/plain\n\n"; print "$version\n"; exit; }
my ($cwd) = ($ENV{SCRIPT_FILENAME} =~ m!^(.+)/.*?$!);
chdir $cwd;
$| = 1;
use strict;
#use warnings;
if($PREF{show_errors_in_browser} =~ /yes/i)
{
use CGI::Carp 'fatalsToBrowser';
}
use CGI;
use CGI qw/:standard/;
$CGI::POST_MAX = $PREF{sizelimit} =~ /^\d+$/ ? $PREF{sizelimit} : 1024 * 1024 * 150; Â # max 3MB posts
load_prefs();
my $output_started = 0;
my $qs
= $ENV{QUERY_STRING};
my $starttime = time;
my $total_upload_size = ();
if($qs =~ /serial=(\d+)&action=get_progress_and_size/)
{
print "Cache-Control: no-store, no-cache\n";
print "Content-type: text/xml\n\n";
my ($progress,$size,$elapsedtime) = get_progress_and_size($1);
my $toobig = $size > $CGI::POST_MAX ? '|toobig' : '';
my $output = "$progress|$size|$elapsedtime$toobig";
print $output;
}
elsif($qs eq 'listfiles')
{
list_uploaded_files();
}
elsif($qs eq 'makePasswordHash')
{
make_password_hash();
}
elsif($qs =~ /(?:^login$|action=login&target=(.+?)(&|$))/)
{
do_login($1);
}
elsif($qs eq 'logout')
{
do_logout();
}
elsif($qs =~ /action=delete&file=(.+?)(!ly=yes)?(?:&|$)/)
{
delete_file($1,$2);
}
elsif($ENV{REQUEST_METHOD} =~ /post/i)
{
process_upload();
}
else
{
print_new_upload_form();
}
sub print_new_upload_form()
{
do_authentication('upload','redirect');
start_html_output('Upload a file', 'css', 'js');
print qq`
$PREF{title}
`;
print_footer_links('list','logout','login');
finish_html_output();
}
sub hook
{
my ($filename, $buffer, $bytes_read, $logfh) = @_;
flock $logfh, 2; # lock the log
seek $logfh, 0, 0; # seek to the beginning
print $logfh "${bytes_read}:${total_upload_size}:${starttime}"; # print the new size
truncate $logfh, tell $logfh; # truncate the file (on the off chance that the new size is less than the old)
flock $logfh, 8; # release the lock
}
sub get_progress_and_size
{
do_authentication('progress','redirect');
my $serial = shift;
my ($line,$progress,$size,$start_time,$elapsedtime) = ();
my $logfile = "$PREF{'logpath'}/$serial.log";
if(-T $logfile)
{
open(READLOGFILE,"<$logfile") or die "$0: couldn't open $logfile for reading: $!\n";
flock READLOGFILE, 1;
seek READLOGFILE, 0, 0;
$line = ;
close READLOGFILE or die "$0: couldn't close $logfile after reading: $!\n";
($progress,$size,$start_time) = split(/:/, $line);
$elapsedtime = time - $start_time;
}
return ($progress,$size,$elapsedtime);
}
sub process_upload()
{
do_authentication('upload','redirect');
($PREF{serial}) = ($qs =~ /(?:^|&)serial=(\d+)(?:&|$)/);
$total_upload_size = $ENV{CONTENT_LENGTH};
my $logfile = "$PREF{logpath}/$PREF{serial}.log";
open(my $logfh,">$logfile") or die "$0: couldn't open $logfile for writing: $!\n";
flock $logfh, 2;
seek $logfh, 0, 0;
print $logfh "0:${total_upload_size}:$starttime";
flock $logfh, 8;
  my $query = CGI->new(\&hook,$logfh);
my $serial = $PREF{serial};
my $filename = $query->param('uploadname');
$filename =~ s/^.*[\\\/]//; # remove any path info.
my $filesize = ();
my $file_ext = 'null';
($filename,$file_ext) = ($filename =~ /(.+)\.(.+)$/);
my $upload_filehandle = $query->upload('uploadname');
my $fullfile = "$PREF{DOCROOT}$PREF{uploaded_files_dir}/$filename.$serial.$file_ext";
my $fullfile_noserial = "$PREF{DOCROOT}$PREF{uploaded_files_dir}/$filename.$file_ext";
my $finalfile = "$PREF{uploaded_files_dir}/$filename.$serial.$file_ext";
if($ENV{CONTENT_LENGTH} > $CGI::POST_MAX)
{
print "Content-type: text/plain\n\n";
print "ERROR: you tried to send $ENV{CONTENT_LENGTH} bytes,\nbut the current limit is $CGI::POST_MAX bytes.\nPlease go back and choose a smaller file.\n";
exit;
}
elsif(!$query->param('uploadname'))
{
print "Content-type: text/plain\n\n";
print "ERROR: the upload file-field is blank.\nEither you didn't choose a file, or there's some problem with your server.\nMaybe you need a newer version of the CGI.pm module?\nOr maybe your webhost/server doesn't allow file uploads?\n";
exit;
}
unless($PREF{uploaded_files_dir} eq '/dev/null')
{
open(UPLOADFILE,">$fullfile") or die "$0: couldn't create file $fullfile: $!\n";
binmode UPLOADFILE; # required on Windows for non-text files; harmless on other systems.
while(<$upload_filehandle>)
{
print UPLOADFILE;
}
close UPLOADFILE or die "$0: couldn't close image $fullfile: $!\n";
chmod 0666, $fullfile;
$filesize = (stat($fullfile))[7];
# remove the serial number only if a file by the same name doesn't already exist.
if( Â (! -e $fullfile_noserial) Â && Â ($PREF{serialize_all_uploads} !~ /yes/i) Â )
{
rename($fullfile, $fullfile_noserial);
$finalfile =~ s/\.$serial//;
}
}
close $logfh or die "$0: couldn't close $logfile after writing: $!\n";
chmod 0666, $logfile;
$filesize = $ENV{CONTENT_LENGTH} unless $filesize;
$filesize = $filesize > 999999 ? onedecimal($filesize/(1024*1024)) . ' MB' : int($filesize/1024) . ' KB';
my $linktofile = (!user_has_list_rights() || ($PREF{uploaded_files_dir} eq '/dev/null')) ? "$filename.$file_ext" : qq`$filename.$file_ext`;
start_html_output('Upload complete', 'css');
print  qq`\n Ihre Datei: $filename ($filesize) wurde erfolgreich hochgeladen.
`
. qq`\n`;
finish_html_output();
}
sub load_prefs()
{
# Some servers seem to not set $ENV{DOCUMENT_ROOT} properly for users who serve pages from their
# home directories, so we'll make our own version based on the start of SCRIPT_FILENAME. Â 99% of
# the time it'll be equal to the normal DOCUMENT_ROOT though.
$PREF{DOCROOT} = $ENV{DOCUMENT_ROOT};
if($ENV{SCRIPT_FILENAME} !~ /^$ENV{DOCUMENT_ROOT}/) # server is screwy!
{
($PREF{DOCROOT}) = ( Â $ENV{SCRIPT_FILENAME} =~ m!^(.+?)/cgi-bin/! Â );
}
my $prefs_file = 'upload_prefs.txt';
if(-T $prefs_file)
{
open(IN,"<$prefs_file") or die "$0: couldn't open $prefs_file: $!\n";
flock IN, 1;
seek IN, 0, 0;
while()
{
chomp; next if /^\s*(#|$)/;
my ($pref, $value) = split(/=/, $_, 2);
for($pref, $value)
{
s/\s+$//g;
s/^\s+//g;
}
$PREF{$pref} = $value;
}
close IN or die "$0: couldn't close $prefs_file: $!\n";
}
# Any files in the prefs should be specified WRT server-root, so we'll prepend it here.
$PREF{serial} = time . $$ . $ENV{REMOTE_ADDR} . $ENV{HTTP_USER_AGENT};
$PREF{serial} =~ s/[^\d]//g;
$PREF{title} = 'Encodable Industries' &nb
sp; unless exists $PREF{title};
$PREF{here} = '/cgi-bin/digiupload.cgi' unless exists $PREF{here};
$PREF{logpath} = '/cgi-bin/logs' unless exists $PREF{logpath};
$PREF{uploaded_files_dir} = '/upload/posters' &nb
sp; unless exists $PREF{uploaded_files_dir};
$PREF{max_upload_size} = 1024*1024*150 unless exists $PREF{max_upload_size};
$PREF{show_errors_in_browser} = 'yes' &nb
sp; unless exists $PREF{show_errors_in_browser};
$PREF{show_link_to_uploads} = 'no' &nb
sp; unless exists $PREF{show_link_to_uploads};
$PREF{sizelimit} = 1024*1024*150  
; unless exists $PREF{sizelimit};
$PREF{num_days_login_lasts} = 7  
; unless $PREF{num_days_login_lasts};
}
sub get_js
{
return qq`
`;
}
sub get_css
{
return qq`
`;
}
sub onedecimal
{
my $num = shift;
$num =~ /^(\d+\.\d).*/;
return $1 ? $1 : $num;
}
sub list_uploaded_files()
{
do_authentication('list_files','redirect');
my $dir = $PREF{DOCROOT} . $PREF{uploaded_files_dir};
my @files = ();
if(-d $dir)
{
opendir(my $dirh,$dir) or die "$0: couldn't open $dir: $!\n";
@files = grep { !/^(\.|\.\.|\.ht*)$/ } sort { lc($a) cmp lc($b) } readdir($dirh);
closedir($dirh) or die "$0: couldn't close $dir: $!\n";
}
start_html_output('Uploaded Files', 'css');
print  qq`Uploaded Files:
\n\n\n`;
foreach my $file (@files)
{
my $displayname = $file;
if($displayname =~ /(\d{15,})(\..{1,6})$/)
{
my ($to_replace,$end) = ($1,$2);
my ($replacement) = ($to_replace =~ /^(\d{12})/);
$displayname =~ s/$to_replace$end/$replacement...$end/;
}
print qq`
$displayname`;
print qq` [
delete]` if admin_is_logged_in();
print qq`
\n`;
}
print qq`\n
\n\n`;
print_footer_links('back');
finish_html_output('power');
}
sub start_html_output
{
my $title = shift;
my $css = shift;
my $js = shift;
$css = get_css() if $css;
$js = get_js() if $js;
unless($output_started)
{
print "Cache-Control: no-store, no-cache\n";
print "Content-type: text/html\n\n";
$output_started = 1;
}
print  qq``
. qq`\n`
. qq`\n`
. qq`\n`
. qq`\n`
    . qq`\n`
. qq`\n$title\n$js\n$css\n\n\n`
. qq`\n`;
}
sub finish_html_output
{
my $power = shift;
print_powered_by() if $power;
print  qq`\n
\n`;
if(($CGI::VERSION < 3.03) Â && Â ($PREF{ignore_version_error} !~ /yes/i))
{
print  qq`\n`
. qq`The version of the CGI.pm Perl module on your server is $CGI::VERSION.`
. qq`
The progress bar probably won't work unless you upgrade to at least version 3.03.`
. qq`
To disable this message, add \$PREF{ignore_version_error}='yes'; near the top of this script.`
. qq`
`
. qq`\n`;
}
print  qq`\n\n`;
}
sub print_footer_links
{
my @links = ();
while(my $i = shift)
{
 if($i =~ /back/) { push @links, qq`Uploader`; }
elsif($i =~ /home/) { push @links, qq`Home`; }
elsif($i =~ /enc/) { push @links, get_powered_by(); }
elsif($i =~ /list/) { push (@links, qq`List Files`) if $PREF{show_link_to_uploads} =~ /yes/i; }
elsif($i =~ /logout/) { push (@links, qq`Logout`) if user_is_logged_in(); }
elsif($i =~ /login/) { push (@links, qq`Login`) if (login_features_enabled() && !user_is_logged_in()); }
}
print  qq`\n`;
}
sub print_powered_by
{
print  qq`\n`;
print get_powered_by();
print  qq`
\n`;
}
sub get_powered_by
{
return qq`Powered by Encodable`;
}
sub make_password_hash
{
if($ENV{REQUEST_METHOD} =~ /post/i)
{
use Digest::MD5 'md5_hex';
use CGI ':param';
my $hashed_password = md5_hex(param('password'));
start_html_output('Here is your hashed password...', 'css', 'js');
print  qq`The hashed version of the password you just entered is:
$hashed_password
`
. qq`\n`;
print_footer_links('back');
finish_html_output('power');
}
else
{
start_html_output('Enter your new password', 'css', 'js');
print  qq``
. qq`\n`;
print_footer_links('back');
finish_html_output('power');
}
}
sub user_is_logged_in
{
my $hashed_password_in_cookie = get_cookie('enc-uploader-password');
return 0 unless $hashed_password_in_cookie;
return( Â $hashed_password_in_cookie eq $PREF{admin_password_hash} Â || Â $hashed_password_in_cookie eq $PREF{member_password_hash} Â );
}
sub admin_is_logged_in
{
my $hashed_password_in_cookie = get_cookie('enc-uploader-password');
return 0 unless $hashed_password_in_cookie;
return($hashed_password_in_cookie eq $PREF{admin_password_hash});
}
sub do_authentication
{
return 1 if !login_features_enabled();
my $target = shift;
my $mode = shift;
my $hashed_password_in_cookie = get_cookie('enc-uploader-password');
if($PREF{"must_be_admin_to_$target"} =~ /yes/i)
{
if(!$hashed_password_in_cookie  ||  ($hashed_password_in_cookie ne $PREF{admin_password_hash}))
{
if($mode eq 'redirect')
{
print_needlogin_error($target);
}
else
{
return 0;
}
}
else
{
return 1;
}
}
elsif($PREF{"must_be_member_to_$target"} =~ /yes/i)
{
# the admin is considered a member too, i.e. if you have the
# admin password, then you meet the requirements for being a
# member too.
if( !$hashed_password_in_cookie
||
(
$hashed_password_in_cookie ne $PREF{member_password_hash}
&&
$hashed_password_in_cookie ne $PREF{admin_password_hash}
)
)
{
if($mode eq 'redirect')
{
print_needlogin_error($target);
}
else
{
return 0;
}
}
else
{
return 1;
}
}
}
sub print_needlogin_error
{
my $target = shift;
start_html_output('Error: Authentication Required', 'css', 'js');
print  qq`Error: Authentication Required
`
. qq`\n`
. qq`\n`;
finish_html_output('power');
exit;
}
sub do_login
{
my $target = shift;
if($ENV{REQUEST_METHOD} =~ /post/i)
{
use Digest::MD5 'md5_hex';
use CGI ':param';
if(param('password') !~ /\S/) # don't allow blank passwords.
{
start_html_output&
('Error', 'css');
print qq`You must enter the password.
`;
finish_html_output
('power');
exit;
}
my $hashed_password = md5_hex(param('password'));
my $expiry = ();
if(param('persist') eq 'on')
{
$expiry = "+$PREF{num_days_login_lasts}d";
}
if($hashed_password eq $PREF{admin_password_hash} Â || Â $hashed_password eq $PREF{member_password_hash})
{
set_cookie('enc-uploader-password', $hashed_password, $expiry);
if($target eq 'list_files')
{
print "Location: http://$ENV{HTTP_HOST}$PREF{here}?listfiles\n\n";
}
else # default to the front page (the upload page).
{
print "Location: http://$ENV{HTTP_HOST}$PREF{here}\n\n";
}
}
else
{
start_html_output&
('Invalid Login', 'css');
print &
nbsp; Â qq`The password you entered is incorrect.
Go back and try again.
`
. qq`\n`;
finish_html_output
('power');
}
}
else
{
my $scripttarget = $target ? "action=login&target=$target" : 'login';
start_html_output('Enter the password', 'css');
print  qq``
. qq`\n`;
finish_html_output('power');
}
}
sub get_cookies()
{
use CGI ':standard';
use CGI::Cookie;
my %cookies = fetch CGI::Cookie;
return %cookies;
}
sub get_cookie($)
{
my $which = shift;
my %jar = get_cookies();
my $value;
if(exists $jar{$which})
{
$value = $jar{$which}->value;
}
return $value;
}
sub set_cookie($$$)
{
my $name = shift;
my $value = shift;
my $expiry = shift;
my $cookie;
if($expiry eq "") # cookie expires at end of this session.
{
$cookie = new CGI::Cookie( -name   => $name,
-value  => $value,
-path   => '/');
}
else
{
$cookie = new CGI::Cookie( -name   => $name,
-value  => $value,
-expires => $expiry,
-path   => '/');
}
print "Set-Cookie: $cookie\n";
}
sub login_features_enabled
{
if(
(
$PREF{member_password_hash} =~ /\S/
||
$PREF{admin_password_hash} =~ /\S/
)
&&
(
$PREF{must_be_member_to_upload} =~ /yes/i
||
$PREF{must_be_admin_to_upload} =~ /yes/i
||
$PREF{must_be_member_to_list_files} =~ /yes/i
||
$PREF{must_be_admin_to_list_files} =~ /yes/i
)
)
{
return 1;
}
}
sub user_has_list_rights
{
return do_authentication('list_files');
}
sub do_logout()
{
set_cookie('enc-uploader-password', 'blank', '-1d');
# Remove the "logout" from the referrer, otherwise we'll get stuck
# in an infinite logout loop with this Location: call.
$ENV{HTTP_REFERER} =~ s/\?logout$//;
my $go = $ENV{HTTP_REFERER} ? $ENV{HTTP_REFERER} : "http://$ENV{HTTP_HOST}$PREF{here}";
print "Location: $go\n\n";
}
sub delete_file
{
my $file = shift;
my $really = shift;
unless(admin_is_logged_in())
{
start_html_output('Error: Authentication Required', 'css', 'js');
print  qq`Error: Authentication Required
`
. qq`\nYou must
log in as admin to do that.
`
. qq`\n`;
print_footer_links('back','list');
finish_html_output('power');
}
my $displayname = $file;
$displayname =~ s/%([A-Fa-f\d]{2})/chr hex $1/eg;
if($displayname =~ /(\d{15,})(\..{1,6})$/)
{
my ($to_replace,$end) = ($1,$2);
my ($replacement) = ($to_replace =~ /^(\d{12})/);
$displayname =~ s/$to_replace$end/$replacement...$end/;
}
if($really)
{
my $diskfile = $file;
$diskfile =~ s/%([A-Fa-f\d]{2})/chr hex $1/eg;
$diskfile = "$PREF{DOCROOT}$PREF{uploaded_files_dir}/$diskfile";
unlink($diskfile) or die "$0: couldn't delete \"$diskfile\": $!\n";
start_html_output('File deleted', 'css');
print  qq`File deleted successfully:
`
. qq`\n$displayname
`
. qq`\n`;
print_footer_links('back','list');
finish_html_output('power');
}
else
{
start_html_output('Confirm deletion', 'css');
print  qq`Really delete this file?
`
. qq`\n`
. qq`\n`;
finish_html_output('power');
}
}