Overview:

Every time I've written some code to upload a file, either to send it off as an email attachment or as an image for some dynamic content piece, I've always meant to write a few functions so I don't have to write the code again.
Well, people on the Back-end.org support board have been asking for the ability to add images to news and articles recently, so I *finally* sat down and wrote this script in order that I can add that functionality to the next release.
To make things more interesting and to make it truly re-useable, I'm going to make it totally generic so we can use it again and again for differing file types.
If you want the code to look @ in your favourite editor whilst you are reading this piece, you can download it below.
Starting us off, we need to lay down some parameters, for the maximum file size you want to allow the user to upload and - if it's an image - how high and wide the image can be, else you might get a 1.5MB, 1024 x 768 Jpeg filling up the entire screen!

<?php

$my_max_file_size     
"102400";     # in bytes
$image_max_width    "300";     # in pixels
$image_max_height    "300";    # in pixels

?>
Next is the path where the resulting file will sit on your server, this should be absolute and PHP needs 'write' permissions to that directory.

<?php

$the_path        
"/usr/local/apache/htdocs/sites/dev/phpbuilder/upload/files";

?>
Now, we want to list a number of file-types so we can give sensible error messages later on in the script. This is simply an array indexed by 'content-types' with a value that is human readable

<?php

$registered_types 
= array(
    
"application/x-gzip-compressed"     => ".tar.gz, .tgz",
    
"application/x-zip-compressed"         => ".zip",
    
"application/x-tar"            => ".tar",
    
"text/plain"                => ".html, .php, .txt, .inc (etc)",
    
"image/bmp"                 => ".bmp, .ico",
    
"image/gif"                 => ".gif",
    
"image/pjpeg"                => ".jpg, .jpeg",
    
"image/jpeg"                => ".jpg, .jpeg",
    
"application/x-shockwave-flash"     => ".swf",
    
"application/msword"            => ".doc",
    
"application/vnd.ms-excel"        => ".xls",
    
"application/octet-stream"        => ".exe, .fla (etc)"
); # these are only a few examples, you can add as many as you like

?>
Finally, we want to specify what type of file the user is allowed to upload, for this example, it's an image that we might want to embed in a web page, so we build up an array of related 'image' content-types.

<?php

$allowed_types 
= array("image/bmp","image/gif","image/pjpeg","image/jpeg");

?>
An OO developer would probably make this whole script a class and have each one of the following functions as methods. Personally, I prefer to write my code using a function based approach.
To start us off, I want to write the upload form as a small function, I can then call it many times in this code without lots of messy repeating. If you are wondering why there are lots of \n's everywhere, this is so that the outputted HTML is not all squashed onto one line. In bigger applications it makes for easier debugging when you view source, so I'm not being anal for no reason.

<?php

function form($error=false) {

global 
$PHP_SELF,$my_max_file_size;

    if (
$error) print $error "<br><br>";
    
    print 
"\n<form ENCTYPE=\"multipart/form-data\"  action=\"" $PHP_SELF "\" method=\"post\">";
    print 
"\n<INPUT TYPE=\"hidden\" name=\"MAX_FILE_SIZE\" value=\"" $my_max_file_size "\">";
    print 
"\n<INPUT TYPE=\"hidden\" name=\"task\" value=\"upload\">";
    print 
"\n<P>Upload a file";
    print 
"\n<BR>NOTE: Max file size is " . ($my_max_file_size 1024) . "KB";
     print 
"\n<br><INPUT NAME=\"the_file\" TYPE=\"file\" SIZE=\"35\"><br>";
    print 
"\n<input type=\"submit\" Value=\"Upload\">";
    print 
"\n</form>";

# END form

?>
Note, the <form enctype..> if we don't add this nothing will get uploaded. I've also set the parameter $error to false, this is a nice way of pre-defining the value, so I don't have to always pass the parameter in.
Our second function is to check if a certain value is in an array, now PHP4 has this built in, but PHP3 does not, so we wrap an IF statement around it checking the PHP version, if you are running PHP4, you can delete the whole thing but I've left in so it can use it on differing servers without too much thought.

<?php

if (!ereg("^4",phpversion())) {
    function 
in_array($needle,$haystack) { # we have this function in PHP4, so for you PHP3 people
        
for ($i=0$i count($haystack); $i++) {
            if (
$haystack[$i] == $needle) {    
                return 
true;
            }
        }
    }
}

?>
The main function in this script is for the validation of our upload, here we do things like check the file type, and if an image, what it's dimensions are.

<?php

function validate_upload($the_file) {
 
global 
$my_max_file_size$image_max_width$image_max_height,$allowed_types,$the_file_type,$registered_types;
    
$start_error "\n<b>Error:</b>\n<ul>";
    
    if (
$the_file == "none") { # do we even have a file?
    
        
$error .= "\n<li>You did not upload anything!</li>";
    
    } else { 
# check if we are allowed to upload this file_type
    
        
if (!in_array($the_file_type,$allowed_types)) {
            
$error .= "\n<li>The file that you uploaded was of a ".
                
"type that is not allowed, you are only
                allowed to upload files of the type:\n<ul>"
;
            while (
$type current($allowed_types)) {
                
$error .= "\n<li>" $registered_types[$type] . " (" $type ")</li>";
                
next($allowed_types);
            }
            
$error .= "\n</ul>";
        }
    
        if (
ereg("image",$the_file_type) && (in_array($the_file_type,$allowed_types))) {
        
            
$size GetImageSize($the_file);
            list(
$foo,$width,$bar,$height) = explode("\"",$size[3]);
    
            if (
$width $image_max_width) {
                
$error .= "\n<li>Your image should be no wider than " 
                    
$image_max_width " Pixels</li>";
            }
            
            if (
$height $image_max_height) {    
                
$error .= "\n<li>Your image should be no higher than " 
                    
$image_max_height " Pixels</li>";
            }
        
        }
        
        if (
$error) {
            
$error $start_error $error "\n</ul>";
            return 
$error;
        } else {
            return 
false;
        }
    }
 } 
# END validate_upload

?>
Notes:
1-4: we start our function off and tell PHP what variables have a global scope (i.e. originated outside this function), this also saves us having to use $GLOBALS[] everywhere.
5: Start our error message off
6-9: if there is no file, $the_file is set to 'none' so we check if the user actually specified a file to upload
13-21: Here, our in_array function and the two arrays we built earlier come into play, we loop through the $allowed_types array and see if the PHP set $the_file_type is in there, if it is the script knows that it is allowed to accept this. If it is not allowed, then we use the $registered_types array to give a sensible error message.
23-35: If the uploaded file is an image, we want to check that it's dimensions are smaller than our pre-defined maximum values. We use the PHP function getimagesize() to check the file dimensions and build up the error string if it is too big.
37-42: If we have anything in $error, we finish the string off and return it, else we return false. There that's the hard work done.
This next function isn't really needed, but I've put it in so our user gets some confirmation that the file was uploaded, all it does is loop through the upload directory and print out the files in it, except for '.' and '..'

<?php

function list_files() {

global 
$the_path;
    
    
$handle dir($the_path);
    print 
"\n<b>Uploaded files:</b><br>";
    while (
$file $handle->read()) {
        if ((
$file != ".") && ($file != "..")) {
            print 
"\n" $file "<br>";
           }
    }
    print 
"<hr>";
}

?>
Our last function brings all of our functions together. It takes the file as input and then uses validate_upload() to check that it is allowed. If it is then we need to copy it from the temp directory (this is specified in php.ini) and move it to it's final resting place. One last sanity check is to see that we can copy to that directory. If this falls over it is almost certainly a permission problem or an invalid path. If everything is OK, then we call list_files() and then re-display the form.

<?php

function upload($the_file) {

global 
$the_path,$the_file_name;
    
    
$error validate_upload($the_file);
    if (
$error) {
        
form($error);
    } else { 
# cool, we can continue
        
if (!@copy($the_file$the_path "/" $the_file_name)) {
            
form("\n<b>Something barfed, check the path to and ".
            
"the permissions for the upload directory</b>");
        } else {
            
list_files();
            
form();
        }
    }
# END upload

?>
Our last bit of code now is to kick things off. We use the case() statement to switch between tasks. It's a slight overkill here, but I've left it in so you can see how it might be used, for larger projects it makes for easily readable code, though some developers might disagree with me.
If $task = upload then the upload process begins, otherwise as a default we just show the form

<?php

print "<html>\n<head>\n<title>Upload example</title>\n</head>\n<body>";

switch(
$task) {
    case 
'upload':
        
upload($the_file);
    break;
    default:
        
form();
}

print 
"\n</body>\n</html>";

?>
That's it, upon running this script the user will see just a form with an upload field, they browse the hard-drive for a file and click submit. If there are no errors in the validation stage, they will get returned a directory listing of the upload folder and the form again so they can upload another file.
-- Bealers

Notes:

I am well aware that I've added no error checking for the max_file_size, AFAIK the only way to gracefully capture this error is to turn all error_reporting off in php.ini and do code based checking. However I like to see my errors for development, so I guess we are stuck with the messy PHP error that is generated.
This example does not show you what to do with the image once it is uploaded. For example, as I will be for the next release of Back-end, you might want to allow a user to upload an image for a news article. In this case you would possibly add the file name to the article so when the page is viewed, php can pull the corresponding image onto the page. I'll leave you to figure that one out =)