Completed
Push — master ( f4a74c...61e189 )
by Nazar
04:13
created

Data_and_files::parse_multipart_into_parts()   C

Complexity

Conditions 9
Paths 19

Size

Total Lines 83
Code Lines 50

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
c 1
b 1
f 0
dl 0
loc 83
rs 5.48
cc 9
eloc 50
nc 19
nop 2

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * @package   CleverStyle CMS
4
 * @author    Nazar Mokrynskyi <[email protected]>
5
 * @copyright Copyright (c) 2016, Nazar Mokrynskyi
6
 * @license   MIT License, see license.txt
7
 */
8
namespace cs\Request;
9
use
10
	cs\ExitException;
11
12
trait Data_and_files {
13
	/**
14
	 * Data array, similar to `$_POST`
15
	 *
16
	 * @var array
17
	 */
18
	public $data;
19
	/**
20
	 * Normalized files array
21
	 *
22
	 * Each file item can be either single file or array of files (in contrast with native PHP arrays where each field like `name` become an array) with keys
23
	 * `name`, `type`, `size`, `tmp_name`, `stream` and `error`
24
	 *
25
	 * `name`, `type`, `size` and `error` keys are similar to native PHP fields in `$_FILES`; `tmp_name` might not be temporary file, but file descriptor
26
	 * wrapper like `request-file:///file` instead and `stream` is resource like obtained with `fopen('/tmp/xyz', 'rb')`
27
	 *
28
	 * @var array[]
29
	 */
30
	public $files;
31
	/**
32
	 * Data stream resource, similar to `fopen('php://input', 'rb')`
33
	 *
34
	 * Make sure you're controlling position in stream where you read something, if code in some other place might seek on this stream
35
	 *
36
	 * Stream is read-only
37
	 *
38
	 * @var null|resource
39
	 */
40
	public $data_stream;
41
	/**
42
	 * `$this->init_server()` assumed to be called already
43
	 *
44
	 * @param array                $data        Typically `$_POST`
45
	 * @param array[]              $files       Typically `$_FILES`; might be like native PHP array `$_FILES` or normalized; each file item MUST contain keys
46
	 *                                          `name`, `type`, `size`, `error` and at least one of `tmp_name` or `stream`
47
	 * @param null|resource|string $data_stream String, like `php://input` or resource, like `fopen('php://input', 'rb')` with request body, will be parsed for
48
	 *                                          data and files if necessary
49
	 * @param bool                 $copy_stream Sometimes data stream can only being read once (like most of times with `php://input`), so it is necessary to
50
	 *                                          copy it and store its contents for longer period of time
51
	 *
52
	 * @throws ExitException
53
	 */
54
	function init_data_and_files ($data = [], $files = [], $data_stream = null, $copy_stream = true) {
55
		if (is_resource($this->data_stream)) {
56
			fclose($this->data_stream);
57
		}
58
		$this->data  = $data;
59
		$this->files = $this->normalize_files($files);
60
		$data_stream = is_string($data_stream) ? fopen($data_stream, 'rb') : $data_stream;
61
		if ($copy_stream && is_resource($data_stream)) {
62
			$this->data_stream = fopen('php://temp', 'w+b');
63
			stream_copy_to_stream($data_stream, $this->data_stream);
64
			rewind($this->data_stream);
65
			fclose($data_stream);
66
		} else {
67
			$this->data_stream = $data_stream;
68
		}
69
		$this->parse_data_stream();
70
		// Hack: for compatibility we'll override $_POST since it might be filled during parsing
71
		$_POST = $this->data;
72
	}
73
	/**
74
	 * Get data item by name
75
	 *
76
	 * @param string|string[] $name
77
	 *
78
	 * @return false|mixed|mixed[] Data if exists or `false` otherwise (in case if `$name` is an array even one missing key will cause the whole thing to fail)
79
	 */
80
	function data ($name) {
81
		if (is_array($name)) {
82
			foreach ($name as &$n) {
83
				if (!isset($this->data[$n])) {
84
					return false;
85
				}
86
				$n = $this->data[$n];
87
			}
88
			return $name;
89
		}
90
		/** @noinspection OffsetOperationsInspection */
91
		return isset($this->data[$name]) ? $this->data[$name] : false;
92
	}
93
	/**
94
	 * @param array[] $files
95
	 * @param string  $file_path
96
	 *
97
	 * @return array[]
98
	 */
99
	protected function normalize_files ($files, $file_path = '') {
100
		if (!isset($files['name'])) {
101
			foreach ($files as $field => &$file) {
102
				$file = $this->normalize_files($file, "$file_path/$field");
103
			}
104
			return $files;
105
		}
106
		if (is_array($files['name'])) {
107
			$result = [];
108
			foreach (array_keys($files['name']) as $index) {
109
				$result[] = $this->normalize_file(
110
					[
111
						'name'     => $files['name'][$index],
112
						'type'     => $files['type'][$index],
113
						'size'     => $files['size'][$index],
114
						'tmp_name' => @$files['tmp_name'][$index] ?: null,
115
						'stream'   => @$files['stream'][$index] ?: null,
116
						'error'    => $files['error'][$index]
117
					],
118
					$file_path
119
				);
120
			}
121
			return $result;
122
		} else {
123
			return $this->normalize_file($files, $file_path);
124
		}
125
	}
126
	/**
127
	 * @param array  $file
128
	 * @param string $file_path
129
	 *
130
	 * @return array
131
	 */
132
	protected function normalize_file ($file, $file_path) {
133
		$file += [
134
			'tmp_name' => null,
135
			'stream'   => null
136
		];
137
		if (isset($file['tmp_name']) && $file['stream'] === null) {
138
			$file['stream'] = fopen($file['tmp_name'], 'rb');
139
		}
140
		if (isset($file['stream']) && !$file['tmp_name']) {
141
			$file['tmp_name'] = "request-file://".$file_path;
142
		}
143
		if ($file['tmp_name'] === null && $file['stream'] === null) {
144
			$file['error'] = UPLOAD_ERR_NO_FILE;
145
		}
146
		return $file;
147
	}
148
	/**
149
	 * Parsing request body for following Content-Type: `application/json`, `application/x-www-form-urlencoded` and `multipart/form-data`
150
	 *
151
	 * @throws ExitException
152
	 */
153
	protected function parse_data_stream () {
154
		if ($this->data || $this->files) {
155
			return;
156
		}
157
		$this->data   = [];
158
		$this->files  = [];
159
		$content_type = $this->header('content-type');
160
		/**
161
		 * application/json
162
		 */
163
		if (preg_match('#^application/([^+\s]+\+)?json#', $content_type)) {
164
			$this->data = _json_decode(stream_get_contents($this->data_stream)) ?: [];
165
			return;
166
		}
167
		/**
168
		 * application/x-www-form-urlencoded
169
		 */
170
		if (strpos($content_type, 'application/x-www-form-urlencoded') === 0) {
171
			@parse_str(stream_get_contents($this->data_stream), $this->data);
172
			return;
173
		}
174
		/**
175
		 * multipart/form-data
176
		 */
177
		if (preg_match('#multipart/form-data;.*boundary="?([^;"]{1,70})(?:"|;|$)#Ui', $content_type, $matches)) {
178
			$parts = $this->parse_multipart_into_parts($this->data_stream, trim($matches[1])) ?: [];
179
			list($this->data, $files) = $this->parse_multipart_analyze_parts($this->data_stream, $parts);
180
			$this->files = $this->normalize_files($files);
181
		}
182
	}
183
	/**
184
	 * Parse content stream
185
	 *
186
	 * @param resource $stream
187
	 * @param string   $boundary
188
	 *
189
	 * @return array[]|false
1 ignored issue
show
Documentation introduced by
Should the return type not be false|array? Also, consider making the array more specific, something like array<String>, or String[].

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

If the return type contains the type array, this check recommends the use of a more specific type like String[] or array<String>.

Loading history...
190
	 *
191
	 * @throws ExitException
192
	 */
193
	protected function parse_multipart_into_parts ($stream, $boundary) {
194
		$parts    = [];
195
		$crlf     = "\r\n";
196
		$position = 0;
197
		$body     = '';
198
		$result   = $this->parse_multipart_find($stream, $body, "--$boundary$crlf");
199
		if ($result === false) {
200
			return false;
201
		}
202
		list($offset, $body) = $result;
203
		/**
204
		 * strlen doesn't take into account trailing CRLF since we'll need it in loop below
205
		 */
206
		$position += $offset + strlen("--$boundary");
207
		$body = substr($body, strlen("--$boundary"));
208
		$body .= fread($stream, 1024);
209
		/**
210
		 * Each part always starts with CRLF
211
		 */
212
		while (strpos($body, $crlf) === 0) {
213
			$position += 2;
214
			$body = substr($body, 2);
215
			$part = [
216
				'headers' => [
217
					'offset' => $position,
218
					'size'   => 0
219
				],
220
				'body'    => [
221
					'offset' => 0,
222
					'size'   => 0
223
				]
224
			];
225
			if (strpos($body, $crlf) === 0) {
226
				/**
227
				 * No headers
228
				 */
229
				$position += 2;
230
				$body = substr($body, 2);
231
			} else {
232
				/**
233
				 * Find headers end in order to determine size
234
				 */
235
				$result = $this->parse_multipart_find($stream, $body, $crlf.$crlf);
236
				if ($result === false) {
237
					return false;
238
				}
239
				list($offset, $body) = $result;
240
				$part['headers']['size'] = $offset;
241
				$position += $offset + 4;
242
				$body = substr($body, 4);
243
			}
244
			$part['body']['offset'] = $position;
245
			/**
246
			 * Find body end in order to determine its size
247
			 */
248
			$result = $this->parse_multipart_find($stream, $body, "$crlf--$boundary");
249
			if ($result === false) {
250
				return false;
251
			}
252
			list($offset, $body) = $result;
253
			$part['body']['size'] = $offset;
254
			$position += $offset + strlen("$crlf--$boundary");
255
			$body = substr($body, strlen("$crlf--$boundary"));
256
			if (!$part['headers']['size']) {
257
				$parts[] = $part;
258
			}
259
			$body .= fread($stream, 1024);
260
		}
261
		/**
262
		 * Last boundary after all parts ends with '--' and we don't care what rubbish happens after it
263
		 */
264
		$post_max_size = $this->post_max_size();
265
		if (0 !== strpos($body, '--')) {
266
			return false;
267
		}
268
		/**
269
		 * Check whether body size is bigger than allowed limit
270
		 */
271
		if ($position + strlen($body) > $post_max_size) {
272
			throw new ExitException(413);
273
		}
274
		return $parts;
275
	}
276
	/**
277
	 * @param resource $stream
278
	 * @param array[]  $parts
279
	 *
280
	 * @return array[]
281
	 */
282
	protected function parse_multipart_analyze_parts ($stream, $parts) {
283
		$data  = [];
284
		$files = [];
285
		foreach ($parts as $part) {
286
			fseek($stream, $part['headers']['offset']);
287
			$headers = $this->parse_multipart_headers(
288
				fread($stream, $part['headers']['size'])
289
			);
290
			if (
291
				!isset($headers['content-disposition'][0], $headers['content-disposition']['name']) ||
292
				$headers['content-disposition'][0] != 'form-data'
293
			) {
294
				continue;
295
			}
296
			$name = $headers['content-disposition']['name'];
297
			if (isset($headers['content-disposition']['filename'])) {
298
				$file = [
299
					'name'     => $headers['content-disposition']['filename'],
300
					'type'     => @$headers['content-type'] ?: 'application/octet-stream',
301
					'size'     => $part['body']['size'],
302
					'tmp_name' => 'request-data://'.$part['body']['offset'].':'.$part['body']['size'],
303
					'error'    => UPLOAD_ERR_OK
304
				];
305
				if ($headers['content-disposition']['filename'] === '') {
306
					$file['type']     = '';
307
					$file['tmp_name'] = '';
308
					$file['error']    = UPLOAD_ERR_NO_FILE;
309
				}
310
				if ($file['size'] > $this->upload_max_file_size()) {
311
					$file['tmp_name'] = '';
312
					$file['error']    = UPLOAD_ERR_INI_SIZE;
313
				}
314
				$this->parse_multipart_set_target($files, $name, $file);
315
			} else {
316
				if ($part['body']['size'] == 0) {
317
					$this->parse_multipart_set_target($data, $name, '');
318
				} else {
319
					fseek($stream, $part['body']['offset']);
320
					$this->parse_multipart_set_target(
321
						$data,
322
						$name,
323
						fread($stream, $part['body']['size'])
324
					);
325
				}
326
			}
327
		}
328
		return [$data, $files];
329
	}
330
	/**
331
	 * @return int
332
	 */
333
	protected function post_max_size () {
334
		$post_max_size = ini_get('post_max_size') ?: ini_get('hhvm.server.max_post_size');
335
		switch (strtolower(substr($post_max_size, -1))) {
336
			case 'g';
337
				$post_max_size = (int)$post_max_size * 1024;
338
			case 'm';
339
				$post_max_size = (int)$post_max_size * 1024;
340
			case 'k';
341
				$post_max_size = (int)$post_max_size * 1024;
342
		}
343
		return (int)$post_max_size ?: PHP_INT_MAX;
344
	}
345
	/**
346
	 * @return int
347
	 */
348
	protected function upload_max_file_size () {
349
		$upload_max_file_size = ini_get('upload_max_filesize') ?: ini_get('hhvm.server.upload.upload_max_file_size');
350
		switch (strtolower(substr($upload_max_file_size, -1))) {
351
			case 'g';
352
				$upload_max_file_size = (int)$upload_max_file_size * 1024;
353
			case 'm';
354
				$upload_max_file_size = (int)$upload_max_file_size * 1024;
355
			case 'k';
356
				$upload_max_file_size = (int)$upload_max_file_size * 1024;
357
		}
358
		return (int)$upload_max_file_size ?: PHP_INT_MAX;
359
	}
360
	/**
361
	 * @param resource $stream
362
	 * @param string   $next_data
363
	 * @param string   $target
364
	 *
365
	 * @return array|false
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use false|array<integer|string>.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
366
	 */
367
	protected function parse_multipart_find ($stream, $next_data, $target) {
368
		$offset    = 0;
369
		$prev_data = '';
370
		while (($found = strpos($prev_data.$next_data, $target)) === false) {
371
			if (feof($stream)) {
372
				return false;
373
			}
374
			if ($prev_data) {
375
				$offset += strlen($prev_data);
376
			}
377
			$prev_data = $next_data;
378
			$next_data = fread($stream, 1024);
379
		}
380
		$offset += $found;
381
		$remainder = substr($prev_data.$next_data, $found);
382
		return [$offset, $remainder];
383
	}
384
	/**
385
	 * @param string $content
386
	 *
387
	 * @return array
388
	 */
389
	protected function parse_multipart_headers ($content) {
390
		$headers = [];
391
		foreach (explode("\r\n", $content) as $header) {
392
			list($name, $value) = explode(':', $header, 2);
393
			if (!preg_match_all('/(.+)(?:="*?(.*)"?)?(?:;\s|$)/U', $value, $matches)) {
394
				continue;
395
			}
396
			$name           = strtolower($name);
397
			$headers[$name] = [];
398
			foreach (array_keys($matches[1]) as $index) {
399
				if (isset($headers[$name][0]) || strlen($matches[2][$index])) {
400
					$headers[$name][trim($matches[1][$index])] = urldecode(trim($matches[2][$index]));
401
				} else {
402
					$headers[$name][] = trim($matches[1][$index]);
403
				}
404
			}
405
			if (count($headers[$name]) == 1) {
406
				$headers[$name] = $headers[$name][0];
407
			}
408
		}
409
		return $headers;
410
	}
411
	/**
412
	 * @param array        $source
413
	 * @param string       $name
414
	 * @param array|string $value
415
	 */
416
	protected function parse_multipart_set_target (&$source, $name, $value) {
417
		preg_match_all('/(?:^|\[)([^\[\]]*)\]?/', $name, $matches);
418
		if ($matches[1][0] === '') {
419
			return;
420
		}
421
		foreach ($matches[1] as $component) {
422
			if (!isset($source[$component])) {
423
				$source[$component] = [];
424
			}
425
			if (!strlen($component)) {
426
				$source = &$source[$component][];
427
			} else {
428
				$source = &$source[$component];
429
			}
430
		}
431
		$source = $value;
432
	}
433
}
434