Test Failed
Branch master (19f93b)
by Michael
10:44
created

XoopsMediaUploader::getSavedDestination()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
eloc 1
nc 1
nop 0
1
<?php
0 ignored issues
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 47 and the first side effect is on line 20.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
2
/**
3
 * XOOPS file uploader
4
 *
5
 * You may not change or alter any portion of this comment or credits
6
 * of supporting developers from this source code or any supporting source code
7
 * which is considered copyrighted (c) material of the original comment or credit authors.
8
 * This program is distributed in the hope that it will be useful,
9
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
11
 *
12
 * @copyright       (c) 2000-2016 XOOPS Project (www.xoops.org)
13
 * @license             GNU GPL 2 (http://www.gnu.org/licenses/gpl-2.0.html)
14
 * @package             kernel
15
 * @since               2.0.0
16
 * @author              Kazumi Ono (http://www.myweb.ne.jp/, http://jp.xoops.org/)
17
 * @author              Taiwen Jiang <[email protected]>
18
 */
19
20
defined('XOOPS_ROOT_PATH') || exit('Restricted access');
21
22
/**
23
 * Upload Media files
24
 *
25
 * Example of usage:
26
 * <code>
27
 * include_once 'uploader.php';
28
 * $allowed_mimetypes = array('image/gif', 'image/jpeg', 'image/pjpeg', 'image/x-png');
29
 * $maxfilesize = 50000;
30
 * $maxfilewidth = 120;
31
 * $maxfileheight = 120;
32
 * $uploader = new XoopsMediaUploader('/home/xoops/uploads', $allowed_mimetypes, $maxfilesize, $maxfilewidth, $maxfileheight);
33
 * if ($uploader->fetchMedia($_POST['uploade_file_name'])) {
34
 *        if (!$uploader->upload()) {
35
 *           echo $uploader->getErrors();
36
 *        } else {
37
 *           echo '<h4>File uploaded successfully!</h4>'
38
 *           echo 'Saved as: ' . $uploader->getSavedFileName() . '<br>';
39
 *           echo 'Full path: ' . $uploader->getSavedDestination();
40
 *        }
41
 * } else {
42
 *        echo $uploader->getErrors();
43
 * }
44
 * </code>
45
 *
46
 */
47
class XoopsMediaUploader
48
{
49
    /**
50
     * Flag indicating if unrecognized mimetypes should be allowed (use with precaution ! may lead to security issues )
51
     */
52
53
    public $allowUnknownTypes       = false;
54
    public $mediaName;
55
    public $mediaType;
56
    public $mediaSize;
57
    public $mediaTmpName;
58
    public $mediaError;
59
    public $mediaRealType           = '';
60
    public $uploadDir               = '';
61
    public $allowedMimeTypes        = array();
62
    public $deniedMimeTypes         = array(
63
        'application/x-httpd-php');
64
    public $maxFileSize             = 0;
65
    public $maxWidth;
66
    public $maxHeight;
67
    public $targetFileName;
68
    public $prefix;
69
    public $errors                  = array();
70
    public $savedDestination;
71
    public $savedFileName;
72
    public $extensionToMime         = array();
73
    public $checkImageType          = true;
74
    public $extensionsToBeSanitized = array(
75
        'php',
76
        'phtml',
77
        'phtm',
78
        'php3',
79
        'php4',
80
        'cgi',
81
        'pl',
82
        'asp',
83
        'php5',
84
        'php7',
85
    );
86
    // extensions needed image check (anti-IE Content-Type XSS)
87
    public $imageExtensions = array(
88
        1  => 'gif',
89
        2  => 'jpg',
90
        3  => 'png',
91
        4  => 'swf',
92
        5  => 'psd',
93
        6  => 'bmp',
94
        7  => 'tif',
95
        8  => 'tif',
96
        9  => 'jpc',
97
        10 => 'jp2',
98
        11 => 'jpx',
99
        12 => 'jb2',
100
        13 => 'swc',
101
        14 => 'iff',
102
        15 => 'wbmp',
103
        16 => 'xbm');
104
    public $randomFilename  = false;
105
106
    /**
107
     * Constructor
108
     *
109
     * @param string $uploadDir
110
     * @param array  $allowedMimeTypes
111
     * @param int    $maxFileSize
112
     * @param int    $maxWidth
113
     * @param int    $maxHeight
114
     * @param bool   $randomFilename
115
     */
116
117
    public function __construct($uploadDir, $allowedMimeTypes, $maxFileSize = 0, $maxWidth = null, $maxHeight = null, $randomFilename = false)
118
    {
119
        $this->extensionToMime = include $GLOBALS['xoops']->path('include/mimetypes.inc.php');
120
        if (!is_array($this->extensionToMime)) {
121
            $this->extensionToMime = array();
122
123
            return false;
124
        }
125
        if (is_array($allowedMimeTypes)) {
0 ignored issues
show
introduced by
The condition is_array($allowedMimeTypes) is always true.
Loading history...
126
            $this->allowedMimeTypes =& $allowedMimeTypes;
127
        }
128
        $this->uploadDir = $uploadDir;
129
130
        $maxUploadInBytes   = $this->return_bytes(ini_get('upload_max_filesize'));
131
        $maxPostInBytes     = $this->return_bytes(ini_get('post_max_size'));
132
        $memoryLimitInBytes = $this->return_bytes(ini_get('memory_limit'));
133
        if ((int)$maxFileSize > 0) {
134
            $maxFileSizeInBytes = $this->return_bytes($maxFileSize);
135
            $newMaxFileSize     = min($maxFileSizeInBytes, $maxUploadInBytes, $maxPostInBytes, $memoryLimitInBytes);
136
        } else {
137
            $newMaxFileSize = min($maxUploadInBytes, $maxPostInBytes, $memoryLimitInBytes);
138
        }
139
        $this->maxFileSize = $newMaxFileSize;
140
141
        if (isset($maxWidth)) {
142
            $this->maxWidth = (int)$maxWidth;
143
        }
144
        if (isset($maxHeight)) {
145
            $this->maxHeight = (int)$maxHeight;
146
        }
147
        if (isset($randomFilename)) {
148
            $this->randomFilename = $randomFilename;
149
        }
150
        if (!include_once $GLOBALS['xoops']->path('language/' . $GLOBALS['xoopsConfig']['language'] . '/uploader.php')) {
151
            include_once $GLOBALS['xoops']->path('language/english/uploader.php');
152
        }
153
    }
154
155
    /**
156
     * converts memory/file sizes as defined in php.ini to bytes
157
     *
158
     * @param $size_str
159
     *
160
     * @return int
161
     */
162
    public function return_bytes($size_str)
163
    {
164
        switch (substr($size_str, -1)) {
165
            case 'K':
166
            case 'k':
167
                return (int)$size_str * 1024;
168
            case 'M':
169
            case 'm':
170
                return (int)$size_str * 1048576;
171
            case 'G':
172
            case 'g':
173
                return (int)$size_str * 1073741824;
174
            default:
175
                return $size_str;
176
        }
177
    }
178
179
    /**
180
     * Fetch the uploaded file
181
     *
182
     * @param  string $media_name Name of the file field
183
     * @param  int    $index      Index of the file (if more than one uploaded under that name)
184
     * @return bool
185
     */
186
    public function fetchMedia($media_name, $index = null)
187
    {
188
        if (empty($this->extensionToMime)) {
189
            $this->setErrors(_ER_UP_MIMETYPELOAD);
190
191
            return false;
192
        }
193
        if (!isset($_FILES[$media_name])) {
194
            $this->setErrors(_ER_UP_FILENOTFOUND);
195
196
            return false;
197
        } elseif (is_array($_FILES[$media_name]['name']) && isset($index)) {
198
            $index           = (int)$index;
199
            $this->mediaName = get_magic_quotes_gpc() ? stripslashes($_FILES[$media_name]['name'][$index]) : $_FILES[$media_name]['name'][$index];
200
            if ($this->randomFilename) {
201
                $unique          = uniqid();
202
                $this->mediaName = '' . $unique . '--' . $this->mediaName;
203
            }
204
            $this->mediaType    = $_FILES[$media_name]['type'][$index];
205
            $this->mediaSize    = $_FILES[$media_name]['size'][$index];
206
            $this->mediaTmpName = $_FILES[$media_name]['tmp_name'][$index];
207
            $this->mediaError   = !empty($_FILES[$media_name]['error'][$index]) ? $_FILES[$media_name]['error'][$index] : 0;
208
        } else {
209
            $media_name      =& $_FILES[$media_name];
210
            $this->mediaName = get_magic_quotes_gpc() ? stripslashes($media_name['name']) : $media_name['name'];
211
            if ($this->randomFilename) {
212
                $unique          = uniqid();
213
                $this->mediaName = '' . $unique . '--' . $this->mediaName;
214
            }
215
            $this->mediaType    = $media_name['type'];
216
            $this->mediaSize    = $media_name['size'];
217
            $this->mediaTmpName = $media_name['tmp_name'];
218
            $this->mediaError   = !empty($media_name['error']) ? $media_name['error'] : 0;
219
        }
220
221
        if (($ext = strrpos($this->mediaName, '.')) !== false) {
222
            $ext = strtolower(substr($this->mediaName, $ext + 1));
223
            if (isset($this->extensionToMime[$ext])) {
224
                $this->mediaRealType = $this->extensionToMime[$ext];
225
            }
226
        }
227
        $this->errors = array();
228
        if ($this->mediaError > 0) {
229
            switch($this->mediaError){
230
                case UPLOAD_ERR_INI_SIZE:
231
                    $this->setErrors(_ER_UP_INISIZE);
232
                    return false;
233
                    break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
234
                case UPLOAD_ERR_FORM_SIZE:
235
                    $this->setErrors(_ER_UP_FORMSIZE);
236
                    return false;
237
                    break;
238
                case UPLOAD_ERR_PARTIAL:
239
                    $this->setErrors(_ER_UP_PARTIAL);
240
                    return false;
241
                    break;
242
                case UPLOAD_ERR_NO_FILE:
243
                    $this->setErrors(_ER_UP_NOFILE);
244
                    return false;
245
                    break;
246
                case UPLOAD_ERR_NO_TMP_DIR:
247
                    $this->setErrors(_ER_UP_NOTMPDIR);
248
                    return false;
249
                    break;
250
                case UPLOAD_ERR_CANT_WRITE:
251
                    $this->setErrors(_ER_UP_CANTWRITE);
252
                    return false;
253
                    break;
254
                case UPLOAD_ERR_EXTENSION:
255
                    $this->setErrors(_ER_UP_EXTENSION);
256
                    return false;
257
                    break;
258
                default:
259
                    $this->setErrors(_ER_UP_UNKNOWN);
260
                    return false;
261
                    break;
262
            }
263
        }
264
265
        if ((int)$this->mediaSize < 0) {
266
            $this->setErrors(_ER_UP_INVALIDFILESIZE);
267
268
            return false;
269
        }
270
        if ($this->mediaName == '') {
271
            $this->setErrors(_ER_UP_FILENAMEEMPTY);
272
273
            return false;
274
        }
275
        if ($this->mediaTmpName === 'none' || !is_uploaded_file($this->mediaTmpName)) {
276
            $this->setErrors(_ER_UP_NOFILEUPLOADED);
277
278
            return false;
279
        }
280
281
        return true;
282
    }
283
284
    /**
285
     * Set the target filename
286
     *
287
     * @param string $value
288
     */
289
    public function setTargetFileName($value)
290
    {
291
        $this->targetFileName = (string)trim($value);
292
    }
293
294
    /**
295
     * Set the prefix
296
     *
297
     * @param string $value
298
     */
299
    public function setPrefix($value)
300
    {
301
        $this->prefix = (string)trim($value);
302
    }
303
304
    /**
305
     * Get the uploaded filename
306
     *
307
     * @return string
308
     */
309
    public function getMediaName()
310
    {
311
        return $this->mediaName;
312
    }
313
314
    /**
315
     * Get the type of the uploaded file
316
     *
317
     * @return string
318
     */
319
    public function getMediaType()
320
    {
321
        return $this->mediaType;
322
    }
323
324
    /**
325
     * Get the size of the uploaded file
326
     *
327
     * @return int
328
     */
329
    public function getMediaSize()
330
    {
331
        return $this->mediaSize;
332
    }
333
334
    /**
335
     * Get the temporary name that the uploaded file was stored under
336
     *
337
     * @return string
338
     */
339
    public function getMediaTmpName()
340
    {
341
        return $this->mediaTmpName;
342
    }
343
344
    /**
345
     * Get the saved filename
346
     *
347
     * @return string
348
     */
349
    public function getSavedFileName()
350
    {
351
        return $this->savedFileName;
352
    }
353
354
    /**
355
     * Get the destination the file is saved to
356
     *
357
     * @return string
358
     */
359
    public function getSavedDestination()
360
    {
361
        return $this->savedDestination;
362
    }
363
364
    /**
365
     * Check the file and copy it to the destination
366
     *
367
     * @param  int $chmod
368
     * @return bool
369
     */
370
    public function upload($chmod = 0644)
371
    {
372
        if ($this->uploadDir == '') {
373
            $this->setErrors(_ER_UP_UPLOADDIRNOTSET);
374
375
            return false;
376
        }
377
        if (!is_dir($this->uploadDir)) {
378
            $this->setErrors(sprintf(_ER_UP_FAILEDOPENDIR, $this->uploadDir));
379
380
            return false;
381
        }
382
        if (!is_writable($this->uploadDir)) {
383
            $this->setErrors(sprintf(_ER_UP_FAILEDOPENDIRWRITE, $this->uploadDir));
384
385
            return false;
386
        }
387
        $this->sanitizeMultipleExtensions();
388
389
        if (!$this->checkMaxFileSize()) {
390
            return false;
391
        }
392
        if (!$this->checkMaxWidth()) {
393
            return false;
394
        }
395
        if (!$this->checkMaxHeight()) {
396
            return false;
397
        }
398
        if (!$this->checkMimeType()) {
399
            return false;
400
        }
401
        if (!$this->checkImageType()) {
402
            return false;
403
        }
404
        if (count($this->errors) > 0) {
405
            return false;
406
        }
407
408
        return $this->_copyFile($chmod);
409
    }
410
411
    /**
412
     * Copy the file to its destination
413
     *
414
     * @param $chmod
415
     * @return bool
416
     */
417
    public function _copyFile($chmod)
418
    {
419
        $matched = array();
420
        if (!preg_match("/\.([a-zA-Z0-9]+)$/", $this->mediaName, $matched)) {
421
            $this->setErrors(_ER_UP_INVALIDFILENAME);
422
423
            return false;
424
        }
425
        if (isset($this->targetFileName)) {
426
            $this->savedFileName = $this->targetFileName;
427
        } elseif (isset($this->prefix)) {
428
            $this->savedFileName = uniqid($this->prefix) . '.' . strtolower($matched[1]);
429
        } else {
430
            $this->savedFileName = strtolower($this->mediaName);
431
        }
432
433
        $this->savedFileName = iconv('UTF-8', 'ASCII//TRANSLIT', $this->savedFileName);
434
        $this->savedFileName = preg_replace('!\s+!', '_', $this->savedFileName);
435
        $this->savedFileName = preg_replace("/[^a-zA-Z0-9\._-]/", '', $this->savedFileName);
436
437
        $this->savedDestination = $this->uploadDir . '/' . $this->savedFileName;
438
        if (!move_uploaded_file($this->mediaTmpName, $this->savedDestination)) {
439
            $this->setErrors(sprintf(_ER_UP_FAILEDSAVEFILE, $this->savedDestination));
440
441
            return false;
442
        }
443
        // Check IE XSS before returning success
444
        $ext = strtolower(substr(strrchr($this->savedDestination, '.'), 1));
445
        if (in_array($ext, $this->imageExtensions)) {
446
            $info = @getimagesize($this->savedDestination);
447
            if ($info === false || $this->imageExtensions[(int)$info[2]] != $ext) {
448
                $this->setErrors(_ER_UP_SUSPICIOUSREFUSED);
449
                @unlink($this->savedDestination);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). 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

449
                /** @scrutinizer ignore-unhandled */ @unlink($this->savedDestination);

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...
450
451
                return false;
452
            }
453
        }
454
        @chmod($this->savedDestination, $chmod);
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

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

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...
455
456
        return true;
457
    }
458
459
    /**
460
     * Is the file the right size?
461
     *
462
     * @return bool
463
     */
464
    public function checkMaxFileSize()
465
    {
466
        if (!isset($this->maxFileSize)) {
467
            return true;
468
        }
469
        if ($this->mediaSize > $this->maxFileSize) {
470
            $this->setErrors(sprintf(_ER_UP_FILESIZETOOLARGE, $this->maxFileSize, $this->mediaSize));
471
472
            return false;
473
        }
474
475
        return true;
476
    }
477
478
    /**
479
     * Is the picture the right width?
480
     *
481
     * @return bool
482
     */
483
    public function checkMaxWidth()
484
    {
485
        if (!isset($this->maxWidth)) {
486
            return true;
487
        }
488
        if (false !== $dimension = getimagesize($this->mediaTmpName)) {
489
            if ($dimension[0] > $this->maxWidth) {
490
                $this->setErrors(sprintf(_ER_UP_FILEWIDTHTOOLARGE, $this->maxWidth, $dimension[0]));
491
492
                return false;
493
            }
494
        } else {
495
            trigger_error(sprintf(_ER_UP_FAILEDFETCHIMAGESIZE, $this->mediaTmpName), E_USER_WARNING);
496
        }
497
498
        return true;
499
    }
500
501
    /**
502
     * Is the picture the right height?
503
     *
504
     * @return bool
505
     */
506
    public function checkMaxHeight()
507
    {
508
        if (!isset($this->maxHeight)) {
509
            return true;
510
        }
511
        if (false !== $dimension = getimagesize($this->mediaTmpName)) {
512
            if ($dimension[1] > $this->maxHeight) {
513
                $this->setErrors(sprintf(_ER_UP_FILEHEIGHTTOOLARGE, $this->maxHeight, $dimension[1]));
514
515
                return false;
516
            }
517
        } else {
518
            trigger_error(sprintf(_ER_UP_FAILEDFETCHIMAGESIZE, $this->mediaTmpName), E_USER_WARNING);
519
        }
520
521
        return true;
522
    }
523
524
    /**
525
     * Check whether or not the uploaded file type is allowed
526
     *
527
     * @return bool
528
     */
529
    public function checkMimeType()
530
    {
531
        // if the browser supplied mime type looks suspicious, refuse it
532
        $structureCheck = (bool) preg_match('/^\w+\/[-+.\w]+$/', $this->mediaType);
533
        if (false === $structureCheck) {
534
            $this->mediaType = 'invalid';
535
            $this->setErrors(_ER_UP_UNKNOWNFILETYPEREJECTED);
536
            return false;
537
        }
538
539
        if (empty($this->mediaRealType) && empty($this->allowUnknownTypes)) {
540
            $this->setErrors(_ER_UP_UNKNOWNFILETYPEREJECTED);
541
542
            return false;
543
        }
544
545
        if ((!empty($this->allowedMimeTypes) && !in_array($this->mediaRealType, $this->allowedMimeTypes)) || (!empty($this->deniedMimeTypes) && in_array($this->mediaRealType, $this->deniedMimeTypes))) {
546
            $this->setErrors(sprintf(_ER_UP_MIMETYPENOTALLOWED, htmlspecialchars($this->mediaRealType, ENT_QUOTES)));
547
548
            return false;
549
        }
550
551
        return true;
552
    }
553
554
    /**
555
     * Check whether or not the uploaded image type is valid
556
     *
557
     * @return bool
558
     */
559
    public function checkImageType()
560
    {
561
        if (empty($this->checkImageType)) {
562
            return true;
563
        }
564
565
        if (('image' === substr($this->mediaType, 0, strpos($this->mediaType, '/'))) || (!empty($this->mediaRealType) && 'image' === substr($this->mediaRealType, 0, strpos($this->mediaRealType, '/')))) {
566
            if (!($info = @getimagesize($this->mediaTmpName))) {
0 ignored issues
show
Unused Code introduced by
The assignment to $info is dead and can be removed.
Loading history...
567
                $this->setErrors(_ER_UP_INVALIDIMAGEFILE);
568
569
                return false;
570
            }
571
        }
572
573
        return true;
574
    }
575
576
    /**
577
     * Sanitize executable filename with multiple extensions
578
     */
579
    public function sanitizeMultipleExtensions()
580
    {
581
        if (empty($this->extensionsToBeSanitized)) {
582
            return null;
583
        }
584
585
        $patterns = array();
586
        $replaces = array();
587
        foreach ($this->extensionsToBeSanitized as $ext) {
588
            $patterns[] = "/\." . preg_quote($ext) . "\./i";
589
            $replaces[] = '_' . $ext . '.';
590
        }
591
        $this->mediaName = preg_replace($patterns, $replaces, $this->mediaName);
592
    }
593
594
    /**
595
     * Add an error
596
     *
597
     * @param string $error
598
     */
599
    public function setErrors($error)
600
    {
601
        $this->errors[] = trim($error);
602
    }
603
604
    /**
605
     * Get generated errors
606
     *
607
     * @param  bool $ashtml Format using HTML?
608
     * @return array |string    Array of array messages OR HTML string
609
     */
610
    public function &getErrors($ashtml = true)
611
    {
612
        if (!$ashtml) {
613
            return $this->errors;
614
        } else {
615
            $ret = '';
616
            if (count($this->errors) > 0) {
617
                $ret = '<h4>' . sprintf(_ER_UP_ERRORSRETURNED, htmlspecialchars($this->mediaName, ENT_QUOTES)) . '</h4>';
618
                foreach ($this->errors as $error) {
619
                    $ret .= $error . '<br>';
620
                }
621
            }
622
623
            return $ret;
624
        }
625
    }
626
}
627