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\plupload; 15 16/** 17* This class handles all server-side plupload functions 18*/ 19class plupload 20{ 21 /** 22 * @var string 23 */ 24 protected $phpbb_root_path; 25 26 /** 27 * @var \phpbb\config\config 28 */ 29 protected $config; 30 31 /** 32 * @var \phpbb\request\request_interface 33 */ 34 protected $request; 35 36 /** 37 * @var \phpbb\user 38 */ 39 protected $user; 40 41 /** 42 * @var \bantu\IniGetWrapper\IniGetWrapper 43 */ 44 protected $php_ini; 45 46 /** 47 * @var \phpbb\mimetype\guesser 48 */ 49 protected $mimetype_guesser; 50 51 /** 52 * Final destination for uploaded files, i.e. the "files" directory. 53 * @var string 54 */ 55 protected $upload_directory; 56 57 /** 58 * Temporary upload directory for plupload uploads. 59 * @var string 60 */ 61 protected $temporary_directory; 62 63 /** 64 * Constructor. 65 * 66 * @param string $phpbb_root_path 67 * @param \phpbb\config\config $config 68 * @param \phpbb\request\request_interface $request 69 * @param \phpbb\user $user 70 * @param \bantu\IniGetWrapper\IniGetWrapper $php_ini 71 * @param \phpbb\mimetype\guesser $mimetype_guesser 72 */ 73 public function __construct($phpbb_root_path, \phpbb\config\config $config, \phpbb\request\request_interface $request, \phpbb\user $user, \bantu\IniGetWrapper\IniGetWrapper $php_ini, \phpbb\mimetype\guesser $mimetype_guesser) 74 { 75 $this->phpbb_root_path = $phpbb_root_path; 76 $this->config = $config; 77 $this->request = $request; 78 $this->user = $user; 79 $this->php_ini = $php_ini; 80 $this->mimetype_guesser = $mimetype_guesser; 81 82 $this->set_default_directories(); 83 } 84 85 /** 86 * Plupload allows for chunking so we must check for that and assemble 87 * the whole file first before performing any checks on it. 88 * 89 * @param string $form_name The name of the file element in the upload form 90 * 91 * @return array|null null if there are no chunks to piece together 92 * otherwise array containing the path to the 93 * pieced-together file and its size 94 */ 95 public function handle_upload($form_name) 96 { 97 $chunks_expected = $this->request->variable('chunks', 0); 98 99 // If chunking is disabled or we are not using plupload, just return 100 // and handle the file as usual 101 if ($chunks_expected < 2) 102 { 103 return; 104 } 105 106 $file_name = $this->request->variable('name', ''); 107 $chunk = $this->request->variable('chunk', 0); 108 109 $this->user->add_lang('plupload'); 110 $this->prepare_temporary_directory(); 111 112 $file_path = $this->temporary_filepath($file_name); 113 $this->integrate_uploaded_file($form_name, $chunk, $file_path); 114 115 // If we are done with all the chunks, strip the .part suffix and then 116 // handle the resulting file as normal, otherwise die and await the 117 // next chunk. 118 if ($chunk == $chunks_expected - 1) 119 { 120 rename("{$file_path}.part", $file_path); 121 122 // Reset upload directories to defaults once completed 123 $this->set_default_directories(); 124 125 // Need to modify some of the $_FILES values to reflect the new file 126 return array( 127 'tmp_name' => $file_path, 128 'name' => $this->request->variable('real_filename', '', true), 129 'size' => filesize($file_path), 130 'type' => $this->mimetype_guesser->guess($file_path, $file_name), 131 ); 132 } 133 else 134 { 135 $json_response = new \phpbb\json_response(); 136 $json_response->send(array( 137 'jsonrpc' => '2.0', 138 'id' => 'id', 139 'result' => null, 140 )); 141 } 142 } 143 144 /** 145 * Fill in the plupload configuration options in the template 146 * 147 * @param \phpbb\cache\service $cache 148 * @param \phpbb\template\template $template 149 * @param string $s_action The URL to submit the POST data to 150 * @param int $forum_id The ID of the forum 151 * @param int $max_files Maximum number of files allowed. 0 for unlimited. 152 * 153 * @return null 154 */ 155 public function configure(\phpbb\cache\service $cache, \phpbb\template\template $template, $s_action, $forum_id, $max_files) 156 { 157 $filters = $this->generate_filter_string($cache, $forum_id); 158 $chunk_size = $this->get_chunk_size(); 159 $resize = $this->generate_resize_string(); 160 161 $template->assign_vars(array( 162 'S_RESIZE' => $resize, 163 'S_PLUPLOAD' => true, 164 'FILTERS' => $filters, 165 'CHUNK_SIZE' => $chunk_size, 166 'S_PLUPLOAD_URL' => htmlspecialchars_decode($s_action, ENT_COMPAT), 167 'MAX_ATTACHMENTS' => $max_files, 168 'ATTACH_ORDER' => ($this->config['display_order']) ? 'asc' : 'desc', 169 'L_TOO_MANY_ATTACHMENTS' => $this->user->lang('TOO_MANY_ATTACHMENTS', $max_files), 170 )); 171 172 $this->user->add_lang('plupload'); 173 } 174 175 /** 176 * Checks whether the page request was sent by plupload or not 177 * 178 * @return bool 179 */ 180 public function is_active() 181 { 182 return $this->request->header('X-PHPBB-USING-PLUPLOAD', false); 183 } 184 185 /** 186 * Returns whether the current HTTP request is a multipart request. 187 * 188 * @return bool 189 */ 190 public function is_multipart() 191 { 192 $content_type = $this->request->server('CONTENT_TYPE'); 193 194 return strpos($content_type, 'multipart') === 0; 195 } 196 197 /** 198 * Sends an error message back to the client via JSON response 199 * 200 * @param int $code The error code 201 * @param string $msg The translation string of the message to be sent 202 * 203 * @return null 204 */ 205 public function emit_error($code, $msg) 206 { 207 $json_response = new \phpbb\json_response(); 208 $json_response->send(array( 209 'jsonrpc' => '2.0', 210 'id' => 'id', 211 'error' => array( 212 'code' => $code, 213 'message' => $this->user->lang($msg), 214 ), 215 )); 216 } 217 218 /** 219 * Looks at the list of allowed extensions and generates a string 220 * appropriate for use in configuring plupload with 221 * 222 * @param \phpbb\cache\service $cache Cache service object 223 * @param string $forum_id The forum identifier 224 * 225 * @return string 226 */ 227 public function generate_filter_string(\phpbb\cache\service $cache, $forum_id) 228 { 229 $groups = []; 230 $filters = []; 231 232 $attach_extensions = $cache->obtain_attach_extensions($forum_id); 233 unset($attach_extensions['_allowed_']); 234 235 // Re-arrange the extension array to $groups[$group_name][] 236 foreach ($attach_extensions as $extension => $extension_info) 237 { 238 $groups[$extension_info['group_name']]['extensions'][] = $extension; 239 $groups[$extension_info['group_name']]['max_file_size'] = (int) $extension_info['max_filesize']; 240 } 241 242 foreach ($groups as $group => $group_info) 243 { 244 $filters[] = sprintf( 245 "{title: '%s', extensions: '%s', max_file_size: %s}", 246 addslashes(ucfirst(strtolower($group))), 247 addslashes(implode(',', $group_info['extensions'])), 248 $group_info['max_file_size'] 249 ); 250 } 251 252 return implode(',', $filters); 253 } 254 255 /** 256 * Generates a string that is used to tell plupload to automatically resize 257 * files before uploading them. 258 * 259 * @return string 260 */ 261 public function generate_resize_string() 262 { 263 $resize = ''; 264 if ($this->config['img_max_height'] > 0 && $this->config['img_max_width'] > 0) 265 { 266 $preserve_headers_value = $this->config['img_strip_metadata'] ? 'false' : 'true'; 267 $resize = sprintf( 268 'resize: {width: %d, height: %d, quality: %d, preserve_headers: %s},', 269 (int) $this->config['img_max_width'], 270 (int) $this->config['img_max_height'], 271 (int) $this->config['img_quality'], 272 $preserve_headers_value 273 ); 274 } 275 276 return $resize; 277 } 278 279 /** 280 * Checks various php.ini values to determine the maximum chunk 281 * size a file should be split into for upload. 282 * 283 * The intention is to calculate a value which reflects whatever 284 * the most restrictive limit is set to. And to then set the chunk 285 * size to half that value, to ensure any required transfer overhead 286 * and POST data remains well within the limit. Or, if all of the 287 * limits are set to unlimited, the chunk size will also be unlimited. 288 * 289 * @return int 290 * 291 * @access public 292 */ 293 public function get_chunk_size() 294 { 295 $max = 0; 296 297 $limits = [ 298 $this->php_ini->getBytes('memory_limit'), 299 $this->php_ini->getBytes('upload_max_filesize'), 300 $this->php_ini->getBytes('post_max_size'), 301 ]; 302 303 foreach ($limits as $limit_type) 304 { 305 if ($limit_type > 0) 306 { 307 $max = ($max !== 0) ? min($limit_type, $max) : $limit_type; 308 } 309 } 310 311 return floor($max / 2); 312 } 313 314 protected function temporary_filepath($file_name) 315 { 316 // Must preserve the extension for plupload to work. 317 return sprintf( 318 '%s/%s_%s%s', 319 $this->temporary_directory, 320 $this->config['plupload_salt'], 321 md5($file_name), 322 \phpbb\files\filespec::get_extension($file_name) 323 ); 324 } 325 326 /** 327 * Checks whether the chunk we are about to deal with was actually uploaded 328 * by PHP and actually exists, if not, it generates an error 329 * 330 * @param string $form_name The name of the file in the form data 331 * @param int $chunk Chunk number 332 * @param string $file_path File path 333 * 334 * @return null 335 */ 336 protected function integrate_uploaded_file($form_name, $chunk, $file_path) 337 { 338 $is_multipart = $this->is_multipart(); 339 $upload = $this->request->file($form_name); 340 if ($is_multipart && (!isset($upload['tmp_name']) || !is_uploaded_file($upload['tmp_name']))) 341 { 342 $this->emit_error(103, 'PLUPLOAD_ERR_MOVE_UPLOADED'); 343 } 344 345 $tmp_file = $this->temporary_filepath($upload['tmp_name']); 346 347 if (!phpbb_is_writable($this->temporary_directory) || !move_uploaded_file($upload['tmp_name'], $tmp_file)) 348 { 349 $this->emit_error(103, 'PLUPLOAD_ERR_MOVE_UPLOADED'); 350 } 351 352 $out = fopen("{$file_path}.part", $chunk == 0 ? 'wb' : 'ab'); 353 if (!$out) 354 { 355 $this->emit_error(102, 'PLUPLOAD_ERR_OUTPUT'); 356 } 357 358 $in = fopen(($is_multipart) ? $tmp_file : 'php://input', 'rb'); 359 if (!$in) 360 { 361 $this->emit_error(101, 'PLUPLOAD_ERR_INPUT'); 362 } 363 364 while ($buf = fread($in, 4096)) 365 { 366 fwrite($out, $buf); 367 } 368 369 fclose($in); 370 fclose($out); 371 372 if ($is_multipart) 373 { 374 unlink($tmp_file); 375 } 376 } 377 378 /** 379 * Creates the temporary directory if it does not already exist. 380 * 381 * @return null 382 */ 383 protected function prepare_temporary_directory() 384 { 385 if (!file_exists($this->temporary_directory)) 386 { 387 mkdir($this->temporary_directory); 388 389 copy( 390 $this->upload_directory . '/index.htm', 391 $this->temporary_directory . '/index.htm' 392 ); 393 } 394 } 395 396 /** 397 * Sets the default directories for uploads 398 * 399 * @return null 400 */ 401 protected function set_default_directories() 402 { 403 $this->upload_directory = $this->phpbb_root_path . $this->config['upload_path']; 404 $this->temporary_directory = $this->upload_directory . '/plupload'; 405 } 406 407 /** 408 * Sets the upload directories to the specified paths 409 * 410 * @param string $upload_directory Upload directory 411 * @param string $temporary_directory Temporary directory 412 * 413 * @return null 414 */ 415 public function set_upload_directories($upload_directory, $temporary_directory) 416 { 417 $this->upload_directory = $upload_directory; 418 $this->temporary_directory = $temporary_directory; 419 } 420} 421