MimeUploadValidator::validate()   A
last analyzed

Complexity

Conditions 4
Paths 6

Size

Total Lines 32
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 4
eloc 19
nc 6
nop 0
dl 0
loc 32
rs 9.6333
c 2
b 0
f 0
1
<?php
2
3
namespace SilverStripe\MimeValidator;
4
5
use finfo;
6
use Exception;
7
use SilverStripe\Core\Config\Config;
8
use SilverStripe\Control\HTTP;
9
use SilverStripe\Assets\Upload_Validator;
10
11
/**
12
 * Adds an additional validation rule to Upload_Validator that attempts to detect
13
 * the file extension of an uploaded file matches it's contents, which is done
14
 * by detecting the MIME type and doing a fuzzy match.
15
 *
16
 * Class MimeUploadValidator
17
 * @package SilverStripe\MimeValidator
18
 */
19
class MimeUploadValidator extends Upload_Validator
20
{
21
    /**
22
     * The preg_replace() pattern to use against MIME types. Used to strip out
23
     * useless characters so matching of MIME types can be fuzzy.
24
     *
25
     * @var string Regexp pattern
26
     */
27
    protected $filterPattern = '/.*[\/\.\-\+]/i';
28
29
    /**
30
     * @param string $pattern
31
     */
32
    public function setFilterPattern($pattern)
33
    {
34
        $this->filterPattern = $pattern;
35
    }
36
37
    /**
38
     * @return string
39
     */
40
    public function getFilterPattern()
41
    {
42
        return $this->filterPattern;
43
    }
44
45
    /**
46
     * Check if the temporary file has a valid MIME type for it's extension.
47
     *
48
     * @uses finfo php extension
49
     * @return bool|null
50
     * @throws MimeUploadValidatorException
51
     */
52
    public function isValidMime()
53
    {
54
        $extension = strtolower(pathinfo($this->tmpFile['name'], PATHINFO_EXTENSION));
55
56
        // we can't check filenames without an extension or no temp file path, let them pass validation.
57
        if (!$extension || !$this->tmpFile['tmp_name']) {
58
            return true;
59
        }
60
61
        $expectedMimes = $this->getExpectedMimeTypes($this->tmpFile);
62
        if (empty($expectedMimes)) {
63
            throw new MimeUploadValidatorException(
64
                sprintf('Could not find a MIME type for extension %s', $extension)
65
            );
66
        }
67
68
        $fileInfo = new finfo(FILEINFO_MIME_TYPE);
69
        $foundMime = $fileInfo->file($this->tmpFile['tmp_name']);
70
        if (!$foundMime) {
71
            throw new MimeUploadValidatorException(
72
                sprintf('Could not find a MIME type for file %s', $this->tmpFile['tmp_name'])
73
            );
74
        }
75
76
        foreach ($expectedMimes as $expected) {
77
            if ($this->compareMime($foundMime, $expected)) {
78
                return true;
79
            }
80
        }
81
82
        return false;
83
    }
84
85
    /**
86
     * Fetches an array of valid mimetypes.
87
     *
88
     * @param $file
89
     * @return array
90
     * @throws MimeUploadValidatorException
91
     */
92
    public function getExpectedMimeTypes($file)
93
    {
94
        $extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
95
96
        // if the finfo php extension isn't loaded, we can't complete this check.
97
        if (!class_exists('finfo')) {
98
            throw new MimeUploadValidatorException('PHP extension finfo is not loaded');
99
        }
100
101
        // Attempt to figure out which mime types are expected/acceptable here.
102
        $expectedMimes = array();
103
104
        // Get the mime types set in framework core
105
        $knownMimes = Config::inst()->get(HTTP::class, 'MimeTypes');
106
        if (isset($knownMimes[$extension])) {
107
            $expectedMimes[] = $knownMimes[$extension];
108
        }
109
110
        // Get the mime types and their variations from mime validator
111
        $knownMimes = $this->config()->get('MimeTypes');
112
        if (isset($knownMimes[$extension])) {
113
            $mimes = (array) $knownMimes[$extension];
114
115
            foreach ($mimes as $mime) {
116
                if (!in_array($mime, $expectedMimes)) {
117
                    $expectedMimes[] = $mime;
118
                }
119
            }
120
        }
121
122
        return $expectedMimes;
123
    }
124
125
    /**
126
     * Check two MIME types roughly match eachother.
127
     *
128
     * Before we check MIME types, remove known prefixes "vnd.", "x-" etc.
129
     * If there is a suffix, we'll use that to compare. Examples:
130
     *
131
     * application/x-json = json
132
     * application/json = json
133
     * application/xhtml+xml = xml
134
     * application/xml = xml
135
     *
136
     * @param string $first The first MIME type to compare to the second
137
     * @param string $second The second MIME type to compare to the first
138
     * @return boolean
139
     */
140
    public function compareMime($first, $second)
141
    {
142
        return preg_replace($this->filterPattern, '', $first) === preg_replace($this->filterPattern, '', $second);
143
    }
144
145
    public function validate()
146
    {
147
        if (parent::validate() === false) {
148
            return false;
149
        }
150
151
        try {
152
            $result = $this->isValidMime();
153
            if ($result === false) {
154
                $extension = strtolower(pathinfo($this->tmpFile['name'], PATHINFO_EXTENSION));
155
                $this->errors[] = _t(
156
                    __CLASS__ . '.INVALIDMIME',
157
                    'File type does not match extension (.{extension})',
158
                    [
159
                        'extension' => $extension,
160
                    ]
161
                );
162
163
                return false;
164
            }
165
        } catch (MimeUploadValidatorException $e) {
166
            $this->errors[] = _t(
167
                __CLASS__ . '.FAILEDMIMECHECK',
168
                'MIME validation failed: {message}',
169
                'Argument 1: Message about why MIME type detection failed',
170
                ['message' => $e->getMessage()]
171
            );
172
173
            return false;
174
        }
175
176
        return true;
177
    }
178
}
179