ClamAV as a Validation Filter in Zend Framework

Share this article

Ok, so you’re pretty comfortable with using the Zend Framework, specifically the use of Forms. Along with that, you have a good working knowledge of how to combine a host of standard validators such as CreditCard, EmailAddress, Db_RecordExists, and Hex, and standard filters such as Compress/Decompress, BaseName, Encrypt, and RealPath. But what do you do when a situation arises that’s outside the scope of the pre-packaged validators and filters?

Let’s say you want to guard against users uploading files that contain viruses, for example. You would have to write a custom validator that checks the uploads aren’t infected. Today I’ll show you how to do just that – how to write a new file validation filter for Zend Framework that uses ClamAV to ensure uploaded files are virus-free.

Adding ClamAV Support to PHP

First you’ll need to install ClamAV support. I’m basing this installation procedure around Linux, specifically Ubuntu. If you’re using another distribution, you may need to adjust the commands accordingly. Unfortunately, if you’re using Windows however, you’ll need to use a Linux-based Virtual Appliance or setup a virtual machine running Linux to follow along since the php-clamav extension doesn’t support Windows as yet.

Before you attempt to install ClamAv, ensure that you have the library’s dependencies installed. You’ll also want to make sure you have the PHP dev package installed so phpize is available. You can do this by running the following command:

msetter@tango:~$ sudo apt-get install php5-dev libclamav-dev clamav libclamav6 clamav-freshclam

Once you have the dependencies installed, grab a copy of the php-clamav library from sourceforge.net/projects/php-clamav and extract it to a temporary directory on your system. Navigate into the extracted library’s directory and run the following commands:

msetter@tango:~/php-clamav$ phpize
msetter@tango:~/php-clamav$ ./configure --with-clamav
msetter@tango:~/php-clamav$ make

If they all execute without errors, you’ll find a newly compiled module in the modules subdirectory. Copy the module to the directory in which the rest of your PHP modules reside. Your system may vary, but I was able to do it with:

msetter@tango:~/php-clamav$ sudo cp modules/clamav.so /usr/lib/php5/20090626+lfs/

You then need to enable the module in PHP’s configuration file. This is done pretty simply by adding the following line to php.ini and restarting Apache:

extension=clamav.so

Finally, either run php -i from the command line or execute a simple PHP script that contains just a call to phpinfo() to verify the new extension is enabled. You should see output similar to that below.

clamav extension in phpinfo output

The ClamAv library comes with a series of constants and functions, but in this article I will focus on just two functions, cl_scanfile() and cl_pretcode(), as all you need to do is scan the uploaded file and report what the virus is if one is found. For more information on the other available functions visit php-clamav.sourceforge.net.

Building the File Upload Validator

Now that the extension is installed and enabled, let’s get underway and build the Zend Framework ClamAV file upload validator. I’ll assume that you already have a working Zend Framework project which has module support enabled and ready to go. Add support for the new validation library by adding the following line to your application.ini file:

autoloaderNamespaces[] = "Common_"

Then, under the library directory of your Zend Framework project root, create the directory Common/Validate/File and within it a file named ClamAv.php with the following content:

<?php
class Common_Validate_File_ClamAv extends Zend_Validate_Abstract
{
}

With that, your new validator class will be available to the project.

If you’re not familiar with validators in Zend Framework, they’re a pretty straight-forward affair. You can either extend them from Zend_Validate_Abstract or Zend_Validate_Interface. For the purposes of this example, I’m basing the validator on the former. Given that, you will only have to implement two methods: the constructor and isValid().

The constructor should check whether the ClamAv extension is loaded as it’s not shipped with a standard distribution of PHP.

The isValid() method will perform the core work of the validator. Normally the method validates some input and either returns true if the validation was successful or sets an error message in the errors list that is shown afterwards and returns false if the validation failed. Depending on the configuration of your form validators, returning false will either halt the form validation at that point or let the remaining validators continue to run.

Fill out the Common_Validate_File_ClamAv class so it looks like this:

<?php
class Common_Validate_File_ClamAv extends Zend_Validate_Abstract
{
    const STATUS_CLEAN = 0;
    const NOT_READABLE = "fileNotReadable";
    const FILE_INFECTED = "fileInfected";

    protected $_messageTemplates = array(
        self::FILE_INFECTED => "File '%value%' is infected",
        self::NOT_READABLE => "File '%value%' is not readable");

    public function __construct() {
        if (!extension_loaded('clamav')) {
            throw new Zend_Validate_Exception(
                "ClamAv extension is not loaded");
        }
    }
 
    public function isValid($value, $file = null) {
        if ($file === null) {
            $file = array("type" => null, "name" => $value);
        }
        
        if (!Zend_Loader::isReadable($value)) {
            return $this->_throw($file, self::NOT_READABLE);
        }

        $retcode = cl_scanfile($value, $virusname);
        if ($retcode !== self::STATUS_CLEAN) {
            printf("File path: %s | Return code: %s | Virus found name: %s",
                $value, cl_pretcode($retcode), $virusname);
            return $this->_throw($file, self::FILE_INFECTED);
        }
        
        return true;
    }
    
    protected function _throw($file, $errorType) {
        $this->_value = $file["name"];
        $this->_error($errorType);
        return false;
    }
}

First a set of class constants are specified that define the return status for the virus check string templates for custom errors messages. Following that, the constructor checks for ClamAv support being available. If it’s not available, then an exception is thrown.

The isValid() method checks if it the incoming $value argument contains a filename and that the file is readable. If it is, then the cl_scanfile() function is called. The return code from cl_scanfile() indicates whether the file is virus-free. If not, then the name of the virus is retrieved using the cl_pretcode() function and the information is printed.

The _throw() method takes care of setting the appropriate error constant in the class and returning false to indicate that validation has failed. If this happens, the error message linked to the constant will be displayed in the upload form through the use of an error decorator on the input element.

Testing the Validator

With the validator written, you’ll need a form to make use of it and test that it works. Either manually or with zf.sh, create a new action in the IndexController class of the default module and call it “fileUpload”. Add the following code to it:

<?php
class IndexController extends Zend_Controller_Action
{
...
    public function fileUploadAction() {
        $form = new Zend_Form();
        $form->setAction("/default/index/file-upload")
             ->setMethod("post");
    
        $uploadFile = new Zend_Form_Element_File("uploadfile");
        $uploadFile->addValidator(new Common_Validate_File_ClamAv())
           ->setRequired(true)
           ->setLabel("Upload file:");
       
        $form->addElement($uploadFile);
        $form->addElement(new Zend_Form_Element_Submit("submit"));
    
        if ($form->isValid($_POST)) {
            $values = $form->getValues();
            $this->view->messages = array("File uploaded");
        }
    
        $this->view->form = $form;
    }
}

Here you’ve created a simple form and set its action and method properties, a submit button, and a file element. The newly created ClamAv file validator is added to the file element. In addition, the required flag Is set to true ensuring that a file must be uploaded. Following this, both elements are added to the form and a simple if statement checks whether the form has been submitted.

If the form doesn’t validate after being submitted (i.e. the file has a virus), then a validation message will be displayed using the standard error message decorator. Otherwise, a message is added to the view’s messages which will be displayed to the user to indicate the upload was successful.

The last piece is the view script, which is shown below:

<h1>Zend Framework - ClamAV File Upload Validator</h1>
<?php
if (count($this->messages)) {
    echo '<ul id="messages">';
    foreach ($this->messages as $message) {
        echo "<li>" . $this->escape($message) . "</li>";
    }
    echo "</ul>";
}
echo $this->form;

As the lions share of the work already taken care of by the controller and the validator, the view script doesn’t need to do a lot. It simply displays any messages that have been set by the controller and renders the form.

Summary

After working through all that code, you now have a new validator for the Zend Framework that, via the PHP ClamAv library, will check if a file is virus free. I hope that you found this article helpful, both for showing how to create your own custom validators in the Zend Framework and for being able to ensure that you have virus free uploads in the applications that you create from here on in. If you’d like to inspect the code further, code for this article is available for cloning on GitHub.

Image via mathagraphics / Shutterstock

Matthew SetterMatthew Setter
View Author

Matthew Setter is a software developer, specialising in reliable, tested, and secure PHP code. He’s also the author of Mezzio Essentials (https://mezzioessentials.com) a comprehensive introduction to developing applications with PHP's Mezzio Framework.

Advanced
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week