From 11e66ddc87db9ac86abbe5136e68ac604796e2c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Sch=C3=B6lzel?= <142507449+fschoelzel@users.noreply.github.com> Date: Wed, 26 Feb 2025 15:16:01 +0100 Subject: [PATCH] [BUGFIX] MIME Type Filter (#1419) Co-authored-by: Sebastian Meyer --- Classes/Common/Helper.php | 45 ++-- Classes/Controller/PageViewController.php | 5 +- Classes/Controller/ToolboxController.php | 10 +- Tests/Unit/Common/HelperTest.php | 271 ++++++++++++++++++++++ 4 files changed, 312 insertions(+), 19 deletions(-) diff --git a/Classes/Common/Helper.php b/Classes/Common/Helper.php index b4541af8f9..d27ed16701 100644 --- a/Classes/Common/Helper.php +++ b/Classes/Common/Helper.php @@ -946,26 +946,33 @@ private static function getLocalConfigurationByPath(string $path) } /** - * Filters a file based on its mimetype categories. + * Filters a file based on its mimetype. * * This method checks if the provided file array contains a specified mimetype key and - * verifies if the mimetype belongs to any of the specified categories or matches any of the additional custom mimetypes. + * verifies if the mimetype belongs to any of the allowed mimetypes or matches any of the additional custom mimetypes. * * @param mixed $file The file array to filter - * @param array $categories The MIME type categories to filter by (e.g., ['audio'], ['video'] or ['image', 'application']) - * @param array $dlfMimeTypes The custom DLF mimetype keys IIIF, IIP or ZOOMIFY to check against (default is an empty array) + * @param array $allowedCategories The allowed MIME type categories to filter by (e.g., ['audio'], ['video'] or ['image', 'application']) + * @param null|bool|array $dlfMimeTypes Optional array of custom dlf mimetype keys to filter by. Default is null. + * - null: use no custom dlf mimetypes + * - true: use all custom dlf mimetypes + * - array: use only specific types - Accepted values: 'IIIF', 'IIP', 'ZOOMIFY', 'JPG' * @param string $mimeTypeKey The key used to access the mimetype in the file array (default is 'mimetype') * - * @return bool True if the file mimetype belongs to any of the specified categories or matches any custom mimetypes, false otherwise + * @return bool True if the file mimetype belongs to any of the allowed mimetypes or matches any custom dlf mimetypes, false otherwise */ - public static function filterFilesByMimeType($file, array $categories, array $dlfMimeTypes = [], string $mimeTypeKey = 'mimetype'): bool + public static function filterFilesByMimeType($file, array $allowedCategories, null|bool|array $dlfMimeTypes = null, string $mimeTypeKey = 'mimetype'): bool { + if (empty($allowedCategories) && empty($dlfMimeTypes)) { + return false; + } + // Retrieves MIME types from the TYPO3 Core MimeTypeCollection $mimeTypeCollection = GeneralUtility::makeInstance(MimeTypeCollection::class); - $mimeTypes = array_filter( + $allowedMimeTypes = array_filter( $mimeTypeCollection->getMimeTypes(), - function ($mimeType) use ($categories) { - foreach ($categories as $category) { + function ($mimeType) use ($allowedCategories) { + foreach ($allowedCategories as $category) { if (strpos($mimeType, $category . '/') === 0) { return true; } @@ -978,16 +985,26 @@ function ($mimeType) use ($categories) { $dlfMimeTypeArray = [ 'IIIF' => 'application/vnd.kitodo.iiif', 'IIP' => 'application/vnd.netfpx', - 'ZOOMIFY' => 'application/vnd.kitodo.zoomify' + 'ZOOMIFY' => 'application/vnd.kitodo.zoomify', + 'JPG' => 'image/jpg' // Wrong declared JPG MIME type in falsy METS Files for JPEG files ]; - // Filter custom MIME types based on provided keys - $filteredDlfMimeTypes = array_intersect_key($dlfMimeTypeArray, array_flip($dlfMimeTypes)); + // Apply filtering to the custom dlf MIME type array + $filteredDlfMimeTypes = match (true) { + $dlfMimeTypes === null => [], + $dlfMimeTypes === true => $dlfMimeTypeArray, + is_array($dlfMimeTypes) => array_intersect_key($dlfMimeTypeArray, array_flip($dlfMimeTypes)), + default => [] + }; + // Actual filtering to check if the file's MIME type is allowed if (is_array($file) && isset($file[$mimeTypeKey])) { - return in_array($file[$mimeTypeKey], $mimeTypes) || in_array($file[$mimeTypeKey], $filteredDlfMimeTypes); + return in_array($file[$mimeTypeKey], $allowedMimeTypes) || + in_array($file[$mimeTypeKey], $filteredDlfMimeTypes); + } else { + self::log('MIME type validation failed: File array is invalid or MIME type key is not set. File array: ' . json_encode($file) . ', mimeTypeKey: ' . $mimeTypeKey, 2); + return false; } - return false; } /** diff --git a/Classes/Controller/PageViewController.php b/Classes/Controller/PageViewController.php index f478daf779..9dc34c1c6e 100644 --- a/Classes/Controller/PageViewController.php +++ b/Classes/Controller/PageViewController.php @@ -636,13 +636,14 @@ protected function getImage(int $page, MetsDocument $specificDoc = null): array foreach ($useGroups as $useGroup) { // Get file info for the specific page and file group $file = $this->fetchFileInfo($page, $useGroup, $specificDoc); - if ($file && Helper::filterFilesByMimeType($file, ['image', 'application'], ['IIIF', 'IIP', 'ZOOMIFY'], 'mimeType')) { + + if ($file && Helper::filterFilesByMimeType($file, ['image'], true, 'mimeType')) { $image['url'] = $file['location']; $image['mimetype'] = $file['mimeType']; // Only deliver static images via the internal PageViewProxy. // (For IIP and IIIF, the viewer needs to build and access a separate metadata URL, see `getMetadataURL` in `OLSources.js`.) - if ($this->settings['useInternalProxy'] && !Helper::filterFilesByMimeType($file, ['application'], ['IIIF', 'IIP', 'ZOOMIFY'], 'mimeType')) { + if ($this->settings['useInternalProxy'] && !Helper::filterFilesByMimeType($image, ['application'], ['IIIF', 'IIP', 'ZOOMIFY'])) { $this->configureProxyUrl($image['url']); } break; diff --git a/Classes/Controller/ToolboxController.php b/Classes/Controller/ToolboxController.php index f18bbeeaf4..488c098044 100644 --- a/Classes/Controller/ToolboxController.php +++ b/Classes/Controller/ToolboxController.php @@ -145,7 +145,11 @@ private function getImage(int $page): array $image = $this->getFile($page, $this->useGroupsConfiguration->getImage()); if (isset($image['mimetype'])) { $fileExtension = Helper::getFileExtensionsForMimeType($image['mimetype']); - $image['mimetypeLabel'] = !empty($fileExtension) ? ' (' . strtoupper($fileExtension[0]) . ')' : ''; + if ($image['mimetype'] == 'image/jpg') { + $image['mimetypeLabel'] = ' (JPG)'; // "image/jpg" is not a valid MIME type, so we need to handle it separately. + } else { + $image['mimetypeLabel'] = !empty($fileExtension) ? ' (' . strtoupper($fileExtension[0]) . ')' : ''; + } } return $image; } @@ -283,13 +287,13 @@ private function renderImageDownloadTool(): void $imageArray = []; // Get left or single page download. $image = $this->getImage($page); - if (Helper::filterFilesByMimeType($image, ['image'])) { + if (Helper::filterFilesByMimeType($image, ['image'], true)) { $imageArray[0] = $image; } if ($this->requestData['double'] == 1) { $image = $this->getImage($page + 1); - if (Helper::filterFilesByMimeType($image, ['image'])) { + if (Helper::filterFilesByMimeType($image, ['image'], true)) { $imageArray[1] = $image; } } diff --git a/Tests/Unit/Common/HelperTest.php b/Tests/Unit/Common/HelperTest.php index acd3f4f680..15f655b177 100644 --- a/Tests/Unit/Common/HelperTest.php +++ b/Tests/Unit/Common/HelperTest.php @@ -13,10 +13,49 @@ namespace Kitodo\Dlf\Tests\Unit\Common; use Kitodo\Dlf\Common\Helper; +use TYPO3\CMS\Core\Log\LogManager; +use TYPO3\CMS\Core\Log\Logger; +use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\TestingFramework\Core\Unit\UnitTestCase; +use PHPUnit\Framework\MockObject\MockObject; class HelperTest extends UnitTestCase { + /** + * @var bool + */ + protected bool $resetSingletonInstances = true; + + /** + * @var LogManager|MockObject + */ + protected $logManagerMock; + + /** + * @var Logger|MockObject + */ + protected $loggerMock; + + protected function setUp(): void + { + parent::setUp(); + + // create Logger Mock + $this->loggerMock = $this->createMock(Logger::class); + + // create LogManager Mock + $this->logManagerMock = $this->createMock(LogManager::class); + $this->logManagerMock->method('getLogger')->willReturn($this->loggerMock); + + GeneralUtility::setSingletonInstance(LogManager::class, $this->logManagerMock); + } + + protected function tearDown(): void + { + GeneralUtility::purgeInstances(); + parent::tearDown(); + } + public function assertInvalidXml($xml) { $result = Helper::getXmlFileAsString($xml); @@ -55,4 +94,236 @@ public function validXmlIsAccepted(): void self::assertIsObject($node); self::assertEquals('root', $node->getName()); } + + /** + * @test + * @group filterFilesByMimeType + */ + public function filterFilesByMimeTypeHandlesInvalidInput(): void + { + // Empty categories and types + self::assertFalse(Helper::filterFilesByMimeType( + ['mimetype' => 'image/jpeg'], + [], + null + )); + + // Invalid file input + self::assertFalse(Helper::filterFilesByMimeType( + null, + ['image'], + null + )); + + // Missing mime type key + self::assertFalse(Helper::filterFilesByMimeType( + ['wrong_key' => 'image/jpeg'], + ['image'], + null + )); + } + + /** + * @test + * @group filterFilesByMimeType + */ + public function filterFilesByMimeTypeAcceptsStandardMimeTypes(): void + { + $file = ['mimetype' => 'image/jpeg']; + + self::assertTrue(Helper::filterFilesByMimeType( + $file, + ['image'] + )); + + self::assertFalse(Helper::filterFilesByMimeType( + $file, + ['video'] + )); + + // Test multiple categories + self::assertTrue(Helper::filterFilesByMimeType( + $file, + ['video', 'image'] + )); + } + + /** + * @test + * @group filterFilesByMimeType + */ + public function filterFilesByMimeTypeHandlesCustomDlfTypes(): void + { + $testCases = [ + ['mimetype' => 'application/vnd.kitodo.iiif'], + ['mimetype' => 'application/vnd.netfpx'], + ['mimetype' => 'application/vnd.kitodo.zoomify'], + ['mimetype' => 'image/jpg'] + ]; + + foreach ($testCases as $file) { + self::assertTrue(Helper::filterFilesByMimeType( + $file, + [], + true + )); + } + + foreach ($testCases as $file) { + self::assertTrue(Helper::filterFilesByMimeType( + $file, + [], + ['IIIF', 'IIP', 'ZOOMIFY', 'JPG'] + )); + } + + // Test specific DLF type filtering + $file = ['mimetype' => 'application/vnd.kitodo.iiif']; + self::assertTrue(Helper::filterFilesByMimeType( + $file, + [], + true + )); + self::assertFalse(Helper::filterFilesByMimeType( + $file, + [], + ['IIP'] + )); + } + + /** + * @test + * @group filterFilesByMimeType + */ + public function filterFilesByMimeTypeHandlesCustomMimeTypeKey(): void + { + $file = ['customKey' => 'image/jpeg']; + + self::assertTrue(Helper::filterFilesByMimeType( + $file, + ['image'], + null, + 'customKey' + )); + + self::assertFalse(Helper::filterFilesByMimeType( + $file, + ['image'] + )); + } + + /** + * @test + * @group filterFilesByMimeType + */ + public function filterFilesByMimeTypeHandlesMixedScenarios(): void + { + // Standard mime type with DLF types enabled + self::assertTrue(Helper::filterFilesByMimeType( + ['mimetype' => 'image/jpeg'], + ['image'], + ['IIIF', 'IIP'] + )); + + // DLF mime type with matching category + self::assertTrue(Helper::filterFilesByMimeType( + ['mimetype' => 'application/vnd.kitodo.iiif'], + ['application'], + ['IIIF'] + )); + + // DLF mime type with non-matching category but allowed DLF type + self::assertTrue(Helper::filterFilesByMimeType( + ['mimetype' => 'application/vnd.kitodo.iiif'], + ['image'], + ['IIIF'] + )); + } + + /** + * @test + * @group filterFilesByMimeType + */ + public function filterFilesByMimeTypeHandlesWrongJpg(): void + { + $wrongJpg = ['mimetype' => 'image/jpg']; + + // file with wrong JPG mime type and no custom dlf mime type key + self::assertFalse(Helper::filterFilesByMimeType( + $wrongJpg, + ['image'], + [] + )); + + // file with wrong JPG mime type and custom dlf mime type key + self::assertTrue(Helper::filterFilesByMimeType( + $wrongJpg, + [], + ['JPG'] + )); + + // file with wrong JPG mime type in allowed key and custom dlf mime type key + self::assertTrue(Helper::filterFilesByMimeType( + $wrongJpg, + ['image'], + ['JPG'] + )); + } + + /** + * @test + * @group filterFilesByMimeType + */ + public function filterFilesByMimeTypeHandlesDifferentDlfModeTypes(): void + { + // Test-Setup + $imageFile = ['mimetype' => 'image/jpeg']; + $iiifFile = ['mimetype' => 'application/vnd.kitodo.iiif']; + $iipFile = ['mimetype' => 'application/vnd.netfpx']; + + // Test: No DLF MIME Types (only Standard-Types) + self::assertTrue(Helper::filterFilesByMimeType( + $imageFile, + ['image'], + null + ), 'Standard image type should be accepted when DLF types are null' + ); + + self::assertFalse(Helper::filterFilesByMimeType( + $iiifFile, + ['image'], + null + ), 'DLF type should be rejected when DLF types are null' + ); + + // Test: All DLF MIME Types + self::assertTrue(Helper::filterFilesByMimeType( + $iiifFile, + ['image'], + true + ), 'IIIF should be accepted when all DLF types are enabled' + ); + + self::assertTrue(Helper::filterFilesByMimeType( + $iipFile, + ['image'], + true + ), 'IIP should be accepted when all DLF types are enabled' + ); + + // Test: Spezific DLF MIME Types + self::assertTrue(Helper::filterFilesByMimeType( + $iiifFile, + ['image'], + ['IIIF'] + ), 'IIIF should be accepted when specifically allowed' + ); + + self::assertFalse(Helper::filterFilesByMimeType( + $iipFile, + ['image'], + ['IIIF'] + ), 'IIP should be rejected when not in allowed DLF types' + ); + } }