Skip to content

Commit

Permalink
Adding IPv6 Converter and usage in uri-interfaces and uri-components
Browse files Browse the repository at this point in the history
  • Loading branch information
nyamsprod committed Jun 25, 2024
1 parent 432681a commit c33bb44
Show file tree
Hide file tree
Showing 11 changed files with 222 additions and 8 deletions.
4 changes: 4 additions & 0 deletions components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ All Notable changes to `League\Uri\Components` will be documented in this file

- `UrlSearchParams::uniqueKeyCount`
- `Modifier::getIdnUriString`
- `Modifier::hostToIpv6Compressed`
- `Modifier::hostToIpv6Expanded`
- `Host::expand`
- `Host::compress`;

### Fixed

Expand Down
11 changes: 11 additions & 0 deletions components/Components/Host.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use League\Uri\Idna\Converter as IdnConverter;
use League\Uri\IPv4\Converter as IPv4Converter;
use League\Uri\IPv4Normalizer;
use League\Uri\IPv6\Converter;
use Psr\Http\Message\UriInterface as Psr7UriInterface;
use Stringable;

Expand Down Expand Up @@ -333,6 +334,16 @@ public function toUnicode(): ?string
};
}

public function compress(): ?string
{
return Converter::compress($this->value());
}

public function expand(): ?string
{
return Converter::expand($this->value());
}

public function getIpVersion(): ?string
{
return $this->ipVersion;
Expand Down
21 changes: 18 additions & 3 deletions components/Components/UserInfo.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,8 @@ public static function new(#[SensitiveParameter] Stringable|string|null $value =
public function value(): ?string
{
return match (true) {
null === $this->username => null,
null === $this->password => Encoder::encodeUser($this->username),
default => Encoder::encodeUser($this->username).':'.Encoder::encodePassword($this->password),
null === $this->password => $this->getUsername(),
default => $this->getUsername().':'.$this->getPassword(),
};
}

Expand All @@ -134,6 +133,22 @@ public function getPass(): ?string
return $this->password;
}

public function getUsername(): ?string
{
return match ($this->username) {
null => null,
default => Encoder::encodeUser($this->username)
};
}

public function getPassword(): ?string
{
return match ($this->password) {
null => null,
default => Encoder::encodePassword($this->password)
};
}

/**
* @return array{user: ?string, pass: ?string}
*/
Expand Down
10 changes: 10 additions & 0 deletions components/Modifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,16 @@ public function hostToHexadecimal(): static
};
}

public function hostToIpv6Compressed(): static
{
return new static($this->uri->withHost(Host::fromUri($this->uri)->compress()));
}

public function hostToIpv6Expanded(): static
{
return new static($this->uri->withHost(Host::fromUri($this->uri)->expand()));
}

/**
* Prepend a label or a host to the current URI host.
*
Expand Down
1 change: 1 addition & 0 deletions docs/_data/menu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ packages:
URI Parser: '/interfaces/7.0/uri-parser-builder/'
Query Parser: '/interfaces/7.0/query-parser-builder/'
IPv4 Converter: '/interfaces/7.0/ipv4/'
IPv6 Converter: '/interfaces/7.0/ipv6/'
IDN Converter: '/interfaces/7.0/idn/'
'2.0':
Documentation:
Expand Down
6 changes: 6 additions & 0 deletions docs/components/7.0/host.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,14 @@ public Host::isIpv6(): bool
public Host::isIpFuture(): bool
public Host::hasZoneIdentifier(): bool
public Host::withoutZoneIdentifier(): self
public Host::compress(): string
public Host::expand(): string
~~~

<p class="message-info">The <code>Host::compress</code> and <code>Host::expand</code>,
are added in version <code>7.6.0</code> and normalized an IPv6 host, See the <a href="/components/7.0/ipv6/">IPv6 Converter documentation</a> page for more information.
</p>

### Host::fromIp

This method allows creating a Host object from an IP.
Expand Down
30 changes: 30 additions & 0 deletions docs/components/7.0/modifiers.md
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,36 @@ echo Modifier::from($uri)->hostToOctal()->getUri();
//display 'http://0xc0a811/path/to/the/sky.php'
~~~

### Modifier::hostToIpv6Compressed

Normalizes the URI host content to a compressed IPv6 notation if possible.
See the [IPv6 Converter documentation](/components/7.0/ipv6/) page for more information.

~~~php
<?php

use League\Uri\Modifier;

$uri = 'http://[1050:0000:0000:0000:0005:0000:300c:326b]/path/to/the/sky.php';
echo Modifier::from($uri)->hostToIpv6Compressed()->getUriString();
//display 'http://[1050::5:0:300c:326b]/path/to/the/sky.php'
~~~

### Modifier::hostToIpv6Expanded

Normalizes the URI host content to a expanded IPv6 notation if possible.
See the [IPv6 Converter documentation](/components/7.0/ipv6/) page for more information.

~~~php
<?php

use League\Uri\Modifier;

$uri = 'http://[0000:0000:0000:0000:0000:0000:0000:0001]/path/to/the/sky.php';
echo Modifier::from($uri)->hostToIpv6Compressed()->getUriString();
//display 'http://[::1]/path/to/the/sky.php'
~~~

### Modifier::removeZoneIdentifier

Removes the host zone identifier if present
Expand Down
10 changes: 6 additions & 4 deletions docs/interfaces/7.0/contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ The `UriInterface` interface defines the following methods to access the URI str

public UriInterface::getScheme(): ?string
public UriInterface::getUserInfo(): ?string
public UriInterface::getUsername(): ?string
public UriInterface::getPassword(): ?string
public UriInterface::getHost(): ?string
public UriInterface::getPort(): ?int
public UriInterface::getAuthority(): ?string
Expand All @@ -51,7 +53,7 @@ Delimiter characters are not part of the URI component and **must not** be added
<?php

public UriInterface::withScheme(Stringable|string|null $scheme): self
public UriInterface::withUserInfo(Stringable|string|null $user [, string $password = null]): self
public UriInterface::withUserInfo(Stringable|string|null $user [, Stringable|string|null $password = null]): self
public UriInterface::withHost(Stringable|string|null $host): self
public UriInterface::withPort(?int $port): self
public UriInterface::withPath(Stringable|string $path): self
Expand All @@ -64,8 +66,8 @@ public UriInterface::withFragment(Stringable|string|null $fragment): self
This interface exposes the same methods as `Psr\Http\Message\UriInterface`. But, differs on the following keys:

- This interface does not require the `http` and `https` schemes to be supported.
- Setter and Getter component methods, with the exception of the path component, accept and can return the `null` value.
- If no scheme is present, you are not required to fallback to `http` and `https` schemes specific validation rules.
- Setter and Getter component methods, except the path component, accept and can return the `null` value.
- If no scheme is present, the requirement to fall back to `http` and `https` schemes specific validation rules is not enforced.

### League\Uri\Contract\UriComponentInterface

Expand All @@ -78,7 +80,7 @@ The `UriComponentInterface` interface defines the following methods to access th
~~~php
<?php
public UriComponentInterface::value(): ?string
public UriComponentInterface::toString(): ?string
public UriComponentInterface::toString(): string
public UriComponentInterface::getUriComponent(): ?string
public UriComponentInterface::jsonSerialize(): ?string
public UriComponentInterface::__toString(): string
Expand Down
41 changes: 41 additions & 0 deletions docs/interfaces/7.0/ipv6.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
layout: default
title: IPv6 Converter
---

IPv6 Converter
=======

The `League\Uri\IPv6\Converter` is a IPv6 Converter.

```php
<?php

use League\Uri\IPv6\Converter;

echo Converter::expand('[::1]');
// returns '[0000:0000:0000:0000:0000:0000:0000:0001]'
echo Converter::compress('[1050:0000:0000:0000:0005:0000:300c:326b]');
// returns [1050::5:0:300c:326b]
```

Usage
--------

The `Converter::compress` method convert an expanded IPv6 host into its compressed form.
The method only parameter should represent a host value. The `Converter::exoend` method
does the opposite.

If you submit a host which is not an IPv6 one then, the submitted host value will be returned
as is.Conversely, trying to expand an IPv6 host which is already expanded or trying to compress
an already compressed IPv6 host will return the same value so nothing will be gain performing
such obvious operation.

```php
<?php

echo Converter::compress('[::1]');
// returns '[::1]'
echo Converter::expand('[1050:0000:0000:0000:0005:0000:300c:326b]');
// returns [1050:0000:0000:0000:0005:0000:300c:326b]
```
2 changes: 1 addition & 1 deletion interfaces/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ All Notable changes to `League\Uri\Interfaces` will be documented in this file

- `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

- Adding Host resolution caching to speed up URI parsing in `UriString`
Expand Down
94 changes: 94 additions & 0 deletions interfaces/IPv6/Converter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php

declare(strict_types=1);

namespace League\Uri\IPv6;

use Stringable;
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 compress(Stringable|string|null $host): ?string
{
$components = self::parse($host);
if (null === $components['ipv6']) {
return match ($host) {
null => $host,
default => (string) $host,
};
}

['ipv6' => $ipv6, 'zoneIdentifier' => $zoneIdentifier] = $components;
$ipv6 = (string) inet_ntop((string) inet_pton($ipv6));

return self::build($ipv6, $zoneIdentifier);
}

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,
};
}

['ipv6' => $ipv6, 'zoneIdentifier' => $zoneIdentifier] = $components;
$hex = (array) unpack("H*hex", (string) inet_pton($ipv6));
$ipv6 = implode(':', str_split(strtolower($hex['hex'] ?? ''), 4));

return self::build($ipv6, $zoneIdentifier);
}

private static function build(string $ip, ?string $zoneIdentifier): string
{
$zoneIdentifier = match ($zoneIdentifier) {
null => '',
default => '%'.$zoneIdentifier,
};

return '['.$ip.$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];
}
}

0 comments on commit c33bb44

Please sign in to comment.