Skip to content

Commit 7c92bd2

Browse files
authored
Fix parsing bug with uri scheme/user (#232)
* test: isolate bug #231 * fix: better uri parsing of user/scheme closes #231 * test: improve test coverage - authority related
1 parent 0c9a10d commit 7c92bd2

2 files changed

Lines changed: 137 additions & 1 deletion

File tree

src/Uri.php

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,98 @@ public function __construct(?string $uri = null) {
2323
return;
2424
}
2525

26-
$parts = parse_url($uri);
26+
$parts = $this->parseUri($uri);
2727
if($parts === false) {
2828
throw new UriParseErrorException($uri);
2929
}
3030
$this->applyParts($parts);
3131
}
3232

33+
/** @return false|array<string, int|string> */
34+
protected function parseUri(string $uri):false|array {
35+
$parts = parse_url($uri);
36+
if($parts === false) {
37+
return false;
38+
}
39+
40+
$authorityStyleParts = $this->parseAuthorityStyleParts($uri, $parts);
41+
if(!is_null($authorityStyleParts)) {
42+
return $authorityStyleParts;
43+
}
44+
45+
return $parts;
46+
}
47+
48+
/**
49+
* @param array<string, int|string> $parts
50+
* @return null|array<string, int|string>
51+
*/
52+
protected function parseAuthorityStyleParts(
53+
string $uri,
54+
array $parts
55+
):?array {
56+
if(!$this->canBeAuthorityStyleUri($uri, $parts)) {
57+
return null;
58+
}
59+
60+
$authorityParts = parse_url("//" . $uri);
61+
if($authorityParts === false) {
62+
return null;
63+
}
64+
65+
if(!$this->hasRequiredAuthorityParts($authorityParts)) {
66+
return null;
67+
}
68+
69+
if(!$this->isAuthorityPathValid($authorityParts)) {
70+
return null;
71+
}
72+
73+
if(!$this->isAuthorityHostLike($authorityParts)) {
74+
return null;
75+
}
76+
77+
return $authorityParts;
78+
}
79+
80+
/** @param array<string, int|string> $parts */
81+
protected function canBeAuthorityStyleUri(string $uri, array $parts):bool {
82+
if(str_contains($uri, "://")) {
83+
return false;
84+
}
85+
86+
if(!isset($parts["scheme"]) || isset($parts["host"])) {
87+
return false;
88+
}
89+
90+
$path = (string)($parts["path"] ?? "");
91+
return str_contains($path, "@");
92+
}
93+
94+
/** @param array<string, int|string> $authorityParts */
95+
protected function hasRequiredAuthorityParts(array $authorityParts):bool {
96+
return isset($authorityParts["user"], $authorityParts["pass"], $authorityParts["host"]);
97+
}
98+
99+
/** @param array<string, int|string> $authorityParts */
100+
protected function isAuthorityPathValid(array $authorityParts):bool {
101+
$authorityPath = (string)($authorityParts["path"] ?? "");
102+
return strlen($authorityPath) === 0 || str_starts_with($authorityPath, "/");
103+
}
104+
105+
/** @param array<string, int|string> $authorityParts */
106+
protected function isAuthorityHostLike(array $authorityParts):bool {
107+
$host = (string)$authorityParts["host"];
108+
if(filter_var($host, FILTER_VALIDATE_IP) !== false) {
109+
return true;
110+
}
111+
112+
return str_contains($host, ".")
113+
|| str_contains($host, ":")
114+
|| str_starts_with($host, "[")
115+
|| $host === self::DEFAULT_HOST_HTTP;
116+
}
117+
33118
/** @inheritDoc */
34119
public function __toString():string {
35120
$uri = "";

test/phpunit/UriTest.php

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,57 @@ public function testCanConstructFalseyUriParts() {
169169
$this->assertSame('0://0:0@0/0?0#0', (string)$uri);
170170
}
171171

172+
public function testParsesAuthorityStyleCredentialsWithoutScheme() {
173+
$uri = new Uri('admin:admin@10.10.0.8/status.xml');
174+
$this->assertSame('', $uri->getScheme());
175+
$this->assertSame('admin:admin', $uri->getUserInfo());
176+
$this->assertSame('10.10.0.8', $uri->getHost());
177+
$this->assertSame('/status.xml', $uri->getPath());
178+
}
179+
180+
public function testBuildsAuthorityFromCredentialsWithoutScheme() {
181+
$uri = new Uri('admin:admin@10.10.0.8/status.xml');
182+
$this->assertSame('admin:admin@10.10.0.8', $uri->getAuthority());
183+
}
184+
185+
public function testKeepsOriginalParseWhenAuthorityFallbackCannotParseDoubleSlash() {
186+
$uri = new Uri('admin:admin@?/x');
187+
$this->assertSame('admin', $uri->getScheme());
188+
$this->assertSame('admin@', $uri->getPath());
189+
$this->assertSame('/x', $uri->getQuery());
190+
}
191+
192+
public function testKeepsOriginalParseWhenAuthorityHostIsNotHostLike() {
193+
$uri = new Uri('admin:admin@example/status.xml');
194+
$this->assertSame('admin', $uri->getScheme());
195+
$this->assertSame('', $uri->getAuthority());
196+
$this->assertSame('admin@example/status.xml', $uri->getPath());
197+
}
198+
199+
public function testCanRejectAuthorityFallbackWhenRequiredAuthorityPartsAreMissing() {
200+
$uri = new class('admin:admin@10.10.0.8/status.xml') extends Uri {
201+
protected function hasRequiredAuthorityParts(array $authorityParts):bool {
202+
return false;
203+
}
204+
};
205+
206+
$this->assertSame('admin', $uri->getScheme());
207+
$this->assertSame('', $uri->getAuthority());
208+
$this->assertSame('admin@10.10.0.8/status.xml', $uri->getPath());
209+
}
210+
211+
public function testCanRejectAuthorityFallbackWhenAuthorityPathIsInvalid() {
212+
$uri = new class('admin:admin@10.10.0.8/status.xml') extends Uri {
213+
protected function isAuthorityPathValid(array $authorityParts):bool {
214+
return false;
215+
}
216+
};
217+
218+
$this->assertSame('admin', $uri->getScheme());
219+
$this->assertSame('', $uri->getAuthority());
220+
$this->assertSame('admin@10.10.0.8/status.xml', $uri->getPath());
221+
}
222+
172223
/**
173224
* @dataProvider getPortTestCases
174225
*/

0 commit comments

Comments
 (0)