diff --git a/src/HTTP/URL.php b/src/HTTP/URL.php index 67dc3f1..80a8992 100644 --- a/src/HTTP/URL.php +++ b/src/HTTP/URL.php @@ -1,191 +1,208 @@ url = $url; $this->queryEncoding = $queryEncoding; } public static function compose (string $protocol, string $domain, string $query, $queryEncoding = self::ENCODE_RFC3986_SLASH_EXCEPTED ) : self { return (new URL("", $queryEncoding)) ->update($protocol, $domain, $query); } /// /// Getters and setters /// public function getProtocol () : string { if (preg_match("@(.*?)://.*@", $this->url, $matches)) { return $matches[1]; } return ""; } + private function getUrlParts() : array { + preg_match("@://(.*)@", $this->url, $matches); + return explode("/", $matches[1], 2); + } + public function getDomain () : string { - if (preg_match("@://(.*?)/@", $this->url, $matches)) { - return self::beautifyDomain($matches[1]); + if (strpos($this->url, "://") === false) { + return ""; } - return ""; + $domain = $this->getUrlParts()[0]; + + if ($domain === "") { + return ""; + } + + return self::beautifyDomain($domain); } public function getQuery () : string { - if (preg_match("@(://.*?)?(/.*)@", $this->url, $matches)) { - return $this->beautifyQuery($matches[2]); + if (strpos($this->url, "://") === false) { + return $this->url; } - return ""; + $parts = $this->getUrlParts(); + + if (count($parts) < 2 || $parts[1] === "" || $parts[1] === "/") { + return "/"; + } + + return "/" . $this->beautifyQuery($parts[1]); } public function setProtocol ($protocol) : self { $this->update($protocol, $this->getDomain(), $this->getQuery()); return $this; } public function setDomain ($domain) : self { $this->update($this->getProtocol(), $domain, $this->getQuery()); return $this; } public function setQuery ($query, $encodeMode = self::ENCODE_RFC3986_SLASH_EXCEPTED ) : self { $this->queryEncoding = $encodeMode; $this->update($this->getProtocol(), $this->getDomain(), $query); return $this; } private function isRootQuery($query) : bool { return $this->queryEncoding !== self::ENCODE_RFC3986_PURE && $query !== "" && $query[0] === '/'; } private function update (string $protocol, string $domain, string $query) : self { $url = ""; if ($domain !== "") { if ($protocol !== "") { $url = $protocol; } $url .= "://" . self::normalizeDomain($domain); if (!$this->isRootQuery($query)) { $url .= "/"; } } $url .= $this->normalizeQuery($query); $this->url = $url; return $this; } public function normalizeQuery (string $query) : string { switch ($this->queryEncoding) { case self::ENCODE_RFC3986_SLASH_EXCEPTED: return (new OmniString($query)) ->explode("/") ->map("rawurlencode") ->implode("/") ->__toString(); case self::ENCODE_AS_IS: return $query; case self::ENCODE_RFC3986_PURE: return rawurlencode($query); } throw new \Exception('Unexpected encoding value'); } public function beautifyQuery (string $query) : string { switch ($this->queryEncoding) { case self::ENCODE_RFC3986_SLASH_EXCEPTED: return (new OmniString($query)) ->explode("/") ->map("rawurldecode") ->implode("/") ->__toString(); case self::ENCODE_AS_IS: return $query; case self::ENCODE_RFC3986_PURE: return rawurldecode($query); } throw new \Exception('Unexpected encoding value'); } public static function normalizeDomain (string $domain) : string { return idn_to_ascii($domain, 0, INTL_IDNA_VARIANT_UTS46); } public static function beautifyDomain (string $domain) : string { return idn_to_utf8($domain, 0, INTL_IDNA_VARIANT_UTS46); } public function __toString () { return $this->url; } } diff --git a/tests/HTTP/URLTest.php b/tests/HTTP/URLTest.php index 1c43e15..3b8802c 100644 --- a/tests/HTTP/URLTest.php +++ b/tests/HTTP/URLTest.php @@ -1,84 +1,90 @@ assertEquals($expectedDomain, $url->getDomain()); } /** * @dataProvider provideURLsAndComponents */ public function testGetProtocol ($url, $_, $expectedProtocol) : void { $url = new URL($url); $this->assertEquals($expectedProtocol, $url->getProtocol()); } /** * @dataProvider provideURLsAndComponents */ public function testGetQuery ($url, $_, $__, $expectedQuery) : void { $url = new URL($url); $this->assertEquals($expectedQuery, $url->getQuery()); } public function testSetProtocol () : void { $url = new URL("https://acme.tld/foo"); $url->setProtocol("xizzy"); $this->assertEquals("xizzy", $url->getProtocol()); } public function testSetDomain () : void { $url = new URL("https://acme.tld/foo"); $url->setDomain("xizzy"); $this->assertEquals("xizzy", $url->getDomain()); } public function testSetQuery () : void { $url = new URL("https://acme.tld/foo"); $url->setQuery("/xizzy"); $this->assertEquals("/xizzy", $url->getQuery()); } public function testSetQueryWithSlashForgotten () : void { $url = new URL("https://acme.tld/foo"); $url->setQuery("xizzy"); $this->assertEquals("/xizzy", $url->getQuery()); } /** * @dataProvider provideURLsAndComponents */ - public function testCompose ($expectedUrl, $domain, $protocol, $query) { + public function testCompose ($url, $domain, $protocol, $query, + $expectedUrl = null) { $this->assertEquals( - $expectedUrl, + $expectedUrl ?? $url, URL::compose($protocol, $domain, $query)->__toString() ); } public function provideURLsAndComponents () : iterable { + // base URL, domain, protocol, query[, expected URL] + // When omitted, the expected URL is the base URL. + yield ["http://foo/bar", "foo", "http", "/bar"]; yield ["https://xn--dghrefn-mxa.nasqueron.org/", "dæghrefn.nasqueron.org", "https", "/"]; yield ["://foo/bar", "foo", "", "/bar"]; yield ["/bar", "", "", "/bar"]; yield ["http://foo/bar%20quux", "foo", "http", "/bar quux"]; + yield ["https://foo/", "foo", "https", "/"]; + yield ["https://foo", "foo", "https", "/", "https://foo/"]; } }