Skip to content

Commit

Permalink
Merge pull request #45 from clue-labs/resolve
Browse files Browse the repository at this point in the history
Resolve and follow redirects to relative URIs
  • Loading branch information
clue committed Aug 6, 2015
2 parents c8da4a4 + a4a9037 commit 411c1d6
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 55 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,11 @@ The `getUri()` method can be used to get its [`Uri`](#uri) instance.

### Uri

An `Uri` represents an absolute URI (aka URL).

By definition of this library, an `Uri` instance is always absolute and can not contain any placeholders.
As such, any incomplete/relative URI will be rejected with an `InvalidArgumentException`.

Each [`Request`](#request) contains a (full) absolute request URI.

```
Expand All @@ -268,6 +273,8 @@ assert('/' == $uri->getPath());

See its [class outline](src/Message/Uri.php) for more details.

Internally, this class uses the excellent [ml/iri](https://github.com/lanthaler/IRI) library under the hood.

### ResponseException

The `ResponseException` is an `Exception` sub-class that will be used to reject
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"react/socket-client": "0.3.*|0.4.*",
"react/dns": "0.3.*|0.4.*",
"react/promise": "1.*|2.*",
"rize/uri-template": "~0.3.0"
"rize/uri-template": "~0.3.0",
"ml/iri": "~1.0"
}
}
31 changes: 19 additions & 12 deletions src/Io/Transaction.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,18 +64,8 @@ public function onResponse(Response $response, Request $request)
{
$this->progress('response', array($response, $request));

if ($this->followRedirects && ($response->getCode() >= 300 && $response->getCode() < 400 && $location = $response->getHeader('Location'))) {
// naïve approach..
$method = ($request->getMethod() === 'HEAD') ? 'HEAD' : 'GET';
$request = new Request($method, $location);

$this->progress('redirect', array($request));

if ($this->numRequests >= $this->maxRedirects) {
throw new \RuntimeException('Maximum number of redirects (' . $this->maxRedirects . ') exceeded');
}

return $this->next($request);
if ($this->followRedirects && ($response->getCode() >= 300 && $response->getCode() < 400)) {
return $this->onResponseRedirect($response, $request);
}

// only status codes 200-399 are considered to be valid, reject otherwise
Expand All @@ -94,6 +84,23 @@ public function onError(Exception $error, Request $request)
throw $error;
}

private function onResponseRedirect(Response $response, Request $request)
{
$location = $request->getUri()->resolve($response->getHeader('Location'));

// naïve approach..
$method = ($request->getMethod() === 'HEAD') ? 'HEAD' : 'GET';
$request = new Request($method, $location);

$this->progress('redirect', array($request));

if ($this->numRequests >= $this->maxRedirects) {
throw new \RuntimeException('Maximum number of redirects (' . $this->maxRedirects . ') exceeded');
}

return $this->next($request);
}

private function progress($name, array $args = array())
{
return;
Expand Down
91 changes: 50 additions & 41 deletions src/Message/Uri.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,70 +3,87 @@
namespace Clue\React\Buzz\Message;

use InvalidArgumentException;
use ML\IRI\IRI;

/**
* An `Uri` represents an absolute URI (aka URL).
*
* By definition of this library, an `Uri` instance is always absolute and can not contain any placeholders.
*/
class Uri
{
private $scheme;
private $host;
private $port;
private $path;
private $query;
private $iri;

/**
* Instantiate new absolute URI instance
*
* By definition of this library, an `Uri` instance is always absolute and can not contain any placeholders.
* As such, any incomplete/relative URI will be rejected with an `InvalidArgumentException`.
*
* @param string|Uri|IRI $uri
* @throws InvalidArgumentException for incomplete/relative URIs
*/
public function __construct($uri)
{
$parts = parse_url($uri);
if (!$parts || !isset($parts['scheme'], $parts['host'], $parts['path'])) {
if (!$uri instanceof IRI) {
$uri = new IRI($uri);
}

if (!$uri->isAbsolute() || $uri->getHost() === '' || $uri->getPath() === '') {
throw new InvalidArgumentException('Not a valid absolute URI');
}

if (strpos($uri, '{') !== false) {
throw new \InvalidArgumentException('Contains placeholders');
throw new InvalidArgumentException('Contains placeholders');
}

$this->scheme = $parts['scheme'];
$this->host = $parts['host'];
$this->port = isset($parts['port']) ? $parts['port'] : null;
$this->path = $parts['path'];
$this->query = isset($parts['query']) ? $parts['query'] : '';
$this->iri = $uri;
}

public function __toString()
{
$url = $this->scheme . '://' . $this->host;
if ($this->port !== null) {
$url .= ':' . $this->port;
}
$url .= $this->path;
if ($this->query !== '') {
$url .= '?' . $this->query;
}

return $url;
return (string)$this->iri;
}

public function getScheme()
{
return $this->scheme;
return $this->iri->getScheme();
}

public function getHost()
{
return $this->host;
return $this->iri->getHost();
}

public function getPort()
{
return $this->port;
return $this->iri->getPort();
}

public function getPath()
{
return $this->path;
return $this->iri->getPath();
}

public function getQuery()
{
return $this->query;
return $this->iri->getQuery();
}

/**
* Resolve a (relative) URI reference against this URI
*
* @param string|Uri $uri relative or absolute URI
* @return Uri absolute URI
* @link http://tools.ietf.org/html/rfc3986#section-5.2
* @uses IRI::resolve()
*/
public function resolve($uri)
{
if ($uri instanceof self) {
$uri = (string)$uri;
}
return new self($this->iri->resolve($uri));
}

/**
Expand Down Expand Up @@ -95,25 +112,17 @@ public function expandBase($uri)
return $uri;
}

$new = clone $this;

$pos = strpos($uri, '?');
if ($pos !== false) {
$new->query = substr($uri, $pos + 1);
$uri = substr($uri, 0, $pos);
}
$base = (string)$this;

if ($uri !== '' && substr($new->path, -1) !== '/') {
$new->path .= '/';
if ($uri !== '' && substr($base, -1) !== '/' && substr($uri, 0, 1) !== '?') {
$base .= '/';
}

if (isset($uri[0]) && $uri[0] === '/') {
$uri = substr($uri, 1);
}

$new->path .= $uri;

return (string)$new;
return $base . $uri;
}

/**
Expand All @@ -127,7 +136,7 @@ public function expandBase($uri)
*/
public function assertBaseOf(Uri $new)
{
if ($new->scheme !== $this->scheme || $new->host !== $this->host || $new->port !== $this->port || strpos($new->path, $this->path) !== 0) {
if (strpos((string)$new, (string)$this) !== 0) {
throw new \UnexpectedValueException('Invalid base, "' . $new . '" does not appear to be below "' . $this . '"');
}

Expand Down
9 changes: 8 additions & 1 deletion tests/FunctionalBrowserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,14 @@ public function testSimpleRequest()
$this->loop->run();
}

public function testRedirectRequest()
public function testRedirectRequestRelative()
{
$this->expectPromiseResolve($this->browser->get($this->base . 'redirect-to?url=get'));

$this->loop->run();
}

public function testRedirectRequestAbsolute()
{
$this->expectPromiseResolve($this->browser->get($this->base . 'redirect-to?url=' . urlencode($this->base . 'get')));

Expand Down
25 changes: 25 additions & 0 deletions tests/Message/UriTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,4 +106,29 @@ public function testAssertNotBase($other)

$base->assertBaseOf(new Uri($other));
}

public function testResolveRelative()
{
$base = new Uri('http://example.com/base/');

$this->assertEquals('http://example.com/base/', $base->resolve(''));
$this->assertEquals('http://example.com/', $base->resolve('/'));

$this->assertEquals('http://example.com/base/a', $base->resolve('a'));
$this->assertEquals('http://example.com/a', $base->resolve('../a'));
}

public function testResolveAbsolute()
{
$base = new Uri('http://example.org/');

$this->assertEquals('http://www.example.com/', $base->resolve('http://www.example.com/'));
}

public function testResolveUri()
{
$base = new Uri('http://example.org/');

$this->assertEquals('http://www.example.com/', $base->resolve(new Uri('http://www.example.com/')));
}
}

0 comments on commit 411c1d6

Please sign in to comment.