1<?php 2 3/** 4 * League.Uri (https://uri.thephpleague.com) 5 * 6 * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> 7 * 8 * For the full copyright and license information, please view the LICENSE 9 * file that was distributed with this source code. 10 */ 11 12declare(strict_types=1); 13 14namespace League\Uri; 15 16use League\Uri\Contracts\UriInterface; 17use Psr\Http\Message\UriInterface as Psr7UriInterface; 18use function array_pop; 19use function array_reduce; 20use function count; 21use function end; 22use function explode; 23use function gettype; 24use function implode; 25use function in_array; 26use function sprintf; 27use function str_repeat; 28use function strpos; 29use function substr; 30 31final class UriResolver 32{ 33 /** 34 * @var array<string,int> 35 */ 36 const DOT_SEGMENTS = ['.' => 1, '..' => 1]; 37 38 /** 39 * @codeCoverageIgnore 40 */ 41 private function __construct() 42 { 43 } 44 45 /** 46 * Resolve an URI against a base URI using RFC3986 rules. 47 * 48 * If the first argument is a UriInterface the method returns a UriInterface object 49 * If the first argument is a Psr7UriInterface the method returns a Psr7UriInterface object 50 * 51 * @param Psr7UriInterface|UriInterface $uri 52 * @param Psr7UriInterface|UriInterface $base_uri 53 * 54 * @return Psr7UriInterface|UriInterface 55 */ 56 public static function resolve($uri, $base_uri) 57 { 58 self::filterUri($uri); 59 self::filterUri($base_uri); 60 $null = $uri instanceof Psr7UriInterface ? '' : null; 61 62 if ($null !== $uri->getScheme()) { 63 return $uri 64 ->withPath(self::removeDotSegments($uri->getPath())); 65 } 66 67 if ($null !== $uri->getAuthority()) { 68 return $uri 69 ->withScheme($base_uri->getScheme()) 70 ->withPath(self::removeDotSegments($uri->getPath())); 71 } 72 73 $user = $null; 74 $pass = null; 75 $userInfo = $base_uri->getUserInfo(); 76 if (null !== $userInfo) { 77 [$user, $pass] = explode(':', $userInfo, 2) + [1 => null]; 78 } 79 80 [$uri_path, $uri_query] = self::resolvePathAndQuery($uri, $base_uri); 81 82 return $uri 83 ->withPath(self::removeDotSegments($uri_path)) 84 ->withQuery($uri_query) 85 ->withHost($base_uri->getHost()) 86 ->withPort($base_uri->getPort()) 87 ->withUserInfo((string) $user, $pass) 88 ->withScheme($base_uri->getScheme()) 89 ; 90 } 91 92 /** 93 * Filter the URI object. 94 * 95 * @param mixed $uri an URI object 96 * 97 * @throws \TypeError if the URI object does not implements the supported interfaces. 98 */ 99 private static function filterUri($uri): void 100 { 101 if (!$uri instanceof UriInterface && !$uri instanceof Psr7UriInterface) { 102 throw new \TypeError(sprintf('The uri must be a valid URI object received `%s`', gettype($uri))); 103 } 104 } 105 106 /** 107 * Remove dot segments from the URI path. 108 */ 109 private static function removeDotSegments(string $path): string 110 { 111 if (false === strpos($path, '.')) { 112 return $path; 113 } 114 115 $old_segments = explode('/', $path); 116 $new_path = implode('/', array_reduce($old_segments, [UriResolver::class, 'reducer'], [])); 117 if (isset(self::DOT_SEGMENTS[end($old_segments)])) { 118 $new_path .= '/'; 119 } 120 121 // @codeCoverageIgnoreStart 122 // added because some PSR-7 implementations do not respect RFC3986 123 if (0 === strpos($path, '/') && 0 !== strpos($new_path, '/')) { 124 return '/'.$new_path; 125 } 126 // @codeCoverageIgnoreEnd 127 128 return $new_path; 129 } 130 131 /** 132 * Remove dot segments. 133 * 134 * @return array<int, string> 135 */ 136 private static function reducer(array $carry, string $segment): array 137 { 138 if ('..' === $segment) { 139 array_pop($carry); 140 141 return $carry; 142 } 143 144 if (!isset(self::DOT_SEGMENTS[$segment])) { 145 $carry[] = $segment; 146 } 147 148 return $carry; 149 } 150 151 /** 152 * Resolve an URI path and query component. 153 * 154 * @param Psr7UriInterface|UriInterface $uri 155 * @param Psr7UriInterface|UriInterface $base_uri 156 * 157 * @return array{0:string, 1:string|null} 158 */ 159 private static function resolvePathAndQuery($uri, $base_uri): array 160 { 161 $target_path = $uri->getPath(); 162 $target_query = $uri->getQuery(); 163 $null = $uri instanceof Psr7UriInterface ? '' : null; 164 $baseNull = $base_uri instanceof Psr7UriInterface ? '' : null; 165 166 if (0 === strpos($target_path, '/')) { 167 return [$target_path, $target_query]; 168 } 169 170 if ('' === $target_path) { 171 if ($null === $target_query) { 172 $target_query = $base_uri->getQuery(); 173 } 174 175 $target_path = $base_uri->getPath(); 176 //@codeCoverageIgnoreStart 177 //because some PSR-7 Uri implementations allow this RFC3986 forbidden construction 178 if ($baseNull !== $base_uri->getAuthority() && 0 !== strpos($target_path, '/')) { 179 $target_path = '/'.$target_path; 180 } 181 //@codeCoverageIgnoreEnd 182 183 return [$target_path, $target_query]; 184 } 185 186 $base_path = $base_uri->getPath(); 187 if ($baseNull !== $base_uri->getAuthority() && '' === $base_path) { 188 $target_path = '/'.$target_path; 189 } 190 191 if ('' !== $base_path) { 192 $segments = explode('/', $base_path); 193 array_pop($segments); 194 if ([] !== $segments) { 195 $target_path = implode('/', $segments).'/'.$target_path; 196 } 197 } 198 199 return [$target_path, $target_query]; 200 } 201 202 /** 203 * Relativize an URI according to a base URI. 204 * 205 * This method MUST retain the state of the submitted URI instance, and return 206 * an URI instance of the same type that contains the applied modifications. 207 * 208 * This method MUST be transparent when dealing with error and exceptions. 209 * It MUST not alter of silence them apart from validating its own parameters. 210 * 211 * @param Psr7UriInterface|UriInterface $uri 212 * @param Psr7UriInterface|UriInterface $base_uri 213 * 214 * @return Psr7UriInterface|UriInterface 215 */ 216 public static function relativize($uri, $base_uri) 217 { 218 self::filterUri($uri); 219 self::filterUri($base_uri); 220 $uri = self::formatHost($uri); 221 $base_uri = self::formatHost($base_uri); 222 if (!self::isRelativizable($uri, $base_uri)) { 223 return $uri; 224 } 225 226 $null = $uri instanceof Psr7UriInterface ? '' : null; 227 $uri = $uri->withScheme($null)->withPort(null)->withUserInfo($null)->withHost($null); 228 $target_path = $uri->getPath(); 229 if ($target_path !== $base_uri->getPath()) { 230 return $uri->withPath(self::relativizePath($target_path, $base_uri->getPath())); 231 } 232 233 if (self::componentEquals('getQuery', $uri, $base_uri)) { 234 return $uri->withPath('')->withQuery($null); 235 } 236 237 if ($null === $uri->getQuery()) { 238 return $uri->withPath(self::formatPathWithEmptyBaseQuery($target_path)); 239 } 240 241 return $uri->withPath(''); 242 } 243 244 /** 245 * Tells whether the component value from both URI object equals. 246 * 247 * @param Psr7UriInterface|UriInterface $uri 248 * @param Psr7UriInterface|UriInterface $base_uri 249 */ 250 private static function componentEquals(string $method, $uri, $base_uri): bool 251 { 252 return self::getComponent($method, $uri) === self::getComponent($method, $base_uri); 253 } 254 255 /** 256 * Returns the component value from the submitted URI object. 257 * 258 * @param Psr7UriInterface|UriInterface $uri 259 */ 260 private static function getComponent(string $method, $uri): ?string 261 { 262 $component = $uri->$method(); 263 if ($uri instanceof Psr7UriInterface && '' === $component) { 264 return null; 265 } 266 267 return $component; 268 } 269 270 /** 271 * Filter the URI object. 272 * 273 * @param null|mixed $uri 274 * 275 * @throws \TypeError if the URI object does not implements the supported interfaces. 276 * 277 * @return Psr7UriInterface|UriInterface 278 */ 279 private static function formatHost($uri) 280 { 281 if (!$uri instanceof Psr7UriInterface) { 282 return $uri; 283 } 284 285 $host = $uri->getHost(); 286 if ('' === $host) { 287 return $uri; 288 } 289 290 return $uri->withHost((string) Uri::createFromComponents(['host' => $host])->getHost()); 291 } 292 293 /** 294 * Tell whether the submitted URI object can be relativize. 295 * 296 * @param Psr7UriInterface|UriInterface $uri 297 * @param Psr7UriInterface|UriInterface $base_uri 298 */ 299 private static function isRelativizable($uri, $base_uri): bool 300 { 301 return !UriInfo::isRelativePath($uri) 302 && self::componentEquals('getScheme', $uri, $base_uri) 303 && self::componentEquals('getAuthority', $uri, $base_uri); 304 } 305 306 /** 307 * Relative the URI for a authority-less target URI. 308 */ 309 private static function relativizePath(string $path, string $basepath): string 310 { 311 $base_segments = self::getSegments($basepath); 312 $target_segments = self::getSegments($path); 313 $target_basename = array_pop($target_segments); 314 array_pop($base_segments); 315 foreach ($base_segments as $offset => $segment) { 316 if (!isset($target_segments[$offset]) || $segment !== $target_segments[$offset]) { 317 break; 318 } 319 unset($base_segments[$offset], $target_segments[$offset]); 320 } 321 $target_segments[] = $target_basename; 322 323 return self::formatPath( 324 str_repeat('../', count($base_segments)).implode('/', $target_segments), 325 $basepath 326 ); 327 } 328 329 /** 330 * returns the path segments. 331 * 332 * @return string[] 333 */ 334 private static function getSegments(string $path): array 335 { 336 if ('' !== $path && '/' === $path[0]) { 337 $path = substr($path, 1); 338 } 339 340 return explode('/', $path); 341 } 342 343 /** 344 * Formatting the path to keep a valid URI. 345 */ 346 private static function formatPath(string $path, string $basepath): string 347 { 348 if ('' === $path) { 349 return in_array($basepath, ['', '/'], true) ? $basepath : './'; 350 } 351 352 if (false === ($colon_pos = strpos($path, ':'))) { 353 return $path; 354 } 355 356 $slash_pos = strpos($path, '/'); 357 if (false === $slash_pos || $colon_pos < $slash_pos) { 358 return "./$path"; 359 } 360 361 return $path; 362 } 363 364 /** 365 * Formatting the path to keep a resolvable URI. 366 */ 367 private static function formatPathWithEmptyBaseQuery(string $path): string 368 { 369 $target_segments = self::getSegments($path); 370 /** @var string $basename */ 371 $basename = end($target_segments); 372 373 return '' === $basename ? './' : $basename; 374 } 375} 376