File Uploads With Web Forms and PHP

by Patrick Horgan

(Back to Web tutorials)

What do we want to do?

We want the user to be able to click on a button which will cause a file browser to pop up which will allow files to be selected to be sent to the server.

RFC 1867 - Form-based File Upload in HTML

In November of 1995, Larry Masinter, and Ernesto Nebel published an experimental RFC 1867 - Form-based File Upload in HTML (if you don't know what an RFC is, then look at RFCs and a script to get them), suggesting a new input tag for html forms that would let you do just this. This was an extension of HTML 2.0 (RFC 1866). Additionally, the part of RFC 1867 that talked about how the files would be transported to the server was fleshed out as RFC 2388 - Returning Values from Forms: multiplart/form-data. I encourage anyone that wants to understand this better to read both. Currently HTML 4.0 and HTML5 are extent. The 4.0 spec refers to the two RFCs cited above, but the HTML5 spec, for the first time has a lot of detailed information about how this works and doesn't mention RFC 1867, and mentions RFC 2388 only to say that in general it follows RFC 2388, but also to say in what ways the spec differs from it.

What's the HTML look like?

You just have to make a form, and have a file input element>.

<form enctype="multipart/form-data" action='getfile.php' method='post'> <input type="hidden" name="MAX_FILE_SIZE" value="30000" /> <input type='file' name='thefile' /> <input type='submit'' value='Ok' /> </form>

It's important that you have the enctype attribute on the form, and that you set it to 'multipart/form-data'. The method of the form has to be set to post. The input element of type file has to have its name attribute set, and you need a submit button. That's it. The hidden input is useful to let the browser refuse to upload files that are bigger than the back end on the server is willing to deal with. You can't count on that on the back end, though, since the users don't have to go through our website, they can mock up their own interface without that restriction. If you use it, it must preceed the the file input.

On your machine, this is what it looks like unstyled

To use it, you would first click on the button to browse for the file name, and then click on the Submit button to send it.

If you click on it and send a file, we'll take you to a page that describes the associated variables used to access it. Please don't send anything private, nor large. I don't keep them, but I don't want your information.

Here's the PHP that will deal with it

<?php echo "<p>\$_FILES['thefile']['name']='".$_FILES['thefile']['name']."'</p>"; echo "<p>\$_FILES['thefile']['type']='".$_FILES['thefile']['type']."'</p>"; echo "<p>\$_FILES['thefile']['size']=".$_FILES['thefile']['size']."</p>"; echo "<p>\$_FILES['thefile']['tmp_name']='".$_FILES['thefile']['tmp_name']."'</p>"; echo "<p>\$_FILES['thefile']['error']=".$_FILES['thefile']['error']."</p>";

The first five lines are just used to print out the values in the _FILES array for the file, 'thefile', that was uploaded. This is just for your entertainment and education.

if($_FILES['thefile']['error']==UPLOAD_ERR_OK){ // Upload happened ok. Lets try to move the file. $uploaddir='../../uploads/'; $uploadfile=$uploaddir.basename($_FILES['thefile']['name']);

If the error entry for our file is UPLOAD_ERR_OK, (0), then we're going to try to move it somewhere. If we don't move it, then when this php code is done, php will unlink (remove), the file and it will be too late. So first, we form $uploadfile that says where we want to move the file, and what we want to name it.

N.B. The directory you're moving to has to be writable by the userid that the http server is running as. If not, the next part, the move_uploaded_file() will fail.

if (move_uploaded_file($_FILES['thefile']['tmp_name'], $uploadfile)) { echo "<p>File is valid, and successfully uploaded as $uploadfile.</p>"; } else { echo "<p>"; }

We call move_uploaded_file() to do two things. First, it makes sure that the first argument is actually a file that was uploaded via PHPs HTTP POST method. If not it will refuse to deal with it. Second, it tries to move it to the location and name in the second argument. If all that works, it returns TRUE. If anything fails it returns FALSE, and issues a warning. If you want to know why it fails, include this line

ini_set("display_errors", "1");

in your php code before the line that causes the problem.

}else{ // File didn't upload correctly, the error tells why

Here we enter error handling for when the file didn't upload correctly, i.e. the _FILES['thefile']['error'] is something other than UPLOAD_ERR_OK. In this code we tell you as much as possible what went wrong, but you'd want to be a bit more user friendly in a real application.

switch($_FILES['thefile']['error']){ case UPLOAD_ERR_INI_SIZE: echo "FILE TOO LARGE, bigger than php.ini allows."; break; case UPLOAD_ERR_FORM_SIZE: echo "FILE TOO LARGE, bigger than html form allows."; break; case UPLOAD_ERR_PARTIAL: echo "PARTIAL UPLOAD,"; break; case UPLOAD_ERR_NO_FILE: echo "NO FILE UPLOADED,"; break; case UPLOAD_ERR_NO_TMP_DIR: echo "Upload temp directory doesn't exist,"; break; case UPLOAD_ERR_CANT_WRITE: echo "Couldn't write to the temp directory,"; break; case UPLOAD_ERR_EXTENSION: echo "Upload was blocked by a PHP extension,"; default: echo "Unknown error, "; } echo " file '".$_FILES['thefile']['name']."' was not uploaded.</p>"; } ?>

Here's an example of the output from the php

Assuming you said, (as I just did), that you wanted to upload a file named .bashrc, you might see output such as

$_FILES['thefile']['name']='.bashrc' $_FILES['thefile']['type']='application/octet-stream' $_FILES['thefile']['size']=3251 $_FILES['thefile']['tmp_name']='/tmp/php8PjkW4' $_FILES['thefile']['error']=0 File is valid, and was successfully uploaded as ../../uploads/.bashrc.

Notice that the index into the php array _FILES, is the name of the file input from our form, 'thefile'.

Associated with that entry in the _FILES array are

Multiple files

There's a couple of ways to upload multiple files, first you can have multiple file input statements all using the same HTML array, and second, you can use the newer multiple attribute, for the file input, although this will also have to use an HTML array.

Multiple file input elements

<form enctype='multipart/form-data' action='getfiles.php' method='post'> <input type="hidden" name="MAX_FILE_SIZE" value="30000" /> <input type='file' name='thefiles[]' /> <input type='file' name='thefiles[]' /> <input type='file' name='thefiles[]' /> <input type='file' name='thefiles[]' /> <input type='submit' value='Submit the File' /> </form>

Using the multiple attribute

For the last few years, you've been able to give a new attribute, multiple to the file input element.
<form enctype='multipart/form-data' action='getfiles.php' method='post'> <input type="hidden" name="MAX_FILE_SIZE" value="30000" /> <input type='file' multiple name='thefiles[]' /> <input type='submit' value='Submit the File(s)' /> </form>

Notice that both of them use brackets [] with the file name to tell the browser that you expect to create an array of files.

The first one works in more browsers so far, but limits you to the number of file elements you put on your page. It has the advantage that the file browser pops up for each individual file so they don't have to come from the same directory.

The second one doesn't work on as many browsers, but lets you <CTRL>-click in the browser to choose more than one file, or <SHIFT>-click to select a range of files. All of the files, though, have to be from the same directory.

PHP backend for multiple files

$_FILES['thefiles']['name'] $_FILES['thefiles']['type'] $_FILES['thefiles']['size'] $_FILES['thefiles']['tmp_name'] $_FILES['thefiles']['error']
$_FILES['thefiles']['name'][0] $_FILES['thefiles']['type'][0] $_FILES['thefiles']['size'][0] $_FILES['thefiles']['tmp_name'][0] $_FILES['thefiles']['error'][0]

The array as you'll deal with it in PHP has a strange structure. You still have the same $_FILES as before, except that each of them, now, is an array. If you want to look at the information for the first file in the array, you would index each of them with the index 0.

Here's the PHP I use to handle both of the multiple file forms

<?php echo '<p>There are '.count($_FILES['thefiles']['error']).' files </p>'; foreach($_FILES['thefiles']['error'] as $key => $error){ echo "<p><strong>'".$_FILES['thefiles']['name'][$key]."'</strong></p>"; echo "<ul>"; echo "<li>type: '".$_FILES['thefiles']['type'][$key]."'</li>"; echo "<li>size: ".$_FILES['thefiles']['size'][$key]."</li>"; echo "<li>tmp_name: '".$_FILES['thefiles']['tmp_name'][$key]."'</li>"; echo "<li>error: ".$_FILES['thefiles']['error'][$key]."</li></ul>";

The above lines are just to echo information so you can see what's going on. In real code, this wouldn't be the kind of feedback you'd give.

The important thing to notice in in the foreach, where $key is set. It's the key for the $_FILES['thefiles']['error'] array, a number from 0-N where N is one less than the number of uploaded files. We'll use that to index into the ['name'], ['type'], ['size'], and ['tmp_name'] equivalents as well.

if($error==UPLOAD_ERR_OK ){ // Upload happened ok. Lets try to move the file. $uploaddir='../../uploads/'; $uploadfile=$uploaddir.basename($_FILES['thefiles']['name'][$key]); if (move_uploaded_file($_FILES['thefiles']['tmp_name'][$key], $uploadfile)){ echo "<p>File is valid, and successfully uploaded as $uploadfile.</p>"; echo "<br />"; } else { echo "<p>File failed to upload.</p>"; }

This is the code that deals with an uploaded file. You'll see that it's identical to what we used before in the single file case, with the addition of an additional index [$key] on each access to part of the $_FILES array.

}else{ // File didn't upload correctly, the error tells why echo "<p>"; switch($_FILES['thefiles']['error'][$key]){ case UPLOAD_ERR_INI_SIZE: echo "FILE TOO LARGE, bigger than php.ini allows."; break; case UPLOAD_ERR_FORM_SIZE: echo "FILE TOO LARGE, bigger than html form allows."; break; case UPLOAD_ERR_PARTIAL: echo "PARTIAL UPLOAD,"; break; case UPLOAD_ERR_NO_FILE: echo "NO FILE UPLOADED,"; break; case UPLOAD_ERR_NO_TMP_DIR: echo "Upload temp directory doesn't exist,"; break; case UPLOAD_ERR_CANT_WRITE: echo "Couldn't write to the temp directory,"; break; case UPLOAD_ERR_EXTENSION: echo "Upload was blocked by a PHP extension,"; default: echo "Unknown error, "; } echo " file '".$_FILES['thefiles']['name'][$key]."' was not uploaded.</p>"; } } ?>

Finally the code to handle for each file the case where it didn't upload correctly, is just that same as we saw before, again, with the exception of the additional [$key] index.

N.B. on the first multiple file example, a user can choose to not select files for some of the slots. If they don't, then we'll get into this code, because no file was uploaded for each of the slots that they didn't make a selection on. In the second type of multiple file upload form, only as many slots show up as the user selected files, so the same thing won't happen. Of course in either case, they could click on the submit button without selecting anything, and in that case the first type with four file elements will have four empty elements, the the second one using the multiple keyword on one file input element will have one empty entry.

PHP Configuration

There are several things you can set on the server side to change how PHP deals with file uploads. These can be set system wide in the php.ini file for your system, e.g.

file_uploads=On

Or you can set them on a per directory basis in the .htaccess for that directory, e.g.

php_value file_uploads=On

Caveats

That's all there is

As long as everything is set up correctly, that's all that you need to know. Of course once you have the file, you'll have to do something with it. Keep writing code.

(Back to Web tutorials)