Skip to content

Commit

Permalink
Improve internal codebase (#138)
Browse files Browse the repository at this point in the history
* Improve internal codebase
* Deprecate usage of PSR-7 in API
* Adding IPv6 Converter and usage in uri-interfaces and uri-components
  • Loading branch information
nyamsprod authored Jun 26, 2024
1 parent a48569f commit a58080b
Show file tree
Hide file tree
Showing 6 changed files with 253 additions and 16 deletions.
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ All Notable changes to `League\Uri\Interfaces` will be documented in this file

### Added

- `UriAccess::getIdnUriString`
- `UriInterface::getUsername` returns the encoded user component of the URI.
- `UriInterface::getPassword` returns the encoded scheme-specific information about how to gain authorization to access the resource.
- `Uri\IPv6\Converter` allow expanding and compressing IPv6.

### Fixed

- None
- Adding Host resolution caching to speed up URI parsing in `UriString`

### Deprecated

Expand Down
3 changes: 0 additions & 3 deletions Contracts/UriAccess.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,6 @@

use Psr\Http\Message\UriInterface as Psr7UriInterface;

/**
* @method string getIdnUriString() returns the RFC3986 string representation of the complete URI with its host in IDNA form
*/
interface UriAccess
{
public function getUri(): UriInterface|Psr7UriInterface;
Expand Down
3 changes: 3 additions & 0 deletions Contracts/UriInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@

/**
* @phpstan-import-type ComponentMap from UriString
*
* @method string|null getUsername() returns the user component of the URI.
* @method string|null getPassword() returns the scheme-specific information about how to gain authorization to access the resource.
*/
interface UriInterface extends JsonSerializable, Stringable
{
Expand Down
113 changes: 113 additions & 0 deletions IPv6/Converter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<?php

declare(strict_types=1);

namespace League\Uri\IPv6;

use Stringable;
use ValueError;
use const FILTER_FLAG_IPV6;
use const FILTER_VALIDATE_IP;

use function filter_var;
use function inet_pton;
use function implode;
use function str_split;
use function strtolower;
use function unpack;

final class Converter
{
public static function compressIp(string $ipv6): string
{
if (false === filter_var($ipv6, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
throw new ValueError('The submitted IP is not a valid IPv6 address.');
}

return (string) inet_ntop((string) inet_pton($ipv6));
}

public static function expandIp(string $ipv6): string
{
if (false === filter_var($ipv6, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
throw new ValueError('The submitted IP is not a valid IPv6 address.');
}

$hex = (array) unpack("H*hex", (string) inet_pton($ipv6));

return implode(':', str_split(strtolower($hex['hex'] ?? ''), 4));
}

public static function compress(Stringable|string|null $host): ?string
{
$components = self::parse($host);
if (null === $components['ipv6']) {
return match ($host) {
null => $host,
default => (string) $host,
};
}

$components['ipv6'] = self::compressIp($components['ipv6']);

return self::build($components);
}

public static function expand(Stringable|string|null $host): ?string
{
$components = self::parse($host);
if (null === $components['ipv6']) {
return match ($host) {
null => $host,
default => (string) $host,
};
}

$components['ipv6'] = self::expandIp($components['ipv6']);

return self::build($components);
}

private static function build(array $components): string
{
$components['ipv6'] ??= null;
$components['zoneIdentifier'] ??= null;

return '['.$components['ipv6'].match ($components['zoneIdentifier']) {
null => '',
default => '%'.$components['zoneIdentifier'],
}.']';
}

/**]
* @param Stringable|string|null $host
*
* @return array{ipv6:?string, zoneIdentifier:?string}
*/
private static function parse(Stringable|string|null $host): array
{
if ($host === null) {
return ['ipv6' => null, 'zoneIdentifier' => null];
}

$host = (string) $host;
if ($host === '') {
return ['ipv6' => null, 'zoneIdentifier' => null];
}

if (!str_starts_with($host, '[')) {
return ['ipv6' => null, 'zoneIdentifier' => null];
}

if (!str_ends_with($host, ']')) {
return ['ipv6' => null, 'zoneIdentifier' => null];
}

[$ipv6, $zoneIdentifier] = explode('%', substr($host, 1, -1), 2) + [1 => null];
if (false === filter_var($ipv6, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
return ['ipv6' => null, 'zoneIdentifier' => null];
}

return ['ipv6' => $ipv6, 'zoneIdentifier' => $zoneIdentifier];
}
}
77 changes: 77 additions & 0 deletions IPv6/ConverterTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

declare(strict_types=1);

namespace League\Uri\IPv6;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use ValueError;

final class ConverterTest extends TestCase
{
#[DataProvider('ipv6NormalizationUriProvider')]
public function testItCanExpandOrCompressTheHost(
string $ipv6,
string $ipv6Compressed,
string $ipv6Expanded,
): void {

self::assertSame($ipv6Compressed, Converter::compress($ipv6));
self::assertSame($ipv6Expanded, Converter::expand($ipv6));
}

public static function ipv6NormalizationUriProvider(): iterable
{
yield 'no change happen with a non IP ipv6' => [
'ipv6' => 'example.com',
'ipv6Compressed' => 'example.com',
'ipv6Expanded' => 'example.com',
];

yield 'no change happen with a IPv4 ipv6' => [
'ipv6' => '127.0.0.1',
'ipv6Compressed' => '127.0.0.1',
'ipv6Expanded' => '127.0.0.1',
];

yield 'IPv6 gets expanded if needed' => [
'ipv6' => '[fe80::a%25en1]',
'ipv6Compressed' => '[fe80::a%25en1]',
'ipv6Expanded' => '[fe80:0000:0000:0000:0000:0000:0000:000a%25en1]',
];

yield 'IPv6 gets compressed if needed' => [
'ipv6' => '[0000:0000:0000:0000:0000:0000:0000:0001]',
'ipv6Compressed' => '[::1]',
'ipv6Expanded' => '[0000:0000:0000:0000:0000:0000:0000:0001]',
];
}

#[DataProvider('invalidIpv6')]
public function testItFailsToCompressANonIpv6(string $invalidIp): void
{
$this->expectException(ValueError::class);

Converter::compressIp($invalidIp);
}

#[DataProvider('invalidIpv6')]
public function testItFailsToExpandANonIpv6(string $invalidIp): void
{
$this->expectException(ValueError::class);

Converter::expandIp($invalidIp);
}

public static function invalidIpv6(): iterable
{
yield 'hostname' => ['invalidIp' => 'example.com'];

yield 'IPv4' => ['invalidIp' => '127.0.0.2'];

yield 'ip future' => ['invalidIp' => '[v42.fdfsffd]'];

yield 'IPv6 with zoneIdentifier' => ['invalidIp' => 'fe80::a%25en1'];
}
}
Loading

0 comments on commit a58080b

Please sign in to comment.