PhotoUploader::checkImageType()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 1
dl 0
loc 9
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace XoopsModules\Extgallery;
4
5
/**
6
 * ExtGallery Class Manager
7
 *
8
 * You may not change or alter any portion of this comment or credits
9
 * of supporting developers from this source code or any supporting source code
10
 * which is considered copyrighted (c) material of the original comment or credit authors.
11
 * This program is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
14
 *
15
 * @copyright   {@link https://xoops.org/ XOOPS Project}
16
 * @license     GNU GPL 2 (https://www.gnu.org/licenses/old-licenses/gpl-2.0.html)
17
 * @author      Zoullou (http://www.zoullou.net)
18
 * @package     ExtGallery
19
 */
20
21
use Xmf\Request;
22
use XoopsModules\Extgallery;
23
24
/**
25
 * Class PhotoUploader
26
 * @package XoopsModules\Extgallery
27
 */
28
class PhotoUploader
29
{
30
    public $uploadDir;
31
    public $savedDestination;
32
    public $savedFilename;
33
    public $maxFileSize;
34
    public $maxWidth;
35
    public $maxHeight;
36
    public $isError;
37
    public $error;
38
    public $checkMd5;
39
40
    /**
41
     * Extgallery\PhotoUploader constructor.
42
     * @param      $uploadDir
43
     * @param int  $maxFileSize
44
     * @param null $maxWidth
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $maxWidth is correct as it would always require null to be passed?
Loading history...
45
     * @param null $maxHeight
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $maxHeight is correct as it would always require null to be passed?
Loading history...
46
     */
47
    public function __construct($uploadDir, $maxFileSize = 0, $maxWidth = null, $maxHeight = null)
48
    {
49
        $this->uploadDir   = $uploadDir;
50
        $this->maxFileSize = (int)$maxFileSize;
51
        if (isset($maxWidth)) {
52
            $this->maxWidth = (int)$maxWidth;
53
        }
54
        if (isset($maxHeight)) {
55
            $this->maxHeight = (int)$maxHeight;
56
        }
57
58
        $this->isError  = false;
59
        $this->error    = '';
60
        $this->checkMd5 = true;
61
    }
62
63
    /**
64
     * @param $file
65
     *
66
     * @return bool
67
     */
68
    public function fetchPhoto($file)
69
    {
70
        $jupart  = Request::getInt('jupart', 0, 'POST');
71
        $jufinal = Request::getInt('jufinal', 1, 'POST');
72
        $md5sums = $_POST['md5sum'][0] ?? null;
73
74
        if ('' == $this->uploadDir) {
75
            $this->abort('upload dir not defined');
76
77
            return false;
78
        }
79
80
        if (!\is_dir($this->uploadDir)) {
81
            $this->abort('fail to open upload dir');
82
83
            return false;
84
        }
85
86
        if (!\is_writable($this->uploadDir)) {
87
            $this->abort('upload dir not writable');
88
89
            return false;
90
        }
91
92
        if ($this->checkMd5 && !isset($md5sums)) {
93
            $this->abort('Expecting an MD5 checksum');
94
95
            return false;
96
        }
97
98
        $dstdir  = $this->uploadDir;
99
        $dstname = $dstdir . '/juvar.' . \session_id();
100
        $tmpname = $dstdir . '/juvar.tmp' . \session_id();
101
102
        if (!\move_uploaded_file($file['tmp_name'], $tmpname)) {
103
            $this->abort('Unable to move uploaded file');
104
105
            return false;
106
        }
107
108
        if ($jupart) {
109
            // got a chunk of a multi-part upload
110
            $len                       = \filesize($tmpname);
111
            $_SESSION['juvar.tmpsize'] += $len;
112
            if ($len > 0) {
113
                $src = \fopen($tmpname, 'rb');
114
                $dst = \fopen($dstname, (1 == $jupart) ? 'wb' : 'ab');
115
                while ($len > 0) {
116
                    $rlen = ($len > 8192) ? 8192 : $len;
117
                    $buf  = \fread($src, $rlen);
118
                    if (!$buf) {
119
                        \fclose($src);
120
                        \fclose($dst);
121
                        \unlink($dstname);
122
                        $this->abort('read IO error');
123
124
                        return false;
125
                    }
126
                    if (!\fwrite($dst, $buf, $rlen)) {
127
                        \fclose($src);
128
                        \fclose($dst);
129
                        \unlink($dstname);
130
                        $this->abort('write IO error');
131
132
                        return false;
133
                    }
134
                    $len -= $rlen;
135
                }
136
                \fclose($src);
137
                \fclose($dst);
138
                \unlink($tmpname);
139
            }
140
            if ($jufinal) {
141
                // This is the last chunk. Check total lenght and rename it to it's final name.
142
                $dlen = \filesize($dstname);
143
                if ($dlen != $_SESSION['juvar.tmpsize']) {
144
                    $this->abort('file size mismatch');
145
146
                    return false;
147
                }
148
                if ($this->checkMd5 && ($md5sums != \md5_file($dstname))) {
149
                    $this->abort('MD5 checksum mismatch');
150
151
                    return false;
152
                }
153
                // remove zero sized files
154
                if ($dlen > 0) {
155
                    if (!$this->_saveFile($dstname, $file['name'])) {
156
                        return false;
157
                    }
158
                } else {
159
                    $this->abort('0 file size');
160
161
                    return false;
162
                }
163
                // reset session var
164
                $_SESSION['juvar.tmpsize'] = 0;
165
            }
166
        } else {
167
            // Got a single file upload. Trivial.
168
            if ($this->checkMd5 && $md5sums != \md5_file($tmpname)) {
169
                $this->abort('MD5 checksum mismatch');
170
171
                return false;
172
            }
173
            if (!$this->_saveFile($tmpname, $file['name'])) {
174
                return false;
175
            }
176
        }
177
178
        return true;
179
    }
180
181
    /**
182
     * @param $tmpDestination
183
     * @param $fileName
184
     *
185
     * @return bool
186
     */
187
    public function _saveFile($tmpDestination, $fileName)
188
    {
189
        $this->savedFilename    = $fileName;
190
        $this->savedDestination = $this->uploadDir . $fileName;
191
192
        if (!$this->_checkFile($tmpDestination)) {
193
            return false;
194
        }
195
196
        if (!\rename($tmpDestination, $this->savedDestination)) {
197
            $this->abort('error renaming file');
198
199
            return false;
200
        }
201
202
        @\chmod($this->savedDestination, 0644);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for chmod(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

202
        /** @scrutinizer ignore-unhandled */ @\chmod($this->savedDestination, 0644);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
203
204
        return true;
205
    }
206
207
    /**
208
     * @param $tmpDestination
209
     *
210
     * @return bool
211
     */
212
    public function _checkFile($tmpDestination)
213
    {
214
        //  $imageExtensions = array(IMAGETYPE_GIF => 'gif', IMAGETYPE_JPEG => 'jpeg', IMAGETYPE_JPG => 'jpg', IMAGETYPE_PNG => 'png');
215
216
        $valid_types = [IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG, IMAGETYPE_BMP];
0 ignored issues
show
Unused Code introduced by
The assignment to $valid_types is dead and can be removed.
Loading history...
217
218
        $imageExtensions = ['gif', 'jpg', 'jpeg', 'png'];
219
220
        // Check IE XSS before returning success
221
        $ext       = mb_strtolower(mb_substr(mb_strrchr($this->savedDestination, '.'), 1));
222
        $photoInfo = \getimagesize($tmpDestination);
223
        if (false === $photoInfo || $imageExtensions[(int)$photoInfo[2]] != $ext) {
224
            $this->abort('Suspicious image upload refused');
225
226
            return false;
227
        }
228
229
        if (!$this->checkMaxFileSize($tmpDestination)) {
230
            $this->abort('Max file size error');
231
232
            return false;
233
        }
234
235
        if (!$this->checkMaxWidth($photoInfo)) {
236
            $this->abort('Max width error');
237
238
            return false;
239
        }
240
241
        if (!$this->checkMaxHeight($photoInfo)) {
242
            $this->abort('Max height error');
243
244
            return false;
245
        }
246
247
        if (!$this->checkImageType($photoInfo)) {
248
            $this->abort('File type not allowed');
249
250
            return false;
251
        }
252
253
        return true;
254
    }
255
256
    /**
257
     * @param $file
258
     *
259
     * @return bool
260
     */
261
    public function checkMaxFileSize($file)
262
    {
263
        if (!isset($this->maxFileSize)) {
264
            return true;
265
        }
266
267
        if (\filesize($file) > $this->maxFileSize) {
268
            return false;
269
        }
270
271
        return true;
272
    }
273
274
    /**
275
     * @param $photoInfo
276
     *
277
     * @return bool
278
     */
279
    public function checkMaxWidth($photoInfo)
280
    {
281
        if (!isset($this->maxWidth)) {
282
            return true;
283
        }
284
285
        if ($photoInfo[0] > $this->maxWidth) {
286
            return false;
287
        }
288
289
        return true;
290
    }
291
292
    /**
293
     * @param $photoInfo
294
     *
295
     * @return bool
296
     */
297
    public function checkMaxHeight($photoInfo)
298
    {
299
        if (!isset($this->maxHeight)) {
300
            return true;
301
        }
302
303
        if ($photoInfo[1] > $this->maxHeight) {
304
            return false;
305
        }
306
307
        return true;
308
    }
309
310
    /**
311
     * @param $photoInfo
312
     *
313
     * @return bool
314
     */
315
    public function checkImageType($photoInfo)
316
    {
317
        //  $allowedMimeTypes = array(IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_JPG, IMAGETYPE_PNG);
318
        $allowedMimeTypes = ['image/gif', 'image/jpg', 'image/jpeg', 'image/pjpeg', 'image/png', 'image/x-png'];
319
        if (!\in_array($photoInfo['mime'], $allowedMimeTypes)) {
320
            return false;
321
        }
322
323
        return true;
324
    }
325
326
    /**
327
     * @param string $msg
328
     */
329
    public function abort($msg = '')
330
    {
331
        // remove all uploaded files of *this* request
332
        if (isset($_FILES)) {
333
            foreach ($_FILES as $key => $val) {
334
                //@unlink($val['tmp_name']);
335
            }
336
        }
337
338
        // remove accumulated file, if any.
339
        //@unlink($this->uploadDir .'/juvar.'.session_id());
340
        //@unlink($this->uploadDir .'/juvar.tmp'.session_id());
341
342
        // reset session var
343
        $_SESSION['juvar.tmpsize'] = 0;
344
345
        $this->isError = true;
346
        $this->error   = $msg;
347
    }
348
349
    /**
350
     * @return bool
351
     */
352
    public function isError()
353
    {
354
        return $this->isError;
355
    }
356
357
    /**
358
     * @return string
359
     */
360
    public function getError()
361
    {
362
        return $this->error;
363
    }
364
365
    public function getSavedDestination()
366
    {
367
        return $this->savedDestination;
368
    }
369
370
    public function getSavedFilename()
371
    {
372
        return $this->savedFilename;
373
    }
374
}
375