From 9264fba1a0f46d2375f2a2f2ab6f1a8833851387 Mon Sep 17 00:00:00 2001 From: ignace nyamagana butera Date: Fri, 29 Sep 2023 17:05:21 +0200 Subject: [PATCH] Adding Uri::fromData named constructor (#127) Adding Uri::fromData named constructors --- docs/uri/7.0/rfc3986.md | 7 ++++ uri/CHANGELOG.md | 1 + uri/FactoryTest.php | 79 +++++++++++++++++++++++++++++++++++++++++ uri/Uri.php | 52 +++++++++++++++++++++++---- 4 files changed, 133 insertions(+), 6 deletions(-) diff --git a/docs/uri/7.0/rfc3986.md b/docs/uri/7.0/rfc3986.md index d442a97c..883192b5 100644 --- a/docs/uri/7.0/rfc3986.md +++ b/docs/uri/7.0/rfc3986.md @@ -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 @@ -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. diff --git a/uri/CHANGELOG.md b/uri/CHANGELOG.md index e6bd05fc..895d525e 100644 --- a/uri/CHANGELOG.md +++ b/uri/CHANGELOG.md @@ -6,6 +6,7 @@ All Notable changes to `League\Uri` will be documented in this file ### Added +- `Uri::fromData` - `BaseUri::unixPath` - `BaseUri::windowsPath` diff --git a/uri/FactoryTest.php b/uri/FactoryTest.php index de07628a..28532c90 100644 --- a/uri/FactoryTest.php +++ b/uri/FactoryTest.php @@ -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 */ diff --git a/uri/Uri.php b/uri/Uri.php index 09ccd1d4..df03e2f0 100644 --- a/uri/Uri.php +++ b/uri/Uri.php @@ -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. */ @@ -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), }; @@ -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,'; @@ -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; } @@ -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.'); @@ -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.'); } @@ -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);