Passed
Push — main ( f2efe3...53dc0a )
by smiley
10:15
created

clean_query_params()   C

Complexity

Conditions 13
Paths 10

Size

Total Lines 35
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 13
eloc 20
nc 10
nop 3
dl 0
loc 35
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, TypeError;
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_scalar, is_string,
17
	json_decode, json_encode, parse_str, parse_url, rawurlencode, simplexml_load_string, sort, strtolower, trim,
18
	ucfirst, uksort;
19
20
use const PHP_URL_QUERY, SORT_STRING;
21
22
const PSR7_INCLUDES = true;
23
24
/**
25
 * @link http://svn.apache.org/repos/asf/httpd/httpd/branches/1.3.x/conf/mime.types
26
 */
27
const MIMETYPES = [
28
	'3gp'     => 'video/3gpp',
29
	'7z'      => 'application/x-7z-compressed',
30
	'aac'     => 'audio/x-aac',
31
	'ai'      => 'application/postscript',
32
	'aif'     => 'audio/x-aiff',
33
	'asc'     => 'text/plain',
34
	'asf'     => 'video/x-ms-asf',
35
	'atom'    => 'application/atom+xml',
36
	'avi'     => 'video/x-msvideo',
37
	'bmp'     => 'image/bmp',
38
	'bz2'     => 'application/x-bzip2',
39
	'cer'     => 'application/pkix-cert',
40
	'crl'     => 'application/pkix-crl',
41
	'crt'     => 'application/x-x509-ca-cert',
42
	'css'     => 'text/css',
43
	'csv'     => 'text/csv',
44
	'cu'      => 'application/cu-seeme',
45
	'deb'     => 'application/x-debian-package',
46
	'doc'     => 'application/msword',
47
	'docx'    => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
48
	'dvi'     => 'application/x-dvi',
49
	'eot'     => 'application/vnd.ms-fontobject',
50
	'eps'     => 'application/postscript',
51
	'epub'    => 'application/epub+zip',
52
	'etx'     => 'text/x-setext',
53
	'flac'    => 'audio/flac',
54
	'flv'     => 'video/x-flv',
55
	'gif'     => 'image/gif',
56
	'gz'      => 'application/gzip',
57
	'htm'     => 'text/html',
58
	'html'    => 'text/html',
59
	'ico'     => 'image/x-icon',
60
	'ics'     => 'text/calendar',
61
	'ini'     => 'text/plain',
62
	'iso'     => 'application/x-iso9660-image',
63
	'jar'     => 'application/java-archive',
64
	'jpe'     => 'image/jpeg',
65
	'jpeg'    => 'image/jpeg',
66
	'jpg'     => 'image/jpeg',
67
	'js'      => 'text/javascript',
68
	'json'    => 'application/json',
69
	'latex'   => 'application/x-latex',
70
	'log'     => 'text/plain',
71
	'm4a'     => 'audio/mp4',
72
	'm4v'     => 'video/mp4',
73
	'mid'     => 'audio/midi',
74
	'midi'    => 'audio/midi',
75
	'mov'     => 'video/quicktime',
76
	'mkv'     => 'video/x-matroska',
77
	'mp3'     => 'audio/mpeg',
78
	'mp4'     => 'video/mp4',
79
	'mp4a'    => 'audio/mp4',
80
	'mp4v'    => 'video/mp4',
81
	'mpe'     => 'video/mpeg',
82
	'mpeg'    => 'video/mpeg',
83
	'mpg'     => 'video/mpeg',
84
	'mpg4'    => 'video/mp4',
85
	'oga'     => 'audio/ogg',
86
	'ogg'     => 'audio/ogg',
87
	'ogv'     => 'video/ogg',
88
	'ogx'     => 'application/ogg',
89
	'pbm'     => 'image/x-portable-bitmap',
90
	'pdf'     => 'application/pdf',
91
	'pgm'     => 'image/x-portable-graymap',
92
	'png'     => 'image/png',
93
	'pnm'     => 'image/x-portable-anymap',
94
	'ppm'     => 'image/x-portable-pixmap',
95
	'ppt'     => 'application/vnd.ms-powerpoint',
96
	'pptx'    => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
97
	'ps'      => 'application/postscript',
98
	'qt'      => 'video/quicktime',
99
	'rar'     => 'application/x-rar-compressed',
100
	'ras'     => 'image/x-cmu-raster',
101
	'rss'     => 'application/rss+xml',
102
	'rtf'     => 'application/rtf',
103
	'sgm'     => 'text/sgml',
104
	'sgml'    => 'text/sgml',
105
	'svg'     => 'image/svg+xml',
106
	'swf'     => 'application/x-shockwave-flash',
107
	'tar'     => 'application/x-tar',
108
	'tif'     => 'image/tiff',
109
	'tiff'    => 'image/tiff',
110
	'torrent' => 'application/x-bittorrent',
111
	'ttf'     => 'application/x-font-ttf',
112
	'txt'     => 'text/plain',
113
	'wav'     => 'audio/x-wav',
114
	'webm'    => 'video/webm',
115
	'wma'     => 'audio/x-ms-wma',
116
	'wmv'     => 'video/x-ms-wmv',
117
	'woff'    => 'application/x-font-woff',
118
	'wsdl'    => 'application/wsdl+xml',
119
	'xbm'     => 'image/x-xbitmap',
120
	'xls'     => 'application/vnd.ms-excel',
121
	'xlsx'    => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
122
	'xml'     => 'application/xml',
123
	'xpm'     => 'image/x-xpixmap',
124
	'xwd'     => 'image/x-xwindowdump',
125
	'yaml'    => 'text/yaml',
126
	'yml'     => 'text/yaml',
127
	'zip'     => 'application/zip',
128
];
129
130
/**
131
 * Normalizes an array of header lines to format ["Name" => "Value (, Value2, Value3, ...)", ...]
132
 * An exception is being made for Set-Cookie, which holds an array of values for each cookie.
133
 * For multiple cookies with the same name, only the last value will be kept.
134
 *
135
 * @param array $headers
136
 *
137
 * @return array
138
 */
139
function normalize_message_headers(array $headers):array{
140
	$normalized_headers = [];
141
142
	foreach($headers as $key => $val){
143
144
		// the key is numeric, so $val is either a string or an array
145
		if(is_numeric($key)){
146
147
			// "key: val"
148
			if(is_string($val)){
149
				$header = explode(':', $val, 2);
150
151
				if(count($header) !== 2){
152
					continue;
153
				}
154
155
				$key = $header[0];
156
				$val = $header[1];
157
			}
158
			// [$key, $val], ["key" => $key, "val" => $val]
159
			elseif(is_array($val)){
160
				$key = array_keys($val)[0];
161
				$val = array_values($val)[0];
162
			}
163
			else{
164
				continue;
165
			}
166
		}
167
		// the key is named, so we assume $val holds the header values only, either as string or array
168
		else{
169
			if(is_array($val)){
170
				$val = implode(', ', array_values($val));
171
			}
172
		}
173
174
		$key = implode('-', array_map(fn(string $v):string => ucfirst(strtolower(trim($v))), explode('-', $key)));
175
		$val = trim($val);
176
177
		// skip if the header already exists but the current value is empty
178
		if(isset($normalized_headers[$key]) && empty($val)){
179
			continue;
180
		}
181
182
		// cookie headers may appear multiple times
183
		// https://tools.ietf.org/html/rfc6265#section-4.1.2
184
		if($key === 'Set-Cookie'){
185
			// i'll just collect the last value here and leave parsing up to you :P
186
			$normalized_headers[$key][strtolower(explode('=', $val, 2)[0])] = $val;
187
		}
188
		// combine header fields with the same name
189
		// https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
190
		else{
191
			isset($normalized_headers[$key]) && !empty($normalized_headers[$key])
192
				? $normalized_headers[$key] .= ', '.$val
193
				: $normalized_headers[$key] = $val;
194
		}
195
	}
196
197
	return $normalized_headers;
198
}
199
200
/**
201
 * @param string|string[] $data
202
 *
203
 * @return string|string[]
204
 * @throws \TypeError
205
 */
206
function r_rawurlencode($data){
207
208
	if(is_array($data)){
209
		return array_map(__FUNCTION__, $data);
210
	}
211
212
	if(!is_scalar($data)){
0 ignored issues
show
introduced by
The condition is_scalar($data) is always true.
Loading history...
213
		throw new TypeError('$data is not scalar');
214
	}
215
216
	return rawurlencode((string)$data);
217
}
218
219
/**
220
 * from https://github.com/abraham/twitteroauth/blob/master/src/Util.php
221
 */
222
function build_http_query(array $params, bool $urlencode = null, string $delimiter = null, string $enclosure = null):string{
223
224
	if(empty($params)){
225
		return '';
226
	}
227
228
	// urlencode both keys and values
229
	if($urlencode ?? true){
230
		$params = array_combine(
231
			r_rawurlencode(array_keys($params)),
0 ignored issues
show
Bug introduced by
It seems like r_rawurlencode(array_keys($params)) can also be of type string; however, parameter $keys of array_combine() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

231
			/** @scrutinizer ignore-type */ r_rawurlencode(array_keys($params)),
Loading history...
232
			r_rawurlencode(array_values($params))
0 ignored issues
show
Bug introduced by
It seems like r_rawurlencode(array_values($params)) can also be of type string; however, parameter $values of array_combine() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

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