Skip to content

Commit

Permalink
Adding more URI string representation (#150)
Browse files Browse the repository at this point in the history
  • Loading branch information
nyamsprod authored Dec 24, 2024
1 parent 03fb333 commit d74c6b3
Show file tree
Hide file tree
Showing 3 changed files with 286 additions and 11 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ All Notable changes to `League\Uri` will be documented in this file
- `Uri::getOrigin`
- `Uri::isSameOrigin`
- `Uri::isCrossOrigin`
- `Uri::todisplayString` shows the URI in a human-readable format which may be an invalid URI.
- `Uri::toUnixPath` returns the URI path as a Unix Path or `null`
- `Uri::toWindowsPath` returns the URI path as a Windows Path or `null`
- `Uri::toRfc8089` return the URI in a RFC8089 formator `null`

### Fixed

Expand Down
104 changes: 93 additions & 11 deletions Uri.php
Original file line number Diff line number Diff line change
Expand Up @@ -1000,25 +1000,112 @@ private function isNonEmptyHostUriWithoutFragmentAndQuery(): bool
return $this->isNonEmptyHostUri() && null === $this->fragment && null === $this->query;
}

public function __toString(): string
{
return $this->toString();
}

public function jsonSerialize(): string
{
return $this->toString();
}

public function toString(): string
{
return $this->uri;
}

public function toNormalizedString(): string
{
return $this->normalize()->toString();
}

public function toDisplayString(): string
{
/** @var ComponentMap $components */
$components = array_map(
fn (?string $value): ?string => (null === $value || '' === $value) ? $value : rawurldecode($value),
$this->normalize()->toComponents()
);

if (null !== $components['host']) {
$components['host'] = IdnaConverter::toUnicode($components['host'])->domain();
}

if ('/' === $components['path'] && null !== $this->authority) {
$components['path'] = '';
}

return UriString::build($components);
}

/**
* {@inheritDoc}
* Returns the Unix filesystem path.
*
* The method will return null if a scheme is present and is not the `file` scheme
*/
public function __toString(): string
public function toUnixPath(): ?string
{
return $this->toString();
return match ($this->scheme) {
'file', null => rawurldecode($this->path),
default => null,
};
}

/**
* {@inheritDoc}
* Returns the Windows filesystem path.
*
* The method will return null if a scheme is present and is not the `file` scheme
*/
public function jsonSerialize(): string
public function toWindowsPath(): ?string
{
return $this->toString();
static $regexpWindowsPath = ',^(?<root>[a-zA-Z]:),';

if (!in_array($this->scheme, ['file', null], true)) {
return null;
}

$originalPath = $this->path;
$path = $originalPath;
if ('/' === ($path[0] ?? '')) {
$path = substr($path, 1);
}

if (1 === preg_match($regexpWindowsPath, $path, $matches)) {
$root = $matches['root'];
$path = substr($path, strlen($root));

return $root.str_replace('/', '\\', rawurldecode($path));
}

$host = $this->host;

return match (null) {
$host => str_replace('/', '\\', rawurldecode($originalPath)),
default => '\\\\'.$host.'\\'.str_replace('/', '\\', rawurldecode($path)),
};
}

/**
* Returns a string representation of a File URI according to RFC8089.
*
* The method will return null if the URI scheme is not the `file` scheme
*
* @see https://datatracker.ietf.org/doc/html/rfc8089
*/
public function toRfc8089(): ?string
{
$path = $this->path;

return match (true) {
'file' !== $this->scheme => null,
in_array($this->authority, ['', null, 'localhost'], true) => 'file:'.match (true) {
'' === $path,
'/' === $path[0] => $path,
default => '/'.$path,
},
default => $this->toString(),
};
}

/**
Expand Down Expand Up @@ -1408,11 +1495,6 @@ public function equals(UriInterface|Stringable|string $uri, bool $excludeFragmen
};
}

public function toNormalizedString(): string
{
return $this->normalize()->toString();
}

/**
* Tells whether the URI contains an Internationalized Domain Name (IDN).
*/
Expand Down
189 changes: 189 additions & 0 deletions UriTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@

namespace League\Uri;

use GuzzleHttp\Psr7\Utils;
use League\Uri\Components\HierarchicalPath;
use League\Uri\Components\Port;
use League\Uri\Exceptions\SyntaxError;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\UriInterface as Psr7UriInterface;
use TypeError;
Expand Down Expand Up @@ -792,4 +794,191 @@ public static function getCrossOriginExamples(): array
'cross origin using a blob' => ['blob:http://mozilla.org:443/', 'https://mozilla.org/123', true],
];
}

#[DataProvider('idnUriProvider')]
public function testItReturnsTheCorrectUriString(string $expected, string $input): void
{
self::assertSame($expected, Uri::new($input)->toDisplayString());
}

public static function idnUriProvider(): iterable
{
yield 'basic uri stays the same' => [
'expected' => 'http://example.com/foo/bar',
'input' => 'http://example.com/foo/bar',
];

yield 'idn host are changed' => [
'expected' => 'http://bébé.be',
'input' => 'http://xn--bb-bjab.be',
];

yield 'idn host are the same' => [
'expected' => 'http://bébé.be',
'input' => 'http://bébé.be',
];

yield 'the rest of the URI is not affected and uses RFC3986 rules' => [
'expected' => 'http://bébé.be?q=toto le héros',
'input' => 'http://bébé.be:80?q=toto%20le%20h%C3%A9ros',
];
}

#[DataProvider('unixpathProvider')]
public function testReturnsUnixPath(?string $expected, string $input): void
{
self::assertSame($expected, Uri::new($input)->toUnixPath());
self::assertSame($expected, Uri::new(Utils::uriFor($input))->toUnixPath());
}

public static function unixpathProvider(): array
{
return [
'relative path' => [
'expected' => 'path',
'input' => 'path',
],
'absolute path' => [
'expected' => '/path',
'input' => 'file:///path',
],
'path with empty char' => [
'expected' => '/path empty/bar',
'input' => 'file:///path%20empty/bar',
],
'relative path with dot segments' => [
'expected' => 'path/./relative',
'input' => 'path/./relative',
],
'absolute path with dot segments' => [
'expected' => '/path/./../relative',
'input' => 'file:///path/./../relative',
],
'unsupported scheme' => [
'expected' => null,
'input' => 'http://example.com/foo/bar',
],
];
}

#[DataProvider('windowLocalPathProvider')]
public function testReturnsWindowsPath(?string $expected, string $input): void
{
self::assertSame($expected, Uri::new($input)->toWindowsPath());
}

public static function windowLocalPathProvider(): array
{
return [
'relative path' => [
'expected' => 'path',
'input' => 'path',
],
'relative path with dot segments' => [
'expected' => 'path\.\relative',
'input' => 'path/./relative',
],
'absolute path' => [
'expected' => 'c:\windows\My Documents 100%20\foo.txt',
'input' => 'file:///c:/windows/My%20Documents%20100%2520/foo.txt',
],
'windows relative path' => [
'expected' => 'c:My Documents 100%20\foo.txt',
'input' => 'file:///c:My%20Documents%20100%2520/foo.txt',
],
'absolute path with `|`' => [
'expected' => 'c:\windows\My Documents 100%20\foo.txt',
'input' => 'file:///c:/windows/My%20Documents%20100%2520/foo.txt',
],
'windows relative path with `|`' => [
'expected' => 'c:My Documents 100%20\foo.txt',
'input' => 'file:///c:My%20Documents%20100%2520/foo.txt',
],
'absolute path with dot segments' => [
'expected' => '\path\.\..\relative',
'input' => '/path/./../relative',
],
'absolute UNC path' => [
'expected' => '\\\\server\share\My Documents 100%20\foo.txt',
'input' => 'file://server/share/My%20Documents%20100%2520/foo.txt',
],
'unsupported scheme' => [
'expected' => null,
'input' => 'http://example.com/foo/bar',
],
];
}

#[DataProvider('rfc8089UriProvider')]
public function testReturnsRFC8089UriString(?string $expected, string $input): void
{
self::assertSame($expected, Uri::new($input)->toRfc8089());
}

public static function rfc8089UriProvider(): iterable
{
return [
'localhost' => [
'expected' => 'file:/etc/fstab',
'input' => 'file://localhost/etc/fstab',
],
'empty authority' => [
'expected' => 'file:/etc/fstab',
'input' => 'file:///etc/fstab',
],
'file with authority' => [
'expected' => 'file://yesman/etc/fstab',
'input' => 'file://yesman/etc/fstab',
],
'invalid scheme' => [
'expected' => null,
'input' => 'foobar://yesman/etc/fstab',
],
];
}

#[Test]
#[DataProvider('providesUriToDisplay')]
public function it_will_generate_the_display_uri_string(string $input, string $output): void
{
self::assertSame($output, Uri::new($input)->toDisplayString());
}

public static function providesUriToDisplay(): iterable
{
yield 'empty string' => [
'input' => '',
'output' => '',
];

yield 'host IPv6' => [
'input' => 'https://[fe80:0000:0000:0000:0000:0000:0000:000a%25en1]/foo/bar',
'output' => 'https://[fe80::a%en1]/foo/bar',
];

yield 'IPv6 gets expanded if needed' => [
'input' => 'http://bébé.be?q=toto%20le%20h%C3%A9ros',
'output' => 'http://bébé.be?q=toto le héros',
];

yield 'complex URI' => [
'input' => 'https://xn--google.com/secret/../search?q=%F0%9F%8D%94',
'output' => 'https://䕮䕵䕶䕱.com/search?q=🍔',
];

yield 'basic uri stays the same' => [
'input' => 'http://example.com/foo/bar',
'output' => 'http://example.com/foo/bar',
];

yield 'idn host are changed' => [
'input' => 'http://xn--bb-bjab.be',
'output' => 'http://bébé.be',
];

yield 'idn host are the same' => [
'input' => 'http://bébé.be',
'output' => 'http://bébé.be',
];
}
}

0 comments on commit d74c6b3

Please sign in to comment.