friendship ended with social-app. php is my new best friend
1<?php
2/**
3 * Class UriUtils
4 *
5 * @created 22.10.2022
6 * @author smiley <smiley@chillerlan.net>
7 * @copyright 2022 smiley
8 * @license MIT
9 */
10declare(strict_types=1);
11
12namespace chillerlan\HTTP\Utils;
13
14use Psr\Http\Message\UriInterface;
15use function array_filter, array_map, explode, implode, parse_url, preg_match,
16 preg_replace_callback, rawurldecode, urldecode, urlencode;
17
18final class UriUtil{
19
20 public const URI_DEFAULT_PORTS = [
21 'http' => 80,
22 'https' => 443,
23 'ftp' => 21,
24 'gopher' => 70,
25 'nntp' => 119,
26 'news' => 119,
27 'telnet' => 23,
28 'tn3270' => 23,
29 'imap' => 143,
30 'pop' => 110,
31 'ldap' => 389,
32 ];
33
34 /**
35 * Checks whether the UriInterface has a port set and if that port is one of the default ports for the given scheme
36 */
37 public static function isDefaultPort(UriInterface $uri):bool{
38 $port = $uri->getPort();
39 $scheme = $uri->getScheme();
40
41 return $port === null || (isset(self::URI_DEFAULT_PORTS[$scheme]) && $port === self::URI_DEFAULT_PORTS[$scheme]);
42 }
43
44 /**
45 * Checks Whether the URI is absolute, i.e. it has a scheme.
46 *
47 * An instance of UriInterface can either be an absolute URI or a relative reference. This method returns true
48 * if it is the former. An absolute URI has a scheme. A relative reference is used to express a URI relative
49 * to another URI, the base URI. Relative references can be divided into several forms:
50 * - network-path references, e.g. '//example.com/path'
51 * - absolute-path references, e.g. '/path'
52 * - relative-path references, e.g. 'subpath'
53 *
54 * @see self::isNetworkPathReference
55 * @see self::isAbsolutePathReference
56 * @see self::isRelativePathReference
57 * @see https://tools.ietf.org/html/rfc3986#section-4
58 */
59 public static function isAbsolute(UriInterface $uri):bool{
60 return $uri->getScheme() !== '';
61 }
62
63 /**
64 * Checks Whether the URI is a network-path reference.
65 *
66 * A relative reference that begins with two slash characters is termed a network-path reference.
67 *
68 * @link https://tools.ietf.org/html/rfc3986#section-4.2
69 */
70 public static function isNetworkPathReference(UriInterface $uri):bool{
71 return $uri->getScheme() === '' && $uri->getAuthority() !== '';
72 }
73
74 /**
75 * Checks Whether the URI is an absolute-path reference.
76 *
77 * A relative reference that begins with a single slash character is termed an absolute-path reference.
78 *
79 * @link https://tools.ietf.org/html/rfc3986#section-4.2
80 */
81 public static function isAbsolutePathReference(UriInterface $uri):bool{
82 return $uri->getScheme() === '' && $uri->getAuthority() === '' && isset($uri->getPath()[0]) && $uri->getPath()[0] === '/';
83 }
84
85 /**
86 * Checks Whether the URI is a relative-path reference.
87 *
88 * A relative reference that does not begin with a slash character is termed a relative-path reference.
89 *
90 * @link https://tools.ietf.org/html/rfc3986#section-4.2
91 */
92 public static function isRelativePathReference(UriInterface $uri):bool{
93 return $uri->getScheme() === '' && $uri->getAuthority() === '' && (!isset($uri->getPath()[0]) || $uri->getPath()[0] !== '/');
94 }
95
96 /**
97 * Removes a specific query string value.
98 *
99 * Any existing query string values that exactly match the provided $key are removed.
100 */
101 public static function withoutQueryValue(UriInterface $uri, string $key):UriInterface{
102 $current = $uri->getQuery();
103
104 if($current === ''){
105 return $uri;
106 }
107
108 $result = array_filter(explode('&', $current), fn($part) => rawurldecode(explode('=', $part)[0]) !== rawurldecode($key));
109
110 return $uri->withQuery(implode('&', $result));
111 }
112
113 /**
114 * Adds a specific query string value.
115 *
116 * Any existing query string values that exactly match the provided $key are
117 * removed and replaced with the given $key $value pair.
118 *
119 * A value of null will set the query string key without a value, e.g. "key" instead of "key=value".
120 */
121 public static function withQueryValue(UriInterface $uri, string $key, string|null $value = null):UriInterface{
122 $current = $uri->getQuery();
123
124 $result = ($current !== '')
125 ? array_filter(explode('&', $current), fn($part) => rawurldecode(explode('=', $part)[0]) !== rawurldecode($key))
126 : [];
127
128 // Query string separators ("=", "&") within the key or value need to be encoded
129 // (while preventing double-encoding) before setting the query string. All other
130 // chars that need percent-encoding will be encoded by withQuery().
131 $replaceQuery = ['=' => '%3D', '&' => '%26'];
132 $key = strtr($key, $replaceQuery);
133
134 $result[] = ($value !== null)
135 ? $key.'='.strtr($value, $replaceQuery)
136 : $key;
137
138 return $uri->withQuery(implode('&', $result));
139 }
140
141 /**
142 * UTF-8 aware \parse_url() replacement.
143 *
144 * The internal function produces broken output for non ASCII domain names
145 * (IDN) when used with locales other than "C".
146 *
147 * On the other hand, cURL understands IDN correctly only when UTF-8 locale
148 * is configured ("C.UTF-8", "en_US.UTF-8", etc.).
149 *
150 * @see https://bugs.php.net/bug.php?id=52923
151 * @see https://www.php.net/manual/en/function.parse-url.php#114817
152 * @see https://curl.haxx.se/libcurl/c/CURLOPT_URL.html#ENCODING
153 *
154 * @link https://github.com/guzzle/psr7/blob/c0dcda9f54d145bd4d062a6d15f54931a67732f9/src/Uri.php#L89-L130
155 *
156 * @return array{scheme?: string, host?: int|string, port?: string, user?: string, pass?: string, path?: string, query?: string, fragment?: string}|null
157 */
158 public static function parseUrl(string $url):array|null{
159 // If IPv6
160 $prefix = '';
161 /** @noinspection RegExpRedundantEscape */
162 if(preg_match('%^(.*://\[[0-9:a-f]+\])(.*?)$%', $url, $matches)){
163 /** @var array{0:string, 1:string, 2:string} $matches */
164 $prefix = $matches[1];
165 $url = $matches[2];
166 }
167
168 $encodedUrl = preg_replace_callback('%[^:/@?&=#]+%usD', fn($matches) => urlencode($matches[0]), $url);
169 $result = parse_url($prefix.$encodedUrl);
170
171 if($result === false){
172 return null;
173 }
174
175 $parsed = array_map(urldecode(...), $result);
176
177 if(isset($parsed['port'])){
178 $parsed['port'] = (int)$parsed['port'];
179 }
180
181 return $parsed;
182 }
183
184}