Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 479
0.00% covered (danger)
0.00%
0 / 21
CRAP
0.00% covered (danger)
0.00%
0 / 1
Upload
0.00% covered (danger)
0.00%
0 / 479
0.00% covered (danger)
0.00%
0 / 21
26082
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 __destruct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 __
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 clear
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
2
 clearUploadedAtTemp
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
42
 getUploadedData
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
210
 moveUploadedFiles
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
132
 renameDuplicateFile
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
30
 securityScan
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
90
 setErrorMessage
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
42
 setInputFileName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setNewFileName
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
72
 setNewFileNameToRandom
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setupFileExtensionsMimeTypesForValidation
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
42
 setWebSafeFileName
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 testGetUploadedMimetype
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
132
 upload
0.00% covered (danger)
0.00%
0 / 42
0.00% covered (danger)
0.00%
0 / 1
342
 uploadSingleFile
0.00% covered (danger)
0.00%
0 / 123
0.00% covered (danger)
0.00%
0 / 1
600
 validateExtensionAndMimeType
0.00% covered (danger)
0.00%
0 / 67
0.00% covered (danger)
0.00%
0 / 1
182
 validateFileSize
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
42
 validateOptionsProperties
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
272
1<?php
2/**
3 * Rundiz Upload component.
4 *
5 * @author Vee W.
6 * @license http://opensource.org/licenses/MIT MIT
7 */
8
9
10namespace Rundiz\Upload;
11
12/**
13 * PHP upload class that is able to validate requirements and limitations, real file's mime type check, detect the errors and report.
14 *
15 * @package Upload
16 * @version 2.0.2
17 * @author Vee W.
18 */
19class Upload
20{
21
22
23    /**
24     * @var array Allowed file extensions. Example: array('jpg', 'gif', 'png').
25     */
26    public $allowed_file_extensions;
27    /**
28     * @var array The array set of file extensions and its valid mime types for check when process the uploaded files.<br>
29     * Example:
30     * <pre>
31     * array(
32     *     'jpg' => array('image/jpeg', 'image/pjpeg'),
33     *     'txt' => array('text/plain'),
34     * );
35     * </pre>
36     * If you don't want to validate mime type, set this property to an empty array. Example: $Upload->file_extensions_mime_types = array();
37     */
38    public $file_extensions_mime_types;
39    /**
40     * @var integer Set max file size to upload. This file size unit is in bytes only.
41     */
42    public $max_file_size;
43    /**
44     * @var string Set new file name, just set the file name only. No extension.<br>
45     * Important! This property is not recommend to set it if you upload multiple files with same input file name. It is recommended to leave this as null and set overwrite property to true or false.<br>
46     * If you want to set the name while upload multiple files, it is recommended that you set overwrite property to false.
47     */
48    public $new_file_name;
49    /**
50     * @var boolean To overwrite the uploaded file set it to true, otherwise set it to false.
51     */
52    public $overwrite = false;
53    /**
54     * @var boolean To rename uploaded file name to safe for web set it to true, otherwise set it to false.<br>
55     * The safe for web file name is English and number chacters, no space (replaced with dash), no special characters, allowed dash and underscore.
56     */
57    public $web_safe_file_name = true;
58    /**
59     * @var boolean Set to true to enable security scan such as php open tag (<?php). Set to false to not scan. This is optional security.
60     */
61    public $security_scan = false;
62    /**
63     * @var boolean If you upload multiple files and there is at least one file that did not pass the validation, do you want it to stop?<br>
64     * Set to true to stop and delete all uploaded files (all uploaded files must pass validation).<br>
65     * Set to false to skip the error files (failed validation files are report as error, success validation files continue the process).
66     */
67    public $stop_on_failed_upload_multiple = true;
68    /**
69     * @var string Path to store files that was uploaded to move to. Do not end with trailing slash.
70     */
71    public $move_uploaded_to = '.';
72
73    /**
74     * Contain error codes.
75     *
76     * The array format will be:
77     * <pre>
78     * array(
79     *     index => array(// the `index` will be the same as it is in error_messages property.
80     *         'code' => 'RDU_1',// this will be error code, start with RDU_ and follow with number or short error message without space. it is easy for check and replace with your translation.
81     *         'errorAttributes' => 'string',// this key contain error attributes as string
82     *             // for example: 9MB > 2MB in case that limit file size to 2MB but uploaded 9MB, or showing file name that have problem.
83     *             // this key may contain empty string so check it before use.
84     *         'errorFileName' => 'filename.ext',// the file name with extension that cause error message.
85     *             // this key may contain empty string.
86     *         'errorFileSize' => '12345',// the file size in bytes.
87     *             // this key may contain empty string.
88     *         'errorFileMime' => 'mime/type',// the file mime type.
89     *             // this key may contain empty string.
90     *     )
91     * )
92     * </pre>
93     *
94     * The error codes and description.
95     *
96     * RDU_MOVE_UPLOADED_FAILED = Failed to move uploaded file.<br>
97     * RDU_SEC_ERR_PHP = Security error! Found PHP embedded in the uploaded file.<br>
98     * RDU_SEC_ERR_CGI = Security error! Found CGI/Pearl embedded in the uploaded file.<br>
99     * RDU_MOVE_UPLOADED_TO_NOT_DIR = The target upload location is not folder.<br>
100     * RDU_MOVE_UPLOADED_TO_NOT_WRITABLE = The target upload location is not writable. Please check folder permission.<br>
101     * RDU_UNABLE_VALIDATE_EXT = Unable to validate file extension.<br>
102     * RDU_NOT_ALLOW_EXT = The uploaded file is not in allowed extensions.<br>
103     * RDU_UNABLE_VALIDATE_EXT_AND_MIME = Unable to validate file extension and mime type.<br>
104     * RDU_INVALID_MIME = The uploaded file has invalid mime type.<br>
105     * RDU_UNABLE_VALIDATE_MIME = Unable to validate mime type.
106     *
107     * For the RDU_1 to RDU_8 use PHP upload errors. ( http://php.net/manual/en/features.file-upload.errors.php ).<br>
108     * RDU_1 = The uploaded file exceeds the max file size directive.<br>
109     * RDU_2 = The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.<br>
110     * RDU_3 = The uploaded file was only partially uploaded.<br>
111     * RDU_4 = No file was uploaded.<br>
112     * RDU_6 = Missing a temporary folder.<br>
113     * RDU_7 = Failed to write file to disk.<br>
114     * RDU_8 = A PHP extension stopped the file upload.<br>
115     *
116     * @since 2.0.1
117     * @var array If there is at least one error, it will be set to here.
118     */
119    public $error_codes = array();
120
121    /**
122     * @var array If there is at least one error message it will be set to here.
123     */
124    public $error_messages = array();
125
126    /**
127     * @var string The input file name. ($_FILES['input_file_name']).
128     */
129    protected $input_file_name;
130    /**
131     * @var array Contain all values from $_FILES['input_file_name'] for works with upload process. This will be very useful when upload multiple files.<br>
132     * Example:<br>
133     * <code>$Upload->files['input_file_name'];</code> is same as <code>$_FILES['input_file_name']</code>
134     */
135    protected $files = array();
136    /**
137     * @var array The queue for move uploaded file(s). This is very useful when upload multiple files.
138     */
139    protected $move_uploaded_queue = array();
140
141
142    /**
143     * Begins upload class.
144     *
145     * @param string $input_file_name The name of input file.
146     */
147    public function __construct($input_file_name)
148    {
149        $this->clear();
150        $this->setInputFileName($input_file_name);
151    }// __construct
152
153
154    /**
155     * Class destructor. Works at end of class (unset class's variable).
156     */
157    public function __destruct()
158    {
159        $this->clear();
160    }// __destruct
161
162
163    /**
164     * Placeholder method for language editor program like Poedit to lookup for the words that is using this method.<br>
165     * This method does nothing but for who use program like Poedit to search/lookup the words that is using this method to create translation.<br>
166     * Example:<br>
167     * There is this code in generator class. <code>static::__('Hello');</code><br>
168     * Use Poedit to search for __ function to update/retreive the source text and translate it.
169     *
170     * @param string $string The message to use.
171     * @return string Return the same string.
172     */
173    protected static function __($string)
174    {
175        return $string;
176    }// __
177
178
179    /**
180     * Clear all properties to its default values.
181     */
182    public function clear()
183    {
184        $this->allowed_file_extensions = null;
185        $this->error_messages = array();
186        $this->file_extensions_mime_types = null;
187        $this->files = array();
188        $this->input_file_name = null;
189        $this->max_file_size = null;
190        $this->move_uploaded_queue = array();
191        $this->move_uploaded_to = '.';
192        $this->new_file_name = null;
193        $this->overwrite = false;
194        $this->web_safe_file_name = true;
195        $this->security_scan = false;
196        $this->stop_on_failed_upload_multiple = true;
197    }// clear
198
199
200    /**
201     * Clear uploaded files at temp folder. (if it is able to write/delete).
202     */
203    protected function clearUploadedAtTemp()
204    {
205        foreach ($this->move_uploaded_queue as $key => $queue_item) {
206            if (is_array($queue_item) && isset($queue_item['tmp_name'])) {
207                if (is_file($queue_item['tmp_name']) && is_writable($queue_item['tmp_name'])) {
208                    unlink($queue_item['tmp_name']);
209                }
210            }
211        }// endforeach;
212        unset($key, $queue_item);
213        $this->move_uploaded_queue = array();
214    }// clearUploadedAtTemp
215
216
217    /**
218     * Get the uploaded data.
219     *
220     * @return array Return array set of successful uploaded files and its data.<br>
221     * Example:
222     * <pre>
223     * $output = array(
224     *     'input_file_name_key' => array(
225     *         'name' => 'file_name_where_user_selected_in_the_upload_form.ext',
226     *         'extension' => 'ext',
227     *         'size' => 'file size in bytes.',
228     *         'new_name' => 'new_file_name_that_was_set_while_upload_process.ext',
229     *         'full_path_new_name' => '/full/move_uploaded_path/to/new_file_name_that_was_set_while_upload_process.ext',
230     *         'mime' => 'The real file mime type',
231     *         'md5_file' => 'The md5 file value.',
232     *     ),
233     *     'other_input_file_name_key' => array(
234     *         'name' => '...',
235     *         'extension' => '...',
236     *         'size' => '...',
237     *         'new_name' => '...',
238     *         'full_path_new_name' => '...',
239     *         'mime' => '...',
240     *         'md5_file' => '...',
241     *     ),
242     * );
243     * </pre>
244     * If failed to upload, it will return empty array.
245     */
246    public function getUploadedData()
247    {
248        if (empty($this->move_uploaded_queue) || !is_array($this->move_uploaded_queue)) {
249            return array();
250        }
251
252        $output = array();
253
254        foreach ($this->move_uploaded_queue as $key => $queue_item) {
255            if (
256                is_array($queue_item) &&
257                array_key_exists('name', $queue_item) &&
258                array_key_exists('tmp_name', $queue_item) &&
259                array_key_exists('new_name', $queue_item) &&
260                array_key_exists('move_uploaded_to', $queue_item) &&
261                array_key_exists('move_uploaded_status', $queue_item) &&
262                $queue_item['move_uploaded_status'] === 'success'
263            ) {
264                // get file extension only
265                $file_name_explode = explode('.', $queue_item['name']);
266                $file_extension = (isset($file_name_explode[count($file_name_explode)-1]) ? $file_name_explode[count($file_name_explode)-1] : null);
267                unset($file_name_explode);
268
269                // get file info
270                $Finfo = new \finfo();
271                $mime = $Finfo->file($queue_item['move_uploaded_to'], FILEINFO_MIME_TYPE);
272                unset($Finfo);
273
274                $output[$key] = array();
275                $output[$key]['name'] = $queue_item['name'];
276                $output[$key]['extension'] = $file_extension;
277                $output[$key]['size'] = (is_file($queue_item['move_uploaded_to']) ? filesize($queue_item['move_uploaded_to']) : 0);
278                $output[$key]['new_name'] = $queue_item['new_name'];
279                $output[$key]['full_path_new_name'] = $queue_item['move_uploaded_to'];
280                $output[$key]['mime'] = $mime;
281                $output[$key]['md5_file'] = (is_file($queue_item['move_uploaded_to']) ? md5_file($queue_item['move_uploaded_to']) : null);
282
283                unset($file_extension, $mime);
284            }
285        }
286
287        return $output;
288    }// getUploadedData
289
290
291    /**
292     * Move the uploaded file(s).
293     *
294     * @return boolean Return true on success, false on failure.
295     */
296    protected function moveUploadedFiles()
297    {
298        $i = 0;
299        if (is_array($this->move_uploaded_queue)) {
300            foreach ($this->move_uploaded_queue as $key => $queue_item) {
301                if (is_array($queue_item) && isset($queue_item['name']) && isset($queue_item['tmp_name']) && isset($queue_item['new_name'])) {
302                    $destination_name = $queue_item['new_name'];
303
304                    if ($this->overwrite === false) {
305                        // verify file exists and set new name.
306                        $destination_name = $this->renameDuplicateFile($destination_name);
307                    }
308
309                    $move_result = move_uploaded_file($queue_item['tmp_name'], $this->move_uploaded_to.DIRECTORY_SEPARATOR.$destination_name);
310                    if ($move_result === true) {
311                        // move uploaded file success. add status and some data to array.
312                        $this->move_uploaded_queue[$key] = array_merge(
313                            $this->move_uploaded_queue[$key],
314                            array(
315                                'new_name' => $destination_name,
316                                'move_uploaded_status' => 'success',
317                                'move_uploaded_to' => $this->move_uploaded_to.DIRECTORY_SEPARATOR.$destination_name,
318                            )
319                        );
320                        $i++;
321                    } else {
322                        $this->setErrorMessage(
323                            sprintf(static::__('Unable to move uploaded file. (%s =&gt; %s)'), $queue_item['name'], $this->move_uploaded_to . DIRECTORY_SEPARATOR . $destination_name),
324                            'RDU_MOVE_UPLOADED_FAILED',
325                            $queue_item['name'] . '=&gt; ' . $this->move_uploaded_to . DIRECTORY_SEPARATOR . $destination_name
326                        );
327                    }
328
329                    unset($destination_name, $move_result);
330                }
331            }// endforeach;
332            unset($key, $queue_item);
333        }
334
335        if ($i == count($this->move_uploaded_queue) && $i > 0) {
336            return true;
337        } else {
338            return false;
339        }
340    }// moveUploadedFiles
341
342
343    /**
344     * Rename the file where it is duplicate with existing file.
345     *
346     * @param string $file_name File name to check
347     * @return string Return renamed file that will not duplicate the existing file.
348     */
349    protected function renameDuplicateFile($file_name, $loop_count = 1)
350    {
351        if (!file_exists($this->move_uploaded_to.DIRECTORY_SEPARATOR.$file_name)) {
352            return $file_name;
353        } else {
354            $file_name_explode = explode('.', $file_name);
355            $file_extension = (isset($file_name_explode[count($file_name_explode)-1]) ? $file_name_explode[count($file_name_explode)-1] : null);
356            unset($file_name_explode[count($file_name_explode)-1]);
357            $file_name_only = implode('.', $file_name_explode);
358            unset($file_name_explode);
359
360            $i = 1;
361            $found = true;
362            do {
363                $new_file_name = $file_name_only.'_'.$i.'.'.$file_extension;
364                if (file_exists($this->move_uploaded_to.DIRECTORY_SEPARATOR.$new_file_name)) {
365                    $found = true;
366                    if ($i > 1000) {
367                        // too many loop
368                        $file_name = uniqid().'-'.str_replace('.', '', microtime(true));
369                        $found = false;
370                    }
371                } else {
372                    $file_name = $new_file_name;
373                    $found = false;
374                }
375                $i++;
376            } while ($found === true);
377
378            unset($file_extension, $file_name_only, $new_file_name);
379            return $file_name;
380        }
381    }// renameDuplicateFile
382
383
384    /**
385     * Security scan. Scan for such as embedded php code in the uploaded file.
386     *
387     * @return boolean Return true on safety, return false for otherwise.
388     */
389    protected function securityScan()
390    {
391        if (
392            is_array($this->files[$this->input_file_name]) &&
393            array_key_exists('name', $this->files[$this->input_file_name]) &&
394            array_key_exists('tmp_name', $this->files[$this->input_file_name]) &&
395            $this->files[$this->input_file_name]['tmp_name'] != null
396        ) {
397            // there is an uploaded file.
398            if (is_file($this->files[$this->input_file_name]['tmp_name'])) {
399                $file_content = file_get_contents($this->files[$this->input_file_name]['tmp_name']);
400
401                // scan php open tag
402                if (strpos($file_content, '<?php') !== false) {
403                    // found php open tag. (<?php).
404                    $this->setErrorMessage(
405                        sprintf(static::__('Error! Found php embedded in the uploaded file. (%s).'), $this->files[$this->input_file_name]['name']),
406                        'RDU_SEC_ERR_PHP',
407                        $this->files[$this->input_file_name]['name'],
408                        $this->files[$this->input_file_name]['name'],
409                        $this->files[$this->input_file_name]['size'],
410                        $this->files[$this->input_file_name]['type']
411                    );
412                    return false;
413                }
414
415                // scan cgi/perl
416                if (strpos($file_content, '#!/') !== false && strpos($file_content, '/perl') !== false) {
417                    // found cgi/perl header.
418                    $this->setErrorMessage(
419                        sprintf(static::__('Error! Found cgi/perl embedded in the uploaded file. (%s).'), $this->files[$this->input_file_name]['name']),
420                        'RDU_SEC_ERR_CGI',
421                        $this->files[$this->input_file_name]['name'],
422                        $this->files[$this->input_file_name]['name'],
423                        $this->files[$this->input_file_name]['size'],
424                        $this->files[$this->input_file_name]['type']
425                    );
426                    return false;
427                }
428
429                unset($file_content);
430            }
431        }
432
433        return true;
434    }// securityScan
435
436
437    /**
438     * Set the error message into error_messages and error_codes properties.
439     *
440     * @since 2.0.1
441     * @param string $error_messages The error message.
442     * @param string $code The error code, start with RDU_ and follow with number or short error message without space.
443     * @param string $errorAttributes Error attributes. For example: 9MB > 2MB in case that limit file size to 2MB but uploaded 9MB, or showing file name that have problem.
444     * @param string $errorFileName The file name with extension.
445     * @param string $errorFileSize The file size in bytes.
446     * @param string $errorFileMime The file mime type.
447     */
448    protected function setErrorMessage(
449        $error_messages,
450        $code,
451        $errorAttributes = '',
452        $errorFileName = '',
453        $errorFileSize = '',
454        $errorFileMime = ''
455    ) {
456        $arg_list = func_get_args();
457        $numargs = func_num_args();
458        for ($i = 0; $i < $numargs; $i++) {
459            if (is_array($arg_list) && array_key_exists($i, $arg_list) && !is_scalar($arg_list[$i])) {
460                return false;
461            } elseif ($arg_list === false) {
462                return false;
463            }
464        }
465        unset($arg_list, $i, $numargs);
466
467        $this->error_messages[] = $error_messages;
468        $this->error_codes[] = array(
469            'code' => $code,
470            'errorAttributes' => $errorAttributes,
471            'errorFileName' => $errorFileName,
472            'errorFileSize' => $errorFileSize,
473            'errorFileMime' => $errorFileMime,
474        );
475    }// setErrorMessage
476
477
478    /**
479     * Set input file name.<br>
480     * If you begins new class object then you don't have to call this method. You must call this method after called to the clear() method.<br>
481     * Or you can call this method in case that you want to process the other uploaded file next to previous one.
482     *
483     * @param string $input_file_name The name of input file.
484     */
485    public function setInputFileName($input_file_name)
486    {
487        $this->input_file_name = $input_file_name;
488    }// setInputFileName
489
490
491    /**
492     * Set the new file name if it was not set, check for reserved file name and removed those characters.
493     *
494     * @link http://windows.microsoft.com/en-us/windows/file-names-extensions-faq#1TC=windows-7 Windows file name FAQ.
495     * @link https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx?f=255&MSPPError=-2147217396 Windows reserved file name.
496     * @link https://en.wikipedia.org/wiki/Filename Global reserved file name.
497     */
498    protected function setNewFileName()
499    {
500        $this->new_file_name = trim($this->new_file_name);
501
502        if ($this->new_file_name == null) {
503            // if new file name was not set, set it from uploaded file name.
504            if (is_array($this->files[$this->input_file_name]) && array_key_exists('name', $this->files[$this->input_file_name])) {
505                $file_name_explode = explode('.', $this->files[$this->input_file_name]['name']);
506                unset($file_name_explode[count($file_name_explode)-1]);
507                $this->new_file_name = implode('.', $file_name_explode);
508                unset($file_name_explode);
509            } else {
510                $this->setNewFileNameToRandom();
511            }
512        }
513
514        // do not allow name that contain one of these characters.
515        $reserved_characters = array('\\', '/', '?', '%', '*', ':', '|', '"', '<', '>', '!', '@');
516        $this->new_file_name = str_replace($reserved_characters, '', $this->new_file_name);
517        unset($reserved_characters);
518
519        if (preg_match('#[^\.]+#iu', $this->new_file_name) == 0) {
520            // found the name is only dots. example ., .., ..., ....
521            $this->setNewFileNameToRandom();
522        }
523
524        // reserved words or reserved names. do not allow if new name is set to one of these words or names.
525        // make it case in-sensitive.
526        $reserved_words = array(
527            'CON', 'PRN', 'AUX', 'CLOCK$', 'NUL',
528            'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9',
529            'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9',
530            'LST', 'KEYBD$', 'SCREEN$', '$IDLE$', 'CONFIG$',
531            '$Mft', '$MftMirr', '$LogFile', '$Volume', '$AttrDef', '$Bitmap', '$Boot', '$BadClus', '$Secure',
532            '$Upcase', '$Extend', '$Quota', '$ObjId', '$Reparse',
533        );
534        foreach ($reserved_words as $reserved_word) {
535            if (strtolower($reserved_word) == strtolower($this->new_file_name)) {
536                $this->setNewFileNameToRandom();
537            }
538        }
539        unset($reserved_word, $reserved_words);
540
541        // in the end if it is still left new name as null... set random name to it.
542        if ($this->new_file_name == null) {
543            $this->setNewFileNameToRandom();
544        }
545    }// setNewFileName
546
547
548    /**
549     * Set the new file name to random. (unique id and microtime).
550     */
551    protected function setNewFileNameToRandom()
552    {
553        $this->new_file_name = uniqid().'-'.str_replace('.', '', microtime(true));
554    }// setNewFileNameToRandom
555
556
557    /**
558     * Setup file extensions mime types for validation. (In case that it was not set).
559     */
560    protected function setupFileExtensionsMimeTypesForValidation()
561    {
562        if (!is_array($this->file_extensions_mime_types) && $this->file_extensions_mime_types == null) {
563            // extensions mime types was not set and not set to NOT validate (empty array).
564            $default_mime_types_file = 'file-extensions-mime-types.php';
565            if (is_file(__DIR__.DIRECTORY_SEPARATOR.$default_mime_types_file)) {
566                $this->file_extensions_mime_types = include __DIR__.DIRECTORY_SEPARATOR.$default_mime_types_file;
567            }
568            unset($default_mime_types_file);
569        }
570
571        if (is_array($this->file_extensions_mime_types)) {
572            // if mime types was set, change the keys to lower case.
573            $this->file_extensions_mime_types = array_change_key_case($this->file_extensions_mime_types, CASE_LOWER);
574        }
575
576        if (is_array($this->allowed_file_extensions)) {
577            // if allowed extensions was set, change the values to lower case.
578            $this->allowed_file_extensions = array_map('strtolower', $this->allowed_file_extensions);
579        }
580    }// setupFileExtensionsMimeTypesForValidation
581
582
583    /**
584     * Set the file name that is safe for web.<br>
585     * The safe for web file name is English and number chacters, no space (replaced with dash), no special characters, allowed dash and underscore.
586     */
587    protected function setWebSafeFileName()
588    {
589        if ($this->new_file_name == null) {
590            $this->setNewFileName();
591        }
592
593        // replace multiple spaces to one space.
594        $this->new_file_name = preg_replace('#\s+#iu', ' ', $this->new_file_name);
595        // replace space to dash.
596        $this->new_file_name = str_replace(' ', '-', $this->new_file_name);
597        // replace non alpha-numeric to nothing.
598        $this->new_file_name = preg_replace('#[^\da-z\-_]#iu', '', $this->new_file_name);
599        // replace multiple dashes to one dash.
600        $this->new_file_name = preg_replace('#-{2,}#', '-', $this->new_file_name);
601    }// setWebSafeFileName
602
603
604    /**
605     * Test get the real file's mime type using finfo_file.<br>
606     * This is very useful when you want to add new file extension and mime type to validate uploaded files.
607     *
608     * @link http://php.net/manual/en/function.finfo-file.php More info about finfo_file() function.
609     * @param string $input_file_name The input file name. This support only one file upload.
610     * @return string Return file's mime type or error message.
611     */
612    public function testGetUploadedMimetype($input_file_name = null)
613    {
614        if ($input_file_name == null) {
615            $input_file_name = $this->input_file_name;
616        }
617
618        if (
619            !isset($_FILES[$input_file_name]['name']) ||
620            (isset($_FILES[$input_file_name]['name']) && $_FILES[$input_file_name]['name'] == null) ||
621            !isset($_FILES[$input_file_name]['tmp_name']) ||
622            (isset($_FILES[$input_file_name]['tmp_name']) && $_FILES[$input_file_name]['tmp_name'] == null)
623        ) {
624            return static::__('You did not upload any file, please upload a file to get info.');
625        }
626
627        if (!function_exists('finfo_open') || !function_exists('finfo_file')) {
628            return static::__('There is no finfo_open() function or finfo_file() function to get file\'s info. Please verify PHP installation.');
629        }
630
631        $output = sprintf(static::__('File name: %s'), $_FILES[$input_file_name]['name']).'<br>'."\n";
632        $file_name_exp = explode('.', $_FILES[$input_file_name]['name']);
633        $file_extension = $file_name_exp[count($file_name_exp)-1];
634        unset($file_name_exp);
635        $output .= sprintf(static::__('File extension: %s'), $file_extension).'<br>'."\n";
636
637        $Finfo = new \finfo();
638        $file_mimetype = $Finfo->file($_FILES[$input_file_name]['tmp_name'], FILEINFO_MIME_TYPE);
639        $output .= sprintf(static::__('Mime type: %s'), $file_mimetype).'<br>'."\n";
640        $output .= '<br>'."\n";
641        $output .= static::__('The array for use with extension-mime types validation.').'<br>'."\n";
642        $output .= 'array(<br>'."\n";
643        $output .= '&nbsp; &nbsp; \''.$file_extension.'\' =&gt; array(\''.$file_mimetype.'\'),<br>'."\n";
644        $output .= ');'."\n";
645        unset($Finfo);
646
647        if (is_writable($_FILES[$input_file_name]['tmp_name'])) {
648            unlink($_FILES[$input_file_name]['tmp_name']);
649        }
650
651        unset($file_extension, $file_mimetype);
652        return $output;
653    }// testGetUploadedMimetype
654
655
656    /**
657     * Start the upload and move uploaded files process.
658     *
659     * @return boolean Return true on success, false for otherwise. If upload multiple file and there is error only one it return false.
660     */
661    public function upload()
662    {
663        // validate that all options properties was properly set to correct type.
664        $this->validateOptionsProperties();
665        // setup file extensions and mime types for validation. (in case that it was not set).
666        $this->setupFileExtensionsMimeTypesForValidation();
667
668        // verify that location where the uploaded file(s) will be moved to is writable.
669        if (!is_dir($this->move_uploaded_to)) {
670            $this->setErrorMessage(
671                static::__('The target location where the uploaded file(s) will be moved to is not folder or directory.'),
672                'RDU_MOVE_UPLOADED_TO_NOT_DIR',
673                $this->move_uploaded_to
674            );
675            return false;
676        } elseif (is_dir($this->move_uploaded_to) && !is_writable($this->move_uploaded_to)) {
677            $this->setErrorMessage(
678                static::__('The target location where the uploaded file(s) will be moved to is not writable. Please check the folder permission.'),
679                'RDU_MOVE_UPLOADED_TO_NOT_WRITABLE',
680                $this->move_uploaded_to
681            );
682            return false;
683        } else {
684            // solve the move uploaded to as a real path.
685            $this->move_uploaded_to = realpath($this->move_uploaded_to);
686        }
687
688        if (isset($_FILES[$this->input_file_name]['name']) && is_array($_FILES[$this->input_file_name]['name'])) {
689            foreach ($_FILES[$this->input_file_name]['name'] as $key => $value) {
690                $this->files[$this->input_file_name]['input_file_key'] = $key;
691                $this->files[$this->input_file_name]['name'] = $_FILES[$this->input_file_name]['name'][$key];
692                $this->files[$this->input_file_name]['type'] = (isset($_FILES[$this->input_file_name]['type'][$key]) ? $_FILES[$this->input_file_name]['type'][$key] : null);
693                $this->files[$this->input_file_name]['tmp_name'] = (isset($_FILES[$this->input_file_name]['tmp_name'][$key]) ? $_FILES[$this->input_file_name]['tmp_name'][$key] : null);
694                $this->files[$this->input_file_name]['error'] = (isset($_FILES[$this->input_file_name]['error'][$key]) ? $_FILES[$this->input_file_name]['error'][$key] : 4);
695                $this->files[$this->input_file_name]['size'] = (isset($_FILES[$this->input_file_name]['size'][$key]) ? $_FILES[$this->input_file_name]['size'][$key] : 0);
696
697                $result = $this->uploadSingleFile();
698
699                if ($result == false && $this->stop_on_failed_upload_multiple === true) {
700                    // it was set to sop on failed to upload multiple file. return false.
701                    unset($result);
702                    return false;
703                }
704            }// endforeach;
705            unset($key, $value);
706        } else {
707            $this->files[$this->input_file_name] = $_FILES[$this->input_file_name];
708            $this->files[$this->input_file_name]['input_file_key'] = 0;
709
710            $result = $this->uploadSingleFile();
711        }
712
713        if (isset($result) && $result == false && $this->stop_on_failed_upload_multiple === true) {
714            // there is at lease one upload error and it was set to stop on error.
715            unset($result);
716            $this->clearUploadedAtTemp();
717            return false;
718        } elseif (count($this->error_messages) > 0 && $this->stop_on_failed_upload_multiple === true) {
719            // there is at lease one upload error and it was set to stop on error.
720            unset($result);
721            $this->clearUploadedAtTemp();
722            return false;
723        }
724
725        return $this->moveUploadedFiles();
726    }// upload
727
728
729    /**
730     * Start upload process for single file.<br>
731     * Even upload multiple file will call to this method because it will be re-format the uploaded files property to become a single file and then call this.
732     *
733     * @return boolean Return true on success, false for otherwise.
734     */
735    protected function uploadSingleFile()
736    {
737        // check if there is error while uploading from error array key.
738        if (is_array($this->files[$this->input_file_name]) && array_key_exists('error', $this->files[$this->input_file_name]) && $this->files[$this->input_file_name]['error'] != 0) {
739            switch ($this->files[$this->input_file_name]['error']) {
740                case 1:
741                    $this->setErrorMessage(
742                        sprintf(static::__('The uploaded file exceeds the max file size directive. (%s &gt; %s).'), $this->files[$this->input_file_name]['size'], ini_get('upload_max_filesize')),
743                        'RDU_' . $this->files[$this->input_file_name]['error'],
744                        $this->files[$this->input_file_name]['size'] . ' &gt; ' . ini_get('upload_max_filesize'),
745                        $this->files[$this->input_file_name]['name'],
746                        $this->files[$this->input_file_name]['size'],
747                        $this->files[$this->input_file_name]['type']
748                    );
749                    return false;
750                case 2:
751                    $this->setErrorMessage(
752                        static::__('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.'),
753                        'RDU_' . $this->files[$this->input_file_name]['error'],
754                        '',
755                        $this->files[$this->input_file_name]['name'],
756                        $this->files[$this->input_file_name]['size'],
757                        $this->files[$this->input_file_name]['type']
758                    );
759                    return false;
760                case 3:
761                    $this->setErrorMessage(
762                        static::__('The uploaded file was only partially uploaded.'),
763                        'RDU_' . $this->files[$this->input_file_name]['error'],
764                        '',
765                        $this->files[$this->input_file_name]['name'],
766                        $this->files[$this->input_file_name]['size'],
767                        $this->files[$this->input_file_name]['type']
768                    );
769                    return false;
770                case 4:
771                    $this->setErrorMessage(
772                        static::__('You did not upload the file.'),
773                        'RDU_' . $this->files[$this->input_file_name]['error'],
774                        '',
775                        $this->files[$this->input_file_name]['name'],
776                        $this->files[$this->input_file_name]['size'],
777                        $this->files[$this->input_file_name]['type']
778                    );
779                    return false;
780                case 6:
781                    $this->setErrorMessage(
782                        static::__('Missing a temporary folder.'),
783                        'RDU_' . $this->files[$this->input_file_name]['error'],
784                        '',
785                        $this->files[$this->input_file_name]['name'],
786                        $this->files[$this->input_file_name]['size'],
787                        $this->files[$this->input_file_name]['type']
788                    );
789                    return false;
790                case 7:
791                    $this->setErrorMessage(
792                        static::__('Failed to write file to disk.'),
793                        'RDU_' . $this->files[$this->input_file_name]['error'],
794                        '',
795                        $this->files[$this->input_file_name]['name'],
796                        $this->files[$this->input_file_name]['size'],
797                        $this->files[$this->input_file_name]['type']
798                    );
799                    return false;
800                case 8:
801                    $this->setErrorMessage(
802                        static::__('A PHP extension stopped the file upload.'),
803                        'RDU_' . $this->files[$this->input_file_name]['error'],
804                        '',
805                        $this->files[$this->input_file_name]['name'],
806                        $this->files[$this->input_file_name]['size'],
807                        $this->files[$this->input_file_name]['type']
808                    );
809                    return false;
810            }
811        }
812
813        // validate that there is file upload.
814        if (
815            empty($this->files[$this->input_file_name]) ||
816            (
817                is_array($this->files[$this->input_file_name]) &&
818                array_key_exists('name', $this->files[$this->input_file_name]) &&
819                $this->files[$this->input_file_name]['name'] == null
820            ) ||
821            (
822                is_array($this->files[$this->input_file_name]) &&
823                array_key_exists('tmp_name', $this->files[$this->input_file_name]) &&
824                $this->files[$this->input_file_name]['tmp_name'] == null
825            )
826        ) {
827            $this->setErrorMessage(
828                static::__('You did not upload the file.'),
829                'RDU_4',
830                '',
831                $this->files[$this->input_file_name]['name'],
832                $this->files[$this->input_file_name]['size'],
833                $this->files[$this->input_file_name]['type']
834            );
835            return false;
836        }
837
838        // validate allowed extension and its mime types.
839        $result = $this->validateExtensionAndMimeType();
840        if ($result !== true) {
841            return false;
842        }
843        unset($result);
844
845        // validate max file size.
846        $result = $this->validateFileSize();
847        if ($result !== true) {
848            return false;
849        }
850        unset($result);
851
852        // security scan.
853        if ($this->security_scan === true) {
854            $result = $this->securityScan();
855            if ($result !== true) {
856                return false;
857            }
858            unset($result);
859        }
860
861        // set new file name (in case that it was not set) and check for reserved file name.
862        $tmp_new_file_name = $this->new_file_name;
863        $this->setNewFileName();
864
865        // check for safe web file name if this option was set to true.
866        if ($this->web_safe_file_name === true) {
867            $this->setWebSafeFileName();
868        }
869
870        // now, it should all passed validation. add the uploaded file to move uploaded queue in case that it is upload multiple file and has option to stop on error.
871        // get the uploaded file extension.
872        $file_name_explode = explode('.', $this->files[$this->input_file_name]['name']);
873        $file_extension = null;
874        if (is_array($file_name_explode)) {
875            $file_extension = '.'.$file_name_explode[count($file_name_explode)-1];
876        }
877        unset($file_name_explode);
878        // add to queue.
879        $this->move_uploaded_queue = array_merge(
880            $this->move_uploaded_queue,
881            array(
882                $this->files[$this->input_file_name]['input_file_key'] => array(
883                    'name' => $this->files[$this->input_file_name]['name'],
884                    'tmp_name' => $this->files[$this->input_file_name]['tmp_name'],
885                    'new_name' => $this->new_file_name.$file_extension,
886                )
887            )
888        );
889        // restore temp of new file name to ready for next loop of upload multiple.
890        $this->new_file_name = $tmp_new_file_name;
891        unset($file_extension, $tmp_new_file_name);
892
893        // done.
894        return true;
895    }// uploadSingleFile
896
897
898    /**
899     * Validate allowed extension and its mime types (if all of these was set).
900     *
901     * @return boolean Return true on success, false on failure.
902     */
903    protected function validateExtensionAndMimeType()
904    {
905        if ($this->allowed_file_extensions == null && ($this->file_extensions_mime_types == null || empty($this->file_extensions_mime_types))) {
906            // it is not set to limit uploaded file extensions and mime types.
907            return true;
908        }
909
910        // get only file extension of uploaded file.
911        $file_name_explode = explode('.', $this->files[$this->input_file_name]['name']);
912        if (!is_array($file_name_explode)) {
913            unset($file_name_explode);
914            $this->setErrorMessage(
915                sprintf(static::__('Unable to validate extension for the file %s.'), $this->files[$this->input_file_name]['name']),
916                'RDU_UNABLE_VALIDATE_EXT',
917                $this->files[$this->input_file_name]['name'],
918                $this->files[$this->input_file_name]['name'],
919                $this->files[$this->input_file_name]['size'],
920                $this->files[$this->input_file_name]['type']
921            );
922            return false;
923        }
924        $file_extension = strtolower($file_name_explode[count($file_name_explode)-1]);
925        unset($file_name_explode);
926
927        // validate allowed extensions.
928        if (is_array($this->allowed_file_extensions) && !in_array($file_extension, $this->allowed_file_extensions)) {
929            unset($file_extension);
930            $this->setErrorMessage(
931                sprintf(static::__('You have uploaded the file that is not allowed extension. (%s)'), $this->files[$this->input_file_name]['name']),
932                'RDU_NOT_ALLOW_EXT',
933                $this->files[$this->input_file_name]['name'],
934                $this->files[$this->input_file_name]['name'],
935                $this->files[$this->input_file_name]['size'],
936                $this->files[$this->input_file_name]['type']
937            );
938            return false;
939        }
940
941        // validate allowed mime types that match uploaded file's extension.
942        if (is_array($this->file_extensions_mime_types) && !empty($this->file_extensions_mime_types)) {
943            if (!array_key_exists($file_extension, $this->file_extensions_mime_types)) {
944                unset($file_extension);
945                $this->setErrorMessage(
946                    sprintf(static::__('Unable to validate the file extension and mime type. (%s). This file extension was not set in the &quot;file_extensions_mime_types&quot; property.'), $this->files[$this->input_file_name]['name']),
947                    'RDU_UNABLE_VALIDATE_EXT_AND_MIME',
948                    $this->files[$this->input_file_name]['name'],
949                    $this->files[$this->input_file_name]['name'],
950                    $this->files[$this->input_file_name]['size'],
951                    $this->files[$this->input_file_name]['type']
952                );
953                return false;
954            } else {
955                $Finfo = new \finfo();
956                $file_mimetype = $Finfo->file($this->files[$this->input_file_name]['tmp_name'], FILEINFO_MIME_TYPE);
957                if (is_array($this->file_extensions_mime_types[$file_extension]) && !in_array(strtolower($file_mimetype), array_map('strtolower', $this->file_extensions_mime_types[$file_extension]))) {
958                    unset($file_extension, $Finfo);
959                    $this->setErrorMessage(
960                        sprintf(static::__('The uploaded file has invalid mime type. (%s : %s).'), $this->files[$this->input_file_name]['name'], $file_mimetype),
961                        'RDU_INVALID_MIME',
962                        $this->files[$this->input_file_name]['name'] . ' : ' . $file_mimetype,
963                        $this->files[$this->input_file_name]['name'],
964                        $this->files[$this->input_file_name]['size'],
965                        $file_mimetype
966                    );
967                    unset($file_mimetype);
968                    return false;
969                } elseif (!is_array($this->file_extensions_mime_types[$file_extension])) {
970                    unset($file_extension, $file_mimetype, $Finfo);
971                    $this->setErrorMessage(
972                        static::__('Unable to validate mime type. The format of &quot;file_extensions_mime_types&quot; property is incorrect.'),
973                        'RDU_UNABLE_VALIDATE_MIME',
974                        '',
975                        $this->files[$this->input_file_name]['name'],
976                        $this->files[$this->input_file_name]['size'],
977                        $this->files[$this->input_file_name]['type']
978                    );
979                    return false;
980                }
981                unset($file_mimetype, $Finfo);
982            }
983        }
984
985        unset($file_extension);
986        return true;
987    }// validateExtensionAndMimeType
988
989
990    /**
991     * Validate uploaded file must not exceed max file size limit. (if max file size limit was set).
992     *
993     * @return boolean Return true on success, false on failure.
994     */
995    protected function validateFileSize()
996    {
997        if (!is_numeric($this->max_file_size) && !is_int($this->max_file_size)) {
998            // it is not set max file size limitation.
999            return true;
1000        }
1001
1002        if (is_array($this->files[$this->input_file_name]) && array_key_exists('size', $this->files[$this->input_file_name]) && $this->files[$this->input_file_name]['size'] > $this->max_file_size) {
1003            $this->setErrorMessage(
1004                sprintf(static::__('The uploaded file exceeds the max file size directive. (%s &gt; %s).'), $this->files[$this->input_file_name]['size'], $this->max_file_size),
1005                'RDU_1',
1006                $this->files[$this->input_file_name]['size'] . ' &gt; ' . $this->max_file_size,
1007                $this->files[$this->input_file_name]['name'],
1008                $this->files[$this->input_file_name]['size'],
1009                $this->files[$this->input_file_name]['type']
1010            );
1011            return false;
1012        }
1013
1014        return true;
1015    }// validateFileSize
1016
1017
1018    /**
1019     * Validate that these options properties has properly set in the correct type.
1020     */
1021    protected function validateOptionsProperties()
1022    {
1023        if (!is_array($this->allowed_file_extensions) && $this->allowed_file_extensions != null) {
1024            $this->allowed_file_extensions = array($this->allowed_file_extensions);
1025        }
1026
1027        if (!is_array($this->file_extensions_mime_types) && $this->file_extensions_mime_types != null) {
1028            $this->file_extensions_mime_types = null;
1029        }
1030
1031        if (is_numeric($this->max_file_size) && !is_int($this->max_file_size)) {
1032            $this->max_file_size = intval($this->max_file_size);
1033        } elseif (!is_int($this->max_file_size) && $this->max_file_size != null) {
1034            $this->max_file_size = null;
1035        }
1036
1037        if ($this->move_uploaded_to == null) {
1038            $this->move_uploaded_to = '.';
1039        }
1040
1041        if (!is_string($this->new_file_name) && $this->new_file_name != null) {
1042            $this->new_file_name = null;
1043        }
1044
1045        if (!is_bool($this->overwrite)) {
1046            $this->overwrite = false;
1047        }
1048
1049        if (!is_bool($this->web_safe_file_name)) {
1050            $this->web_safe_file_name = true;
1051        }
1052
1053        if (!is_bool($this->security_scan)) {
1054            $this->security_scan = false;
1055        }
1056
1057        if (!is_bool($this->stop_on_failed_upload_multiple)) {
1058            $this->stop_on_failed_upload_multiple = true;
1059        }
1060    }// validateOptionsProperties
1061
1062
1063}