Image   A
last analyzed

Complexity

Total Complexity 28

Size/Duplication

Total Lines 170
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 28
eloc 94
dl 0
loc 170
rs 10
c 1
b 0
f 0

6 Methods

Rating   Name   Duplication   Size   Complexity  
A getProcessedContent() 0 6 1
B processImage() 0 47 8
A validate() 0 15 3
C binaryScan() 0 30 12
A mimeScan() 0 9 3
A mimes() 0 7 1
1
<?php
2
/*
3
 * This file is part of the Scrawler package.
4
 *
5
 * (c) Pranjal Pandey <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
namespace Scrawler\Validator\Storage;
12
13
use Symfony\Component\HttpFoundation\File\File;
14
use Symfony\Component\HttpFoundation\File\UploadedFile;
15
16
class Image extends AbstractValidator
17
{
18
    protected ?string $mime = null;
19
    /**
20
     * @var array<string>
21
     */
22
    protected array $allowedMimeTypes = [
23
        'image/gif',
24
        'image/x-gif',
25
        'image/agif',
26
        'image/x-png',
27
        'image/png',
28
        'image/a-png',
29
        'image/apng',
30
        'image/jpg',
31
        'image/jpe',
32
        'image/jpeg',
33
        'image/pjpeg',
34
        'image/x-jpeg',
35
    ];
36
37
    /**
38
     * @var array<string>
39
     */
40
    protected array $allowedExtensions = [
41
        'jpeg',
42
        'jpg',
43
        'png',
44
        'gif',
45
        'apng',
46
    ];
47
48
    protected int $maxSize = 10 * 1024 * 1024;
49
50
    /**
51
     * @return array<string>
52
     */
53
    private function mimes(string $which): array
54
    {
55
        return match ($which) {
56
            'png' => ['image/x-png', 'image/png', 'image/a-png', 'image/apng'],
57
            'jpg' => ['image/jpg', 'image/jpe', 'image/jpeg', 'image/pjpeg', 'image/x-jpeg'],
58
            'normalize' => ['image/gif', 'image/jpeg', 'image/png'],
59
            default => [],
60
        };
61
    }
62
63
    /**
64
     * Validate the uploaded file.
65
     *
66
     * @throws \Scrawler\Exception\FileValidationException
67
     */
68
    #[\Override]
69
    public function validate(UploadedFile|File $file): void
70
    {
71
        if (!in_array($file->getMimeType(), $this->allowedMimeTypes)) {
72
            throw new \Scrawler\Exception\FileValidationException('Invalid file type.');
73
        }
74
75
        // @codeCoverageIgnoreStart
76
        if (!in_array($file->guessExtension(), $this->allowedExtensions)) {
77
            throw new \Scrawler\Exception\FileValidationException('Invalid file extension.');
78
        }
79
        // @codeCoverageIgnoreEnd
80
81
        $this->mimeScan($file);
82
        $this->binaryScan($file);
83
    }
84
85
    #[\Override]
86
    public function getProcessedContent(UploadedFile|File $file): string
87
    {
88
        $file = $this->processImage($file);
89
90
        return $file->getContent();
91
    }
92
93
    private function mimeScan(UploadedFile|File $file): void
94
    {
95
        // Mime-type assignment
96
        if (in_array($file->getMimeType(), $this->mimes('png'))) {
97
            $this->mime = 'image/png';
98
        } elseif (in_array($file->getMimeType(), $this->mimes('jpg'))) {
99
            $this->mime = 'image/jpeg';
100
        } else {
101
            $this->mime = $file->getMimeType(); // unknown or gif.
102
        }
103
    }
104
105
    private function binaryScan(UploadedFile|File $file): void
106
    {
107
        $readfile = $file->getContent();
108
        $chunk = strtolower(bin2hex($readfile));
109
        $normalize = $this->mimes('normalize');
110
111
        // Experimental binary validation
112
        // @codeCoverageIgnoreStart
113
        switch ($this->mime) {
114
            // We allow for 16 bit padding
115
            case $normalize[0]:
116
                if (!\Safe\preg_match('/474946/msx', substr($chunk, 0, 16)) && 'image/gif' === $this->mime) {
117
                    throw new \Scrawler\Exception\FileValidationException('Invalid GIF file');
118
                }
119
                break;
120
            case $normalize[1]:
121
                if (!\Safe\preg_match('/ff(d8|d9|c0|c2|c4|da|db|dd)/msx', substr($chunk, 0, 16)) && 'image/jpeg' === $this->mime) {
122
                    throw new \Scrawler\Exception\FileValidationException(message: 'Invalid JPEG file');
123
                    // preg_match('/[{0001}-{0022}]/u', $chunk);
124
                }
125
                if (!\Safe\preg_match('/ffd9/', substr($chunk, strlen($chunk) - 32, 32)) && 'image/jpeg' === $this->mime) {
126
                    throw new \Scrawler\Exception\FileValidationException(message: 'Invalid JPEG file');
127
                }
128
129
                break;
130
            case $normalize[2]:
131
                if (!\Safe\preg_match('/504e47/', substr($chunk, 0, 16)) && 'image/png' === $this->mime) {
132
                    throw new \Scrawler\Exception\FileValidationException(message: 'Invalid PNG file');
133
                }
134
                break;
135
        }
136
        // @codeCoverageIgnoreEnd
137
    }
138
139
    private function processImage(UploadedFile|File $file): UploadedFile|File
140
    {
141
        $normalize = $this->mimes('normalize');
142
143
        [$w, $h] = \Safe\getimagesize($file->getPathname());
144
        // If thumbnail, get new size.
145
146
        $new_width = $w;
147
        $new_height = $h;
148
149
        // Check if GD is available
150
        $gdcheck = gd_info();
151
152
        // Re-sample.
153
        switch ($this->mime) {
154
            case $normalize[0]:
155
                if (true == isset($gdcheck['GIF Create Support'])) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
156
                    $image = \Safe\imagecreatefromgif($file->getPathname());
157
                    $resampled = \Safe\imagecreatetruecolor($new_width, $new_height);
158
                    \Safe\imagecopyresampled($resampled, $image, 0, 0, 0, 0, $new_width, $new_height, $w, $h);
159
                    \Safe\imagegif($resampled, $file->getPathname());
160
                    $endsize = \Safe\filesize($file->getPathname());
0 ignored issues
show
Unused Code introduced by
The assignment to $endsize is dead and can be removed.
Loading history...
161
                }
162
                break;
163
            case $normalize[1]:
164
                if (true == isset($gdcheck['JPG Support']) || true == isset($gdcheck['JPEG Support'])) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
165
                    $image = \Safe\imagecreatefromjpeg($file->getPathname());
166
                    $resampled = \Safe\imagecreatetruecolor($new_width, $new_height);
167
                    \Safe\imagecopyresampled($resampled, $image, 0, 0, 0, 0, $new_width, $new_height, $w, $h);
168
                    \Safe\imagejpeg($resampled, $file->getPathname(), 100);
169
                    $endsize = \Safe\filesize($file->getPathname());
170
                }
171
                break;
172
            case $normalize[2]:
173
                if (true == isset($gdcheck['PNG Support'])) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
174
                    $resampled = \Safe\imagecreatetruecolor($new_width, $new_height);
175
                    $image = \Safe\imagecreatefrompng($file->getPathname());
176
                    \Safe\imagealphablending($resampled, true);
177
                    \Safe\imagesavealpha($resampled, true);
178
                    \Safe\imagecopyresampled($resampled, $image, 0, 0, 0, 0, $new_width, $new_height, $w, $h);
179
                    \Safe\imagepng($resampled, $file->getPathname(), 9);
180
                    $endsize = \Safe\filesize($file->getPathname());
181
                }
182
                break;
183
        }
184
185
        return $file;
186
    }
187
}
188