PHP Upload Script Example
In this example, we are going to see how to make file uploads with PHP, that is, how to upload a file from the client side, to the server side.
The process itself is pretty simple, but several checks have to be done to ensure that the file upload is made safely, i.e., to neutralise any hypothetical malicious misuse; and to be able to detect every possible error to act accordingly, so, this is something that must be done carefully.
For this tutorial, we will use:
- Ubuntu (14.04) as Operating System.
- Apache HTTP server (2.4.7).
- PHP (5.5.9).
You may skip environment preparation and jump directly to the beginning of the example below.
1. Preparing the environment
1.1. Installation
Below, are shown the commands to install Apache and PHP:
sudo apt-get update sudo apt-get install apache2 php5 libapache2-mod-php5 sudo service apache2 restart
1.2. PHP configuration
Is necessary to configure PHP to allow file uploads. With your favourite editor, open the php.ini
file:
sudo vi /etc/php5/apache2/php.ini
And check that the file_uploads
directive value is set to On
:
file_uploads = On
If you want, you can change the upload_max_filesize
directive, to change the maximum size allowed for upload.
Don’t forget to restart Apache after doing any change.
Note: if you want to use Windows, installing XAMPP is the fastest and easiest way to install a complete web server that meets the prerequisites.
2. Upload form
First, we need to create a form, where the file will be attached. It should be similar to this:
upload_form.html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Upload form</title> </head> <body> <form action="upload.php" method="post" enctype="multipart/form-data"> <div> <label for="file">Select a file to upload:</label> <input type="file" name="file" id="file"> </div> <div> <input type="submit" value="Upload file!"> </div> </form> </body> </html>
There are several things to take into account:
- Form
action
must point to the PHP file that will perform the upload (this is quite obvious). - Submission method must be
POST
. - Form
enctype
attribute must be specified, withmultipart/form-data
value. This is not to make the form encode the characters.
2.1. Accepting only certain type of files
It is possible to restrict the uploading files to only expected file types. This can be made in several ways:
- By file extension. For example, if we are expecting a document, we can restrict the extensions to
.pdf
and.doc
. - By file type. We can directly say that we are expecting an image, an audio or/and a video.
- By media type, specifying the MIME type. An example could be
text/plain
, if we are expecting plain files.
All of these can be combinated, so we could say, for example, that we are expecting a document file, and it can be accepted in pdf, doc, docx and odt and format.
Applying this last example to the form above (line 11), the result would be the following:
upload_form.html
<input type="file" name="file" id="file" accept=".pdf,.doc,.docx,.odt">
As you can see, the conditions are sepparated by comma.
Note: keep always in mind that client-side validation is never enough. Every validation we do in the client-side, it should be done also in the server – remember that here is the person in the client-side who has the control, not you!
3. PHP upload script
Once the form is submitted, is time for the server to handle the upload. The following script will do the job:
upload.php
<?php define('UPLOAD_DIRECTORY', '/var/php_uploaded_files/'); $uploadedTempFile = $_FILES['file']['tmp_name']; $filename = $_FILES['file']['name']; $destFile = UPLOAD_DIRECTORY . $filename; move_uploaded_file($uploadedTempFile, $destFile);
Easy, isn’t it? We just define the target directory where the file will be stored, we get the file PHP uploaded temporarily, original file’s name, and we move that file to the defined target directory, with its name.
But, as we said in the introduction, in this task we must focus the efforts into the security. This script does not check:
- File size. We should set a maximum allowed size (what if an user is trying to upload a file bigger than server’s available space?)
- File type. If we are expecting, for example, a document, receiving an executable file would be quite suspicious, wouldn’t it?
- That the file was properly uploaded to the server, and moved to the target directory. We can’t take anything for granted. With that script, we won’t notice if an error occurs in the upload.
3.1. A secure file upload script
Let’s update our script to make a safe file upload, supposing that we expect only document-type files, as we defined 2.1 section, of pdf, doc, docx and odt format:
upload.php
<?php define('UPLOAD_DIRECTORY', '/var/php_uploaded_files/'); define('MAXSIZE', 5242880); // 5MB in bytes. // Before PHP 5.6, we can't define arrays as constants. $ALLOWED_EXTENSIONS = array('pdf', 'doc', 'docx', 'odt'); $ALLOWED_MIMES = array('application/pdf', // For .pdf files. 'application/msword', // For .doc files. 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // For .docx files. 'application/vnd.oasis.opendocument.text', // For .odt files. ); /** * Checks if given file's extension and MIME are defined as allowed, which are defined in * array $ALLOWED_EXTENSIONS and $ALLOWED_MIMES, respectively. * * @param $uploadedTempFile The file that is has been uploaded already, from where the MIME * will be read. * @param $destFilePath The path that the dest file will have, from where the extension will * be read. * @return True if file's extension and MIME are allowed; false if at least one of them is not. */ function validFileType($uploadedTempFile, $destFilePath) { global $ALLOWED_EXTENSIONS, $ALLOWED_MIMES; $fileExtension = pathinfo($destFilePath, PATHINFO_EXTENSION); $fileMime = mime_content_type($uploadedTempFile); $validFileExtension = in_array($fileExtension, $ALLOWED_EXTENSIONS); $validFileMime = in_array($fileMime, $ALLOWED_MIMES); $validFileType = $validFileExtension && $validFileMime; return $validFileType; } /** * Handles the file upload, first, checking if the file we are going to deal with is actually an * uploaded file; second, if file's size is smaller than specified; and third, if the file is * a valid file (extension and MIME). * * @return Response with string of the result; if it has been successful or not. */ function handleUpload() { $uploadedTempFile = $_FILES['file']['tmp_name']; $filename = basename($_FILES['file']['name']); $destFile = UPLOAD_DIRECTORY . $filename; $isUploadedFile = is_uploaded_file($uploadedTempFile); $validSize = $_FILES['file']['size'] <= MAXSIZE && $_FILES['file']['size'] >= 0; if ($isUploadedFile && $validSize && validFileType($uploadedTempFile, $destFile)) { $success = move_uploaded_file($uploadedTempFile, $destFile); if ($success) { $response = 'The file was uploaded successfully!'; } else { $response = 'An unexpected error occurred; the file could not be uploaded.'; } } else { $response = 'Error: the file you tried to upload is not a valid file. Check file type and size.'; } return $response; } // Flow starts here. $validFormSubmission = !empty($_FILES); if ($validFormSubmission) { $error = $_FILES['file']['error']; switch($error) { case UPLOAD_ERR_OK: $response = handleUpload(); break; case UPLOAD_ERR_INI_SIZE: $response = 'Error: file size is bigger than allowed.'; break; case UPLOAD_ERR_PARTIAL: $response = 'Error: the file was only partially uploaded.'; break; case UPLOAD_ERR_NO_FILE: $response = 'Error: no file could have been uploaded.'; break; case UPLOAD_ERR_NO_TMP_DIR: $response = 'Error: no temp directory! Contact the administrator.'; break; case UPLOAD_ERR_CANT_WRITE: $response = 'Error: it was not possible to write in the disk. Contact the administrator.'; break; case UPLOAD_ERR_EXTENSION: $response = 'Error: a PHP extension stopped the upload. Contact the administrator.'; break; default: $response = 'An unexpected error occurred; the file could not be uploaded.'; break; } } else { $response = 'Error: the form was not submitted correctly - did you try to access the action url directly?'; } echo $response;
Now, let’s see the key instructions added to this script:
- We first check the
$_FILES
superglobal, in line 70. If it’s empty, it would be because the access to this script has not been done through a form submission, so, we shouldn’t perform any action. - We get the
error
property of the superglobal, in line 73. Here is saved the state of the file submission, indicating if it has been done correctly, or if any error occured. The state is later evaluated in theswitch
, where we perform different actions depending on the state. - Getting the basename of the file, in line 47. This PHP built-in gives us the file name. Even if it is defined in
$_FILES['file']['name']
, it is always recommended to get file names using this function. - Checking if the file is actually an uploaded file, in line 50. This function checks that the given file is really a file uploaded through HTTP POST. This check is necessary to avoid malicious actions that may want to have access server files.
- Checking the size of the file, in line 51. Before proceding, we ensure that the uploading file doesn’t exceed the maximum size set. Because the limit does not always have to be the one set in
php.ini
. - Checking the file type, in line 53. This is probably one of the most interesting checks. Here, we ensure that the receiving file is actually a document, in two steps: getting the file extension (line 27), and the file MIME (line 28). It is not enough checking only one of these. Consider the following scenario: we are checking only the file extension, which is expected to be one of those defined in the array. If an evil user wants to upload an executable file, it only has to change its extension to one of those. But the content type is not modified with the extension. So, we have to check both to ensure that the files are real documents.
- Finally, we check that the file has been moved correctly to the target directory, with its original name (line 54). The difference here compared with the previous script, is that we retrieve the boolean value returned by the
move_uploaded_files
function, because it may fail, for any reason. As we said before, we can’t take anything for granted.
3.2. Considerations
You may noticed that the temp files created by PHP ($_FILES['file']['tmp_name']
) are not being deleted. This is because we don’t have to care about it; when the script finishes it execution, PHP will delete them for us.
About the checking of allowed files, in some cases, when the upload is not oriented only to a very specific file type, it may be more interesting to have a blacklist where not allowed file types are defined, instead of having a whitelist, like in the script above. Generally, we won’t expect to receive files that can be harmful, like executable files, or even scripts.
4. Summary
With this script, files will be safely uploaded to our server, ensuring that the received file is of the type/types we are expecting (remember that the validation must reside always in the server-side), and also that doesn’t exceed the established size. And, if something goes wrong, we will be able to identify the exact error, that will allow us to take the most appropiate decision for each case.
5. Download the source code
This was an example of file uploading with PHP.
You can download the full source code of this example here: PHPUploadScript
That’s great, thank you for sharing :-)
How to allow excel files as well (.xlsx, .xls) ?
That’s great, thank you for sharing :-)
How to allow excel files as well (.xlsx, .xls) ?
Also, I was getting error ( Fatal error: Call to undefined function mime_content_type() in c:\xampp\htdocs\test\upload.php on line 28)
Then i searched for that, most people are telling it’s not recommended to use
Also the PHP.net tell: Warning: Mimetype extensions has been removed from PHP 5.3.0 in favor of Fileinfo.
what to do plz ?
Hello Ramin, For allowing other types of files, you would have to: 1: Add the extension to the $ALLOWED_EXTENSIONS array. E.g.: $ALLOWED_EXTENSIONS = array(‘pdf’, ‘doc’, ‘docx’, ‘odt’, ‘xls’, ‘xlsx’); 2: Add the MIME of the filetype to the $ALLOWED_MIMES array. E.g.: $ALLOWED_MIMES = array(‘application/pdf’, // For .pdf files. ‘application/msword’, // For .doc files. ‘application/vnd.openxmlformats-officedocument.wordprocessingml.document’, // For .docx files. ‘application/vnd.oasis.opendocument.text’, // For .odt files. ‘application/vnd.ms-excel’, // For .xlsx files. ‘application/vnd.openxmlformats-officedocument.spreadsheetml.sheet’, // For .xlsx files. ‘application/octet-stream’ // For unknown mimes. ); For this case, we may have to add the ‘application/octet-stream’ MIME type (the MIME is sent by the browser, so perhaps… Read more »