Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
36.64% |
48 / 131 |
|
43.75% |
7 / 16 |
CRAP | |
0.00% |
0 / 1 |
filespec_storage | |
36.64% |
48 / 131 |
|
43.75% |
7 / 16 |
1244.08 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
set_upload_ary | |
94.44% |
17 / 18 |
|
0.00% |
0 / 1 |
8.01 | |||
set_upload_namespace | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
init_error | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
set_error | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
clean_filename | |
26.32% |
5 / 19 |
|
0.00% |
0 / 1 |
33.60 | |||
get | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
3.33 | |||
is_image | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
is_uploaded | |
66.67% |
4 / 6 |
|
0.00% |
0 / 1 |
10.37 | |||
remove | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
get_extension | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
get_mimetype | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
20 | |||
get_filesize | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
check_content | |
20.00% |
2 / 10 |
|
0.00% |
0 / 1 |
17.80 | |||
move_file | |
0.00% |
0 / 35 |
|
0.00% |
0 / 1 |
272 | |||
additional_checks | |
0.00% |
0 / 16 |
|
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 | |
14 | namespace phpbb\files; |
15 | |
16 | use 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 | class 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 | } |