Passed
Push — main ( b6169a...2d2e59 )
by smiley
10:24
created

uriIsDefaultPort()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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