Skip to content

Commit

Permalink
Adding Uri::fromData named constructor (#127)
Browse files Browse the repository at this point in the history
Adding Uri::fromData named constructors
  • Loading branch information
nyamsprod authored Sep 29, 2023
1 parent e457b42 commit 9264fba
Show file tree
Hide file tree
Showing 4 changed files with 133 additions and 6 deletions.
7 changes: 7 additions & 0 deletions docs/uri/7.0/rfc3986.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ echo Uri::fromTemplate($template, $variables)->toString();
$uri = Uri::fromFileContents('path/to/my/png/image.png');
echo $uri; //returns 'data:image/png;charset=binary;base64,...'
//where '...' represent the base64 representation of the file

$uri = Uri::fromData('Héllo World!', 'text/plain', 'charset=utf8');
echo $uri; // returns data:text/plain;charset=utf8,H%C3%A9llo%20World%21
~~~

The `fromBaseUri` method resolves URI using the same logic behind URL construction
Expand All @@ -86,6 +89,10 @@ named constructor generates a [Data URI](https://datatracker.ietf.org/doc/html/r
following its RFC specification. with the provided file location, the method will
base64 encode the content of the file and return the generated URI.

The `fromData`
named constructor generates a [Data URI](https://datatracker.ietf.org/doc/html/rfc2397)
following its RFC specification. with the provided data and an optional mimetype and parameters.

Last but not least, you can easily translate Windows and Unix paths to URI using the two
following methods.

Expand Down
1 change: 1 addition & 0 deletions uri/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ All Notable changes to `League\Uri` will be documented in this file

### Added

- `Uri::fromData`
- `BaseUri::unixPath`
- `BaseUri::windowsPath`

Expand Down
79 changes: 79 additions & 0 deletions uri/FactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,85 @@ public static function validFilePath(): array
];
}

/** @dataProvider provideValidData */
public function testFromData(string $data, string $mimetype, string $parameters, string $expected): void
{
self::assertSame($expected, Uri::fromData($data, $mimetype, $parameters)->toString());
}

public static function provideValidData(): iterable
{
yield 'empty data' => [
'data' => '',
'mimetype' => '',
'parameters' => '',
'expected' => 'data:text/plain;charset=us-ascii,',
];

yield 'simple string' => [
'data' => 'Hello World!',
'mimetype' => '',
'parameters' => '',
'expected' => 'data:text/plain;charset=us-ascii,Hello%20World%21',
];

yield 'changing the mimetype' => [
'data' => 'Hello World!',
'mimetype' => 'text/no-plain',
'parameters' => '',
'expected' => 'data:text/no-plain;charset=us-ascii,Hello%20World%21',
];

yield 'empty mimetype but added parameters' => [
'data' => 'Hello World!',
'mimetype' => '',
'parameters' => 'foo=bar',
'expected' => 'data:text/plain;foo=bar,Hello%20World%21',
];

yield 'empty data with optional argument field' => [
'data' => '',
'mimetype' => 'application/json',
'parameters' => 'foo=bar',
'expected' => 'data:application/json;foo=bar,',
];

yield 'changing the parameters' => [
'data' => 'Hello World!',
'mimetype' => 'text/no-plain',
'parameters' => 'foo=bar',
'expected' => 'data:text/no-plain;foo=bar,Hello%20World%21',
];

yield 'the parameters can start with the ; character' => [
'data' => 'Hello World!',
'mimetype' => 'text/no-plain',
'parameters' => ';foo=bar',
'expected' => 'data:text/no-plain;foo=bar,Hello%20World%21',
];
}

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

Uri::fromData('Hello World!', 'text\plain');
}

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

Uri::fromData('Hello World!', 'text/plain', 'base64=foobar');
}

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

Uri::fromData('Hello World!', 'text/plain', 'foobar');
}

/**
* @dataProvider unixpathProvider
*/
Expand Down
52 changes: 46 additions & 6 deletions uri/Uri.php
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,46 @@ public static function fromFileContents(Stringable|string $path, $context = null
]);
}

/**
* Create a new instance from a data string.
*
* @throws SyntaxError If the parameter syntax is invalid
*/
public static function fromData(string $data, string $mimetype = '', string $parameters = ''): self
{
static $regexpMimetype = ',^\w+/[-.\w]+(?:\+[-.\w]+)?$,';

$mimetype = match (true) {
'' === $mimetype => 'text/plain',
1 === preg_match($regexpMimetype, $mimetype) => $mimetype,
default => throw new SyntaxError('Invalid mimeType, `'.$mimetype.'`.'),
};

if ('' != $parameters) {
if (str_starts_with($parameters, ';')) {
$parameters = substr($parameters, 1);
}

$validateParameter = function (string $parameter): bool {
$properties = explode('=', $parameter);

return 2 != count($properties) || 'base64' === strtolower($properties[0]);
};

$params = array_filter(explode(';', $parameters));
if ([] !== array_filter($params, $validateParameter(...))) {
throw new SyntaxError(sprintf('Invalid mediatype parameters, `%s`.', $parameters));
}

$parameters = ';'.$parameters;
}

return self::fromComponents([
'scheme' => 'data',
'path' => self::formatDataPath($mimetype.$parameters.','.rawurlencode($data)),
]);
}

/**
* Create a new instance from a Unix path string.
*/
Expand Down Expand Up @@ -690,7 +730,7 @@ private function setAuthority(): ?string
private function formatPath(string $path): string
{
return match (true) {
'data' === $this->scheme => Encoder::encodePath($this->formatDataPath($path)),
'data' === $this->scheme => Encoder::encodePath(self::formatDataPath($path)),
'file' === $this->scheme => $this->formatFilePath(Encoder::encodePath($path)),
default => Encoder::encodePath($path),
};
Expand All @@ -703,7 +743,7 @@ private function formatPath(string $path): string
*
* @throws SyntaxError If the path is not compliant with RFC2397
*/
private function formatDataPath(string $path): string
private static function formatDataPath(string $path): string
{
if ('' == $path) {
return 'text/plain;charset=us-ascii,';
Expand All @@ -726,7 +766,7 @@ private function formatDataPath(string $path): string
$parameters = 'charset=us-ascii';
}

$this->assertValidPath($mimetype, $parameters, $data);
self::assertValidPath($mimetype, $parameters, $data);

return $mimetype.';'.$parameters.','.$data;
}
Expand All @@ -738,7 +778,7 @@ private function formatDataPath(string $path): string
*
* @throws SyntaxError If the mediatype or the data are not compliant with the RFC2397
*/
private function assertValidPath(string $mimetype, string $parameters, string $data): void
private static function assertValidPath(string $mimetype, string $parameters, string $data): void
{
if (1 !== preg_match(self::REGEXP_MIMETYPE, $mimetype)) {
throw new SyntaxError('The path mimetype `'.$mimetype.'` is invalid.');
Expand All @@ -749,7 +789,7 @@ private function assertValidPath(string $mimetype, string $parameters, string $d
$parameters = substr($parameters, 0, - strlen($matches[0]));
}

$res = array_filter(array_filter(explode(';', $parameters), $this->validateParameter(...)));
$res = array_filter(array_filter(explode(';', $parameters), self::validateParameter(...)));
if ([] !== $res) {
throw new SyntaxError('The path paremeters `'.$parameters.'` is invalid.');
}
Expand All @@ -767,7 +807,7 @@ private function assertValidPath(string $mimetype, string $parameters, string $d
/**
* Validate mediatype parameter.
*/
private function validateParameter(string $parameter): bool
private static function validateParameter(string $parameter): bool
{
$properties = explode('=', $parameter);

Expand Down

0 comments on commit 9264fba

Please sign in to comment.