Arka Roy

Create File Upload Class in PHP

File upload is a very common functionality that almost all applications have. But we need to make sure that the system is smart enough to prevent malicious usage of this functionality. How can we achieve that?

The basic process of providing a HTML form for uploading user submitted files to the server with PHP is fairly easy and simple. But there are some security implications that many of us are unaware of. We will be building a custom PHP class for secure file upload. This class will check the type and size of the file and rename the file in case of duplication.

Those who are absolutely beginner in handling file upload, are requested to take a look at Enhance File Upload Security with PHP. That article will help you to better understand file uploading from scratch.

The Class

We will start our Upload class with a blank constructor

<?php

class Upload {
    function __construct() {

    }
}

Now we will add some properties to our class to store some data.

<?php

protected $config = array();
protected $current;
protected $errors = array();
protected $new_name;

The $config property holds the configurations like target directory, size limit etc. $current refers to the current key of $_FILES array. $errors contains the errors encountered during process. $new_name stores the name of the uploaded file to be set.

Public Methods

Now we are going to define some public methods to set up some configuration settings.

But first, we need to implement a method for handling errors.

<?php

public function error($msg = null) {
    if ($msg) {
        $this->errors[] = $msg;
        return $this;
    } else {
        return $this->errors;
    }
}

This method will store error messages in the $errors. This method will work as both a getter and setter.

Extensions

Of course you want to prevent the users from uploading specified types of files. Here is the method for allowing or disallowing file types.

<?php

public function setAllowedExtensions($extensions) {
    if (is_array($extensions)) {
        $this->config['allowed_extensions'] = $extensions;
    } else {
        $extensions = explode(',', $extensions);
        $this->config['allowed_extensions'] = array();
        foreach ($extensions as $ext) {
            $this->config['allowed_extensions'][] = trim($ext);
        }
    }
    return $this;
}

public function setDisallowedExtensions($extensions) {
    if (is_array($extensions)) {
        $this->config['disallowed_extensions'] = $extensions;
    } else {
        $extensions = explode(',', $extensions);
        $this->config['disallowed_extensions'] = array();
        foreach ($extensions as $ext) {
            $this->config['disallowed_extensions'][] = trim($ext);
        }
    }
    return $this;
}

The allowed extensions will be stored in $config['allowed_extensions'] and the disallowed extensions in $config['disallowed_extensions']. You can either specify allowed file types or disallowed file types.

Target Directory

Now its time to specify where our uploaded images will live. We should add a method to setup this setting.

<?php

public function setDirectory($dir) {
    $dir = str_replace('\\', '/', $dir);
    $dir = rtrim($dir, '/');
    $this->config['directory'] = $dir;
    return $this;
}

Max Size

Obviously we want to limit the size of the files to be uploaded. But our specified limit cannot be greater than that specified in PHP config.

<?php

public function setMaxSize($size) {
    $max_size = $this->convertSizeToBytes($size);
    $upload_max_filesize = $this->convertSizeToBytes(ini_get('upload_max_filesize'));
    $this->config['max_size'] = ($max_size <= $upload_max_filesize) ? $max_size : $upload_max_filesize;
    return $this;
}

This will check the size limit from PHP configuration. If the specified limit is larger, it will automatically set the size from PHP config. For this method to work properly, we need to implement a helper method.

<?php

protected function convertSizeToBytes($size) {
    $size = trim($size);
    $unit = strtoupper($size[strlen($size) - 1]);
    if (in_array($unit, array('T', 'G', 'M', 'K', 'B'))) {
        switch ($unit) {
            case 'T':
                $size *= 1024;
            case 'G':
                $size *= 1024;
            case 'M':
                $size *= 1024;
            case 'K':
                $size *= 1024;
            case 'B':
                $size *= 1;
        }
    }
    return $size;
}

Overwrite

We need provide a way to specify whether we want to overwrite files or not.

<?php

public function setOverwrite($overwrite = TRUE) {
    $this->config['overwrite'] = $overwrite;
    return $this;
}

Methods for the Magic

The methods mentioned till now are going to be used to set the configuration settings. For the actual file uploading process, we need to add a few more internal methods to check the settings or generate a new name for the file or upload the file etc.

<?php

// Check whether the file is of allowed type
protected function allowedExtension() {
    $allowed = TRUE;
    if (!isset($this->config['allowed_extensions']) || !$this->config['allowed_extensions']) {
        $allowed = TRUE;
    }
    if (!isset($this->config['disallowed_extensions']) || !$this->config['disallowed_extensions']) {
        $allowed = TRUE;
    }
    $ext = pathinfo($this->current['name'], PATHINFO_EXTENSION);
    $ext = strtolower($ext);
    if (isset($this->config['allowed_extensions']) && $this->config['allowed_extensions']) {
        $allowed = in_array($ext, $this->config['allowed_extensions']);
    }
    if (isset($this->config['disallowed_extensions']) && $this->config['disallowed_extensions']) {
        $allowed = !in_array($ext, $this->config['disallowed_extensions']);
    }
    return $allowed;
}

// Check whether the file is within size limit
protected function allowedSize() {
    $size = $this->current['size'];
    return $size <= $this->config['max_size'];
}

// Validate the target directory
protected function checkDirectory() {
    if (!isset($this->config['directory']) || empty($this->config['directory'])) {
        return FALSE;
    }
    if (!is_dir($this->config['directory'])) {
        mkdir($this->config['directory']);
    }
    return is_writable($this->config['directory']);
}

// Generate new name for the file to be saved
// it will replace spaces with dashes
// name will be lowercase
// generate new name if file exists
protected function setNewName() {
    $filename = $this->current['name'];
    $name = pathinfo($filename, PATHINFO_FILENAME);
    $ext = pathinfo($filename, PATHINFO_EXTENSION);
    $name = str_replace(' ', '-', $name);
    $name = strtolower($name);
    $this->new_name = $name . '.' . $ext;
    if (isset($this->config['overwrite']) && $this->config['overwrite']) {
        return $this;
    }
    $count = 0;
    while (file_exists($this->config['directory'] . '/' . $this->new_name)) {
        $count++;
        $this->new_name = $name . '_' . $count . '.' . $ext;
    }
    return $this;
}

// Move uploaded file with new name
protected function moveFile() {
    $this->setNewName();
    return move_uploaded_file($this->current['tmp_name'], $this->config['directory'] . '/' . $this->new_name);
}

// Sets error messages according to upload error
protected function uploadError() {
    switch ($this->current['error']) {
        case 1:
        case 2:
            $this->error('The file is bigger than specified limit.');
            break;
        case 3:
            $this->error('The file is uploaded partially. Please try again.');
            break;
        case 4:
            $this->error('No file selected to upload.');
            break;
        case 6:
            $this->error('No upload directory has been specified.');
            break;
        default :
            $this->error('Failed to upload the file.');
            break;
    }
}

// Public method to execute the upload process
public function run() {
    $this->current = current($_FILES);
    if ($this->current['error'] != 0) {
        $this->uploadError();
        return FALSE;
    }
    if (!$this->allowedExtension()) {
        $this->error('The type of the file is not supported.');
        return FALSE;
    }
    if (!$this->allowedSize()) {
        $this->error('The size of the file is bigger than limit.');
        return FALSE;
    }
    if (!$this->checkDirectory()) {
        $this->error('Unable to access upload directory');
        return FALSE;
    }
    if ($this->moveFile()) {
        return $this->new_name;
    }
    return FALSE;
}

Adding power to the Constructor

While we are able set configuration settings by calling respective methods, it will be more comprehensive if we can provide an option to set the configuration in the constructor.

<?php

function __construct($config = array()) {
    if ($config && !is_array($config)) {
        return FALSE;
    }
    foreach ($config as $option => $value) {
        $parts = explode('_', $option);
        $cap = array_map('ucfirst', $parts);
        $method = 'set' . implode("", $cap);
        if (method_exists($this, $method)) {
            call_user_func_array(array($this, $method), (array) $value);
        }
    }
}

Usage

To use this class, lets have a form first.

<form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="post" enctype="multipart/form-data">
    <label for="file">Select File:</label>
    <input type="file" name="file" id="file">
    <input type="submit" name="upload" value="Upload File">
</form>

Add following lines of code at the top of the document.

<?php

if(isset($_POST['upload'])) {
    require 'upload.php';
    $upload = new Upload([
        'overwrite' => FALSE,
        'max_size' => '5M',
        'directory' => __DIR__ . '/uploads/',
        'disallowed_extensions' => 'exe, bin, sh, py',
    ]);
    $response = $upload->run();
    if(!$response) {
        $errors = $upload->error();
    }
}

Other Articles

Enhance File Upload Security with PHP

Allowing user to be able to upload a file is a very common functionality that developers need to implement. But how can we make sure that user would not upload anything malicious?

Download ZIP File Dynamically with PHP

We will see how we can make a webpage act as an initializer to download a zip file. We will just provide the location of the file and PHP will download it to the user. In the back-end, the HTTP headers are responsible for the download. We will set the headers with PHP.