Completed
Push — master ( add50c...988ea9 )
by smiley
02:26
created

message_helpers.php ➔ normalize_request_headers()   B

Complexity

Conditions 10
Paths 18

Size

Total Lines 47

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
nc 18
nop 1
dl 0
loc 47
rs 7.2896
c 0
b 0
f 0

How to fix   Complexity   

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
 * @filesource   message_helpers.php
4
 * @created      28.08.2018
5
 * @author       smiley <[email protected]>
6
 * @copyright    2018 smiley
7
 * @license      MIT
8
 */
9
10
namespace chillerlan\HTTP\Psr7;
11
12
use InvalidArgumentException;
13
use Psr\Http\Message\{MessageInterface, RequestInterface, ResponseInterface, UploadedFileInterface};
14
15
use function array_combine, array_keys, array_map, array_merge, array_values, call_user_func_array, count, explode,
16
	gzdecode, gzinflate, gzuncompress, implode, is_array, is_bool, is_iterable, is_numeric, is_string, json_decode,
17
	json_encode, parse_str, parse_url, rawurlencode, simplexml_load_string, sort, strtolower, trim, ucfirst, uksort;
18
19
use const PHP_URL_QUERY, SORT_STRING;
20
21
const PSR7_INCLUDES = true;
22
23
/**
24
 * @link http://svn.apache.org/repos/asf/httpd/httpd/branches/1.3.x/conf/mime.types
25
 */
26
const MIMETYPES = [
27
	'3gp'     => 'video/3gpp',
28
	'7z'      => 'application/x-7z-compressed',
29
	'aac'     => 'audio/x-aac',
30
	'ai'      => 'application/postscript',
31
	'aif'     => 'audio/x-aiff',
32
	'asc'     => 'text/plain',
33
	'asf'     => 'video/x-ms-asf',
34
	'atom'    => 'application/atom+xml',
35
	'avi'     => 'video/x-msvideo',
36
	'bmp'     => 'image/bmp',
37
	'bz2'     => 'application/x-bzip2',
38
	'cer'     => 'application/pkix-cert',
39
	'crl'     => 'application/pkix-crl',
40
	'crt'     => 'application/x-x509-ca-cert',
41
	'css'     => 'text/css',
42
	'csv'     => 'text/csv',
43
	'cu'      => 'application/cu-seeme',
44
	'deb'     => 'application/x-debian-package',
45
	'doc'     => 'application/msword',
46
	'docx'    => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
47
	'dvi'     => 'application/x-dvi',
48
	'eot'     => 'application/vnd.ms-fontobject',
49
	'eps'     => 'application/postscript',
50
	'epub'    => 'application/epub+zip',
51
	'etx'     => 'text/x-setext',
52
	'flac'    => 'audio/flac',
53
	'flv'     => 'video/x-flv',
54
	'gif'     => 'image/gif',
55
	'gz'      => 'application/gzip',
56
	'htm'     => 'text/html',
57
	'html'    => 'text/html',
58
	'ico'     => 'image/x-icon',
59
	'ics'     => 'text/calendar',
60
	'ini'     => 'text/plain',
61
	'iso'     => 'application/x-iso9660-image',
62
	'jar'     => 'application/java-archive',
63
	'jpe'     => 'image/jpeg',
64
	'jpeg'    => 'image/jpeg',
65
	'jpg'     => 'image/jpeg',
66
	'js'      => 'text/javascript',
67
	'json'    => 'application/json',
68
	'latex'   => 'application/x-latex',
69
	'log'     => 'text/plain',
70
	'm4a'     => 'audio/mp4',
71
	'm4v'     => 'video/mp4',
72
	'mid'     => 'audio/midi',
73
	'midi'    => 'audio/midi',
74
	'mov'     => 'video/quicktime',
75
	'mkv'     => 'video/x-matroska',
76
	'mp3'     => 'audio/mpeg',
77
	'mp4'     => 'video/mp4',
78
	'mp4a'    => 'audio/mp4',
79
	'mp4v'    => 'video/mp4',
80
	'mpe'     => 'video/mpeg',
81
	'mpeg'    => 'video/mpeg',
82
	'mpg'     => 'video/mpeg',
83
	'mpg4'    => 'video/mp4',
84
	'oga'     => 'audio/ogg',
85
	'ogg'     => 'audio/ogg',
86
	'ogv'     => 'video/ogg',
87
	'ogx'     => 'application/ogg',
88
	'pbm'     => 'image/x-portable-bitmap',
89
	'pdf'     => 'application/pdf',
90
	'pgm'     => 'image/x-portable-graymap',
91
	'png'     => 'image/png',
92
	'pnm'     => 'image/x-portable-anymap',
93
	'ppm'     => 'image/x-portable-pixmap',
94
	'ppt'     => 'application/vnd.ms-powerpoint',
95
	'pptx'    => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
96
	'ps'      => 'application/postscript',
97
	'qt'      => 'video/quicktime',
98
	'rar'     => 'application/x-rar-compressed',
99
	'ras'     => 'image/x-cmu-raster',
100
	'rss'     => 'application/rss+xml',
101
	'rtf'     => 'application/rtf',
102
	'sgm'     => 'text/sgml',
103
	'sgml'    => 'text/sgml',
104
	'svg'     => 'image/svg+xml',
105
	'swf'     => 'application/x-shockwave-flash',
106
	'tar'     => 'application/x-tar',
107
	'tif'     => 'image/tiff',
108
	'tiff'    => 'image/tiff',
109
	'torrent' => 'application/x-bittorrent',
110
	'ttf'     => 'application/x-font-ttf',
111
	'txt'     => 'text/plain',
112
	'wav'     => 'audio/x-wav',
113
	'webm'    => 'video/webm',
114
	'wma'     => 'audio/x-ms-wma',
115
	'wmv'     => 'video/x-ms-wmv',
116
	'woff'    => 'application/x-font-woff',
117
	'wsdl'    => 'application/wsdl+xml',
118
	'xbm'     => 'image/x-xbitmap',
119
	'xls'     => 'application/vnd.ms-excel',
120
	'xlsx'    => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
121
	'xml'     => 'application/xml',
122
	'xpm'     => 'image/x-xpixmap',
123
	'xwd'     => 'image/x-xwindowdump',
124
	'yaml'    => 'text/yaml',
125
	'yml'     => 'text/yaml',
126
	'zip'     => 'application/zip',
127
];
128
129
/**
130
 * Normalizes an array of header lines to format ["Name" => "Value (, Value2, Value3, ...)", ...]
131
 * An exception is being made for Set-Cookie, which holds an array of values for each cookie.
132
 * For multiple cookies with the same name, only the last value will be kept.
133
 *
134
 * @param array $headers
135
 *
136
 * @return array
137
 */
138
function normalize_message_headers(array $headers):array{
139
	$normalized_headers = [];
140
141
	foreach($headers as $key => $val){
142
143
		// the key is numeric, so $val is either a string or an array
144
		if(is_numeric($key)){
145
146
			// "key: val"
147
			if(is_string($val)){
148
				$header = explode(':', $val, 2);
149
150
				if(count($header) !== 2){
151
					continue;
152
				}
153
154
				$key = $header[0];
155
				$val = $header[1];
156
			}
157
			// [$key, $val], ["key" => $key, "val" => $val]
158
			elseif(is_array($val)){
159
				$key = array_keys($val)[0];
160
				$val = array_values($val)[0];
161
			}
162
			else{
163
				continue;
164
			}
165
		}
166
		// the key is named, so we assume $val holds the header values only, either as string or array
167
		else{
168
			if(is_array($val)){
169
				$val = implode(', ', array_values($val));
170
			}
171
		}
172
173
		// PHP 7.4 -> fn()
174
		$key = array_map(function(string $v):string{
175
			return ucfirst(strtolower(trim($v)));
176
		}, explode('-', $key));
177
178
		$key = implode('-', $key);
179
		$val = trim($val);
180
181
		// skip if the header already exists but the current value is empty
182
		if(isset($normalized_headers[$key]) && empty($val)){
183
			continue;
184
		}
185
186
		// cookie headers may appear multiple times
187
		// https://tools.ietf.org/html/rfc6265#section-4.1.2
188
		if($key === 'Set-Cookie'){
189
			// i'll just collect the last value here and leave parsing up to you :P
190
			$normalized_headers[$key][strtolower(explode('=', $val, 2)[0])] = $val;
191
		}
192
		// combine header fields with the same name
193
		// https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
194
		else{
195
			isset($normalized_headers[$key]) && !empty($normalized_headers[$key])
196
				? $normalized_headers[$key] .= ', '.$val
197
				: $normalized_headers[$key] = $val;
198
		}
199
	}
200
201
	return $normalized_headers;
202
}
203
204
/**
205
 * @param string|array $data
206
 *
207
 * @return string|array
208
 */
209
function r_rawurlencode($data){
210
211
	if(is_array($data)){
212
		return array_map(__FUNCTION__, $data);
213
	}
214
215
	return rawurlencode($data);
216
}
217
218
/**
219
 * from https://github.com/abraham/twitteroauth/blob/master/src/Util.php
220
 *
221
 * @param array  $params
222
 * @param bool   $urlencode
223
 * @param string $delimiter
224
 * @param string $enclosure
225
 *
226
 * @return string
227
 */
228
function build_http_query(array $params, bool $urlencode = null, string $delimiter = null, string $enclosure = null):string{
229
230
	if(empty($params)){
231
		return '';
232
	}
233
234
	// urlencode both keys and values
235
	if($urlencode ?? true){
236
		$params = array_combine(
237
			r_rawurlencode(array_keys($params)),
238
			r_rawurlencode(array_values($params))
239
		);
240
	}
241
242
	// Parameters are sorted by name, using lexicographical byte value ordering.
243
	// Ref: Spec: 9.1.1 (1)
244
	uksort($params, 'strcmp');
245
246
	$pairs     = [];
247
	$enclosure = $enclosure ?? '';
248
249
	foreach($params as $parameter => $value){
250
251
		if(is_array($value)){
252
			// If two or more parameters share the same name, they are sorted by their value
253
			// Ref: Spec: 9.1.1 (1)
254
			// June 12th, 2010 - changed to sort because of issue 164 by hidetaka
255
			sort($value, SORT_STRING);
256
257
			foreach($value as $duplicateValue){
258
				$pairs[] = $parameter.'='.$enclosure.$duplicateValue.$enclosure;
259
			}
260
261
		}
262
		else{
263
			$pairs[] = $parameter.'='.$enclosure.$value.$enclosure;
264
		}
265
266
	}
267
268
	// For each parameter, the name is separated from the corresponding value by an '=' character (ASCII code 61)
269
	// Each name-value pair is separated by an '&' character (ASCII code 38)
270
	return implode($delimiter ?? '&', $pairs);
271
}
272
273
const BOOLEANS_AS_BOOL       = 0;
274
const BOOLEANS_AS_INT        = 1;
275
const BOOLEANS_AS_STRING     = 2;
276
const BOOLEANS_AS_INT_STRING = 3;
277
278
/**
279
 * @param iterable  $params
280
 * @param int|null  $bool_cast    converts booleans to a type determined like following:
281
 *                                BOOLEANS_AS_BOOL      : unchanged boolean value (default)
282
 *                                BOOLEANS_AS_INT       : integer values 0 or 1
283
 *                                BOOLEANS_AS_STRING    : "true"/"false" strings
284
 *                                BOOLEANS_AS_INT_STRING: "0"/"1"
285
 *
286
 * @param bool|null $remove_empty remove empty and NULL values
287
 *
288
 * @return array
289
 */
290
function clean_query_params(iterable $params, int $bool_cast = null, bool $remove_empty = null):iterable{
291
	$p            = [];
292
	$bool_cast    = $bool_cast ?? BOOLEANS_AS_BOOL;
293
	$remove_empty = $remove_empty ?? true;
294
295
	foreach($params as $key => $value){
296
297
		if(is_bool($value)){
298
299
			if($bool_cast === BOOLEANS_AS_BOOL){
300
				$p[$key] = $value;
301
			}
302
			elseif($bool_cast === BOOLEANS_AS_INT){
303
				$p[$key] = (int)$value;
304
			}
305
			elseif($bool_cast === BOOLEANS_AS_STRING){
306
				$p[$key] = $value ? 'true' : 'false';
307
			}
308
			elseif($bool_cast === BOOLEANS_AS_INT_STRING){
309
				$p[$key] = (string)(int)$value;
310
			}
311
312
		}
313
		elseif(is_iterable($value)){
314
			$p[$key] = call_user_func_array(__FUNCTION__, [$value, $bool_cast, $remove_empty]);
315
		}
316
		elseif($remove_empty === true && ($value === null || (!is_numeric($value) && empty($value)))){
317
			continue;
318
		}
319
		else{
320
			$p[$key] = $value;
321
		}
322
	}
323
324
	return $p;
325
}
326
327
/**
328
 * merges additional query parameters into an existing query string
329
 *
330
 * @param string $uri
331
 * @param array  $query
332
 *
333
 * @return string
334
 */
335
function merge_query(string $uri, array $query):string{
336
	parse_str(parse_url($uri, PHP_URL_QUERY), $parsedquery);
337
338
	$requestURI = explode('?', $uri)[0];
339
	$params     = array_merge($parsedquery, $query);
340
341
	if(!empty($params)){
342
		$requestURI .= '?'.build_http_query($params);
343
	}
344
345
	return $requestURI;
346
}
347
348
/**
349
 * Return an UploadedFile instance array.
350
 *
351
 * @param array $files A array which respect $_FILES structure
352
 *
353
 * @return array
354
 * @throws \InvalidArgumentException for unrecognized values
355
 */
356
function normalize_files(array $files):array{
357
	$normalized = [];
358
359
	foreach($files as $key => $value){
360
361
		if($value instanceof UploadedFileInterface){
362
			$normalized[$key] = $value;
363
		}
364
		elseif(is_array($value) && isset($value['tmp_name'])){
365
			$normalized[$key] = create_uploaded_file_from_spec($value);
366
		}
367
		elseif(is_array($value)){
368
			$normalized[$key] = normalize_files($value);
369
			continue;
370
		}
371
		else{
372
			throw new InvalidArgumentException('Invalid value in files specification');
373
		}
374
375
	}
376
377
	return $normalized;
378
}
379
380
/**
381
 * Create and return an UploadedFile instance from a $_FILES specification.
382
 *
383
 * If the specification represents an array of values, this method will
384
 * delegate to normalizeNestedFileSpec() and return that return value.
385
 *
386
 * @param array $value $_FILES struct
387
 *
388
 * @return array|\Psr\Http\Message\UploadedFileInterface
389
 */
390
function create_uploaded_file_from_spec(array $value){
391
392
	if(is_array($value['tmp_name'])){
393
		return normalize_nested_file_spec($value);
394
	}
395
396
	return new UploadedFile($value['tmp_name'], (int)$value['size'], (int)$value['error'], $value['name'], $value['type']);
397
}
398
399
/**
400
 * Normalize an array of file specifications.
401
 *
402
 * Loops through all nested files and returns a normalized array of
403
 * UploadedFileInterface instances.
404
 *
405
 * @param array $files
406
 *
407
 * @return \Psr\Http\Message\UploadedFileInterface[]
408
 */
409
function normalize_nested_file_spec(array $files = []):array{
410
	$normalizedFiles = [];
411
412
	foreach(array_keys($files['tmp_name']) as $key){
413
		$spec = [
414
			'tmp_name' => $files['tmp_name'][$key],
415
			'size'     => $files['size'][$key],
416
			'error'    => $files['error'][$key],
417
			'name'     => $files['name'][$key],
418
			'type'     => $files['type'][$key],
419
		];
420
421
		$normalizedFiles[$key] = create_uploaded_file_from_spec($spec);
422
	}
423
424
	return $normalizedFiles;
425
}
426
427
/**
428
 * @param \Psr\Http\Message\MessageInterface $message
429
 * @param bool|null                          $assoc
430
 *
431
 * @return \stdClass|array|bool
432
 */
433
function get_json(MessageInterface $message, bool $assoc = null){
434
	$data = json_decode($message->getBody()->__toString(), $assoc);
435
436
	$message->getBody()->rewind();
437
438
	return $data;
439
}
440
441
/**
442
 * @param \Psr\Http\Message\MessageInterface $message
443
 * @param bool|null                          $assoc
444
 *
445
 * @return \SimpleXMLElement|array|bool
446
 */
447
function get_xml(MessageInterface $message, bool $assoc = null){
448
	$data = simplexml_load_string($message->getBody()->__toString());
449
450
	$message->getBody()->rewind();
451
452
	return $assoc === true
453
		? json_decode(json_encode($data), true) // cruel
454
		: $data;
455
}
456
457
/**
458
 * Returns the string representation of an HTTP message. (from Guzzle)
459
 *
460
 * @param \Psr\Http\Message\MessageInterface $message Message to convert to a string.
461
 *
462
 * @return string
463
 */
464
function message_to_string(MessageInterface $message):string{
465
	$msg = '';
466
467
	if($message instanceof RequestInterface){
468
		$msg = trim($message->getMethod().' '.$message->getRequestTarget()).' HTTP/'.$message->getProtocolVersion();
469
470
		if(!$message->hasHeader('host')){
471
			$msg .= "\r\nHost: ".$message->getUri()->getHost();
472
		}
473
474
	}
475
	elseif($message instanceof ResponseInterface){
476
		$msg = 'HTTP/'.$message->getProtocolVersion().' '.$message->getStatusCode().' '.$message->getReasonPhrase();
477
	}
478
479
	foreach($message->getHeaders() as $name => $values){
480
		$msg .= "\r\n".$name.': '.implode(', ', $values);
481
	}
482
483
	$data = $message->getBody()->__toString();
484
	$message->getBody()->rewind();
485
486
	return $msg."\r\n\r\n".$data;
487
}
488
489
/**
490
 * Decompresses the message content according to the Content-Encoding header and returns the decompressed data
491
 *
492
 * @param \Psr\Http\Message\MessageInterface $message
493
 *
494
 * @return string
495
 */
496
function decompress_content(MessageInterface $message):string{
497
	$data = $message->getBody()->__toString();
498
	$message->getBody()->rewind();
499
500
	switch($message->getHeaderLine('content-encoding')){
501
#		case 'br'      : return brotli_uncompress($data); // @todo: https://github.com/kjdev/php-ext-brotli
502
		case 'compress': return gzuncompress($data);
503
		case 'deflate' : return gzinflate($data);
504
		case 'gzip'    : return gzdecode($data);
505
		default: return $data;
506
	}
507
508
}
509