friendship ended with social-app. php is my new best friend
1<?php 2 3namespace React\Http\Io; 4 5use Psr\Http\Message\ServerRequestInterface; 6 7/** 8 * [Internal] Parses a string body with "Content-Type: multipart/form-data" into structured data 9 * 10 * This is use internally to parse incoming request bodies into structured data 11 * that resembles PHP's `$_POST` and `$_FILES` superglobals. 12 * 13 * @internal 14 * @link https://tools.ietf.org/html/rfc7578 15 * @link https://tools.ietf.org/html/rfc2046#section-5.1.1 16 */ 17final class MultipartParser 18{ 19 /** 20 * @var ServerRequestInterface|null 21 */ 22 private $request; 23 24 /** 25 * @var int|null 26 */ 27 private $maxFileSize; 28 29 /** 30 * Based on $maxInputVars and $maxFileUploads 31 * 32 * @var int 33 */ 34 private $maxMultipartBodyParts; 35 36 /** 37 * ini setting "max_input_vars" 38 * 39 * Does not exist in PHP < 5.3.9 or HHVM, so assume PHP's default 1000 here. 40 * 41 * @var int 42 * @link http://php.net/manual/en/info.configuration.php#ini.max-input-vars 43 */ 44 private $maxInputVars = 1000; 45 46 /** 47 * ini setting "max_input_nesting_level" 48 * 49 * Does not exist in HHVM, but assumes hard coded to 64 (PHP's default). 50 * 51 * @var int 52 * @link http://php.net/manual/en/info.configuration.php#ini.max-input-nesting-level 53 */ 54 private $maxInputNestingLevel = 64; 55 56 /** 57 * ini setting "upload_max_filesize" 58 * 59 * @var int 60 */ 61 private $uploadMaxFilesize; 62 63 /** 64 * ini setting "max_file_uploads" 65 * 66 * Additionally, setting "file_uploads = off" effectively sets this to zero. 67 * 68 * @var int 69 */ 70 private $maxFileUploads; 71 72 private $multipartBodyPartCount = 0; 73 private $postCount = 0; 74 private $filesCount = 0; 75 private $emptyCount = 0; 76 private $cursor = 0; 77 78 /** 79 * @param int|string|null $uploadMaxFilesize 80 * @param int|null $maxFileUploads 81 */ 82 public function __construct($uploadMaxFilesize = null, $maxFileUploads = null) 83 { 84 $var = \ini_get('max_input_vars'); 85 if ($var !== false) { 86 $this->maxInputVars = (int)$var; 87 } 88 $var = \ini_get('max_input_nesting_level'); 89 if ($var !== false) { 90 $this->maxInputNestingLevel = (int)$var; 91 } 92 93 if ($uploadMaxFilesize === null) { 94 $uploadMaxFilesize = \ini_get('upload_max_filesize'); 95 } 96 97 $this->uploadMaxFilesize = IniUtil::iniSizeToBytes($uploadMaxFilesize); 98 $this->maxFileUploads = $maxFileUploads === null ? (\ini_get('file_uploads') === '' ? 0 : (int)\ini_get('max_file_uploads')) : (int)$maxFileUploads; 99 100 $this->maxMultipartBodyParts = $this->maxInputVars + $this->maxFileUploads; 101 } 102 103 public function parse(ServerRequestInterface $request) 104 { 105 $contentType = $request->getHeaderLine('content-type'); 106 if(!\preg_match('/boundary="?(.*?)"?$/', $contentType, $matches)) { 107 return $request; 108 } 109 110 $this->request = $request; 111 $this->parseBody('--' . $matches[1], (string)$request->getBody()); 112 113 $request = $this->request; 114 $this->request = null; 115 $this->multipartBodyPartCount = 0; 116 $this->cursor = 0; 117 $this->postCount = 0; 118 $this->filesCount = 0; 119 $this->emptyCount = 0; 120 $this->maxFileSize = null; 121 122 return $request; 123 } 124 125 private function parseBody($boundary, $buffer) 126 { 127 $len = \strlen($boundary); 128 129 // ignore everything before initial boundary (SHOULD be empty) 130 $this->cursor = \strpos($buffer, $boundary . "\r\n"); 131 132 while ($this->cursor !== false) { 133 // search following boundary (preceded by newline) 134 // ignore last if not followed by boundary (SHOULD end with "--") 135 $this->cursor += $len + 2; 136 $end = \strpos($buffer, "\r\n" . $boundary, $this->cursor); 137 if ($end === false) { 138 break; 139 } 140 141 // parse one part and continue searching for next 142 $this->parsePart(\substr($buffer, $this->cursor, $end - $this->cursor)); 143 $this->cursor = $end; 144 145 if (++$this->multipartBodyPartCount > $this->maxMultipartBodyParts) { 146 break; 147 } 148 } 149 } 150 151 private function parsePart($chunk) 152 { 153 $pos = \strpos($chunk, "\r\n\r\n"); 154 if ($pos === false) { 155 return; 156 } 157 158 $headers = $this->parseHeaders((string)substr($chunk, 0, $pos)); 159 $body = (string)\substr($chunk, $pos + 4); 160 161 if (!isset($headers['content-disposition'])) { 162 return; 163 } 164 165 $name = $this->getParameterFromHeader($headers['content-disposition'], 'name'); 166 if ($name === null) { 167 return; 168 } 169 170 $filename = $this->getParameterFromHeader($headers['content-disposition'], 'filename'); 171 if ($filename !== null) { 172 $this->parseFile( 173 $name, 174 $filename, 175 isset($headers['content-type'][0]) ? $headers['content-type'][0] : null, 176 $body 177 ); 178 } else { 179 $this->parsePost($name, $body); 180 } 181 } 182 183 private function parseFile($name, $filename, $contentType, $contents) 184 { 185 $file = $this->parseUploadedFile($filename, $contentType, $contents); 186 if ($file === null) { 187 return; 188 } 189 190 $this->request = $this->request->withUploadedFiles($this->extractPost( 191 $this->request->getUploadedFiles(), 192 $name, 193 $file 194 )); 195 } 196 197 private function parseUploadedFile($filename, $contentType, $contents) 198 { 199 $size = \strlen($contents); 200 201 // no file selected (zero size and empty filename) 202 if ($size === 0 && $filename === '') { 203 // ignore excessive number of empty file uploads 204 if (++$this->emptyCount + $this->filesCount > $this->maxInputVars) { 205 return; 206 } 207 208 return new UploadedFile( 209 new BufferedBody(''), 210 $size, 211 \UPLOAD_ERR_NO_FILE, 212 $filename, 213 $contentType 214 ); 215 } 216 217 // ignore excessive number of file uploads 218 if (++$this->filesCount > $this->maxFileUploads) { 219 return; 220 } 221 222 // file exceeds "upload_max_filesize" ini setting 223 if ($size > $this->uploadMaxFilesize) { 224 return new UploadedFile( 225 new BufferedBody(''), 226 $size, 227 \UPLOAD_ERR_INI_SIZE, 228 $filename, 229 $contentType 230 ); 231 } 232 233 // file exceeds MAX_FILE_SIZE value 234 if ($this->maxFileSize !== null && $size > $this->maxFileSize) { 235 return new UploadedFile( 236 new BufferedBody(''), 237 $size, 238 \UPLOAD_ERR_FORM_SIZE, 239 $filename, 240 $contentType 241 ); 242 } 243 244 return new UploadedFile( 245 new BufferedBody($contents), 246 $size, 247 \UPLOAD_ERR_OK, 248 $filename, 249 $contentType 250 ); 251 } 252 253 private function parsePost($name, $value) 254 { 255 // ignore excessive number of post fields 256 if (++$this->postCount > $this->maxInputVars) { 257 return; 258 } 259 260 $this->request = $this->request->withParsedBody($this->extractPost( 261 $this->request->getParsedBody(), 262 $name, 263 $value 264 )); 265 266 if (\strtoupper($name) === 'MAX_FILE_SIZE') { 267 $this->maxFileSize = (int)$value; 268 269 if ($this->maxFileSize === 0) { 270 $this->maxFileSize = null; 271 } 272 } 273 } 274 275 private function parseHeaders($header) 276 { 277 $headers = array(); 278 279 foreach (\explode("\r\n", \trim($header)) as $line) { 280 $parts = \explode(':', $line, 2); 281 if (!isset($parts[1])) { 282 continue; 283 } 284 285 $key = \strtolower(trim($parts[0])); 286 $values = \explode(';', $parts[1]); 287 $values = \array_map('trim', $values); 288 $headers[$key] = $values; 289 } 290 291 return $headers; 292 } 293 294 private function getParameterFromHeader(array $header, $parameter) 295 { 296 foreach ($header as $part) { 297 if (\preg_match('/' . $parameter . '="?(.*?)"?$/', $part, $matches)) { 298 return $matches[1]; 299 } 300 } 301 302 return null; 303 } 304 305 private function extractPost($postFields, $key, $value) 306 { 307 $chunks = \explode('[', $key); 308 if (\count($chunks) == 1) { 309 $postFields[$key] = $value; 310 return $postFields; 311 } 312 313 // ignore this key if maximum nesting level is exceeded 314 if (isset($chunks[$this->maxInputNestingLevel])) { 315 return $postFields; 316 } 317 318 $chunkKey = \rtrim($chunks[0], ']'); 319 $parent = &$postFields; 320 for ($i = 1; isset($chunks[$i]); $i++) { 321 $previousChunkKey = $chunkKey; 322 323 if ($previousChunkKey === '') { 324 $parent[] = array(); 325 \end($parent); 326 $parent = &$parent[\key($parent)]; 327 } else { 328 if (!isset($parent[$previousChunkKey]) || !\is_array($parent[$previousChunkKey])) { 329 $parent[$previousChunkKey] = array(); 330 } 331 $parent = &$parent[$previousChunkKey]; 332 } 333 334 $chunkKey = \rtrim($chunks[$i], ']'); 335 } 336 337 if ($chunkKey === '') { 338 $parent[] = $value; 339 } else { 340 $parent[$chunkKey] = $value; 341 } 342 343 return $postFields; 344 } 345}