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}