Handling File Uploads With PHP
Example Form
Below is an example form for uploading a file.
<form enctype="multipart/form-data" action="/" method="POST">
<input type="hidden" name="MAX_FILE_SIZE" value="2097152" />
<input name="file_input_name" type="file" /><br /><br />
<input type="submit" value="Send File" />
</form>
Encoding Type
When creating a form for a file upload, the data encoding type, enctype
, must be specified as multipart/form-data
.
MAX_FILE_SIZE
Using the a hidden input with MAX_FILE_SIZE
preceding the form input for the file can prevent users from trying to upload a file too large that the server will reject it. It expects a value in bytes.
The user could manipulate this, but that won't stop the server rejecting a file that it considers too large. It just provides convenience for the user in preventing them from wasting time.
Unfortunately, the user will never see a warning, and this only takes effect after the user has been uploading up-to that limit, but it is still better than nothing as it prevents the user having to wait until they have uploaded the entire file before it errors out.
The maximum size that your server can take will be the smaller of the two configuration variables post_max_size
, and upload_max_filesize
.
Unfortunately, when you use ini_get()
on these, it will return a string like 2M
instead of the number of bytes, which is what the input field expects. Hence we need to run a conversion function if we wish to automatically set the value, like below:
<?php
function convertToBytes($val) : int
{
$val = trim($val);
$last = strtolower($val[strlen($val)-1]);
switch ($last)
{
case 'g': $val *= (1024 * 1024 * 1024); break;
case 'm': $val *= (1024 * 1024); break;
case 'k': $val *= 1024; break;
}
return $val;
}
$sizes = [
convertToBytes(ini_get('post_max_size')),
convertToBytes(ini_get('upload_max_filesize')),
];
$maxFileSizeInBytes = min($sizes);
// ... other code and HTML here.
?>
<input type="hidden" name="MAX_FILE_SIZE" value="<?= $maxFileSizeInBytes; ?>" />
$_FILES Output
If you want to see an example of what a var_dump of the $_FILES variable looks like after uploading a file here is an example:
array(1) {
["file_input_name"]=>
array(5) {
["name"]=>
string(17) "Selection_003.png"
["type"]=>
string(9) "image/png"
["tmp_name"]=>
string(14) "/tmp/phpJMlS2W"
["error"]=>
int(0)
["size"]=>
int(39826)
}
}
file_input_name
. It will match whatever you named your input field for the file.
Handling Errors
When something goes wrong, you will get an error code in the "error" field and your $_FILES array will look something like:
array(1) {
["file_input_name"]=>
array(5) {
["name"]=>
string(17) "Selection_009.png"
["type"]=>
string(0) ""
["tmp_name"]=>
string(0) ""
["error"]=>
int(2)
["size"]=>
int(0)
}
}
file_input_name
. It will match whatever you named your input field for the file.
These error codes can be translated into what they mean here. Alternatively, you could use the UploadException class shown below.
UploadException
Here is a useful Exception to use when dealing with file uploads. This will allow you to automatically get the error message you need.
class UploadException extends Exception
{
public function __construct($code)
{
$message = $this->codeToMessage($code);
parent::__construct($message, $code);
}
private function codeToMessage($code)
{
switch ($code)
{
case UPLOAD_ERR_INI_SIZE: $message = "The uploaded file exceeds the upload_max_filesize directive in php.ini"; break;
case UPLOAD_ERR_FORM_SIZE: $message = "The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form"; break;
case UPLOAD_ERR_PARTIAL: $message = "The uploaded file was only partially uploaded"; break;
case UPLOAD_ERR_NO_FILE: $message = "No file was uploaded"; break;
case UPLOAD_ERR_NO_TMP_DIR: $message = "Missing a temporary folder"; break;
case UPLOAD_ERR_CANT_WRITE: $message = "Failed to write file to disk"; break;
case UPLOAD_ERR_EXTENSION: $message = "File upload stopped by extension"; break;
default: $message = "Unknown upload error"; break;
}
return $message;
}
}
Usage:
if ($_FILES['file_input_name']['error'] === UPLOAD_ERR_OK)
{
//uploading successfully done
}
else
{
throw new UploadException($_FILES['file_input_name']['error']);
}
file_input_name
. It will match whatever you named your input field for the file.
Determining File Type
You will have noticed that you have a type
in the $_FILES array to indicate the file type. In our example this was image/png
.
Unfortunately, you shouldn't trust it, as it takes whatever is sent from the browser, so a user could "lie" and say it's a different type to what it actually is.
Most file formats have a header that helps identify what kind of file it is. For example, GIF files always begin with GIF87.
You can use finfo_open, which will use this header to to verify the MIME type of a file.
The method uses "the magic number database", which is just a list of all headers that allows finfo()
to identify the files.
Example
$finfo = new finfo(FILEINFO_MIME);
if (!$finfo)
{
throw new Exception("Opening fileinfo database failed");
}
$filepath = $_FILES['file_input_name']['tmp_name'];
echo $finfo->file($filename); // "image/gif; charset=binary" for Gif, "video/mp4; charset=binary" for an mp4
Don't Specify Magic Database Location
When I was initially studying this, I saw a lot of posts telling me that I needed to specify the path to the magic number database, and I found mine at /etc/magic.mime
, but if I specified this path I would always get
a result of application/octet-stream
. It looks like the magic database is now built into PHP, and not specifying the path now works.
FILEINFO_EXTENSION
I thought it would be easier to use FILEINFO_EXTENSION
instead of FILEINFO_MIME
, in order to get the appropriate extension for the file instead of comparing all the possible mime types.
Unfortunately, this would always return a value of ???
to indicate that it didn't know.
A Package To Make Life Easier
I made a package which encapsulates all the logic above. You can use it with:
composer require programster/upload-file-manager
References
- PHP Manual - POST method uploads
- Stack Overflow- Determining Filetype with PHP. What is Magic Database?
- PHP Manual - finfo_open
- Stack Overflow - HTML Upload MAX_FILE_SIZE does not appear to work
First published: 13th April 2020