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\filesystem\filesystem_interface;
17use phpbb\language\language;
18use phpbb\request\request_interface;
19
20/**
21 * File upload class
22 * Init class (all parameters optional and able to be set/overwritten separately) - scope is global and valid for all uploads
23 */
24class upload
25{
26	/** @var array Allowed file extensions */
27	public $allowed_extensions = array();
28
29	/** @var array Disallowed content */
30	protected $disallowed_content = array('body', 'head', 'html', 'img', 'plaintext', 'a href', 'pre', 'script', 'table', 'title');
31
32	/** @var int Maximum filesize */
33	public $max_filesize = 0;
34
35	/** @var int Minimum width of images */
36	public $min_width = 0;
37
38	/** @var int Minimum height of images */
39	public $min_height = 0;
40
41	/** @var int Maximum width of images */
42	public $max_width = 0;
43
44	/** @var int Maximum height of images */
45	public $max_height = 0;
46
47	/** @var string Prefix for language variables of errors */
48	public $error_prefix = '';
49
50	/** @var int Timeout for remote upload */
51	public $upload_timeout = 6;
52
53	/** @var filesystem_interface */
54	protected $filesystem;
55
56	/** @var \phpbb\files\factory Files factory */
57	protected $factory;
58
59	/** @var \bantu\IniGetWrapper\IniGetWrapper ini_get() wrapper */
60	protected $php_ini;
61
62	/** @var language Language class */
63	protected $language;
64
65	/** @var request_interface Request class */
66	protected $request;
67
68	/**
69	 * Init file upload class.
70	 *
71	 * @param filesystem_interface $filesystem
72	 * @param factory $factory Files factory
73	 * @param language $language Language class
74	 * @param \bantu\IniGetWrapper\IniGetWrapper $php_ini ini_get() wrapper
75	 * @param request_interface $request Request class
76	 */
77	public function __construct(filesystem_interface $filesystem, factory $factory, language $language, \bantu\IniGetWrapper\IniGetWrapper $php_ini, request_interface $request)
78	{
79		$this->filesystem = $filesystem;
80		$this->factory = $factory;
81		$this->language = $language;
82		$this->php_ini = $php_ini;
83		$this->request = $request;
84	}
85
86	/**
87	 * Reset vars
88	 */
89	public function reset_vars()
90	{
91		$this->max_filesize = 0;
92		$this->min_width = $this->min_height = $this->max_width = $this->max_height = 0;
93		$this->error_prefix = '';
94		$this->allowed_extensions = array();
95		$this->disallowed_content = array();
96	}
97
98	/**
99	 * Set allowed extensions
100	 *
101	 * @param array $allowed_extensions Allowed file extensions
102	 *
103	 * @return \phpbb\files\upload This instance of upload
104	 */
105	public function set_allowed_extensions($allowed_extensions)
106	{
107		if ($allowed_extensions !== false && is_array($allowed_extensions))
108		{
109			$this->allowed_extensions = $allowed_extensions;
110		}
111
112		return $this;
113	}
114
115	/**
116	 * Set allowed dimensions
117	 *
118	 * @param int $min_width Minimum image width
119	 * @param int $min_height Minimum image height
120	 * @param int $max_width Maximum image width
121	 * @param int $max_height Maximum image height
122	 *
123	 * @return \phpbb\files\upload This instance of upload
124	 */
125	public function set_allowed_dimensions($min_width, $min_height, $max_width, $max_height)
126	{
127		$this->min_width = (int) $min_width;
128		$this->min_height = (int) $min_height;
129		$this->max_width = (int) $max_width;
130		$this->max_height = (int) $max_height;
131
132		return $this;
133	}
134
135	/**
136	 * Set maximum allowed file size
137	 *
138	 * @param int $max_filesize Maximum file size
139	 *
140	 * @return \phpbb\files\upload This instance of upload
141	 */
142	public function set_max_filesize($max_filesize)
143	{
144		if ($max_filesize !== false && (int) $max_filesize)
145		{
146			$this->max_filesize = (int) $max_filesize;
147		}
148
149		return $this;
150	}
151
152	/**
153	 * Set disallowed strings
154	 *
155	 * @param array $disallowed_content Disallowed content
156	 *
157	 * @return \phpbb\files\upload This instance of upload
158	 */
159	public function set_disallowed_content($disallowed_content)
160	{
161		if ($disallowed_content !== false && is_array($disallowed_content))
162		{
163			$this->disallowed_content = array_diff($disallowed_content, array(''));
164		}
165
166		return $this;
167	}
168
169	/**
170	 * Set error prefix
171	 *
172	 * @param string $error_prefix Prefix for language variables of errors
173	 *
174	 * @return \phpbb\files\upload This instance of upload
175	 */
176	public function set_error_prefix($error_prefix)
177	{
178		$this->error_prefix = $error_prefix;
179
180		return $this;
181	}
182
183	/**
184	 * Handle upload based on type
185	 *
186	 * @param string $type Upload type
187	 *
188	 * @return \phpbb\files\filespec|bool A filespec instance if upload was
189	 *		successful, false if there were issues or the type is not supported
190	 */
191	public function handle_upload($type)
192	{
193		$args = func_get_args();
194		array_shift($args);
195		$type_class = $this->factory->get($type)
196			->set_upload($this);
197
198		return (is_object($type_class)) ? call_user_func_array(array($type_class, 'upload'), $args) : false;
199	}
200
201	/**
202	 * Assign internal error
203	 *
204	 * @param string $errorcode Error code to assign
205	 *
206	 * @return string Error string
207	 * @access public
208	 */
209	public function assign_internal_error($errorcode)
210	{
211		switch ($errorcode)
212		{
213			case UPLOAD_ERR_INI_SIZE:
214				$max_filesize = $this->php_ini->getString('upload_max_filesize');
215				$unit = 'MB';
216
217				if (!empty($max_filesize))
218				{
219					$unit = strtolower(substr($max_filesize, -1, 1));
220					$max_filesize = (int) $max_filesize;
221
222					$unit = ($unit == 'k') ? 'KB' : (($unit == 'g') ? 'GB' : 'MB');
223				}
224
225				$error = (empty($max_filesize)) ? $this->language->lang($this->error_prefix . 'PHP_SIZE_NA') : $this->language->lang($this->error_prefix . 'PHP_SIZE_OVERRUN', $max_filesize, $this->language->lang($unit));
226			break;
227
228			case UPLOAD_ERR_FORM_SIZE:
229				$max_filesize = get_formatted_filesize($this->max_filesize, false);
230
231				$error = $this->language->lang($this->error_prefix . 'WRONG_FILESIZE', $max_filesize['value'], $max_filesize['unit']);
232			break;
233
234			case UPLOAD_ERR_PARTIAL:
235				$error = $this->language->lang($this->error_prefix . 'PARTIAL_UPLOAD');
236			break;
237
238			case UPLOAD_ERR_NO_FILE:
239				$error = $this->language->lang($this->error_prefix . 'NOT_UPLOADED');
240			break;
241
242			case UPLOAD_ERR_NO_TMP_DIR:
243			case UPLOAD_ERR_CANT_WRITE:
244				$error = $this->language->lang($this->error_prefix . 'NO_TEMP_DIR');
245			break;
246
247			case UPLOAD_ERR_EXTENSION:
248				$error = $this->language->lang($this->error_prefix . 'PHP_UPLOAD_STOPPED');
249			break;
250
251			default:
252				$error = false;
253			break;
254		}
255
256		return $error;
257	}
258
259	/**
260	 * Perform common file checks
261	 *
262	 * @param filespec $file Instance of filespec class
263	 */
264	public function common_checks($file)
265	{
266		// Filesize is too big or it's 0 if it was larger than the maxsize in the upload form
267		if ($this->max_filesize && ($file->get('filesize') > $this->max_filesize || $file->get('filesize') == 0))
268		{
269			$max_filesize = get_formatted_filesize($this->max_filesize, false);
270
271			$file->error[] = $this->language->lang($this->error_prefix . 'WRONG_FILESIZE', $max_filesize['value'], $max_filesize['unit']);
272		}
273
274		// check Filename
275		if (preg_match("#[\\/:*?\"<>|]#i", $file->get('realname')))
276		{
277			$file->error[] = $this->language->lang($this->error_prefix . 'INVALID_FILENAME', $file->get('realname'));
278		}
279
280		// Invalid Extension
281		if (!$this->valid_extension($file))
282		{
283			$file->error[] = $this->language->lang($this->error_prefix . 'DISALLOWED_EXTENSION', $file->get('extension'));
284		}
285
286		// MIME Sniffing
287		if (!$this->valid_content($file))
288		{
289			$file->error[] = $this->language->lang($this->error_prefix . 'DISALLOWED_CONTENT');
290		}
291	}
292
293	/**
294	 * Check for allowed extension
295	 *
296	 * @param filespec $file Instance of filespec class
297	 *
298	 * @return bool True if extension is allowed, false if not
299	 */
300	public function valid_extension($file)
301	{
302		return (in_array($file->get('extension'), $this->allowed_extensions)) ? true : false;
303	}
304
305	/**
306	 * Check for allowed dimension
307	 *
308	 * @param filespec $file Instance of filespec class
309	 *
310	 * @return bool True if dimensions are valid or no constraints set, false
311	 *			if not
312	 */
313	public function valid_dimensions($file)
314	{
315		if (!$this->max_width && !$this->max_height && !$this->min_width && !$this->min_height)
316		{
317			return true;
318		}
319
320		if (($file->get('width') > $this->max_width && $this->max_width) ||
321			($file->get('height') > $this->max_height && $this->max_height) ||
322			($file->get('width') < $this->min_width && $this->min_width) ||
323			($file->get('height') < $this->min_height && $this->min_height))
324		{
325			return false;
326		}
327
328		return true;
329	}
330
331	/**
332	 * Check if form upload is valid
333	 *
334	 * @param string $form_name Name of form
335	 *
336	 * @return bool True if form upload is valid, false if not
337	 */
338	public function is_valid($form_name)
339	{
340		$upload = $this->request->file($form_name);
341
342		return (!empty($upload) && $upload['name'] !== 'none');
343	}
344
345
346	/**
347	 * Check for bad content (IE mime-sniffing)
348	 *
349	 * @param filespec $file Instance of filespec class
350	 *
351	 * @return bool True if content is valid, false if not
352	 */
353	public function valid_content($file)
354	{
355		return ($file->check_content($this->disallowed_content));
356	}
357
358	/**
359	 * Get image type/extension mapping
360	 *
361	 * @return array Array containing the image types and their extensions
362	 */
363	static public function image_types()
364	{
365		$result = [
366			IMAGETYPE_GIF		=> ['gif'],
367			IMAGETYPE_JPEG		=> ['jpg', 'jpeg'],
368			IMAGETYPE_PNG		=> ['png'],
369			IMAGETYPE_SWF		=> ['swf'],
370			IMAGETYPE_PSD		=> ['psd'],
371			IMAGETYPE_BMP		=> ['bmp'],
372			IMAGETYPE_TIFF_II	=> ['tif', 'tiff'],
373			IMAGETYPE_TIFF_MM	=> ['tif', 'tiff'],
374			IMAGETYPE_JPC		=> ['jpg', 'jpeg'],
375			IMAGETYPE_JP2		=> ['jpg', 'jpeg'],
376			IMAGETYPE_JPX		=> ['jpg', 'jpeg'],
377			IMAGETYPE_JB2		=> ['jpg', 'jpeg'],
378			IMAGETYPE_IFF		=> ['iff'],
379			IMAGETYPE_WBMP		=> ['wbmp'],
380			IMAGETYPE_XBM		=> ['xbm'],
381			IMAGETYPE_WEBP		=> ['webp'],
382		];
383
384		if (defined('IMAGETYPE_SWC'))
385		{
386			$result[IMAGETYPE_SWC] = ['swc'];
387		}
388
389		return $result;
390	}
391}
392