From cf7a988adefa3693aa6047501be40654d41f242e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 6 Aug 2015 00:46:43 +0200 Subject: [PATCH 1/4] Add Uri::resolve() via ml/iri package --- composer.json | 3 ++- src/Message/Uri.php | 16 ++++++++++++++++ tests/Message/UriTest.php | 25 +++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 4fddf0e..c67a840 100644 --- a/composer.json +++ b/composer.json @@ -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" } } diff --git a/src/Message/Uri.php b/src/Message/Uri.php index a3be692..8026015 100644 --- a/src/Message/Uri.php +++ b/src/Message/Uri.php @@ -3,6 +3,7 @@ namespace Clue\React\Buzz\Message; use InvalidArgumentException; +use ML\IRI\IRI; class Uri { @@ -69,6 +70,21 @@ public function getQuery() return $this->query; } + /** + * 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) + { + $iri = new IRI((string)$this); + + return new Uri($iri->resolve((string)$uri)); + } + /** * Resolves the given relative or absolute $uri by appending it behind $this base URI * diff --git a/tests/Message/UriTest.php b/tests/Message/UriTest.php index e2da1c0..1a2c4b8 100644 --- a/tests/Message/UriTest.php +++ b/tests/Message/UriTest.php @@ -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/'))); + } } From 587f2fd2e18acfe7a4020a5e58090570adea102e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 6 Aug 2015 00:47:31 +0200 Subject: [PATCH 2/4] Resolve and follow redirects to relative URIs --- src/Io/Transaction.php | 31 +++++++++++++++++++------------ tests/FunctionalBrowserTest.php | 9 ++++++++- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/Io/Transaction.php b/src/Io/Transaction.php index 62e36bf..afba2c9 100644 --- a/src/Io/Transaction.php +++ b/src/Io/Transaction.php @@ -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 @@ -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; diff --git a/tests/FunctionalBrowserTest.php b/tests/FunctionalBrowserTest.php index b062ec2..84221db 100644 --- a/tests/FunctionalBrowserTest.php +++ b/tests/FunctionalBrowserTest.php @@ -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'))); From 80ef8fd20f2e797828d4fc9b4bd128ddb8ce5e2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 6 Aug 2015 01:13:44 +0200 Subject: [PATCH 3/4] Use decorated IRI instance for all Uri methods (mostly an adapter) --- README.md | 2 ++ src/Message/Uri.php | 65 +++++++++++++++------------------------------ 2 files changed, 24 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index be2149d..d4cdb82 100644 --- a/README.md +++ b/README.md @@ -265,6 +265,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 diff --git a/src/Message/Uri.php b/src/Message/Uri.php index 8026015..db5211f 100644 --- a/src/Message/Uri.php +++ b/src/Message/Uri.php @@ -7,16 +7,15 @@ class Uri { - private $scheme; - private $host; - private $port; - private $path; - private $query; + private $iri; 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'); } @@ -24,50 +23,37 @@ public function __construct($uri) 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(); } /** @@ -80,9 +66,10 @@ public function getQuery() */ public function resolve($uri) { - $iri = new IRI((string)$this); - - return new Uri($iri->resolve((string)$uri)); + if ($uri instanceof self) { + $uri = (string)$uri; + } + return new self($this->iri->resolve($uri)); } /** @@ -111,25 +98,17 @@ public function expandBase($uri) return $uri; } - $new = clone $this; + $base = (string)$this; - $pos = strpos($uri, '?'); - if ($pos !== false) { - $new->query = substr($uri, $pos + 1); - $uri = substr($uri, 0, $pos); - } - - 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; } /** @@ -143,7 +122,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 . '"'); } From a4a903738a63432b2cd0682a1fadefb4c1972685 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 7 Aug 2015 00:48:39 +0200 Subject: [PATCH 4/4] Improve documentation for URIs --- README.md | 5 +++++ src/Message/Uri.php | 16 +++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d4cdb82..946577f 100644 --- a/README.md +++ b/README.md @@ -252,6 +252,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. ``` diff --git a/src/Message/Uri.php b/src/Message/Uri.php index db5211f..8d86323 100644 --- a/src/Message/Uri.php +++ b/src/Message/Uri.php @@ -5,10 +5,24 @@ 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 $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) { if (!$uri instanceof IRI) { @@ -20,7 +34,7 @@ public function __construct($uri) } if (strpos($uri, '{') !== false) { - throw new \InvalidArgumentException('Contains placeholders'); + throw new InvalidArgumentException('Contains placeholders'); } $this->iri = $uri;