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