Skip to content

Commit

Permalink
Improve UriRenderer interface
Browse files Browse the repository at this point in the history
  • Loading branch information
nyamsprod committed Jan 23, 2025
1 parent da7a72d commit e234ab2
Show file tree
Hide file tree
Showing 3 changed files with 140 additions and 16 deletions.
4 changes: 2 additions & 2 deletions interfaces/Contracts/UriRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public function toHtmlAnchor(?string $linkTextTemplate = null, iterable $attribu
/**
* Returns the Link tag content for the current instance.
*
* @param iterable<string, string|null> $attributes an ordered map of key value. you must quote the value if needed
* @param iterable<string, string|null> $attributes an ordered map of key value
*
* @throws DOMException
*/
Expand All @@ -81,7 +81,7 @@ public function toHtmlLink(iterable $attributes = []): string;
/**
* Returns the Link header content for a single item.
*
* @param iterable<string, string|int|float|bool> $parameters an ordered map of key value. you must quote the value if needed
* @param iterable<string, string|int|float|bool|null> $parameters an ordered map of key value.
*
* @see https://www.rfc-editor.org/rfc/rfc7230.html#section-3.2.6
*/
Expand Down
93 changes: 93 additions & 0 deletions uri/FactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -637,4 +637,97 @@ public static function provideAnchorTagMarkdown(): iterable
'expected' => 'https://example.com/foobar',
];
}

#[Test]
#[DataProvider('provideInvalidMarkdown')]
public function it_fails_to_parse_an_invalid_markdown(string $html): void
{
$this->expectException(InvalidArgumentException::class);
Uri::fromMarkdownAnchor($html);
}

public static function provideInvalidMarkdown(): iterable
{
yield 'missing markdown placeholder' => ['html' => 'this is **markdown**'];
yield 'invalid markdown placeholder; missing URI part' => ['html' => '[this is an imcomplete] http://example.com markdown'];
yield 'invalid markdown placeholder; missing content part' => ['html' => 'this is an imcomplete(http://example.com) markdown'];
}

#[Test]
#[DataProvider('provideHeaderLinkValue')]
public function it_parses_uri_string_from_an_link_header_value(string $html, ?string $baseUri, string $expected): void
{
self::assertSame($expected, Uri::fromHeaderLinkValue($html, $baseUri)->toString());
}

public static function provideHeaderLinkValue(): iterable
{
yield 'empty string' => [
'html' => '<>; rel="previous"',
'baseUri' => null,
'expected' => '',
];

yield 'empty string with base URI' => [
'html' => '<>; rel="next"',
'baseUri' => 'https://example.com/',
'expected' => 'https://example.com/',
];

yield 'URI with base URI' => [
'html' => '</style.css>; rel="stylesheet"',
'baseUri' => 'https://www.example.com',
'expected' => 'https://www.example.com/style.css',
];

yield 'multiple anchor tag' => [
'html' => '</style.css>; rel="stylesheet", </foobar.css>; rel="stylesheet"',
'baseUri' => 'https://example.com/',
'expected' => 'https://example.com/style.css',
];
}

#[Test]
#[DataProvider('provideInvalidHeaderLinkValue')]
public function it_fails_to_parse_an_invalid_http_header_link_with_invalid_characters(string $html): void
{
$this->expectException(InvalidArgumentException::class);

Uri::fromHeaderLinkValue($html);
}

public static function provideInvalidHeaderLinkValue(): iterable
{
yield 'header value with invalid characters' => ['html' => '</style.css>; title="stylesheet"'."\r"];
yield 'header value with missing URI part' => ['html' => '; rel="stylesheet"'];
yield 'header value with missing semicolon' => ['html' => '</style.css> title="stylesheet"'];
yield 'header value with missing parameters' => ['html' => '</style.css>'];
yield 'header value with missing rel parameter' => ['html' => '</style.css> title="stylesheet"'];
}

#[Test]
#[DataProvider('provideInvalidUri')]
public function it_fails_to_parse_with_new(Stringable|string|null $uri): void
{
self::assertNull(Uri::tryNew($uri));
}

public static function provideInvalidUri(): iterable
{
yield 'null value' => ['uri' => null];
yield 'invalid URI' => ['uri' => 'http://example.com/ '];
}

#[Test]
#[DataProvider('provideInvalidUriForResolution')]
public function it_fails_to_parse_with_parse(Stringable|string $uri, Stringable|string|null $baseUri): void
{
self::assertNull(Uri::parse($uri, $baseUri));
}

public static function provideInvalidUriForResolution(): iterable
{
yield 'invalid URI' => ['uri' => ':', 'baseUri' => null];
yield 'invalid resolution with a non absolute URI' => ['uri' => '', 'baseUri' => '/absolute/path'];
}
}
59 changes: 45 additions & 14 deletions uri/Uri.php
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,8 @@ final class Uri implements Conditionable, UriInterface, UriRenderer, UriInspecto
/** @var array<string,int> */
private const WHATWG_SPECIAL_SCHEMES = ['ftp' => 1, 'http' => 1, 'https' => 1, 'ws' => 1, 'wss' => 1];

private const ABOUT_BLANK = 'about:blank';

private readonly ?string $scheme;
private readonly ?string $user;
private readonly ?string $pass;
Expand Down Expand Up @@ -666,8 +668,8 @@ public static function fromUnixPath(Stringable|string $path): self
*/
public static function fromWindowsPath(Stringable|string $path): self
{
$path = (string) $path;
$root = '';
$path = (string) $path;
if (1 === preg_match(self::REGEXP_WINDOW_PATH, $path, $matches)) {
$root = substr($matches['root'], 0, -1).':';
$path = substr($path, strlen($root));
Expand Down Expand Up @@ -731,26 +733,51 @@ public static function fromMarkdownAnchor(Stringable|string $markdown, Stringabl
throw new SyntaxError('The markdown string `'.$markdown.'` is not valid anchor markdown tag.');
}

if (null === $baseUri) {
return self::new($matches['uri']);
if (null !== $baseUri) {
$baseUri = (string) $baseUri;
}

return self::fromBaseUri($matches['uri'], $baseUri);
return match ($baseUri) {
self::ABOUT_BLANK, null => self::new($matches['uri']),
default => self::fromBaseUri($matches['uri'], $baseUri),
};
}

public static function fromHeaderLinkValue(Stringable|string $headerValue, Stringable|string|null $baseUri = null): self
{
static $regexp = '/<(?<uri>.*?)>.*/';
$headerValue = rtrim((string) $headerValue);
$headerValue = (string) $headerValue;
if (
1 === preg_match("/(?:(?:(?<!\r)\n)|(?:\r(?!\n))|(?:\r\n(?![ \t])))/", $headerValue) ||
1 === preg_match('/[^\x09\x0a\x0d\x20-\x7E\x80-\xFE]/', $headerValue)
) {
throw new InvalidArgumentException('The value `'.$headerValue.'` contains invalid characters.');
}

$headerValue = ltrim($headerValue);
static $regexp = '/<(?<uri>.*?)>(?<parameters>.*)/';
if (1 !== preg_match($regexp, $headerValue, $matches)) {
throw new SyntaxError('As per RFC8288, the URI must be defined inside two `<>` characters.');
throw new InvalidArgumentException('As per RFC8288, the URI must be defined inside two `<>` characters.');
}

if (null === $baseUri) {
return self::new($matches['uri']);
$attributes = [];
if (false !== preg_match_all('/;\s*(?<name>\w*)\*?="(?<value>[^"]*)"/', $matches['parameters'], $attrMatches, PREG_SET_ORDER)) {
foreach ($attrMatches as $attrMatch) {
$attributes[$attrMatch['name']] = $attrMatch['value'];
}
}

if (!isset($attributes['rel'])) {
throw new SyntaxError('The `rel` attribute must be defined.');
}

if (null !== $baseUri) {
$baseUri = (string) $baseUri;
}

return self::fromBaseUri($matches['uri'], $baseUri);
return match ($baseUri) {
self::ABOUT_BLANK, null => self::new($matches['uri']),
default => self::fromBaseUri($matches['uri'], $baseUri),
};
}

/**
Expand Down Expand Up @@ -1181,15 +1208,15 @@ public function toHtmlLink(iterable $attributes = []): string
/**
* Returns the Link header content for a single item.
*
* @param iterable<string, string|int|float|bool> $parameters an ordered map of key value. you must quote the value if needed
* @param iterable<string, string|int|float|bool|null> $parameters an ordered map of key value.
*
* @see https://www.rfc-editor.org/rfc/rfc7230.html#section-3.2.6
*/
public function toHeaderLinkValue(iterable $parameters = []): string
{
$attributes = [];
foreach ($parameters as $name => $val) {
if (null !== $val) {
if (null !== $val && false !== $val) {
$attributes[] = $this->formatHeaderValueParameter($name, $val);
}
}
Expand Down Expand Up @@ -1929,9 +1956,13 @@ private static function parseHtml(

$uri = $tag->getAttribute($attributeName);

if (null !== $baseUri) {
$baseUri = (string) $baseUri;
}

return match (true) {
null !== $baseUri => self::fromBaseUri($uri, $baseUri),
null !== $dom->documentURI && 'about:blank' !== $dom->documentURI => self::fromBaseUri($uri, $dom->documentURI),
null !== $baseUri && self::ABOUT_BLANK !== $baseUri => self::fromBaseUri($uri, $baseUri),
null !== $dom->documentURI && self::ABOUT_BLANK !== $dom->documentURI => self::fromBaseUri($uri, $dom->documentURI),
default => self::new($uri),
};
}
Expand Down

0 comments on commit e234ab2

Please sign in to comment.