Skip to content

Commit c4d2e44

Browse files
committed
Improve polyfill
1 parent 37e1e02 commit c4d2e44

File tree

3 files changed

+99
-86
lines changed

3 files changed

+99
-86
lines changed

Polyfill/UrlTest.php

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
namespace League\Uri\Polyfill;
1515

1616
use PHPUnit\Framework\Attributes\CoversClass;
17+
use PHPUnit\Framework\Attributes\DataProvider;
1718
use PHPUnit\Framework\Attributes\Test;
1819
use PHPUnit\Framework\TestCase;
1920
use Uri\InvalidUriException;
@@ -148,7 +149,7 @@ public function it_succeed_when_updating_an_invalid_ipv4_host(): void
148149
}
149150

150151
$invalidIpv4Host = '255.255.255.256';
151-
$url = new Url('schem://host/path');
152+
$url = new Url('scheme://host/path');
152153
$urlBis = $url->withHost($invalidIpv4Host);
153154

154155
self::assertSame($invalidIpv4Host, $urlBis->getAsciiHost());
@@ -247,21 +248,26 @@ public function it_will_convert_to_unicode_the_host_in_the_uri_while_preserving_
247248
}
248249

249250
#[Test]
250-
public function it_will_fail_to_update_with_an_invalid_port(): void
251+
#[DataProvider('providesInvalidPort')]
252+
public function it_will_fail_to_update_with_an_invalid_port(int $port): void
251253
{
252254
$this->expectException(InvalidUrlException::class);
253255

254-
$url = new Url('https://user:[email protected]/foo/bar');
255-
$url->withPort(12345678);
256+
(new Url('https://user:[email protected]/foo/bar'))->withPort($port);
257+
}
258+
259+
public static function providesInvalidPort(): iterable
260+
{
261+
yield 'Port is too high' => ['port' => 65536];
262+
yield 'Port is too low' => ['port' => -1];
256263
}
257264

258265
#[Test]
259266
public function it_will_fail_to_update_with_an_invalid_host(): void
260267
{
261268
$this->expectException(InvalidUrlException::class);
262269

263-
$url = new Url('https://user:[email protected]/foo/bar');
264-
$url->withHost('::1');
270+
(new Url('https://user:[email protected]/foo/bar'))->withHost('::1');
265271
}
266272

267273
#[Test]
@@ -339,4 +345,18 @@ public function it_returns_a_singleton_error_instance(): void
339345
self::assertSame($e->errors[0]->type, UrlValidationErrorType::PortOutOfRange);
340346
}
341347
}
348+
349+
#[Test]
350+
public function it_will_update_the_port_and_the_scheme(): void
351+
{
352+
$uri = new Url('https://example.com:432');
353+
$resScheme = $uri->withScheme('http')->withPort(8080);
354+
$resSchemeDot = $uri->withScheme('http:')->withPort(8080);
355+
$resSchemeDotSlashes = $uri->withScheme('HtTp://')->withPort(8080);
356+
357+
self::assertSame('http', $resScheme->getScheme());
358+
self::assertSame(8080, $resScheme->getPort());
359+
self::assertTrue($resScheme->equals($resSchemeDot));
360+
self::assertTrue($resSchemeDot->equals($resSchemeDotSlashes));
361+
}
342362
}

lib/Rfc3986/Uri.php

Lines changed: 61 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -151,13 +151,58 @@ private function setNormalizedComponents(): void
151151
*/
152152
private function withComponent(array $components): self
153153
{
154-
try {
155-
$uri = UriString::build([...$this->rawComponents, ...$components]);
156-
} catch (Exception $exception) {
157-
throw new InvalidUriException($exception->getMessage(), previous: $exception);
154+
return new self(UriString::build(
155+
$this->prepareModification([...$this->rawComponents, ...$components])
156+
));
157+
}
158+
159+
/**
160+
* Formatting the path when setting the path to avoid
161+
* exception to be thrown on an invalid path.
162+
* see https://github.com/php/php-src/issues/19897.
163+
*
164+
* @param InputComponentMap $components
165+
*
166+
* @return InputComponentMap
167+
*/
168+
private function prepareModification(array $components): array
169+
{
170+
if (!isset($components['path']) || '' === $components['path']) {
171+
return $components;
172+
}
173+
174+
$path = $components['path'];
175+
$isAbsolute = str_starts_with($path, '/');
176+
$authority = UriString::buildAuthority($components);
177+
if (null !== $authority) {
178+
// If there is an authority, the path must start with a `/`
179+
$components['path'] = $isAbsolute ? $path : '/'.$path;
180+
181+
return $components;
182+
}
183+
184+
// If there is no authority, the path cannot start with `//`
185+
if ($isAbsolute) {
186+
$components['path'] = '/.'.$path;
187+
188+
return $components;
189+
}
190+
191+
$colonPos = strpos($path, ':');
192+
if (false === $colonPos) {
193+
$components['path'] = $path;
194+
195+
return $components;
158196
}
159197

160-
return new self($uri);
198+
// In the absence of a scheme and of an authority,
199+
// the first path segment cannot contain a colon (":") character.'
200+
$slashPos = strpos($path, '/');
201+
if (false === $slashPos || $colonPos < $slashPos) {
202+
$components['path'] = './'.$path;
203+
}
204+
205+
return $components;
161206
}
162207

163208
public function getRawScheme(): ?string
@@ -175,19 +220,11 @@ public function getScheme(): ?string
175220
*/
176221
public function withScheme(?string $scheme): self
177222
{
178-
if ($scheme === $this->getRawScheme()) {
179-
return $this;
180-
}
181-
182-
if (!UriString::isValidScheme($scheme)) {
183-
throw new InvalidUriException('The scheme string component `'.$scheme.'` is an invalid scheme.');
184-
}
185-
186-
$components = $this->rawComponents;
187-
$components['scheme'] = $scheme;
188-
$components['path'] = $this->preparePathForModification($components['path'], $components);
189-
190-
return $this->withComponent($components);
223+
return match (true) {
224+
$scheme === $this->getRawScheme() => $this,
225+
UriString::isValidScheme($scheme) => $this->withComponent(['scheme' => $scheme]),
226+
default => throw new InvalidUriException('The scheme string component `'.$scheme.'` is an invalid scheme.'),
227+
};
191228
}
192229

193230
public function getRawUserInfo(): ?string
@@ -255,19 +292,11 @@ public function getHost(): ?string
255292
*/
256293
public function withHost(?string $host): self
257294
{
258-
if ($host === $this->getRawHost()) {
259-
return $this;
260-
}
261-
262-
if (!UriString::isValidHost($host)) {
263-
throw new InvalidUriException('The host component value `'.$host.'` is not a valid host.');
264-
}
265-
266-
$components = $this->rawComponents;
267-
$components['host'] = $host;
268-
$components['path'] = $this->preparePathForModification($components['path'], $components);
269-
270-
return $this->withComponent($components);
295+
return match (true) {
296+
$host === $this->getRawHost() => $this,
297+
UriString::isValidHost($host) => $this->withComponent(['host' => $host]),
298+
default => throw new InvalidUriException('The host component value `'.$host.'` is not a valid host.'),
299+
};
271300
}
272301

273302
public function getPort(): ?int
@@ -310,7 +339,7 @@ public function withPath(string $path): self
310339
{
311340
return match (true) {
312341
$path === $this->getRawPath() => $this,
313-
Encoder::isPathEncoded($path) => $this->withComponent(['path' => $this->preparePathForModification($path, $this->rawComponents)]),
342+
Encoder::isPathEncoded($path) => $this->withComponent(['path' => $path]),
314343
default => throw new InvalidUriException('The encoded path component `'.$path.'` contains invalid characters.'),
315344
};
316345
}
@@ -428,41 +457,5 @@ public function __debugInfo(): array
428457
'fragment' => $this->rawComponents['fragment'],
429458
];
430459
}
431-
432-
/**
433-
* Formatting the path when setting the path to avoid
434-
* exception to be thrown on an invalid path.
435-
* see https://github.com/php/php-src/issues/19897.
436-
*
437-
* @param InputComponentMap $components
438-
*/
439-
private function preparePathForModification(string $path, array $components): string
440-
{
441-
$isAbsolute = str_starts_with($path, '/');
442-
$authority = UriString::buildAuthority($components);
443-
if (null !== $authority) {
444-
// If there is an authority, the path must start with a `/`
445-
return $isAbsolute ? $path : '/'.$path;
446-
}
447-
448-
// If there is no authority, the path cannot start with `//`
449-
if ($isAbsolute) {
450-
return '/.'.$path;
451-
}
452-
453-
$colonPos = strpos($path, ':');
454-
if (false === $colonPos) {
455-
return $path;
456-
}
457-
458-
// In the absence of a scheme and of an authority,
459-
// the first path segment cannot contain a colon (":") character.'
460-
$slashPos = strpos($path, '/');
461-
if (false === $slashPos || $colonPos < $slashPos) {
462-
return './'.$path;
463-
}
464-
465-
return $path;
466-
}
467460
}
468461
}

lib/WhatWg/Url.php

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
use Rowbot\URL\BasicURLParser;
2222
use Rowbot\URL\Component\Host\StringHost;
2323
use Rowbot\URL\ParserState;
24-
use Rowbot\URL\String\Utf8String;
2524
use Rowbot\URL\URL as WhatWgURL;
2625
use Rowbot\URL\URLRecord;
2726
use SensitiveParameter;
@@ -111,16 +110,17 @@ public function withScheme(?string $scheme): self
111110
}
112111

113112
$copy = $this->copy();
114-
$urlRecord = self::urlRecord($copy);
115-
$collector = new UrlValidationErrorCollector();
116-
$parser = new BasicUrlParser($collector);
117-
$result = $parser->parse(
118-
input: Utf8String::fromUnsafe($scheme),
119-
url: $urlRecord,
120-
stateOverride: ParserState::SCHEME
121-
);
113+
if ('' === $scheme) {
114+
$copy->url->protocol = '';
115+
116+
return $copy;
117+
}
118+
119+
static $regexp = ',^(?<scheme>[a-zA-Z][a-zA-Z0-9+\-.]*)(:(?://?)?)?$,';
120+
121+
1 === preg_match($regexp, $scheme, $matches) || throw new InvalidUrlException('The specified scheme is malformed.');
122122

123-
false !== $result || throw new InvalidUrlException('Can not set the scheme', $collector->errors());
123+
$copy->url->protocol = $matches['scheme'];
124124

125125
return $copy;
126126
}
@@ -237,7 +237,7 @@ public function withHost(?string $host): self
237237
stateOverride: ParserState::HOST
238238
);
239239

240-
false !== $result || throw new InvalidUrlException('Can not set the host', $collector->errors());
240+
false !== $result || throw new InvalidUrlException('The specified host is malformed', $collector->errors());
241241

242242
return $copy;
243243
}
@@ -263,7 +263,7 @@ public function withPort(?int $port): self
263263
return $copy;
264264
}
265265

266-
throw new InvalidUrlException('Port must be between '.self::PORT_RANGE_MIN.' and '.self::PORT_RANGE_MAX, [new UrlValidationError((string) $port, UrlValidationErrorType::PortOutOfRange, true)]);
266+
throw new InvalidUrlException('The specified port is malformed. Port must be between '.self::PORT_RANGE_MIN.' and '.self::PORT_RANGE_MAX, [new UrlValidationError((string) $port, UrlValidationErrorType::PortOutOfRange, true)]);
267267
}
268268

269269
public function getPath(): string

0 commit comments

Comments
 (0)