Passed
Push — gh-pages ( 22b0fe...eb2d91 )
by
unknown
12:27 queued 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
 * @created      28.08.2018
4
 * @author       smiley <[email protected]>
5
 * @copyright    2018 smiley
6
 * @license      MIT
7
 */
8
9
namespace chillerlan\HTTP\Psr7;
10
11
use InvalidArgumentException, TypeError;
12
use Psr\Http\Message\{MessageInterface, RequestInterface, ResponseInterface, UploadedFileInterface};
13
14
use function array_combine, array_keys, array_map, array_merge, array_values, call_user_func_array, count, explode,
15
	gzdecode, gzinflate, gzuncompress, implode, is_array, is_bool, is_iterable, is_numeric, is_scalar, is_string,
16
	json_decode, json_encode, parse_str, parse_url, rawurlencode, simplexml_load_string, sort, strtolower, trim,
17
	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)));
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|string[] $data
201
 *
202
 * @return string|string[]
203
 * @throws \TypeError
204
 */
205
function r_rawurlencode($data){
206
207
	if(is_array($data)){
208
		return array_map(__FUNCTION__, $data);
209
	}
210
211
	if(!is_scalar($data)){
0 ignored issues
show
introduced by
The condition is_scalar($data) is always true.
Loading history...
212
		throw new TypeError('$data is not scalar');
213
	}
214
215
	return rawurlencode((string)$data);
216
}
217
218
/**
219
 * from https://github.com/abraham/twitteroauth/blob/master/src/Util.php
220
 */
221
function build_http_query(array $params, bool $urlencode = null, string $delimiter = null, string $enclosure = null):string{
222
223
	if(empty($params)){
224
		return '';
225
	}
226
227
	// urlencode both keys and values
228
	if($urlencode ?? true){
229
		$params = array_combine(
230
			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

230
			/** @scrutinizer ignore-type */ r_rawurlencode(array_keys($params)),
Loading history...
231
			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

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