Search moodle.org's
Developer Documentation

See Release Notes

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