From 5c5b426bae7e28ae57f0fb25f7378bf4c943f83f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 23 Mar 2024 17:15:23 +0100 Subject: [PATCH] Add internal `Uri::resolve()` to resolve URIs relative to base URI --- src/Browser.php | 4 +- src/Io/Transaction.php | 4 +- src/Message/Uri.php | 64 ++++++++++++++++++++ tests/Message/UriTest.php | 124 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 192 insertions(+), 4 deletions(-) diff --git a/src/Browser.php b/src/Browser.php index b7bf4425..01a266ca 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -3,12 +3,12 @@ namespace React\Http; use Psr\Http\Message\ResponseInterface; -use RingCentral\Psr7\Uri; use React\EventLoop\Loop; use React\EventLoop\LoopInterface; use React\Http\Io\Sender; use React\Http\Io\Transaction; use React\Http\Message\Request; +use React\Http\Message\Uri; use React\Promise\PromiseInterface; use React\Socket\ConnectorInterface; use React\Stream\ReadableStreamInterface; @@ -834,7 +834,7 @@ private function requestMayBeStreaming($method, $url, array $headers = array(), { if ($this->baseUrl !== null) { // ensure we're actually below the base URL - $url = Uri::resolve($this->baseUrl, $url); + $url = Uri::resolve($this->baseUrl, new Uri($url)); } foreach ($this->defaultHeaders as $key => $value) { diff --git a/src/Io/Transaction.php b/src/Io/Transaction.php index b93c490c..64738f56 100644 --- a/src/Io/Transaction.php +++ b/src/Io/Transaction.php @@ -8,11 +8,11 @@ use React\EventLoop\LoopInterface; use React\Http\Message\Response; use React\Http\Message\ResponseException; +use React\Http\Message\Uri; use React\Promise\Deferred; use React\Promise\Promise; use React\Promise\PromiseInterface; use React\Stream\ReadableStreamInterface; -use RingCentral\Psr7\Uri; /** * @internal @@ -264,7 +264,7 @@ public function onResponse(ResponseInterface $response, RequestInterface $reques private function onResponseRedirect(ResponseInterface $response, RequestInterface $request, Deferred $deferred, ClientRequestState $state) { // resolve location relative to last request URI - $location = Uri::resolve($request->getUri(), $response->getHeaderLine('Location')); + $location = Uri::resolve($request->getUri(), new Uri($response->getHeaderLine('Location'))); $request = $this->makeRedirectRequest($request, $location, $response->getStatusCode()); $this->progress('redirect', array($request)); diff --git a/src/Message/Uri.php b/src/Message/Uri.php index b74d871a..a4f0af57 100644 --- a/src/Message/Uri.php +++ b/src/Message/Uri.php @@ -287,4 +287,68 @@ function (array $match) { $part ); } + + /** + * [Internal] Resolve URI relative to base URI and return new absolute URI + * + * @internal + * @param UriInterface $base + * @param UriInterface $rel + * @return UriInterface + * @throws void + */ + public static function resolve(UriInterface $base, UriInterface $rel) + { + if ($rel->getScheme() !== '') { + return $rel->getPath() === '' ? $rel : $rel->withPath(self::removeDotSegments($rel->getPath())); + } + + $reset = false; + $new = $base; + if ($rel->getAuthority() !== '') { + $reset = true; + $userInfo = \explode(':', $rel->getUserInfo(), 2); + $new = $base->withUserInfo($userInfo[0], isset($userInfo[1]) ? $userInfo[1]: null)->withHost($rel->getHost())->withPort($rel->getPort()); + } + + if ($reset && $rel->getPath() === '') { + $new = $new->withPath(''); + } elseif (($path = $rel->getPath()) !== '') { + $start = ''; + if ($path === '' || $path[0] !== '/') { + $start = $base->getPath(); + if (\substr($start, -1) !== '/') { + $start .= '/../'; + } + } + $reset = true; + $new = $new->withPath(self::removeDotSegments($start . $path)); + } + if ($reset || $rel->getQuery() !== '') { + $reset = true; + $new = $new->withQuery($rel->getQuery()); + } + if ($reset || $rel->getFragment() !== '') { + $new = $new->withFragment($rel->getFragment()); + } + + return $new; + } + + /** + * @param string $path + * @return string + */ + private static function removeDotSegments($path) + { + $segments = array(); + foreach (\explode('/', $path) as $segment) { + if ($segment === '..') { + \array_pop($segments); + } elseif ($segment !== '.' && $segment !== '') { + $segments[] = $segment; + } + } + return '/' . \implode('/', $segments) . ($path !== '/' && \substr($path, -1) === '/' ? '/' : ''); + } } diff --git a/tests/Message/UriTest.php b/tests/Message/UriTest.php index 95c7fa4e..05eec723 100644 --- a/tests/Message/UriTest.php +++ b/tests/Message/UriTest.php @@ -578,4 +578,128 @@ public function testWithFragmentReturnsSameInstanceWhenFragmentIsUnchangedEncode $this->assertSame($uri, $new); $this->assertEquals('section%20new%20text!', $uri->getFragment()); } + + public static function provideResolveUris() + { + return array( + array( + 'http://localhost/', + '', + 'http://localhost/' + ), + array( + 'http://localhost/', + 'http://example.com/', + 'http://example.com/' + ), + array( + 'http://localhost/', + 'path', + 'http://localhost/path' + ), + array( + 'http://localhost/', + 'path/', + 'http://localhost/path/' + ), + array( + 'http://localhost/', + 'path//', + 'http://localhost/path/' + ), + array( + 'http://localhost', + 'path', + 'http://localhost/path' + ), + array( + 'http://localhost/a/b', + '/path', + 'http://localhost/path' + ), + array( + 'http://localhost/', + '/a/b/c', + 'http://localhost/a/b/c' + ), + array( + 'http://localhost/a/path', + 'b/c', + 'http://localhost/a/b/c' + ), + array( + 'http://localhost/a/path', + '/b/c', + 'http://localhost/b/c' + ), + array( + 'http://localhost/a/path/', + 'b/c', + 'http://localhost/a/path/b/c' + ), + array( + 'http://localhost/a/path/', + '../b/c', + 'http://localhost/a/b/c' + ), + array( + 'http://localhost', + '../../../a/b', + 'http://localhost/a/b' + ), + array( + 'http://localhost/path', + '?query', + 'http://localhost/path?query' + ), + array( + 'http://localhost/path', + '#fragment', + 'http://localhost/path#fragment' + ), + array( + 'http://localhost/path', + 'http://localhost', + 'http://localhost' + ), + array( + 'http://localhost/path', + 'http://localhost/?query#fragment', + 'http://localhost/?query#fragment' + ), + array( + 'http://localhost/path/?a#fragment', + '?b', + 'http://localhost/path/?b' + ), + array( + 'http://localhost/path', + '//localhost', + 'http://localhost' + ), + array( + 'http://localhost/path', + '//localhost/a?query', + 'http://localhost/a?query' + ), + array( + 'http://localhost/path', + '//LOCALHOST', + 'http://localhost' + ) + ); + } + + /** + * @dataProvider provideResolveUris + * @param string $base + * @param string $rel + * @param string $expected + */ + public function testResolveReturnsResolvedUri($base, $rel, $expected) + { + $uri = Uri::resolve(new Uri($base), new Uri($rel)); + + $this->assertEquals($expected, (string) $uri); + } }