ZF-9635: Zend_Validate_File_MimeType overrides default PHP fileinfo behavior

Description

I would like to propose an improvement to Zend_Validate_File_MimeType as the current method of setting the magic files seems over-complicated and doesn't work on all distributions. On my Mac and on several other Linux distributions, the mime type for images is returned as 'application/octet-stream'.

I first noticed the issue on my Mac OS X 10.5.X (Leopard) running Zend Server w/PHP 5.3.1. If I replaced my /usr/share/file/magic* files with those from Mac OS X 10.6.X (Snow Leopard), the problem went away. My Ubuntu instance with PHP 5.2.10 or 5.3.2 worked fine as it has updated libmagic1 package. However, our production systems use RHEL with compiled PHP 5.2.13 or 5.3.2, but the libmagic files there seem very outdated, causing the validator to fail for Images.

Moreover, Zend_Validate_File_MimeType doesn't allow you to use the finfo_open function with the second parameter set to the default of NULL. All distributions I've tried seem to work when the magic file is not specified.

The patch below changes Zend_Validate_File_MimeType to work more like Zend_File_Transfer_Adapter_Abstract...it doesn't have any complex logic to automatically find a magic file, if the user sets one, it uses it. Otherwise, it does finfo_open w/o specifying a magic file:


<?php
/**
 * Zend Framework
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://framework.zend.com/license/new-bsd
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@zend.com so we can send you a copy immediately.
 *
 * @category  Zend
 * @package   Zend_Validate
 * @copyright  Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
 * @license   http://framework.zend.com/license/new-bsd     New BSD License
 * @version   $Id: MimeType.php 21472 2010-03-11 22:16:55Z thomas $
 */

/**
 * @see Zend_Validate_Abstract
 */
require_once 'Zend/Validate/Abstract.php';

/**
 * Validator for the mime type of a file
 *
 * @category  Zend
 * @package   Zend_Validate
 * @copyright  Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
 * @license   http://framework.zend.com/license/new-bsd     New BSD License
 */
class Zend_Validate_File_MimeType extends Zend_Validate_Abstract
{
    /**#@+
     * @const Error type constants
     */
    const FALSE_TYPE   = 'fileMimeTypeFalse';
    const NOT_DETECTED = 'fileMimeTypeNotDetected';
    const NOT_READABLE = 'fileMimeTypeNotReadable';
    /**#@-*/

    /**
     * @var array Error message templates
     */
    protected $_messageTemplates = array(
        self::FALSE_TYPE   => "File '%value%' has a false mimetype of '%type%'",
        self::NOT_DETECTED => "The mimetype of file '%value%' could not be detected",
        self::NOT_READABLE => "File '%value%' can not be read",
    );

    /**
     * @var array
     */
    protected $_messageVariables = array(
        'type' => '_type'
    );

    /**
     * @var string
     */
    protected $_type;

    /**
     * Mimetypes
     *
     * If null, there is no mimetype
     *
     * @var string|null
     */
    protected $_mimetype;

    /**
     * Magicfile to use
     *
     * @var string|null
     */
    protected $_magicfile;

    /**
     * Option to allow header check
     *
     * @var boolean
     */
    protected $_headerCheck = false;

    /**
     * Sets validator options
     *
     * Mimetype to accept
     *
     * @param  string|array $mimetype MimeType
     * @return void
     */
    public function __construct($mimetype)
    {
        if ($mimetype instanceof Zend_Config) {
            $mimetype = $mimetype->toArray();
        } elseif (is_string($mimetype)) {
            $mimetype = explode(',', $mimetype);
        } elseif (!is_array($mimetype)) {
            require_once 'Zend/Validate/Exception.php';
            throw new Zend_Validate_Exception("Invalid options to validator provided");
        }

        if (isset($mimetype['magicfile'])) {
            $this->setMagicFile($mimetype['magicfile']);
            unset($mimetype['magicfile']);
        }

        if (isset($mimetype['headerCheck'])) {
            $this->enableHeaderCheck($mimetype['headerCheck']);
            unset($mimetype['headerCheck']);
        }

        $this->setMimeType($mimetype);
    }

    /**
     * Returns the actual set magicfile
     *
     * @return string
     */
    public function getMagicFile()
    {
        return $this->_magicfile;
    }

    /**
     * Sets the magicfile to use
     * if null, the MAGIC constant from php is used
     * if the MAGIC file is errorous, no file will be set
     *
     * @param  string $file
     * @throws Zend_Validate_Exception When finfo can not read the magicfile
     * @return Zend_Validate_File_MimeType Provides fluid interface
     */
    public function setMagicFile($file)
    {
        if (isset($file)) {
            $this->_magicfile = (string)$file;
        }
        return $this;
    }

    /**
     * Returns the Header Check option
     *
     * @return boolean
     */
    public function getHeaderCheck()
    {
        return $this->_headerCheck;
    }

    /**
     * Defines if the http header should be used
     * Note that this is unsave and therefor the default value is false
     *
     * @param  boolean $checkHeader
     * @return Zend_Validate_File_MimeType Provides fluid interface
     */
    public function enableHeaderCheck($headerCheck = true)
    {
        $this->_headerCheck = (boolean) $headerCheck;
        return $this;
    }

    /**
     * Returns the set mimetypes
     *
     * @param  boolean $asArray Returns the values as array, when false an concated string is returned
     * @return string|array
     */
    public function getMimeType($asArray = false)
    {
        $asArray   = (bool) $asArray;
        $mimetype = (string) $this->_mimetype;
        if ($asArray) {
            $mimetype = explode(',', $mimetype);
        }

        return $mimetype;
    }

    /**
     * Sets the mimetypes
     *
     * @param  string|array $mimetype The mimetypes to validate
     * @return Zend_Validate_File_Extension Provides a fluent interface
     */
    public function setMimeType($mimetype)
    {
        $this->_mimetype = null;
        $this->addMimeType($mimetype);
        return $this;
    }

    /**
     * Adds the mimetypes
     *
     * @param  string|array $mimetype The mimetypes to add for validation
     * @return Zend_Validate_File_Extension Provides a fluent interface
     */
    public function addMimeType($mimetype)
    {
        $mimetypes = $this->getMimeType(true);

        if (is_string($mimetype)) {
            $mimetype = explode(',', $mimetype);
        } elseif (!is_array($mimetype)) {
            require_once 'Zend/Validate/Exception.php';
            throw new Zend_Validate_Exception("Invalid options to validator provided");
        }

        if (isset($mimetype['magicfile'])) {
            unset($mimetype['magicfile']);
        }

        foreach ($mimetype as $content) {
            if (empty($content) || !is_string($content)) {
                continue;
            }
            $mimetypes[] = trim($content);
        }
        $mimetypes = array_unique($mimetypes);

        // Sanity check to ensure no empty values
        foreach ($mimetypes as $key => $mt) {
            if (empty($mt)) {
                unset($mimetypes[$key]);
            }
        }

        $this->_mimetype = implode(',', $mimetypes);

        return $this;
    }

    /**
     * Defined by Zend_Validate_Interface
     *
     * Returns true if the mimetype of the file matches the given ones. Also parts
     * of mimetypes can be checked. If you give for example "image" all image
     * mime types will be accepted like "image/gif", "image/jpeg" and so on.
     *
     * @param  string $value Real file to check for mimetype
     * @param  array  $file  File data from Zend_File_Transfer
     * @return boolean
     */
    public function isValid($value, $file = null)
    {
        if ($file === null) {
            $file = array(
                'type' => null,
                'name' => $value
            );
        }

        // Is file readable ?
        require_once 'Zend/Loader.php';
        if (!Zend_Loader::isReadable($value)) {
            return $this->_throw($file, self::NOT_READABLE);
        }

        if (class_exists('finfo', false)) {
            $const = defined('FILEINFO_MIME_TYPE') ? FILEINFO_MIME_TYPE : FILEINFO_MIME;
            if (!empty($this->_magicfile)) {
                $finfo = @finfo_open($const, $this->_magicfile);
            }

            if (empty($finfo)) {
                $finfo = @finfo_open($const);
            }

            if ($finfo !== false) {
                $this->_type = finfo_file($finfo, $value);
            }

            unset($finfo);
        }

        if (empty($this->_type) &&
            (function_exists('mime_content_type') && ini_get('mime_magic.magicfile'))) {
                $this->_type = mime_content_type($value);
        }

        if (empty($this->_type) && $this->_headerCheck) {
            $this->_type = $file['type'];
        }

        if (empty($this->_type)) {
            return $this->_throw($file, self::NOT_DETECTED);
        }

        $mimetype = $this->getMimeType(true);
        if (in_array($this->_type, $mimetype)) {
            return true;
        }

        $types = explode('/', $this->_type);
        $types = array_merge($types, explode('-', $this->_type));
        $types = array_merge($types, explode(';', $this->_type));
        foreach($mimetype as $mime) {
            if (in_array($mime, $types)) {
                return true;
            }
        }

        return $this->_throw($file, self::FALSE_TYPE);
    }

    /**
     * Throws an error of the given type
     *
     * @param  string $file
     * @param  string $errorType
     * @return false
     */
    protected function _throw($file, $errorType)
    {
        $this->_value = $file['name'];
        $this->_error($errorType);
        return false;
    }
}

Comments

According to documentation and API you could simply set the option "magicfile" to FALSE which leads to finfo_open to be called without the magicfile parameter.

And there ARE distributions which are delivered with a broken finfo implementation. That was the original reason why we implemented this feature.

Closing as won't fix as this already works and is documented

Thomas,

Thanks for your quick reply.

I don't see this in documentation or in API. The current implementation is not as you have stated, setting the option "magicfile" to false causes $this->_magicfile to be NULL, then during validation (isValid function), getMagicFile is called, and when $this->_magicfile is null, getMagicFile loops over the array $this->_magicFiles. Code that shows this:


<?php 
require_once 'Zend/Validate/File/IsImage.php';

foreach (glob("/Users/calugarn/Desktop/OM Creatives/*") as $filename) {
    $validator = new Zend_Validate_File_IsImage();
    $validator->setMagicFile(false);
    echo $filename . ": ";
    echo ($validator->isValid($filename))?'Yes':'No';
    echo "\n";
}

I have yet to experience problems with fileinfo on any distribution if the magicfile is not passed to finfo_open and I don't understand why the default Zend Framework behavior should be to loop over an array of possible locations. This is slow.

PHP / fileinfo extension provides a way to set the magic file by setting an environment variable: "If not specified, the MAGIC environment variable is used" (http://us.php.net/manual/en/…). This default behavior should suffice. Please review and consider my original code.

Thanks,

-Nick

I'm absolute agree with Nicholas. The problem on line 332 in class Zend_Validate_File_MimeType. This call of method getMagicFile absolutely wont needed. As you can setup magicfile by hand with setMagicFile. Anyway implementation on getMagicFile blocks way to use fopen without setting magicfile as described below. If you setMagicFile as false, method set it as null always, but in method getMagicFile if _magicfile property is null than method searches over _magicFiles array this is problem. In this case you can not set empty magicfile anyway.