Skip to content

Commit

Permalink
Add internal Uri::resolve() to resolve URIs relative to base URI
Browse files Browse the repository at this point in the history
  • Loading branch information
clue committed Mar 25, 2024
1 parent a73e9f7 commit c6caa12
Show file tree
Hide file tree
Showing 4 changed files with 192 additions and 4 deletions.
4 changes: 2 additions & 2 deletions src/Browser.php
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions src/Io/Transaction.php
Expand Up @@ -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
Expand Down Expand Up @@ -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));
Expand Down
64 changes: 64 additions & 0 deletions src/Message/Uri.php
Expand Up @@ -289,4 +289,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) === '/' ? '/' : '');
}
}
124 changes: 124 additions & 0 deletions tests/Message/UriTest.php
Expand Up @@ -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);
}
}

0 comments on commit c6caa12

Please sign in to comment.