From 86cba639fc32f81e6d838ac50c41c639aba6f371 Mon Sep 17 00:00:00 2001 From: Hypolite Petovan Date: Thu, 28 Sep 2023 21:17:40 -0400 Subject: [PATCH] Add implementation of the Content-Type header value from the MIME type RFC - Add tests for the new classes --- src/Network/Entity/MimeType.php | 69 +++++++++++++ src/Network/Factory/MimeType.php | 76 +++++++++++++++ tests/src/Network/MimeTypeTest.php | 152 +++++++++++++++++++++++++++++ 3 files changed, 297 insertions(+) create mode 100644 src/Network/Entity/MimeType.php create mode 100644 src/Network/Factory/MimeType.php create mode 100644 tests/src/Network/MimeTypeTest.php diff --git a/src/Network/Entity/MimeType.php b/src/Network/Entity/MimeType.php new file mode 100644 index 0000000000..4400a9df60 --- /dev/null +++ b/src/Network/Entity/MimeType.php @@ -0,0 +1,69 @@ +. + * + */ + +namespace Friendica\Network\Entity; + +use Friendica\BaseEntity; + +/** + * Implementation of the Content-Type header value from the MIME type RFC + * + * @see https://www.rfc-editor.org/rfc/rfc2045#section-5 + * + * @property-read string $type + * @property-read string $subtype + * @property-read array $parameters + */ +class MimeType extends BaseEntity +{ + /** @var string */ + protected $type; + /** @var string */ + protected $subtype; + /** @var array */ + protected $parameters; + + public function __construct(string $type, string $subtype, array $parameters = []) + { + $this->type = $type; + $this->subtype = $subtype; + $this->parameters = $parameters; + } + + public function __toString(): string + { + $parameters = array_map(function (string $attribute, string $value) { + if ( + strpos($value, '"') !== false || + strpos($value, '\\') !== false || + strpos($value, "\r") !== false + ) { + $value = '"' . str_replace(['\\', '"', "\r"], ['\\\\', '\\"', "\\\r"], $value) . '"'; + } + + return '; ' . $attribute . '=' . $value; + }, array_keys($this->parameters), array_values($this->parameters)); + + return $this->type . '/' . + $this->subtype . + implode('', $parameters); + } +} diff --git a/src/Network/Factory/MimeType.php b/src/Network/Factory/MimeType.php new file mode 100644 index 0000000000..eb096582c6 --- /dev/null +++ b/src/Network/Factory/MimeType.php @@ -0,0 +1,76 @@ +. + * + */ + +namespace Friendica\Network\Factory; + +use Friendica\BaseFactory; +use Friendica\Core\System; +use Friendica\Network\Entity; + +/** + * Implementation of the Content-Type header value from the MIME type RFC + * + * @see https://www.rfc-editor.org/rfc/rfc2045#section-5 + */ +class MimeType extends BaseFactory +{ + public function createFromContentType(?string $contentType): Entity\MimeType + { + if ($contentType) { + $parameterStrings = explode(';', $contentType); + $mimetype = array_shift($parameterStrings); + + $types = explode('/', $mimetype); + if (count($types) >= 2) { + $filetype = strtolower($types[0]); + $subtype = strtolower($types[1]); + } else { + $this->logger->notice('Unknown MimeType', ['type' => $contentType, 'callstack' => System::callstack(10)]); + } + + $parameters = []; + foreach ($parameterStrings as $parameterString) { + $parameterString = trim($parameterString); + $parameterParts = explode('=', $parameterString, 2); + if (count($parameterParts) < 2) { + continue; + } + + $attribute = trim($parameterParts[0]); + $valueString = trim($parameterParts[1]); + + if ($valueString[0] == '"' && $valueString[strlen($valueString) - 1] == '"') { + $valueString = substr(str_replace(['\\"', '\\\\', "\\\r"], ['"', '\\', "\r"], $valueString), 1, -1); + } + + $value = preg_replace('#\s*\([^()]*?\)#', '', $valueString); + + $parameters[$attribute] = $value; + } + } + + return new Entity\MimeType( + $filetype ?? 'unkn', + $subtype ?? 'unkn', + $parameters ?? [], + ); + } +} diff --git a/tests/src/Network/MimeTypeTest.php b/tests/src/Network/MimeTypeTest.php new file mode 100644 index 0000000000..64a52d8d6b --- /dev/null +++ b/tests/src/Network/MimeTypeTest.php @@ -0,0 +1,152 @@ +. + * + */ + +namespace Friendica\Test\src\Network; + +use Friendica\Network\Entity; +use Friendica\Network\Factory; +use PHPUnit\Framework\TestCase; +use Psr\Log\NullLogger; + +class MimeTypeTest extends TestCase +{ + public function dataCreateFromContentType(): array + { + return [ + 'image/jpg' => [ + 'expected' => new Entity\MimeType('image', 'jpg'), + 'contentType' => 'image/jpg', + ], + 'image/jpg;charset=utf8' => [ + 'expected' => new Entity\MimeType('image', 'jpg', ['charset' => 'utf8']), + 'contentType' => 'image/jpg; charset=utf8', + ], + 'image/jpg; charset=utf8' => [ + 'expected' => new Entity\MimeType('image', 'jpg', ['charset' => 'utf8']), + 'contentType' => 'image/jpg; charset=utf8', + ], + 'image/jpg; charset = utf8' => [ + 'expected' => new Entity\MimeType('image', 'jpg', ['charset' => 'utf8']), + 'contentType' => 'image/jpg; charset=utf8', + ], + 'image/jpg; charset="utf8"' => [ + 'expected' => new Entity\MimeType('image', 'jpg', ['charset' => 'utf8']), + 'contentType' => 'image/jpg; charset="utf8"', + ], + 'image/jpg; charset="\"utf8\""' => [ + 'expected' => new Entity\MimeType('image', 'jpg', ['charset' => '"utf8"']), + 'contentType' => 'image/jpg; charset="\"utf8\""', + ], + 'image/jpg; charset="\"utf8\" (comment)"' => [ + 'expected' => new Entity\MimeType('image', 'jpg', ['charset' => '"utf8"']), + 'contentType' => 'image/jpg; charset="\"utf8\" (comment)"', + ], + 'image/jpg; charset=utf8 (comment)' => [ + 'expected' => new Entity\MimeType('image', 'jpg', ['charset' => 'utf8']), + 'contentType' => 'image/jpg; charset="utf8 (comment)"', + ], + 'image/jpg; charset=utf8; attribute=value' => [ + 'expected' => new Entity\MimeType('image', 'jpg', ['charset' => 'utf8', 'attribute' => 'value']), + 'contentType' => 'image/jpg; charset=utf8; attribute=value', + ], + 'empty' => [ + 'expected' => new Entity\MimeType('unkn', 'unkn'), + 'contentType' => '', + ], + 'unknown' => [ + 'expected' => new Entity\MimeType('unkn', 'unkn'), + 'contentType' => 'unknown', + ], + ]; + } + + /** + * @dataProvider dataCreateFromContentType + * @param Entity\MimeType $expected + * @param string $contentType + * @return void + */ + public function testCreateFromContentType(Entity\MimeType $expected, string $contentType) + { + $factory = new Factory\MimeType(new NullLogger()); + + $this->assertEquals($expected, $factory->createFromContentType($contentType)); + } + + public function dataToString(): array + { + return [ + 'image/jpg' => [ + 'expected' => 'image/jpg', + 'mimeType' => new Entity\MimeType('image', 'jpg'), + ], + 'image/jpg;charset=utf8' => [ + 'expected' => 'image/jpg; charset=utf8', + 'mimeType' => new Entity\MimeType('image', 'jpg', ['charset' => 'utf8']), + ], + 'image/jpg; charset="\"utf8\""' => [ + 'expected' => 'image/jpg; charset="\"utf8\""', + 'mimeType' => new Entity\MimeType('image', 'jpg', ['charset' => '"utf8"']), + ], + 'image/jpg; charset=utf8; attribute=value' => [ + 'expected' => 'image/jpg; charset=utf8; attribute=value', + 'mimeType' => new Entity\MimeType('image', 'jpg', ['charset' => 'utf8', 'attribute' => 'value']), + ], + 'empty' => [ + 'expected' => 'unkn/unkn', + 'mimeType' => new Entity\MimeType('unkn', 'unkn'), + ], + ]; + } + + /** + * @dataProvider dataToString + * @param string $expected + * @param Entity\MimeType $mimeType + * @return void + */ + public function testToString(string $expected, Entity\MimeType $mimeType) + { + $this->assertEquals($expected, $mimeType->__toString()); + } + + public function dataRoundtrip(): array + { + return [ + ['image/jpg'], + ['image/jpg; charset=utf8'], + ['image/jpg; charset="\"utf8\""'], + ['image/jpg; charset=utf8; attribute=value'], + ]; + } + + /** + * @dataProvider dataRoundtrip + * @param string $expected + * @return void + */ + public function testRoundtrip(string $expected) + { + $factory = new Factory\MimeType(new NullLogger()); + + $this->assertEquals($expected, $factory->createFromContentType($expected)->__toString()); + } +}