1: <?php
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 (https://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 <phppp@users.sourceforge.net>
18: */
19:
20: defined('XOOPS_ROOT_PATH') || exit('Restricted access');
21:
22: /**
23: * Upload Media files
24: *
25: * Example of usage (single file):
26: * <code>
27: * include_once __DIR__ . '/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: * $randomFilename = true;
33: * $uploader = new XoopsMediaUploader('/home/xoops/uploads', $allowed_mimetypes, $maxfilesize, $maxfilewidth, $maxfileheight, $randomFilename);
34: * if ($uploader->fetchMedia('single_file_name')) {
35: * if (!$uploader->upload()) {
36: * echo $uploader->getErrors();
37: * } else {
38: * echo '<h4>File uploaded successfully!</h4>'
39: * echo 'Saved as: ' . $uploader->getSavedFileName() . '<br>';
40: * echo 'Full path: ' . $uploader->getSavedDestination();
41: * }
42: * } else {
43: * echo $uploader->getErrors();
44: * }
45: * </code>
46: *
47: * Example of usage (multiple file):
48: * <code>
49: * include_once __DIR__ . '/uploader.php';
50: * $allowed_mimetypes = array('image/gif', 'image/jpeg', 'image/pjpeg', 'image/x-png', 'image/webp');
51: * $maxfilesize = 50000;
52: * $maxfilewidth = 120;
53: * $maxfileheight = 120;
54: * $randomFilename = true;
55: * $uploader = new XoopsMediaUploader('/home/xoops/uploads', $allowed_mimetypes, $maxfilesize, $maxfilewidth, $maxfileheight, $randomFilename);
56: * for ($i = 0; $i < $uploader->countMedia('multiple_file_name'); $i++) {
57: * if ($uploader->fetchMedia('multiple_file_name')) {
58: * if (!$uploader->upload()) {
59: * echo $uploader->getErrors();
60: * } else {
61: * echo '<h4>File uploaded successfully!</h4>'
62: * echo 'Saved as: ' . $uploader->getSavedFileName() . '<br>';
63: * echo 'Full path: ' . $uploader->getSavedDestination();
64: * }
65: * } else {
66: * echo $uploader->getErrors();
67: * }
68: * }
69: * </code>
70: *
71: */
72: class XoopsMediaUploader
73: {
74: /**
75: * Flag indicating if unrecognized mimetypes should be allowed (use with precaution ! may lead to security issues )
76: */
77:
78: public $allowUnknownTypes = false;
79: public $mediaName;
80: public $mediaType;
81: public $mediaSize;
82: public $mediaTmpName;
83: public $mediaError;
84: public $mediaRealType = '';
85: public $uploadDir = '';
86: public $allowedMimeTypes = array();
87: public $deniedMimeTypes = array(
88: 'application/x-httpd-php');
89: public $maxFileSize = 0;
90: public $maxWidth;
91: public $maxHeight;
92: public $targetFileName;
93: public $prefix;
94: public $errors = array();
95: public $savedDestination;
96: public $savedFileName;
97: public $extensionToMime = array();
98: public $checkImageType = true;
99: public $extensionsToBeSanitized = array(
100: 'php',
101: 'phtml',
102: 'phtm',
103: 'php3',
104: 'php4',
105: 'cgi',
106: 'pl',
107: 'asp',
108: 'php5',
109: 'php7',
110: );
111: // extensions needed image check (anti-IE Content-Type XSS)
112: public $imageExtensions = array(
113: 1 => 'gif',
114: 2 => 'jpg',
115: 3 => 'png',
116: 4 => 'swf',
117: 5 => 'psd',
118: 6 => 'bmp',
119: 7 => 'tif',
120: 8 => 'tif',
121: 9 => 'jpc',
122: 10 => 'jp2',
123: 11 => 'jpx',
124: 12 => 'jb2',
125: 13 => 'swc',
126: 14 => 'iff',
127: 15 => 'wbmp',
128: 16 => 'xbm',
129: 17 => 'webp');
130: public $randomFilename = false;
131:
132: /**
133: * Constructor
134: *
135: * @param string $uploadDir
136: * @param array $allowedMimeTypes
137: * @param int $maxFileSize
138: * @param int $maxWidth
139: * @param int $maxHeight
140: * @param bool $randomFilename
141: */
142:
143: public function __construct($uploadDir, $allowedMimeTypes, $maxFileSize = 0, $maxWidth = null, $maxHeight = null, $randomFilename = false)
144: {
145: $this->extensionToMime = include $GLOBALS['xoops']->path('include/mimetypes.inc.php');
146: if (!is_array($this->extensionToMime)) {
147: $this->extensionToMime = array();
148:
149: return false;
150: }
151: if (is_array($allowedMimeTypes)) {
152: $this->allowedMimeTypes =& $allowedMimeTypes;
153: }
154: $this->uploadDir = $uploadDir;
155:
156: $limits = array();
157: $limits = $this->arrayPushIfPositive($limits, $maxFileSize);
158: $limits = $this->arrayPushIfPositive($limits, $this->return_bytes(ini_get('upload_max_filesize')));
159: $limits = $this->arrayPushIfPositive($limits, $this->return_bytes(ini_get('post_max_size')));
160: $limits = $this->arrayPushIfPositive($limits, $this->return_bytes(ini_get('memory_limit')));
161: $this->maxFileSize = min($limits);
162:
163: if (isset($maxWidth)) {
164: $this->maxWidth = (int)$maxWidth;
165: }
166: if (isset($maxHeight)) {
167: $this->maxHeight = (int)$maxHeight;
168: }
169: if (isset($randomFilename)) {
170: $this->randomFilename = $randomFilename;
171: }
172: if (!include_once $GLOBALS['xoops']->path('language/' . $GLOBALS['xoopsConfig']['language'] . '/uploader.php')) {
173: include_once $GLOBALS['xoops']->path('language/english/uploader.php');
174: }
175: }
176:
177: /**
178: * converts memory/file sizes as defined in php.ini to bytes
179: *
180: * @param $size_str
181: *
182: * @return int
183: */
184: public function return_bytes($size_str)
185: {
186: switch (substr($size_str, -1)) {
187: case 'K':
188: case 'k':
189: return (int)$size_str * 1024;
190: case 'M':
191: case 'm':
192: return (int)$size_str * 1048576;
193: case 'G':
194: case 'g':
195: return (int)$size_str * 1073741824;
196: default:
197: return $size_str;
198: }
199: }
200:
201: /**
202: * Count the uploaded files (in case of miltiple upload)
203: *
204: * @param string $media_name Name of the file field
205: * @return int|false
206: */
207: public function countMedia($media_name) {
208: if (!isset($_FILES[$media_name])) {
209: $this->setErrors(_ER_UP_FILENOTFOUND);
210: return false;
211: }
212: return count($_FILES[$media_name]['name']);
213: }
214:
215: /**
216: * Fetch the uploaded file
217: *
218: * @param string $media_name Name of the file field
219: * @param int $index Index of the file (if more than one uploaded under that name)
220: * @return bool
221: */
222: public function fetchMedia($media_name, $index = null)
223: {
224: if (empty($this->extensionToMime)) {
225: $this->setErrors(_ER_UP_MIMETYPELOAD);
226:
227: return false;
228: }
229: if (!isset($_FILES[$media_name])) {
230: $this->setErrors(_ER_UP_FILENOTFOUND);
231:
232: return false;
233: } elseif (is_array($_FILES[$media_name]['name']) && isset($index)) {
234: $index = (int)$index;
235: $this->mediaName = @get_magic_quotes_gpc() ? stripslashes($_FILES[$media_name]['name'][$index]) : $_FILES[$media_name]['name'][$index];
236: if ($this->randomFilename) {
237: $unique = uniqid('', true);
238: $this->targetFileName = '' . $unique . '--' . $this->mediaName;
239: }
240: $this->mediaType = $_FILES[$media_name]['type'][$index];
241: $this->mediaSize = $_FILES[$media_name]['size'][$index];
242: $this->mediaTmpName = $_FILES[$media_name]['tmp_name'][$index];
243: $this->mediaError = !empty($_FILES[$media_name]['error'][$index]) ? $_FILES[$media_name]['error'][$index] : 0;
244: } elseif (is_array($_FILES[$media_name]['name']) && !isset($index)) {
245: $this->setErrors(_ER_UP_INDEXNOTSET);
246:
247: return false;
248: } else {
249: $media_name =& $_FILES[$media_name];
250: $this->mediaName = @get_magic_quotes_gpc() ? stripslashes($media_name['name']) : $media_name['name'];
251: if ($this->randomFilename) {
252: $unique = uniqid('', true);
253: $this->targetFileName = '' . $unique . '--' . $this->mediaName;
254: }
255: $this->mediaType = $media_name['type'];
256: $this->mediaSize = $media_name['size'];
257: $this->mediaTmpName = $media_name['tmp_name'];
258: $this->mediaError = !empty($media_name['error']) ? $media_name['error'] : 0;
259: }
260:
261: if (($ext = strrpos($this->mediaName, '.')) !== false) {
262: $ext = strtolower(substr($this->mediaName, $ext + 1));
263: if (isset($this->extensionToMime[$ext])) {
264: $this->mediaRealType = $this->extensionToMime[$ext];
265: }
266: }
267: $this->errors = array();
268: if ($this->mediaError > 0) {
269: switch($this->mediaError){
270: case UPLOAD_ERR_INI_SIZE:
271: $this->setErrors(_ER_UP_INISIZE);
272: return false;
273: break;
274: case UPLOAD_ERR_FORM_SIZE:
275: $this->setErrors(_ER_UP_FORMSIZE);
276: return false;
277: break;
278: case UPLOAD_ERR_PARTIAL:
279: $this->setErrors(_ER_UP_PARTIAL);
280: return false;
281: break;
282: case UPLOAD_ERR_NO_FILE:
283: $this->setErrors(_ER_UP_NOFILE);
284: return false;
285: break;
286: case UPLOAD_ERR_NO_TMP_DIR:
287: $this->setErrors(_ER_UP_NOTMPDIR);
288: return false;
289: break;
290: case UPLOAD_ERR_CANT_WRITE:
291: $this->setErrors(_ER_UP_CANTWRITE);
292: return false;
293: break;
294: case UPLOAD_ERR_EXTENSION:
295: $this->setErrors(_ER_UP_EXTENSION);
296: return false;
297: break;
298: default:
299: $this->setErrors(_ER_UP_UNKNOWN);
300: return false;
301: break;
302: }
303: }
304:
305: if ((int)$this->mediaSize < 0) {
306: $this->setErrors(_ER_UP_INVALIDFILESIZE);
307:
308: return false;
309: }
310: if ($this->mediaName == '') {
311: $this->setErrors(_ER_UP_FILENAMEEMPTY);
312:
313: return false;
314: }
315: if ($this->mediaTmpName === 'none' || !is_uploaded_file($this->mediaTmpName)) {
316: $this->setErrors(_ER_UP_NOFILEUPLOADED);
317:
318: return false;
319: }
320:
321: return true;
322: }
323:
324: /**
325: * Set the target filename
326: *
327: * @param string $value
328: */
329: public function setTargetFileName($value)
330: {
331: $this->targetFileName = (string)trim($value);
332: }
333:
334: /**
335: * Set the prefix
336: *
337: * @param string $value
338: */
339: public function setPrefix($value)
340: {
341: $this->prefix = (string)trim($value);
342: }
343:
344: /**
345: * Get the uploaded filename
346: *
347: * @return string
348: */
349: public function getMediaName()
350: {
351: return $this->mediaName;
352: }
353:
354: /**
355: * Get the type of the uploaded file
356: *
357: * @return string
358: */
359: public function getMediaType()
360: {
361: return $this->mediaType;
362: }
363:
364: /**
365: * Get the size of the uploaded file
366: *
367: * @return int
368: */
369: public function getMediaSize()
370: {
371: return $this->mediaSize;
372: }
373:
374: /**
375: * Get the temporary name that the uploaded file was stored under
376: *
377: * @return string
378: */
379: public function getMediaTmpName()
380: {
381: return $this->mediaTmpName;
382: }
383:
384: /**
385: * Get the saved filename
386: *
387: * @return string
388: */
389: public function getSavedFileName()
390: {
391: return $this->savedFileName;
392: }
393:
394: /**
395: * Get the destination the file is saved to
396: *
397: * @return string
398: */
399: public function getSavedDestination()
400: {
401: return $this->savedDestination;
402: }
403:
404: /**
405: * Check the file and copy it to the destination
406: *
407: * @param int $chmod
408: * @return bool
409: */
410: public function upload($chmod = 0644)
411: {
412: if ($this->uploadDir == '') {
413: $this->setErrors(_ER_UP_UPLOADDIRNOTSET);
414:
415: return false;
416: }
417: if (!is_dir($this->uploadDir)) {
418: $this->setErrors(sprintf(_ER_UP_FAILEDOPENDIR, $this->uploadDir));
419:
420: return false;
421: }
422: if (!is_writable($this->uploadDir)) {
423: $this->setErrors(sprintf(_ER_UP_FAILEDOPENDIRWRITE, $this->uploadDir));
424:
425: return false;
426: }
427: $this->sanitizeMultipleExtensions();
428:
429: if (!$this->checkMaxFileSize()) {
430: return false;
431: }
432: if (!$this->checkMaxWidth()) {
433: return false;
434: }
435: if (!$this->checkMaxHeight()) {
436: return false;
437: }
438: if (!$this->checkMimeType()) {
439: return false;
440: }
441: if (!$this->checkImageType()) {
442: return false;
443: }
444: if (count($this->errors) > 0) {
445: return false;
446: }
447:
448: return $this->_copyFile($chmod);
449: }
450:
451: /**
452: * Copy the file to its destination
453: *
454: * @param $chmod
455: * @return bool
456: */
457: public function _copyFile($chmod)
458: {
459: $matched = array();
460: if (!preg_match("/\.([a-zA-Z0-9]+)$/", $this->mediaName, $matched)) {
461: $this->setErrors(_ER_UP_INVALIDFILENAME);
462:
463: return false;
464: }
465: if (isset($this->targetFileName)) {
466: $this->savedFileName = $this->targetFileName;
467: } elseif (isset($this->prefix)) {
468: $this->savedFileName = uniqid($this->prefix, true) . '.' . strtolower($matched[1]);
469: } else {
470: $this->savedFileName = strtolower($this->mediaName);
471: }
472:
473: $this->savedFileName = iconv('UTF-8', 'ASCII//TRANSLIT', $this->savedFileName);
474: $this->savedFileName = preg_replace('!\s+!', '_', $this->savedFileName);
475: $this->savedFileName = preg_replace("/[^a-zA-Z0-9\._-]/", '', $this->savedFileName);
476:
477: $this->savedDestination = $this->uploadDir . '/' . $this->savedFileName;
478: if (!move_uploaded_file($this->mediaTmpName, $this->savedDestination)) {
479: $this->setErrors(sprintf(_ER_UP_FAILEDSAVEFILE, $this->savedDestination));
480:
481: return false;
482: }
483: // Check IE XSS before returning success
484: $ext = strtolower(substr(strrchr($this->savedDestination, '.'), 1));
485: if (in_array($ext, $this->imageExtensions)) {
486: $info = @getimagesize($this->savedDestination);
487: if ($info === false || $this->imageExtensions[(int)$info[2]] != $ext) {
488: $this->setErrors(_ER_UP_SUSPICIOUSREFUSED);
489: @unlink($this->savedDestination);
490:
491: return false;
492: }
493: }
494: @chmod($this->savedDestination, $chmod);
495:
496: return true;
497: }
498:
499: /**
500: * Is the file the right size?
501: *
502: * @return bool
503: */
504: public function checkMaxFileSize()
505: {
506: if (!isset($this->maxFileSize)) {
507: return true;
508: }
509: if ($this->mediaSize > $this->maxFileSize) {
510: $this->setErrors(sprintf(_ER_UP_FILESIZETOOLARGE, $this->maxFileSize, $this->mediaSize));
511:
512: return false;
513: }
514:
515: return true;
516: }
517:
518: /**
519: * Is the picture the right width?
520: *
521: * @return bool
522: */
523: public function checkMaxWidth()
524: {
525: if (!isset($this->maxWidth)) {
526: return true;
527: }
528: if (false !== $dimension = getimagesize($this->mediaTmpName)) {
529: if ($dimension[0] > $this->maxWidth) {
530: $this->setErrors(sprintf(_ER_UP_FILEWIDTHTOOLARGE, $this->maxWidth, $dimension[0]));
531:
532: return false;
533: }
534: } else {
535: trigger_error(sprintf(_ER_UP_FAILEDFETCHIMAGESIZE, $this->mediaTmpName), E_USER_WARNING);
536: }
537:
538: return true;
539: }
540:
541: /**
542: * Is the picture the right height?
543: *
544: * @return bool
545: */
546: public function checkMaxHeight()
547: {
548: if (!isset($this->maxHeight)) {
549: return true;
550: }
551: if (false !== $dimension = getimagesize($this->mediaTmpName)) {
552: if ($dimension[1] > $this->maxHeight) {
553: $this->setErrors(sprintf(_ER_UP_FILEHEIGHTTOOLARGE, $this->maxHeight, $dimension[1]));
554:
555: return false;
556: }
557: } else {
558: trigger_error(sprintf(_ER_UP_FAILEDFETCHIMAGESIZE, $this->mediaTmpName), E_USER_WARNING);
559: }
560:
561: return true;
562: }
563:
564: /**
565: * Check whether or not the uploaded file type is allowed
566: *
567: * @return bool
568: */
569: public function checkMimeType()
570: {
571: // if the browser supplied mime type looks suspicious, refuse it
572: $structureCheck = (bool) preg_match('/^\w+\/[-+.\w]+$/', $this->mediaType);
573: if (false === $structureCheck) {
574: $this->mediaType = 'invalid';
575: $this->setErrors(_ER_UP_UNKNOWNFILETYPEREJECTED);
576: return false;
577: }
578:
579: if (empty($this->mediaRealType) && empty($this->allowUnknownTypes)) {
580: $this->setErrors(_ER_UP_UNKNOWNFILETYPEREJECTED);
581:
582: return false;
583: }
584:
585: if ((!empty($this->allowedMimeTypes) && !in_array($this->mediaRealType, $this->allowedMimeTypes)) || (!empty($this->deniedMimeTypes) && in_array($this->mediaRealType, $this->deniedMimeTypes))) {
586: $this->setErrors(sprintf(_ER_UP_MIMETYPENOTALLOWED, htmlspecialchars($this->mediaRealType, ENT_QUOTES)));
587:
588: return false;
589: }
590:
591: return true;
592: }
593:
594: /**
595: * Check whether or not the uploaded image type is valid
596: *
597: * @return bool
598: */
599: public function checkImageType()
600: {
601: if (empty($this->checkImageType)) {
602: return true;
603: }
604:
605: if (('image' === substr($this->mediaType, 0, strpos($this->mediaType, '/'))) || (!empty($this->mediaRealType) && 'image' === substr($this->mediaRealType, 0, strpos($this->mediaRealType, '/')))) {
606: if (!($info = @getimagesize($this->mediaTmpName))) {
607: $this->setErrors(_ER_UP_INVALIDIMAGEFILE);
608:
609: return false;
610: }
611: }
612:
613: return true;
614: }
615:
616: /**
617: * Sanitize executable filename with multiple extensions
618: */
619: public function sanitizeMultipleExtensions()
620: {
621: if (empty($this->extensionsToBeSanitized)) {
622: return null;
623: }
624:
625: $patterns = array();
626: $replaces = array();
627: foreach ($this->extensionsToBeSanitized as $ext) {
628: $patterns[] = "/\." . preg_quote($ext, '/') . "\./i";
629: $replaces[] = '_' . $ext . '.';
630: }
631: $this->mediaName = preg_replace($patterns, $replaces, $this->mediaName);
632: }
633:
634: /**
635: * Add an error
636: *
637: * @param string $error
638: */
639: public function setErrors($error)
640: {
641: $this->errors[] = trim($error);
642: }
643:
644: /**
645: * Get generated errors
646: *
647: * @param bool $ashtml Format using HTML?
648: * @return array |string Array of array messages OR HTML string
649: */
650: public function &getErrors($ashtml = true)
651: {
652: if (!$ashtml) {
653: return $this->errors;
654: } else {
655: $ret = '';
656: if (count($this->errors) > 0) {
657: $ret = '<h4>' . sprintf(_ER_UP_ERRORSRETURNED, htmlspecialchars($this->mediaName, ENT_QUOTES)) . '</h4>';
658: foreach ($this->errors as $error) {
659: $ret .= $error . '<br>';
660: }
661: }
662:
663: return $ret;
664: }
665: }
666:
667: /**
668: * Push value onto set.
669: * Used in max file size calculation to eliminate -1 (unlimited) ini values
670: *
671: * @param array $set array of values
672: * @param int $value value to push
673: *
674: * @return mixed
675: */
676: protected function arrayPushIfPositive($set, $value) {
677: if ($value > 0) {
678: array_push($set, $value);
679: }
680: return $set;
681: }
682: }
683: