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