File Uploads With Web Forms and PHP
by Patrick Horgan
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.
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
- name - The name of the file on the machine it was uploaded from.
- type - the mime type of the file, here 'application/octet-stream'.
This comes from the browser and you can't really rely on it. It often
just reflects the file extension of the file, e.g.
.png, so is really
under the control of the user. If knowing the type of the file is
important to you, you need to check for yourself, for example checking
the first few bytes of the file.
- size - the size of the uploaded file
- tmp_name - the name of the file as uploaded. If you don't grab
this file before you exit php, it will be removed. If error is not
zero, this will be an empty string since the file was not uploaded.
- error - if the file didn't upload, this gives a clue possible values
are
- UPLOAD_ERR_OK - File was uploaded
- UPLOAD_ERR_INI_SIZE - File was larger than the
upload_max_filesize directive in
the php.ini file
- UPLOAD_ERR_FORM_SIZE - File was larger than MAX_FILE_SIZE hidden
input from the html form
- UPLOAD_ERR_PARTIAL - File was partially uploaded.
- UPLOAD_ERR_NO_FILE - Generic file not uploaded.
- UPLOAD_ERR_TMP_DIR - The upload direction doesn't exist
- UPLOAD_ERR_CANT_WRITE - Can't write to the temp dir
- UPLOAD_ERR_EXTENSION - some extension blocked the upload
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
- file_uploads - Whether to allow file uploads at all, On, or Off
- upload_max_filesize - Max for all files, e.g. upload_max_filesize=100M
- post_max_size - Maximum for total post all files are part of this, e.g. 101M, must be greater than upload_max_filesize.
- max_file_uploads - Number of files which can be uploaded at one time. e.g. 20
- upload_tmp_dir - On linux defaults to /tmp. Must be writable by the
userid used by the web server to run php. On Windows must be on the same
partition as the directory you're trying to move files to.
e.g. upload_tmp_dir=/tmp
- memory_limit - If this is too small you won't be able to upload large files, e.g. memory_limit=128M
- max_execution_time - If this is too low we can time out uploading files. e.g. max_execution_time = 5000
- max_input_time - How much time to spend processing request data,
e.g. max_input_time=-1 (unlimited)
Caveats
- If the file name passed to you will end up in a database cleanse it for
all the usual extra problematical quotes and semi-colons
- Make sure the tmp directory and the directory you move to both exist and
are both writable by the web browser
- Do you really want to trust that the file is what it says it is without
checking? Are you going to believe the file extension?
- Files with the same name will overwrite previous ones.
- An Apache web server can have a configuration setting
LimitRequestBody to limit the size of a request. You may have to
raise this as well
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.