Skip to content

Commit

Permalink
Introduce the Encoder class
Browse files Browse the repository at this point in the history
* Introduce the Encoder class to normalize encoding/decoding in all packages
* Introduce the KeyValuePairConverter class to normalize key/value parsing and building
* Rewrite QueryString parser/builder class
* Add new methods to QueryString
  • Loading branch information
nyamsprod authored Aug 19, 2023
1 parent c1cf91e commit 2b57d2a
Show file tree
Hide file tree
Showing 10 changed files with 97 additions and 117 deletions.
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,24 @@

All Notable changes to `League\Uri\Components` will be documented in this file

## Next - TBD

### Added

- `Modifier::encodeQuery`

### Fixed

- Using the `Encoder` class to normalize encoding and decoding in all packages

### Deprecated

- None

### Removed

- None

## [7.0.0](https://github.com/thephpleague/uri-components/compare/2.4.1...7.0.0) - 2023-08-10

### Added
Expand Down
59 changes: 2 additions & 57 deletions Components/Component.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,31 +16,17 @@
use League\Uri\Contracts\UriAccess;
use League\Uri\Contracts\UriComponentInterface;
use League\Uri\Contracts\UriInterface;
use League\Uri\Encoder;
use League\Uri\Exceptions\SyntaxError;
use League\Uri\Uri;
use Psr\Http\Message\UriInterface as Psr7UriInterface;
use Stringable;
use function preg_match;
use function preg_replace_callback;
use function rawurldecode;
use function rawurlencode;
use function sprintf;
use function strtoupper;

abstract class Component implements UriComponentInterface
{
protected const REGEXP_ENCODED_CHARS = ',%[A-Fa-f0-9]{2},';
protected const REGEXP_INVALID_URI_CHARS = '/[\x00-\x1f\x7f]/';
protected const REGEXP_NO_ENCODING = '/[^A-Za-z0-9_\-.~]/';
protected const REGEXP_NON_ASCII_PATTERN = '/[^\x20-\x7f]/';
protected const REGEXP_PREVENTS_DECODING = ',%
2[A-F|1-2|4-9]|
3[0-9|B|D]|
4[1-9|A-F]|
5[0-9|A|F]|
6[1-9|A-F]|
7[0-9|E]
,ix';

abstract public function value(): ?string;

Expand Down Expand Up @@ -78,7 +64,7 @@ final protected static function filterUri(Stringable|string $uri): UriInterface|
*/
protected function validateComponent(Stringable|int|string|null $component): ?string
{
return $this->decodeComponent(self::filterComponent($component));
return Encoder::decodePartial($component);
}

/**
Expand All @@ -95,45 +81,4 @@ final protected static function filterComponent(Stringable|int|string|null $comp
default => (string) $component,
};
}

/**
* Filter the URI password component.
*/
protected function decodeComponent(?string $str): ?string
{
return match (true) {
null === $str => null,
default => preg_replace_callback(self::REGEXP_ENCODED_CHARS, $this->decodeMatches(...), $str),
};
}

/**
* Decodes Matches sequence.
*/
protected function decodeMatches(array $matches): string
{
return match (true) {
1 === preg_match(static::REGEXP_PREVENTS_DECODING, $matches[0]) => strtoupper($matches[0]),
default => rawurldecode($matches[0]),
};
}

/**
* Returns the component as converted for RFC3986.
*/
protected function encodeComponent(?string $str, string $regexp): ?string
{
return match (true) {
null === $str || 1 !== preg_match(self::REGEXP_NO_ENCODING, $str) => $str,
default => preg_replace_callback($regexp, $this->encodeMatches(...), $str) ?? rawurlencode($str),
};
}

/**
* Encode Matches sequence.
*/
protected function encodeMatches(array $matches): string
{
return rawurlencode($matches[0]);
}
}
20 changes: 10 additions & 10 deletions Components/DataPath.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,15 +103,11 @@ private function filterPath(string $path): string
*/
private function filterMimeType(string $mimetype): string
{
if ('' == $mimetype) {
return self::DEFAULT_MIMETYPE;
}

if (1 === preg_match(self::REGEXP_MIMETYPE, $mimetype)) {
return $mimetype;
}

throw new SyntaxError(sprintf('Invalid mimeType, `%s`.', $mimetype));
return match (true) {
'' == $mimetype => self::DEFAULT_MIMETYPE,
1 === preg_match(self::REGEXP_MIMETYPE, $mimetype) => $mimetype,
default => throw new SyntaxError(sprintf('Invalid mimeType, `%s`.', $mimetype)),
};
}

/**
Expand Down Expand Up @@ -302,7 +298,11 @@ private function formatComponent(

$path = $mimetype.$parameters.','.$data;

return preg_replace_callback(self::REGEXP_DATAPATH_ENCODING, $this->encodeMatches(...), $path) ?? $path;
return preg_replace_callback(
self::REGEXP_DATAPATH_ENCODING,
static fn (array $matches): string => rawurlencode($matches[0]),
$path
) ?? $path;
}

public function toAscii(): DataPathInterface
Expand Down
5 changes: 2 additions & 3 deletions Components/Fragment.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,12 @@

use League\Uri\Contracts\FragmentInterface;
use League\Uri\Contracts\UriInterface;
use League\Uri\Encoder;
use Psr\Http\Message\UriInterface as Psr7UriInterface;
use Stringable;

final class Fragment extends Component implements FragmentInterface
{
private const REGEXP_FRAGMENT_ENCODING = '/[^A-Za-z0-9_\-.~!$&\'()*+,;=%:\/@?]+|%(?![A-Fa-f0-9]{2})/';

private readonly ?string $fragment;

/**
Expand Down Expand Up @@ -53,7 +52,7 @@ public static function fromUri(Stringable|string $uri): self

public function value(): ?string
{
return $this->encodeComponent($this->fragment, self::REGEXP_FRAGMENT_ENCODING);
return Encoder::encodeQueryOrFragment($this->fragment);
}

public function getUriComponent(): string
Expand Down
7 changes: 4 additions & 3 deletions Components/HierarchicalPath.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use League\Uri\Contracts\PathInterface;
use League\Uri\Contracts\SegmentedPathInterface;
use League\Uri\Contracts\UriInterface;
use League\Uri\Encoder;
use League\Uri\Exceptions\OffsetOutOfBounds;
use League\Uri\Exceptions\SyntaxError;
use Psr\Http\Message\UriInterface as Psr7UriInterface;
Expand Down Expand Up @@ -59,7 +60,7 @@ private function __construct(Stringable|string $path)
}

$this->path = $path;
$segments = $this->decodeComponent($this->path->value()) ?? '';
$segments = $this->path->decoded();
if ($this->path->isAbsolute()) {
$segments = substr($segments, 1);
}
Expand Down Expand Up @@ -153,7 +154,7 @@ public function decoded(): string

public function getDirname(): string
{
$path = (string) $this->decodeComponent($this->path->toString());
$path = $this->path->decoded();

return str_replace(
['\\', "\0"],
Expand Down Expand Up @@ -297,7 +298,7 @@ public function withSegment(int $key, Stringable|string $segment): SegmentedPath
$segment = new self($segment);
}

$segment = $this->decodeComponent((string) $segment);
$segment = Encoder::decodeAll($segment);
if ($segment === $this->segments[$key]) {
return $this;
}
Expand Down
2 changes: 2 additions & 0 deletions Components/Host.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@

final class Host extends Component implements IpHostInterface
{
protected const REGEXP_NON_ASCII_PATTERN = '/[^\x20-\x7f]/';

/**
* @see https://tools.ietf.org/html/rfc3986#section-3.2.2
*
Expand Down
4 changes: 2 additions & 2 deletions Components/Path.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

use League\Uri\Contracts\PathInterface;
use League\Uri\Contracts\UriInterface;
use League\Uri\Encoder;
use Psr\Http\Message\UriInterface as Psr7UriInterface;
use Stringable;
use function array_pop;
Expand All @@ -27,7 +28,6 @@
final class Path extends Component implements PathInterface
{
private const DOT_SEGMENTS = ['.' => 1, '..' => 1];
private const REGEXP_PATH_ENCODING = '/[^A-Za-z0-9_\-.!$&\'()*+,;=%:\/@]+|%(?![A-Fa-f0-9]{2})/';
private const SEPARATOR = '/';

private readonly string $path;
Expand Down Expand Up @@ -76,7 +76,7 @@ public static function fromUri(Stringable|string $uri): self

public function value(): ?string
{
return $this->encodeComponent($this->path, self::REGEXP_PATH_ENCODING);
return Encoder::encodePath($this->path);
}

public function decoded(): string
Expand Down
22 changes: 14 additions & 8 deletions Components/QueryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ protected function setUp(): void
public function testSeparator(): void
{
$query = Query::new('foo=bar&kingkong=toto');
$newQuery = $query->withSeparator('|');
$newQuery = $query->withSeparator(';');
self::assertSame('&', $query->getSeparator());
self::assertSame('|', $newQuery->getSeparator());
self::assertSame('foo=bar|kingkong=toto', $newQuery->value());
self::assertSame(';', $newQuery->getSeparator());
self::assertSame('foo=bar;kingkong=toto', $newQuery->value());

$this->expectException(SyntaxError::class);
$newQuery->withSeparator('');
Expand Down Expand Up @@ -682,11 +682,17 @@ public static function getURIProvider(): iterable
];
}

public function testCreateFromRFCSpecification(): void
public function testItFailsToCreateFromRFCSpecificationWithInvalidSeparator(): void
{
self::assertEquals(
Query::fromRFC3986('foo=b%20ar|foo=baz', '|'),
Query::fromRFC1738('foo=b+ar|foo=baz', '|')
);
$this->expectException(SyntaxError::class);

Query::fromRFC3986('foo=b%20ar;foo=baz', ''); /* @phpstan-ignore-line */
}

public function testItFailsToCreateFromRFCSpecificationWithEmptySeparator(): void
{
$this->expectException(SyntaxError::class);

Query::fromRFC1738('foo=b%20ar;foo=baz', ''); /* @phpstan-ignore-line */
}
}
45 changes: 11 additions & 34 deletions Components/UserInfo.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,15 @@
use League\Uri\Contracts\UriComponentInterface;
use League\Uri\Contracts\UriInterface;
use League\Uri\Contracts\UserInfoInterface;
use League\Uri\Encoder;
use League\Uri\Exceptions\SyntaxError;
use Psr\Http\Message\UriInterface as Psr7UriInterface;
use SensitiveParameter;
use Stringable;
use function explode;
use function preg_replace_callback;
use function rawurldecode;

final class UserInfo extends Component implements UserInfoInterface
{
private const REGEXP_USER_ENCODING = '/[^A-Za-z0-9_\-.~!$&\'()*+,;=%]+|%(?![A-Fa-f0-9]{2})/x';
private const REGEXP_PASS_ENCODING = '/[^A-Za-z0-9_\-.~!$&\'()*+,;=%:]+|%(?![A-Fa-f0-9]{2})/x';
private const REGEXP_ENCODED_CHAR = ',%[A-Fa-f0-9]{2},';

private readonly ?string $username;
private readonly ?string $password;

Expand Down Expand Up @@ -69,11 +64,10 @@ public static function fromUri(Stringable|string $uri): self
*/
public static function fromAuthority(Stringable|string|null $authority): self
{
if (!$authority instanceof AuthorityInterface) {
$authority = Authority::new($authority);
}

return self::new($authority->getUserInfo());
return match (true) {
$authority instanceof AuthorityInterface => self::new($authority->getUserInfo()),
default => self::new(Authority::new($authority)->getUserInfo()),
};
}

/**
Expand Down Expand Up @@ -111,33 +105,16 @@ public static function new(Stringable|string|null $value = null): self

[$user, $pass] = explode(':', $value, 2) + [1 => null];

return new self(self::decode($user), self::decode($pass));
}

/**
* Decodes an encoded string.
*/
private static function decode(?string $str): ?string
{
return null === $str ? null : preg_replace_callback(
self::REGEXP_ENCODED_CHAR,
static fn (array $matches): string => rawurldecode($matches[0]),
$str
);
return new self(Encoder::decodeAll($user), Encoder::decodeAll($pass));
}

public function value(): ?string
{
if (null === $this->username) {
return null;
}

$userInfo = $this->encodeComponent($this->username, self::REGEXP_USER_ENCODING);
if (null === $this->password) {
return $userInfo;
}

return $userInfo.':'.$this->encodeComponent($this->password, self::REGEXP_PASS_ENCODING);
return match (true) {
null === $this->username => null,
null === $this->password => Encoder::encodeUser($this->username),
default => Encoder::encodeUser($this->username).':'.Encoder::encodePassword($this->password),
};
}

public function getUriComponent(): string
Expand Down
Loading

0 comments on commit 2b57d2a

Please sign in to comment.