Passed
Push — main ( 404000...b70a85 )
by smiley
01:56
created

parseUrl()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 18
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 9
c 0
b 0
f 0
nc 4
nop 1
dl 0
loc 18
rs 9.9666
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 TypeError;
12
use Psr\Http\Message\{MessageInterface, RequestInterface, ResponseInterface, UriInterface};
13
14
use function array_filter, array_map, explode, gzdecode, gzinflate, gzuncompress, implode,
15
	is_array, is_scalar, json_decode, json_encode, parse_url, preg_match, preg_replace_callback, rawurldecode,
16
	rawurlencode, simplexml_load_string, trim, urlencode;
17
18
const PSR7_INCLUDES = true;
19
20
/**
21
 * @param string|string[] $data
22
 *
23
 * @return string|string[]
24
 * @throws \TypeError
25
 */
26
function r_rawurlencode($data){
27
28
	if(is_array($data)){
29
		return array_map(__FUNCTION__, $data);
30
	}
31
32
	if(!is_scalar($data) && $data !== null){
0 ignored issues
show
introduced by
The condition is_scalar($data) is always true.
Loading history...
33
		throw new TypeError('$data is neither scalar nor null');
34
	}
35
36
	return rawurlencode((string)$data);
37
}
38
39
/**
40
 * @param \Psr\Http\Message\MessageInterface $message
41
 * @param bool|null                          $assoc
42
 *
43
 * @return \stdClass|array|bool
44
 */
45
function get_json(MessageInterface $message, bool $assoc = null){
46
	$data = json_decode($message->getBody()->__toString(), $assoc);
47
48
	$message->getBody()->rewind();
49
50
	return $data;
51
}
52
53
/**
54
 * @param \Psr\Http\Message\MessageInterface $message
55
 * @param bool|null                          $assoc
56
 *
57
 * @return \SimpleXMLElement|array|bool
58
 */
59
function get_xml(MessageInterface $message, bool $assoc = null){
60
	$data = simplexml_load_string($message->getBody()->__toString());
61
62
	$message->getBody()->rewind();
63
64
	return $assoc === true
65
		? json_decode(json_encode($data), true) // cruel
66
		: $data;
67
}
68
69
/**
70
 * Returns the string representation of an HTTP message. (from Guzzle)
71
 *
72
 * @param \Psr\Http\Message\MessageInterface $message Message to convert to a string.
73
 *
74
 * @return string
75
 */
76
function message_to_string(MessageInterface $message):string{
77
	$msg = '';
78
79
	if($message instanceof RequestInterface){
80
		$msg = trim($message->getMethod().' '.$message->getRequestTarget()).' HTTP/'.$message->getProtocolVersion();
81
82
		if(!$message->hasHeader('host')){
83
			$msg .= "\r\nHost: ".$message->getUri()->getHost();
84
		}
85
86
	}
87
	elseif($message instanceof ResponseInterface){
88
		$msg = 'HTTP/'.$message->getProtocolVersion().' '.$message->getStatusCode().' '.$message->getReasonPhrase();
89
	}
90
91
	foreach($message->getHeaders() as $name => $values){
92
		$msg .= "\r\n".$name.': '.implode(', ', $values);
93
	}
94
95
	$data = $message->getBody()->__toString();
96
	$message->getBody()->rewind();
97
98
	return $msg."\r\n\r\n".$data;
99
}
100
101
/**
102
 * Decompresses the message content according to the Content-Encoding header and returns the decompressed data
103
 *
104
 * @param \Psr\Http\Message\MessageInterface $message
105
 *
106
 * @return string
107
 */
108
function decompress_content(MessageInterface $message):string{
109
	$data = $message->getBody()->__toString();
110
	$message->getBody()->rewind();
111
112
	switch($message->getHeaderLine('content-encoding')){
113
#		case 'br'      : return brotli_uncompress($data); // @todo: https://github.com/kjdev/php-ext-brotli
114
		case 'compress': return gzuncompress($data);
115
		case 'deflate' : return gzinflate($data);
116
		case 'gzip'    : return gzdecode($data);
117
		default: return $data;
118
	}
119
120
}
121
122
const URI_DEFAULT_PORTS = [
123
	'http'   => 80,
124
	'https'  => 443,
125
	'ftp'    => 21,
126
	'gopher' => 70,
127
	'nntp'   => 119,
128
	'news'   => 119,
129
	'telnet' => 23,
130
	'tn3270' => 23,
131
	'imap'   => 143,
132
	'pop'    => 110,
133
	'ldap'   => 389,
134
];
135
136
function uriIsDefaultPort(UriInterface $uri):bool{
137
	$port   = $uri->getPort();
138
	$scheme = $uri->getScheme();
139
140
	return $port === null || (isset(URI_DEFAULT_PORTS[$scheme]) && $port === URI_DEFAULT_PORTS[$scheme]);
141
}
142
143
/**
144
 * Whether the URI is absolute, i.e. it has a scheme.
145
 *
146
 * An instance of UriInterface can either be an absolute URI or a relative reference. This method returns true
147
 * if it is the former. An absolute URI has a scheme. A relative reference is used to express a URI relative
148
 * to another URI, the base URI. Relative references can be divided into several forms:
149
 * - network-path references, e.g. '//example.com/path'
150
 * - absolute-path references, e.g. '/path'
151
 * - relative-path references, e.g. 'subpath'
152
 *
153
 * @see  Uri::isNetworkPathReference
154
 * @see  Uri::isAbsolutePathReference
155
 * @see  Uri::isRelativePathReference
156
 * @link https://tools.ietf.org/html/rfc3986#section-4
157
 */
158
function uriIsAbsolute(UriInterface $uri):bool{
159
	return $uri->getScheme() !== '';
160
}
161
162
/**
163
 * Whether the URI is a network-path reference.
164
 *
165
 * A relative reference that begins with two slash characters is termed an network-path reference.
166
 *
167
 * @link https://tools.ietf.org/html/rfc3986#section-4.2
168
 */
169
function uriIsNetworkPathReference(UriInterface $uri):bool{
170
	return $uri->getScheme() === '' && $uri->getAuthority() !== '';
171
}
172
173
/**
174
 * Whether the URI is a absolute-path reference.
175
 *
176
 * A relative reference that begins with a single slash character is termed an absolute-path reference.
177
 *
178
 * @link https://tools.ietf.org/html/rfc3986#section-4.2
179
 */
180
function uriIsAbsolutePathReference(UriInterface $uri):bool{
181
	return $uri->getScheme() === '' && $uri->getAuthority() === '' && isset($uri->getPath()[0]) && $uri->getPath()[0] === '/';
182
}
183
184
/**
185
 * Whether the URI is a relative-path reference.
186
 *
187
 * A relative reference that does not begin with a slash character is termed a relative-path reference.
188
 *
189
 * @return bool
190
 * @link https://tools.ietf.org/html/rfc3986#section-4.2
191
 */
192
function uriIsRelativePathReference(UriInterface $uri):bool{
193
	return $uri->getScheme() === '' && $uri->getAuthority() === '' && (!isset($uri->getPath()[0]) || $uri->getPath()[0] !== '/');
194
}
195
196
/**
197
 * removes a specific query string value.
198
 *
199
 * Any existing query string values that exactly match the provided key are
200
 * removed.
201
 *
202
 * @param string $key Query string key to remove.
203
 */
204
function uriWithoutQueryValue(UriInterface $uri, string $key):UriInterface{
205
	$current = $uri->getQuery();
206
207
	if($current === ''){
208
		return $uri;
209
	}
210
211
	$decodedKey = rawurldecode($key);
212
213
	$result = array_filter(explode('&', $current), function($part) use ($decodedKey){
214
		return rawurldecode(explode('=', $part)[0]) !== $decodedKey;
215
	});
216
217
	return $uri->withQuery(implode('&', $result));
218
}
219
220
/**
221
 * adds a specific query string value.
222
 *
223
 * Any existing query string values that exactly match the provided key are
224
 * removed and replaced with the given key value pair.
225
 *
226
 * A value of null will set the query string key without a value, e.g. "key"
227
 * instead of "key=value".
228
 *
229
 * @param string      $key   Key to set.
230
 * @param string|null $value Value to set
231
 */
232
function uriWithQueryValue(UriInterface $uri, string $key, string $value = null):UriInterface{
233
	$current = $uri->getQuery();
234
235
	if($current === ''){
236
		$result = [];
237
	}
238
	else{
239
		$decodedKey = rawurldecode($key);
240
		$result     = array_filter(explode('&', $current), function($part) use ($decodedKey){
241
			return rawurldecode(explode('=', $part)[0]) !== $decodedKey;
242
		});
243
	}
244
245
	// Query string separators ("=", "&") within the key or value need to be encoded
246
	// (while preventing double-encoding) before setting the query string. All other
247
	// chars that need percent-encoding will be encoded by withQuery().
248
	$replaceQuery = ['=' => '%3D', '&' => '%26'];
249
	$key          = strtr($key, $replaceQuery);
250
251
	$result[] = $value !== null
252
		? $key.'='.strtr($value, $replaceQuery)
253
		: $key;
254
255
	return $uri->withQuery(implode('&', $result));
256
}
257
258
/**
259
 * UTF-8 aware \parse_url() replacement.
260
 *
261
 * The internal function produces broken output for non ASCII domain names
262
 * (IDN) when used with locales other than "C".
263
 *
264
 * On the other hand, cURL understands IDN correctly only when UTF-8 locale
265
 * is configured ("C.UTF-8", "en_US.UTF-8", etc.).
266
 *
267
 * @see https://bugs.php.net/bug.php?id=52923
268
 * @see https://www.php.net/manual/en/function.parse-url.php#114817
269
 * @see https://curl.haxx.se/libcurl/c/CURLOPT_URL.html#ENCODING
270
 *
271
 * @link https://github.com/guzzle/psr7/blob/c0dcda9f54d145bd4d062a6d15f54931a67732f9/src/Uri.php#L89-L130
272
 */
273
function parseUrl(string $url):?array{
274
	// If IPv6
275
	$prefix = '';
276
	/** @noinspection RegExpRedundantEscape */
277
	if(preg_match('%^(.*://\[[0-9:a-f]+\])(.*?)$%', $url, $matches)){
278
		/** @var array{0:string, 1:string, 2:string} $matches */
279
		$prefix = $matches[1];
280
		$url    = $matches[2];
281
	}
282
283
	$encodedUrl = preg_replace_callback('%[^:/@?&=#]+%usD', fn($matches) => urlencode($matches[0]), $url);
284
	$result     = parse_url($prefix.$encodedUrl);
285
286
	if($result === false){
287
		return null;
288
	}
289
290
	return array_map('urldecode', $result);
291
}
292