#!/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  qq`
$PREF{intro}
\n\n` if $PREF{intro}; print qq` Datei zum hochladen wählen:



`; 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 $filename Ihre Datei: $filename ($filesize) wurde erfolgreich hochgeladen.
gewünschtes Druckformat:

Anzahl: Beschneiden?
` . 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`\nEnter your new password:` . qq`\n

` . qq`\n

` . qq`\n
` . 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
You must log in first.
` . 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`\nEnter the password:` . qq`\n

` . qq`\n

Keep me logged in for $PREF{num_days_login_lasts} days` . qq`\n

` . qq`\n
` . 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`\n
You 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
$displayname` . qq`\n

[Yes]   ` . qq`\n[No]
` . qq`\n`; finish_html_output('power'); } }