Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.06% covered (success)
98.06%
152 / 155
87.50% covered (warning)
87.50%
14 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
filespec
98.06% covered (success)
98.06%
152 / 155
87.50% covered (warning)
87.50%
14 / 16
81
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 set_upload_ary
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
8
 set_upload_namespace
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 init_error
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 set_error
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 clean_filename
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
8
 get
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 is_image
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 is_uploaded
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
8
 remove
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 get_extension
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 get_mimetype
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 get_filesize
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 check_content
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
5
 move_file
98.25% covered (success)
98.25%
56 / 57
0.00% covered (danger)
0.00%
0 / 1
29
 additional_checks
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
6
1<?php
2/**
3 *
4 * This file is part of the phpBB Forum Software package.
5 *
6 * @copyright (c) phpBB Limited <https://www.phpbb.com>
7 * @license GNU General Public License, version 2 (GPL-2.0)
8 *
9 * For full copyright and license information, please see
10 * the docs/CREDITS.txt file.
11 *
12 */
13
14namespace phpbb\files;
15
16use phpbb\language\language;
17
18/**
19 * Responsible for holding all file relevant information, as well as doing file-specific operations.
20 * The {@link fileupload fileupload class} can be used to upload several files, each of them being this object to operate further on.
21 *
22 * @deprecated 4.0.0    Use phpbb\files\filespec_storage instead
23 */
24class filespec
25{
26    /** @var string File name */
27    protected $filename = '';
28
29    /** @var string Real name of file */
30    protected $realname = '';
31
32    /** @var string Upload name of file */
33    protected $uploadname = '';
34
35    /** @var string Mimetype of file */
36    protected $mimetype = '';
37
38    /** @var string File extension */
39    protected $extension = '';
40
41    /** @var int File size */
42    protected $filesize = 0;
43
44    /** @var int Width of file */
45    protected $width = 0;
46
47    /** @var int Height of file */
48    protected $height = 0;
49
50    /** @var array Image info including type and size */
51    protected $image_info = array();
52
53    /** @var string Destination file name */
54    protected $destination_file = '';
55
56    /** @var string Destination file path */
57    protected $destination_path = '';
58
59    /** @var bool Whether file was moved */
60    protected $file_moved = false;
61
62    /** @var bool Whether file is local */
63    protected $local = false;
64
65    /** @var bool Class initialization flag */
66    protected $class_initialized = false;
67
68    /** @var array Error array */
69    public $error = array();
70
71    /** @var upload Instance of upload class  */
72    public $upload;
73
74    /** @var \phpbb\filesystem\filesystem_interface */
75    protected $filesystem;
76
77    /** @var \bantu\IniGetWrapper\IniGetWrapper ini_get() wrapper class */
78    protected $php_ini;
79
80    /** @var \FastImageSize\FastImageSize */
81    protected $imagesize;
82
83    /** @var language Language class */
84    protected $language;
85
86    /** @var string phpBB root path */
87    protected $phpbb_root_path;
88
89    /** @var \phpbb\plupload\plupload|null The plupload object */
90    protected $plupload;
91
92    /** @var \phpbb\mimetype\guesser|null phpBB Mimetype guesser */
93    protected $mimetype_guesser;
94
95    /**
96     * File upload class
97     *
98     * @param \phpbb\filesystem\filesystem_interface    $phpbb_filesystem Filesystem
99     * @param language                    $language Language
100     * @param \bantu\IniGetWrapper\IniGetWrapper            $php_ini ini_get() wrapper
101     * @param \FastImageSize\FastImageSize $imagesize Imagesize class
102     * @param string                    $phpbb_root_path phpBB root path
103     * @param \phpbb\mimetype\guesser|null    $mimetype_guesser Mime type guesser
104     * @param \phpbb\plupload\plupload|null    $plupload Plupload
105     */
106    public function __construct(\phpbb\filesystem\filesystem_interface $phpbb_filesystem, language $language, \bantu\IniGetWrapper\IniGetWrapper $php_ini, \FastImageSize\FastImageSize $imagesize, $phpbb_root_path, \phpbb\mimetype\guesser $mimetype_guesser = null, \phpbb\plupload\plupload $plupload = null)
107    {
108        $this->filesystem = $phpbb_filesystem;
109        $this->language = $language;
110        $this->php_ini = $php_ini;
111        $this->imagesize = $imagesize;
112        $this->phpbb_root_path = $phpbb_root_path;
113        $this->plupload = $plupload;
114        $this->mimetype_guesser = $mimetype_guesser;
115    }
116
117    /**
118     * Set upload ary
119     *
120     * @param array $upload_ary Upload ary
121     *
122     * @return filespec This instance of the filespec class
123     */
124    public function set_upload_ary($upload_ary)
125    {
126        if (!isset($upload_ary) || !count($upload_ary))
127        {
128            return $this;
129        }
130
131        $this->class_initialized = true;
132        $this->filename = $upload_ary['tmp_name'];
133        $this->filesize = $upload_ary['size'];
134        $name = $upload_ary['name'];
135        $name = trim(utf8_basename($name));
136        $this->realname = $this->uploadname = $name;
137        $this->mimetype = $upload_ary['type'];
138
139        // Opera adds the name to the mime type
140        $this->mimetype    = ($this->mimetype && str_contains($this->mimetype, '; name')) ? str_replace(strstr($this->mimetype, '; name'), '', $this->mimetype) : $this->mimetype;
141
142        if (!$this->mimetype)
143        {
144            $this->mimetype = 'application/octet-stream';
145        }
146
147        $this->extension = strtolower(self::get_extension($this->realname));
148
149        // Try to get real filesize from temporary folder (not always working) ;)
150        $this->filesize = ($this->get_filesize($this->filename)) ?: $this->filesize;
151
152        $this->width = $this->height = 0;
153        $this->file_moved = false;
154
155        $this->local = (isset($upload_ary['local_mode'])) ? true : false;
156
157        return $this;
158    }
159
160    /**
161     * Set the upload namespace
162     *
163     * @param upload $namespace Instance of upload class
164     *
165     * @return filespec This instance of the filespec class
166     */
167    public function set_upload_namespace($namespace)
168    {
169        $this->upload = $namespace;
170
171        return $this;
172    }
173
174    /**
175     * Check if class members were not properly initialised yet
176     *
177     * @return bool True if there was an init error, false if not
178     */
179    public function init_error()
180    {
181        return !$this->class_initialized;
182    }
183
184    /**
185     * Set error in error array
186     *
187     * @param mixed $error Content for error array
188     *
189     * @return \phpbb\files\filespec This instance of the filespec class
190     */
191    public function set_error($error)
192    {
193        $this->error[] = $error;
194
195        return $this;
196    }
197
198    /**
199     * Cleans destination filename
200     *
201     * @param string $mode Either real, unique, or unique_ext. Real creates a
202     *                realname, filtering some characters, lowering every
203     *                character. Unique creates a unique filename.
204     * @param string $prefix Prefix applied to filename
205     * @param string $user_id The user_id is only needed for when cleaning a user's avatar
206     */
207    public function clean_filename($mode = 'unique', $prefix = '', $user_id = '')
208    {
209        if ($this->init_error())
210        {
211            return;
212        }
213
214        switch ($mode)
215        {
216            case 'real':
217                // Remove every extension from filename (to not let the mime bug being exposed)
218                if (strpos($this->realname, '.') !== false)
219                {
220                    $this->realname = substr($this->realname, 0, strpos($this->realname, '.'));
221                }
222
223                // Replace any chars which may cause us problems with _
224                $bad_chars = array("'", "\\", ' ', '/', ':', '*', '?', '"', '<', '>', '|');
225
226                $this->realname = rawurlencode(str_replace($bad_chars, '_', strtolower($this->realname)));
227                $this->realname = preg_replace("/%(\w{2})/", '_', $this->realname);
228
229                $this->realname = $prefix . $this->realname . '.' . $this->extension;
230            break;
231
232            case 'unique':
233                $this->realname = $prefix . md5(unique_id());
234            break;
235
236            case 'avatar':
237                $this->extension = strtolower($this->extension);
238                $this->realname = $prefix . $user_id . '.' . $this->extension;
239
240            break;
241
242            case 'unique_ext':
243            default:
244                $this->realname = $prefix . md5(unique_id()) . '.' . $this->extension;
245        }
246    }
247
248    /**
249     * Get property from file object
250     *
251     * @param string $property Name of property
252     *
253     * @return mixed Content of property
254     */
255    public function get($property)
256    {
257        if ($this->init_error() || !isset($this->$property))
258        {
259            return false;
260        }
261
262        return $this->$property;
263    }
264
265    /**
266     * Check if file is an image (mime type)
267     *
268     * @return bool true if it is an image, false if not
269     */
270    public function is_image()
271    {
272        return (strpos($this->mimetype, 'image/') === 0);
273    }
274
275    /**
276     * Check if the file got correctly uploaded
277     *
278     * @return bool true if it is a valid upload, false if not
279     */
280    public function is_uploaded()
281    {
282        $is_plupload = $this->plupload && $this->plupload->is_active();
283
284        if (!$this->local && !$is_plupload && !is_uploaded_file($this->filename))
285        {
286            return false;
287        }
288
289        if (($this->local || $is_plupload) && !file_exists($this->filename))
290        {
291            return false;
292        }
293
294        return true;
295    }
296
297    /**
298     * Remove file
299     */
300    public function remove()
301    {
302        if ($this->file_moved)
303        {
304            @unlink($this->destination_file);
305        }
306    }
307
308    /**
309     * Get file extension
310     *
311     * @param string $filename Filename that needs to be checked
312     *
313     * @return string Extension of the supplied filename
314     */
315    public static function get_extension($filename)
316    {
317        $filename = utf8_basename($filename);
318
319        if (strpos($filename, '.') === false)
320        {
321            return '';
322        }
323
324        $filename = explode('.', $filename);
325        return array_pop($filename);
326    }
327
328    /**
329     * Get mime type
330     *
331     * @param string $filename Filename that needs to be checked
332     * @return string Mime type of supplied filename or empty string if mimetype could not be guessed
333     */
334    public function get_mimetype($filename)
335    {
336        if ($this->mimetype_guesser !== null)
337        {
338            $mimetype = $this->mimetype_guesser->guess($filename, $this->uploadname);
339
340            if ($mimetype !== 'application/octet-stream')
341            {
342                $this->mimetype = $mimetype;
343            }
344        }
345
346        return $this->mimetype ?: '';
347    }
348
349    /**
350     * Get file size
351     *
352     * @param string $filename File name of file to check
353     *
354     * @return int File size
355     */
356    public function get_filesize($filename)
357    {
358        return @filesize($filename);
359    }
360
361
362    /**
363     * Check the first 256 bytes for forbidden content
364     *
365     * @param array $disallowed_content Array containg disallowed content
366     *
367     * @return bool False if disallowed content found, true if not
368     */
369    public function check_content($disallowed_content)
370    {
371        if (empty($disallowed_content))
372        {
373            return true;
374        }
375
376        $fp = @fopen($this->filename, 'rb');
377
378        if ($fp !== false)
379        {
380            $ie_mime_relevant = fread($fp, 256);
381            fclose($fp);
382            foreach ($disallowed_content as $forbidden)
383            {
384                if (stripos($ie_mime_relevant, '<' . $forbidden) !== false)
385                {
386                    return false;
387                }
388            }
389        }
390        return true;
391    }
392
393    /**
394     * Move file to destination folder
395     * The phpbb_root_path variable will be applied to the destination path
396     *
397     * @param string $destination Destination path, for example $config['avatar_path']
398     * @param bool $overwrite If set to true, an already existing file will be overwritten
399     * @param bool $skip_image_check If set to true, the check for the file to be a valid image is skipped
400     * @param string|bool $chmod Permission mask for chmodding the file after a successful move.
401     *                The mode entered here reflects the mode defined by {@link \phpbb\filesystem\filesystem::phpbb_chmod()}
402     *
403     * @return bool True if file was moved, false if not
404     * @access public
405     */
406    public function move_file($destination, $overwrite = false, $skip_image_check = false, $chmod = false)
407    {
408        if (count($this->error))
409        {
410            return false;
411        }
412
413        $chmod = ($chmod === false) ? \phpbb\filesystem\filesystem_interface::CHMOD_READ | \phpbb\filesystem\filesystem_interface::CHMOD_WRITE : $chmod;
414
415        // We need to trust the admin in specifying valid upload directories and an attacker not being able to overwrite it...
416        $this->destination_path = $this->phpbb_root_path . $destination;
417
418        // Check if the destination path exist...
419        if (!file_exists($this->destination_path))
420        {
421            @unlink($this->filename);
422            return false;
423        }
424
425        $upload_mode = ($this->php_ini->getBool('open_basedir')) ? 'move' : 'copy';
426        $upload_mode = ($this->local) ? 'local' : $upload_mode;
427        $this->destination_file = $this->destination_path . '/' . utf8_basename($this->realname);
428
429        // Check if the file already exist, else there is something wrong...
430        if (file_exists($this->destination_file) && !$overwrite)
431        {
432            @unlink($this->filename);
433            $this->error[] = $this->language->lang($this->upload->error_prefix . 'GENERAL_UPLOAD_ERROR', $this->destination_file);
434            $this->file_moved = false;
435            return false;
436        }
437        else
438        {
439            if (file_exists($this->destination_file))
440            {
441                @unlink($this->destination_file);
442            }
443
444            switch ($upload_mode)
445            {
446                case 'copy':
447
448                    if (!@copy($this->filename, $this->destination_file))
449                    {
450                        if (!@move_uploaded_file($this->filename, $this->destination_file))
451                        {
452                            $this->error[] = $this->language->lang($this->upload->error_prefix . 'GENERAL_UPLOAD_ERROR', $this->destination_file);
453                        }
454                    }
455
456                break;
457
458                case 'move':
459
460                    if (!@move_uploaded_file($this->filename, $this->destination_file))
461                    {
462                        if (!@copy($this->filename, $this->destination_file))
463                        {
464                            $this->error[] = $this->language->lang($this->upload->error_prefix . 'GENERAL_UPLOAD_ERROR', $this->destination_file);
465                        }
466                    }
467
468                break;
469
470                case 'local':
471
472                    if (!@copy($this->filename, $this->destination_file))
473                    {
474                        $this->error[] = $this->language->lang($this->upload->error_prefix . 'GENERAL_UPLOAD_ERROR', $this->destination_file);
475                    }
476
477                break;
478            }
479
480            // Remove temporary filename
481            if (file_exists($this->filename))
482            {
483                @unlink($this->filename);
484            }
485
486            if (count($this->error))
487            {
488                return false;
489            }
490
491            try
492            {
493                $this->filesystem->phpbb_chmod($this->destination_file, $chmod);
494            }
495            catch (\phpbb\filesystem\exception\filesystem_exception $e)
496            {
497                // Do nothing
498            }
499        }
500
501        // Try to get real filesize from destination folder
502        $this->filesize = ($this->get_filesize($this->destination_file)) ?: $this->filesize;
503
504        // Get mimetype of supplied file
505        $this->mimetype = $this->get_mimetype($this->destination_file);
506
507        if ($this->is_image() && !$skip_image_check)
508        {
509            $this->width = $this->height = 0;
510
511            $this->image_info = $this->imagesize->getImageSize($this->destination_file, $this->mimetype);
512
513            if ($this->image_info !== false)
514            {
515                $this->width = $this->image_info['width'];
516                $this->height = $this->image_info['height'];
517
518                // Check image type
519                $types = upload::image_types();
520
521                if (!isset($types[$this->image_info['type']]) || !in_array($this->extension, $types[$this->image_info['type']]))
522                {
523                    if (!isset($types[$this->image_info['type']]))
524                    {
525                        $this->error[] = $this->language->lang('IMAGE_FILETYPE_INVALID', $this->image_info['type'], $this->mimetype);
526                    }
527                    else
528                    {
529                        $this->error[] = $this->language->lang('IMAGE_FILETYPE_MISMATCH', $types[$this->image_info['type']][0], $this->extension);
530                    }
531                }
532
533                // Make sure the dimensions match a valid image
534                if (empty($this->width) || empty($this->height))
535                {
536                    $this->error[] = $this->language->lang('ATTACHED_IMAGE_NOT_IMAGE');
537                }
538            }
539            else
540            {
541                $this->error[] = $this->language->lang('UNABLE_GET_IMAGE_SIZE');
542            }
543        }
544
545        $this->file_moved = true;
546        $this->additional_checks();
547        unset($this->upload);
548
549        return true;
550    }
551
552    /**
553     * Performing additional checks
554     *
555     * @return bool False if issue was found, true if not
556     */
557    public function additional_checks()
558    {
559        if (!$this->file_moved)
560        {
561            return false;
562        }
563
564        // Filesize is too big or it's 0 if it was larger than the maxsize in the upload form
565        if ($this->upload->max_filesize && ($this->get('filesize') > $this->upload->max_filesize || $this->filesize == 0))
566        {
567            $max_filesize = get_formatted_filesize($this->upload->max_filesize, false);
568
569            $this->error[] = $this->language->lang($this->upload->error_prefix . 'WRONG_FILESIZE', $max_filesize['value'], $max_filesize['unit']);
570
571            return false;
572        }
573
574        if (!$this->upload->valid_dimensions($this))
575        {
576            $this->error[] = $this->language->lang($this->upload->error_prefix . 'WRONG_SIZE',
577                $this->language->lang('PIXELS', (int) $this->upload->min_width),
578                $this->language->lang('PIXELS', (int) $this->upload->min_height),
579                $this->language->lang('PIXELS', (int) $this->upload->max_width),
580                $this->language->lang('PIXELS', (int) $this->upload->max_height),
581                $this->language->lang('PIXELS', (int) $this->width),
582                $this->language->lang('PIXELS', (int) $this->height));
583
584            return false;
585        }
586
587        return true;
588    }
589}