Skip to content

Commit 0e79ec1

Browse files
committed
Improve Host validation and normalization
1 parent c5da542 commit 0e79ec1

File tree

4 files changed

+29
-2
lines changed

4 files changed

+29
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ All Notable changes to `League\Uri\Interfaces` will be documented in this file
5050
- `Uri\IPv4\Converter::toIPv6Using6to4` allows converting an IPv4 into an IPv6 host using the 6to4 notation.
5151
- `Uri\IPv4\Converter::toIPv6UsingMapping` allows mapping an IPv4 address into an IPv6 one.
5252
- Using PHP8.4 `Deprecated` attribute to signal deprecated public API methods and constants.
53+
- `HostInterface::encoded` method to RFC3986 URL encode the host
5354

5455
### Fixed
5556

Contracts/HostInterface.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313

1414
namespace League\Uri\Contracts;
1515

16+
/**
17+
* @method string|null encoded() returns RFC3986 encoded host
18+
*/
1619
interface HostInterface extends UriComponentInterface
1720
{
1821
/**

FeatureDetection.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@
1717
use League\Uri\Exceptions\MissingFeature;
1818
use League\Uri\IPv4\Calculator;
1919

20+
use function class_exists;
21+
use function defined;
2022
use function extension_loaded;
23+
use function function_exists;
2124

2225
use const PHP_INT_SIZE;
2326

UriString.php

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,25 @@
1919
use League\Uri\Idna\Converter as IdnaConverter;
2020
use League\Uri\IPv6\Converter as IPv6Converter;
2121
use Stringable;
22-
2322
use Throwable;
23+
2424
use function array_merge;
2525
use function array_pop;
2626
use function array_reduce;
27+
use function defined;
2728
use function explode;
2829
use function filter_var;
30+
use function function_exists;
2931
use function implode;
3032
use function in_array;
3133
use function inet_pton;
3234
use function preg_match;
35+
use function preg_replace_callback;
3336
use function rawurldecode;
3437
use function sprintf;
3538
use function strpos;
3639
use function strtolower;
40+
use function strtoupper;
3741
use function substr;
3842

3943
use const FILTER_FLAG_IPV4;
@@ -286,11 +290,19 @@ public static function parseNormalized(Stringable|string $uri): array
286290
$components['scheme'] = strtolower($components['scheme']);
287291
}
288292

293+
static $isSupported = null;
294+
$isSupported ??= (function_exists('\idn_to_ascii') && defined('\INTL_IDNA_VARIANT_UTS46'));
295+
289296
if (null !== $components['host'] &&
290297
false === filter_var($components['host'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) &&
291298
!IPv6Converter::isIpv6($components['host'])
292299
) {
293-
$components['host'] = IdnaConverter::toAscii($components['host'])->domain();
300+
$formattedHost = rawurldecode($components['host']);
301+
$components['host'] = $isSupported ? IdnaConverter::toAscii($formattedHost)->domain() : (string) preg_replace_callback(
302+
'/%[0-9A-F]{2}/i',
303+
fn (array $matches): string => strtoupper($matches[0]),
304+
strtolower($components['host'])
305+
);
294306
}
295307

296308
$path = $components['path'];
@@ -691,6 +703,14 @@ public static function isHost(Stringable|string|null $host): bool
691703
private static function filterRegisteredName(string $host): void
692704
{
693705
$formattedHost = rawurldecode($host);
706+
if ($formattedHost !== $host) {
707+
if (IdnaConverter::toAscii($formattedHost)->hasErrors()) {
708+
throw new SyntaxError(sprintf('Host `%s` is invalid: the host is not a valid registered name', $host));
709+
}
710+
711+
return;
712+
}
713+
694714
if (1 === preg_match(self::REGEXP_REGISTERED_NAME, $formattedHost)) {
695715
return;
696716
}

0 commit comments

Comments
 (0)