Completed
Push — master ( 988ea9...868c80 )
by smiley
07:26
created

message_helpers.php ➔ clean_query_params()   C

Complexity

Conditions 13
Paths 10

Size

Total Lines 36

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 13
nc 10
nop 3
dl 0
loc 36
rs 6.6166
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
		$key = implode('-', array_map(fn(string $v):string => ucfirst(strtolower(trim($v))), explode('-', $key)));
0 ignored issues
show
Bug introduced by
This code did not parse for me. Apparently, there is an error somewhere around this line:

Syntax error, unexpected T_VARIABLE, expecting ',' or ')'
Loading history...
174
		$val = trim($val);
175
176
		// skip if the header already exists but the current value is empty
177
		if(isset($normalized_headers[$key]) && empty($val)){
178
			continue;
179
		}
180
181
		// cookie headers may appear multiple times
182
		// https://tools.ietf.org/html/rfc6265#section-4.1.2
183
		if($key === 'Set-Cookie'){
184
			// i'll just collect the last value here and leave parsing up to you :P
185
			$normalized_headers[$key][strtolower(explode('=', $val, 2)[0])] = $val;
186
		}
187
		// combine header fields with the same name
188
		// https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
189
		else{
190
			isset($normalized_headers[$key]) && !empty($normalized_headers[$key])
191
				? $normalized_headers[$key] .= ', '.$val
192
				: $normalized_headers[$key] = $val;
193
		}
194
	}
195
196
	return $normalized_headers;
197
}
198
199
/**
200
 * @param string|array $data
201
 *
202
 * @return string|array
203
 */
204
function r_rawurlencode($data){
205
206
	if(is_array($data)){
207
		return array_map(__FUNCTION__, $data);
208
	}
209
210
	return rawurlencode($data);
211
}
212
213
/**
214
 * from https://github.com/abraham/twitteroauth/blob/master/src/Util.php
215
 */
216
function build_http_query(array $params, bool $urlencode = null, string $delimiter = null, string $enclosure = null):string{
217
218
	if(empty($params)){
219
		return '';
220
	}
221
222
	// urlencode both keys and values
223
	if($urlencode ?? true){
224
		$params = array_combine(
225
			r_rawurlencode(array_keys($params)),
226
			r_rawurlencode(array_values($params))
227
		);
228
	}
229
230
	// Parameters are sorted by name, using lexicographical byte value ordering.
231
	// Ref: Spec: 9.1.1 (1)
232
	uksort($params, 'strcmp');
233
234
	$pairs     = [];
235
	$enclosure = $enclosure ?? '';
236
237
	foreach($params as $parameter => $value){
238
239
		if(is_array($value)){
240
			// If two or more parameters share the same name, they are sorted by their value
241
			// Ref: Spec: 9.1.1 (1)
242
			// June 12th, 2010 - changed to sort because of issue 164 by hidetaka
243
			sort($value, SORT_STRING);
244
245
			foreach($value as $duplicateValue){
246
				$pairs[] = $parameter.'='.$enclosure.$duplicateValue.$enclosure;
247
			}
248
249
		}
250
		else{
251
			$pairs[] = $parameter.'='.$enclosure.$value.$enclosure;
252
		}
253
254
	}
255
256
	// For each parameter, the name is separated from the corresponding value by an '=' character (ASCII code 61)
257
	// Each name-value pair is separated by an '&' character (ASCII code 38)
258
	return implode($delimiter ?? '&', $pairs);
259
}
260
261
const BOOLEANS_AS_BOOL       = 0;
262
const BOOLEANS_AS_INT        = 1;
263
const BOOLEANS_AS_STRING     = 2;
264
const BOOLEANS_AS_INT_STRING = 3;
265
266
/**
267
 * @param iterable  $params
268
 * @param int|null  $bool_cast    converts booleans to a type determined like following:
269
 *                                BOOLEANS_AS_BOOL      : unchanged boolean value (default)
270
 *                                BOOLEANS_AS_INT       : integer values 0 or 1
271
 *                                BOOLEANS_AS_STRING    : "true"/"false" strings
272
 *                                BOOLEANS_AS_INT_STRING: "0"/"1"
273
 *
274
 * @param bool|null $remove_empty remove empty and NULL values
275
 *
276
 * @return array
277
 */
278
function clean_query_params(iterable $params, int $bool_cast = null, bool $remove_empty = null):iterable{
279
	$p            = [];
280
	$bool_cast    = $bool_cast ?? BOOLEANS_AS_BOOL;
281
	$remove_empty = $remove_empty ?? true;
282
283
	foreach($params as $key => $value){
284
285
		if(is_bool($value)){
286
287
			if($bool_cast === BOOLEANS_AS_BOOL){
288
				$p[$key] = $value;
289
			}
290
			elseif($bool_cast === BOOLEANS_AS_INT){
291
				$p[$key] = (int)$value;
292
			}
293
			elseif($bool_cast === BOOLEANS_AS_STRING){
294
				$p[$key] = $value ? 'true' : 'false';
295
			}
296
			elseif($bool_cast === BOOLEANS_AS_INT_STRING){
297
				$p[$key] = (string)(int)$value;
298
			}
299
300
		}
301
		elseif(is_iterable($value)){
302
			$p[$key] = call_user_func_array(__FUNCTION__, [$value, $bool_cast, $remove_empty]);
303
		}
304
		elseif($remove_empty === true && ($value === null || (!is_numeric($value) && empty($value)))){
305
			continue;
306
		}
307
		else{
308
			$p[$key] = $value;
309
		}
310
	}
311
312
	return $p;
313
}
314
315
/**
316
 * merges additional query parameters into an existing query string
317
 *
318
 * @param string $uri
319
 * @param array  $query
320
 *
321
 * @return string
322
 */
323
function merge_query(string $uri, array $query):string{
324
	parse_str(parse_url($uri, PHP_URL_QUERY), $parsedquery);
325
326
	$requestURI = explode('?', $uri)[0];
327
	$params     = array_merge($parsedquery, $query);
328
329
	if(!empty($params)){
330
		$requestURI .= '?'.build_http_query($params);
331
	}
332
333
	return $requestURI;
334
}
335
336
/**
337
 * Return an UploadedFile instance array.
338
 *
339
 * @param array $files A array which respect $_FILES structure
340
 *
341
 * @return array
342
 * @throws \InvalidArgumentException for unrecognized values
343
 */
344
function normalize_files(array $files):array{
345
	$normalized = [];
346
347
	foreach($files as $key => $value){
348
349
		if($value instanceof UploadedFileInterface){
350
			$normalized[$key] = $value;
351
		}
352
		elseif(is_array($value) && isset($value['tmp_name'])){
353
			$normalized[$key] = create_uploaded_file_from_spec($value);
354
		}
355
		elseif(is_array($value)){
356
			$normalized[$key] = normalize_files($value);
357
			continue;
358
		}
359
		else{
360
			throw new InvalidArgumentException('Invalid value in files specification');
361
		}
362
363
	}
364
365
	return $normalized;
366
}
367
368
/**
369
 * Create and return an UploadedFile instance from a $_FILES specification.
370
 *
371
 * If the specification represents an array of values, this method will
372
 * delegate to normalizeNestedFileSpec() and return that return value.
373
 *
374
 * @param array $value $_FILES struct
375
 *
376
 * @return array|\Psr\Http\Message\UploadedFileInterface
377
 */
378
function create_uploaded_file_from_spec(array $value){
379
380
	if(is_array($value['tmp_name'])){
381
		return normalize_nested_file_spec($value);
382
	}
383
384
	return new UploadedFile($value['tmp_name'], (int)$value['size'], (int)$value['error'], $value['name'], $value['type']);
385
}
386
387
/**
388
 * Normalize an array of file specifications.
389
 *
390
 * Loops through all nested files and returns a normalized array of
391
 * UploadedFileInterface instances.
392
 *
393
 * @param array $files
394
 *
395
 * @return \Psr\Http\Message\UploadedFileInterface[]
396
 */
397
function normalize_nested_file_spec(array $files = []):array{
398
	$normalizedFiles = [];
399
400
	foreach(array_keys($files['tmp_name']) as $key){
401
		$spec = [
402
			'tmp_name' => $files['tmp_name'][$key],
403
			'size'     => $files['size'][$key],
404
			'error'    => $files['error'][$key],
405
			'name'     => $files['name'][$key],
406
			'type'     => $files['type'][$key],
407
		];
408
409
		$normalizedFiles[$key] = create_uploaded_file_from_spec($spec);
410
	}
411
412
	return $normalizedFiles;
413
}
414
415
/**
416
 * @param \Psr\Http\Message\MessageInterface $message
417
 * @param bool|null                          $assoc
418
 *
419
 * @return \stdClass|array|bool
420
 */
421
function get_json(MessageInterface $message, bool $assoc = null){
422
	$data = json_decode($message->getBody()->__toString(), $assoc);
423
424
	$message->getBody()->rewind();
425
426
	return $data;
427
}
428
429
/**
430
 * @param \Psr\Http\Message\MessageInterface $message
431
 * @param bool|null                          $assoc
432
 *
433
 * @return \SimpleXMLElement|array|bool
434
 */
435
function get_xml(MessageInterface $message, bool $assoc = null){
436
	$data = simplexml_load_string($message->getBody()->__toString());
437
438
	$message->getBody()->rewind();
439
440
	return $assoc === true
441
		? json_decode(json_encode($data), true) // cruel
442
		: $data;
443
}
444
445
/**
446
 * Returns the string representation of an HTTP message. (from Guzzle)
447
 *
448
 * @param \Psr\Http\Message\MessageInterface $message Message to convert to a string.
449
 *
450
 * @return string
451
 */
452
function message_to_string(MessageInterface $message):string{
453
	$msg = '';
454
455
	if($message instanceof RequestInterface){
456
		$msg = trim($message->getMethod().' '.$message->getRequestTarget()).' HTTP/'.$message->getProtocolVersion();
457
458
		if(!$message->hasHeader('host')){
459
			$msg .= "\r\nHost: ".$message->getUri()->getHost();
460
		}
461
462
	}
463
	elseif($message instanceof ResponseInterface){
464
		$msg = 'HTTP/'.$message->getProtocolVersion().' '.$message->getStatusCode().' '.$message->getReasonPhrase();
465
	}
466
467
	foreach($message->getHeaders() as $name => $values){
468
		$msg .= "\r\n".$name.': '.implode(', ', $values);
469
	}
470
471
	$data = $message->getBody()->__toString();
472
	$message->getBody()->rewind();
473
474
	return $msg."\r\n\r\n".$data;
475
}
476
477
/**
478
 * Decompresses the message content according to the Content-Encoding header and returns the decompressed data
479
 *
480
 * @param \Psr\Http\Message\MessageInterface $message
481
 *
482
 * @return string
483
 */
484
function decompress_content(MessageInterface $message):string{
485
	$data = $message->getBody()->__toString();
486
	$message->getBody()->rewind();
487
488
	switch($message->getHeaderLine('content-encoding')){
489
#		case 'br'      : return brotli_uncompress($data); // @todo: https://github.com/kjdev/php-ext-brotli
490
		case 'compress': return gzuncompress($data);
491
		case 'deflate' : return gzinflate($data);
492
		case 'gzip'    : return gzdecode($data);
493
		default: return $data;
494
	}
495
496
}
497