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}