UriResolver.php 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. <?php
  2. namespace GuzzleHttp\Psr7;
  3. use Psr\Http\Message\UriInterface;
  4. /**
  5. * Resolves a URI reference in the context of a base URI and the opposite way.
  6. *
  7. * @author Tobias Schultze
  8. *
  9. * @link https://tools.ietf.org/html/rfc3986#section-5
  10. */
  11. final class UriResolver
  12. {
  13. /**
  14. * Removes dot segments from a path and returns the new path.
  15. *
  16. * @param string $path
  17. *
  18. * @return string
  19. * @link http://tools.ietf.org/html/rfc3986#section-5.2.4
  20. */
  21. public static function removeDotSegments($path)
  22. {
  23. if ($path === '' || $path === '/') {
  24. return $path;
  25. }
  26. $results = [];
  27. $segments = explode('/', $path);
  28. foreach ($segments as $segment) {
  29. if ($segment === '..') {
  30. array_pop($results);
  31. } elseif ($segment !== '.') {
  32. $results[] = $segment;
  33. }
  34. }
  35. $newPath = implode('/', $results);
  36. if ($path[0] === '/' && (!isset($newPath[0]) || $newPath[0] !== '/')) {
  37. // Re-add the leading slash if necessary for cases like "/.."
  38. $newPath = '/' . $newPath;
  39. } elseif ($newPath !== '' && ($segment === '.' || $segment === '..')) {
  40. // Add the trailing slash if necessary
  41. // If newPath is not empty, then $segment must be set and is the last segment from the foreach
  42. $newPath .= '/';
  43. }
  44. return $newPath;
  45. }
  46. /**
  47. * Converts the relative URI into a new URI that is resolved against the base URI.
  48. *
  49. * @param UriInterface $base Base URI
  50. * @param UriInterface $rel Relative URI
  51. *
  52. * @return UriInterface
  53. * @link http://tools.ietf.org/html/rfc3986#section-5.2
  54. */
  55. public static function resolve(UriInterface $base, UriInterface $rel)
  56. {
  57. if ((string) $rel === '') {
  58. // we can simply return the same base URI instance for this same-document reference
  59. return $base;
  60. }
  61. if ($rel->getScheme() != '') {
  62. return $rel->withPath(self::removeDotSegments($rel->getPath()));
  63. }
  64. if ($rel->getAuthority() != '') {
  65. $targetAuthority = $rel->getAuthority();
  66. $targetPath = self::removeDotSegments($rel->getPath());
  67. $targetQuery = $rel->getQuery();
  68. } else {
  69. $targetAuthority = $base->getAuthority();
  70. if ($rel->getPath() === '') {
  71. $targetPath = $base->getPath();
  72. $targetQuery = $rel->getQuery() != '' ? $rel->getQuery() : $base->getQuery();
  73. } else {
  74. if ($rel->getPath()[0] === '/') {
  75. $targetPath = $rel->getPath();
  76. } else {
  77. if ($targetAuthority != '' && $base->getPath() === '') {
  78. $targetPath = '/' . $rel->getPath();
  79. } else {
  80. $lastSlashPos = strrpos($base->getPath(), '/');
  81. if ($lastSlashPos === false) {
  82. $targetPath = $rel->getPath();
  83. } else {
  84. $targetPath = substr($base->getPath(), 0, $lastSlashPos + 1) . $rel->getPath();
  85. }
  86. }
  87. }
  88. $targetPath = self::removeDotSegments($targetPath);
  89. $targetQuery = $rel->getQuery();
  90. }
  91. }
  92. return new Uri(Uri::composeComponents(
  93. $base->getScheme(),
  94. $targetAuthority,
  95. $targetPath,
  96. $targetQuery,
  97. $rel->getFragment()
  98. ));
  99. }
  100. /**
  101. * Returns the target URI as a relative reference from the base URI.
  102. *
  103. * This method is the counterpart to resolve():
  104. *
  105. * (string) $target === (string) UriResolver::resolve($base, UriResolver::relativize($base, $target))
  106. *
  107. * One use-case is to use the current request URI as base URI and then generate relative links in your documents
  108. * to reduce the document size or offer self-contained downloadable document archives.
  109. *
  110. * $base = new Uri('http://example.com/a/b/');
  111. * echo UriResolver::relativize($base, new Uri('http://example.com/a/b/c')); // prints 'c'.
  112. * echo UriResolver::relativize($base, new Uri('http://example.com/a/x/y')); // prints '../x/y'.
  113. * echo UriResolver::relativize($base, new Uri('http://example.com/a/b/?q')); // prints '?q'.
  114. * echo UriResolver::relativize($base, new Uri('http://example.org/a/b/')); // prints '//example.org/a/b/'.
  115. *
  116. * This method also accepts a target that is already relative and will try to relativize it further. Only a
  117. * relative-path reference will be returned as-is.
  118. *
  119. * echo UriResolver::relativize($base, new Uri('/a/b/c')); // prints 'c' as well
  120. *
  121. * @param UriInterface $base Base URI
  122. * @param UriInterface $target Target URI
  123. *
  124. * @return UriInterface The relative URI reference
  125. */
  126. public static function relativize(UriInterface $base, UriInterface $target)
  127. {
  128. if ($target->getScheme() !== '' &&
  129. ($base->getScheme() !== $target->getScheme() || $target->getAuthority() === '' && $base->getAuthority() !== '')
  130. ) {
  131. return $target;
  132. }
  133. if (Uri::isRelativePathReference($target)) {
  134. // As the target is already highly relative we return it as-is. It would be possible to resolve
  135. // the target with `$target = self::resolve($base, $target);` and then try make it more relative
  136. // by removing a duplicate query. But let's not do that automatically.
  137. return $target;
  138. }
  139. if ($target->getAuthority() !== '' && $base->getAuthority() !== $target->getAuthority()) {
  140. return $target->withScheme('');
  141. }
  142. // We must remove the path before removing the authority because if the path starts with two slashes, the URI
  143. // would turn invalid. And we also cannot set a relative path before removing the authority, as that is also
  144. // invalid.
  145. $emptyPathUri = $target->withScheme('')->withPath('')->withUserInfo('')->withPort(null)->withHost('');
  146. if ($base->getPath() !== $target->getPath()) {
  147. return $emptyPathUri->withPath(self::getRelativePath($base, $target));
  148. }
  149. if ($base->getQuery() === $target->getQuery()) {
  150. // Only the target fragment is left. And it must be returned even if base and target fragment are the same.
  151. return $emptyPathUri->withQuery('');
  152. }
  153. // If the base URI has a query but the target has none, we cannot return an empty path reference as it would
  154. // inherit the base query component when resolving.
  155. if ($target->getQuery() === '') {
  156. $segments = explode('/', $target->getPath());
  157. $lastSegment = end($segments);
  158. return $emptyPathUri->withPath($lastSegment === '' ? './' : $lastSegment);
  159. }
  160. return $emptyPathUri;
  161. }
  162. private static function getRelativePath(UriInterface $base, UriInterface $target)
  163. {
  164. $sourceSegments = explode('/', $base->getPath());
  165. $targetSegments = explode('/', $target->getPath());
  166. array_pop($sourceSegments);
  167. $targetLastSegment = array_pop($targetSegments);
  168. foreach ($sourceSegments as $i => $segment) {
  169. if (isset($targetSegments[$i]) && $segment === $targetSegments[$i]) {
  170. unset($sourceSegments[$i], $targetSegments[$i]);
  171. } else {
  172. break;
  173. }
  174. }
  175. $targetSegments[] = $targetLastSegment;
  176. $relativePath = str_repeat('../', count($sourceSegments)) . implode('/', $targetSegments);
  177. // A reference to am empty last segment or an empty first sub-segment must be prefixed with "./".
  178. // This also applies to a segment with a colon character (e.g., "file:colon") that cannot be used
  179. // as the first segment of a relative-path reference, as it would be mistaken for a scheme name.
  180. if ('' === $relativePath || false !== strpos(explode('/', $relativePath, 2)[0], ':')) {
  181. $relativePath = "./$relativePath";
  182. } elseif ('/' === $relativePath[0]) {
  183. if ($base->getAuthority() != '' && $base->getPath() === '') {
  184. // In this case an extra slash is added by resolve() automatically. So we must not add one here.
  185. $relativePath = ".$relativePath";
  186. } else {
  187. $relativePath = "./$relativePath";
  188. }
  189. }
  190. return $relativePath;
  191. }
  192. private function __construct()
  193. {
  194. // cannot be instantiated
  195. }
  196. }