1<?php 2 3namespace Sabre\Uri; 4 5/** 6 * Resolves relative urls, like a browser would. 7 * 8 * This function takes a basePath, which itself _may_ also be relative, and 9 * then applies the relative path on top of it. 10 * 11 * @param string $basePath 12 * @param string $newPath 13 * @return string 14 */ 15function resolve($basePath, $newPath) { 16 17 $base = parse($basePath); 18 $delta = parse($newPath); 19 20 $pick = function($part) use ($base, $delta) { 21 22 if ($delta[$part]) { 23 return $delta[$part]; 24 } elseif ($base[$part]) { 25 return $base[$part]; 26 } 27 return null; 28 29 }; 30 31 // If the new path defines a scheme, it's absolute and we can just return 32 // that. 33 if ($delta['scheme']) { 34 return build($delta); 35 } 36 37 $newParts = []; 38 39 $newParts['scheme'] = $pick('scheme'); 40 $newParts['host'] = $pick('host'); 41 $newParts['port'] = $pick('port'); 42 43 $path = ''; 44 if ($delta['path']) { 45 // If the path starts with a slash 46 if ($delta['path'][0] === '/') { 47 $path = $delta['path']; 48 } else { 49 // Removing last component from base path. 50 $path = $base['path']; 51 if (strpos($path, '/') !== false) { 52 $path = substr($path, 0, strrpos($path, '/')); 53 } 54 $path .= '/' . $delta['path']; 55 } 56 } else { 57 $path = $base['path'] ?: '/'; 58 } 59 // Removing .. and . 60 $pathParts = explode('/', $path); 61 $newPathParts = []; 62 foreach($pathParts as $pathPart) { 63 64 switch($pathPart) { 65 //case '' : 66 case '.' : 67 break; 68 case '..' : 69 array_pop($newPathParts); 70 break; 71 default : 72 $newPathParts[] = $pathPart; 73 break; 74 } 75 } 76 77 $path = implode('/', $newPathParts); 78 79 // If the source url ended with a /, we want to preserve that. 80 $newParts['path'] = $path; 81 if ($delta['query']) { 82 $newParts['query'] = $delta['query']; 83 } elseif (!empty($base['query']) && empty($delta['host']) && empty($delta['path'])) { 84 // Keep the old query if host and path didn't change 85 $newParts['query'] = $base['query']; 86 } 87 if ($delta['fragment']) { 88 $newParts['fragment'] = $delta['fragment']; 89 } 90 return build($newParts); 91 92} 93 94/** 95 * Takes a URI or partial URI as its argument, and normalizes it. 96 * 97 * After normalizing a URI, you can safely compare it to other URIs. 98 * This function will for instance convert a %7E into a tilde, according to 99 * rfc3986. 100 * 101 * It will also change a %3a into a %3A. 102 * 103 * @param string $uri 104 * @return string 105 */ 106function normalize($uri) { 107 108 $parts = parse($uri); 109 110 if (!empty($parts['path'])) { 111 $pathParts = explode('/', ltrim($parts['path'], '/')); 112 $newPathParts = []; 113 foreach($pathParts as $pathPart) { 114 switch($pathPart) { 115 case '.': 116 // skip 117 break; 118 case '..' : 119 // One level up in the hierarchy 120 array_pop($newPathParts); 121 break; 122 default : 123 // Ensuring that everything is correctly percent-encoded. 124 $newPathParts[] = rawurlencode(rawurldecode($pathPart)); 125 break; 126 } 127 } 128 $parts['path'] = '/' . implode('/', $newPathParts); 129 } 130 131 if ($parts['scheme']) { 132 $parts['scheme'] = strtolower($parts['scheme']); 133 $defaultPorts = [ 134 'http' => '80', 135 'https' => '443', 136 ]; 137 138 if (!empty($parts['port']) && isset($defaultPorts[$parts['scheme']]) && $defaultPorts[$parts['scheme']] == $parts['port']) { 139 // Removing default ports. 140 unset($parts['port']); 141 } 142 // A few HTTP specific rules. 143 switch($parts['scheme']) { 144 case 'http' : 145 case 'https' : 146 if (empty($parts['path'])) { 147 // An empty path is equivalent to / in http. 148 $parts['path'] = '/'; 149 } 150 break; 151 } 152 } 153 154 if ($parts['host']) $parts['host'] = strtolower($parts['host']); 155 156 return build($parts); 157 158} 159 160/** 161 * Parses a URI and returns its individual components. 162 * 163 * This method largely behaves the same as PHP's parse_url, except that it will 164 * return an array with all the array keys, including the ones that are not 165 * set by parse_url, which makes it a bit easier to work with. 166 * 167 * @param string $uri 168 * @return array 169 */ 170function parse($uri) { 171 172 return 173 parse_url($uri) + [ 174 'scheme' => null, 175 'host' => null, 176 'path' => null, 177 'port' => null, 178 'user' => null, 179 'query' => null, 180 'fragment' => null, 181 ]; 182 183} 184 185/** 186 * This function takes the components returned from PHP's parse_url, and uses 187 * it to generate a new uri. 188 * 189 * @param array $parts 190 * @return string 191 */ 192function build(array $parts) { 193 194 $uri = ''; 195 196 $authority = ''; 197 if (!empty($parts['host'])) { 198 $authority = $parts['host']; 199 if (!empty($parts['user'])) { 200 $authority = $parts['user'] . '@' . $authority; 201 } 202 if (!empty($parts['port'])) { 203 $authority = $authority . ':' . $parts['port']; 204 } 205 } 206 207 if (!empty($parts['scheme'])) { 208 // If there's a scheme, there's also a host. 209 $uri = $parts['scheme'] . ':'; 210 211 } 212 if ($authority) { 213 // No scheme, but there is a host. 214 $uri .= '//' . $authority; 215 216 } 217 218 if (!empty($parts['path'])) { 219 $uri .= $parts['path']; 220 } 221 if (!empty($parts['query'])) { 222 $uri .= '?' . $parts['query']; 223 } 224 if (!empty($parts['fragment'])) { 225 $uri .= '#' . $parts['fragment']; 226 } 227 228 return $uri; 229 230} 231 232/** 233 * Returns the 'dirname' and 'basename' for a path. 234 * 235 * The reason there is a custom function for this purpose, is because 236 * basename() is locale aware (behaviour changes if C locale or a UTF-8 locale 237 * is used) and we need a method that just operates on UTF-8 characters. 238 * 239 * In addition basename and dirname are platform aware, and will treat 240 * backslash (\) as a directory separator on windows. 241 * 242 * This method returns the 2 components as an array. 243 * 244 * If there is no dirname, it will return an empty string. Any / appearing at 245 * the end of the string is stripped off. 246 * 247 * @param string $path 248 * @return array 249 */ 250function split($path) { 251 252 $matches = []; 253 if(preg_match('/^(?:(?:(.*)(?:\/+))?([^\/]+))(?:\/?)$/u', $path, $matches)) { 254 return [$matches[1], $matches[2]]; 255 } 256 return [null,null]; 257 258} 259