Skip to content

Commit

Permalink
Remove Link handling related feature
Browse files Browse the repository at this point in the history
  • Loading branch information
nyamsprod committed Jan 24, 2025
1 parent 33879de commit ec1d0d3
Show file tree
Hide file tree
Showing 4 changed files with 32 additions and 351 deletions.
2 changes: 0 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ All Notable changes to `League\Uri` will be documented in this file
- `Uri::getUser` returns the encoded user component of the URI an alias for `Uri::getUsername`
- `Uri::fromMarkdownAnchor`
- `Uri::fromHtmlAnchor`
- `Uri::fromHtmlLink`
- `Uri::fromHeaderLinkValue`

### Fixed

Expand Down
53 changes: 0 additions & 53 deletions FactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -653,59 +653,6 @@ public static function provideInvalidMarkdown(): iterable
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"'];
yield 'header value with invalid parameters' => ['html' => '<https://example.com/page1> title="prev"; rel="Previous Page"'];
}

#[Test]
#[DataProvider('provideInvalidUri')]
public function it_fails_to_parse_with_new(Stringable|string|null $uri): void
Expand Down
207 changes: 32 additions & 175 deletions Uri.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
use DOMDocument;
use DOMException;
use finfo;
use InvalidArgumentException;
use League\Uri\Contracts\Conditionable;
use League\Uri\Contracts\UriComponentInterface;
use League\Uri\Contracts\UriException;
Expand Down Expand Up @@ -59,17 +58,13 @@
use function inet_pton;
use function is_array;
use function is_bool;
use function is_float;
use function is_int;
use function is_string;
use function json_encode;
use function ltrim;
use function preg_match;
use function preg_replace_callback;
use function rawurldecode;
use function rawurlencode;
use function restore_error_handler;
use function round;
use function set_error_handler;
use function sprintf;
use function str_contains;
Expand All @@ -87,14 +82,9 @@
use const FILEINFO_MIME_TYPE;
use const FILTER_FLAG_IPV4;
use const FILTER_FLAG_IPV6;
use const FILTER_FLAG_STRIP_HIGH;
use const FILTER_FLAG_STRIP_LOW;
use const FILTER_NULL_ON_FAILURE;
use const FILTER_UNSAFE_RAW;
use const FILTER_VALIDATE_BOOLEAN;
use const FILTER_VALIDATE_IP;
use const JSON_PRESERVE_ZERO_FRACTION;
use const PHP_ROUND_HALF_EVEN;

/**
* @phpstan-import-type ComponentMap from UriString
Expand Down Expand Up @@ -743,68 +733,51 @@ public static function fromMarkdownAnchor(Stringable|string $markdown, Stringabl
};
}

public static function fromHeaderLinkValue(Stringable|string $headerValue, Stringable|string|null $baseUri = null): self
/**
* If the html content contains more than one anchor element, only the first one will be parsed.
*
* @throws DOMException
*/
public static function fromHtmlAnchor(Stringable|string $html, Stringable|string|null $baseUri = null): self
{
$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 InvalidArgumentException('As per RFC8288, the URI must be defined inside two `<>` characters.');
}

$parameters = ltrim($matches['parameters']);
if (!str_starts_with($parameters, ';')) {
throw new InvalidArgumentException('The value `'.$headerValue.'` contains invalid characters.');
}

$attributes = [];
if (false !== preg_match_all('/;\s*(?<name>\w*)\*?="(?<value>[^"]*)"/', $parameters, $attrMatches, PREG_SET_ORDER)) {
foreach ($attrMatches as $attrMatch) {
$attributes[$attrMatch['name']] = $attrMatch['value'];
FeatureDetection::supportsDom();
$html = (string) $html;
set_error_handler(fn (int $errno, string $errstr, string $errfile, int $errline) => true);
try {
$result = true;
$exception = null;
if (class_exists(HTMLDocument::class)) {
$dom = HTMLDocument::createFromString($html);
} else {
$dom = new DOMDocument();
$result = $dom->loadHTML($html);
}
} catch (Throwable $exception) {
$result = false;
$dom = null;
}
restore_error_handler();
if (false === $result || null === $dom) {
throw $exception ?? new DOMException('The content could not be parsed as a valid HTML content.');
}

if (!isset($attributes['rel'])) {
throw new SyntaxError('The `rel` attribute must be defined.');
$element = $dom->getElementsByTagName('a')->item(0);
if (null === $element) {
throw new DOMException('No anchor element was found in the content.');
}

$uri = $element->getAttribute('href');
if (null !== $baseUri) {
$baseUri = (string) $baseUri;
}

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

/**
* If the html content contains more than one anchor element, only the first one will be parsed.
*
* @throws DOMException
*/
public static function fromHtmlAnchor(string $html, Stringable|string|null $baseUri = null): self
{
return self::parseHtml($html, 'a', 'href', $baseUri);
}

/**
* If the html content contains more than one link element, only the first one will be parsed.
*
* @throws DOMException
*/
public static function fromHtmlLink(string $html, Stringable|string|null $baseUri = null): self
{
return self::parseHtml($html, 'link', 'href', $baseUri);
}

/**
* Returns the environment scheme.
*/
Expand Down Expand Up @@ -1202,83 +1175,6 @@ public function toHtmlAnchor(?string $linkTextTemplate = null, iterable $attribu
return self::buildHtml($this, 'a', $attributes, $content);
}

/**
* Returns the Link tag content for the current instance.
*
* @param iterable<string, string|null|array<string>> $attributes an ordered map of key value. you must quote the value if needed
*
* @throws DOMException
*/
public function toHtmlLink(iterable $attributes = []): string
{
return self::buildHtml($this, 'link', $attributes, null);
}

/**
* Returns the Link header content for a single item.
*
* @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 && false !== $val) {
$attributes[] = $this->formatHeaderValueParameter($name, $val);
}
}

$value = '<'.$this->toString().'>';
if ([] === $attributes) {
return $value;
}

return $value.implode('', $attributes);
}

private function formatHeaderValueParameter(string $name, string|int|float|bool $value): string
{
$name = strtolower($name);

return '; '.$name.match (true) {
1 !== preg_match('/^([a-z*][a-z\d.*_-]*)$/i', $name) => throw new InvalidArgumentException('The parameter name `'.$name.'` contains invalid characters.'),
true === $value => '',
false === $value => '=?0',
is_float($value) => '="'.json_encode(round($value, 3, PHP_ROUND_HALF_EVEN), JSON_PRESERVE_ZERO_FRACTION).'"',
is_int($value) => '="'.$value.'"',
default => $this->formatHeaderValueStringParameter($name, $value),
};
}

private function formatHeaderValueStringParameter(string $name, string $value): string
{
if (
1 === preg_match("/(?:(?:(?<!\r)\n)|(?:\r(?!\n))|(?:\r\n(?![ \t])))/", $value) ||
1 === preg_match('/[^\x09\x0a\x0d\x20-\x7E\x80-\xFE]/', $value)
) {
throw new InvalidArgumentException('The value `'.$value.'` contains invalid characters.');
}

$flag = FILTER_FLAG_STRIP_LOW;
if (1 === preg_match('/[^\x20-\x7E]/', $value)) {
$flag |= FILTER_FLAG_STRIP_HIGH;
}

$filteredValue = str_replace('%', '', (string) filter_var($value, FILTER_UNSAFE_RAW, $flag));
$headerValue = sprintf('="%s"', str_replace('"', '\\"', $filteredValue));
if ($value === $filteredValue) {
return $headerValue;
}

return $headerValue.sprintf("; %s*=utf-8''%s", $name, preg_replace_callback(
'/[%"\x00-\x1F\x7F-\xFF]/',
static fn (array $matches): string => strtolower(rawurlencode($matches[0])),
$value
));
}

/**
* Returns the Unix filesystem path.
*
Expand Down Expand Up @@ -1937,45 +1833,6 @@ public function __unserialize(array $data): void
$this->origin = $this->setOrigin();
}

private static function parseHtml(
Stringable|string $content,
string $tagName,
string $attributeName,
Stringable|string|null $baseUri = null
): self {
FeatureDetection::supportsDom();

set_error_handler(fn (int $errno, string $errstr, string $errfile, int $errline) => true);
$result = true;
if (class_exists(HTMLDocument::class)) {
$dom = HTMLDocument::createFromString((string) $content);
} else {
$dom = new DOMDocument();
$result = $dom->loadHTML((string) $content);
}
restore_error_handler();
if (false === $result) {
throw new DOMException('The content could not be parsed as a valid HTML content.');
}

$tag = $dom->getElementsByTagName($tagName)->item(0);
if (null === $tag) {
throw new DOMException('No `'.$tagName.'` element was found in the content.');
}

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

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

return match (true) {
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),
};
}

/**
* @param iterable<string, string|null|list<string>> $attributes
*
Expand Down
Loading

0 comments on commit ec1d0d3

Please sign in to comment.