From 1dd76dcbae57e2a4401f945267bab03ab5d56e02 Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Wed, 1 Mar 2023 15:10:00 +0000 Subject: [PATCH 01/54] [TASK] Migrate to new query build api --- Classes/Command/AbstractCloudinaryCommand.php | 2 +- Classes/Command/CloudinaryFixJpegCommand.php | 2 +- Classes/Domain/Repository/ExplicitDataCacheRepository.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Classes/Command/AbstractCloudinaryCommand.php b/Classes/Command/AbstractCloudinaryCommand.php index 6a309fc..31dd7c3 100644 --- a/Classes/Command/AbstractCloudinaryCommand.php +++ b/Classes/Command/AbstractCloudinaryCommand.php @@ -110,7 +110,7 @@ protected function getFiles(ResourceStorage $storage, InputInterface $input): ar } } - return $query->execute()->fetchAll(); + return $query->execute()->fetchAllAssociative(); } /** diff --git a/Classes/Command/CloudinaryFixJpegCommand.php b/Classes/Command/CloudinaryFixJpegCommand.php index 2b5f1fb..1b60cd7 100644 --- a/Classes/Command/CloudinaryFixJpegCommand.php +++ b/Classes/Command/CloudinaryFixJpegCommand.php @@ -120,6 +120,6 @@ protected function getJpegFiles(): array ->from($this->tableName) ->where($query->expr()->eq('storage', $this->targetStorage->getUid()), $query->expr()->eq('extension', $query->expr()->literal('jpeg'))); - return $query->execute()->fetchAll(); + return $query->execute()->fetchAllAssociative(); } } diff --git a/Classes/Domain/Repository/ExplicitDataCacheRepository.php b/Classes/Domain/Repository/ExplicitDataCacheRepository.php index 1a9d68b..93ac5f8 100644 --- a/Classes/Domain/Repository/ExplicitDataCacheRepository.php +++ b/Classes/Domain/Repository/ExplicitDataCacheRepository.php @@ -48,7 +48,7 @@ public function findByStorageAndPublicIdAndOptions(int $storageId, string $publi ) ) ); - $item = $query->execute()->fetch(); + $item = $query->execute()->fetchAssociative(); if (!$item) { return null; From f0e7bacee5d04d3c4b7b668d70cb171dbb4363b6 Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Wed, 1 Mar 2023 15:10:47 +0000 Subject: [PATCH 02/54] [TASK] Clean up some php comments --- Classes/Command/CloudinaryScanCommand.php | 2 +- Classes/Services/CloudinaryFolderService.php | 110 ++----------- .../Services/CloudinaryResourceService.php | 132 ++------------- Classes/Services/CloudinaryScanService.php | 150 +++++++----------- 4 files changed, 78 insertions(+), 316 deletions(-) diff --git a/Classes/Command/CloudinaryScanCommand.php b/Classes/Command/CloudinaryScanCommand.php index 41abbc1..fd7fece 100644 --- a/Classes/Command/CloudinaryScanCommand.php +++ b/Classes/Command/CloudinaryScanCommand.php @@ -71,7 +71,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($input->getOption('empty') === null || $input->getOption('empty')) { $this->log('Emptying all mirrored resources for storage "%s"', [$this->storage->getUid()]); $this->log(); - $this->getCloudinaryScanService()->empty(); + $this->getCloudinaryScanService()->deleteAll(); } $this->log('Hint! Look at the log to get more insight:'); diff --git a/Classes/Services/CloudinaryFolderService.php b/Classes/Services/CloudinaryFolderService.php index 1102c01..1bd1954 100644 --- a/Classes/Services/CloudinaryFolderService.php +++ b/Classes/Services/CloudinaryFolderService.php @@ -14,37 +14,18 @@ use TYPO3\CMS\Core\Database\Query\QueryBuilder; use TYPO3\CMS\Core\Utility\GeneralUtility; -/** - * Class CloudinaryFolderService - */ class CloudinaryFolderService { - /** - * @var string - */ - protected $tableName = 'tx_cloudinary_folder'; + protected string $tableName = 'tx_cloudinary_folder'; - /** - * @var int - */ - protected $storageUid; + protected int $storageUid; - /** - * CloudinaryResourceService constructor. - * - * @param int $storageUid - */ public function __construct(int $storageUid) { $this->storageUid = $storageUid; } - /** - * @param string $folder - * - * @return array - */ public function getFolder(string $folder): array { $query = $this->getQueryBuilder(); @@ -59,15 +40,10 @@ public function getFolder(string $folder): array ) ); - $folder = $query->execute()->fetch(); - return $folder - ? $folder - : []; + $folder = $query->execute()->fetchAssociative(); + return $folder ?: []; } - /** - * @return int - */ public function markAsMissing(): int { $values = ['missing' => 1,]; @@ -75,12 +51,6 @@ public function markAsMissing(): int return $this->getConnection()->update($this->tableName, $values, $identifier); } - /** - * @param string $parentFolder - * @param array $orderings - * - * @return array - */ public function getSubFolders(string $parentFolder, array $orderings, bool $recursive = false): array { $query = $this->getQueryBuilder(); @@ -104,15 +74,9 @@ public function getSubFolders(string $parentFolder, array $orderings, bool $recu ); $query->andWhere($expresion); - return $query->execute()->fetchAll(); + return $query->execute()->fetchAllAssociative(); } - /** - * @param string $parentFolder - * @param bool $recursive - * - * @return int - */ public function countSubFolders(string $parentFolder, bool $recursive = false): int { $query = $this->getQueryBuilder(); @@ -135,14 +99,9 @@ public function countSubFolders(string $parentFolder, bool $recursive = false): ); $query->andWhere($expresion); - return (int)$query->execute()->fetchColumn(0); + return (int)$query->execute()->fetchOne(0); } - /** - * @param string $folder - * - * @return int - */ public function delete(string $folder): int { $identifier['folder'] = $folder; @@ -150,22 +109,12 @@ public function delete(string $folder): int return $this->getConnection()->delete($this->tableName, $identifier); } - /** - * @param array $identifier - * - * @return int - */ - public function deleteAll(array $identifier = []): int + public function deleteAll(array $identifiers = []): int { - $identifier['storage'] = $this->storageUid; - return $this->getConnection()->delete($this->tableName, $identifier); + $identifiers['storage'] = $this->storageUid; + return $this->getConnection()->delete($this->tableName, $identifiers); } - /** - * @param string $folder - * - * @return array - */ public function save(string $folder): array { $folderHash = sha1($folder); @@ -175,11 +124,6 @@ public function save(string $folder): array : ['folder_created' => $this->add($folder)]; } - /** - * @param string $folder - * - * @return int - */ protected function add(string $folder): int { return $this->getConnection()->insert( @@ -188,12 +132,6 @@ protected function add(string $folder): int ); } - /** - * @param string $folder - * @param string $folderHash - * - * @return int - */ protected function update(string $folder, string $folderHash): int { return $this->getConnection()->update( @@ -206,11 +144,6 @@ protected function update(string $folder, string $folderHash): int ); } - /** - * @param string $folderPath - * - * @return string - */ protected function computeParentFolder(string $folderPath): string { return dirname($folderPath) === '.' @@ -218,11 +151,6 @@ protected function computeParentFolder(string $folderPath): string : dirname($folderPath); } - /** - * @param string $folderHash - * - * @return int - */ protected function exists(string $folderHash): int { $query = $this->getQueryBuilder(); @@ -237,14 +165,9 @@ protected function exists(string $folderHash): int ) ); - return (int)$query->execute()->fetchColumn(0); + return (int)$query->execute()->fetchOne(0); } - /** - * @param string $folder - * - * @return array - */ protected function getValues(string $folder): array { return [ @@ -258,12 +181,6 @@ protected function getValues(string $folder): array ]; } - /** - * @param string $key - * @param array $cloudinaryResource - * - * @return string - */ protected function getValue(string $key, array $cloudinaryResource): string { return isset($cloudinaryResource[$key]) @@ -271,9 +188,6 @@ protected function getValue(string $key, array $cloudinaryResource): string : ''; } - /** - * @return object|QueryBuilder - */ protected function getQueryBuilder(): QueryBuilder { /** @var ConnectionPool $connectionPool */ @@ -281,13 +195,11 @@ protected function getQueryBuilder(): QueryBuilder return $connectionPool->getQueryBuilderForTable($this->tableName); } - /** - * @return object|Connection - */ protected function getConnection(): Connection { /** @var ConnectionPool $connectionPool */ $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class); return $connectionPool->getConnectionForTable($this->tableName); } + } diff --git a/Classes/Services/CloudinaryResourceService.php b/Classes/Services/CloudinaryResourceService.php index 8cfafa9..0f88905 100644 --- a/Classes/Services/CloudinaryResourceService.php +++ b/Classes/Services/CloudinaryResourceService.php @@ -20,29 +20,15 @@ */ class CloudinaryResourceService { - /** - * @var string - */ - protected $tableName = 'tx_cloudinary_resource'; - - /** - * @var ResourceStorage - */ - protected $storage; - - /** - * CloudinaryResourceService constructor. - * - * @param ResourceStorage $storage - */ + protected string $tableName = 'tx_cloudinary_resource'; + + protected ResourceStorage $storage; + public function __construct(ResourceStorage $storage) { $this->storage = $storage; } - /** - * @return int - */ public function markAsMissing(): int { $values = ['missing' => 1]; @@ -50,11 +36,6 @@ public function markAsMissing(): int return $this->getConnection()->update($this->tableName, $values, $identifier); } - /** - * @param string $publicId - * - * @return array - */ public function getResource(string $publicId): array { $query = $this->getQueryBuilder(); @@ -68,17 +49,9 @@ public function getResource(string $publicId): array ->setMaxResults(1); $resource = $query->execute()->fetchAssociative(); - return $resource ? $resource : []; + return $resource ?: []; } - /** - * @param string $folder - * @param array $orderings - * @param array $pagination - * @param bool $recursive - * - * @return array - */ public function getResources( string $folder, array $orderings = [], @@ -105,15 +78,9 @@ public function getResources( $query->setMaxResults((int) $pagination['maxResult']); $query->setFirstResult((int) $pagination['firstResult']); } - return $query->execute()->fetchAll(); + return $query->execute()->fetchAllAssociative(); } - /** - * @param string $folder - * @param bool $recursive - * - * @return int - */ public function count(string $folder, bool $recursive = false): int { $query = $this->getQueryBuilder(); @@ -128,14 +95,9 @@ public function count(string $folder, bool $recursive = false): int : $query->expr()->eq('folder', $query->expr()->literal($folder)); $query->andWhere($expresion); - return (int) $query->execute()->fetchColumn(0); + return (int) $query->execute()->fetchOne(0); } - /** - * @param string $publicId - * - * @return int - */ public function delete(string $publicId): int { $identifier['public_id'] = $publicId; @@ -143,22 +105,12 @@ public function delete(string $publicId): int return $this->getConnection()->delete($this->tableName, $identifier); } - /** - * @param array $identifier - * - * @return int - */ - public function deleteAll(array $identifier = []): int + public function deleteAll(array $identifiers = []): int { - $identifier['storage'] = $this->storage->getUid(); - return $this->getConnection()->delete($this->tableName, $identifier); + $identifiers['storage'] = $this->storage->getUid(); + return $this->getConnection()->delete($this->tableName, $identifiers); } - /** - * @param array $cloudinaryResource - * - * @return array - */ public function save(array $cloudinaryResource): array { $publicIdHash = $this->getPublicIdHash($cloudinaryResource); @@ -174,22 +126,11 @@ public function save(array $cloudinaryResource): array : ['created' => $this->add($cloudinaryResource), 'publicIdHash' => $publicIdHash]; } - /** - * @param array $cloudinaryResource - * - * @return int - */ protected function add(array $cloudinaryResource): int { return $this->getConnection()->insert($this->tableName, $this->getValues($cloudinaryResource)); } - /** - * @param array $cloudinaryResource - * @param string $publicIdHash - * - * @return int - */ protected function update(array $cloudinaryResource, string $publicIdHash): int { return $this->getConnection()->update($this->tableName, $this->getValues($cloudinaryResource), [ @@ -198,11 +139,6 @@ protected function update(array $cloudinaryResource, string $publicIdHash): int ]); } - /** - * @param string $publicIdHash - * - * @return int - */ protected function exists(string $publicIdHash): int { $query = $this->getQueryBuilder(); @@ -217,11 +153,6 @@ protected function exists(string $publicIdHash): int return (int) $query->execute()->fetchOne(0); } - /** - * @param array $cloudinaryResource - * - * @return array - */ protected function getValues(array $cloudinaryResource): array { $publicIdHash = $this->getPublicIdHash($cloudinaryResource); @@ -255,85 +186,47 @@ protected function getValues(array $cloudinaryResource): array ]; } - /** - * @param string $key - * @param array $cloudinaryResource - * - * @return string - */ protected function getValue(string $key, array $cloudinaryResource): string { return isset($cloudinaryResource[$key]) ? (string) $cloudinaryResource[$key] : ''; } - /** - * @param array $cloudinaryResource - * - * @return string - */ protected function getFileName(array $cloudinaryResource): string { return basename($this->getValue('public_id', $cloudinaryResource)); } - /** - * @param array $cloudinaryResource - * - * @return string - */ protected function getFolder(array $cloudinaryResource): string { $folder = dirname($this->getValue('public_id', $cloudinaryResource)); return $folder === '.' ? '' : $folder; } - /** - * @param array $cloudinaryResource - * - * @return string - */ protected function getCreatedAt(array $cloudinaryResource): string { $createdAt = $this->getValue('created_at', $cloudinaryResource); return date('Y-m-d h:i:s', strtotime($createdAt)); } - /** - * @param array $cloudinaryResource - * - * @return string - */ protected function getPublicIdHash(array $cloudinaryResource): string { $publicId = $this->getValue('public_id', $cloudinaryResource); return sha1($publicId); } - /** - * @param array $cloudinaryResource - * - * @return string - */ protected function getUpdatedAt(array $cloudinaryResource): string { $updatedAt = $this->getValue('updated_at', $cloudinaryResource) - ? $this->getValue('updated_at', $cloudinaryResource) - : $this->getValue('created_at', $cloudinaryResource); + ?: $this->getValue('created_at', $cloudinaryResource); return date('Y-m-d h:i:s', strtotime($updatedAt)); } - /** - * @return object|CloudinaryFolderService - */ protected function getCloudinaryFolderService(): CloudinaryFolderService { return GeneralUtility::makeInstance(CloudinaryFolderService::class, $this->storage->getUid()); } - /** - * @return object|QueryBuilder - */ protected function getQueryBuilder(): QueryBuilder { /** @var ConnectionPool $connectionPool */ @@ -341,9 +234,6 @@ protected function getQueryBuilder(): QueryBuilder return $connectionPool->getQueryBuilderForTable($this->tableName); } - /** - * @return object|Connection - */ protected function getConnection(): Connection { /** @var ConnectionPool $connectionPool */ diff --git a/Classes/Services/CloudinaryScanService.php b/Classes/Services/CloudinaryScanService.php index 2d5eff7..a2b44a2 100644 --- a/Classes/Services/CloudinaryScanService.php +++ b/Classes/Services/CloudinaryScanService.php @@ -8,6 +8,9 @@ * For the full copyright and license information, please read the * LICENSE.md file that was distributed with this source code. */ + +use Cloudinary\Api; +use TYPO3\CMS\Core\Exception; use TYPO3\CMS\Core\Log\Logger; use Cloudinary\Search; use Symfony\Component\Console\Style\SymfonyStyle; @@ -20,9 +23,6 @@ use Visol\Cloudinary\Driver\CloudinaryDriver; use Visol\Cloudinary\Utility\CloudinaryApiUtility; -/** - * Class CloudinaryScanService - */ class CloudinaryScanService { @@ -33,25 +33,13 @@ class CloudinaryScanService private const FAILED = 'failed'; private const FOLDER_DELETED = 'folder_deleted'; - /** - * @var ResourceStorage - */ - protected $storage; - - /** - * @var CloudinaryPathService - */ - protected $cloudinaryPathService; - - /** - * @var string - */ - protected $processedFolder = '_processed_'; - - /** - * @var array - */ - protected $statistics = [ + protected ResourceStorage $storage; + + protected CloudinaryPathService $cloudinaryPathService; + + protected string $processedFolder = '_processed_'; + + protected array $statistics = [ self::CREATED => 0, self::UPDATED => 0, self::DELETED => 0, @@ -61,19 +49,8 @@ class CloudinaryScanService self::FOLDER_DELETED => 0, ]; - /** - * @var SymfonyStyle|null - */ - protected $io; - - /** - * CloudinaryScanService constructor. - * - * @param ResourceStorage $storage - * @param SymfonyStyle|null $io - * - * @throws \Exception - */ + protected ?SymfonyStyle $io = null; + public function __construct(ResourceStorage $storage, SymfonyStyle $io = null) { if ($storage->getDriverType() !== CloudinaryDriver::DRIVER_TYPE) { @@ -83,18 +60,23 @@ public function __construct(ResourceStorage $storage, SymfonyStyle $io = null) $this->io = $io; } - /** - * @return void - */ - public function empty(): void + public function deleteAll(): void { $this->getCloudinaryResourceService()->deleteAll(); $this->getCloudinaryFolderService()->deleteAll(); } - /** - * @return array - */ + public function scanOne(string $publicId): array|null + { + try { + $resource = (array)$this->getApi()->resource($publicId); + $result = $this->getCloudinaryResourceService()->save($resource); + } catch (Exception $exception) { + $result = null; + } + return $result; + } + public function scan(): array { $this->preScan(); @@ -113,16 +95,14 @@ public function scan(): array $expressions[] = sprintf('NOT folder=%s/*', $this->processedFolder); } - if ($this->io) { - $this->io->writeln('Mirroring...' . chr(10)); - } + $this->console('Mirroring...', true); do { $nextCursor = isset($response) ? $response['next_cursor'] : ''; - $this->log( + $this->info( '[API][SEARCH] Cloudinary\Search() - fetch resources from folder "%s" %s', [ $cloudinaryFolder, @@ -147,9 +127,7 @@ public function scan(): array foreach ($response['resources'] as $resource) { $fileIdentifier = $this->getCloudinaryPathService()->computeFileIdentifier($resource); try { - if ($this->io) { - $this->io->writeln($fileIdentifier); - } + $this->console($fileIdentifier); // Save mirrored file $result = $this->getCloudinaryResourceService()->save($resource); @@ -157,10 +135,7 @@ public function scan(): array // Find if the file exists in sys_file already if (!$this->fileExistsInStorage($fileIdentifier)) { - if ($this->io) { - $this->io->writeln('Indexing new file: ' . $fileIdentifier); - $this->io->writeln(''); - } + $this->console('Indexing new file: ' . $fileIdentifier, true); // This will trigger a file indexation $this->storage->getFile($fileIdentifier); @@ -173,12 +148,9 @@ public function scan(): array // In any case we can add a file to the counter. // Later we can verify the total corresponds to the "created" + "updated" + "deleted" files $this->statistics[self::TOTAL]++; - } - catch (\Exception $e) { + } catch (\Exception $e) { $this->statistics[self::FAILED]++; - if ($this->io) { - $this->io->warning(sprintf('Error could not process "%s"', $fileIdentifier)); - } + $this->console(sprintf('Error could not process "%s"', $fileIdentifier)); // ignore } } @@ -190,30 +162,19 @@ public function scan(): array return $this->statistics; } - /** - * @return void - */ protected function preScan(): void { $this->getCloudinaryResourceService()->markAsMissing(); $this->getCloudinaryFolderService()->markAsMissing(); } - /** - * @return void - */ protected function postScan(): void { - $identifier = ['missing' => 1]; - $this->statistics[self::DELETED] = $this->getCloudinaryResourceService()->deleteAll($identifier); - $this->statistics[self::FOLDER_DELETED] = $this->getCloudinaryFolderService()->deleteAll($identifier); + $identifiers = ['missing' => 1]; + $this->statistics[self::DELETED] = $this->getCloudinaryResourceService()->deleteAll($identifiers); + $this->statistics[self::FOLDER_DELETED] = $this->getCloudinaryFolderService()->deleteAll($identifiers); } - /** - * @param string $fileIdentifier - * - * @return bool - */ protected function fileExistsInStorage(string $fileIdentifier): bool { $query = $this->getQueryBuilder(); @@ -230,12 +191,9 @@ protected function fileExistsInStorage(string $fileIdentifier): bool ) ); - return (bool)$query->execute()->fetchColumn(0); + return (bool)$query->execute()->fetchOne(0); } - /** - * @return object|QueryBuilder - */ protected function getQueryBuilder(): QueryBuilder { /** @var ConnectionPool $connectionPool */ @@ -243,33 +201,21 @@ protected function getQueryBuilder(): QueryBuilder return $connectionPool->getQueryBuilderForTable('sys_file'); } - /** - * @return void - */ - protected function initializeApi() + protected function initializeApi(): void { CloudinaryApiUtility::initializeByConfiguration($this->storage->getConfiguration()); } - /** - * @return object|CloudinaryResourceService - */ protected function getCloudinaryResourceService(): CloudinaryResourceService { return GeneralUtility::makeInstance(CloudinaryResourceService::class, $this->storage); } - /** - * @return object|CloudinaryFolderService - */ protected function getCloudinaryFolderService(): CloudinaryFolderService { return GeneralUtility::makeInstance(CloudinaryFolderService::class, $this->storage->getUid()); } - /** - * @return CloudinaryPathService - */ protected function getCloudinaryPathService(): CloudinaryPathService { if (!$this->cloudinaryPathService) { @@ -282,12 +228,16 @@ protected function getCloudinaryPathService(): CloudinaryPathService return $this->cloudinaryPathService; } - /** - * @param string $message - * @param array $arguments - * @param array $data - */ - protected function log(string $message, array $arguments = [], array $data = []) + protected function getApi() + { + // Initialize and configure the API for each call + $this->initializeApi(); + + // create a new instance upon each API call to avoid driver confusion + return new Api(); + } + + protected function info(string $message, array $arguments = [], array $data = []): void { /** @var Logger $logger */ $logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__); @@ -297,4 +247,14 @@ protected function log(string $message, array $arguments = [], array $data = []) $data ); } + + protected function console(string $message, $additionalBlankLine = false): void + { + if ($this->io) { + $this->io->writeln($message); + if ($additionalBlankLine) { + $this->io->writeln(''); + } + } + } } From 6ad6815399948055a4f75b48af6dac3cb0f45bff Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Wed, 1 Mar 2023 15:11:11 +0000 Subject: [PATCH 03/54] [FEATURE] Introduce cloudinary web hook handler --- .../CloudinaryWebHookController.php | 258 ++++++++++++++++++ Classes/Driver/CloudinaryFastDriver.php | 27 +- .../CloudinaryNotFoundException.php | 16 ++ .../Exceptions/PublicIdMissingException.php | 16 ++ .../UnknownRequestTypeException.php | 16 ++ Classes/Utility/CloudinaryFileUtility.php | 37 +++ Configuration/TypoScript/setup.typoscript | 10 +- README.md | 20 +- ext_localconf.php | 42 ++- 9 files changed, 393 insertions(+), 49 deletions(-) create mode 100644 Classes/Controller/CloudinaryWebHookController.php create mode 100644 Classes/Exceptions/CloudinaryNotFoundException.php create mode 100644 Classes/Exceptions/PublicIdMissingException.php create mode 100644 Classes/Exceptions/UnknownRequestTypeException.php create mode 100644 Classes/Utility/CloudinaryFileUtility.php diff --git a/Classes/Controller/CloudinaryWebHookController.php b/Classes/Controller/CloudinaryWebHookController.php new file mode 100644 index 0000000..1701882 --- /dev/null +++ b/Classes/Controller/CloudinaryWebHookController.php @@ -0,0 +1,258 @@ +checkEnvironment(); + + /** @var ResourceFactory $resourceFactory */ + $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class); + + $storage = $resourceFactory->getStorageObject((int)$this->settings['storage']); + $this->cloudinaryResourceService = GeneralUtility::makeInstance( + CloudinaryResourceService::class, + $storage, + ); + + $this->scanService = GeneralUtility::makeInstance( + CloudinaryScanService::class, + $storage + ); + + $this->cloudinaryPathService = GeneralUtility::makeInstance( + CloudinaryPathService::class, + $storage->getConfiguration() + ); + + $this->storage = $storage; + + $this->processedFileRepository = GeneralUtility::makeInstance(ProcessedFileRepository::class); + } + + public function processAction(): ResponseInterface + { + $parsedBody = (string)file_get_contents('php://input'); + $payload = json_decode($parsedBody, true); + self::getLogger()->debug($parsedBody); + + if ($this->shouldStopProcessing($payload)) { + return $this->sendResponse(['result' => 'ok', 'message' => 'Nothing to do...']); + } + + try { + [$requestType, $publicIds] = $this->getRequestInfo($payload); + + self::getLogger()->debug(sprintf('Start cache flushing for file "%s". ', $requestType)); + + foreach ($publicIds as $publicId) { + $cloudinaryResource = $this->getCloudinaryResource($publicId); + + // #. retrieve the source file + $file = $this->getFile($cloudinaryResource); + + + // #. flush the process files + $this->clearProcessedFiles($file); + + // #. flush cache pages + $this->clearCachePages($file); + } + } catch (\Exception $e) { + return $this->sendResponse([ + 'result' => 'ko', + 'message' => $e->getMessage(), + ]); + } + + return $this->sendResponse(['result' => 'ok', 'message' => 'Cache flushed']); + } + + protected function getFile(array $cloudinaryResource): File + { + $fileIdentifier = $this->cloudinaryPathService->computeFileIdentifier($cloudinaryResource); + return $this->storage->getFileByIdentifier($fileIdentifier); + } + + protected function getRequestInfo(array $payload): array + { + if ($this->isRequestUploadOverwrite($payload)) { + $requestType = self::NOTIFICATION_TYPE_UPLOAD; + $publicIds = [$payload['public_id']]; + } elseif ($this->isRequestRename($payload)) { + $requestType = self::NOTIFICATION_TYPE_RENAME; + $publicIds = [$payload['from_public_id']]; + //$nextPublicId = $payload['to_public_id']; + } elseif ($this->isRequestDelete($payload)) { + $requestType = self::NOTIFICATION_TYPE_DELETE; + $publicIds = []; + foreach ($payload['resources'] as $resource) { + $publicIds[] = $resource['public_id']; + } + } else { + throw new UnknownRequestTypeException('Unknown request type', 1677860080); + } + + if (empty($publicIds)) { + throw new PublicIdMissingException('Missing public id', 1677860090); + } + + return [$requestType, $publicIds,]; + } + + protected function getCloudinaryResource(string $publicId): array + { + $cloudinaryResource = $this->cloudinaryResourceService->getResource($publicId); + + // The resource does not exist, time to fetch + if (!$cloudinaryResource) { + $result = $this->scanService->scanOne($publicId); + if (!$result) { + $message = sprintf('I could not find a corresponding resource for public id %s', $publicId); + throw new CloudinaryNotFoundException($message, 1677859470); + } + $cloudinaryResource = $this->cloudinaryResourceService->getResource($publicId); + } + + return $cloudinaryResource; + } + + protected function clearProcessedFiles(File $file): void + { + + $processedFiles = $this->processedFileRepository->findAllByOriginalFile($file); + $temporaryFileNameAndPath = CloudinaryFileUtility::getTemporaryFile($file->getStorage()->getUid(), $file->getIdentifier()); + + foreach ($processedFiles as $processedFile) { + $processedFile->getStorage()->setEvaluatePermissions(false); + $processedFile->delete(); + } + } + + protected function clearCachePages(File $file): void + { + $tags = []; + foreach ($this->findPagesWithFileReferences($file) as $page) { + $tags[] = 'pageId_' . $page['pid']; + } + + GeneralUtility::makeInstance(CacheManager::class) + ->flushCachesInGroupByTags('pages', $tags); + } + + protected function findPagesWithFileReferences(File $file): array + { + $queryBuilder = $this->getQueryBuilder('sys_file_reference'); + return $queryBuilder + ->select('pid') + ->from('sys_file_reference') + ->groupBy('pid') // no support for distinct + ->andWhere( + 'pid > 0', + 'uid_local = ' . $file->getUid() + ) + ->execute() + ->fetchAllAssociative(); + } + + protected function shouldStopProcessing(mixed $payload): bool + { + return !($this->isRequestUploadOverwrite($payload) || $this->isRequestRename($payload)); + } + + protected function isRequestUploadOverwrite(mixed $payload): bool + { + return is_array($payload) && + array_key_exists('notification_type', $payload) && + array_key_exists('overwritten', $payload) && + $payload['notification_type'] === self::NOTIFICATION_TYPE_UPLOAD + && $payload['overwritten']; + } + + protected function isRequestRename(mixed $payload): bool + { + return is_array($payload) && + array_key_exists('notification_type', $payload) && + $payload['notification_type'] === self::NOTIFICATION_TYPE_RENAME; + } + + protected function isRequestDelete(mixed $payload): bool + { + return is_array($payload) && + array_key_exists('notification_type', $payload) && + $payload['notification_type'] === self::NOTIFICATION_TYPE_DELETE; + } + + protected function sendResponse(array $data): ResponseInterface + { + return $this->jsonResponse( + json_encode($data) + ); + } + + protected function checkEnvironment(): void + { + $storageUid = $this->settings['storage'] ?? 0; + if ($storageUid <= 0) { + throw new \RuntimeException('Check your configuration while calling the cloudinary web hook. I am missing a storage id', 1677583654); + } + } + + protected function getQueryBuilder($tableName): QueryBuilder + { + /** @var ConnectionPool $connectionPool */ + $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class); + return $connectionPool->getQueryBuilderForTable($tableName); + } + + protected static function getLogger(): Logger + { + /** @var Logger $logger */ + static $logger = null; + if ($logger === null) { + $logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__); + } + return $logger; + } + +} diff --git a/Classes/Driver/CloudinaryFastDriver.php b/Classes/Driver/CloudinaryFastDriver.php index 5e5a49c..45562e5 100644 --- a/Classes/Driver/CloudinaryFastDriver.php +++ b/Classes/Driver/CloudinaryFastDriver.php @@ -32,6 +32,7 @@ use Visol\Cloudinary\Services\CloudinaryPathService; use Visol\Cloudinary\Services\CloudinaryTestConnectionService; use Visol\Cloudinary\Services\ConfigurationService; +use Visol\Cloudinary\Utility\CloudinaryFileUtility; /** * Class CloudinaryFastDriver @@ -1188,36 +1189,12 @@ protected function applyFilterMethodsToDirectoryItem( return true; } - /** - * Returns a temporary path for a given file, including the file extension. - * - * @param string $fileIdentifier - * - * @return string - */ - protected function getTemporaryPathForFile($fileIdentifier): string - { - $temporaryFileNameAndPath = - Environment::getPublicPath() . - DIRECTORY_SEPARATOR . - 'typo3temp/var/transient/' . - $this->storageUid . - $fileIdentifier; - - $temporaryFolder = GeneralUtility::dirname($temporaryFileNameAndPath); - - if (!is_dir($temporaryFolder)) { - GeneralUtility::mkdir_deep($temporaryFolder); - } - return $temporaryFileNameAndPath; - } - /** * We want to remove the local temporary file */ protected function cleanUpTemporaryFile(string $fileIdentifier): void { - $temporaryLocalFile = $this->getTemporaryPathForFile($fileIdentifier); + $temporaryLocalFile = CloudinaryFileUtility::getTemporaryFile($this->storageUid, $fileIdentifier); if (is_file($temporaryLocalFile)) { unlink($temporaryLocalFile); } diff --git a/Classes/Exceptions/CloudinaryNotFoundException.php b/Classes/Exceptions/CloudinaryNotFoundException.php new file mode 100644 index 0000000..29e640f --- /dev/null +++ b/Classes/Exceptions/CloudinaryNotFoundException.php @@ -0,0 +1,16 @@ +run vendorName = Visol extensionName = Cloudinary - pluginName = Cache + pluginName = WebHook + settings { + storage = ### !!! Add a storage uid + } switchableControllerActions { - CloudinaryScan { - 1 = scan + CloudinaryWebHook { + 1 = process } } } diff --git a/README.md b/README.md index 81e98a0..95b180d 100644 --- a/README.md +++ b/README.md @@ -198,16 +198,26 @@ Available targets: Web Hook -------- -Whenever uploading or editing a file through the Cloudinary Manager you can configure an URL -as a web hook to be called to invalidate the cache in TYPO3. -This is highly recommended to keep the data consistent between Cloudinary and TYPO3. + +Whenever uploading or editing a file in the cloudinary library, you can configure in the cloudinary settings a URL to +be called as a web hook. This is recommended to keep the data consistent between Cloudinary and TYPO3. When overridding +or moving a file across folders, cloudinary will inform TYPO3 that something has changed. + +It will basically: + +* invalidate the processed files +* invalidate the page cache where the the file is involved. + ```shell script https://domain.tld/?type=1573555440 ``` -**Beware**: Do not rename, move or delete files in the Cloudinary Media Library. TYPO3 will not know about the change. -We may need to implement a web hook. For now, it is necessary to perform these action in the File module in the Backend. +This, however, will not work out of the box and requires some manual configuration. +Refer to the file ext:cloudinary/Configuration/TypoScript/setup.typoscript where we define a custom type. +This is an example TypoScript file. Make sure that the file is loaded, and that you have defined a storage UID. +Your system may contain multiple Cloudinary storages, and each web hook must refer to its own Cloudinary storage. +Eventually you will end up having as many config as you have cloudinary storage. Source of inspiration --------------------- diff --git a/ext_localconf.php b/ext_localconf.php index 32683a3..e104d92 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -1,24 +1,20 @@ ', - ); +call_user_func(callback: function () { // Override default class to add cloudinary button $GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['nodeRegistry'][1652423292] = [ @@ -29,13 +25,13 @@ ExtensionUtility::configurePlugin( \Cloudinary::class, - 'Cache', + 'WebHook', [ - CloudinaryScanController::class => 'scan', + CloudinaryWebHookController::class => 'process', ], // non-cacheable actions [ - CloudinaryScanController::class => 'scan', + CloudinaryWebHookController::class => 'process', ], ); @@ -52,16 +48,32 @@ $metaDataExtractorRegistry = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Resource\Index\ExtractorRegistry::class); $metaDataExtractorRegistry->registerExtractionService(\Visol\Cloudinary\Services\Extractor\CloudinaryMetaDataExtractor::class); - $GLOBALS['TYPO3_CONF_VARS']['LOG']['Visol'][\Cloudinary::class]['Service']['writerConfiguration'] - = $GLOBALS['TYPO3_CONF_VARS']['LOG']['Visol'][\Cloudinary::class]['Cache']['writerConfiguration'] - = $GLOBALS['TYPO3_CONF_VARS']['LOG']['Visol'][\Cloudinary::class]['Driver']['writerConfiguration'] + // Log configuration for cloudinary web hook + $GLOBALS['TYPO3_CONF_VARS']['LOG']['Visol']['Cloudinary']['Controller']['CloudinaryWebHookController']['writerConfiguration'] = [ + LogLevel::DEBUG => [ + FileWriter::class => [ + 'logFile' => Environment::getVarPath() . '/log/cloudinary-web-hook.log' + ], + ], + + // Configuration for WARNING severity, including all + // levels with higher severity (ERROR, CRITICAL, EMERGENCY) + LogLevel::WARNING => [ + \TYPO3\CMS\Core\Log\Writer\SyslogWriter::class => [], + ], + ]; + + // Log configuration for cloudinary driver + $GLOBALS['TYPO3_CONF_VARS']['LOG']['Visol']['Cloudinary']['Service']['writerConfiguration'] + = $GLOBALS['TYPO3_CONF_VARS']['LOG']['Visol']['Cloudinary']['Cache']['writerConfiguration'] + = $GLOBALS['TYPO3_CONF_VARS']['LOG']['Visol']['Cloudinary']['Driver']['writerConfiguration'] = [ // configuration for WARNING severity, including all // levels with higher severity (ERROR, CRITICAL, EMERGENCY) LogLevel::INFO => [ FileWriter::class => [ // configuration for the writer - 'logFile' => 'typo3temp/var/logs/cloudinary.log', + 'logFile' => Environment::getVarPath() . '/log/cloudinary.log', ], ], ]; From fb176291933884daa138e5c79e58384890637251 Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Tue, 7 Mar 2023 19:12:20 +0000 Subject: [PATCH 04/54] fixup! [FEATURE] Introduce cloudinary web hook handler --- Classes/Controller/CloudinaryWebHookController.php | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/Classes/Controller/CloudinaryWebHookController.php b/Classes/Controller/CloudinaryWebHookController.php index 1701882..dd82840 100644 --- a/Classes/Controller/CloudinaryWebHookController.php +++ b/Classes/Controller/CloudinaryWebHookController.php @@ -91,10 +91,12 @@ public function processAction(): ResponseInterface // #. retrieve the source file $file = $this->getFile($cloudinaryResource); - // #. flush the process files $this->clearProcessedFiles($file); + // #. clean up local temporary file - var/variant folder + $this->cleanUpTemporaryFile($file); + // #. flush cache pages $this->clearCachePages($file); } @@ -159,9 +161,7 @@ protected function getCloudinaryResource(string $publicId): array protected function clearProcessedFiles(File $file): void { - $processedFiles = $this->processedFileRepository->findAllByOriginalFile($file); - $temporaryFileNameAndPath = CloudinaryFileUtility::getTemporaryFile($file->getStorage()->getUid(), $file->getIdentifier()); foreach ($processedFiles as $processedFile) { $processedFile->getStorage()->setEvaluatePermissions(false); @@ -169,6 +169,14 @@ protected function clearProcessedFiles(File $file): void } } + protected function cleanUpTemporaryFile(File $file): void + { + $temporaryFileNameAndPath = CloudinaryFileUtility::getTemporaryFile($file->getStorage()->getUid(), $file->getIdentifier()); + if (is_file($temporaryFileNameAndPath)) { + unlink($temporaryFileNameAndPath); + } + } + protected function clearCachePages(File $file): void { $tags = []; From a1493fcf6a0144bca2822982328293bde02a7b55 Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Tue, 21 Mar 2023 14:07:03 +0100 Subject: [PATCH 05/54] [CLEANUP] Rework annotations --- Classes/Command/AbstractCloudinaryCommand.php | 59 ++--------------- .../CloudinaryAcceptanceTestCommand.php | 32 ++-------- Classes/Command/CloudinaryCopyCommand.php | 48 +++----------- Classes/Command/CloudinaryFixJpegCommand.php | 31 +++------ Classes/Command/CloudinaryMoveCommand.php | 64 ++++--------------- Classes/Command/CloudinaryQueryCommand.php | 45 ++----------- Classes/Command/CloudinaryScanCommand.php | 32 ++-------- 7 files changed, 55 insertions(+), 256 deletions(-) diff --git a/Classes/Command/AbstractCloudinaryCommand.php b/Classes/Command/AbstractCloudinaryCommand.php index 31dd7c3..00948f2 100644 --- a/Classes/Command/AbstractCloudinaryCommand.php +++ b/Classes/Command/AbstractCloudinaryCommand.php @@ -29,30 +29,15 @@ abstract class AbstractCloudinaryCommand extends Command const WARNING = 'warning'; const ERROR = 'error'; - /** - * @var SymfonyStyle - */ - protected $io; + protected SymfonyStyle $io; - /** - * @var bool - */ - protected $isSilent = false; + protected bool $isSilent = false; - /** - * @var string - */ - protected $tableName = 'sys_file'; + protected string $tableName = 'sys_file'; - /** - * @param ResourceStorage $storage - * @param InputInterface $input - * - * @return array - */ protected function getFiles(ResourceStorage $storage, InputInterface $input): array { - $query = $this->getQueryBuilder(); + $query = $this->getQueryBuilder($this->tableName); $query ->select('*') ->from($this->tableName) @@ -113,10 +98,6 @@ protected function getFiles(ResourceStorage $storage, InputInterface $input): ar return $query->execute()->fetchAllAssociative(); } - /** - * @param string $type - * @param array $files - */ protected function writeLog(string $type, array $files) { $logFileName = sprintf( @@ -141,22 +122,15 @@ protected function writeLog(string $type, array $files) ); } - /** - * @param ResourceStorage $storage - * - * @return bool - */ protected function checkDriverType(ResourceStorage $storage): bool { return $storage->getDriverType() === CloudinaryDriver::DRIVER_TYPE; } /** - * @param string $message - * @param array $arguments * @param string $severity can be 'warning', 'error', 'success' */ - protected function log(string $message = '', array $arguments = [], $severity = '') + protected function log(string $message = '', array $arguments = [], string $severity = '') { if (!$this->isSilent) { $formattedMessage = vsprintf($message, $arguments); @@ -168,47 +142,28 @@ protected function log(string $message = '', array $arguments = [], $severity = } } - /** - * @param string $message - * @param array $arguments - */ protected function success(string $message = '', array $arguments = []) { $this->log($message, $arguments, self::SUCCESS); } - /** - * @param string $message - * @param array $arguments - */ protected function warning(string $message = '', array $arguments = []) { $this->log($message, $arguments, self::WARNING); } - /** - * @param string $message - * @param array $arguments - */ protected function error(string $message = '', array $arguments = []) { $this->log($message, $arguments, self::ERROR); } - - /** - * @return object|QueryBuilder - */ - protected function getQueryBuilder(): QueryBuilder + protected function getQueryBuilder(string $tableName): QueryBuilder { /** @var ConnectionPool $connectionPool */ $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class); - return $connectionPool->getQueryBuilderForTable($this->tableName); + return $connectionPool->getQueryBuilderForTable($tableName); } - /** - * @return object|Connection - */ protected function getConnection(): Connection { /** @var ConnectionPool $connectionPool */ diff --git a/Classes/Command/CloudinaryAcceptanceTestCommand.php b/Classes/Command/CloudinaryAcceptanceTestCommand.php index ae8cac6..5a5f334 100644 --- a/Classes/Command/CloudinaryAcceptanceTestCommand.php +++ b/Classes/Command/CloudinaryAcceptanceTestCommand.php @@ -9,7 +9,9 @@ * LICENSE.md file that was distributed with this source code. */ +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Style\SymfonyStyle; +use TYPO3\CMS\Core\Core\Environment; use TYPO3\CMS\Core\Database\ConnectionPool; use Visol\Cloudinary\Driver\CloudinaryDriver; use Symfony\Component\Console\Input\InputArgument; @@ -66,21 +68,11 @@ protected function configure() ); } - /** - * @param InputInterface $input - * @param OutputInterface $output - */ protected function initialize(InputInterface $input, OutputInterface $output) { $this->io = new SymfonyStyle($input, $output); } - /** - * @param InputInterface $input - * @param OutputInterface $output - * - * @return int - */ protected function execute(InputInterface $input, OutputInterface $output): int { // We should dynamically inject the configuration. For now use an existing driver @@ -94,19 +86,19 @@ protected function execute(InputInterface $input, OutputInterface $output): int $message .= 'https://cloudinary.com/console' . LF; $message .= 'Strong advice! Take a free account to run the test suite'; $this->error($message); - return 1; + return Command::INVALID; } - $this->log('Starting tests...'); + $logFile = Environment::getVarPath() . '/log/cloudinary.log'; $this->log('Hint! Look at the log to get more insight:'); - $this->log('tail -f web/typo3temp/var/logs/cloudinary.log'); + $this->log('tail -f ' . $logFile); $this->log(); // Create a testing storage $storageId = $this->setUp($couldName, $apiKey, $apiSecret); if (!$storageId) { $this->error('Something went wrong. I could not create a testing storage'); - return 2; + return Command::FAILURE; } // Test case for video file @@ -118,16 +110,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->tearDown($storageId); - return 0; + return Command::SUCCESS; } - /** - * @param string $cloudName - * @param string $apiKey - * @param string $apiSecret - * - * @return int - */ protected function setUp(string $cloudName, string $apiKey, string $apiSecret): int { $values = [ @@ -178,9 +163,6 @@ protected function setUp(string $cloudName, string $apiKey, string $apiSecret): return (int)$db->lastInsertId(); } - /** - * @param int $storageId - */ protected function tearDown(int $storageId) { /** @var ConnectionPool $connectionPool */ diff --git a/Classes/Command/CloudinaryCopyCommand.php b/Classes/Command/CloudinaryCopyCommand.php index dba3ca2..0e8d010 100644 --- a/Classes/Command/CloudinaryCopyCommand.php +++ b/Classes/Command/CloudinaryCopyCommand.php @@ -9,6 +9,7 @@ * LICENSE.md file that was distributed with this source code. */ +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -25,25 +26,12 @@ */ class CloudinaryCopyCommand extends AbstractCloudinaryCommand { - /** - * @var array - */ - protected $missingFiles = []; + protected array $missingFiles = []; - /** - * @var ResourceStorage - */ - protected $sourceStorage; + protected ResourceStorage $sourceStorage; - /** - * @var ResourceStorage - */ - protected $targetStorage; + protected ResourceStorage $targetStorage; - /** - * @param InputInterface $input - * @param OutputInterface $output - */ protected function initialize(InputInterface $input, OutputInterface $output) { $this->io = new SymfonyStyle($input, $output); @@ -74,22 +62,18 @@ protected function configure() ->setHelp('Usage: ./vendor/bin/typo3 cloudinary:copy 1 2'); } - /** - * @param InputInterface $input - * @param OutputInterface $output - */ protected function execute(InputInterface $input, OutputInterface $output): int { if (!$this->checkDriverType($this->targetStorage)) { $this->log('Look out! target storage is not of type "cloudinary"'); - return 1; + return Command::INVALID; } $files = $this->getFiles($this->sourceStorage, $input); if (count($files) === 0) { $this->log('No files found, no work for me!'); - return 0; + return Command::SUCCESS; } $this->log('Copying %s files from storage "%s" (%s) to "%s" (%s)', [ @@ -106,7 +90,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if (!$response) { $this->log('Script aborted'); - return 0; + return Command::SUCCESS; } } @@ -148,28 +132,17 @@ protected function execute(InputInterface $input, OutputInterface $output): int print_r($this->missingFiles); } - return 0; + return Command::SUCCESS; } - /** - * @param File $fileObject - * @param string $url - * - * @return bool - */ public function download(File $fileObject, string $url): bool { $this->ensureDirectoryExistence($fileObject); $contents = file_get_contents($url); - return $contents ? (bool) file_put_contents($this->getAbsolutePath($fileObject), $contents) : false; + return $contents ? (bool)file_put_contents($this->getAbsolutePath($fileObject), $contents) : false; } - /** - * @param File $fileObject - * - * @return string - */ protected function getAbsolutePath(File $fileObject): string { // Compute the absolute file name of the file to move @@ -178,9 +151,6 @@ protected function getAbsolutePath(File $fileObject): string return GeneralUtility::getFileAbsFileName($fileRelativePath); } - /** - * @param File $fileObject - */ protected function ensureDirectoryExistence(File $fileObject) { // Make sure the directory exists diff --git a/Classes/Command/CloudinaryFixJpegCommand.php b/Classes/Command/CloudinaryFixJpegCommand.php index 1b60cd7..c80898f 100644 --- a/Classes/Command/CloudinaryFixJpegCommand.php +++ b/Classes/Command/CloudinaryFixJpegCommand.php @@ -9,6 +9,7 @@ * LICENSE.md file that was distributed with this source code. */ +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -23,15 +24,10 @@ */ class CloudinaryFixJpegCommand extends AbstractCloudinaryCommand { - /** - * @var ResourceStorage - */ - protected $targetStorage; + protected ResourceStorage $targetStorage; + + protected string $tableName = 'sys_file'; - /** - * @param InputInterface $input - * @param OutputInterface $output - */ protected function initialize(InputInterface $input, OutputInterface $output) { $this->io = new SymfonyStyle($input, $output); @@ -58,24 +54,19 @@ protected function configure() /** * Move file - * - * @param InputInterface $input - * @param OutputInterface $output - * - * @return int */ protected function execute(InputInterface $input, OutputInterface $output): int { if (!$this->checkDriverType($this->targetStorage)) { $this->log('Look out! target storage is not of type "cloudinary"'); - return 1; + return Command::INVALID; } $files = $this->getJpegFiles(); if (count($files) === 0) { $this->log('No files found, no work for me!'); - return 0; + return Command::SUCCESS; } $this->log('I will update %s files by replacing "jpeg" to "jpg" in various fields in storage "%s" (%s)', [ @@ -90,7 +81,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if (!$response) { $this->log('Script aborted'); - return 0; + return Command::SUCCESS; } } @@ -106,15 +97,13 @@ protected function execute(InputInterface $input, OutputInterface $output): int $connection->query($query)->execute(); - return 0; + + return Command::SUCCESS; } - /** - * @return array - */ protected function getJpegFiles(): array { - $query = $this->getQueryBuilder(); + $query = $this->getQueryBuilder($this->tableName); $query ->select('*') ->from($this->tableName) diff --git a/Classes/Command/CloudinaryMoveCommand.php b/Classes/Command/CloudinaryMoveCommand.php index bea50ea..2273295 100644 --- a/Classes/Command/CloudinaryMoveCommand.php +++ b/Classes/Command/CloudinaryMoveCommand.php @@ -10,6 +10,7 @@ */ use Exception; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Style\SymfonyStyle; use TYPO3\CMS\Core\Resource\ResourceStorage; use TYPO3\CMS\Core\Utility\PathUtility; @@ -27,30 +28,15 @@ */ class CloudinaryMoveCommand extends AbstractCloudinaryCommand { - /** - * @var array - */ - protected $faultyUploadedFiles; + protected array $faultyUploadedFiles; - /** - * @var array - */ - protected $skippedFiles; + protected array $skippedFiles; - /** - * @var array - */ - protected $missingFiles = []; + protected array $missingFiles = []; - /** - * @var ResourceStorage - */ - protected $sourceStorage; + protected ResourceStorage $sourceStorage; - /** - * @var ResourceStorage - */ - protected $targetStorage; + protected ResourceStorage $targetStorage; /** * Configure the command by defining the name, options and arguments @@ -71,10 +57,6 @@ protected function configure() ->setHelp('Usage: ./vendor/bin/typo3 cloudinary:move 1 2'); } - /** - * @param InputInterface $input - * @param OutputInterface $output - */ protected function initialize(InputInterface $input, OutputInterface $output) { $this->io = new SymfonyStyle($input, $output); @@ -88,26 +70,18 @@ protected function initialize(InputInterface $input, OutputInterface $output) $this->targetStorage = $resourceFactory->getStorageObject($input->getArgument('target')); } - /** - * Move file - * - * @param InputInterface $input - * @param OutputInterface $output - * - * @return int - */ protected function execute(InputInterface $input, OutputInterface $output): int { if (!$this->checkDriverType($this->targetStorage)) { $this->log('Look out! target storage is not of type "cloudinary"'); - return 1; + return Command::INVALID; } $files = $this->getFiles($this->sourceStorage, $input); if (count($files) === 0) { $this->log('No files found, no work for me!'); - return 0; + return Command::SUCCESS; } $this->log('I will process %s files to be moved from storage "%s" (%s) to "%s" (%s)', [ @@ -124,7 +98,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int if (!$response) { $this->log('Script aborted'); - return 0; + + return Command::SUCCESS; } } @@ -194,14 +169,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->writeLog('skipped', $this->skippedFiles); } - return 0; + return Command::SUCCESS; } - /** - * @param File $fileObject - * - * @return bool - */ protected function isFileSkipped(File $fileObject): bool { $isDisallowedPath = false; @@ -219,35 +189,23 @@ protected function isFileSkipped(File $fileObject): bool $isDisallowedPath; } - /** - * @return array - */ protected function getDisallowedExtensions(): array { // Empty for now return []; } - /** - * @return array - */ protected function getDisallowedFileIdentifiers(): array { // Empty for now return []; } - /** - * @return array - */ protected function getDisallowedPaths(): array { return ['user_upload/_temp_/', '_temp_/', '_processed_/']; } - /** - * @return object|FileMoveService - */ protected function getFileMoveService(): FileMoveService { return GeneralUtility::makeInstance(FileMoveService::class); diff --git a/Classes/Command/CloudinaryQueryCommand.php b/Classes/Command/CloudinaryQueryCommand.php index 964676f..332f9de 100644 --- a/Classes/Command/CloudinaryQueryCommand.php +++ b/Classes/Command/CloudinaryQueryCommand.php @@ -9,6 +9,7 @@ * LICENSE.md file that was distributed with this source code. */ +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -45,15 +46,8 @@ */ class CloudinaryQueryCommand extends AbstractCloudinaryCommand { - /** - * @var ResourceStorage - */ - protected $storage; + protected ResourceStorage $storage; - /** - * @param InputInterface $input - * @param OutputInterface $output - */ protected function initialize(InputInterface $input, OutputInterface $output) { $this->io = new SymfonyStyle($input, $output); @@ -82,15 +76,11 @@ protected function configure() ->setHelp('Usage: ./vendor/bin/typo3 cloudinary:query [0-9]'); } - /** - * @param InputInterface $input - * @param OutputInterface $output - */ protected function execute(InputInterface $input, OutputInterface $output): int { if (!$this->checkDriverType($this->storage)) { $this->log('Look out! Storage is not of type "cloudinary"'); - return 1; + return Command::INVALID; } // Get the chance to define a filter @@ -141,14 +131,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int } } - return 0; + return Command::SUCCESS; } - /** - * @param InputInterface $input - * - * @return array - */ protected function listFoldersAction(InputInterface $input): array { $folders = $this->storage->getFoldersInFolder($this->getFolder($input->getOption('path')), 0, 0, true, $input->getOption('recursive')); @@ -160,11 +145,6 @@ protected function listFoldersAction(InputInterface $input): array return $folders; } - /** - * @param InputInterface $input - * - * @return array - */ protected function listFilesAction(InputInterface $input): array { $files = $this->storage->getFilesInFolder($this->getFolder($input->getOption('path')), 0, 0, true, $input->getOption('recursive')); @@ -175,11 +155,6 @@ protected function listFilesAction(InputInterface $input): array return $files; } - /** - * @param InputInterface $input - * - * @return void - */ protected function countFoldersAction(InputInterface $input): void { $numberOfFolders = $this->storage->countFoldersInFolder($this->getFolder($input->getOption('path')), true, $input->getOption('recursive')); @@ -187,11 +162,6 @@ protected function countFoldersAction(InputInterface $input): void $this->log('I found %s folder(s)', [$numberOfFolders]); } - /** - * @param InputInterface $input - * - * @return void - */ protected function countFilesAction(InputInterface $input): void { $numberOfFiles = $this->storage->countFilesInFolder($this->getFolder($input->getOption('path')), true, $input->getOption('recursive')); @@ -199,12 +169,7 @@ protected function countFilesAction(InputInterface $input): void $this->log('I found %s files(s)', [$numberOfFiles]); } - /** - * @param string $folderIdentifier - * - * @return object|Folder - */ - protected function getFolder($folderIdentifier): Folder + protected function getFolder(string $folderIdentifier): Folder { $folderIdentifier = $folderIdentifier === DIRECTORY_SEPARATOR ? $folderIdentifier : DIRECTORY_SEPARATOR . trim($folderIdentifier, '/') . DIRECTORY_SEPARATOR; diff --git a/Classes/Command/CloudinaryScanCommand.php b/Classes/Command/CloudinaryScanCommand.php index fd7fece..d5eae8c 100644 --- a/Classes/Command/CloudinaryScanCommand.php +++ b/Classes/Command/CloudinaryScanCommand.php @@ -9,11 +9,13 @@ * LICENSE.md file that was distributed with this source code. */ +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; +use TYPO3\CMS\Core\Core\Environment; use TYPO3\CMS\Core\Resource\ResourceFactory; use TYPO3\CMS\Core\Resource\ResourceStorage; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -24,15 +26,8 @@ */ class CloudinaryScanCommand extends AbstractCloudinaryCommand { - /** - * @var ResourceStorage - */ protected ResourceStorage $storage; - /** - * @param InputInterface $input - * @param OutputInterface $output - */ protected function initialize(InputInterface $input, OutputInterface $output) { $this->io = new SymfonyStyle($input, $output); @@ -42,21 +37,11 @@ protected function initialize(InputInterface $input, OutputInterface $output) $this->storage = $resourceFactory->getStorageObject($input->getArgument('storage')); } - /** - * Configure the command by defining the name, options and arguments - */ protected function configure() { $message = 'Scan and warm up a cloudinary storage.'; $this->setDescription($message) ->addOption('silent', 's', InputOption::VALUE_OPTIONAL, 'Mute output as much as possible', false) - ->addOption( - 'empty', - 'e', - InputOption::VALUE_OPTIONAL, - 'Before scanning empty all resources for a given storage', - false, - ) ->addArgument('storage', InputArgument::REQUIRED, 'Storage identifier') ->setHelp('Usage: ./vendor/bin/typo3 cloudinary:scan [0-9]'); } @@ -65,17 +50,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int { if (!$this->checkDriverType($this->storage)) { $this->log('Look out! Storage is not of type "cloudinary"'); - return 1; - } - - if ($input->getOption('empty') === null || $input->getOption('empty')) { - $this->log('Emptying all mirrored resources for storage "%s"', [$this->storage->getUid()]); - $this->log(); - $this->getCloudinaryScanService()->deleteAll(); + return Command::INVALID; } + $logFile = Environment::getVarPath() . '/log/cloudinary.log'; $this->log('Hint! Look at the log to get more insight:'); - $this->log('tail -f web/typo3temp/var/logs/cloudinary.log'); + $this->log('tail -f ' . $logFile); $this->log(); $result = $this->getCloudinaryScanService()->scan(); @@ -99,7 +79,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $result['folder_deleted'], ]); - return 0; + return Command::SUCCESS; } protected function getCloudinaryScanService(): CloudinaryScanService From 4419f3632f236ea123866f840a4a058843d28cb5 Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Tue, 21 Mar 2023 14:07:48 +0100 Subject: [PATCH 06/54] [TASK] Fix type --- Classes/Services/CloudinaryResourceService.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Classes/Services/CloudinaryResourceService.php b/Classes/Services/CloudinaryResourceService.php index 0f88905..ed1445b 100644 --- a/Classes/Services/CloudinaryResourceService.php +++ b/Classes/Services/CloudinaryResourceService.php @@ -65,10 +65,10 @@ public function getResources( ->where($query->expr()->eq('storage', $this->storage->getUid())); // We should handle recursion - $expresion = $recursive + $expression = $recursive ? $query->expr()->like('folder', $query->expr()->literal($folder . '%')) : $query->expr()->eq('folder', $query->expr()->literal($folder)); - $query->andWhere($expresion); + $query->andWhere($expression); if ($orderings) { $query->orderBy($orderings['fieldName'], $orderings['direction']); From 88c216383bd35d781720eff427cfcd17a10e2834 Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Tue, 21 Mar 2023 14:08:08 +0100 Subject: [PATCH 07/54] [FEATURE] Introduce cloudinary resource tagging and metadata --- Classes/Command/CloudinaryMetadataCommand.php | 122 ++++++++++++++++++ Configuration/Services.yaml | 7 + 2 files changed, 129 insertions(+) create mode 100644 Classes/Command/CloudinaryMetadataCommand.php diff --git a/Classes/Command/CloudinaryMetadataCommand.php b/Classes/Command/CloudinaryMetadataCommand.php new file mode 100644 index 0000000..4b532b9 --- /dev/null +++ b/Classes/Command/CloudinaryMetadataCommand.php @@ -0,0 +1,122 @@ +io = new SymfonyStyle($input, $output); + + /** @var ResourceFactory $resourceFactory */ + $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class); + $this->storage = $resourceFactory->getStorageObject($input->getArgument('storage')); + + $this->cloudinaryPathService = GeneralUtility::makeInstance( + CloudinaryPathService::class, + $this->storage->getConfiguration(), + ); + } + + protected function configure() + { + $message = 'Set metadata on cloudinary resources such as file reference and file usage.'; + $this->setDescription($message) + ->addOption('silent', 's', InputOption::VALUE_OPTIONAL, 'Mute output as much as possible', false) + ->addArgument('storage', InputArgument::REQUIRED, 'Storage identifier') + ->setHelp('Usage: ./vendor/bin/typo3 cloudinary:metadata [0-9]'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + if (!$this->checkDriverType($this->storage)) { + $this->log('Look out! Storage is not of type "cloudinary"'); + return Command::INVALID; + } + + $q = $this->getQueryBuilder('sys_file'); + $items = $q->select('file.*', 'reference.*') + ->from('sys_file', 'file') + ->innerJoin( + 'file', + 'sys_file_reference', + 'reference', + 'file.uid = reference.uid_local' + ) + ->where( + $q->expr()->eq('file.storage', $this->storage->getUid()), + $q->expr()->or( + // we could extend to more tables... + $q->expr()->eq('tablenames', $q->expr()->literal('tt_content')), + $q->expr()->eq('tablenames', $q->expr()->literal('pages')) + ) + ) + ->execute() + ->fetchAllAssociative(); + + $site = $this->getFirstSite(); + + $publicIdOptions = []; + foreach ($items as $item) { + $publicId = $this->cloudinaryPathService->computeCloudinaryPublicId($item['identifier']); + $publicIdOptions[$publicId]['tags'][$item['pid']] = 't3-page-' . $item['pid']; + $publicIdOptions[$publicId]['context']['t3-page-' . $item['pid']] = rtrim((string)$site->getBase(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . '?id=' . $item['pid']; + } + + // Initialize and configure the API + $this->initializeApi(); + foreach ($publicIdOptions as $publicId => $options) { + $this->log('Updating tags and metadata for public id ' . $publicId); + \Cloudinary\Uploader::explicit( + $publicId, + [ + 'type' => 'upload', + 'tags' => 't3,t3-page,' . implode(', ', $options['tags']), + 'context' => $options['context'] + ] + ); + } + + return Command::SUCCESS; + } + + public function getFirstSite(): Site + { + $siteFinder = GeneralUtility::makeInstance(SiteFinder::class); + $sites = $siteFinder->getAllSites(); + return array_values($sites)[0]; + } + + protected function initializeApi(): void + { + CloudinaryApiUtility::initializeByConfiguration($this->storage->getConfiguration()); + } + +} diff --git a/Configuration/Services.yaml b/Configuration/Services.yaml index 7acc094..93ecc52 100644 --- a/Configuration/Services.yaml +++ b/Configuration/Services.yaml @@ -42,6 +42,13 @@ services: schedulable: false description: Scan and warm up a cloudinary storage. + Visol\Cloudinary\Command\CloudinaryMetadataCommand: + tags: + - name: 'console.command' + command: 'cloudinary:metadata' + schedulable: false + description: Set metadata on cloudinary resources such as file reference and file usage. + Visol\Cloudinary\Command\CloudinaryQueryCommand: tags: - name: 'console.command' From 5723bb52c6a3fc77af0c0744242287cf88790c13 Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Tue, 21 Mar 2023 17:44:22 +0100 Subject: [PATCH 08/54] fixup! fixup! [FEATURE] Introduce cloudinary web hook handler --- .../CloudinaryWebHookController.php | 128 +++++++++++++++++- 1 file changed, 126 insertions(+), 2 deletions(-) diff --git a/Classes/Controller/CloudinaryWebHookController.php b/Classes/Controller/CloudinaryWebHookController.php index dd82840..2d1960e 100644 --- a/Classes/Controller/CloudinaryWebHookController.php +++ b/Classes/Controller/CloudinaryWebHookController.php @@ -9,12 +9,15 @@ * LICENSE.md file that was distributed with this source code. */ +use Causal\Cloudflare\Services\CloudflareService; use Psr\Http\Message\ResponseInterface; use TYPO3\CMS\Core\Cache\CacheManager; +use TYPO3\CMS\Core\Configuration\ExtensionConfiguration; use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Database\Query\QueryBuilder; use TYPO3\CMS\Core\Log\Logger; use TYPO3\CMS\Core\Log\LogManager; +use TYPO3\CMS\Core\Package\PackageManager; use TYPO3\CMS\Core\Resource\File; use TYPO3\CMS\Core\Resource\ProcessedFileRepository; use TYPO3\CMS\Core\Resource\ResourceFactory; @@ -27,6 +30,7 @@ use Visol\Cloudinary\Services\CloudinaryPathService; use Visol\Cloudinary\Services\CloudinaryResourceService; use Visol\Cloudinary\Services\CloudinaryScanService; +use Visol\Cloudinary\Utility\CloudinaryApiUtility; use Visol\Cloudinary\Utility\CloudinaryFileUtility; class CloudinaryWebHookController extends ActionController @@ -37,11 +41,17 @@ class CloudinaryWebHookController extends ActionController protected const NOTIFICATION_TYPE_DELETE = 'delete'; protected CloudinaryResourceService $cloudinaryResourceService; + protected CloudinaryScanService $scanService; + protected CloudinaryPathService $cloudinaryPathService; + protected ProcessedFileRepository $processedFileRepository; + protected ResourceStorage $storage; + protected PackageManager $packageManager; + protected function initializeAction(): void { $this->checkEnvironment(); @@ -68,6 +78,8 @@ protected function initializeAction(): void $this->storage = $storage; $this->processedFileRepository = GeneralUtility::makeInstance(ProcessedFileRepository::class); + + $this->packageManager = GeneralUtility::makeInstance(PackageManager::class); } public function processAction(): ResponseInterface @@ -84,8 +96,22 @@ public function processAction(): ResponseInterface [$requestType, $publicIds] = $this->getRequestInfo($payload); self::getLogger()->debug(sprintf('Start cache flushing for file "%s". ', $requestType)); + $this->initializeApi(); foreach ($publicIds as $publicId) { + + self::getLogger()->warning($publicId, ['asdf']); + + if ($requestType === self::NOTIFICATION_TYPE_DELETE) { + if (strpos($publicId, '_processed_') === null) { + $message = sprintf('Deleted file "%s", this should not happen. A file is going to be missing.', $publicId); + } else { + $message = sprintf('Processed file deleted "%s". Nothing to do, stopping here...', $publicId); + } + self::getLogger()->warning($message); + continue; + } + $cloudinaryResource = $this->getCloudinaryResource($publicId); // #. retrieve the source file @@ -99,6 +125,21 @@ public function processAction(): ResponseInterface // #. flush cache pages $this->clearCachePages($file); + + // #. flush cloudinary cdn cache + $this->flushCloudinaryCdn($publicId); + + // #. handle file rename + if ($requestType === self::NOTIFICATION_TYPE_RENAME) { + + // Delete the old cache resource + $this->cloudinaryResourceService->delete($publicId); + + // Rename the resource + $nextPublicId = $payload['to_public_id']; + $nextCloudinaryResource = $this->scanService->scanOne($nextPublicId); + $this->handleFileRename($file, $nextCloudinaryResource); + } } } catch (\Exception $e) { return $this->sendResponse([ @@ -110,6 +151,65 @@ public function processAction(): ResponseInterface return $this->sendResponse(['result' => 'ok', 'message' => 'Cache flushed']); } + protected function flushCloudflareCdn(array $tags): void + { + $config = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('cloudflare'); + + /** @var CloudflareService $cloudflareService */ + $cloudflareService = GeneralUtility::makeInstance(CloudflareService::class, $config); + + $domains = $config['domains'] ? GeneralUtility::trimExplode(',', $config['domains'], true) : []; + + foreach ($domains as $domain) { + try { + [$identifier, $zoneName] = explode('|', $domain, 2); + $result = $cloudflareService->send( + '/zones/' . $identifier . '/purge_cache', + [ + 'tags' => [$tags], + ], + 'DELETE' + ); + + if (is_array($result) && $result['success']) { + $message = vsprintf('Cleared the cache on Cloudflare using Cache-Tag (domain: "%s")', [$zoneName, implode(LF, $result['errors'])]); + self::getLogger()->info($message); + } else { + $message = vsprintf('Failed to clear the cache on Cloudflare using Cache-Tag (domain: "%s"): %s', [$zoneName, implode(LF, $result['errors'] ?? [])]); + self::getLogger()->warning($message); + } + } catch (\RuntimeException $e) { + self::getLogger()->error($e->getMessage()); + } + } + + } + + protected function flushCloudinaryCdn($publicId): void + { + // Invalidate CDN cache + \Cloudinary\Uploader::explicit( + $publicId, + [ + 'type' => 'upload', + 'invalidate' => true + ] + ); + } + + protected function handleFileRename(File $file, array $cloudinaryResource): void + { + $nextFileIdentifier = $this->cloudinaryPathService->computeFileIdentifier($cloudinaryResource); + $tableName = 'sys_file'; + $q = $this->getQueryBuilder($tableName); + $q->update($tableName) + ->where( + $q->expr()->eq('uid', $file->getUid()) + ) + ->set('identifier', $q->quoteIdentifier($nextFileIdentifier), false) + ->executeStatement(); + } + protected function getFile(array $cloudinaryResource): File { $fileIdentifier = $this->cloudinaryPathService->computeFileIdentifier($cloudinaryResource); @@ -124,7 +224,6 @@ protected function getRequestInfo(array $payload): array } elseif ($this->isRequestRename($payload)) { $requestType = self::NOTIFICATION_TYPE_RENAME; $publicIds = [$payload['from_public_id']]; - //$nextPublicId = $payload['to_public_id']; } elseif ($this->isRequestDelete($payload)) { $requestType = self::NOTIFICATION_TYPE_DELETE; $publicIds = []; @@ -173,6 +272,7 @@ protected function cleanUpTemporaryFile(File $file): void { $temporaryFileNameAndPath = CloudinaryFileUtility::getTemporaryFile($file->getStorage()->getUid(), $file->getIdentifier()); if (is_file($temporaryFileNameAndPath)) { + self::getLogger()->debug($temporaryFileNameAndPath); unlink($temporaryFileNameAndPath); } } @@ -186,6 +286,11 @@ protected function clearCachePages(File $file): void GeneralUtility::makeInstance(CacheManager::class) ->flushCachesInGroupByTags('pages', $tags); + + // #. flush cloudinary cdn cache if extension is available + if ($this->packageManager->isPackageAvailable('cloudflare')) { + $this->flushCloudflareCdn($tags); + } } protected function findPagesWithFileReferences(File $file): array @@ -203,9 +308,23 @@ protected function findPagesWithFileReferences(File $file): array ->fetchAllAssociative(); } + /** + * We only react for notification type "upload", "rename", "delete" + * @see other notification types + * https://cloudinary.com/documentation/notifications + * + * - create_folder, + * - resource_tags_changed, + * - resource_context_changed + * - ... + */ protected function shouldStopProcessing(mixed $payload): bool { - return !($this->isRequestUploadOverwrite($payload) || $this->isRequestRename($payload)); + return !( + $this->isRequestUploadOverwrite($payload) || + $this->isRequestRename($payload) || + $this->isRequestDelete($payload) + ); } protected function isRequestUploadOverwrite(mixed $payload): bool @@ -263,4 +382,9 @@ protected static function getLogger(): Logger return $logger; } + protected function initializeApi(): void + { + CloudinaryApiUtility::initializeByConfiguration($this->storage->getConfiguration()); + } + } From 83ae9a8195caa7694844a727029e7f14b0cf1d2c Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Tue, 28 Mar 2023 09:20:13 +0200 Subject: [PATCH 09/54] [TASK] Remove legacy driver --- Classes/Cache/CloudinaryTypo3Cache.php | 189 --- Classes/Driver/CloudinaryDriver.php | 1477 ------------------------ ext_localconf.php | 7 - 3 files changed, 1673 deletions(-) delete mode 100644 Classes/Cache/CloudinaryTypo3Cache.php delete mode 100644 Classes/Driver/CloudinaryDriver.php diff --git a/Classes/Cache/CloudinaryTypo3Cache.php b/Classes/Cache/CloudinaryTypo3Cache.php deleted file mode 100644 index eba3d91..0000000 --- a/Classes/Cache/CloudinaryTypo3Cache.php +++ /dev/null @@ -1,189 +0,0 @@ -storageUid = $storageUid; - } - - /** - * @param string $folderIdentifier - * @return array|false - */ - public function getCachedFiles(string $folderIdentifier) - { - return $this->get($this->computeFileCacheKey($folderIdentifier)); - } - - /** - * @param string $folderIdentifier - * @param array $files - */ - public function setCachedFiles(string $folderIdentifier, array $files): void - { - $this->set($this->computeFileCacheKey($folderIdentifier), $files, self::TAG_FILE); - } - - /** - * @param string $folderIdentifier - * @return array|false - */ - public function getCachedFolders(string $folderIdentifier) - { - return $this->get($this->computeFolderCacheKey($folderIdentifier)); - } - - /** - * @param string $folderIdentifier - * @param array $folders - */ - public function setCachedFolders(string $folderIdentifier, array $folders): void - { - $this->set($this->computeFolderCacheKey($folderIdentifier), $folders, self::TAG_FOLDER); - } - - /** - * @param string $identifier - * @return array|false - */ - protected function get(string $identifier) - { - return $this->isCacheEnabled ? $this->getCacheInstance()->get($identifier) : false; - } - - /** - * @param string $identifier - * @param array $data - * @param string $tag - */ - protected function set(string $identifier, array $data, $tag): void - { - if ($this->isCacheEnabled) { - $this->getCacheInstance()->set($identifier, $data, [$tag], self::LIFETIME); - - $this->log('Caching "%s" data with folder identifier "%s"', [$tag, $identifier]); - } - } - - /** - * @param string $folderIdentifier - * @return mixed - */ - protected function computeFolderCacheKey($folderIdentifier): string - { - // Sanitize the cache format as the key can not contains certain characters such as "/", ":", etc.. - return sprintf('storage-%s-folders-%s', $this->storageUid, str_replace('/', '%', $folderIdentifier)); - } - - /** - * @param string $folderIdentifier - * @return mixed - */ - protected function computeFileCacheKey($folderIdentifier): string - { - // Sanitize the cache format as the key can not contains certain characters such as "/", ":", etc.. - return sprintf('storage-%s-files-%s', $this->storageUid, str_replace('/', '%', $folderIdentifier)); - } - - /** - * @return void - */ - public function flushFileCache(): void - { - $this->getCacheInstance()->flushByTags([self::TAG_FILE]); - $this->log('Method "flushFileCache": file cache flushed'); - } - - /** - * @return void - */ - public function flushFolderCache(): void - { - $this->getCacheInstance()->flushByTags([self::TAG_FOLDER]); - $this->log('Method "flushFolderCache": folder cache flushed'); - } - - /** - * @return void - */ - public function flushAll(): void - { - $this->getCacheInstance()->flush(); - $this->log('Method "flushAll": all cache flushed'); - } - - /** - * @return AbstractFrontend - */ - protected function getCacheInstance() - { - return $this->getCacheManager()->getCache('cloudinary'); - } - - /** - * Return the Cache Manager - * - * @return CacheManager|object - */ - protected function getCacheManager() - { - return GeneralUtility::makeInstance(CacheManager::class); - } - - /** - * @param string $message - * @param array $arguments - */ - public function log(string $message, array $arguments = []) - { - /** @var Logger $logger */ - $logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__); - #$logger->log( - # LogLevel::INFO, - # vsprintf('[CACHE] ' . $message, $arguments) - #); - } -} diff --git a/Classes/Driver/CloudinaryDriver.php b/Classes/Driver/CloudinaryDriver.php deleted file mode 100644 index 127570c..0000000 --- a/Classes/Driver/CloudinaryDriver.php +++ /dev/null @@ -1,1477 +0,0 @@ - ['r' => bool, 'w' => bool] - * - * @var array - */ - protected $cachedPermissions = []; - - /** - * Cache to avoid creating multiple local files since it is time consuming. - * We must download the file. - * - * @var array - */ - protected $localProcessingFiles = []; - - /** - * @var ResourceStorage - */ - protected $storage = null; - - /** - * @var CharsetConverter - */ - protected $charsetConversion = null; - - /** - * @var string - */ - protected $languageFile = 'LLL:EXT:cloudinary/Resources/Private/Language/backend.xlf'; - - /** - * @var Dispatcher - */ - protected $signalSlotDispatcher; - - /** - * @var Api $api - */ - protected $api; - - /** - * @var CloudinaryTypo3Cache - */ - protected $cloudinaryTypo3Cache; - - /** - * @var CloudinaryPathService - */ - protected $cloudinaryPathService; - - /** - * @param array $configuration - */ - public function __construct(array $configuration = []) - { - $this->configuration = $configuration; - parent::__construct($configuration); - - // The capabilities default of this driver. See CAPABILITY_* constants for possible values - $this->capabilities = - ResourceStorage::CAPABILITY_BROWSABLE | - ResourceStorage::CAPABILITY_PUBLIC | - ResourceStorage::CAPABILITY_WRITABLE; - } - - /** - * @return void - */ - public function processConfiguration() - { - } - - /** - * @return void - */ - public function initialize() - { - // Test connection if we are in the edit view of this storage - if (ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isBackend() && !empty($_GET['edit']['sys_file_storage'])) { - $this->testConnection(); - } - } - - /** - * @param string $fileIdentifier - * - * @return string - */ - public function getPublicUrl($fileIdentifier) - { - return $this->resourceExists($fileIdentifier) - ? $this->getCachedCloudinaryResource($fileIdentifier)['secure_url'] - : ''; - } - - /** - * @param string $message - * @param array $arguments - * @param array $data - */ - protected function log(string $message, array $arguments = [], array $data = []) - { - /** @var Logger $logger */ - $logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__); - $logger->log(LogLevel::INFO, vsprintf($message, $arguments), $data); - } - - /** - * Creates a (cryptographic) hash for a file. - * - * @param string $fileIdentifier - * @param string $hashAlgorithm - * - * @return string - */ - public function hash($fileIdentifier, $hashAlgorithm) - { - return $this->hashIdentifier($fileIdentifier); - } - - /** - * Returns the identifier of the default folder new files should be put into. - * - * @return string - */ - public function getDefaultFolder() - { - return $this->getRootLevelFolder(); - } - - /** - * Returns the identifier of the root level folder of the storage. - * - * @return string - */ - public function getRootLevelFolder() - { - return DIRECTORY_SEPARATOR; - } - - /** - * Returns information about a file. - * - * @param string $fileIdentifier - * @param array $propertiesToExtract Array of properties which are be extracted - * If empty all will be extracted - * - * @return array - * @throws \Exception - */ - public function getFileInfoByIdentifier($fileIdentifier, array $propertiesToExtract = []) - { - $this->log( - 'Just a notice! Time consuming action ahead. I am going to download a file "%s"', - [$fileIdentifier], - ['getFileInfoByIdentifier'], - ); - - $cloudinaryResource = $this->getCachedCloudinaryResource($fileIdentifier); - - // True at the indexation of the file - // Cloudinary is asynchronous and we might not have the resource at hand. - // Call it one more time to double check! - - if (!$cloudinaryResource) { - $cloudinaryResource = $this->getCloudinaryResource($fileIdentifier); - $this->flushFileCache(); // We flush the cache.... - - // This time we have a problem! - if (!$cloudinaryResource) { - throw new \Exception( - 'I could not find a corresponding cloudinary resource for file ' . $fileIdentifier, - 1591775048, - ); - } - } - - // We are force to download the file in order to correctly find the mime type. - $localFile = $this->getFileForLocalProcessing($fileIdentifier); - - /** @var FileInfo $fileInfo */ - $fileInfo = GeneralUtility::makeInstance(FileInfo::class, $localFile); - $extension = PathUtility::pathinfo($localFile, PATHINFO_EXTENSION); - $mimeType = $fileInfo->getMimeType(); - - $canonicalFolderIdentifier = $this->canonicalizeAndCheckFolderIdentifier(PathUtility::dirname($fileIdentifier)); - - $values = [ - 'identifier_hash' => $this->hashIdentifier($fileIdentifier), - 'folder_hash' => sha1($canonicalFolderIdentifier), - 'creation_date' => strtotime($cloudinaryResource['created_at']), - 'modification_date' => strtotime($cloudinaryResource['created_at']), - 'mime_type' => $mimeType, - 'extension' => $extension, - 'size' => $this->getResourceInfo($cloudinaryResource, 'bytes'), - 'width' => $this->getResourceInfo($cloudinaryResource, 'width'), - 'height' => $this->getResourceInfo($cloudinaryResource, 'height'), - 'storage' => $this->storageUid, - 'identifier' => $fileIdentifier, - 'name' => PathUtility::basename($fileIdentifier), - ]; - - return $values; - } - - /** - * @param array $resource - * @param string $name - * - * @return string - */ - protected function getResourceInfo(array $resource, string $name): string - { - return $resource[$name] ?? ''; - } - - /** - * Checks if a file exists - * - * @param string $identifier - * - * @return bool - */ - public function fileExists($identifier) - { - if (substr($identifier, -1) === DIRECTORY_SEPARATOR || $identifier === '') { - return false; - } - return $this->resourceExists($identifier); - } - - /** - * Checks if a folder exists - * - * @param string $folderIdentifier - * - * @return bool - */ - public function folderExists($folderIdentifier) - { - try { - // Will trigger an exception if the folder identifier does not exist. - $subFolders = $this->getFoldersInFolder($folderIdentifier); - } catch (\Exception $e) { - return false; - } - return is_array($subFolders); - } - - /** - * @param string $fileName - * @param string $folderIdentifier - * - * @return bool - */ - public function fileExistsInFolder($fileName, $folderIdentifier) - { - $fileIdentifier = $folderIdentifier . $fileName; - return $this->resourceExists($fileIdentifier); - } - - /** - * Checks if a folder exists inside a storage folder - * - * @param string $folderName - * @param string $folderIdentifier - * - * @return bool - */ - public function folderExistsInFolder($folderName, $folderIdentifier) - { - $canonicalFolderPath = $this->canonicalizeAndCheckFolderIdentifierAndFolderName($folderIdentifier, $folderName); - return $this->folderExists($canonicalFolderPath); - } - - /** - * Returns the Identifier for a folder within a given folder. - * - * @param string $folderName The name of the target folder - * @param string $folderIdentifier - * - * @return string - */ - public function getFolderInFolder($folderName, $folderIdentifier) - { - return $folderIdentifier . DIRECTORY_SEPARATOR . $folderName; - } - - /** - * @param string $localFilePath - * @param string $targetFolderIdentifier - * @param string $newFileName optional, if not given original name is used - * @param bool $removeOriginal if set the original file will be removed - * after successful operation - * - * @return string the identifier of the new file - * @throws \Exception - */ - public function addFile($localFilePath, $targetFolderIdentifier, $newFileName = '', $removeOriginal = true) - { - $fileName = $this->sanitizeFileName($newFileName !== '' ? $newFileName : PathUtility::basename($localFilePath)); - - $fileIdentifier = $this->canonicalizeAndCheckFileIdentifier( - $this->canonicalizeAndCheckFolderIdentifier($targetFolderIdentifier) . $fileName, - ); - - // Necessary to happen in an early stage. - $this->log('[CACHE] Flushed as adding file', [], ['addFile']); - $this->flushFileCache(); - - $cloudinaryPublicId = $this->getCloudinaryPathService()->computeCloudinaryPublicId($fileIdentifier); - - $this->log( - '[API][UPLOAD] Cloudinary\Uploader::upload() - add resource "%s"', - [$cloudinaryPublicId], - ['addFile()'], - ); - - // Before calling API, make sure we are connected with the right "bucket" - $this->initializeApi(); - - // Upload the file - $resource = Uploader::upload($localFilePath, [ - 'public_id' => PathUtility::basename($cloudinaryPublicId), - 'folder' => $this->getCloudinaryPathService()->computeCloudinaryFolderPath($targetFolderIdentifier), - 'resource_type' => $this->getCloudinaryPathService()->getResourceType($fileIdentifier), - 'overwrite' => true, - ]); - - if (!$resource && $resource['type'] !== 'upload') { - throw new \RuntimeException('Cloudinary upload failed for ' . $fileIdentifier, 1591954943); - } - - return $fileIdentifier; - } - - /** - * @param string $fileIdentifier - * @param string $targetFolderIdentifier - * @param string $newFileName - * - * @return string - */ - public function moveFileWithinStorage($fileIdentifier, $targetFolderIdentifier, $newFileName) - { - $targetIdentifier = $targetFolderIdentifier . $newFileName; - return $this->renameFile($fileIdentifier, $targetIdentifier); - } - - /** - * Copies a file *within* the current storage. - * Note that this is only about an inner storage copy action, - * where a file is just copied to another folder in the same storage. - * - * @param string $fileIdentifier - * @param string $targetFolderIdentifier - * @param string $fileName - * - * @return string the Identifier of the new file - */ - public function copyFileWithinStorage($fileIdentifier, $targetFolderIdentifier, $fileName) - { - // Flush the file cache entries - $this->log('[CACHE] Flushed as copying file', [], ['copyFileWithinStorage']); - $this->flushFileCache(); - - // Before calling API, make sure we are connected with the right "bucket" - $this->initializeApi(); - - Uploader::upload($this->getPublicUrl($fileIdentifier), [ - 'public_id' => PathUtility::basename( - $this->getCloudinaryPathService()->computeCloudinaryPublicId($fileName), - ), - 'folder' => $this->getCloudinaryPathService()->computeCloudinaryFolderPath($targetFolderIdentifier), - 'resource_type' => $this->getCloudinaryPathService()->getResourceType($fileIdentifier), - 'overwrite' => true, - ]); - - $targetIdentifier = $targetFolderIdentifier . $fileName; - return $targetIdentifier; - } - - /** - * Replaces a file with file in local file system. - * - * @param string $fileIdentifier - * @param string $localFilePath - * - * @return bool - */ - public function replaceFile($fileIdentifier, $localFilePath) - { - $cloudinaryPublicId = PathUtility::basename( - $this->getCloudinaryPathService()->computeCloudinaryPublicId($fileIdentifier), - ); - $cloudinaryFolder = $this->getCloudinaryPathService()->computeCloudinaryFolderPath( - PathUtility::dirname($fileIdentifier), - ); - - $options = [ - 'public_id' => $cloudinaryPublicId, - 'folder' => $cloudinaryFolder, - 'resource_type' => $this->getCloudinaryPathService()->getResourceType($fileIdentifier), - 'overwrite' => true, - ]; - - // Flush the file cache entries - $this->log('[CACHE] Flushed as replacing file', [], ['replaceFile']); - $this->flushFileCache(); - - // Before calling the API, make sure we are connected with the right "bucket" - $this->initializeApi(); - - // Upload the file - Uploader::upload($localFilePath, $options); - - return true; - } - - /** - * Removes a file from the filesystem. This does not check if the file is - * still used or if it is a bad idea to delete it for some other reason - * this has to be taken care of in the upper layers (e.g. the Storage)! - * - * @param string $fileIdentifier - * - * @return bool TRUE if deleting the file succeeded - */ - public function deleteFile($fileIdentifier) - { - // Necessary to happen in an early stage. - $this->log('[CACHE] Flushed as deleting file', [], ['deleteFile']); - $this->flushFileCache(); - - $cloudinaryPublicId = $this->getCloudinaryPathService()->computeCloudinaryPublicId($fileIdentifier); - $this->log( - '[API][DELETE] Cloudinary\Api::delete_resources - delete resource "%s"', - [$cloudinaryPublicId], - ['deleteFile'], - ); - - $response = $this->getApi()->delete_resources($cloudinaryPublicId, [ - 'resource_type' => $this->getCloudinaryPathService()->getResourceType($fileIdentifier), - ]); - - $key = is_array($response['deleted']) ? key($response['deleted']) : ''; - - return is_array($response['deleted']) && - isset($response['deleted'][$key]) && - $response['deleted'][$key] === 'deleted'; - } - - /** - * Removes a folder in filesystem. - * - * @param string $folderIdentifier - * @param bool $deleteRecursively - * - * @return bool - * @throws Api\GeneralError - */ - public function deleteFolder($folderIdentifier, $deleteRecursively = false) - { - $cloudinaryFolder = $this->getCloudinaryPathService()->computeCloudinaryFolderPath($folderIdentifier); - if ($deleteRecursively) { - $this->log( - '[API][DELETE] Cloudinary\Api::delete_resources_by_prefix() - folder "%s"', - [$cloudinaryFolder], - ['deleteFolder'], - ); - $this->getApi()->delete_resources_by_prefix($cloudinaryFolder); - } - - // We make sure the folder exists first. It will also delete sub-folder if those ones are empty. - if ($this->folderExists($folderIdentifier)) { - $this->log( - '[API][DELETE] Cloudinary\Api::delete_folder() - folder "%s"', - [$cloudinaryFolder], - ['deleteFolder'], - ); - $this->getApi()->delete_folder($cloudinaryFolder); - } - - // Flush the folder cache entries - $this->log('[CACHE][FOLDER] Flushed as deleting folder', [], ['deleteFolder']); - $this->flushFolderCache(); - - return true; - } - - /** - * @param string $fileIdentifier - * @param bool $writable - * - * @return string - */ - public function getFileForLocalProcessing($fileIdentifier, $writable = true) - { - $temporaryPath = $this->getTemporaryPathForFile($fileIdentifier); - - if (!is_file($temporaryPath) || !filesize($temporaryPath)) { - $this->log( - '[SLOW] Downloading for local processing "%s"', - [$fileIdentifier], - ['getFileForLocalProcessing'], - ); - - $cloudinaryResource = $this->getCloudinaryResource($fileIdentifier); - - // We have a problem! - if (!$cloudinaryResource) { - throw new \Exception( - 'I could not find a corresponding cloudinary resource for file ' . $fileIdentifier, - 1591775049, - ); - } - - $this->log('File downloaded into "%s"', [$temporaryPath], ['getFileForLocalProcessing']); - file_put_contents($temporaryPath, file_get_contents($cloudinaryResource['secure_url'])); - } - - return $temporaryPath; - } - - /** - * Creates a new (empty) file and returns the identifier. - * - * @param string $fileName - * @param string $parentFolderIdentifier - * - * @return string - */ - public function createFile($fileName, $parentFolderIdentifier) - { - throw new \RuntimeException( - 'createFile: not implemented action! Cloudinary Driver is limited to images.', - 1570728107, - ); - } - - /** - * Creates a folder, within a parent folder. - * If no parent folder is given, a root level folder will be created - * - * @param string $newFolderName - * @param string $parentFolderIdentifier - * @param bool $recursive - * - * @return string the Identifier of the new folder - */ - public function createFolder($newFolderName, $parentFolderIdentifier = '', $recursive = false) - { - $canonicalFolderPath = $this->canonicalizeAndCheckFolderIdentifierAndFolderName( - $parentFolderIdentifier, - $newFolderName, - ); - $cloudinaryFolder = $this->getCloudinaryPathService()->normalizeCloudinaryPath($canonicalFolderPath); - - $this->log('[API][CREATE] Cloudinary\Api::createFolder() - folder "%s"', [$cloudinaryFolder], ['createFolder']); - $this->getApi()->create_folder($cloudinaryFolder); - - // Flush the folder cache entries - $this->log('[CACHE][FOLDER] Flushed as creating folder', [], ['createFolder']); - $this->flushFolderCache(); - - return $canonicalFolderPath; - } - - /** - * @param string $fileIdentifier - * - * @return string - */ - public function getFileContents($fileIdentifier) - { - // Will download the file to be faster next time the content is required. - $localFileNameAndPath = $this->getFileForLocalProcessing($fileIdentifier); - return file_get_contents($localFileNameAndPath); - } - - /** - * Sets the contents of a file to the specified value. - * - * @param string $fileIdentifier - * @param string $contents - * - * @return int - */ - public function setFileContents($fileIdentifier, $contents) - { - throw new \RuntimeException('setFileContents: not implemented action!', 1570728106); - } - - /** - * Renames a file in this storage. - * - * @param string $fileIdentifier - * @param string $newFileIdentifier The target path (including the file name!) - * - * @return string The identifier of the file after renaming - */ - public function renameFile($fileIdentifier, $newFileIdentifier) - { - if (!$this->isFileIdentifier($newFileIdentifier)) { - $sanitizedFileName = $this->sanitizeFileName(PathUtility::basename($newFileIdentifier)); - $folderPath = PathUtility::dirname($fileIdentifier); - $newFileIdentifier = $this->canonicalizeAndCheckFileIdentifier( - $this->canonicalizeAndCheckFolderIdentifier($folderPath) . $sanitizedFileName, - ); - } - - $cloudinaryPublicId = $this->getCloudinaryPathService()->computeCloudinaryPublicId($fileIdentifier); - $newCloudinaryPublicId = $this->getCloudinaryPathService()->computeCloudinaryPublicId($newFileIdentifier); - - if ($cloudinaryPublicId !== $newCloudinaryPublicId) { - // Necessary to happen in an early stage. - - $this->log('[CACHE] Flushed as renaming file', [], ['renameFile']); - $this->flushFileCache(); - - // Before calling API, make sure we are connected with the right "bucket" - $this->initializeApi(); - - // Rename the file - Uploader::rename($cloudinaryPublicId, $newCloudinaryPublicId, [ - 'resource_type' => $this->getCloudinaryPathService()->getResourceType($fileIdentifier), - ]); - } - - return $newFileIdentifier; - } - - /** - * Renames a folder in this storage. - * - * @param string $folderIdentifier - * @param string $newFolderName - * - * @return array A map of old to new file identifiers of all affected resources - */ - public function renameFolder($folderIdentifier, $newFolderName) - { - $renamedFiles = []; - - foreach ($this->getFilesInFolder($folderIdentifier, 0, -1) as $fileIdentifier) { - $resource = $this->getCachedCloudinaryResource($fileIdentifier); - $cloudinaryPublicId = $resource['public_id']; - - $pathSegments = GeneralUtility::trimExplode('/', $cloudinaryPublicId); - - $numberOfSegments = count($pathSegments); - if ($numberOfSegments > 1) { - // Replace last folder name by the new folder name - $pathSegments[$numberOfSegments - 2] = $newFolderName; - $newCloudinaryPublicId = implode('/', $pathSegments); - - if ($cloudinaryPublicId !== $newCloudinaryPublicId) { - // Flush files + folder cache - $this->flushCache(); - - // Before calling the API, make sure we are connected with the right "bucket" - $this->initializeApi(); - - // Rename the file - Uploader::rename($cloudinaryPublicId, $newCloudinaryPublicId, [ - 'resource_type' => $this->getCloudinaryPathService()->getResourceType($fileIdentifier), - ]); - $oldFileIdentifier = $this->getCloudinaryPathService()->computeFileIdentifier($resource); - $newFileIdentifier = $this->getCloudinaryPathService()->computeFileIdentifier([ - 'public_id' => $newCloudinaryPublicId, - 'format' => $resource['format'], - ]); - $renamedFiles[$oldFileIdentifier] = $newFileIdentifier; - } - } - } - - // After working so hard, delete the old empty folder. - $this->deleteFolder($folderIdentifier); - - return $renamedFiles; - } - - /** - * @param string $sourceFolderIdentifier - * @param string $targetFolderIdentifier - * @param string $newFolderName - * - * @return array All files which are affected, map of old => new file identifiers - */ - public function moveFolderWithinStorage($sourceFolderIdentifier, $targetFolderIdentifier, $newFolderName) - { - // Compute the new folder identifier and then create it. - $newTargetFolderIdentifier = $targetFolderIdentifier . $newFolderName . DIRECTORY_SEPARATOR; - if (!$this->folderExists($newTargetFolderIdentifier)) { - $this->createFolder($newTargetFolderIdentifier); - } - - $movedFiles = []; - $files = $this->getFilesInFolder($sourceFolderIdentifier, 0, -1); - foreach ($files as $fileIdentifier) { - $movedFiles[$fileIdentifier] = $this->moveFileWithinStorage( - $fileIdentifier, - $newTargetFolderIdentifier, - PathUtility::basename($fileIdentifier), - ); - } - - // Delete the old and empty folder - $this->deleteFolder($sourceFolderIdentifier); - - return $movedFiles; - } - - /** - * @param string $sourceFolderIdentifier - * @param string $targetFolderIdentifier - * @param string $newFolderName - * - * @return bool - */ - public function copyFolderWithinStorage($sourceFolderIdentifier, $targetFolderIdentifier, $newFolderName) - { - // Compute the new folder identifier and then create it. - $newTargetFolderIdentifier = $this->canonicalizeAndCheckFolderIdentifierAndFolderName( - $targetFolderIdentifier, - $newFolderName, - ); - - if (!$this->folderExists($newTargetFolderIdentifier)) { - $this->createFolder($newTargetFolderIdentifier); - } - - $files = $this->getFilesInFolder($sourceFolderIdentifier, 0, -1); - foreach ($files as $fileIdentifier) { - $this->copyFileWithinStorage( - $fileIdentifier, - $newTargetFolderIdentifier, - PathUtility::basename($fileIdentifier), - ); - } - - return true; - } - - /** - * Checks if a folder contains files and (if supported) other folders. - * - * @param string $folderIdentifier - * - * @return bool TRUE if there are no files and folders within $folder - */ - public function isFolderEmpty($folderIdentifier) - { - $cloudinaryFolder = $this->getCloudinaryPathService()->computeCloudinaryFolderPath($folderIdentifier); - $this->log( - '[API] Cloudinary\Api::resources() - fetch files from folder "%s"', - [$cloudinaryFolder], - ['isFolderEmpty'], - ); - $response = $this->getApi()->resources([ - 'resource_type' => 'image', - 'type' => 'upload', - 'max_results' => 1, - 'prefix' => $cloudinaryFolder, - ]); - - return empty($response['resources']); - } - - /** - * Checks if a given identifier is within a container, e.g. if - * a file or folder is within another folder. - * This can e.g. be used to check for web-mounts. - * - * Hint: this also needs to return TRUE if the given identifier - * matches the container identifier to allow access to the root - * folder of a filemount. - * - * @param string $folderIdentifier - * @param string $identifier identifier to be checked against $folderIdentifier - * - * @return bool TRUE if $content is within or matches $folderIdentifier - */ - public function isWithin($folderIdentifier, $identifier) - { - $folderIdentifier = $this->canonicalizeAndCheckFileIdentifier($folderIdentifier); - $fileIdentifier = $this->canonicalizeAndCheckFileIdentifier($identifier); - if ($folderIdentifier === $fileIdentifier) { - return true; - } - - // File identifier canonicalization will not modify a single slash so - // we must not append another slash in that case. - if ($folderIdentifier !== DIRECTORY_SEPARATOR) { - $folderIdentifier .= DIRECTORY_SEPARATOR; - } - - return \str_starts_with($fileIdentifier, $folderIdentifier); - } - - /** - * Returns information about a file. - * - * @param string $folderIdentifier - * - * @return array - */ - public function getFolderInfoByIdentifier($folderIdentifier) - { - $canonicalFolderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier); - return [ - 'identifier' => $canonicalFolderIdentifier, - 'name' => PathUtility::basename( - $this->getCloudinaryPathService()->normalizeCloudinaryPath($canonicalFolderIdentifier), - ), - 'storage' => $this->storageUid, - ]; - } - - /** - * Returns a file inside the specified path - * - * @param string $fileName - * @param string $folderIdentifier - * - * @return string File Identifier - */ - public function getFileInFolder($fileName, $folderIdentifier) - { - $folderIdentifier = $folderIdentifier . DIRECTORY_SEPARATOR . $fileName; - return $folderIdentifier; - } - - /** - * Returns a list of files inside the specified path - * - * @param string $folderIdentifier - * @param int $start - * @param int $numberOfItems - * @param bool $recursive - * @param array $filenameFilterCallbacks callbacks for filtering the items - * @param string $sort Property name used to sort the items. - * Among them may be: '' (empty, no sorting), name, - * fileext, size, tstamp and rw. - * If a driver does not support the given property, it - * should fall back to "name". - * @param bool $sortRev TRUE to indicate reverse sorting (last to first) - * - * @return array of FileIdentifiers - */ - public function getFilesInFolder( - $folderIdentifier, - $start = 0, - $numberOfItems = 40, - $recursive = false, - array $filenameFilterCallbacks = [], - $sort = '', - $sortRev = false - ) { - if ($folderIdentifier === '') { - throw new \RuntimeException( - 'Something went wrong in method "getFilesInFolder"! $folderIdentifier can not be empty', - 1574754623, - ); - } - - if (!isset($this->cachedCloudinaryResources[$folderIdentifier])) { - // Try to fetch from the cache - $this->cachedCloudinaryResources[$folderIdentifier] = $this->getCache()->getCachedFiles($folderIdentifier); - - // If not found in TYPO3 cache, ask Cloudinary - if (!is_array($this->cachedCloudinaryResources[$folderIdentifier])) { - $this->cachedCloudinaryResources[$folderIdentifier] = $this->getCloudinaryResources($folderIdentifier); - } - } - - // Set default sorting - $parameters = (array) GeneralUtility::_GP('SET'); - if (empty($parameters)) { - $parameters['sort'] = 'file'; - $parameters['reverse'] = 0; - } - - // Sort files - if ($parameters['sort'] === 'file') { - if ((int) $parameters['reverse']) { - uasort( - $this->cachedCloudinaryResources[$folderIdentifier], - '\Visol\Cloudinary\Utility\SortingUtility::sortByFileNameDesc', - ); - } else { - uasort( - $this->cachedCloudinaryResources[$folderIdentifier], - '\Visol\Cloudinary\Utility\SortingUtility::sortByFileNameAsc', - ); - } - } elseif ($parameters['sort'] === 'tstamp') { - if ((int) $parameters['reverse']) { - uasort( - $this->cachedCloudinaryResources[$folderIdentifier], - '\Visol\Cloudinary\Utility\SortingUtility::sortByTimeStampDesc', - ); - } else { - uasort( - $this->cachedCloudinaryResources[$folderIdentifier], - '\Visol\Cloudinary\Utility\SortingUtility::sortByTimeStampAsc', - ); - } - } - - // Pagination - if ($numberOfItems > 0) { - $files = array_slice( - $this->cachedCloudinaryResources[$folderIdentifier], - (int) GeneralUtility::_GP('pointer'), - $numberOfItems, - ); - } else { - $files = $this->cachedCloudinaryResources[$folderIdentifier]; - } - - return array_keys($files); - } - - /** - * Returns the number of files inside the specified path - * - * @param string $folderIdentifier - * @param bool $recursive - * @param array $filenameFilterCallbacks callbacks for filtering the items - * - * @return int Number of files in folder - */ - public function countFilesInFolder($folderIdentifier, $recursive = false, array $filenameFilterCallbacks = []) - { - if (!isset($this->cachedCloudinaryResources[$folderIdentifier])) { - $this->getFilesInFolder($folderIdentifier, 0, -1, $recursive, $filenameFilterCallbacks); - } - return count($this->cachedCloudinaryResources[$folderIdentifier]); - } - - /** - * Returns a list of folders inside the specified path - * - * @param string $folderIdentifier - * @param int $start - * @param int $numberOfItems - * @param bool $recursive - * @param array $folderNameFilterCallbacks callbacks for filtering the items - * @param string $sort Property name used to sort the items. - * Among them may be: '' (empty, no sorting), name, - * fileext, size, tstamp and rw. - * If a driver does not support the given property, it - * should fall back to "name". - * @param bool $sortRev TRUE to indicate reverse sorting (last to first) - * - * @return array - */ - public function getFoldersInFolder( - $folderIdentifier, - $start = 0, - $numberOfItems = 40, - $recursive = false, - array $folderNameFilterCallbacks = [], - $sort = '', - $sortRev = false - ) { - $folderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier); - - if (!isset($this->cachedFolders[$folderIdentifier])) { - // Try to fetch from the cache - $this->cachedFolders[$folderIdentifier] = $this->getCache()->getCachedFolders($folderIdentifier); - - // If not found in TYPO3 cache, ask Cloudinary - if (!is_array($this->cachedFolders[$folderIdentifier])) { - $this->cachedFolders[$folderIdentifier] = $this->getCloudinaryFolders($folderIdentifier); - } - } - - // Sort - $parameters = (array) GeneralUtility::_GP('SET'); - if (isset($parameters['sort']) && $parameters['sort'] === 'file') { - (int) $parameters['reverse'] - ? krsort($this->cachedFolders[$folderIdentifier]) - : ksort($this->cachedFolders[$folderIdentifier]); - } - - return $this->cachedFolders[$folderIdentifier]; - } - - /** - * Returns the number of folders inside the specified path - * - * @param string $folderIdentifier - * @param bool $recursive - * @param array $folderNameFilterCallbacks callbacks for filtering the items - * - * @return int Number of folders in folder - */ - public function countFoldersInFolder($folderIdentifier, $recursive = false, array $folderNameFilterCallbacks = []) - { - return count($this->getFoldersInFolder($folderIdentifier, 0, -1, $recursive, $folderNameFilterCallbacks)); - } - - /** - * @param string $identifier - * - * @return string - */ - public function dumpFileContents($identifier) - { - return $this->getFileContents($identifier); - } - - /** - * Returns the permissions of a file/folder as an array - * (keys r, w) of bool flags - * - * @param string $identifier - * - * @return array - */ - public function getPermissions($identifier) - { - if (!isset($this->cachedPermissions[$identifier])) { - // Cloudinary does not handle permissions - $permissions = ['r' => true, 'w' => true]; - $this->cachedPermissions[$identifier] = $permissions; - } - return $this->cachedPermissions[$identifier]; - } - - /** - * Merges the capabilites merged by the user at the storage - * configuration into the actual capabilities of the driver - * and returns the result. - * - * @param int $capabilities - * - * @return int - */ - public function mergeConfigurationCapabilities($capabilities) - { - $this->capabilities &= $capabilities; - return $this->capabilities; - } - - /** - * Returns a string where any character not matching [.a-zA-Z0-9_-] is - * substituted by '_' - * Trailing dots are removed - * - * @param string $fileName Input string, typically the body of a fileName - * @param string $charset Charset of the a fileName (defaults to current charset; depending on context) - * - * @return string Output string with any characters not matching [.a-zA-Z0-9_-] is substituted by '_' and trailing dots removed - * @throws Exception\InvalidFileNameException - */ - public function sanitizeFileName($fileName, $charset = '') - { - $fileName = $this->getCharsetConversion()->specCharsToASCII('utf-8', $fileName); - - // Replace unwanted characters by underscores - $cleanFileName = preg_replace( - '/[' . self::UNSAFE_FILENAME_CHARACTER_EXPRESSION . '\\xC0-\\xFF]/', - '_', - trim($fileName), - ); - - // Strip trailing dots and return - $cleanFileName = rtrim($cleanFileName, '.'); - if ($cleanFileName === '') { - throw new InvalidFileNameException('File name "' . $fileName . '" is invalid.', 1320288991); - } - - // Handle the special jpg case which does not correspond to the file extension. - return preg_replace('/jpeg$/', 'jpg', $cleanFileName); - } - - /** - * Returns a temporary path for a given file, including the file extension. - * - * @param string $fileIdentifier - * - * @return string - */ - protected function getTemporaryPathForFile($fileIdentifier): string - { - $temporaryFileNameAndPath = sprintf( - '%s/typo3temp/var/transient/%s%s', - Environment::getPublicPath(), - $this->storageUid, - $fileIdentifier, - ); - - $temporaryFolder = GeneralUtility::dirname($temporaryFileNameAndPath); - - if (!is_dir($temporaryFolder)) { - GeneralUtility::mkdir_deep($temporaryFolder); - } - return $temporaryFileNameAndPath; - } - - /** - * @param string $newFileIdentifier - * - * @return bool - */ - protected function isFileIdentifier(string $newFileIdentifier): bool - { - return false !== strpos($newFileIdentifier, DIRECTORY_SEPARATOR); - } - - /** - * @param string $folderIdentifier - * @param string $folderName - * - * @return string - */ - protected function canonicalizeAndCheckFolderIdentifierAndFolderName( - string $folderIdentifier, - string $folderName - ): string { - $canonicalFolderPath = $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier); - return $this->canonicalizeAndCheckFolderIdentifier( - $canonicalFolderPath . trim($folderName, DIRECTORY_SEPARATOR), - ); - } - - /** - * @param string $folderIdentifier - * - * @return array - * @throws Api\GeneralError - */ - protected function getCloudinaryFolders(string $folderIdentifier): array - { - $folders = []; - - $cloudinaryFolder = $this->getCloudinaryPathService()->computeCloudinaryFolderPath($folderIdentifier); - - $this->log('Fetch subfolders from folder "%s"', [$cloudinaryFolder], ['getCloudinaryFolders']); - - $resources = (array) $this->getApi()->subfolders($cloudinaryFolder); - - if (!empty($resources['folders'])) { - foreach ($resources['folders'] as $cloudinaryFolder) { - $folders[] = $this->canonicalizeAndCheckFolderIdentifierAndFolderName( - $folderIdentifier, - $cloudinaryFolder['name'], - ); - } - } - - // Add result into typo3 cache to spare [API] Calls the next time... - $this->getCache()->setCachedFolders($folderIdentifier, $folders); - - return $folders; - } - - /** - * @param string $folderIdentifier - * - * @return array - */ - protected function getCloudinaryResources(string $folderIdentifier): array - { - $cloudinaryResources = []; - $cloudinaryFolder = $this->getCloudinaryPathService()->computeCloudinaryFolderPath($folderIdentifier); - if (!$cloudinaryFolder) { - $cloudinaryFolder = self::ROOT_FOLDER_IDENTIFIER . '*'; - } - // Before calling the Search API, make sure we are connected with the right cloudinary account - $this->initializeApi(); - - do { - $nextCursor = isset($response) ? $response['next_cursor'] : ''; - - $this->log( - '[API][SEARCH] Cloudinary\Search() - fetch resources from folder "%s" %s', - [$cloudinaryFolder, $nextCursor ? 'and cursor ' . $nextCursor : ''], - ['getCloudinaryResources()'], - ); - - /** @var Search $search */ - $search = new Search(); - $response = $search - ->expression('folder=' . $cloudinaryFolder) - ->sort_by('public_id', 'asc') - ->max_results(500) - ->next_cursor($nextCursor) - ->execute(); - - if (is_array($response['resources'])) { - foreach ($response['resources'] as $resource) { - // Compute file identifier - $fileIdentifier = $this->canonicalizeAndCheckFileIdentifier( - $this->getCloudinaryPathService()->computeFileIdentifier($resource), - ); - - // Compute folder identifier - #$computedFolderIdentifier = $this->canonicalizeAndCheckFolderIdentifier( - # GeneralUtility::dirname($fileIdentifier) - #); - - // We manually filter the resources belonging to the given folder to handle the "root" folder case. - #if ($computedFolderIdentifier === $folderIdentifier) { - $cloudinaryResources[$fileIdentifier] = $resource; - #} - } - } - } while (!empty($response) && array_key_exists('next_cursor', $response)); - - // Add result into typo3 cache to spare API calls next time... - $this->getCache()->setCachedFiles($folderIdentifier, $cloudinaryResources); - - return $cloudinaryResources; - } - - /** - * @param string $fileIdentifier - * - * @return array|null - */ - protected function getCloudinaryResource(string $fileIdentifier) - { - $cloudinaryResource = null; - try { - // do a double check since we have an asynchronous mechanism. - $cloudinaryPublicId = $this->getCloudinaryPathService()->computeCloudinaryPublicId($fileIdentifier); - $resourceType = $this->getCloudinaryPathService()->getResourceType($fileIdentifier); - $cloudinaryResource = (array) $this->getApi()->resource($cloudinaryPublicId, [ - 'resource_type' => $resourceType, - ]); - } catch (NotFound $e) { - return null; - } - return $cloudinaryResource; - } - - /** - * @param string $fileIdentifier - * - * @return array|false - */ - protected function getCachedCloudinaryResource(string $fileIdentifier) - { - $folderIdentifier = $this->canonicalizeAndCheckFolderIdentifier(GeneralUtility::dirname($fileIdentifier)); - - // Warm up the cache! - if (!isset($this->cachedCloudinaryResources[$folderIdentifier][$fileIdentifier])) { - $this->getFilesInFolder($folderIdentifier, 0, -1); - } - - return isset($this->cachedCloudinaryResources[$folderIdentifier][$fileIdentifier]) - ? $this->cachedCloudinaryResources[$folderIdentifier][$fileIdentifier] - : false; - } - - /** - * @return CloudinaryPathService - */ - protected function getCloudinaryPathService() - { - if (!$this->cloudinaryPathService) { - $this->cloudinaryPathService = GeneralUtility::makeInstance( - CloudinaryPathService::class, - $this->configuration, - ); - } - - return $this->cloudinaryPathService; - } - - /** - * Test the connection - */ - protected function testConnection() - { - $messageQueue = $this->getMessageQueue(); - $localizationPrefix = $this->languageFile . ':driverConfiguration.message.'; - try { - $this->initializeApi(); - - $search = new Search(); - $search->expression('folder=' . self::ROOT_FOLDER_IDENTIFIER)->execute(); - - /** @var FlashMessage $message */ - $message = GeneralUtility::makeInstance( - FlashMessage::class, - LocalizationUtility::translate($localizationPrefix . 'connectionTestSuccessful.message'), - LocalizationUtility::translate($localizationPrefix . 'connectionTestSuccessful.title'), - FlashMessage::OK, - ); - $messageQueue->addMessage($message); - } catch (\Exception $exception) { - /** @var FlashMessage $message */ - $message = GeneralUtility::makeInstance( - FlashMessage::class, - $exception->getMessage(), - LocalizationUtility::translate($localizationPrefix . 'connectionTestFailed.title'), - FlashMessage::WARNING, - ); - $messageQueue->addMessage($message); - } - } - - /** - * @return FlashMessageQueue - */ - protected function getMessageQueue() - { - /** @var FlashMessageService $flashMessageService */ - $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class); - return $flashMessageService->getMessageQueueByIdentifier(); - } - - /** - * Checks if an object exists - * - * @param string $fileIdentifier - * - * @return bool - */ - protected function resourceExists(string $fileIdentifier) - { - // Load from cache - $cloudinaryResource = $this->getCachedCloudinaryResource($fileIdentifier); - if (empty($cloudinaryResource)) { - $cloudinaryResource = $this->getCloudinaryResource($fileIdentifier); - - // If we find a cloudinary resource we had a bit of delay. - // Cloudinary is sometimes asynchronous in the way it handles files. - // In this case, we better flush the cache... - if (!empty($cloudinaryResource)) { - $this->flushFileCache(); - } - $this->log('Resource with identifier "%s" does not (yet) exist.', [$fileIdentifier], ['resourcesExists()']); - } - return !empty($cloudinaryResource); - } - - /** - * @return void - */ - protected function flushCache(): void - { - $this->flushFolderCache(); - $this->flushFileCache(); - } - - /** - * @return void - */ - protected function flushFileCache(): void - { - // Flush the file cache entries - $this->getCache()->flushFileCache(); - - $this->cachedCloudinaryResources = []; - } - - /** - * @return void - */ - protected function flushFolderCache(): void - { - // Flush the file cache entries - $this->getCache()->flushFolderCache(); - - $this->cachedFolders = []; - } - - /** - * @return void - */ - protected function initializeApi() - { - CloudinaryApiUtility::initializeByConfiguration($this->configuration); - } - - /** - * @return Api - */ - protected function getApi() - { - $this->initializeApi(); - - // The object \Cloudinary\Api behaves like a singleton object. - // The problem: if we have multiple driver instances / configuration, we don't get the expected result - // meaning we are wrongly fetching resources from other cloudinary "buckets" because of the singleton behaviour - // Therefore it is better to create a new instance upon each API call to avoid driver confusion - return new Api(); - } - - /** - * @return CloudinaryTypo3Cache|object - */ - protected function getCache() - { - if ($this->cloudinaryTypo3Cache === null) { - $this->cloudinaryTypo3Cache = GeneralUtility::makeInstance( - CloudinaryTypo3Cache::class, - (int) $this->storageUid, - ); - } - return $this->cloudinaryTypo3Cache; - } -} diff --git a/ext_localconf.php b/ext_localconf.php index e104d92..7b297c0 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -78,13 +78,6 @@ ], ]; - if (!isset($GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['cloudinary'])) { - // cache configuration, see https://docs.typo3.org/typo3cms/CoreApiReference/ApiOverview/CachingFramework/Configuration/Index.html#cache-configurations - $GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['cloudinary']['frontend'] = VariableFrontend::class; - $GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['cloudinary']['groups'] = ['all', 'cloudinary']; - $GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['cloudinary']['options']['defaultLifetime'] = 2592000; - } - // Hook for traditional file upload, replace $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_extfilefunc.php']['processData'][] = FileUploadHook::class; From 6327699bc0be14ffc17a81aad8595b010fab46c7 Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Tue, 28 Mar 2023 09:49:38 +0200 Subject: [PATCH 10/54] [TASK] Rename fast driver to cloudinary driver --- .../Form/Container/InlineCloudinaryControlContainer.php | 4 ++-- .../{CloudinaryFastDriver.php => CloudinaryDriver.php} | 5 +---- Classes/Hook/FileUploadHook.php | 4 ++-- .../Services/Extractor/CloudinaryMetaDataExtractor.php | 4 ++-- Classes/Slots/FileProcessingSlot.php | 8 ++------ 5 files changed, 9 insertions(+), 16 deletions(-) rename Classes/Driver/{CloudinaryFastDriver.php => CloudinaryDriver.php} (99%) diff --git a/Classes/Backend/Form/Container/InlineCloudinaryControlContainer.php b/Classes/Backend/Form/Container/InlineCloudinaryControlContainer.php index 5d4cac1..ed5a625 100644 --- a/Classes/Backend/Form/Container/InlineCloudinaryControlContainer.php +++ b/Classes/Backend/Form/Container/InlineCloudinaryControlContainer.php @@ -12,7 +12,7 @@ use TYPO3\CMS\Core\Resource\ResourceStorage; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Fluid\View\StandaloneView; -use Visol\Cloudinary\Driver\CloudinaryFastDriver; +use Visol\Cloudinary\Driver\CloudinaryDriver; use Visol\Cloudinary\Services\ConfigurationService; class InlineCloudinaryControlContainer extends InlineControlContainer @@ -107,7 +107,7 @@ protected function getCloudinaryStorages(): array $storageItems = $query ->select('*') ->from('sys_file_storage') - ->where($query->expr()->eq('driver', $query->expr()->literal(CloudinaryFastDriver::DRIVER_TYPE))) + ->where($query->expr()->eq('driver', $query->expr()->literal(CloudinaryDriver::DRIVER_TYPE))) ->execute() ->fetchAllAssociativeIndexed(); diff --git a/Classes/Driver/CloudinaryFastDriver.php b/Classes/Driver/CloudinaryDriver.php similarity index 99% rename from Classes/Driver/CloudinaryFastDriver.php rename to Classes/Driver/CloudinaryDriver.php index 45562e5..58f9ea1 100644 --- a/Classes/Driver/CloudinaryFastDriver.php +++ b/Classes/Driver/CloudinaryDriver.php @@ -34,10 +34,7 @@ use Visol\Cloudinary\Services\ConfigurationService; use Visol\Cloudinary\Utility\CloudinaryFileUtility; -/** - * Class CloudinaryFastDriver - */ -class CloudinaryFastDriver extends AbstractHierarchicalFilesystemDriver +class CloudinaryDriver extends AbstractHierarchicalFilesystemDriver { public const DRIVER_TYPE = 'VisolCloudinary'; const ROOT_FOLDER_IDENTIFIER = '/'; diff --git a/Classes/Hook/FileUploadHook.php b/Classes/Hook/FileUploadHook.php index 6b39e48..5eb92c6 100644 --- a/Classes/Hook/FileUploadHook.php +++ b/Classes/Hook/FileUploadHook.php @@ -7,7 +7,7 @@ use TYPO3\CMS\Core\Utility\File\ExtendedFileUtilityProcessDataHookInterface; use TYPO3\CMS\Core\Utility\GeneralUtility; use Visol\Cloudinary\Domain\Repository\ExplicitDataCacheRepository; -use Visol\Cloudinary\Driver\CloudinaryFastDriver; +use Visol\Cloudinary\Driver\CloudinaryDriver; use Visol\Cloudinary\Services\CloudinaryImageService; class FileUploadHook implements ExtendedFileUtilityProcessDataHookInterface @@ -27,7 +27,7 @@ public function processData_postProcessAction($action, array $cmdArr, array $res } /** @var File $file */ $file = $result[0][0]; - if ($file->getStorage()->getDriverType() !== CloudinaryFastDriver::DRIVER_TYPE) { + if ($file->getStorage()->getDriverType() !== CloudinaryDriver::DRIVER_TYPE) { return; } $cloudinaryImageService = GeneralUtility::makeInstance(CloudinaryImageService::class); diff --git a/Classes/Services/Extractor/CloudinaryMetaDataExtractor.php b/Classes/Services/Extractor/CloudinaryMetaDataExtractor.php index 9b4a564..ed8a554 100644 --- a/Classes/Services/Extractor/CloudinaryMetaDataExtractor.php +++ b/Classes/Services/Extractor/CloudinaryMetaDataExtractor.php @@ -12,7 +12,7 @@ use TYPO3\CMS\Core\Resource\ResourceStorage; use TYPO3\CMS\Core\Type\File\ImageInfo; use TYPO3\CMS\Core\Utility\GeneralUtility; -use Visol\Cloudinary\Driver\CloudinaryFastDriver; +use Visol\Cloudinary\Driver\CloudinaryDriver; use Visol\Cloudinary\Services\CloudinaryPathService; use Visol\Cloudinary\Services\CloudinaryResourceService; use Visol\Cloudinary\Services\ConfigurationService; @@ -38,7 +38,7 @@ public function getFileTypeRestrictions(): array */ public function getDriverRestrictions(): array { - return [CloudinaryFastDriver::DRIVER_TYPE]; + return [CloudinaryDriver::DRIVER_TYPE]; } /** diff --git a/Classes/Slots/FileProcessingSlot.php b/Classes/Slots/FileProcessingSlot.php index ca25c6e..24aca84 100644 --- a/Classes/Slots/FileProcessingSlot.php +++ b/Classes/Slots/FileProcessingSlot.php @@ -11,15 +11,11 @@ use TYPO3\CMS\Core\Resource\Service\FileProcessingService; use TYPO3\CMS\Core\Resource\Driver\DriverInterface; use TYPO3\CMS\Core\Resource\File; -use TYPO3\CMS\Core\Resource\FileInterface; use TYPO3\CMS\Core\Resource\ProcessedFile; use TYPO3\CMS\Core\Resource\ProcessedFileRepository; -use TYPO3\CMS\Core\Resource\ResourceFactory; use TYPO3\CMS\Core\Utility\GeneralUtility; -use Visol\Cloudinary\Driver\CloudinaryFastDriver; +use Visol\Cloudinary\Driver\CloudinaryDriver; use Visol\Cloudinary\Services\CloudinaryImageService; -use Visol\Cloudinary\Services\CloudinaryPathService; -use Visol\Cloudinary\Services\CloudinaryResourceService; class FileProcessingSlot { @@ -27,7 +23,7 @@ class FileProcessingSlot // We want to remove all processed files public function preFileProcess(FileProcessingService $fileProcessingService, DriverInterface $driver, ProcessedFile $processedFile, File $file, $taskType, array $configuration) { - if (!$driver instanceof CloudinaryFastDriver) { + if (!$driver instanceof CloudinaryDriver) { return; } From 53dc7a6e65df08a54e032d15d126b0c33d091e34 Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Tue, 28 Mar 2023 09:50:53 +0200 Subject: [PATCH 11/54] [TASK] Update help message for cloudinary command query --- Classes/Command/CloudinaryQueryCommand.php | 46 +++++++++++----------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/Classes/Command/CloudinaryQueryCommand.php b/Classes/Command/CloudinaryQueryCommand.php index 332f9de..98f19ff 100644 --- a/Classes/Command/CloudinaryQueryCommand.php +++ b/Classes/Command/CloudinaryQueryCommand.php @@ -22,28 +22,6 @@ use TYPO3\CMS\Core\Utility\GeneralUtility; use Visol\Cloudinary\Filters\RegularExpressionFilter; -/** - * Examples: - * - * ./vendor/bin/typo3 cloudinary:query 2 - * - * # List of files withing a folder - * ./vendor/bin/typo3 cloudinary:query 2 --path=/foo/ - * - * # List of files withing a folder with recursive flag - * ./vendor/bin/typo3 cloudinary:query 2 --path=/foo/ --recursive - * - * # List of files withing a folder with filter flag - * ./vendor/bin/typo3 cloudinary:query 2 --path=/foo/ --filter='[0-9,a-z]\.jpg' - * - * # Count files / folder - * ./vendor/bin/typo3 cloudinary:query 2 --count - * - * # List of folders instead of files - * ./vendor/bin/typo3 cloudinary:query 2 --folder - * - * Class CloudinaryQueryCommand - */ class CloudinaryQueryCommand extends AbstractCloudinaryCommand { protected ResourceStorage $storage; @@ -58,6 +36,27 @@ protected function initialize(InputInterface $input, OutputInterface $output) $this->storage = $resourceFactory->getStorageObject($input->getArgument('storage')); } + protected string $help = ' +Usage: ./vendor/bin/typo3 cloudinary:query [0-9 - storage id] + +Examples + +# List of files withing a folder +typo3 cloudinary:query 2 --path=/foo/ + +# List of files withing a folder with recursive flag +typo3 cloudinary:query 2 --path=/foo/ --recursive + +# List of files withing a folder with filter flag +typo3 cloudinary:query 2 --path=/foo/ --filter=\'[0-9,a-z]\.jpg\' + + # Count files / folder +typo3 cloudinary:query 2 --count + + # List of folders instead of files +typo3 cloudinary:query 2 --folder + ' ; + /** * Configure the command by defining the name, options and arguments */ @@ -73,9 +72,10 @@ protected function configure() ->addOption('recursive', 'r', InputOption::VALUE_NONE, 'Recursive lookup') ->addOption('delete', 'd', InputOption::VALUE_NONE, 'Delete found files / folders.') ->addArgument('storage', InputArgument::REQUIRED, 'Storage identifier') - ->setHelp('Usage: ./vendor/bin/typo3 cloudinary:query [0-9]'); + ->setHelp($this->help); } + protected function execute(InputInterface $input, OutputInterface $output): int { if (!$this->checkDriverType($this->storage)) { From 1233fbe1fe1a89e722ea41bdb3a183ca09e06c62 Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Tue, 28 Mar 2023 09:53:18 +0200 Subject: [PATCH 12/54] [TASK] Remove unnecessary php docs --- Classes/Command/CloudinaryAcceptanceTestCommand.php | 3 --- Classes/Command/CloudinaryCopyCommand.php | 3 --- Classes/Command/CloudinaryFixJpegCommand.php | 3 --- Classes/Command/CloudinaryMoveCommand.php | 3 --- Classes/Command/CloudinaryQueryCommand.php | 1 - Classes/Command/CloudinaryScanCommand.php | 3 --- 6 files changed, 16 deletions(-) diff --git a/Classes/Command/CloudinaryAcceptanceTestCommand.php b/Classes/Command/CloudinaryAcceptanceTestCommand.php index 5a5f334..5c5a606 100644 --- a/Classes/Command/CloudinaryAcceptanceTestCommand.php +++ b/Classes/Command/CloudinaryAcceptanceTestCommand.php @@ -43,9 +43,6 @@ function ($className) { } ); -/** - * Class CloudinaryAcceptanceTestCommand - */ class CloudinaryAcceptanceTestCommand extends AbstractCloudinaryCommand { /** diff --git a/Classes/Command/CloudinaryCopyCommand.php b/Classes/Command/CloudinaryCopyCommand.php index 0e8d010..ff824c8 100644 --- a/Classes/Command/CloudinaryCopyCommand.php +++ b/Classes/Command/CloudinaryCopyCommand.php @@ -21,9 +21,6 @@ use TYPO3\CMS\Core\Resource\ResourceStorage; use TYPO3\CMS\Core\Utility\GeneralUtility; -/** - * Class CloudinaryCopyCommand - */ class CloudinaryCopyCommand extends AbstractCloudinaryCommand { protected array $missingFiles = []; diff --git a/Classes/Command/CloudinaryFixJpegCommand.php b/Classes/Command/CloudinaryFixJpegCommand.php index c80898f..4d09df3 100644 --- a/Classes/Command/CloudinaryFixJpegCommand.php +++ b/Classes/Command/CloudinaryFixJpegCommand.php @@ -19,9 +19,6 @@ use TYPO3\CMS\Core\Resource\ResourceStorage; use TYPO3\CMS\Core\Utility\GeneralUtility; -/** - * Class CloudinaryFixJpegCommand - */ class CloudinaryFixJpegCommand extends AbstractCloudinaryCommand { protected ResourceStorage $targetStorage; diff --git a/Classes/Command/CloudinaryMoveCommand.php b/Classes/Command/CloudinaryMoveCommand.php index 2273295..5f71c24 100644 --- a/Classes/Command/CloudinaryMoveCommand.php +++ b/Classes/Command/CloudinaryMoveCommand.php @@ -23,9 +23,6 @@ use TYPO3\CMS\Core\Resource\ResourceFactory; use TYPO3\CMS\Core\Utility\GeneralUtility; -/** - * Class CloudinaryMoveCommand - */ class CloudinaryMoveCommand extends AbstractCloudinaryCommand { protected array $faultyUploadedFiles; diff --git a/Classes/Command/CloudinaryQueryCommand.php b/Classes/Command/CloudinaryQueryCommand.php index 98f19ff..ccb130f 100644 --- a/Classes/Command/CloudinaryQueryCommand.php +++ b/Classes/Command/CloudinaryQueryCommand.php @@ -75,7 +75,6 @@ protected function configure() ->setHelp($this->help); } - protected function execute(InputInterface $input, OutputInterface $output): int { if (!$this->checkDriverType($this->storage)) { diff --git a/Classes/Command/CloudinaryScanCommand.php b/Classes/Command/CloudinaryScanCommand.php index d5eae8c..b5df487 100644 --- a/Classes/Command/CloudinaryScanCommand.php +++ b/Classes/Command/CloudinaryScanCommand.php @@ -21,9 +21,6 @@ use TYPO3\CMS\Core\Utility\GeneralUtility; use Visol\Cloudinary\Services\CloudinaryScanService; -/** - * Class CloudinaryScanCommand - */ class CloudinaryScanCommand extends AbstractCloudinaryCommand { protected ResourceStorage $storage; From f5b10593f988c27d912ac295594d962c618c23d1 Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Tue, 28 Mar 2023 09:54:47 +0200 Subject: [PATCH 13/54] [TASK] Streamline ext_localconf --- ext_localconf.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/ext_localconf.php b/ext_localconf.php index 7b297c0..44a6663 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -8,9 +8,8 @@ use TYPO3\CMS\Core\Resource\Driver\DriverRegistry; use TYPO3\CMS\Core\Utility\GeneralUtility; use Visol\Cloudinary\Controller\CloudinaryWebHookController; -use Visol\Cloudinary\Driver\CloudinaryFastDriver; +use Visol\Cloudinary\Driver\CloudinaryDriver; use TYPO3\CMS\Core\Log\Writer\FileWriter; -use TYPO3\CMS\Core\Cache\Frontend\VariableFrontend; use Visol\Cloudinary\Hook\FileUploadHook; defined('TYPO3') || die('Access denied.'); @@ -38,8 +37,8 @@ /** @var DriverRegistry $driverRegistry */ $driverRegistry = GeneralUtility::makeInstance(DriverRegistry::class); $driverRegistry->registerDriverClass( - CloudinaryFastDriver::class, - CloudinaryFastDriver::DRIVER_TYPE, + CloudinaryDriver::class, + CloudinaryDriver::DRIVER_TYPE, \Cloudinary::class, 'FILE:EXT:cloudinary/Configuration/FlexForm/CloudinaryFlexForm.xml', ); From f22263ba831b4d9538c99bf8532f00eeecd196e2 Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Tue, 28 Mar 2023 10:48:45 +0200 Subject: [PATCH 14/54] fixup! [TASK] Update help message for cloudinary command query --- Classes/Command/CloudinaryQueryCommand.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Classes/Command/CloudinaryQueryCommand.php b/Classes/Command/CloudinaryQueryCommand.php index ccb130f..2810c68 100644 --- a/Classes/Command/CloudinaryQueryCommand.php +++ b/Classes/Command/CloudinaryQueryCommand.php @@ -42,19 +42,19 @@ protected function initialize(InputInterface $input, OutputInterface $output) Examples # List of files withing a folder -typo3 cloudinary:query 2 --path=/foo/ +typo3 cloudinary:query [0-9] --path=/foo/ # List of files withing a folder with recursive flag -typo3 cloudinary:query 2 --path=/foo/ --recursive +typo3 cloudinary:query [0-9] --path=/foo/ --recursive # List of files withing a folder with filter flag -typo3 cloudinary:query 2 --path=/foo/ --filter=\'[0-9,a-z]\.jpg\' +typo3 cloudinary:query [0-9] --path=/foo/ --filter=\'[0-9,a-z]\.jpg\' # Count files / folder -typo3 cloudinary:query 2 --count +typo3 cloudinary:query [0-9] --count # List of folders instead of files -typo3 cloudinary:query 2 --folder +typo3 cloudinary:query [0-9] --folder ' ; /** From 3ff0b6b67a109bcd3955b0ffd1aa13e65a8da3b6 Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Tue, 28 Mar 2023 10:49:05 +0200 Subject: [PATCH 15/54] [FEATURE] Introduce new typo3 command to interact with cloudinary api --- Classes/Command/CloudinaryApiCommand.php | 103 +++++++++++++++++++++++ Configuration/Services.yaml | 7 ++ 2 files changed, 110 insertions(+) create mode 100644 Classes/Command/CloudinaryApiCommand.php diff --git a/Classes/Command/CloudinaryApiCommand.php b/Classes/Command/CloudinaryApiCommand.php new file mode 100644 index 0000000..7405d8e --- /dev/null +++ b/Classes/Command/CloudinaryApiCommand.php @@ -0,0 +1,103 @@ +1d\' + ' ; + + protected function initialize(InputInterface $input, OutputInterface $output) + { + $this->io = new SymfonyStyle($input, $output); + + /** @var ResourceFactory $resourceFactory */ + $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class); + $this->storage = $resourceFactory->getStorageObject($input->getArgument('storage')); + } + + protected function configure() + { + $message = 'Interact with cloudinary API'; + $this->setDescription($message) + ->addOption('silent', 's', InputOption::VALUE_OPTIONAL, 'Mute output as much as possible', false) + ->addOption('publicId', '', InputOption::VALUE_OPTIONAL, 'Cloudinary public id', '') + ->addOption('expression', '', InputOption::VALUE_OPTIONAL, 'Cloudinary search expression', '') + ->addArgument('storage', InputArgument::REQUIRED, 'Storage identifier') + ->setHelp($this->help); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + if (!$this->checkDriverType($this->storage)) { + $this->log('Look out! Storage is not of type "cloudinary"'); + return Command::INVALID; + } + + $publicId = $input->getOption('publicId'); + $expression = $input->getOption('expression'); + + $this->initializeApi(); + try { + + if ($publicId) { + $resource = $this->getApi()->resource($publicId); + $this->log(var_export((array)$resource, true)); + } elseif ($expression) { + $search = new \Cloudinary\Search(); + $search->expression($expression); + $response = $search->execute(); + $this->log(var_export((array)$response, true)); + } else { + $this->log('Nothing to do...'); + } + } catch (\Exception $exception) { + $this->error($exception->getMessage()); + } + + return Command::SUCCESS; + } + + protected function getApi() + { + // create a new instance upon each API call to avoid driver confusion + return new Api(); + } + + protected function initializeApi(): void + { + CloudinaryApiUtility::initializeByConfiguration($this->storage->getConfiguration()); + } +} diff --git a/Configuration/Services.yaml b/Configuration/Services.yaml index 93ecc52..26a8ea3 100644 --- a/Configuration/Services.yaml +++ b/Configuration/Services.yaml @@ -7,6 +7,13 @@ services: Visol\Cloudinary\: resource: '../Classes/*' + Visol\Cloudinary\Command\CloudinaryApiCommand: + tags: + - name: 'console.command' + command: 'cloudinary:api' + schedulable: false + description: Interact with cloudinary api + Visol\Cloudinary\Command\CloudinaryCopyCommand: tags: - name: 'console.command' From f489f0fdc12716b89c22f604dda61971e82c3fc5 Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Tue, 28 Mar 2023 11:22:29 +0200 Subject: [PATCH 16/54] [TASK] Update phpstan baseline --- .gitignore | 2 + phpstan-baseline.neon | 416 ++++++++++++++++++++++-------------------- 2 files changed, 220 insertions(+), 198 deletions(-) diff --git a/.gitignore b/.gitignore index 7595791..5ecfa0d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ .prettierrc package.json yarn.lock +/public/* +/vendor/* diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 5fed779..c831dd7 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -55,31 +55,6 @@ parameters: count: 1 path: Classes/Backend/Form/Container/InlineCloudinaryControlContainer.php - - - message: "#^Call to an undefined method object\\:\\:getCache\\(\\)\\.$#" - count: 1 - path: Classes/Cache/CloudinaryTypo3Cache.php - - - - message: "#^Call to an undefined method object\\:\\:getLogger\\(\\)\\.$#" - count: 1 - path: Classes/Cache/CloudinaryTypo3Cache.php - - - - message: "#^Method Visol\\\\Cloudinary\\\\Cache\\\\CloudinaryTypo3Cache\\:\\:get\\(\\) should return array\\|false but returns mixed\\.$#" - count: 1 - path: Classes/Cache/CloudinaryTypo3Cache.php - - - - message: "#^Method Visol\\\\Cloudinary\\\\Cache\\\\CloudinaryTypo3Cache\\:\\:log\\(\\) has no return type specified\\.$#" - count: 1 - path: Classes/Cache/CloudinaryTypo3Cache.php - - - - message: "#^PHPDoc tag @return with type mixed is not subtype of native type string\\.$#" - count: 2 - path: Classes/Cache/CloudinaryTypo3Cache.php - - message: "#^Call to an undefined method object\\:\\:get\\(\\)\\.$#" count: 1 @@ -96,7 +71,7 @@ parameters: path: Classes/CloudinaryFactory.php - - message: "#^Cannot call method fetchAll\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#" + message: "#^Cannot call method fetchAllAssociative\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#" count: 1 path: Classes/Command/AbstractCloudinaryCommand.php @@ -130,16 +105,6 @@ parameters: count: 1 path: Classes/Command/AbstractCloudinaryCommand.php - - - message: "#^PHPDoc tag @return with type object is not subtype of native type Doctrine\\\\DBAL\\\\Driver\\\\Connection\\.$#" - count: 1 - path: Classes/Command/AbstractCloudinaryCommand.php - - - - message: "#^PHPDoc tag @return with type object is not subtype of native type TYPO3\\\\CMS\\\\Core\\\\Database\\\\Query\\\\QueryBuilder\\.$#" - count: 1 - path: Classes/Command/AbstractCloudinaryCommand.php - - message: "#^Parameter \\#2 \\$string of static method TYPO3\\\\CMS\\\\Core\\\\Utility\\\\GeneralUtility\\:\\:trimExplode\\(\\) expects string, mixed given\\.$#" count: 2 @@ -170,6 +135,26 @@ parameters: count: 1 path: Classes/Command/CloudinaryAcceptanceTestCommand.php + - + message: "#^Method Visol\\\\Cloudinary\\\\Command\\\\CloudinaryApiCommand\\:\\:configure\\(\\) has no return type specified\\.$#" + count: 1 + path: Classes/Command/CloudinaryApiCommand.php + + - + message: "#^Method Visol\\\\Cloudinary\\\\Command\\\\CloudinaryApiCommand\\:\\:getApi\\(\\) has no return type specified\\.$#" + count: 1 + path: Classes/Command/CloudinaryApiCommand.php + + - + message: "#^Method Visol\\\\Cloudinary\\\\Command\\\\CloudinaryApiCommand\\:\\:initialize\\(\\) has no return type specified\\.$#" + count: 1 + path: Classes/Command/CloudinaryApiCommand.php + + - + message: "#^Parameter \\#1 \\$uid of method TYPO3\\\\CMS\\\\Core\\\\Resource\\\\ResourceFactory\\:\\:getStorageObject\\(\\) expects int\\|null, mixed given\\.$#" + count: 1 + path: Classes/Command/CloudinaryApiCommand.php + - message: "#^Cannot call method exists\\(\\) on TYPO3\\\\CMS\\\\Core\\\\Resource\\\\File\\|TYPO3\\\\CMS\\\\Core\\\\Resource\\\\ProcessedFile\\|null\\.$#" count: 2 @@ -241,7 +226,7 @@ parameters: path: Classes/Command/CloudinaryCopyCommand.php - - message: "#^Cannot call method fetchAll\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#" + message: "#^Cannot call method fetchAllAssociative\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#" count: 1 path: Classes/Command/CloudinaryFixJpegCommand.php @@ -270,6 +255,41 @@ parameters: count: 1 path: Classes/Command/CloudinaryFixJpegCommand.php + - + message: "#^Call to an undefined method object\\:\\:getAllSites\\(\\)\\.$#" + count: 1 + path: Classes/Command/CloudinaryMetadataCommand.php + + - + message: "#^Cannot call method fetchAllAssociative\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#" + count: 1 + path: Classes/Command/CloudinaryMetadataCommand.php + + - + message: "#^Method Visol\\\\Cloudinary\\\\Command\\\\CloudinaryMetadataCommand\\:\\:configure\\(\\) has no return type specified\\.$#" + count: 1 + path: Classes/Command/CloudinaryMetadataCommand.php + + - + message: "#^Method Visol\\\\Cloudinary\\\\Command\\\\CloudinaryMetadataCommand\\:\\:initialize\\(\\) has no return type specified\\.$#" + count: 1 + path: Classes/Command/CloudinaryMetadataCommand.php + + - + message: "#^Parameter \\#1 \\$fileIdentifier of method Visol\\\\Cloudinary\\\\Services\\\\CloudinaryPathService\\:\\:computeCloudinaryPublicId\\(\\) expects string, mixed given\\.$#" + count: 1 + path: Classes/Command/CloudinaryMetadataCommand.php + + - + message: "#^Parameter \\#1 \\$uid of method TYPO3\\\\CMS\\\\Core\\\\Resource\\\\ResourceFactory\\:\\:getStorageObject\\(\\) expects int\\|null, mixed given\\.$#" + count: 1 + path: Classes/Command/CloudinaryMetadataCommand.php + + - + message: "#^Property Visol\\\\Cloudinary\\\\Command\\\\CloudinaryMetadataCommand\\:\\:\\$cloudinaryPathService \\(Visol\\\\Cloudinary\\\\Services\\\\CloudinaryPathService\\) does not accept object\\.$#" + count: 1 + path: Classes/Command/CloudinaryMetadataCommand.php + - message: "#^Cannot call method exists\\(\\) on TYPO3\\\\CMS\\\\Core\\\\Resource\\\\File\\|TYPO3\\\\CMS\\\\Core\\\\Resource\\\\ProcessedFile\\|null\\.$#" count: 1 @@ -300,11 +320,6 @@ parameters: count: 1 path: Classes/Command/CloudinaryMoveCommand.php - - - message: "#^PHPDoc tag @return with type object is not subtype of native type Visol\\\\Cloudinary\\\\Services\\\\FileMoveService\\.$#" - count: 1 - path: Classes/Command/CloudinaryMoveCommand.php - - message: "#^PHPDoc tag @var has invalid value \\(\\$fileObject\\)\\: Unexpected token \"\\$fileObject\", expected type at offset 10$#" count: 1 @@ -360,11 +375,6 @@ parameters: count: 1 path: Classes/Command/CloudinaryQueryCommand.php - - - message: "#^PHPDoc tag @return with type object is not subtype of native type TYPO3\\\\CMS\\\\Core\\\\Resource\\\\Folder\\.$#" - count: 1 - path: Classes/Command/CloudinaryQueryCommand.php - - message: "#^Parameter \\#1 \\$folderIdentifier of method Visol\\\\Cloudinary\\\\Command\\\\CloudinaryQueryCommand\\:\\:getFolder\\(\\) expects string, mixed given\\.$#" count: 4 @@ -461,304 +471,304 @@ parameters: path: Classes/Controller/CloudinaryScanController.php - - message: "#^Call to an undefined method object\\:\\:getPropertyFromAspect\\(\\)\\.$#" - count: 2 - path: Classes/Domain/Repository/ExplicitDataCacheRepository.php + message: "#^Call to an undefined method object\\:\\:flushCachesInGroupByTags\\(\\)\\.$#" + count: 1 + path: Classes/Controller/CloudinaryWebHookController.php - - message: "#^Cannot call method fetch\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#" + message: "#^Call to an undefined method object\\:\\:get\\(\\)\\.$#" count: 1 - path: Classes/Domain/Repository/ExplicitDataCacheRepository.php + path: Classes/Controller/CloudinaryWebHookController.php - - message: "#^Offset 'explicit_data' does not exist on array\\{options\\: mixed\\}\\.$#" + message: "#^Call to an undefined method object\\:\\:getLogger\\(\\)\\.$#" count: 1 - path: Classes/Domain/Repository/ExplicitDataCacheRepository.php + path: Classes/Controller/CloudinaryWebHookController.php - - message: "#^PHPDoc tag @return with type object is not subtype of native type TYPO3\\\\CMS\\\\Core\\\\Database\\\\Connection\\.$#" + message: "#^Call to method send\\(\\) on an unknown class Causal\\\\Cloudflare\\\\Services\\\\CloudflareService\\.$#" count: 1 - path: Classes/Domain/Repository/ExplicitDataCacheRepository.php + path: Classes/Controller/CloudinaryWebHookController.php - - message: "#^PHPDoc tag @return with type object is not subtype of native type TYPO3\\\\CMS\\\\Core\\\\Database\\\\Query\\\\QueryBuilder\\.$#" + message: "#^Cannot access offset 'to_public_id' on mixed\\.$#" count: 1 - path: Classes/Domain/Repository/ExplicitDataCacheRepository.php + path: Classes/Controller/CloudinaryWebHookController.php - - message: "#^PHPDoc tag @var has invalid value \\(\\$tableName\\)\\: Unexpected token \"\\$tableName\", expected type at offset 16$#" + message: "#^Cannot call method fetchAllAssociative\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#" count: 1 - path: Classes/Domain/Repository/ExplicitDataCacheRepository.php + path: Classes/Controller/CloudinaryWebHookController.php - - message: "#^Parameter \\#1 \\$string of function sha1 expects string, string\\|false given\\.$#" + message: "#^Class Causal\\\\Cloudflare\\\\Services\\\\CloudflareService not found\\.$#" count: 1 - path: Classes/Domain/Repository/ExplicitDataCacheRepository.php + path: Classes/Controller/CloudinaryWebHookController.php - - message: "#^Property Visol\\\\Cloudinary\\\\Domain\\\\Repository\\\\ExplicitDataCacheRepository\\:\\:\\$tableName has no type specified\\.$#" + message: "#^Constant LF not found\\.$#" count: 1 - path: Classes/Domain/Repository/ExplicitDataCacheRepository.php + path: Classes/Controller/CloudinaryWebHookController.php - - message: "#^Call to an undefined method Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:getCharsetConversion\\(\\)\\.$#" + message: "#^Method Visol\\\\Cloudinary\\\\Controller\\\\CloudinaryWebHookController\\:\\:flushCloudinaryCdn\\(\\) has parameter \\$publicId with no type specified\\.$#" count: 1 - path: Classes/Driver/CloudinaryDriver.php + path: Classes/Controller/CloudinaryWebHookController.php - - message: "#^Call to an undefined method object\\:\\:flushFileCache\\(\\)\\.$#" + message: "#^Method Visol\\\\Cloudinary\\\\Controller\\\\CloudinaryWebHookController\\:\\:getFile\\(\\) should return TYPO3\\\\CMS\\\\Core\\\\Resource\\\\File but returns TYPO3\\\\CMS\\\\Core\\\\Resource\\\\File\\|TYPO3\\\\CMS\\\\Core\\\\Resource\\\\ProcessedFile\\|null\\.$#" count: 1 - path: Classes/Driver/CloudinaryDriver.php + path: Classes/Controller/CloudinaryWebHookController.php - - message: "#^Call to an undefined method object\\:\\:flushFolderCache\\(\\)\\.$#" + message: "#^Method Visol\\\\Cloudinary\\\\Controller\\\\CloudinaryWebHookController\\:\\:getQueryBuilder\\(\\) has parameter \\$tableName with no type specified\\.$#" count: 1 - path: Classes/Driver/CloudinaryDriver.php + path: Classes/Controller/CloudinaryWebHookController.php - - message: "#^Call to an undefined method object\\:\\:getCachedFiles\\(\\)\\.$#" + message: "#^PHPDoc tag @var for variable \\$cloudflareService contains unknown class Causal\\\\Cloudflare\\\\Services\\\\CloudflareService\\.$#" count: 1 - path: Classes/Driver/CloudinaryDriver.php + path: Classes/Controller/CloudinaryWebHookController.php - - message: "#^Call to an undefined method object\\:\\:getCachedFolders\\(\\)\\.$#" + message: "#^Parameter \\#1 \\$json of method TYPO3\\\\CMS\\\\Extbase\\\\Mvc\\\\Controller\\\\ActionController\\:\\:jsonResponse\\(\\) expects string\\|null, string\\|false given\\.$#" count: 1 - path: Classes/Driver/CloudinaryDriver.php + path: Classes/Controller/CloudinaryWebHookController.php - - message: "#^Call to an undefined method object\\:\\:getLogger\\(\\)\\.$#" + message: "#^Parameter \\#1 \\$message of method Psr\\\\Log\\\\AbstractLogger\\:\\:warning\\(\\) expects string, int given\\.$#" count: 1 - path: Classes/Driver/CloudinaryDriver.php + path: Classes/Controller/CloudinaryWebHookController.php - - message: "#^Call to an undefined method object\\:\\:setCachedFiles\\(\\)\\.$#" + message: "#^Parameter \\#1 \\$payload of method Visol\\\\Cloudinary\\\\Controller\\\\CloudinaryWebHookController\\:\\:getRequestInfo\\(\\) expects array, mixed given\\.$#" count: 1 - path: Classes/Driver/CloudinaryDriver.php + path: Classes/Controller/CloudinaryWebHookController.php - - message: "#^Call to an undefined method object\\:\\:setCachedFolders\\(\\)\\.$#" + message: "#^Property Visol\\\\Cloudinary\\\\Controller\\\\CloudinaryWebHookController\\:\\:\\$cloudinaryPathService \\(Visol\\\\Cloudinary\\\\Services\\\\CloudinaryPathService\\) does not accept object\\.$#" count: 1 - path: Classes/Driver/CloudinaryDriver.php + path: Classes/Controller/CloudinaryWebHookController.php - - message: "#^Cannot access offset 'format' on array\\|false\\.$#" + message: "#^Property Visol\\\\Cloudinary\\\\Controller\\\\CloudinaryWebHookController\\:\\:\\$cloudinaryResourceService \\(Visol\\\\Cloudinary\\\\Services\\\\CloudinaryResourceService\\) does not accept object\\.$#" count: 1 - path: Classes/Driver/CloudinaryDriver.php + path: Classes/Controller/CloudinaryWebHookController.php - - message: "#^Cannot access offset 'public_id' on array\\|false\\.$#" + message: "#^Property Visol\\\\Cloudinary\\\\Controller\\\\CloudinaryWebHookController\\:\\:\\$packageManager \\(TYPO3\\\\CMS\\\\Core\\\\Package\\\\PackageManager\\) does not accept object\\.$#" count: 1 - path: Classes/Driver/CloudinaryDriver.php + path: Classes/Controller/CloudinaryWebHookController.php - - message: "#^Cannot access offset 'secure_url' on array\\|false\\.$#" + message: "#^Property Visol\\\\Cloudinary\\\\Controller\\\\CloudinaryWebHookController\\:\\:\\$processedFileRepository \\(TYPO3\\\\CMS\\\\Core\\\\Resource\\\\ProcessedFileRepository\\) does not accept object\\.$#" count: 1 - path: Classes/Driver/CloudinaryDriver.php + path: Classes/Controller/CloudinaryWebHookController.php - - message: "#^Cannot access offset 'type' on 0\\|0\\.0\\|''\\|'0'\\|array\\{\\}\\|false\\|null\\.$#" + message: "#^Property Visol\\\\Cloudinary\\\\Controller\\\\CloudinaryWebHookController\\:\\:\\$scanService \\(Visol\\\\Cloudinary\\\\Services\\\\CloudinaryScanService\\) does not accept object\\.$#" count: 1 - path: Classes/Driver/CloudinaryDriver.php + path: Classes/Controller/CloudinaryWebHookController.php - - message: "#^Cannot cast mixed to int\\.$#" + message: "#^Strict comparison using \\=\\=\\= between TYPO3\\\\CMS\\\\Core\\\\Log\\\\Logger and null will always evaluate to false\\.$#" count: 1 - path: Classes/Driver/CloudinaryDriver.php + path: Classes/Controller/CloudinaryWebHookController.php - - message: "#^Method Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:getFileContents\\(\\) should return string but returns string\\|false\\.$#" + message: "#^Strict comparison using \\=\\=\\= between int\\<0, max\\>\\|false and null will always evaluate to false\\.$#" count: 1 - path: Classes/Driver/CloudinaryDriver.php + path: Classes/Controller/CloudinaryWebHookController.php - - message: "#^Method Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:log\\(\\) has no return type specified\\.$#" - count: 1 - path: Classes/Driver/CloudinaryDriver.php + message: "#^Call to an undefined method object\\:\\:getPropertyFromAspect\\(\\)\\.$#" + count: 2 + path: Classes/Domain/Repository/ExplicitDataCacheRepository.php - - message: "#^Method Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:sanitizeFileName\\(\\) should return string but returns string\\|null\\.$#" + message: "#^Cannot call method fetchAssociative\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#" count: 1 - path: Classes/Driver/CloudinaryDriver.php + path: Classes/Domain/Repository/ExplicitDataCacheRepository.php - - message: "#^Method Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:testConnection\\(\\) has no return type specified\\.$#" + message: "#^PHPDoc tag @return with type object is not subtype of native type TYPO3\\\\CMS\\\\Core\\\\Database\\\\Connection\\.$#" count: 1 - path: Classes/Driver/CloudinaryDriver.php + path: Classes/Domain/Repository/ExplicitDataCacheRepository.php - - message: "#^Negated boolean expression is always false\\.$#" + message: "#^PHPDoc tag @return with type object is not subtype of native type TYPO3\\\\CMS\\\\Core\\\\Database\\\\Query\\\\QueryBuilder\\.$#" count: 1 - path: Classes/Driver/CloudinaryDriver.php + path: Classes/Domain/Repository/ExplicitDataCacheRepository.php - - message: "#^Parameter \\#1 \\$cloudinaryResource of method Visol\\\\Cloudinary\\\\Services\\\\CloudinaryPathService\\:\\:computeFileIdentifier\\(\\) expects array, array\\|false given\\.$#" + message: "#^PHPDoc tag @var has invalid value \\(\\$tableName\\)\\: Unexpected token \"\\$tableName\", expected type at offset 16$#" count: 1 - path: Classes/Driver/CloudinaryDriver.php + path: Classes/Domain/Repository/ExplicitDataCacheRepository.php - - message: "#^Parameter \\#1 \\$string of function rtrim expects string, string\\|null given\\.$#" - count: 1 - path: Classes/Driver/CloudinaryDriver.php + message: "#^Parameter \\#1 \\$json of function json_decode expects string, mixed given\\.$#" + count: 2 + path: Classes/Domain/Repository/ExplicitDataCacheRepository.php - - message: "#^Property Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:\\$cloudinaryPathService \\(Visol\\\\Cloudinary\\\\Services\\\\CloudinaryPathService\\) does not accept object\\.$#" + message: "#^Parameter \\#1 \\$string of function sha1 expects string, string\\|false given\\.$#" count: 1 - path: Classes/Driver/CloudinaryDriver.php + path: Classes/Domain/Repository/ExplicitDataCacheRepository.php - - message: "#^Property Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:\\$cloudinaryTypo3Cache \\(Visol\\\\Cloudinary\\\\Cache\\\\CloudinaryTypo3Cache\\) does not accept object\\.$#" + message: "#^Property Visol\\\\Cloudinary\\\\Domain\\\\Repository\\\\ExplicitDataCacheRepository\\:\\:\\$tableName has no type specified\\.$#" count: 1 - path: Classes/Driver/CloudinaryDriver.php + path: Classes/Domain/Repository/ExplicitDataCacheRepository.php - message: "#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#" count: 3 - path: Classes/Driver/CloudinaryFastDriver.php + path: Classes/Driver/CloudinaryDriver.php - message: "#^Call to an undefined method object\\:\\:computeCloudinaryFolderPath\\(\\)\\.$#" count: 9 - path: Classes/Driver/CloudinaryFastDriver.php + path: Classes/Driver/CloudinaryDriver.php - message: "#^Call to an undefined method object\\:\\:computeCloudinaryPublicId\\(\\)\\.$#" count: 9 - path: Classes/Driver/CloudinaryFastDriver.php + path: Classes/Driver/CloudinaryDriver.php - message: "#^Call to an undefined method object\\:\\:computeFileIdentifier\\(\\)\\.$#" count: 1 - path: Classes/Driver/CloudinaryFastDriver.php + path: Classes/Driver/CloudinaryDriver.php - message: "#^Call to an undefined method object\\:\\:computeFolderIdentifier\\(\\)\\.$#" count: 1 - path: Classes/Driver/CloudinaryFastDriver.php + path: Classes/Driver/CloudinaryDriver.php - message: "#^Call to an undefined method object\\:\\:delete\\(\\)\\.$#" count: 1 - path: Classes/Driver/CloudinaryFastDriver.php + path: Classes/Driver/CloudinaryDriver.php - message: "#^Call to an undefined method object\\:\\:getLogger\\(\\)\\.$#" count: 1 - path: Classes/Driver/CloudinaryFastDriver.php + path: Classes/Driver/CloudinaryDriver.php - message: "#^Call to an undefined method object\\:\\:getResourceType\\(\\)\\.$#" count: 5 - path: Classes/Driver/CloudinaryFastDriver.php + path: Classes/Driver/CloudinaryDriver.php - message: "#^Call to an undefined method object\\:\\:guessMimeType\\(\\)\\.$#" count: 1 - path: Classes/Driver/CloudinaryFastDriver.php + path: Classes/Driver/CloudinaryDriver.php - message: "#^Call to an undefined method object\\:\\:normalizeCloudinaryPath\\(\\)\\.$#" count: 2 - path: Classes/Driver/CloudinaryFastDriver.php + path: Classes/Driver/CloudinaryDriver.php - message: "#^Call to an undefined method object\\:\\:test\\(\\)\\.$#" count: 1 - path: Classes/Driver/CloudinaryFastDriver.php + path: Classes/Driver/CloudinaryDriver.php - message: "#^Cannot access offset 0 on callable\\(\\)\\: mixed\\.$#" count: 1 - path: Classes/Driver/CloudinaryFastDriver.php + path: Classes/Driver/CloudinaryDriver.php - message: "#^Cannot access offset 1 on callable\\(\\)\\: mixed\\.$#" count: 1 - path: Classes/Driver/CloudinaryFastDriver.php + path: Classes/Driver/CloudinaryDriver.php - message: "#^Cannot cast mixed to int\\.$#" count: 2 - path: Classes/Driver/CloudinaryFastDriver.php + path: Classes/Driver/CloudinaryDriver.php - - message: "#^Method Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryFastDriver\\:\\:getFileContents\\(\\) should return string but returns string\\|false\\.$#" + message: "#^Method Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:getFileContents\\(\\) should return string but returns string\\|false\\.$#" count: 1 - path: Classes/Driver/CloudinaryFastDriver.php + path: Classes/Driver/CloudinaryDriver.php - - message: "#^Method Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryFastDriver\\:\\:isFolderEmpty\\(\\) should return bool but returns int\\.$#" + message: "#^Method Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:isFolderEmpty\\(\\) should return bool but returns int\\.$#" count: 1 - path: Classes/Driver/CloudinaryFastDriver.php + path: Classes/Driver/CloudinaryDriver.php - - message: "#^Method Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryFastDriver\\:\\:log\\(\\) has no return type specified\\.$#" + message: "#^Method Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:log\\(\\) has no return type specified\\.$#" count: 1 - path: Classes/Driver/CloudinaryFastDriver.php + path: Classes/Driver/CloudinaryDriver.php - - message: "#^Method Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryFastDriver\\:\\:sanitizeFileName\\(\\) should return string but returns string\\|null\\.$#" + message: "#^Method Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:sanitizeFileName\\(\\) should return string but returns string\\|null\\.$#" count: 1 - path: Classes/Driver/CloudinaryFastDriver.php + path: Classes/Driver/CloudinaryDriver.php - message: "#^Negated boolean expression is always false\\.$#" count: 3 - path: Classes/Driver/CloudinaryFastDriver.php + path: Classes/Driver/CloudinaryDriver.php - message: "#^Offset 'extension' does not exist on array\\\\|string\\.$#" count: 1 - path: Classes/Driver/CloudinaryFastDriver.php + path: Classes/Driver/CloudinaryDriver.php - message: "#^Offset 'filename' does not exist on array\\\\|string\\.$#" count: 1 - path: Classes/Driver/CloudinaryFastDriver.php + path: Classes/Driver/CloudinaryDriver.php - message: "#^Offset 'type' does not exist on array\\{\\}\\.$#" count: 1 - path: Classes/Driver/CloudinaryFastDriver.php + path: Classes/Driver/CloudinaryDriver.php - message: "#^Parameter \\#1 \\$folder of method Visol\\\\Cloudinary\\\\Services\\\\CloudinaryFolderService\\:\\:delete\\(\\) expects string, mixed given\\.$#" count: 1 - path: Classes/Driver/CloudinaryFastDriver.php + path: Classes/Driver/CloudinaryDriver.php - message: "#^Parameter \\#1 \\$publicId of method Visol\\\\Cloudinary\\\\Services\\\\CloudinaryResourceService\\:\\:delete\\(\\) expects string, mixed given\\.$#" count: 2 - path: Classes/Driver/CloudinaryFastDriver.php + path: Classes/Driver/CloudinaryDriver.php - message: "#^Parameter \\#1 \\$string of function rtrim expects string, string\\|null given\\.$#" count: 1 - path: Classes/Driver/CloudinaryFastDriver.php + path: Classes/Driver/CloudinaryDriver.php - - message: "#^Property Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryFastDriver\\:\\:\\$charsetConversion \\(TYPO3\\\\CMS\\\\Core\\\\Charset\\\\CharsetConverter\\) does not accept object\\.$#" + message: "#^Property Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:\\$charsetConversion \\(TYPO3\\\\CMS\\\\Core\\\\Charset\\\\CharsetConverter\\) does not accept object\\.$#" count: 1 - path: Classes/Driver/CloudinaryFastDriver.php + path: Classes/Driver/CloudinaryDriver.php - - message: "#^Property Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryFastDriver\\:\\:\\$cloudinaryFolderService \\(Visol\\\\Cloudinary\\\\Services\\\\CloudinaryFolderService\\) does not accept object\\.$#" + message: "#^Property Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:\\$cloudinaryFolderService \\(Visol\\\\Cloudinary\\\\Services\\\\CloudinaryFolderService\\) does not accept object\\.$#" count: 1 - path: Classes/Driver/CloudinaryFastDriver.php + path: Classes/Driver/CloudinaryDriver.php - - message: "#^Property Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryFastDriver\\:\\:\\$cloudinaryPathService \\(Visol\\\\Cloudinary\\\\Services\\\\CloudinaryPathService\\) does not accept object\\.$#" + message: "#^Property Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:\\$cloudinaryPathService \\(Visol\\\\Cloudinary\\\\Services\\\\CloudinaryPathService\\) does not accept object\\.$#" count: 1 - path: Classes/Driver/CloudinaryFastDriver.php + path: Classes/Driver/CloudinaryDriver.php - - message: "#^Property Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryFastDriver\\:\\:\\$cloudinaryResourceService \\(Visol\\\\Cloudinary\\\\Services\\\\CloudinaryResourceService\\) does not accept object\\.$#" + message: "#^Property Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:\\$cloudinaryResourceService \\(Visol\\\\Cloudinary\\\\Services\\\\CloudinaryResourceService\\) does not accept object\\.$#" count: 1 - path: Classes/Driver/CloudinaryFastDriver.php + path: Classes/Driver/CloudinaryDriver.php - - message: "#^Property Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryFastDriver\\:\\:\\$configurationService \\(Visol\\\\Cloudinary\\\\Services\\\\ConfigurationService\\) does not accept object\\.$#" + message: "#^Property Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:\\$configurationService \\(Visol\\\\Cloudinary\\\\Services\\\\ConfigurationService\\) does not accept object\\.$#" count: 1 - path: Classes/Driver/CloudinaryFastDriver.php + path: Classes/Driver/CloudinaryDriver.php - message: "#^Method Visol\\\\Cloudinary\\\\Filters\\\\RegularExpressionFilter\\:\\:filter\\(\\) should return bool but returns int\\|true\\.$#" @@ -831,28 +841,23 @@ parameters: path: Classes/Services/CloudinaryFolderService.php - - message: "#^Cannot call method fetch\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#" + message: "#^Cannot call method fetchAllAssociative\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#" count: 1 path: Classes/Services/CloudinaryFolderService.php - - message: "#^Cannot call method fetchAll\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#" + message: "#^Cannot call method fetchAssociative\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#" count: 1 path: Classes/Services/CloudinaryFolderService.php - - message: "#^Cannot call method fetchColumn\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#" + message: "#^Cannot call method fetchOne\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#" count: 2 path: Classes/Services/CloudinaryFolderService.php - - message: "#^PHPDoc tag @return with type object is not subtype of native type Doctrine\\\\DBAL\\\\Driver\\\\Connection\\.$#" - count: 1 - path: Classes/Services/CloudinaryFolderService.php - - - - message: "#^PHPDoc tag @return with type object is not subtype of native type TYPO3\\\\CMS\\\\Core\\\\Database\\\\Query\\\\QueryBuilder\\.$#" - count: 1 + message: "#^Cannot cast mixed to int\\.$#" + count: 2 path: Classes/Services/CloudinaryFolderService.php - @@ -936,7 +941,7 @@ parameters: path: Classes/Services/CloudinaryResourceService.php - - message: "#^Cannot call method fetchAll\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#" + message: "#^Cannot call method fetchAllAssociative\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#" count: 1 path: Classes/Services/CloudinaryResourceService.php @@ -945,19 +950,14 @@ parameters: count: 1 path: Classes/Services/CloudinaryResourceService.php - - - message: "#^Cannot call method fetchColumn\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#" - count: 1 - path: Classes/Services/CloudinaryResourceService.php - - message: "#^Cannot call method fetchOne\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#" - count: 1 + count: 2 path: Classes/Services/CloudinaryResourceService.php - message: "#^Cannot cast mixed to int\\.$#" - count: 1 + count: 2 path: Classes/Services/CloudinaryResourceService.php - @@ -965,21 +965,6 @@ parameters: count: 1 path: Classes/Services/CloudinaryResourceService.php - - - message: "#^PHPDoc tag @return with type object is not subtype of native type Doctrine\\\\DBAL\\\\Driver\\\\Connection\\.$#" - count: 1 - path: Classes/Services/CloudinaryResourceService.php - - - - message: "#^PHPDoc tag @return with type object is not subtype of native type TYPO3\\\\CMS\\\\Core\\\\Database\\\\Query\\\\QueryBuilder\\.$#" - count: 1 - path: Classes/Services/CloudinaryResourceService.php - - - - message: "#^PHPDoc tag @return with type object is not subtype of native type Visol\\\\Cloudinary\\\\Services\\\\CloudinaryFolderService\\.$#" - count: 1 - path: Classes/Services/CloudinaryResourceService.php - - message: "#^Parameter \\#2 \\$timestamp of function date expects int\\|null, int\\|false given\\.$#" count: 2 @@ -991,42 +976,32 @@ parameters: path: Classes/Services/CloudinaryScanService.php - - message: "#^Cannot call method fetchColumn\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#" - count: 1 - path: Classes/Services/CloudinaryScanService.php - - - - message: "#^Method Visol\\\\Cloudinary\\\\Services\\\\CloudinaryScanService\\:\\:getCloudinaryFolderService\\(\\) should return Visol\\\\Cloudinary\\\\Services\\\\CloudinaryFolderService but returns object\\.$#" - count: 1 - path: Classes/Services/CloudinaryScanService.php - - - - message: "#^Method Visol\\\\Cloudinary\\\\Services\\\\CloudinaryScanService\\:\\:getCloudinaryResourceService\\(\\) should return Visol\\\\Cloudinary\\\\Services\\\\CloudinaryResourceService but returns object\\.$#" + message: "#^Cannot call method fetchOne\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#" count: 1 path: Classes/Services/CloudinaryScanService.php - - message: "#^Method Visol\\\\Cloudinary\\\\Services\\\\CloudinaryScanService\\:\\:log\\(\\) has no return type specified\\.$#" + message: "#^Method Visol\\\\Cloudinary\\\\Services\\\\CloudinaryScanService\\:\\:console\\(\\) has parameter \\$additionalBlankLine with no type specified\\.$#" count: 1 path: Classes/Services/CloudinaryScanService.php - - message: "#^Negated boolean expression is always false\\.$#" + message: "#^Method Visol\\\\Cloudinary\\\\Services\\\\CloudinaryScanService\\:\\:getApi\\(\\) has no return type specified\\.$#" count: 1 path: Classes/Services/CloudinaryScanService.php - - message: "#^PHPDoc tag @return with type object is not subtype of native type TYPO3\\\\CMS\\\\Core\\\\Database\\\\Query\\\\QueryBuilder\\.$#" + message: "#^Method Visol\\\\Cloudinary\\\\Services\\\\CloudinaryScanService\\:\\:getCloudinaryFolderService\\(\\) should return Visol\\\\Cloudinary\\\\Services\\\\CloudinaryFolderService but returns object\\.$#" count: 1 path: Classes/Services/CloudinaryScanService.php - - message: "#^PHPDoc tag @return with type object is not subtype of native type Visol\\\\Cloudinary\\\\Services\\\\CloudinaryFolderService\\.$#" + message: "#^Method Visol\\\\Cloudinary\\\\Services\\\\CloudinaryScanService\\:\\:getCloudinaryResourceService\\(\\) should return Visol\\\\Cloudinary\\\\Services\\\\CloudinaryResourceService but returns object\\.$#" count: 1 path: Classes/Services/CloudinaryScanService.php - - message: "#^PHPDoc tag @return with type object is not subtype of native type Visol\\\\Cloudinary\\\\Services\\\\CloudinaryResourceService\\.$#" + message: "#^Negated boolean expression is always false\\.$#" count: 1 path: Classes/Services/CloudinaryScanService.php @@ -1170,6 +1145,11 @@ parameters: count: 1 path: Classes/Utility/CloudinaryApiUtility.php + - + message: "#^Method Visol\\\\Cloudinary\\\\Utility\\\\CloudinaryFileUtility\\:\\:getTemporaryFile\\(\\) has parameter \\$storageUid with no type specified\\.$#" + count: 1 + path: Classes/Utility/CloudinaryFileUtility.php + - message: "#^Call to an undefined method TYPO3\\\\CMS\\\\Core\\\\Resource\\\\FileInterface\\:\\:getOriginalFile\\(\\)\\.$#" count: 1 @@ -1210,11 +1190,36 @@ parameters: count: 1 path: Classes/ViewHelpers/CloudinaryImageDataViewHelper.php + - + message: "#^Parameter \\#1 \\$identifier of method TYPO3Fluid\\\\Fluid\\\\Core\\\\Variables\\\\VariableProviderInterface\\:\\:add\\(\\) expects string, mixed given\\.$#" + count: 1 + path: Classes/ViewHelpers/CloudinaryImageDataViewHelper.php + + - + message: "#^Parameter \\#1 \\$identifier of method TYPO3Fluid\\\\Fluid\\\\Core\\\\Variables\\\\VariableProviderInterface\\:\\:remove\\(\\) expects string, mixed given\\.$#" + count: 1 + path: Classes/ViewHelpers/CloudinaryImageDataViewHelper.php + - message: "#^Parameter \\#1 \\$src of method TYPO3\\\\CMS\\\\Extbase\\\\Service\\\\ImageService\\:\\:getImage\\(\\) expects string, int\\|string given\\.$#" count: 1 path: Classes/ViewHelpers/CloudinaryImageDataViewHelper.php + - + message: "#^Parameter \\#1 \\$url of function parse_url expects string, mixed given\\.$#" + count: 1 + path: Classes/ViewHelpers/CloudinaryImageDataViewHelper.php + + - + message: "#^Parameter \\#2 \\$image of method TYPO3\\\\CMS\\\\Extbase\\\\Service\\\\ImageService\\:\\:getImage\\(\\) expects TYPO3\\\\CMS\\\\Core\\\\Resource\\\\FileInterface\\|TYPO3\\\\CMS\\\\Extbase\\\\Domain\\\\Model\\\\FileReference\\|null, mixed given\\.$#" + count: 1 + path: Classes/ViewHelpers/CloudinaryImageDataViewHelper.php + + - + message: "#^Parameter \\#3 \\$treatIdAsReference of method TYPO3\\\\CMS\\\\Extbase\\\\Service\\\\ImageService\\:\\:getImage\\(\\) expects bool, mixed given\\.$#" + count: 1 + path: Classes/ViewHelpers/CloudinaryImageDataViewHelper.php + - message: "#^Call to an undefined method TYPO3\\\\CMS\\\\Core\\\\Resource\\\\FileInterface\\:\\:getOriginalFile\\(\\)\\.$#" count: 1 @@ -1244,3 +1249,18 @@ parameters: message: "#^Method Visol\\\\Cloudinary\\\\ViewHelpers\\\\CloudinaryImageViewHelper\\:\\:injectImageService\\(\\) has no return type specified\\.$#" count: 1 path: Classes/ViewHelpers/CloudinaryImageViewHelper.php + + - + message: "#^Parameter \\#1 \\$src of method TYPO3\\\\CMS\\\\Extbase\\\\Service\\\\ImageService\\:\\:getImage\\(\\) expects string, mixed given\\.$#" + count: 1 + path: Classes/ViewHelpers/CloudinaryImageViewHelper.php + + - + message: "#^Parameter \\#2 \\$image of method TYPO3\\\\CMS\\\\Extbase\\\\Service\\\\ImageService\\:\\:getImage\\(\\) expects TYPO3\\\\CMS\\\\Core\\\\Resource\\\\FileInterface\\|TYPO3\\\\CMS\\\\Extbase\\\\Domain\\\\Model\\\\FileReference\\|null, mixed given\\.$#" + count: 1 + path: Classes/ViewHelpers/CloudinaryImageViewHelper.php + + - + message: "#^Parameter \\#3 \\$treatIdAsReference of method TYPO3\\\\CMS\\\\Extbase\\\\Service\\\\ImageService\\:\\:getImage\\(\\) expects bool, mixed given\\.$#" + count: 1 + path: Classes/ViewHelpers/CloudinaryImageViewHelper.php From f217dca915fcc79cd6daf0fab0833f901d88e121 Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Tue, 28 Mar 2023 11:54:06 +0200 Subject: [PATCH 17/54] [TASK] Composer update --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 5ecfa0d..a010026 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,6 @@ .prettierrc package.json yarn.lock +composer.lock /public/* /vendor/* From f6c34e4b307b92856ae7ea481b5364d66948d131 Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Tue, 28 Mar 2023 12:08:36 +0200 Subject: [PATCH 18/54] [FEATURE] Add cloudinary api query by file uid --- Classes/Command/CloudinaryApiCommand.php | 32 +++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/Classes/Command/CloudinaryApiCommand.php b/Classes/Command/CloudinaryApiCommand.php index 7405d8e..85a179c 100644 --- a/Classes/Command/CloudinaryApiCommand.php +++ b/Classes/Command/CloudinaryApiCommand.php @@ -16,9 +16,11 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; +use TYPO3\CMS\Core\Resource\File; use TYPO3\CMS\Core\Resource\ResourceFactory; use TYPO3\CMS\Core\Resource\ResourceStorage; use TYPO3\CMS\Core\Utility\GeneralUtility; +use Visol\Cloudinary\Services\CloudinaryPathService; use Visol\Cloudinary\Utility\CloudinaryApiUtility; class CloudinaryApiCommand extends AbstractCloudinaryCommand @@ -26,13 +28,16 @@ class CloudinaryApiCommand extends AbstractCloudinaryCommand protected ResourceStorage $storage; protected string $help = ' -Usage: ./vendor/bin/typo3 cloudinary:api [0-9 storage id] +Usage: ./vendor/bin/typo3 cloudinary:api [storage-uid] Examples # Query by public id typo3 cloudinary:api [0-9] --publicId=\'foo-bar\' +# Query by file uid +typo3 cloudinary:api --fileUid=\'[0-9]\' + # Query with an expression # @see https://cloudinary.com/documentation/search_api typo3 cloudinary:api [0-9] --expression=\'public_id:foo-bar\' @@ -53,9 +58,10 @@ protected function configure() $message = 'Interact with cloudinary API'; $this->setDescription($message) ->addOption('silent', 's', InputOption::VALUE_OPTIONAL, 'Mute output as much as possible', false) + ->addOption('fileUid', '', InputOption::VALUE_OPTIONAL, 'File uid', '') ->addOption('publicId', '', InputOption::VALUE_OPTIONAL, 'Cloudinary public id', '') ->addOption('expression', '', InputOption::VALUE_OPTIONAL, 'Cloudinary search expression', '') - ->addArgument('storage', InputArgument::REQUIRED, 'Storage identifier') + ->addArgument('storage', InputArgument::OPTIONAL, 'Storage identifier') ->setHelp($this->help); } @@ -69,9 +75,19 @@ protected function execute(InputInterface $input, OutputInterface $output): int $publicId = $input->getOption('publicId'); $expression = $input->getOption('expression'); + // @phpstan-ignore-next-line + $fileUid = (int)$input->getOption('fileUid'); + if ($fileUid) { + /** @var ResourceFactory $resourceFactory */ + $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class); + $file = $resourceFactory->getFileObject($fileUid); + + $this->storage = $file->getStorage(); // just to be sure + $publicId = $this->getPublicIdFromFile($file); + } + $this->initializeApi(); try { - if ($publicId) { $resource = $this->getApi()->resource($publicId); $this->log(var_export((array)$resource, true)); @@ -90,6 +106,16 @@ protected function execute(InputInterface $input, OutputInterface $output): int return Command::SUCCESS; } + protected function getPublicIdFromFile(File $file): string + { + /** @var CloudinaryPathService $cloudinaryPathService */ + $cloudinaryPathService = GeneralUtility::makeInstance( + CloudinaryPathService::class, + $file->getStorage()->getConfiguration(), + ); + return $cloudinaryPathService->computeCloudinaryPublicId($file->getIdentifier()); + } + protected function getApi() { // create a new instance upon each API call to avoid driver confusion From 343c2b6e733f653632a7ae7f0e9b6c5513d52ad2 Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Tue, 28 Mar 2023 14:37:06 +0200 Subject: [PATCH 19/54] [TASK] Better handle file rename in web hook --- .../CloudinaryWebHookController.php | 72 +++++++++++-------- 1 file changed, 41 insertions(+), 31 deletions(-) diff --git a/Classes/Controller/CloudinaryWebHookController.php b/Classes/Controller/CloudinaryWebHookController.php index 2d1960e..6da5846 100644 --- a/Classes/Controller/CloudinaryWebHookController.php +++ b/Classes/Controller/CloudinaryWebHookController.php @@ -100,19 +100,37 @@ public function processAction(): ResponseInterface foreach ($publicIds as $publicId) { - self::getLogger()->warning($publicId, ['asdf']); - if ($requestType === self::NOTIFICATION_TYPE_DELETE) { if (strpos($publicId, '_processed_') === null) { $message = sprintf('Deleted file "%s", this should not happen. A file is going to be missing.', $publicId); + self::getLogger()->warning($message); } else { - $message = sprintf('Processed file deleted "%s". Nothing to do, stopping here...', $publicId); + $message = 'Processed file deleted. Nothing to do, stopping here...'; } - self::getLogger()->warning($message); - continue; - } - $cloudinaryResource = $this->getCloudinaryResource($publicId); + // early return + return $this->sendResponse(['result' => 'ok', 'message' => $message]); + + } elseif ($requestType === self::NOTIFICATION_TYPE_RENAME) { // #. handle file rename + + // Delete the old cache resource + $this->cloudinaryResourceService->delete($publicId); + + // Fetch the new cloudinary resource + $nextPublicId = $payload['to_public_id']; + $previousCloudinaryResource = $cloudinaryResource = $this->getCloudinaryResource($nextPublicId); + + $previousCloudinaryResource['public_id'] = $publicId; + $previousFileIdentifier = $this->cloudinaryPathService->computeFileIdentifier($previousCloudinaryResource); + $nextFileIdentifier = $this->cloudinaryPathService->computeFileIdentifier($cloudinaryResource); + + $this->handleFileRename($previousFileIdentifier, $nextFileIdentifier); + } else { + $cloudinaryResource = $this->getCloudinaryResource($publicId); + + // #. flush cloudinary cdn cache only for valid publicId + $this->flushCloudinaryCdn($publicId); + } // #. retrieve the source file $file = $this->getFile($cloudinaryResource); @@ -125,30 +143,15 @@ public function processAction(): ResponseInterface // #. flush cache pages $this->clearCachePages($file); - - // #. flush cloudinary cdn cache - $this->flushCloudinaryCdn($publicId); - - // #. handle file rename - if ($requestType === self::NOTIFICATION_TYPE_RENAME) { - - // Delete the old cache resource - $this->cloudinaryResourceService->delete($publicId); - - // Rename the resource - $nextPublicId = $payload['to_public_id']; - $nextCloudinaryResource = $this->scanService->scanOne($nextPublicId); - $this->handleFileRename($file, $nextCloudinaryResource); - } } } catch (\Exception $e) { return $this->sendResponse([ - 'result' => 'ko', + 'result' => 'exception', 'message' => $e->getMessage(), ]); } - return $this->sendResponse(['result' => 'ok', 'message' => 'Cache flushed']); + return $this->sendResponse(['result' => 'ok', 'message' => 'I did my job with success!']); } protected function flushCloudflareCdn(array $tags): void @@ -166,7 +169,7 @@ protected function flushCloudflareCdn(array $tags): void $result = $cloudflareService->send( '/zones/' . $identifier . '/purge_cache', [ - 'tags' => [$tags], + 'tags' => $tags, ], 'DELETE' ); @@ -175,8 +178,15 @@ protected function flushCloudflareCdn(array $tags): void $message = vsprintf('Cleared the cache on Cloudflare using Cache-Tag (domain: "%s")', [$zoneName, implode(LF, $result['errors'])]); self::getLogger()->info($message); } else { - $message = vsprintf('Failed to clear the cache on Cloudflare using Cache-Tag (domain: "%s"): %s', [$zoneName, implode(LF, $result['errors'] ?? [])]); - self::getLogger()->warning($message); + if (is_array($result['errors'])) { + foreach ($result['errors'] as $error) { + $message = vsprintf('Failed to clear the cache on Cloudflare using Cache-Tag (domain: "%s"): code %s, %s', [$zoneName, $error['code'], $error['message']]); + self::getLogger()->warning($message); + } + } else { + $message = vsprintf('Failed to clear the cache on Cloudflare using Cache-Tag (domain: "%s")', [$zoneName]); + self::getLogger()->warning($message); + } } } catch (\RuntimeException $e) { self::getLogger()->error($e->getMessage()); @@ -197,16 +207,16 @@ protected function flushCloudinaryCdn($publicId): void ); } - protected function handleFileRename(File $file, array $cloudinaryResource): void + protected function handleFileRename(string $previousFileIdentifier, string $nextFileIdentifier): void { - $nextFileIdentifier = $this->cloudinaryPathService->computeFileIdentifier($cloudinaryResource); $tableName = 'sys_file'; $q = $this->getQueryBuilder($tableName); $q->update($tableName) ->where( - $q->expr()->eq('uid', $file->getUid()) + $q->expr()->eq('storage', $this->storage->getUid()), + $q->expr()->eq('identifier', $q->expr()->literal($previousFileIdentifier)) ) - ->set('identifier', $q->quoteIdentifier($nextFileIdentifier), false) + ->set('identifier', $q->expr()->literal($nextFileIdentifier), false) ->executeStatement(); } From 31027519c05f072401e1d99eb0023db201fdea3e Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Tue, 28 Mar 2023 14:37:38 +0200 Subject: [PATCH 20/54] [TASK] Add phpstan command in make file --- Makefile | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Makefile b/Makefile index 2a60cfe..5fcd547 100644 --- a/Makefile +++ b/Makefile @@ -50,6 +50,14 @@ lint-summary: lint-fix: phpcbf +## phpstan analyse +phpstan: + php -d memory_limit=512M ./vendor/bin/phpstan analyse -c phpstan.neon + +## phpstan adjust baseline +phpstan-baseline: + php -d memory_limit=512M ./vendor/bin/phpstan analyse -c phpstan.neon --generate-baseline + ####################### # PHPUnit ####################### From 34cce0610a8ff6740dd9fa26a91bd66dd31b17e2 Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Tue, 28 Mar 2023 15:34:27 +0200 Subject: [PATCH 21/54] [TASK] Fix CGL and introduce an event after clear cache pages --- .../CloudinaryWebHookController.php | 90 ++++++-------- Classes/Events/ClearCachePageEvent.php | 32 +++++ phpstan-baseline.neon | 110 ------------------ phpstan.neon | 5 + 4 files changed, 71 insertions(+), 166 deletions(-) create mode 100644 Classes/Events/ClearCachePageEvent.php diff --git a/Classes/Controller/CloudinaryWebHookController.php b/Classes/Controller/CloudinaryWebHookController.php index 6da5846..fd5cd85 100644 --- a/Classes/Controller/CloudinaryWebHookController.php +++ b/Classes/Controller/CloudinaryWebHookController.php @@ -9,12 +9,12 @@ * LICENSE.md file that was distributed with this source code. */ -use Causal\Cloudflare\Services\CloudflareService; +use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Http\Message\ResponseInterface; use TYPO3\CMS\Core\Cache\CacheManager; -use TYPO3\CMS\Core\Configuration\ExtensionConfiguration; use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Database\Query\QueryBuilder; +use TYPO3\CMS\Core\Exception; use TYPO3\CMS\Core\Log\Logger; use TYPO3\CMS\Core\Log\LogManager; use TYPO3\CMS\Core\Package\PackageManager; @@ -24,6 +24,7 @@ use TYPO3\CMS\Core\Resource\ResourceStorage; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Extbase\Mvc\Controller\ActionController; +use Visol\Cloudinary\Events\ClearCachePageEvent; use Visol\Cloudinary\Exceptions\CloudinaryNotFoundException; use Visol\Cloudinary\Exceptions\PublicIdMissingException; use Visol\Cloudinary\Exceptions\UnknownRequestTypeException; @@ -52,6 +53,12 @@ class CloudinaryWebHookController extends ActionController protected PackageManager $packageManager; + /** + * @var EventDispatcherInterface + */ + protected $eventDispatcher; + + protected function initializeAction(): void { $this->checkEnvironment(); @@ -60,6 +67,9 @@ protected function initializeAction(): void $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class); $storage = $resourceFactory->getStorageObject((int)$this->settings['storage']); + + $this->eventDispatcher = GeneralUtility::makeInstance(EventDispatcherInterface::class); + $this->cloudinaryResourceService = GeneralUtility::makeInstance( CloudinaryResourceService::class, $storage, @@ -85,7 +95,7 @@ protected function initializeAction(): void public function processAction(): ResponseInterface { $parsedBody = (string)file_get_contents('php://input'); - $payload = json_decode($parsedBody, true); + $payload = (array)json_decode($parsedBody, true); self::getLogger()->debug($parsedBody); if ($this->shouldStopProcessing($payload)) { @@ -101,11 +111,11 @@ public function processAction(): ResponseInterface foreach ($publicIds as $publicId) { if ($requestType === self::NOTIFICATION_TYPE_DELETE) { - if (strpos($publicId, '_processed_') === null) { + if (str_contains($publicId, '_processed_')) { + $message = 'Processed file deleted. Nothing to do, stopping here...'; + } else { $message = sprintf('Deleted file "%s", this should not happen. A file is going to be missing.', $publicId); self::getLogger()->warning($message); - } else { - $message = 'Processed file deleted. Nothing to do, stopping here...'; } // early return @@ -117,6 +127,7 @@ public function processAction(): ResponseInterface $this->cloudinaryResourceService->delete($publicId); // Fetch the new cloudinary resource + /** @var string $nextPublicId */ $nextPublicId = $payload['to_public_id']; $previousCloudinaryResource = $cloudinaryResource = $this->getCloudinaryResource($nextPublicId); @@ -151,51 +162,10 @@ public function processAction(): ResponseInterface ]); } - return $this->sendResponse(['result' => 'ok', 'message' => 'I did my job with success!']); + return $this->sendResponse(['result' => 'ok', 'message' => 'Success! I did my job.']); } - protected function flushCloudflareCdn(array $tags): void - { - $config = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('cloudflare'); - - /** @var CloudflareService $cloudflareService */ - $cloudflareService = GeneralUtility::makeInstance(CloudflareService::class, $config); - - $domains = $config['domains'] ? GeneralUtility::trimExplode(',', $config['domains'], true) : []; - - foreach ($domains as $domain) { - try { - [$identifier, $zoneName] = explode('|', $domain, 2); - $result = $cloudflareService->send( - '/zones/' . $identifier . '/purge_cache', - [ - 'tags' => $tags, - ], - 'DELETE' - ); - - if (is_array($result) && $result['success']) { - $message = vsprintf('Cleared the cache on Cloudflare using Cache-Tag (domain: "%s")', [$zoneName, implode(LF, $result['errors'])]); - self::getLogger()->info($message); - } else { - if (is_array($result['errors'])) { - foreach ($result['errors'] as $error) { - $message = vsprintf('Failed to clear the cache on Cloudflare using Cache-Tag (domain: "%s"): code %s, %s', [$zoneName, $error['code'], $error['message']]); - self::getLogger()->warning($message); - } - } else { - $message = vsprintf('Failed to clear the cache on Cloudflare using Cache-Tag (domain: "%s")', [$zoneName]); - self::getLogger()->warning($message); - } - } - } catch (\RuntimeException $e) { - self::getLogger()->error($e->getMessage()); - } - } - - } - - protected function flushCloudinaryCdn($publicId): void + protected function flushCloudinaryCdn(string $publicId): void { // Invalidate CDN cache \Cloudinary\Uploader::explicit( @@ -223,7 +193,13 @@ protected function handleFileRename(string $previousFileIdentifier, string $next protected function getFile(array $cloudinaryResource): File { $fileIdentifier = $this->cloudinaryPathService->computeFileIdentifier($cloudinaryResource); - return $this->storage->getFileByIdentifier($fileIdentifier); + /** @var File|null $file */ + $file = $this->storage->getFileByIdentifier($fileIdentifier); + + if (!$file) { + throw new Exception('No file could be fine for file identifier ' . $fileIdentifier); + } + return $file; } protected function getRequestInfo(array $payload): array @@ -297,15 +273,15 @@ protected function clearCachePages(File $file): void GeneralUtility::makeInstance(CacheManager::class) ->flushCachesInGroupByTags('pages', $tags); - // #. flush cloudinary cdn cache if extension is available - if ($this->packageManager->isPackageAvailable('cloudflare')) { - $this->flushCloudflareCdn($tags); - } + $this->eventDispatcher->dispatch( + new ClearCachePageEvent($tags) + ); } protected function findPagesWithFileReferences(File $file): array { $queryBuilder = $this->getQueryBuilder('sys_file_reference'); + // @phpstan-ignore-next-line return $queryBuilder ->select('pid') ->from('sys_file_reference') @@ -363,7 +339,7 @@ protected function isRequestDelete(mixed $payload): bool protected function sendResponse(array $data): ResponseInterface { return $this->jsonResponse( - json_encode($data) + (string)json_encode($data) ); } @@ -375,7 +351,7 @@ protected function checkEnvironment(): void } } - protected function getQueryBuilder($tableName): QueryBuilder + protected function getQueryBuilder(string $tableName): QueryBuilder { /** @var ConnectionPool $connectionPool */ $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class); @@ -386,7 +362,9 @@ protected static function getLogger(): Logger { /** @var Logger $logger */ static $logger = null; + // @phpstan-ignore-next-line if ($logger === null) { + /** @var LogManager $logger */ $logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__); } return $logger; diff --git a/Classes/Events/ClearCachePageEvent.php b/Classes/Events/ClearCachePageEvent.php new file mode 100644 index 0000000..24a347c --- /dev/null +++ b/Classes/Events/ClearCachePageEvent.php @@ -0,0 +1,32 @@ +tags = $tags; + } + + public function getTags(): array + { + return $this->tags; + } + + public function setTags(array $tags): ClearCachePageEvent + { + $this->tags = $tags; + return $this; + } + +} diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index c831dd7..2949af0 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -470,116 +470,6 @@ parameters: count: 1 path: Classes/Controller/CloudinaryScanController.php - - - message: "#^Call to an undefined method object\\:\\:flushCachesInGroupByTags\\(\\)\\.$#" - count: 1 - path: Classes/Controller/CloudinaryWebHookController.php - - - - message: "#^Call to an undefined method object\\:\\:get\\(\\)\\.$#" - count: 1 - path: Classes/Controller/CloudinaryWebHookController.php - - - - message: "#^Call to an undefined method object\\:\\:getLogger\\(\\)\\.$#" - count: 1 - path: Classes/Controller/CloudinaryWebHookController.php - - - - message: "#^Call to method send\\(\\) on an unknown class Causal\\\\Cloudflare\\\\Services\\\\CloudflareService\\.$#" - count: 1 - path: Classes/Controller/CloudinaryWebHookController.php - - - - message: "#^Cannot access offset 'to_public_id' on mixed\\.$#" - count: 1 - path: Classes/Controller/CloudinaryWebHookController.php - - - - message: "#^Cannot call method fetchAllAssociative\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#" - count: 1 - path: Classes/Controller/CloudinaryWebHookController.php - - - - message: "#^Class Causal\\\\Cloudflare\\\\Services\\\\CloudflareService not found\\.$#" - count: 1 - path: Classes/Controller/CloudinaryWebHookController.php - - - - message: "#^Constant LF not found\\.$#" - count: 1 - path: Classes/Controller/CloudinaryWebHookController.php - - - - message: "#^Method Visol\\\\Cloudinary\\\\Controller\\\\CloudinaryWebHookController\\:\\:flushCloudinaryCdn\\(\\) has parameter \\$publicId with no type specified\\.$#" - count: 1 - path: Classes/Controller/CloudinaryWebHookController.php - - - - message: "#^Method Visol\\\\Cloudinary\\\\Controller\\\\CloudinaryWebHookController\\:\\:getFile\\(\\) should return TYPO3\\\\CMS\\\\Core\\\\Resource\\\\File but returns TYPO3\\\\CMS\\\\Core\\\\Resource\\\\File\\|TYPO3\\\\CMS\\\\Core\\\\Resource\\\\ProcessedFile\\|null\\.$#" - count: 1 - path: Classes/Controller/CloudinaryWebHookController.php - - - - message: "#^Method Visol\\\\Cloudinary\\\\Controller\\\\CloudinaryWebHookController\\:\\:getQueryBuilder\\(\\) has parameter \\$tableName with no type specified\\.$#" - count: 1 - path: Classes/Controller/CloudinaryWebHookController.php - - - - message: "#^PHPDoc tag @var for variable \\$cloudflareService contains unknown class Causal\\\\Cloudflare\\\\Services\\\\CloudflareService\\.$#" - count: 1 - path: Classes/Controller/CloudinaryWebHookController.php - - - - message: "#^Parameter \\#1 \\$json of method TYPO3\\\\CMS\\\\Extbase\\\\Mvc\\\\Controller\\\\ActionController\\:\\:jsonResponse\\(\\) expects string\\|null, string\\|false given\\.$#" - count: 1 - path: Classes/Controller/CloudinaryWebHookController.php - - - - message: "#^Parameter \\#1 \\$message of method Psr\\\\Log\\\\AbstractLogger\\:\\:warning\\(\\) expects string, int given\\.$#" - count: 1 - path: Classes/Controller/CloudinaryWebHookController.php - - - - message: "#^Parameter \\#1 \\$payload of method Visol\\\\Cloudinary\\\\Controller\\\\CloudinaryWebHookController\\:\\:getRequestInfo\\(\\) expects array, mixed given\\.$#" - count: 1 - path: Classes/Controller/CloudinaryWebHookController.php - - - - message: "#^Property Visol\\\\Cloudinary\\\\Controller\\\\CloudinaryWebHookController\\:\\:\\$cloudinaryPathService \\(Visol\\\\Cloudinary\\\\Services\\\\CloudinaryPathService\\) does not accept object\\.$#" - count: 1 - path: Classes/Controller/CloudinaryWebHookController.php - - - - message: "#^Property Visol\\\\Cloudinary\\\\Controller\\\\CloudinaryWebHookController\\:\\:\\$cloudinaryResourceService \\(Visol\\\\Cloudinary\\\\Services\\\\CloudinaryResourceService\\) does not accept object\\.$#" - count: 1 - path: Classes/Controller/CloudinaryWebHookController.php - - - - message: "#^Property Visol\\\\Cloudinary\\\\Controller\\\\CloudinaryWebHookController\\:\\:\\$packageManager \\(TYPO3\\\\CMS\\\\Core\\\\Package\\\\PackageManager\\) does not accept object\\.$#" - count: 1 - path: Classes/Controller/CloudinaryWebHookController.php - - - - message: "#^Property Visol\\\\Cloudinary\\\\Controller\\\\CloudinaryWebHookController\\:\\:\\$processedFileRepository \\(TYPO3\\\\CMS\\\\Core\\\\Resource\\\\ProcessedFileRepository\\) does not accept object\\.$#" - count: 1 - path: Classes/Controller/CloudinaryWebHookController.php - - - - message: "#^Property Visol\\\\Cloudinary\\\\Controller\\\\CloudinaryWebHookController\\:\\:\\$scanService \\(Visol\\\\Cloudinary\\\\Services\\\\CloudinaryScanService\\) does not accept object\\.$#" - count: 1 - path: Classes/Controller/CloudinaryWebHookController.php - - - - message: "#^Strict comparison using \\=\\=\\= between TYPO3\\\\CMS\\\\Core\\\\Log\\\\Logger and null will always evaluate to false\\.$#" - count: 1 - path: Classes/Controller/CloudinaryWebHookController.php - - - - message: "#^Strict comparison using \\=\\=\\= between int\\<0, max\\>\\|false and null will always evaluate to false\\.$#" - count: 1 - path: Classes/Controller/CloudinaryWebHookController.php - - message: "#^Call to an undefined method object\\:\\:getPropertyFromAspect\\(\\)\\.$#" count: 2 diff --git a/phpstan.neon b/phpstan.neon index 0bca2fa..9c263a4 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -7,3 +7,8 @@ parameters: - Configuration checkMissingIterableValueType: false reportUnmatchedIgnoredErrors: false + ignoreErrors: + - + message: '#does not accept object.$#' + - + message: '#^Call to an undefined method object#' From e2f1af9705afa6fff0550fb0f425ef0f0f31180f Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Thu, 30 Mar 2023 10:33:07 +0200 Subject: [PATCH 22/54] [TASK] Improve code readability and enhance support for file renaming --- .../CloudinaryWebHookController.php | 42 +++++++++++++++---- phpstan.neon | 2 + 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/Classes/Controller/CloudinaryWebHookController.php b/Classes/Controller/CloudinaryWebHookController.php index fd5cd85..50dcce6 100644 --- a/Classes/Controller/CloudinaryWebHookController.php +++ b/Classes/Controller/CloudinaryWebHookController.php @@ -23,6 +23,7 @@ use TYPO3\CMS\Core\Resource\ResourceFactory; use TYPO3\CMS\Core\Resource\ResourceStorage; use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Core\Utility\PathUtility; use TYPO3\CMS\Extbase\Mvc\Controller\ActionController; use Visol\Cloudinary\Events\ClearCachePageEvent; use Visol\Cloudinary\Exceptions\CloudinaryNotFoundException; @@ -105,7 +106,7 @@ public function processAction(): ResponseInterface try { [$requestType, $publicIds] = $this->getRequestInfo($payload); - self::getLogger()->debug(sprintf('Start cache flushing for file "%s". ', $requestType)); + self::getLogger()->debug(sprintf('Start flushing cache for file action "%s". ', $requestType)); $this->initializeApi(); foreach ($publicIds as $publicId) { @@ -179,6 +180,9 @@ protected function flushCloudinaryCdn(string $publicId): void protected function handleFileRename(string $previousFileIdentifier, string $nextFileIdentifier): void { + $nextFolderIdentifier = PathUtility::dirname($nextFileIdentifier); + $nextFolderIdentifierHash = sha1($this->canonicalizeAndCheckFolderIdentifier($nextFolderIdentifier)); + $nextFileIdentifierHash = sha1($this->canonicalizeAndCheckFileIdentifier($nextFileIdentifier)); $tableName = 'sys_file'; $q = $this->getQueryBuilder($tableName); $q->update($tableName) @@ -187,19 +191,32 @@ protected function handleFileRename(string $previousFileIdentifier, string $next $q->expr()->eq('identifier', $q->expr()->literal($previousFileIdentifier)) ) ->set('identifier', $q->expr()->literal($nextFileIdentifier), false) + ->set('identifier_hash', $q->expr()->literal($nextFileIdentifierHash), false) + ->set('folder_hash', $q->expr()->literal($nextFolderIdentifierHash), false) + ->setMaxResults(1) ->executeStatement(); } protected function getFile(array $cloudinaryResource): File { $fileIdentifier = $this->cloudinaryPathService->computeFileIdentifier($cloudinaryResource); - /** @var File|null $file */ - $file = $this->storage->getFileByIdentifier($fileIdentifier); + $tableName = 'sys_file'; + $q = $this->getQueryBuilder($tableName); + $fileRecord = $q->select('*') + ->from($tableName) + ->where( + $q->expr()->eq('storage', $this->storage->getUid()), + $q->expr()->eq('identifier', $q->expr()->literal($fileIdentifier)) + ) + ->execute() + ->fetchAssociative(); - if (!$file) { - throw new Exception('No file could be fine for file identifier ' . $fileIdentifier); + if (!$fileRecord) { + throw new Exception('No indexed file could be fine for public id ' . $cloudinaryResource['public_id']); } - return $file; + + $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class); + return $resourceFactory->getFileObject($fileRecord['uid']); } protected function getRequestInfo(array $payload): array @@ -281,11 +298,12 @@ protected function clearCachePages(File $file): void protected function findPagesWithFileReferences(File $file): array { $queryBuilder = $this->getQueryBuilder('sys_file_reference'); + // @phpstan-ignore-next-line return $queryBuilder ->select('pid') ->from('sys_file_reference') - ->groupBy('pid') // no support for distinct + //->groupBy('pid') // no support for distinct ->andWhere( 'pid > 0', 'uid_local = ' . $file->getUid() @@ -294,6 +312,16 @@ protected function findPagesWithFileReferences(File $file): array ->fetchAllAssociative(); } + protected function canonicalizeAndCheckFileIdentifier(string $fileIdentifier): string + { + return '/' . ltrim($fileIdentifier, '/'); + } + + protected function canonicalizeAndCheckFolderIdentifier(string $folderPath): string + { + return rtrim($this->canonicalizeAndCheckFileIdentifier($folderPath), '/') . '/'; + } + /** * We only react for notification type "upload", "rename", "delete" * @see other notification types diff --git a/phpstan.neon b/phpstan.neon index 9c263a4..44b0526 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -12,3 +12,5 @@ parameters: message: '#does not accept object.$#' - message: '#^Call to an undefined method object#' + - + message: '#^Cannot call method fetch.* on Doctrine\\DBAL\\Result\|int#' From 4f5e48ff66d1aead1ca0cb67ac1ef4fa4e67a956 Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Thu, 30 Mar 2023 11:33:27 +0200 Subject: [PATCH 23/54] [TASK] Add example TypoScript configuration for Cloudinary WebHook --- .../{setup.typoscript => setup.webhook.example.typoscript} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Configuration/TypoScript/{setup.typoscript => setup.webhook.example.typoscript} (100%) diff --git a/Configuration/TypoScript/setup.typoscript b/Configuration/TypoScript/setup.webhook.example.typoscript similarity index 100% rename from Configuration/TypoScript/setup.typoscript rename to Configuration/TypoScript/setup.webhook.example.typoscript From a74b55cfd525ef52c50457a887ca77950551cf6b Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Thu, 30 Mar 2023 11:34:30 +0200 Subject: [PATCH 24/54] [CLEANUP] Remove unused import ExtensionManagementUtility --- ext_localconf.php | 1 - 1 file changed, 1 deletion(-) diff --git a/ext_localconf.php b/ext_localconf.php index 44a6663..2a5ea36 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -2,7 +2,6 @@ use TYPO3\CMS\Core\Core\Environment; use TYPO3\CMS\Core\Log\LogLevel; -use TYPO3\CMS\Core\Utility\ExtensionManagementUtility; use TYPO3\CMS\Extbase\Utility\ExtensionUtility; use Visol\Cloudinary\Backend\Form\Container\InlineCloudinaryControlContainer; use TYPO3\CMS\Core\Resource\Driver\DriverRegistry; From fd65b7b09406b88f5db668e4b59faf39b06a1c5e Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Thu, 30 Mar 2023 11:46:36 +0200 Subject: [PATCH 25/54] [TASK] Change message to use simpler language --- Classes/Controller/CloudinaryWebHookController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/Controller/CloudinaryWebHookController.php b/Classes/Controller/CloudinaryWebHookController.php index 50dcce6..ac7514a 100644 --- a/Classes/Controller/CloudinaryWebHookController.php +++ b/Classes/Controller/CloudinaryWebHookController.php @@ -163,7 +163,7 @@ public function processAction(): ResponseInterface ]); } - return $this->sendResponse(['result' => 'ok', 'message' => 'Success! I did my job.']); + return $this->sendResponse(['result' => 'ok', 'message' => 'Success! Job done.']); } protected function flushCloudinaryCdn(string $publicId): void From 287ffe85e7f07c5f9cffd7a8d574b317f84746c5 Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Thu, 30 Mar 2023 12:10:48 +0200 Subject: [PATCH 26/54] [TASK] Add message to response indicating which pages had their cache flushed --- .../Controller/CloudinaryWebHookController.php | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/Classes/Controller/CloudinaryWebHookController.php b/Classes/Controller/CloudinaryWebHookController.php index ac7514a..ba54d34 100644 --- a/Classes/Controller/CloudinaryWebHookController.php +++ b/Classes/Controller/CloudinaryWebHookController.php @@ -103,8 +103,10 @@ public function processAction(): ResponseInterface return $this->sendResponse(['result' => 'ok', 'message' => 'Nothing to do...']); } + try { [$requestType, $publicIds] = $this->getRequestInfo($payload); + $clearCachePages = []; self::getLogger()->debug(sprintf('Start flushing cache for file action "%s". ', $requestType)); $this->initializeApi(); @@ -154,7 +156,7 @@ public function processAction(): ResponseInterface $this->cleanUpTemporaryFile($file); // #. flush cache pages - $this->clearCachePages($file); + $clearCachePages = $this->clearCachePages($file); } } catch (\Exception $e) { return $this->sendResponse([ @@ -163,7 +165,10 @@ public function processAction(): ResponseInterface ]); } - return $this->sendResponse(['result' => 'ok', 'message' => 'Success! Job done.']); + $message = $clearCachePages + ? 'Success! Cache flushed for pages ' . implode(',', $clearCachePages) + : 'Success! Job done'; + return $this->sendResponse(['result' => 'ok', 'message' => $message]); } protected function flushCloudinaryCdn(string $publicId): void @@ -280,11 +285,11 @@ protected function cleanUpTemporaryFile(File $file): void } } - protected function clearCachePages(File $file): void + protected function clearCachePages(File $file): array { $tags = []; foreach ($this->findPagesWithFileReferences($file) as $page) { - $tags[] = 'pageId_' . $page['pid']; + $tags[$page['pid']] = 'pageId_' . $page['pid']; } GeneralUtility::makeInstance(CacheManager::class) @@ -293,6 +298,8 @@ protected function clearCachePages(File $file): void $this->eventDispatcher->dispatch( new ClearCachePageEvent($tags) ); + + return array_keys($tags); } protected function findPagesWithFileReferences(File $file): array From 12f8e63c052a84a5aebfed4abab948770aa78f5b Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Mon, 3 Apr 2023 17:20:12 +0200 Subject: [PATCH 27/54] [TASK] Improve result key to be a boolean --- Classes/Controller/CloudinaryWebHookController.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Classes/Controller/CloudinaryWebHookController.php b/Classes/Controller/CloudinaryWebHookController.php index ba54d34..7fd2319 100644 --- a/Classes/Controller/CloudinaryWebHookController.php +++ b/Classes/Controller/CloudinaryWebHookController.php @@ -100,7 +100,7 @@ public function processAction(): ResponseInterface self::getLogger()->debug($parsedBody); if ($this->shouldStopProcessing($payload)) { - return $this->sendResponse(['result' => 'ok', 'message' => 'Nothing to do...']); + return $this->sendResponse(['result' => true, 'message' => 'Nothing to do...']); } @@ -122,7 +122,7 @@ public function processAction(): ResponseInterface } // early return - return $this->sendResponse(['result' => 'ok', 'message' => $message]); + return $this->sendResponse(['result' => true, 'message' => $message]); } elseif ($requestType === self::NOTIFICATION_TYPE_RENAME) { // #. handle file rename @@ -160,7 +160,7 @@ public function processAction(): ResponseInterface } } catch (\Exception $e) { return $this->sendResponse([ - 'result' => 'exception', + 'result' => false, 'message' => $e->getMessage(), ]); } @@ -168,7 +168,7 @@ public function processAction(): ResponseInterface $message = $clearCachePages ? 'Success! Cache flushed for pages ' . implode(',', $clearCachePages) : 'Success! Job done'; - return $this->sendResponse(['result' => 'ok', 'message' => $message]); + return $this->sendResponse(['result' => true, 'message' => $message]); } protected function flushCloudinaryCdn(string $publicId): void From 403ab0fba3def841ff8be9d1e8b21cf6f09f27b3 Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Thu, 13 Apr 2023 09:44:40 +0200 Subject: [PATCH 28/54] [DOCS] Add configuration TCEFORM for interaction with Cloudinary --- .../backend-cloudinary-integration-01.png | Bin 0 -> 29038 bytes Documentation/driver-configuration-03.png | Bin 0 -> 9099 bytes Documentation/extension-configuration-01.png | Bin 0 -> 38179 bytes README.md | 60 ++++++++++++++++-- ext_conf_template.txt | 4 +- 5 files changed, 55 insertions(+), 9 deletions(-) create mode 100644 Documentation/backend-cloudinary-integration-01.png create mode 100644 Documentation/driver-configuration-03.png create mode 100644 Documentation/extension-configuration-01.png diff --git a/Documentation/backend-cloudinary-integration-01.png b/Documentation/backend-cloudinary-integration-01.png new file mode 100644 index 0000000000000000000000000000000000000000..6c7581b438a98e7739b4554986b358aee8676da9 GIT binary patch literal 29038 zcmeFZcT`i`*Deewq9EcC5EUsNr3)wuNa&yh=6o~(3^mQjUo_`UPDPB^p;RW zRGM@`4~WzdLlj7m{%t(J_dUve@BQ~1cZ`bz*zCR6-fPV@*Idu@%o$(k>8LR?USOo5 zp!`i5wlWP(c?{EzH9hb*ugyIJZ5kSXej1u5 zPibhjfKyMFXlT5y(azZHCDIN)}|2#j*rvO)1IL@0vyo- z|7d70&>TDsIHJ*@<^1ns16slV+(So06X`&6^gs8Q0>2Nw?g8HiZT|UvB#ZXnCuY(8 z*WE|vvyS}NF|87CH_iIBo6Eqj6OZqic+$`?u^xQW(xjxFqoJWoa{wE|jJ35uHf}Cr z)(_nt*oyhOJU(beBj*bO4qa?v*1Wzh&aR#yU-`d(-2noQ4^E5!&HL*Xn3Me9#@c$k zs%{>(yi#J<#IF6Vz{tzXE9ddh4rFlq?thvCf64#-2nKr$5*PRJ@e%X6A?D^`FMeG{ zMn?Ragt&x+C~$|Ur=Kg#+E>)o^YT9r`LE~Pw)M2}aCi)JaC7B7c&_yWH!qm{-+v#x z=)b@I=_ky=?%y}Ldj4lvzyQS$u83b3yC(iWHM8|~_}?@;xbjc4U*r1cb#ey}1DP1u zdb&A#9dt`UT0-vE8~!-?Z~y*j@bCQ)R|lBFe>FLD`fw}bf3N@hn%_?zZlLSoU<(-X zSHG_Rr(3_D`|bXN(Sh_Fd~KbLZ#%fyx_TarMMg$iPW*qo@;|mT`M2c_iEF>N{O!tr zTFQwZ%-?VG`H!>wItzG<0;8Pxe|w$+qdD6$upX6Y)Nd<;eQCep>C@OK`)xmZ--HeD zo~1kX@_0>Q;eDo0y4S9inf-;O2pl^%8bvE8>}~Cny$5#wc$YpQ7L3`O-hei3XPS9$ zP^nbc#T82{?zR;j(q~UQ`-QazR&LWa9>=G-T5p^Wka4}E5Ji$XN(PLVv( zxmFRd7jmiE5tno9{l4e-Q-2J;@C=?|~D)guGW9fvr z$nQYp#cGcK(O!urbmv6hw*TH$lQVzXn}!Z?hF4IWorCrAA#21c(uS?>m{eRjY*Yc!!iYL8T!lJysHK%5mbe-z2({Wi1MveAFI!d7#_r50aoR_n^w zo#MTdCgbV6)R*nO+96Po&X0dCWSBpi5wPOG6SCuuQ9l>`r>&nT)gK4@s&9V%_=;4J z8C;PYqc9!~S{8H~%#X70oq6U>EU8^+2=!d-skUy7n3oGNvEP}EQFMMKT2uDsvRb}H z!Y79d+M0)Krlds2&Yi$OjglH~ufcnp$?pA}b!m^i5%)+1|{ zBxdm~PE)L$Fj%lX(-^Q(19-{SMx|Z%D&IH%h>a(-M|Q5k>wL&lc(%Mu|K*|@oRQgP zvuCfo^!46!pD98TzS;?josE{=dA#ehx4F3OK;Z)3V)4LN%FW2kMbgX%??tNU64N~Y zR)lxtmUga#`XQG&elP33vR^_-BW1YKsKj(^sXy0cs=?2Db3SPzOF8@-4r*0>{)XM? zpspfGfJZU7p=b*mr!iTeow_hBLIb^U{lQcD^*&YVx60me7R8DabmwM!c?ckv^r2I5b4gxtjYcs)d`~YQ}=kS z_Qy&?7APsBN>QYZ#mtZu*`Nv7pmq?uM>W$vsdl;;aK3zNMn{|~#$ckxB`CnH`ZABC zBltm#!+46MQ;G(T^1M=4VeI-KY>v2y# zeP>S8R?1+|JNswY0b7SMiVKzM&rSzE;gt#{EdqsM+6K z78CxIW!vpX^ca5DXkCAlrnzrier0=a*;i8IH+jR-9cAYA(Ht8yKi|7<$p0z z_R!bYcd*lzDFJyH2DsA?T`GEN=F*#b)8Wi9K1&E4I!|~x`*PhvcdB@;*s`2X*VQ87 zfCnw~!MQ24!i10wypKy|x0J~D@$ZyF-oJUK@O#|qK(X&qohT?KjPNLyn~Q_F0y2)z zx$|mi>BBY~P z*z~Bof3|oYPNVF1kxEBA6e;yY>5q4!=3VV`4~4?7ys;xwBb2lserpRRC8mBB&KJ`d zF{k{mq?otfm}0_61j-zG_0-YTtuzn${eW+ydD)Gj!h|R2N*=x^jo*z zw%lL0@FJ9^FzQg28N&jdYWLYskcsH2q4Q=aqmp8iN;@Mk7<{q;wEe~4o8EVxYbH}y zQZFS>RwQRS{MMuAsY+8}9+W!tlLpnuQ%0qhjdd>e&x(5|>pe-SZcXL<+9}4sCip7I z-iz9T7|s5oc)y}ST}oA=-u8y#;BP}dEgjlGKQDa3=!;3MtA$yEulr<8rOL3{gxC?2 zD+SBn!Zla>@9+7%FRy6OR9BVY(&X#z^8Ic9`YIS(D>Z-VG;^u@>oKw4z0 z;7wcEwa+(57$W-eHC;gnS(>rE)&0zsoHo64pG1E5w-2gW8VvKxCmpxe3By$mgT9Xz z{Y1SdT%?;nET4NDb3w8*>3GPGj~r4C16Rm>!2WGWqNUH~bZcU?FtyG*SRH(ez?ZPY$9ZTcbBNl zz*S}5VU3vwf0|+Z%$|U}fIVr*Coj9ZxtLiC?AoKtLq#?K9?0PG{CvG;+!2LHksEI1 z*GXHdHEgLHkeYC|kq@A*ZyUGui8dTYrRY5X9Llv@QreavJG50FA=RG5PixexFtvH!Q7En#Sx> z$L?@g%!QX-QjHj8)s?em)L?tQ6u^1&(f!)#VC*jt^PW-U7kpIX_In6(XJ56PfF|Cw zet%RE8>U5{>VbdoO7zSt@QZSdYppd_{shF-9MH`g>Bl|NBg9()L|=ex!}sS{~A+zcIik zVCP4h%XpQ;iv?!!#`QP~X<|fNm#?!R&EqA{27rFHZIsU|37bGay(?|rTXwRa^e2`` zh3o}}I13J{w)FxtYCYkKEfIfVvJU_Zc4jWsaB)7xA0Jp<+PHky+jN9q zJ%0UE+t4mWqHS+yP1kBrsahiOs>uVlMoMDl?Vw8+1HZS_4C=_Te=}wzcK*KJpPd!- zRj>Cy1*JjU*KhH?$=jO`{XuTp?nz3Ecp~>?7;PcjKq>WH_05q;$j6nvYj6!R4yXhb18hFv0UdUeufcf~wbckE3E2fC$Ata^viBYl;Qlj)p z)8BE_q%aRYF7eKDeLv#Tzz7TV^T!+mGK5R&7g7`qm1%PZfEgzSG(fMFthe!m{Jd{Q zyFnSTBBSgSsiQVhPQ%4d;aUkl)_~PBo*-nXMT^?H?n`dma8CCc{?S$RZQnsQVe@Yy z>}k$yr6!e&@nG!!SHynHJzX&e^ADcv(tTv!a5j+dvHklw+I;5~nXVjQ^z zSr>_=&D3tbx!hURWOk%`PBVa~w#P-?WfF6Uy#c@3LD)3w3vMLWrCl`^F`x^vjq%<~ zJaso6Qxsks7pD=K+k!tr8(c1OOJc1EE)5wFYXF?DnqK+pCU#+~G$hg=sjml#3(j2a zzn(jdkf<>*c}jwON59U}%T;cLKrCA03@^nX=DcQZ{I-!kI|D2%zel?0C+5mVRl)&} zY%)%$A<`MovfV@7)CmE0WJFPk+sz`;GLW>PdVZKYV0+8_!L+^vg>Z!>+w>j0IxJ;{*yRIi^3|X_R)kN z*-`_h()2{QJV82Vz?Gs>N;0ydOTPM^DqLxYgFq>5Zx@Jt+2I=oBo3Tjznivt^Pmd1 zrgf0tZcLFgxM^U=K{EOLFNxhd{~rxjP~W*fBmUwuKG z@4Yp9k`n|gR1}*-XmQ~h`YLo3cVU?`h}pu}HUnoLI+2L~=CFFCqc6=l`w*L>j_^cb zUV~U@VIH!EF^k{_BsdD$bP% zm;ONf(2wQFkl_mA+2wAGiGJ0jo1v2lSuGOY1Y|S|SI+7nM~ji4F$kaclsend;`>v<_VFFV>=?un<~#RqFMN5n&_LPm&MbUnqv)m)BK^{x=|}+9 zzaEs!TNcSGIQG=$Qo4#S*c|iYs<~cxr$k+e(1W%*WtMX!4 z|MGibeAQ5rw)J0=wa}$ND zmJPS&r4nqDKmQiI{#iGt1(UKGprw`yN=C71gi=71x0df*-}=$UGXc=V`6PE>TNkY@ z1}I5)w9LXeLp`e$7_iO?%=aUhImZ*tTpbQ*Co>ml2p}H@DIX<7|MX=VI?F8cu+^6} zc;DG5mng~M?^XftXc?|5JYyef#g?QT&9qeZ%pi`oNXnUvQu*lDa{-lNjd*Camd zI|OQhJA2jm{sDvD1(0J?!p?y{o@5m~Rx{x$yS=wfE-@q@VWc1nhU z%O;Bq3mC`D>068FJc2goI^w$;P66_;#?@vZA5`=4tl20aM9iT+qS*F;$6lvj!ef2J z*L=zkP252kLM!{BpDOO}RN{T6gQx&sF>CavI0N>q-K9?SHtW`Y2Dr>*zILh|fMzxj z!zr$F?Qf)PKAq$QWc*AL09k7RrYchY{Bn$3#plNAOpi{XCdY=eTdgdLY)qYs0EC@3 z+X2ktI{PWAk}_=83&&|D1>q8ob_sUO@!9)qHM1 zl|}xceVY5-a^9-ZqbuR#-l!6v>879{J#^0qbt-`T0H7K}hWX90r)1`0B^c)W2mEYl z1d9giUx{Jb??zoJv0TkbjH*Ez5nF}XMbz!*&y8nX>{V-K=0`_utVaqXjX$R2cDF#Wd}xz*>T z8l}8u=Z|%=J!xuud83m zoulG?tTG+?vy_iaUv5CQN9NtPXldrAFE|+kr0k?#e`Wu3kK{c@H-90)X&c?NwVaSz zw3wtOduMxke=kXpd04Lhi8+&=!x|d9;&?#!I_KdVO7Q2XDl?ZR0qvBBHlJRX16)(# zje1!dcfjafPM0~TDx`~&l*X0?0Q`xpsmWXdkTFva1T%Q_=h+q|SVq%jZx#6C znfQs3D*t@{o4g}sjnwh3T5IU``iNyfRF=8mpNUG|y9ZrFtsC*x7Yz)Xxmh?rzIl%X z*r43q{b$x0B8&h;g<3%WlfXYpy=EQ8@}80g^V&Nbr_lyvE50fs<^Apzf$W%C!62SE zI#Mk7Z?yM%hkEjP#!d)x#-Vi6Y4K1G=1yGwXkWH!<@f_QjfhYzi%Vr?N;wTHVUsA_ zXw*@sv98+T1%rT76dtyp#sce}mDHF!OHsIR-wjB4yy!=|qP@RdA|x*z@WuT?$Nfvs zEaxfpfW~h9xN~GWcw2P-@v%cGYc?IgcUA|SjRZR^fJmo#!|a(h>eOtGe2M+m1D97} z-jrjddAydSdFd%%8%}wD*w?=t`)!K>Eu{(6Oy>b_D6RYYa&PTGi*aNs%{$3Nso*v+ z4*r=&I8geN@80+`4Xu<#Lsx#H6R@|-v;wvwX$jCoao&^eYCN$b&zoiqPH~4s6`l6{B=>=6^gcxYX;JcA2V>=L7$UBohYm3;BnhXvaT%c}hF0ujhB4o?@U6$uFyLfc%IcUCd z7a3O#s3Dd(PB@iRCuZ&-k}Owa*ajKsH#L*Qy1lQP=yEh^-dHl3^Vh+IxqA)OY_DJi z=*V;QK+L{zeF;damOpa)_Tn#fx1svfw1ldibno#ZV{szmI>ch{r3m_7N_@;AVCsHa z&;yXKE*r{cH~|}tiA!c!Kd`NDIRJ@kAtDY19p4;6`>)P*G_~HK#{z4xaJI532vCD+ zC(m%rhwm_n)$v}xdUX+yZ>zPX#;#nK+l(3y&vgHEKKOABSEJA<)ZWXq7p$(lE#AEI zRjnw}yF~MznQQ&LN3S@`?9Fs!#l{MLVhBiHuPs<7P{`BMDmWizuo)HD4)~D$PH!_R zxx26RT{~2(pkz(oiO36l#%|Q^k9xmWD;YkneIOBa(Z?{}BV&kSoYx=R9`1_L87;MH ze&B*teOH{(eFK5zCt^YpiOWvTQ{x( z{7N7nq8EISm7oi>)p_boa!lPDwTUrFm2x>VcsRMp%S|_LV2}}`4S0z#9>OvUq%d?8 z&N{I2Yn5HM`SezTdB_e6uqrnO%%y4g9M@wi1lK+tyqfEzWOy-wuv z-(c&^8}`vbK>8uShC1Un4;ZfEAcgbsY$Z@hVigqBnIN?H#?ix&pY~7|mN$d0`fI)E zLilJL-Pi#7pym856R;yU(hEb=D-Cy z>BY_BewGUVrnP=$+rSM<^h;chf;%@}S!_5%BiI}~g>9ORXVK*CRJI1P7*G`B-|Yg{@#@mZaq zX#n{>0zjwBxaCBvH^Fc2$gz`Ufb{-Mx#3Xg0~;K0AJQ!fqav{B4SII6=b<>fLdP3aLMFI$?3) zllh_3#}iDRL{D{UwbLC6TBqB!0dgoUy>JMPC}n*H$e}I&L)ybnn&SubfK*P;Lui0k z&kh)u1Z%}16iIv3gOyjXw>zr-@S6oI0XCU@!sJhC2#oaqFovv#&rh!&N>0**P6BmN zo}M;hhlxk$Re*llshmBq-tWd#as|d#Uw@tB&(kz?wgB-Jzj2A_&^#;gN&{n?oP5jl zCz_xMl>mGwDLIz@_opAcEH)Jwo2%>R|AqD^A0(^)f0~9u_=I!OVv1r&6Og}^rQD17 zk7p_&Sl`O}ptUpP#tt|UVOSn5VxD`K+01VqUIJ7=xm(;B{x$L}x&z6FH5U3MV;1$# zbQaRk($4{#NhJv2G4E<2F{Q?3_d|C9NIndNujTRTid7WFxpp)@Hx(Q_sap(B6rQ0zVpkvwjJ+whbHtJJ*-rrACH121|$wl zvf7otOE^9U<&{$29PE{|!F!K))R2d}saw4v&M2TnEVlJjWq)RH%CxB1?7&I`Q7mBQ zBS%+V!lWIv%%4OVnLuO7uK117R_{q$4q`*Ml=n85&rkY2AUN0fE%tQna{2$9c_9v{ z%;F_&mH}g@WXa9}8{Y`&ZgqtGFuO3ht)H{r@01xfNn<5yEE68Y5>g$ju2!+9m>atJ z$6aFmbg)Uc{}?-T6o{m6aaEgXOF-7RtV^tUQrv)}j^s1iV(CZ01qR=-Or$K?#BAC) zet|>JnoU`3OwLA5104TjM8Q^;&D!W}Oz$QgceO_K&Qe*bTO$sp@FHM&qBL_OV3|-b z+k}L8>>8wtdvR|8S@+8EDcNHzHA$AiGAM`2bitm*Q0Ll9AiQmog`L_(_uF5|35Gi5 zK;6?7&wWy$Y zQDHm5>&@rP+;=5nh7M6UXl=8(X!@(YZL1K4iwkJ=XZPSUa(bpo2WrU{#tG}FBEQsg zz80fZi8WvF2Fk%Su!8xzmrswRJaaIrHDET%CjiBJ5BGQ0VuH5D>}v@8sbD0fcd>Ih z#cN^Aeu5VLZnp4pOck(Wj{)M8^%0A{TfsX(y0pP>ycE?Q@B}QPKelqBRcc9Y^FzGu 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 An*aa+ literal 0 HcmV?d00001 diff --git a/Documentation/driver-configuration-03.png b/Documentation/driver-configuration-03.png new file mode 100644 index 0000000000000000000000000000000000000000..4c54e79054f340e6817b6069e40aeed043b2084c GIT binary patch literal 9099 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 Xf0%IahGF30pDHpEisEH&i~{}#RiSm6 literal 0 HcmV?d00001 diff --git a/Documentation/extension-configuration-01.png b/Documentation/extension-configuration-01.png new file mode 100644 index 0000000000000000000000000000000000000000..af213bb920c9549878bf6e6af5cc925a357465ab GIT binary patch literal 38179 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 Date: Fri, 14 Apr 2023 17:29:07 +0200 Subject: [PATCH 29/54] [TASK] Indentation and return type --- .../Container/InlineCloudinaryControlContainer.php | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/Classes/Backend/Form/Container/InlineCloudinaryControlContainer.php b/Classes/Backend/Form/Container/InlineCloudinaryControlContainer.php index ed5a625..0e9dbd5 100644 --- a/Classes/Backend/Form/Container/InlineCloudinaryControlContainer.php +++ b/Classes/Backend/Form/Container/InlineCloudinaryControlContainer.php @@ -18,8 +18,8 @@ class InlineCloudinaryControlContainer extends InlineControlContainer { - public function render() { - + public function render() + { // We load here the cloudinary library /** @var AssetCollector $assetCollector */ $assetCollector = GeneralUtility::makeInstance(AssetCollector::class); @@ -32,11 +32,7 @@ public function render() { return parent::render(); } - /** - * @param array $inlineConfiguration - * @return string - */ - protected function renderPossibleRecordsSelectorTypeGroupDB(array $inlineConfiguration) + protected function renderPossibleRecordsSelectorTypeGroupDB(array $inlineConfiguration): string { $typo3Buttons = parent::renderPossibleRecordsSelectorTypeGroupDB($inlineConfiguration); From 2c3bb79fab4f097c00dded542a0b68875c434c97 Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Fri, 14 Apr 2023 17:31:34 +0200 Subject: [PATCH 30/54] [TASK] Improve console log message --- .../Public/JavaScript/CloudinaryMediaLibrary.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Resources/Public/JavaScript/CloudinaryMediaLibrary.js b/Resources/Public/JavaScript/CloudinaryMediaLibrary.js index 5de0398..bef168a 100644 --- a/Resources/Public/JavaScript/CloudinaryMediaLibrary.js +++ b/Resources/Public/JavaScript/CloudinaryMediaLibrary.js @@ -37,7 +37,8 @@ define([ // Detect if the "toggle" irre is ready function isEditIrreElementReady(element) { - // Detect if the element is ready to be used + + // Detect if the element is ready to be initialized const childElement = $(element).parents('div[data-object-uid]').find('.panel-collapse .tab-content') if (childElement.length) { clearTimeout(irreToggleTimout) @@ -48,13 +49,16 @@ define([ } function initializeCloudinaryButtons () { + $('.btn-cloudinary-media-library[data-is-initialized="0"]').map((index, element) => { + const cloudinaryCredentials = Array.isArray($(element).data('cloudinaryCredentials')) ? $(element).data('cloudinaryCredentials') : [] cloudinaryCredentials.map((credential) => { - // Render the "select image or video" button + + // Render the cloudinary button const mediaLibrary = cloudinary.createMediaLibrary( { cloud_name: credential.cloudName, @@ -75,9 +79,7 @@ define([ // search: { expression: 'resource_type:image' }, // todo we could have video, how to filter _processed_file }, { - // showHandler: function () {}, insertHandler: function (data) { - console.log(NProgress) NProgress.start(); const me = this; @@ -130,7 +132,7 @@ define([ // We update the "initialized" flag so that we don't have many buttons initialized $(element).attr('data-is-initialized', "1") - console.log('Cloudinary button initialized!') + console.log('Cloudinary button initialized for field id #' + $(element).attr('id')) }) } From 2c94f4c6f351765da9f8efa2feaff20d23f67518 Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Fri, 5 May 2023 14:56:34 +0200 Subject: [PATCH 31/54] [BUGFIX] Make CloudinaryPathService nullable --- Classes/Services/CloudinaryScanService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/Services/CloudinaryScanService.php b/Classes/Services/CloudinaryScanService.php index a2b44a2..9ef7813 100644 --- a/Classes/Services/CloudinaryScanService.php +++ b/Classes/Services/CloudinaryScanService.php @@ -35,7 +35,7 @@ class CloudinaryScanService protected ResourceStorage $storage; - protected CloudinaryPathService $cloudinaryPathService; + protected ?CloudinaryPathService $cloudinaryPathService = null; protected string $processedFolder = '_processed_'; From 1495898cab4abf4a89f41c913ede1199668bcabd Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Fri, 5 May 2023 14:57:21 +0200 Subject: [PATCH 32/54] [TASK] Change error message to warning message --- Classes/Command/CloudinaryScanCommand.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Classes/Command/CloudinaryScanCommand.php b/Classes/Command/CloudinaryScanCommand.php index b5df487..ca9fea4 100644 --- a/Classes/Command/CloudinaryScanCommand.php +++ b/Classes/Command/CloudinaryScanCommand.php @@ -59,8 +59,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int $numberOfFiles = $result['created'] + $result['updated'] - $result['deleted']; if ($numberOfFiles !== $result['total']) { - $this->error( - 'Something went wrong. There is a problem with the number of files counted. %s !== %s. It should be fixed in the next scan', + $this->warning( + 'There is a problem with the number of files counted. %s !== %s. It should be fixed in the next scan', [$numberOfFiles, $result['total']], ); } From 3378ef99487d258a1200029de2c964306aaf485a Mon Sep 17 00:00:00 2001 From: Daniel Huf Date: Fri, 5 May 2023 16:03:54 +0200 Subject: [PATCH 33/54] fix: Only add extension for raw resources --- Classes/Services/CloudinaryPathService.php | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Classes/Services/CloudinaryPathService.php b/Classes/Services/CloudinaryPathService.php index a61c42d..6f73701 100644 --- a/Classes/Services/CloudinaryPathService.php +++ b/Classes/Services/CloudinaryPathService.php @@ -41,14 +41,12 @@ public function __construct(array $storageConfiguration) */ public function computeFileIdentifier(array $cloudinaryResource): string { - $fileParts = PathUtility::pathinfo($cloudinaryResource['public_id']); - - $extension = isset($fileParts['extension']) - ? '' // We don't need the extension since it is already included in the public_id (resource_type => "raw") - : '.' . $cloudinaryResource['format']; + $fileIdentifier = $cloudinaryResource['resource_type'] === 'raw' + ? $cloudinaryResource['public_id'] + : $cloudinaryResource['public_id'] . '.' . $cloudinaryResource['format']; return self::stripBasePathFromIdentifier( - DIRECTORY_SEPARATOR . $cloudinaryResource['public_id'] . $extension, + DIRECTORY_SEPARATOR . $fileIdentifier, $this->getBasePath() ); } From 1df0377618e0892925c9e3c5b15877e9d6ecb9e6 Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Mon, 8 May 2023 17:45:42 +0200 Subject: [PATCH 34/54] [REFACTOR] Better computing of cloudinary public id --- Classes/Command/CloudinaryApiCommand.php | 2 +- Classes/Command/CloudinaryMetadataCommand.php | 6 +- Classes/Command/CloudinaryScanCommand.php | 17 +- .../CloudinaryWebHookController.php | 2 +- Classes/Driver/CloudinaryDriver.php | 8 +- .../AbstractCloudinaryMediaService.php | 2 +- Classes/Services/CloudinaryPathService.php | 165 +++++------------- Classes/Services/CloudinaryScanService.php | 6 +- .../Extractor/CloudinaryMetaDataExtractor.php | 2 +- .../CloudinaryImageDataViewHelper.php | 2 +- 10 files changed, 76 insertions(+), 136 deletions(-) diff --git a/Classes/Command/CloudinaryApiCommand.php b/Classes/Command/CloudinaryApiCommand.php index 85a179c..9e8de84 100644 --- a/Classes/Command/CloudinaryApiCommand.php +++ b/Classes/Command/CloudinaryApiCommand.php @@ -111,7 +111,7 @@ protected function getPublicIdFromFile(File $file): string /** @var CloudinaryPathService $cloudinaryPathService */ $cloudinaryPathService = GeneralUtility::makeInstance( CloudinaryPathService::class, - $file->getStorage()->getConfiguration(), + $file->getStorage(), ); return $cloudinaryPathService->computeCloudinaryPublicId($file->getIdentifier()); } diff --git a/Classes/Command/CloudinaryMetadataCommand.php b/Classes/Command/CloudinaryMetadataCommand.php index 4b532b9..b9ba892 100644 --- a/Classes/Command/CloudinaryMetadataCommand.php +++ b/Classes/Command/CloudinaryMetadataCommand.php @@ -31,7 +31,7 @@ class CloudinaryMetadataCommand extends AbstractCloudinaryCommand protected CloudinaryPathService $cloudinaryPathService; - protected function initialize(InputInterface $input, OutputInterface $output) + protected function initialize(InputInterface $input, OutputInterface $output): void { $this->io = new SymfonyStyle($input, $output); @@ -41,11 +41,11 @@ protected function initialize(InputInterface $input, OutputInterface $output) $this->cloudinaryPathService = GeneralUtility::makeInstance( CloudinaryPathService::class, - $this->storage->getConfiguration(), + $this->storage, ); } - protected function configure() + protected function configure(): void { $message = 'Set metadata on cloudinary resources such as file reference and file usage.'; $this->setDescription($message) diff --git a/Classes/Command/CloudinaryScanCommand.php b/Classes/Command/CloudinaryScanCommand.php index ca9fea4..b1cb26d 100644 --- a/Classes/Command/CloudinaryScanCommand.php +++ b/Classes/Command/CloudinaryScanCommand.php @@ -25,7 +25,7 @@ class CloudinaryScanCommand extends AbstractCloudinaryCommand { protected ResourceStorage $storage; - protected function initialize(InputInterface $input, OutputInterface $output) + protected function initialize(InputInterface $input, OutputInterface $output): void { $this->io = new SymfonyStyle($input, $output); @@ -34,11 +34,18 @@ protected function initialize(InputInterface $input, OutputInterface $output) $this->storage = $resourceFactory->getStorageObject($input->getArgument('storage')); } - protected function configure() + protected function configure(): void { $message = 'Scan and warm up a cloudinary storage.'; $this->setDescription($message) ->addOption('silent', 's', InputOption::VALUE_OPTIONAL, 'Mute output as much as possible', false) + ->addOption( + 'expression', + '', + InputOption::VALUE_OPTIONAL, + 'Expression used by the cloudinary search api (e.g --expression="folder=fileadmin/* AND NOT folder=fileadmin/_processed_/*', + false + ) ->addArgument('storage', InputArgument::REQUIRED, 'Storage identifier') ->setHelp('Usage: ./vendor/bin/typo3 cloudinary:scan [0-9]'); } @@ -55,7 +62,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->log('tail -f ' . $logFile); $this->log(); - $result = $this->getCloudinaryScanService()->scan(); + $expression = $input->getOption('expression'); + + $result = $this->getCloudinaryScanService() + ->setAdditionalExpression($expression) + ->scan(); $numberOfFiles = $result['created'] + $result['updated'] - $result['deleted']; if ($numberOfFiles !== $result['total']) { diff --git a/Classes/Controller/CloudinaryWebHookController.php b/Classes/Controller/CloudinaryWebHookController.php index 7fd2319..20cf259 100644 --- a/Classes/Controller/CloudinaryWebHookController.php +++ b/Classes/Controller/CloudinaryWebHookController.php @@ -83,7 +83,7 @@ protected function initializeAction(): void $this->cloudinaryPathService = GeneralUtility::makeInstance( CloudinaryPathService::class, - $storage->getConfiguration() + $storage ); $this->storage = $storage; diff --git a/Classes/Driver/CloudinaryDriver.php b/Classes/Driver/CloudinaryDriver.php index 58f9ea1..736db42 100644 --- a/Classes/Driver/CloudinaryDriver.php +++ b/Classes/Driver/CloudinaryDriver.php @@ -1251,9 +1251,15 @@ protected function canonicalizeFolderIdentifierAndFileName(string $folderIdentif protected function getCloudinaryPathService() { if (!$this->cloudinaryPathService) { + if ($this->storageUid) { + $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class); + $storage = $resourceFactory->getStorageObject($this->storageUid); + } $this->cloudinaryPathService = GeneralUtility::makeInstance( CloudinaryPathService::class, - $this->configuration, + $this->storageUid + ? $storage + : $this->configuration, ); } diff --git a/Classes/Services/AbstractCloudinaryMediaService.php b/Classes/Services/AbstractCloudinaryMediaService.php index fcbf0ae..fae90b6 100644 --- a/Classes/Services/AbstractCloudinaryMediaService.php +++ b/Classes/Services/AbstractCloudinaryMediaService.php @@ -91,7 +91,7 @@ protected function getCloudinaryPathService(ResourceStorage $storage) { return GeneralUtility::makeInstance( CloudinaryPathService::class, - $storage->getConfiguration() + $storage ); } diff --git a/Classes/Services/CloudinaryPathService.php b/Classes/Services/CloudinaryPathService.php index 6f73701..35f01bc 100644 --- a/Classes/Services/CloudinaryPathService.php +++ b/Classes/Services/CloudinaryPathService.php @@ -9,36 +9,28 @@ * LICENSE.md file that was distributed with this source code. */ +use TYPO3\CMS\Core\Resource\ResourceStorage; +use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\Utility\PathUtility; -/** - * Class CloudinaryPathService - */ class CloudinaryPathService { + protected ?ResourceStorage $storage; - /** - * @var array - */ - protected $storageConfiguration; + protected array $storageConfiguration; - /** - * CloudinaryPathService constructor. - * - * @param array $storageConfiguration - */ - public function __construct(array $storageConfiguration) + protected array $cachedCloudinaryResources = []; + + public function __construct(array|ResourceStorage $storageObjectOrConfiguration) { - $this->storageConfiguration = $storageConfiguration; + if ($storageObjectOrConfiguration instanceof ResourceStorage) { + $this->storage = $storageObjectOrConfiguration; + $this->storageConfiguration = $this->storage->getConfiguration(); + } else { + $this->storageConfiguration = $storageObjectOrConfiguration; + } } - /** - * Cloudinary to FAL identifier - * - * @param array $cloudinaryResource - * - * @return string - */ public function computeFileIdentifier(array $cloudinaryResource): string { $fileIdentifier = $cloudinaryResource['resource_type'] === 'raw' @@ -51,11 +43,6 @@ public function computeFileIdentifier(array $cloudinaryResource): string ); } - /** - * @param string $cloudinaryFolder - * - * @return string - */ public function computeFolderIdentifier(string $cloudinaryFolder): string { return self::stripBasePathFromIdentifier( @@ -67,7 +54,6 @@ public function computeFolderIdentifier(string $cloudinaryFolder): string /** * Return the basePath. * The basePath never has a trailing slash - * @return string */ protected function getBasePath(): string { @@ -77,39 +63,17 @@ protected function getBasePath(): string : ''; } - /** - * FAL to Cloudinary identifier - * - * @param string $fileIdentifier - * - * @return string - */ public function computeCloudinaryPublicId(string $fileIdentifier): string { - $normalizedFileIdentifier = $this->guessIsImage($fileIdentifier) || $this->guessIsVideo($fileIdentifier) - ? $this->stripExtension($fileIdentifier) - : $fileIdentifier; - - return $this->normalizeCloudinaryPath($normalizedFileIdentifier); + $cloudinaryResource = $this->getCloudinaryResource($fileIdentifier); + return $this->normalizeCloudinaryPath($cloudinaryResource['public_id']); } - /** - * FAL to Cloudinary identifier - * - * @param string $folderIdentifier - * - * @return string - */ public function computeCloudinaryFolderPath(string $folderIdentifier): string { return $this->normalizeCloudinaryPath($folderIdentifier); } - /** - * @param string $cloudinaryPath - * - * @return string - */ public function normalizeCloudinaryPath(string $cloudinaryPath): string { $normalizedCloudinaryPath = trim($cloudinaryPath, DIRECTORY_SEPARATOR); @@ -119,40 +83,17 @@ public function normalizeCloudinaryPath(string $cloudinaryPath): string : $normalizedCloudinaryPath; } - /** - * @param array $fileInfo - * - * @return string - */ public function getMimeType(array $fileInfo): string { - return isset($fileInfo['mime_type']) - ? $fileInfo['mime_type'] - : ''; + return $fileInfo['mime_type'] ?? ''; } - /** - * @param string $fileIdentifier - * - * @return string - */ public function getResourceType(string $fileIdentifier): string { - $resourceType = 'raw'; - if ($this->guessIsImage($fileIdentifier)) { - $resourceType = 'image'; - } elseif ($this->guessIsVideo($fileIdentifier)) { - $resourceType = 'video'; - } - - return $resourceType; + $cloudinaryResource = $this->getCloudinaryResource($fileIdentifier); + return $cloudinaryResource['resource_type'] ?? 'unknown'; } - /** - * @param array $cloudinaryResource - * - * @return string - */ public function guessMimeType(array $cloudinaryResource): string { $mimeType = ''; @@ -168,54 +109,36 @@ public function guessMimeType(array $cloudinaryResource): string return $mimeType; } - /** - * @param string $fileIdentifier - * - * @return bool - */ - protected function guessIsVideo(string $fileIdentifier) + protected function getCloudinaryResource(string $fileIdentifier): array { - $extension = strtolower(PathUtility::pathinfo($fileIdentifier, PATHINFO_EXTENSION)); - $rawExtensions = [ - 'mp4', - 'mov', + $possiblePublicId = $this->stripExtension($fileIdentifier); - 'mp3', // As documented @see https://cloudinary.com/documentation/image_upload_api_reference - ]; + // We cache the resource for performance reasons. + if (!isset($this->cachedCloudinaryResources[$possiblePublicId])) { - return in_array($extension, $rawExtensions, true); - } + // We need to check whether the public id really exists. + $cloudinaryResourceService = GeneralUtility::makeInstance( + CloudinaryResourceService::class, + $this->storage + ); - /** - * See if that is OK like that. The alternatives requires to "heavy" processing - * like downloading the file to check the mime time or use the API SDK to fetch whether - * we are in presence of an image. - * - * @param string $fileIdentifier - * - * @return bool - */ - protected function guessIsImage(string $fileIdentifier) - { - $extension = strtolower(PathUtility::pathinfo($fileIdentifier, PATHINFO_EXTENSION)); - $imageExtensions = [ - 'png', - 'jpe', - 'jpeg', - 'jpg', - 'gif', - 'bmp', - 'ico', - 'tiff', - 'tif', - 'svg', - 'svgz', - 'webp', - - 'pdf', // Cloudinary handles pdf as image - ]; - - return in_array($extension, $imageExtensions, true); + $cloudinaryResource = $cloudinaryResourceService->getResource($possiblePublicId); + + // Try to retrieve the cloudinary with the file identifier. + // That will be the case for raw resources. + if (!$cloudinaryResource) { + $cloudinaryResource = $cloudinaryResourceService->getResource($fileIdentifier); + } + + // Houston, we have a real problem. The public id does not exist + if (!$cloudinaryResource) { + throw new \RuntimeException('Cloudinary resource not found for ' . $fileIdentifier, 1623157880); + } + + $this->cachedCloudinaryResources[$possiblePublicId] = $cloudinaryResource; + } + + return $this->cachedCloudinaryResources[$possiblePublicId]; } /** diff --git a/Classes/Services/CloudinaryScanService.php b/Classes/Services/CloudinaryScanService.php index 9ef7813..2d5a13c 100644 --- a/Classes/Services/CloudinaryScanService.php +++ b/Classes/Services/CloudinaryScanService.php @@ -127,7 +127,7 @@ public function scan(): array foreach ($response['resources'] as $resource) { $fileIdentifier = $this->getCloudinaryPathService()->computeFileIdentifier($resource); try { - $this->console($fileIdentifier); + $this->console('Scanning ' . $fileIdentifier); // Save mirrored file $result = $this->getCloudinaryResourceService()->save($resource); @@ -135,7 +135,7 @@ public function scan(): array // Find if the file exists in sys_file already if (!$this->fileExistsInStorage($fileIdentifier)) { - $this->console('Indexing new file: ' . $fileIdentifier, true); + $this->console('New file needs to be indexed by typo3 ' . $fileIdentifier, true); // This will trigger a file indexation $this->storage->getFile($fileIdentifier); @@ -221,7 +221,7 @@ protected function getCloudinaryPathService(): CloudinaryPathService if (!$this->cloudinaryPathService) { $this->cloudinaryPathService = GeneralUtility::makeInstance( CloudinaryPathService::class, - $this->storage->getConfiguration() + $this->storage ); } diff --git a/Classes/Services/Extractor/CloudinaryMetaDataExtractor.php b/Classes/Services/Extractor/CloudinaryMetaDataExtractor.php index ed8a554..825f7fe 100644 --- a/Classes/Services/Extractor/CloudinaryMetaDataExtractor.php +++ b/Classes/Services/Extractor/CloudinaryMetaDataExtractor.php @@ -71,7 +71,7 @@ public function extractMetaData(File $file, array $previousExtractedData = []): $cloudinaryPathService = GeneralUtility::makeInstance( CloudinaryPathService::class, - $file->getStorage()->getConfiguration(), + $file->getStorage(), ); $publicId = $cloudinaryPathService->computeCloudinaryPublicId($file->getIdentifier()); $resource = $cloudinaryResourceService->getResource($publicId); diff --git a/Classes/ViewHelpers/CloudinaryImageDataViewHelper.php b/Classes/ViewHelpers/CloudinaryImageDataViewHelper.php index 1652044..51146d4 100644 --- a/Classes/ViewHelpers/CloudinaryImageDataViewHelper.php +++ b/Classes/ViewHelpers/CloudinaryImageDataViewHelper.php @@ -178,7 +178,7 @@ protected function getCloudinaryPathService(ResourceStorage $storage) { return GeneralUtility::makeInstance( CloudinaryPathService::class, - $storage->getConfiguration() + $storage ); } From 58f86530ac14a53ba7ac42de5fe787cdbc4c7cd3 Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Mon, 8 May 2023 17:47:50 +0200 Subject: [PATCH 35/54] [TASK] Handle exception in fileExists method --- Classes/Driver/CloudinaryDriver.php | 43 +++++++++-------------------- 1 file changed, 13 insertions(+), 30 deletions(-) diff --git a/Classes/Driver/CloudinaryDriver.php b/Classes/Driver/CloudinaryDriver.php index 736db42..0f71c6d 100644 --- a/Classes/Driver/CloudinaryDriver.php +++ b/Classes/Driver/CloudinaryDriver.php @@ -103,10 +103,7 @@ public function __construct(array $configuration = []) $this->charsetConversion = GeneralUtility::makeInstance(CharsetConverter::class); } - /** - * @return void - */ - public function processConfiguration() + public function processConfiguration(): void { } @@ -163,30 +160,18 @@ protected function log(string $message, array $arguments = [], array $data = []) * * @param string $fileIdentifier * @param string $hashAlgorithm - * - * @return string */ - public function hash($fileIdentifier, $hashAlgorithm) + public function hash($fileIdentifier, $hashAlgorithm): string { return $this->hashIdentifier($fileIdentifier); } - /** - * Returns the identifier of the default folder new files should be put into. - * - * @return string - */ - public function getDefaultFolder() + public function getDefaultFolder(): string { return $this->getRootLevelFolder(); } - /** - * Returns the identifier of the root level folder of the storage. - * - * @return string - */ - public function getRootLevelFolder() + public function getRootLevelFolder(): string { return DIRECTORY_SEPARATOR; } @@ -258,17 +243,17 @@ protected function getResourceInfo(array $resource, string $name): string } /** - * Checks if a file exists - * * @param string $fileIdentifier - * - * @return bool */ - public function fileExists($fileIdentifier) + public function fileExists($fileIdentifier): bool { - $cloudinaryResource = $this->getCloudinaryResourceService()->getResource( - $this->getCloudinaryPathService()->computeCloudinaryPublicId($fileIdentifier), - ); + try { + $cloudinaryResource = $this->getCloudinaryResourceService()->getResource( + $this->getCloudinaryPathService()->computeCloudinaryPublicId($fileIdentifier), + ); + } catch (\Exception $e) { + return false; + } return !empty($cloudinaryResource); } @@ -544,10 +529,8 @@ public function deleteFolder($folderIdentifier, $deleteRecursively = false) /** * @param string $fileIdentifier * @param bool $writable - * - * @return string */ - public function getFileForLocalProcessing($fileIdentifier, $writable = true) + public function getFileForLocalProcessing($fileIdentifier, $writable = true): string { $temporaryPath = $this->getTemporaryPathForFile($fileIdentifier); From 4779723573e6a317ee1d17fbcad91dfbc0a34d97 Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Mon, 8 May 2023 17:48:21 +0200 Subject: [PATCH 36/54] [TASK] Add type hinting and nullable property for cloudinaryPathService --- Classes/Services/FileMoveService.php | 70 ++-------------------------- 1 file changed, 4 insertions(+), 66 deletions(-) diff --git a/Classes/Services/FileMoveService.php b/Classes/Services/FileMoveService.php index d7cb39f..12f6256 100644 --- a/Classes/Services/FileMoveService.php +++ b/Classes/Services/FileMoveService.php @@ -8,6 +8,7 @@ * For the full copyright and license information, please read the * LICENSE.md file that was distributed with this source code. */ + use Cloudinary\Api; use Cloudinary\Uploader; use Doctrine\DBAL\Driver\Connection; @@ -18,28 +19,13 @@ use TYPO3\CMS\Core\Utility\GeneralUtility; use Visol\Cloudinary\Utility\CloudinaryApiUtility; -/** - * Class FileMoveService - */ class FileMoveService { - /** - * @var string - */ protected $tableName = 'sys_file'; - /** - * @var CloudinaryPathService - */ - protected $cloudinaryPathService; - - /** - * @param File $fileObject - * @param ResourceStorage $targetStorage - * - * @return bool - */ + protected ?CloudinaryPathService $cloudinaryPathService = null; + public function fileExists(File $fileObject, ResourceStorage $targetStorage): bool { $this->initializeApi($targetStorage); @@ -60,13 +46,6 @@ public function fileExists(File $fileObject, ResourceStorage $targetStorage): bo return $fileExists; } - /** - * @param File $fileObject - * @param ResourceStorage $targetStorage - * @param bool $removeFile - * - * @return bool - */ #public function forceMove(File $fileObject, ResourceStorage $targetStorage, $removeFile = true): bool #{ # $isUpdated = $isDeletedFromSourceStorage = false; @@ -96,13 +75,6 @@ public function fileExists(File $fileObject, ResourceStorage $targetStorage): bo # return $isUpdated && $isDeletedFromSourceStorage; #} - /** - * @param File $fileObject - * @param ResourceStorage $targetStorage - * @param bool $removeFile - * - * @return bool - */ public function changeStorage(File $fileObject, ResourceStorage $targetStorage, $removeFile = true): bool { // Update the storage uid @@ -121,9 +93,6 @@ public function changeStorage(File $fileObject, ResourceStorage $targetStorage, return $isMigrated; } - /** - * @param File $fileObject - */ protected function ensureDirectoryExistence(File $fileObject) { @@ -134,11 +103,6 @@ protected function ensureDirectoryExistence(File $fileObject) } } - /** - * @param File $fileObject - * - * @return string - */ protected function getAbsolutePath(File $fileObject): string { // Compute the absolute file name of the file to move @@ -147,11 +111,6 @@ protected function getAbsolutePath(File $fileObject): string return GeneralUtility::getFileAbsFileName($fileRelativePath); } - /** - * @param File $fileObject - * @param ResourceStorage $targetStorage - * @param string $baseUrl - */ public function cloudinaryUploadFile( File $fileObject, ResourceStorage $targetStorage, @@ -188,17 +147,11 @@ public function cloudinaryUploadFile( ); } - /** - * @param ResourceStorage $targetStorage - */ protected function initializeApi(ResourceStorage $targetStorage) { CloudinaryApiUtility::initializeByConfiguration($targetStorage->getConfiguration()); } - /** - * @return object|QueryBuilder - */ protected function getQueryBuilder(): QueryBuilder { /** @var ConnectionPool $connectionPool */ @@ -206,9 +159,6 @@ protected function getQueryBuilder(): QueryBuilder return $connectionPool->getQueryBuilderForTable($this->tableName); } - /** - * @return object|Connection - */ protected function getConnection(): Connection { /** @var ConnectionPool $connectionPool */ @@ -216,12 +166,6 @@ protected function getConnection(): Connection return $connectionPool->getConnectionForTable($this->tableName); } - /** - * @param File $fileObject - * @param array $values - * - * @return int - */ protected function updateFile(File $fileObject, array $values): int { $connection = $this->getConnection(); @@ -234,22 +178,16 @@ protected function updateFile(File $fileObject, array $values): int ); } - /** - * @return object|CloudinaryPathService - */ protected function getCloudinaryPathService() { return $this->cloudinaryPathService; } - /** - * @param ResourceStorage $storage - */ protected function initializeCloudinaryService(ResourceStorage $storage) { $this->cloudinaryPathService = GeneralUtility::makeInstance( CloudinaryPathService::class, - $storage->getStorageRecord() + $storage ); } } From faa851a6f2d003878b6c164cad19e0f55eba706b Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Mon, 8 May 2023 17:48:44 +0200 Subject: [PATCH 37/54] [REFACTOR] Replace strpos with str_starts_with function for readability and maintainability --- Classes/Slots/FileProcessingSlot.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/Slots/FileProcessingSlot.php b/Classes/Slots/FileProcessingSlot.php index 24aca84..e0a6a32 100644 --- a/Classes/Slots/FileProcessingSlot.php +++ b/Classes/Slots/FileProcessingSlot.php @@ -31,7 +31,7 @@ public function preFileProcess(FileProcessingService $fileProcessingService, Dri return; } - if (strpos($processedFile->getIdentifier() ?? '', 'PROCESSEDFILE' ) === 0) { + if (str_starts_with($processedFile->getIdentifier() ?? '', 'PROCESSEDFILE')) { return; } From 45e862637a6b6116d5ea6d2f41dbd05704350484 Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Mon, 8 May 2023 17:49:15 +0200 Subject: [PATCH 38/54] [ENHANCE] Option for command cloudinary:scan --- Classes/Services/CloudinaryScanService.php | 45 +++++++++++++++++----- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/Classes/Services/CloudinaryScanService.php b/Classes/Services/CloudinaryScanService.php index 2d5a13c..4d0a7f8 100644 --- a/Classes/Services/CloudinaryScanService.php +++ b/Classes/Services/CloudinaryScanService.php @@ -39,6 +39,10 @@ class CloudinaryScanService protected string $processedFolder = '_processed_'; + protected string $additionalExpression = ''; + + protected array $knownRawFormats = ['youtube', 'vimeo',]; + protected array $statistics = [ self::CREATED => 0, self::UPDATED => 0, @@ -60,12 +64,6 @@ public function __construct(ResourceStorage $storage, SymfonyStyle $io = null) $this->io = $io; } - public function deleteAll(): void - { - $this->getCloudinaryResourceService()->deleteAll(); - $this->getCloudinaryFolderService()->deleteAll(); - } - public function scanOne(string $publicId): array|null { try { @@ -86,13 +84,18 @@ public function scan(): array $cloudinaryFolder = $this->getCloudinaryPathService()->computeCloudinaryFolderPath(DIRECTORY_SEPARATOR); + // We initialize the array. + $expressions = []; + // Add a filter if the root directory contains a base path segment // + remove _processed_ folder from the search if ($cloudinaryFolder) { $expressions[] = sprintf('folder=%s/*', $cloudinaryFolder); $expressions[] = sprintf('NOT folder=%s/%s/*', $cloudinaryFolder, $this->processedFolder); - } else { - $expressions[] = sprintf('NOT folder=%s/*', $this->processedFolder); + } + + if ($this->additionalExpression) { + $expressions[] = $this->additionalExpression; } $this->console('Mirroring...', true); @@ -126,16 +129,32 @@ public function scan(): array if (is_array($response['resources'])) { foreach ($response['resources'] as $resource) { $fileIdentifier = $this->getCloudinaryPathService()->computeFileIdentifier($resource); + + // Skip files in the processed folder is detected. + if (str_contains($fileIdentifier, $this->processedFolder)) { + $this->console('Skipped processed file ' . $fileIdentifier); + continue; + } elseif ($resource['resource_type'] === 'raw' + && !in_array($resource['format'], $this->knownRawFormats, true)) { + // Skip as well if the resource is of type raw + // We might have problem when indexing video such as .youtube and .vimeo + // which are not well-supported between cloudinary and typo3 + $this->console('Skipped unknown raw file ' . $fileIdentifier); + continue; + } + try { - $this->console('Scanning ' . $fileIdentifier); // Save mirrored file $result = $this->getCloudinaryResourceService()->save($resource); + $isCreated = isset($result['created']) ? '(new)' : ''; + $this->console('Scanned ' . $fileIdentifier . ' ' . $isCreated); + // Find if the file exists in sys_file already if (!$this->fileExistsInStorage($fileIdentifier)) { - $this->console('New file needs to be indexed by typo3 ' . $fileIdentifier, true); + $this->console('New file will be indexed in typo3 ' . $fileIdentifier, true); // This will trigger a file indexation $this->storage->getFile($fileIdentifier); @@ -257,4 +276,10 @@ protected function console(string $message, $additionalBlankLine = false): void } } } + + public function setAdditionalExpression(string $additionalExpression): CloudinaryScanService + { + $this->additionalExpression = $additionalExpression; + return $this; + } } From 2b3aebe3cd2a44abe997045d51b588614b1120de Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Mon, 8 May 2023 21:19:01 +0200 Subject: [PATCH 39/54] [REFACTOR] Streamline return types to methods that were missing them --- Classes/Driver/CloudinaryDriver.php | 318 ++++---------------- Classes/Services/CloudinaryImageService.php | 63 +--- 2 files changed, 67 insertions(+), 314 deletions(-) diff --git a/Classes/Driver/CloudinaryDriver.php b/Classes/Driver/CloudinaryDriver.php index 0f71c6d..d6e5d76 100644 --- a/Classes/Driver/CloudinaryDriver.php +++ b/Classes/Driver/CloudinaryDriver.php @@ -8,6 +8,7 @@ * For the full copyright and license information, please read the * LICENSE.md file that was distributed with this source code. */ + use TYPO3\CMS\Core\Http\ApplicationType; use TYPO3\CMS\Core\Resource\Exception\InvalidFileNameException; use Cloudinary; @@ -37,56 +38,33 @@ class CloudinaryDriver extends AbstractHierarchicalFilesystemDriver { public const DRIVER_TYPE = 'VisolCloudinary'; - const ROOT_FOLDER_IDENTIFIER = '/'; - const UNSAFE_FILENAME_CHARACTER_EXPRESSION = '\\x00-\\x2C\\/\\x3A-\\x3F\\x5B-\\x60\\x7B-\\xBF'; + protected const ROOT_FOLDER_IDENTIFIER = '/'; + protected const UNSAFE_FILENAME_CHARACTER_EXPRESSION = '\\x00-\\x2C\\/\\x3A-\\x3F\\x5B-\\x60\\x7B-\\xBF'; /** * The base URL that points to this driver's storage. As long is this is not set, it is assumed that this folder * is not publicly available - * - * @var string */ - protected $baseUrl = ''; + protected string $baseUrl = ''; /** * Object permissions are cached here in subarrays like: * $identifier => ['r' => bool, 'w' => bool] - * - * @var array */ - protected $cachedPermissions = []; + protected array $cachedPermissions = []; - /** @var ConfigurationService */ - protected $configurationService; + protected ConfigurationService $configurationService; - /** - * @var ResourceStorage - */ - protected $storage = null; + protected ?ResourceStorage $storage = null; - /** - * @var CharsetConverter - */ - protected $charsetConversion = null; + protected CharsetConverter $charsetConversion; - /** - * @var CloudinaryPathService - */ - protected $cloudinaryPathService; + protected ?CloudinaryPathService $cloudinaryPathService = null; - /** - * @var CloudinaryResourceService - */ - protected $cloudinaryResourceService; + protected ?CloudinaryResourceService $cloudinaryResourceService = null; - /** - * @var CloudinaryFolderService - */ - protected $cloudinaryFolderService; + protected ?CloudinaryFolderService $cloudinaryFolderService = null; - /** - * @param array $configuration - */ public function __construct(array $configuration = []) { $this->configuration = $configuration; @@ -107,10 +85,7 @@ public function processConfiguration(): void { } - /** - * @return void - */ - public function initialize() + public function initialize(): void { // Test connection if we are in the edit view of this storage if ( @@ -124,8 +99,6 @@ public function initialize() /** * @param string $identifier - * - * @return string */ public function getPublicUrl($identifier): string { @@ -143,12 +116,7 @@ public function getPublicUrl($identifier): string return $cloudinaryResource ? $cloudinaryResource['secure_url'] : ''; } - /** - * @param string $message - * @param array $arguments - * @param array $data - */ - protected function log(string $message, array $arguments = [], array $data = []) + protected function log(string $message, array $arguments = [], array $data = []): void { /** @var Logger $logger */ $logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__); @@ -177,16 +145,11 @@ public function getRootLevelFolder(): string } /** - * Returns information about a file. - * * @param string $fileIdentifier * @param array $propertiesToExtract Array of properties which are be extracted * If empty all will be extracted - * - * @return array - * @throws \Exception */ - public function getFileInfoByIdentifier($fileIdentifier, array $propertiesToExtract = []) + public function getFileInfoByIdentifier($fileIdentifier, array $propertiesToExtract = []): array { $publicId = $this->getCloudinaryPathService()->computeCloudinaryPublicId($fileIdentifier); $cloudinaryResource = $this->getCloudinaryResourceService()->getResource($publicId); @@ -231,15 +194,9 @@ public function getFileInfoByIdentifier($fileIdentifier, array $propertiesToExtr ]; } - /** - * @param array $resource - * @param string $name - * - * @return string - */ protected function getResourceInfo(array $resource, string $name): string { - return isset($resource[$name]) ? $resource[$name] : ''; + return $resource[$name] ?? ''; } /** @@ -259,13 +216,9 @@ public function fileExists($fileIdentifier): bool } /** - * Checks if a folder exists - * * @param string $folderIdentifier - * - * @return bool */ - public function folderExists($folderIdentifier) + public function folderExists($folderIdentifier): bool { if ($folderIdentifier === self::ROOT_FOLDER_IDENTIFIER) { return true; @@ -279,10 +232,8 @@ public function folderExists($folderIdentifier) /** * @param string $fileName * @param string $folderIdentifier - * - * @return bool */ - public function fileExistsInFolder($fileName, $folderIdentifier) + public function fileExistsInFolder($fileName, $folderIdentifier): bool { $fileIdentifier = $this->canonicalizeFolderIdentifierAndFileName($folderIdentifier, $fileName); @@ -290,27 +241,19 @@ public function fileExistsInFolder($fileName, $folderIdentifier) } /** - * Checks if a folder exists inside a storage folder - * * @param string $folderName * @param string $folderIdentifier - * - * @return bool */ - public function folderExistsInFolder($folderName, $folderIdentifier) + public function folderExistsInFolder($folderName, $folderIdentifier): bool { return $this->folderExists($this->canonicalizeFolderIdentifierAndFolderName($folderIdentifier, $folderName)); } /** - * Returns the Identifier for a folder within a given folder. - * * @param string $folderName The name of the target folder * @param string $folderIdentifier - * - * @return string */ - public function getFolderInFolder($folderName, $folderIdentifier) + public function getFolderInFolder($folderName, $folderIdentifier): string { return $folderIdentifier . DIRECTORY_SEPARATOR . $folderName; } @@ -321,11 +264,8 @@ public function getFolderInFolder($folderName, $folderIdentifier) * @param string $newFileName optional, if not given original name is used * @param bool $removeOriginal if set the original file will be removed * after successful operation - * - * @return string the identifier of the new file - * @throws \Exception */ - public function addFile($localFilePath, $targetFolderIdentifier, $newFileName = '', $removeOriginal = true) + public function addFile($localFilePath, $targetFolderIdentifier, $newFileName = '', $removeOriginal = true): bool { $fileName = $this->sanitizeFileName($newFileName !== '' ? $newFileName : PathUtility::basename($localFilePath)); @@ -366,27 +306,19 @@ public function addFile($localFilePath, $targetFolderIdentifier, $newFileName = * @param string $fileIdentifier * @param string $targetFolderIdentifier * @param string $newFileName - * - * @return string */ - public function moveFileWithinStorage($fileIdentifier, $targetFolderIdentifier, $newFileName) + public function moveFileWithinStorage($fileIdentifier, $targetFolderIdentifier, $newFileName): string { $targetIdentifier = $targetFolderIdentifier . $newFileName; return $this->renameFile($fileIdentifier, $targetIdentifier); } /** - * Copies a file *within* the current storage. - * Note that this is only about an inner storage copy action, - * where a file is just copied to another folder in the same storage. - * * @param string $fileIdentifier * @param string $targetFolderIdentifier * @param string $fileName - * - * @return string the Identifier of the new file */ - public function copyFileWithinStorage($fileIdentifier, $targetFolderIdentifier, $fileName) + public function copyFileWithinStorage($fileIdentifier, $targetFolderIdentifier, $fileName): string { $targetFileIdentifier = $this->canonicalizeFolderIdentifierAndFileName($targetFolderIdentifier, $fileName); @@ -411,14 +343,10 @@ public function copyFileWithinStorage($fileIdentifier, $targetFolderIdentifier, } /** - * Replaces a file with file in local file system. - * * @param string $fileIdentifier * @param string $localFilePath - * - * @return bool */ - public function replaceFile($fileIdentifier, $localFilePath) + public function replaceFile($fileIdentifier, $localFilePath): bool { // We remove a possible existing transient file to avoid bad surprise. $this->cleanUpTemporaryFile($fileIdentifier); @@ -449,15 +377,9 @@ public function replaceFile($fileIdentifier, $localFilePath) } /** - * Removes a file from the filesystem. This does not check if the file is - * still used or if it is a bad idea to delete it for some other reason - * this has to be taken care of in the upper layers (e.g. the Storage)! - * * @param string $fileIdentifier - * - * @return bool TRUE if deleting the file succeeded */ - public function deleteFile($fileIdentifier) + public function deleteFile($fileIdentifier): bool { $cloudinaryPublicId = $this->getCloudinaryPathService()->computeCloudinaryPublicId($fileIdentifier); $this->log( @@ -474,7 +396,7 @@ public function deleteFile($fileIdentifier) foreach ($response['deleted'] as $publicId => $status) { if ($status === 'deleted') { - $isDeleted = (bool) $this->getCloudinaryResourceService()->delete($publicId); + $isDeleted = (bool)$this->getCloudinaryResourceService()->delete($publicId); } } @@ -482,15 +404,10 @@ public function deleteFile($fileIdentifier) } /** - * Removes a folder in filesystem. - * * @param string $folderIdentifier * @param bool $deleteRecursively - * - * @return bool - * @throws Api\GeneralError */ - public function deleteFolder($folderIdentifier, $deleteRecursively = false) + public function deleteFolder($folderIdentifier, $deleteRecursively = false): bool { $cloudinaryFolder = $this->getCloudinaryPathService()->computeCloudinaryFolderPath($folderIdentifier); @@ -549,14 +466,10 @@ public function getFileForLocalProcessing($fileIdentifier, $writable = true): st } /** - * Creates a new (empty) file and returns the identifier. - * * @param string $fileName * @param string $parentFolderIdentifier - * - * @return string */ - public function createFile($fileName, $parentFolderIdentifier) + public function createFile($fileName, $parentFolderIdentifier): string { throw new RuntimeException( 'createFile: not implemented action! Cloudinary Driver is limited to images.', @@ -565,16 +478,11 @@ public function createFile($fileName, $parentFolderIdentifier) } /** - * Creates a folder, within a parent folder. - * If no parent folder is given, a root level folder will be created - * * @param string $newFolderName * @param string $parentFolderIdentifier * @param bool $recursive - * - * @return string the Identifier of the new folder */ - public function createFolder($newFolderName, $parentFolderIdentifier = '', $recursive = false) + public function createFolder($newFolderName, $parentFolderIdentifier = '', $recursive = false): string { $canonicalFolderPath = $this->canonicalizeFolderIdentifierAndFolderName( $parentFolderIdentifier, @@ -595,10 +503,8 @@ public function createFolder($newFolderName, $parentFolderIdentifier = '', $recu /** * @param string $fileIdentifier - * - * @return string */ - public function getFileContents($fileIdentifier) + public function getFileContents($fileIdentifier): string { // Will download the file to be faster next time the content is required. $localFileNameAndPath = $this->getFileForLocalProcessing($fileIdentifier); @@ -610,8 +516,6 @@ public function getFileContents($fileIdentifier) * * @param string $fileIdentifier * @param string $contents - * - * @return int */ public function setFileContents($fileIdentifier, $contents) { @@ -619,14 +523,10 @@ public function setFileContents($fileIdentifier, $contents) } /** - * Renames a file in this storage. - * * @param string $fileIdentifier * @param string $newFileIdentifier The target path (including the file name!) - * - * @return string The identifier of the file after renaming */ - public function renameFile($fileIdentifier, $newFileIdentifier) + public function renameFile($fileIdentifier, $newFileIdentifier): string { if (!$this->isFileIdentifier($newFileIdentifier)) { $sanitizedFileName = $this->sanitizeFileName(PathUtility::basename($newFileIdentifier)); @@ -664,8 +564,6 @@ public function renameFile($fileIdentifier, $newFileIdentifier) /** * @param array $cloudinaryResource * @param string $fileIdentifier - * - * @throws Api\GeneralError */ protected function checkCloudinaryUploadStatus(array $cloudinaryResource, $fileIdentifier): void { @@ -675,14 +573,12 @@ protected function checkCloudinaryUploadStatus(array $cloudinaryResource, $fileI } /** - * Renames a folder in this storage. - * * @param string $folderIdentifier * @param string $newFolderName * * @return array A map of old to new file identifiers of all affected resources */ - public function renameFolder($folderIdentifier, $newFolderName) + public function renameFolder($folderIdentifier, $newFolderName): array { $renamedFiles = []; @@ -721,7 +617,7 @@ public function renameFolder($folderIdentifier, $newFolderName) * * @return array All files which are affected, map of old => new file identifiers */ - public function moveFolderWithinStorage($sourceFolderIdentifier, $targetFolderIdentifier, $newFolderName) + public function moveFolderWithinStorage($sourceFolderIdentifier, $targetFolderIdentifier, $newFolderName): array { // Compute the new folder identifier and then create it. $newTargetFolderIdentifier = $this->canonicalizeFolderIdentifierAndFolderName( @@ -753,10 +649,8 @@ public function moveFolderWithinStorage($sourceFolderIdentifier, $targetFolderId * @param string $sourceFolderIdentifier * @param string $targetFolderIdentifier * @param string $newFolderName - * - * @return bool */ - public function copyFolderWithinStorage($sourceFolderIdentifier, $targetFolderIdentifier, $newFolderName) + public function copyFolderWithinStorage($sourceFolderIdentifier, $targetFolderIdentifier, $newFolderName): bool { // Compute the new folder identifier and then create it. $newTargetFolderIdentifier = $this->canonicalizeFolderIdentifierAndFolderName( @@ -786,28 +680,17 @@ public function copyFolderWithinStorage($sourceFolderIdentifier, $targetFolderId * Checks if a folder contains files and (if supported) other folders. * * @param string $folderIdentifier - * - * @return bool TRUE if there are no files and folders within $folder */ - public function isFolderEmpty($folderIdentifier) + public function isFolderEmpty($folderIdentifier): bool { return $this->getCloudinaryFolderService()->countSubFolders($folderIdentifier); } /** - * Checks if a given identifier is within a container, e.g. if - * a file or folder is within another folder. - * - * Hint: this also needs to return TRUE if the given identifier - * matches the container identifier to allow access to the root - * folder of a filemount. - * * @param string $folderIdentifier * @param string $identifier identifier to be checked against $folderIdentifier - * - * @return bool TRUE if $content is within or matches $folderIdentifier */ - public function isWithin($folderIdentifier, $identifier) + public function isWithin($folderIdentifier, $identifier): bool { $folderIdentifier = $this->canonicalizeAndCheckFileIdentifier($folderIdentifier); $fileIdentifier = $this->canonicalizeAndCheckFileIdentifier($identifier); @@ -825,13 +708,9 @@ public function isWithin($folderIdentifier, $identifier) } /** - * Returns information about a file. - * * @param string $folderIdentifier - * - * @return array */ - public function getFolderInfoByIdentifier($folderIdentifier) + public function getFolderInfoByIdentifier($folderIdentifier): array { $canonicalFolderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier); return [ @@ -844,22 +723,16 @@ public function getFolderInfoByIdentifier($folderIdentifier) } /** - * Returns a file inside the specified path - * * @param string $fileName * @param string $folderIdentifier - * - * @return string */ - public function getFileInFolder($fileName, $folderIdentifier) + public function getFileInFolder($fileName, $folderIdentifier): string { $folderIdentifier = $folderIdentifier . DIRECTORY_SEPARATOR . $fileName; return $folderIdentifier; } /** - * Returns a list of files inside the specified path - * * @param string $folderIdentifier * @param int $start * @param int $numberOfItems @@ -871,8 +744,6 @@ public function getFileInFolder($fileName, $folderIdentifier) * If a driver does not support the given property, it * should fall back to "name". * @param bool $sortRev TRUE to indicate reverse sorting (last to first) - * - * @return array of FileIdentifiers */ public function getFilesInFolder( $folderIdentifier, @@ -882,13 +753,14 @@ public function getFilesInFolder( array $filterCallbacks = [], $sort = '', $sortRev = false - ) { + ): array + { $cloudinaryFolder = $this->getCloudinaryPathService()->computeCloudinaryFolderPath( $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier), ); // Set default orderings - $parameters = (array) GeneralUtility::_GP('SET'); + $parameters = (array)GeneralUtility::_GP('SET'); if ($parameters['sort'] === 'file') { $parameters['sort'] = 'filename'; } elseif ($parameters['sort'] === 'tstamp') { @@ -900,12 +772,12 @@ public function getFilesInFolder( $orderings = [ 'fieldName' => $parameters['sort'], - 'direction' => isset($parameters['reverse']) && (int) $parameters['reverse'] ? 'DESC' : 'ASC', + 'direction' => isset($parameters['reverse']) && (int)$parameters['reverse'] ? 'DESC' : 'ASC', ]; $pagination = [ 'maxResult' => $numberOfItems, - 'firstResult' => (int) GeneralUtility::_GP('pointer'), + 'firstResult' => (int)GeneralUtility::_GP('pointer'), ]; $cloudinaryResources = $this->getCloudinaryResourceService()->getResources( @@ -939,15 +811,11 @@ public function getFilesInFolder( } /** - * Returns the number of files inside the specified path - * * @param string $folderIdentifier * @param bool $recursive * @param array $filterCallbacks callbacks for filtering the items - * - * @return int */ - public function countFilesInFolder($folderIdentifier, $recursive = false, array $filterCallbacks = []) + public function countFilesInFolder($folderIdentifier, $recursive = false, array $filterCallbacks = []): int { $folderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier); @@ -965,8 +833,6 @@ public function countFilesInFolder($folderIdentifier, $recursive = false, array } /** - * Returns a list of folders inside the specified path - * * @param string $folderIdentifier * @param int $start * @param int $numberOfItems @@ -978,8 +844,6 @@ public function countFilesInFolder($folderIdentifier, $recursive = false, array * If a driver does not support the given property, it * should fall back to "name". * @param bool $sortRev TRUE to indicate reverse sorting (last to first) - * - * @return array */ public function getFoldersInFolder( $folderIdentifier, @@ -989,8 +853,9 @@ public function getFoldersInFolder( array $filterCallbacks = [], $sort = '', $sortRev = false - ) { - $parameters = (array) GeneralUtility::_GP('SET'); + ): array + { + $parameters = (array)GeneralUtility::_GP('SET'); $cloudinaryFolder = $this->getCloudinaryPathService()->computeCloudinaryFolderPath( $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier), @@ -1000,7 +865,7 @@ public function getFoldersInFolder( $cloudinaryFolder, [ 'fieldName' => 'folder', - 'direction' => isset($parameters['reverse']) && (int) $parameters['reverse'] ? 'DESC' : 'ASC', + 'direction' => isset($parameters['reverse']) && (int)$parameters['reverse'] ? 'DESC' : 'ASC', ], $recursive, ); @@ -1026,15 +891,11 @@ public function getFoldersInFolder( } /** - * Returns the number of folders inside the specified path - * * @param string $folderIdentifier * @param bool $recursive * @param array $filterCallbacks - * - * @return int */ - public function countFoldersInFolder($folderIdentifier, $recursive = false, array $filterCallbacks = []) + public function countFoldersInFolder($folderIdentifier, $recursive = false, array $filterCallbacks = []): int { // true means we have non-core filters that has been added and we must filter on the PHP side. if (count($filterCallbacks) > 1) { @@ -1053,10 +914,8 @@ public function countFoldersInFolder($folderIdentifier, $recursive = false, arra /** * @param string $identifier - * - * @return string */ - public function dumpFileContents($identifier) + public function dumpFileContents($identifier): string { return $this->getFileContents($identifier); } @@ -1066,10 +925,8 @@ public function dumpFileContents($identifier) * (keys r, w) of bool flags * * @param string $identifier - * - * @return array */ - public function getPermissions($identifier) + public function getPermissions($identifier): array { if (!isset($this->cachedPermissions[$identifier])) { // Cloudinary does not handle permissions @@ -1085,10 +942,8 @@ public function getPermissions($identifier) * and returns the result. * * @param int $capabilities - * - * @return int */ - public function mergeConfigurationCapabilities($capabilities) + public function mergeConfigurationCapabilities($capabilities): int { $this->capabilities &= $capabilities; return $this->capabilities; @@ -1101,11 +956,8 @@ public function mergeConfigurationCapabilities($capabilities) * * @param string $fileName Input string, typically the body of a fileName * @param string $charset Charset of the a fileName (defaults to current charset; depending on context) - * - * @return string Output string with any characters not matching [.a-zA-Z0-9_-] is substituted by '_' and trailing dots removed - * @throws Exception\InvalidFileNameException */ - public function sanitizeFileName($fileName, $charset = '') + public function sanitizeFileName($fileName, $charset = ''): string { $fileName = $this->charsetConversion->specCharsToASCII('utf-8', $fileName); @@ -1140,16 +992,14 @@ public function sanitizeFileName($fileName, $charset = '') * @param string $itemName * @param string $itemIdentifier * @param string $parentIdentifier - * - * @return bool - * @throws \RuntimeException */ protected function applyFilterMethodsToDirectoryItem( array $filterMethods, - $itemName, - $itemIdentifier, - $parentIdentifier - ) { + $itemName, + $itemIdentifier, + $parentIdentifier + ): bool + { foreach ($filterMethods as $filter) { if (is_callable($filter)) { $result = call_user_func($filter, $itemName, $itemIdentifier, $parentIdentifier, [], $this); @@ -1183,30 +1033,16 @@ protected function cleanUpTemporaryFile(string $fileIdentifier): void $this->getExplicitDataCacheRepository()->delete($this->storageUid, $fileIdentifier); } - /** - * @return object|ExplicitDataCacheRepository - */ - public function getExplicitDataCacheRepository() + public function getExplicitDataCacheRepository(): ExplicitDataCacheRepository { return GeneralUtility::makeInstance(ExplicitDataCacheRepository::class); } - /** - * @param string $newFileIdentifier - * - * @return bool - */ protected function isFileIdentifier(string $newFileIdentifier): bool { - return false !== strpos($newFileIdentifier, DIRECTORY_SEPARATOR); + return str_contains($newFileIdentifier, DIRECTORY_SEPARATOR); } - /** - * @param string $folderIdentifier - * @param string $folderName - * - * @return string - */ protected function canonicalizeFolderIdentifierAndFolderName(string $folderIdentifier, string $folderName): string { $canonicalFolderPath = $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier); @@ -1215,12 +1051,6 @@ protected function canonicalizeFolderIdentifierAndFolderName(string $folderIdent ); } - /** - * @param string $folderIdentifier - * @param string $fileName - * - * @return string - */ protected function canonicalizeFolderIdentifierAndFileName(string $folderIdentifier, string $fileName): string { return $this->canonicalizeAndCheckFileIdentifier( @@ -1228,10 +1058,7 @@ protected function canonicalizeFolderIdentifierAndFileName(string $folderIdentif ); } - /** - * @return object|CloudinaryPathService - */ - protected function getCloudinaryPathService() + protected function getCloudinaryPathService(): CloudinaryPathService { if (!$this->cloudinaryPathService) { if ($this->storageUid) { @@ -1249,10 +1076,7 @@ protected function getCloudinaryPathService() return $this->cloudinaryPathService; } - /** - * @return CloudinaryResourceService - */ - protected function getCloudinaryResourceService() + protected function getCloudinaryResourceService(): CloudinaryResourceService { if (!$this->cloudinaryResourceService) { /** @var ResourceFactory $resourceFactory */ @@ -1267,18 +1091,12 @@ protected function getCloudinaryResourceService() return $this->cloudinaryResourceService; } - /** - * @return object|CloudinaryTestConnectionService - */ - protected function getCloudinaryTestConnectionService() + protected function getCloudinaryTestConnectionService(): CloudinaryTestConnectionService { return GeneralUtility::makeInstance(CloudinaryTestConnectionService::class, $this->configuration); } - /** - * @return CloudinaryFolderService - */ - protected function getCloudinaryFolderService() + protected function getCloudinaryFolderService(): CloudinaryFolderService { if (!$this->cloudinaryFolderService) { $this->cloudinaryFolderService = GeneralUtility::makeInstance( @@ -1290,10 +1108,7 @@ protected function getCloudinaryFolderService() return $this->cloudinaryFolderService; } - /** - * @return void - */ - protected function initializeApi() + protected function initializeApi(): void { Cloudinary::config([ 'cloud_name' => $this->configurationService->get('cloudName'), @@ -1304,10 +1119,7 @@ protected function initializeApi() ]); } - /** - * @return Api - */ - protected function getApi() + protected function getApi(): Api { $this->initializeApi(); diff --git a/Classes/Services/CloudinaryImageService.php b/Classes/Services/CloudinaryImageService.php index e6b41cb..b8f7840 100644 --- a/Classes/Services/CloudinaryImageService.php +++ b/Classes/Services/CloudinaryImageService.php @@ -12,29 +12,15 @@ use TYPO3\CMS\Core\Resource\StorageRepository; use Cloudinary\Uploader; use Doctrine\DBAL\Exception\UniqueConstraintViolationException; -use TYPO3\CMS\Core\Log\LogLevel; -use TYPO3\CMS\Core\Log\LogManager; use TYPO3\CMS\Core\Resource\File; -use TYPO3\CMS\Core\Resource\ResourceStorage; use TYPO3\CMS\Core\Utility\GeneralUtility; use Visol\Cloudinary\Domain\Repository\ExplicitDataCacheRepository; -use Visol\Cloudinary\Driver\CloudinaryDriver; -use Visol\Cloudinary\Utility\CloudinaryApiUtility; -/** - * Class CloudinaryImageService - */ class CloudinaryImageService extends AbstractCloudinaryMediaService { - /** - * @var ExplicitDataCacheRepository - */ - protected $explicitDataCacheRepository; + protected ExplicitDataCacheRepository $explicitDataCacheRepository; - /** - * @var StorageRepository - */ - protected $storageRepository; + protected ?StorageRepository $storageRepository = null; protected array $defaultOptions = [ 'type' => 'upload', @@ -43,21 +29,11 @@ class CloudinaryImageService extends AbstractCloudinaryMediaService 'quality' => 'auto', ]; - /** - * - */ public function __construct() { $this->explicitDataCacheRepository = GeneralUtility::makeInstance(ExplicitDataCacheRepository::class); } - - /** - * @param File $file - * @param array $options - * - * @return array - */ public function getExplicitData(File $file, array $options): array { $publicId = $this->getPublicIdForFile($file); @@ -77,12 +53,6 @@ public function getExplicitData(File $file, array $options): array return $explicitData; } - /** - * @param File $file - * @param array $options - * - * @return array - */ public function getResponsiveBreakpointData(File $file, array $options): array { $explicitData = $this->getExplicitData($file, $options); @@ -90,21 +60,11 @@ public function getResponsiveBreakpointData(File $file, array $options): array return $explicitData['responsive_breakpoints'][0]['breakpoints']; } - /** - * @param array $breakpoints - * - * @return string - */ public function getSrcsetAttribute(array $breakpoints): string { return implode(',' . PHP_EOL, $this->getSrcset($breakpoints)); } - /** - * @param array $breakpoints - * - * @return array - */ public function getSrcset(array $breakpoints): array { $imageObjects = $this->getImageObjects($breakpoints); @@ -116,11 +76,6 @@ public function getSrcset(array $breakpoints): array return $srcset; } - /** - * @param array $breakpoints - * - * @return string - */ public function getSizesAttribute(array $breakpoints): string { $maxImageObject = $this->getImage($breakpoints, 'max'); @@ -128,9 +83,6 @@ public function getSizesAttribute(array $breakpoints): string } /** - * @param array $breakpoints - * @param string $functionName - * * @return mixed */ public function getImage(array $breakpoints, string $functionName) @@ -170,11 +122,6 @@ public function getImageUrl(File $file, array $options = []): string return \Cloudinary::cloudinary_url($publicId, $options); } - /** - * @param array $breakpoints - * - * @return array - */ public function getImageObjects(array $breakpoints): array { $widthMap = []; @@ -185,12 +132,6 @@ public function getImageObjects(array $breakpoints): array return $widthMap; } - /** - * @param array $settings - * @param bool $enableResponsiveBreakpoints - * - * @return array - */ public function generateOptionsFromSettings(array $settings, bool $enableResponsiveBreakpoints = true): array { $transformations = []; From 0369655ed7f955d78621895725d81a78d2a158c1 Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Tue, 9 May 2023 11:35:04 +0200 Subject: [PATCH 40/54] [BUGFIX] Migrate signal slot to event dispatcher --- .../BeforeFileProcessingEventHandler.php} | 51 +++++++++---------- Configuration/Services.yaml | 6 +++ 2 files changed, 31 insertions(+), 26 deletions(-) rename Classes/{Slots/FileProcessingSlot.php => EventHandlers/BeforeFileProcessingEventHandler.php} (61%) diff --git a/Classes/Slots/FileProcessingSlot.php b/Classes/EventHandlers/BeforeFileProcessingEventHandler.php similarity index 61% rename from Classes/Slots/FileProcessingSlot.php rename to Classes/EventHandlers/BeforeFileProcessingEventHandler.php index e0a6a32..2f421ca 100644 --- a/Classes/Slots/FileProcessingSlot.php +++ b/Classes/EventHandlers/BeforeFileProcessingEventHandler.php @@ -1,6 +1,6 @@ getDriver(); + $processedFile = $event->getProcessedFile(); + /** @var File $file */ + $file = $event->getFile(); + if (!$driver instanceof CloudinaryDriver) { return; } @@ -35,21 +37,22 @@ public function preFileProcess(FileProcessingService $fileProcessingService, Dri return; } - $options = [ - 'type' => 'upload', - 'eager' => [ - [ - //'format' => 'jpg', // `Invalid transformation component - auto` - 'fetch_format' => 'auto', - 'quality' => 'auto:eco', - 'width' => 64, - 'height' => 64, - 'crop' => 'fit', + $explicitData = $this->getCloudinaryImageService()->getExplicitData( + $file, + [ + 'type' => 'upload', + 'eager' => [ + [ + //'format' => 'jpg', // `Invalid transformation component - auto` + 'fetch_format' => 'auto', + 'quality' => 'auto:eco', + 'width' => 64, + 'height' => 64, + 'crop' => 'fit', + ] ] ] - ]; - - $explicitData = $this->getCloudinaryImageService()->getExplicitData($file, $options); + ); $url = $explicitData['eager'][0]['secure_url']; $parts = parse_url($url); @@ -66,12 +69,8 @@ public function preFileProcess(FileProcessingService $fileProcessingService, Dri $processedFileRepository->add($processedFile); } - /** - * @return object|CloudinaryImageService - */ - public function getCloudinaryImageService() + public function getCloudinaryImageService(): CloudinaryImageService { return GeneralUtility::makeInstance(CloudinaryImageService::class); } - } diff --git a/Configuration/Services.yaml b/Configuration/Services.yaml index 26a8ea3..8bb65ae 100644 --- a/Configuration/Services.yaml +++ b/Configuration/Services.yaml @@ -62,3 +62,9 @@ services: command: 'cloudinary:query' schedulable: false description: Query a given storage such a list, count files or folders. + + Visol\Cloudinary\EventHandlers\BeforeFileProcessingEventHandler: + tags: + - name: event.listener + identifier: 'cloudinary-before-file-processing-event-handler' + event: TYPO3\CMS\Core\Resource\Event\BeforeFileProcessingEvent From 02bf93dbc9efacb7b3f8497750fc6c59d8fab031 Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Tue, 9 May 2023 11:36:26 +0200 Subject: [PATCH 41/54] [TASK] Change table name from tx_cloudinary_resource to tx_cloudinary_cache_resources --- Classes/Services/CloudinaryResourceService.php | 2 +- ext_tables.sql | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Classes/Services/CloudinaryResourceService.php b/Classes/Services/CloudinaryResourceService.php index ed1445b..6337396 100644 --- a/Classes/Services/CloudinaryResourceService.php +++ b/Classes/Services/CloudinaryResourceService.php @@ -20,7 +20,7 @@ */ class CloudinaryResourceService { - protected string $tableName = 'tx_cloudinary_resource'; + protected string $tableName = 'tx_cloudinary_cache_resources'; protected ResourceStorage $storage; diff --git a/ext_tables.sql b/ext_tables.sql index a6460dd..e57f0c4 100644 --- a/ext_tables.sql +++ b/ext_tables.sql @@ -16,9 +16,9 @@ CREATE TABLE tx_cloudinary_explicit_data_cache ( ); # -# Table structure for table 'tx_cloudinary_resource' +# Table structure for table 'tx_cloudinary_cache_resources' # -CREATE TABLE tx_cloudinary_resource ( +CREATE TABLE tx_cloudinary_cache_resources ( public_id text, public_id_hash char(40) DEFAULT '' NOT NULL, folder text, From 1a972587dcce3d1d2019bb590e5d99c5def8a1f9 Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Tue, 9 May 2023 11:36:57 +0200 Subject: [PATCH 42/54] [TASK] Remove unused translation for Cloudinary resources in tt_content module --- Resources/Private/Language/backend.xlf | 3 --- 1 file changed, 3 deletions(-) diff --git a/Resources/Private/Language/backend.xlf b/Resources/Private/Language/backend.xlf index 50a64ed..fa1760f 100644 --- a/Resources/Private/Language/backend.xlf +++ b/Resources/Private/Language/backend.xlf @@ -31,9 +31,6 @@ Congratulations! Cloudinary is successfully connected to TYPO3. - - Cloudinary resources - From 004ec228b00c6bfdb8ee320c090ca8a5310a22d9 Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Tue, 9 May 2023 11:38:28 +0200 Subject: [PATCH 43/54] [CGL] Improve php return type --- .../AbstractCloudinaryMediaService.php | 31 ++---------- .../Services/CloudinaryResourceService.php | 47 +++++++++++-------- Classes/Services/CloudinaryUploadService.php | 46 ++++-------------- 3 files changed, 42 insertions(+), 82 deletions(-) diff --git a/Classes/Services/AbstractCloudinaryMediaService.php b/Classes/Services/AbstractCloudinaryMediaService.php index fae90b6..83fa5fc 100644 --- a/Classes/Services/AbstractCloudinaryMediaService.php +++ b/Classes/Services/AbstractCloudinaryMediaService.php @@ -33,12 +33,6 @@ protected function initializeApi(ResourceStorage $storage): void CloudinaryApiUtility::initializeByConfiguration($storage->getConfiguration()); } - /** - * @param File $file - * @param array $options - * - * @return array - */ public function getExplicitData(File $file, array $options): array { $publicId = $this->getPublicIdForFile($file); @@ -58,12 +52,7 @@ public function getExplicitData(File $file, array $options): array return $explicitData; } - /** - * @param string $message - * @param array $arguments - * @param array $data - */ - protected function error(string $message, array $arguments = [], array $data = []) + protected function error(string $message, array $arguments = [], array $data = []): void { /** @var Logger $logger */ $logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__); @@ -74,20 +63,14 @@ protected function error(string $message, array $arguments = [], array $data = [ ); } - /** - * @return File - */ public function getEmergencyPlaceholderFile(): File { /** @var CloudinaryUploadService $cloudinaryUploadService */ $cloudinaryUploadService = GeneralUtility::makeInstance(CloudinaryUploadService::class); - return $cloudinaryUploadService->uploadLocalFile(''); + return $cloudinaryUploadService->getEmergencyFile(); } - /** - * @return object|CloudinaryPathService - */ - protected function getCloudinaryPathService(ResourceStorage $storage) + protected function getCloudinaryPathService(ResourceStorage $storage): CloudinaryPathService { return GeneralUtility::makeInstance( CloudinaryPathService::class, @@ -95,11 +78,6 @@ protected function getCloudinaryPathService(ResourceStorage $storage) ); } - /** - * @param File $file - * - * @return string - */ public function getPublicIdForFile(File $file): string { @@ -113,9 +91,8 @@ public function getPublicIdForFile(File $file): string } // Compute the cloudinary public id - $publicId = $this + return $this ->getCloudinaryPathService($file->getStorage()) ->computeCloudinaryPublicId($file->getIdentifier()); - return $publicId; } } diff --git a/Classes/Services/CloudinaryResourceService.php b/Classes/Services/CloudinaryResourceService.php index 6337396..a223b8b 100644 --- a/Classes/Services/CloudinaryResourceService.php +++ b/Classes/Services/CloudinaryResourceService.php @@ -54,10 +54,11 @@ public function getResource(string $publicId): array public function getResources( string $folder, - array $orderings = [], - array $pagination = [], - bool $recursive = false - ): array { + array $orderings = [], + array $pagination = [], + bool $recursive = false + ): array + { $query = $this->getQueryBuilder(); $query ->select('*') @@ -74,9 +75,9 @@ public function getResources( $query->orderBy($orderings['fieldName'], $orderings['direction']); } - if ($pagination && (int) $pagination['maxResult'] > 0) { - $query->setMaxResults((int) $pagination['maxResult']); - $query->setFirstResult((int) $pagination['firstResult']); + if ($pagination && (int)$pagination['maxResult'] > 0) { + $query->setMaxResults((int)$pagination['maxResult']); + $query->setFirstResult((int)$pagination['firstResult']); } return $query->execute()->fetchAllAssociative(); } @@ -95,7 +96,7 @@ public function count(string $folder, bool $recursive = false): int : $query->expr()->eq('folder', $query->expr()->literal($folder)); $query->andWhere($expresion); - return (int) $query->execute()->fetchOne(0); + return (int)$query->execute()->fetchOne(0); } public function delete(string $publicId): int @@ -121,9 +122,17 @@ public function save(array $cloudinaryResource): array $this->getCloudinaryFolderService()->save($folder); } - return $this->exists($publicIdHash) - ? ['updated' => $this->update($cloudinaryResource, $publicIdHash), 'publicIdHash' => $publicIdHash] - : ['created' => $this->add($cloudinaryResource), 'publicIdHash' => $publicIdHash]; + $result = $this->exists($publicIdHash) + ? ['updated' => $this->update($cloudinaryResource, $publicIdHash),] + : ['created' => $this->add($cloudinaryResource),]; + + return array_merge( + $result, + [ + 'publicIdHash' => $publicIdHash, + 'resource' => $cloudinaryResource, + ] + ); } protected function add(array $cloudinaryResource): int @@ -150,7 +159,7 @@ protected function exists(string $publicIdHash): int $query->expr()->eq('public_id_hash', $query->expr()->literal($publicIdHash)), ); - return (int) $query->execute()->fetchOne(0); + return (int)$query->execute()->fetchOne(0); } protected function getValues(array $cloudinaryResource): array @@ -163,16 +172,16 @@ protected function getValues(array $cloudinaryResource): array 'folder' => $this->getFolder($cloudinaryResource), 'filename' => $this->getFileName($cloudinaryResource), 'format' => $this->getValue('format', $cloudinaryResource), - 'version' => (int) $this->getValue('version', $cloudinaryResource), + 'version' => (int)$this->getValue('version', $cloudinaryResource), 'resource_type' => $this->getValue('resource_type', $cloudinaryResource), 'type' => $this->getValue('type', $cloudinaryResource), 'created_at' => $this->getCreatedAt($cloudinaryResource), 'uploaded_at' => $this->getUpdatedAt($cloudinaryResource), - 'bytes' => (int) $this->getValue('bytes', $cloudinaryResource), - 'width' => (int) $this->getValue('width', $cloudinaryResource), - 'height' => (int) $this->getValue('height', $cloudinaryResource), - 'aspect_ratio' => (float) $this->getValue('aspect_ratio', $cloudinaryResource), - 'pixels' => (int) $this->getValue('pixels', $cloudinaryResource), + 'bytes' => (int)$this->getValue('bytes', $cloudinaryResource), + 'width' => (int)$this->getValue('width', $cloudinaryResource), + 'height' => (int)$this->getValue('height', $cloudinaryResource), + 'aspect_ratio' => (float)$this->getValue('aspect_ratio', $cloudinaryResource), + 'pixels' => (int)$this->getValue('pixels', $cloudinaryResource), 'url' => $this->getValue('url', $cloudinaryResource), 'secure_url' => $this->getValue('secure_url', $cloudinaryResource), 'status' => $this->getValue('status', $cloudinaryResource), @@ -188,7 +197,7 @@ protected function getValues(array $cloudinaryResource): array protected function getValue(string $key, array $cloudinaryResource): string { - return isset($cloudinaryResource[$key]) ? (string) $cloudinaryResource[$key] : ''; + return isset($cloudinaryResource[$key]) ? (string)$cloudinaryResource[$key] : ''; } protected function getFileName(array $cloudinaryResource): string diff --git a/Classes/Services/CloudinaryUploadService.php b/Classes/Services/CloudinaryUploadService.php index 96c68f0..144e2cc 100644 --- a/Classes/Services/CloudinaryUploadService.php +++ b/Classes/Services/CloudinaryUploadService.php @@ -14,40 +14,22 @@ use TYPO3\CMS\Core\Log\LogLevel; use TYPO3\CMS\Core\Log\LogManager; use TYPO3\CMS\Core\Resource\File; -use TYPO3\CMS\Core\Resource\FileInterface; use TYPO3\CMS\Core\Resource\ResourceStorage; use TYPO3\CMS\Core\Utility\GeneralUtility; use Visol\Cloudinary\CloudinaryFactory; -/** - * Class CloudinaryUploadService - */ class CloudinaryUploadService { - /** - * @var string - */ - protected $emergencyFileIdentifier = '/typo3conf/ext/cloudinary/Resources/Public/Images/emergency-placeholder-image.png'; + protected string $emergencyFileIdentifier = '/typo3conf/ext/cloudinary/Resources/Public/Images/emergency-placeholder-image.png'; - /** - * @var ResourceStorage - */ - protected $storage; + protected ResourceStorage $storage; - /** - * @param ResourceStorage $storage - */ public function __construct(ResourceStorage $storage = null) { $this->storage = $storage ?: CloudinaryFactory::getDefaultStorage(); } - /** - * @param string $fileIdentifier - * - * @return File|FileInterface - */ - public function uploadLocalFile(string $fileIdentifier) + public function uploadLocalFile(string $fileIdentifier): File { // Cleanup file identifier in case $fileIdentifier = $this->cleanUp($fileIdentifier); @@ -68,19 +50,16 @@ public function uploadLocalFile(string $fileIdentifier) ); } - /** - * @param string $fileIdentifier - */ - protected function cleanUp(string $fileIdentifier) + public function getEmergencyFile(): File + { + return $this->uploadLocalFile($this->emergencyFileIdentifier); + } + + protected function cleanUp(string $fileIdentifier): string { return DIRECTORY_SEPARATOR . ltrim($fileIdentifier, DIRECTORY_SEPARATOR); } - /** - * @param string $fileIdentifier - * - * @return bool - */ protected function fileExists(string $fileIdentifier): bool { $fileNameAndPath = @@ -88,12 +67,7 @@ protected function fileExists(string $fileIdentifier): bool return is_file($fileNameAndPath); } - /** - * @param string $message - * @param array $arguments - * @param array $data - */ - protected function error(string $message, array $arguments = [], array $data = []) + protected function error(string $message, array $arguments = [], array $data = []): void { /** @var Logger $logger */ $logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__); From cf960efaa8214632ce591738b50d6fb9f84a4cde Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Tue, 9 May 2023 11:39:57 +0200 Subject: [PATCH 44/54] [REFACTOR] Move knownRawFormats to static property --- Classes/Driver/CloudinaryDriver.php | 9 +++++++-- Classes/Services/CloudinaryScanService.php | 4 +--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Classes/Driver/CloudinaryDriver.php b/Classes/Driver/CloudinaryDriver.php index d6e5d76..1033444 100644 --- a/Classes/Driver/CloudinaryDriver.php +++ b/Classes/Driver/CloudinaryDriver.php @@ -38,9 +38,13 @@ class CloudinaryDriver extends AbstractHierarchicalFilesystemDriver { public const DRIVER_TYPE = 'VisolCloudinary'; + protected const ROOT_FOLDER_IDENTIFIER = '/'; + protected const UNSAFE_FILENAME_CHARACTER_EXPRESSION = '\\x00-\\x2C\\/\\x3A-\\x3F\\x5B-\\x60\\x7B-\\xBF'; + static public array $knownRawFormats = ['youtube', 'vimeo']; + /** * The base URL that points to this driver's storage. As long is this is not set, it is assumed that this folder * is not publicly available @@ -209,6 +213,7 @@ public function fileExists($fileIdentifier): bool $this->getCloudinaryPathService()->computeCloudinaryPublicId($fileIdentifier), ); } catch (\Exception $e) { + $fileIdentifier; return false; } @@ -488,7 +493,7 @@ public function createFolder($newFolderName, $parentFolderIdentifier = '', $recu $parentFolderIdentifier, $newFolderName, ); - $cloudinaryFolder = $this->getCloudinaryPathService()->normalizeCloudinaryPath($canonicalFolderPath); + $cloudinaryFolder = $this->getCloudinaryPathService()->normalizeCloudinaryPublicId($canonicalFolderPath); $this->log('[API][CREATE] Cloudinary\Api::createFolder() - folder "%s"', [$cloudinaryFolder], ['createFolder']); $response = $this->getApi()->create_folder($cloudinaryFolder); @@ -716,7 +721,7 @@ public function getFolderInfoByIdentifier($folderIdentifier): array return [ 'identifier' => $canonicalFolderIdentifier, 'name' => PathUtility::basename( - $this->getCloudinaryPathService()->normalizeCloudinaryPath($canonicalFolderIdentifier), + $this->getCloudinaryPathService()->normalizeCloudinaryPublicId($canonicalFolderIdentifier), ), 'storage' => $this->storageUid, ]; diff --git a/Classes/Services/CloudinaryScanService.php b/Classes/Services/CloudinaryScanService.php index 4d0a7f8..27840ad 100644 --- a/Classes/Services/CloudinaryScanService.php +++ b/Classes/Services/CloudinaryScanService.php @@ -41,8 +41,6 @@ class CloudinaryScanService protected string $additionalExpression = ''; - protected array $knownRawFormats = ['youtube', 'vimeo',]; - protected array $statistics = [ self::CREATED => 0, self::UPDATED => 0, @@ -135,7 +133,7 @@ public function scan(): array $this->console('Skipped processed file ' . $fileIdentifier); continue; } elseif ($resource['resource_type'] === 'raw' - && !in_array($resource['format'], $this->knownRawFormats, true)) { + && !in_array($resource['format'], CloudinaryDriver::$knownRawFormats, true)) { // Skip as well if the resource is of type raw // We might have problem when indexing video such as .youtube and .vimeo // which are not well-supported between cloudinary and typo3 From 23c3cc3086aced8e6ed941eeecedf4386a31a6de Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Tue, 9 May 2023 11:46:01 +0200 Subject: [PATCH 45/54] [BUGFIX] streamline computeCloudinaryPublicId method The computeCloudinaryPublicId method was simplified to use a ternary operator to check if the file extension is in the known raw formats. If it is, the file identifier is returned as is, otherwise the file extension is stripped and the resulting string is normalized as a cloudinary public id. The stripExtension method was renamed to stripFileExtension for clarity. --- Classes/Services/CloudinaryPathService.php | 32 +++++++++++++--------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/Classes/Services/CloudinaryPathService.php b/Classes/Services/CloudinaryPathService.php index 35f01bc..d1672f1 100644 --- a/Classes/Services/CloudinaryPathService.php +++ b/Classes/Services/CloudinaryPathService.php @@ -12,6 +12,7 @@ use TYPO3\CMS\Core\Resource\ResourceStorage; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\Utility\PathUtility; +use Visol\Cloudinary\Driver\CloudinaryDriver; class CloudinaryPathService { @@ -65,18 +66,22 @@ protected function getBasePath(): string public function computeCloudinaryPublicId(string $fileIdentifier): string { - $cloudinaryResource = $this->getCloudinaryResource($fileIdentifier); - return $this->normalizeCloudinaryPath($cloudinaryResource['public_id']); + $fileExtension = $this->getFileExtension($fileIdentifier); + $publicId = in_array($fileExtension, CloudinaryDriver::$knownRawFormats) + ? $fileIdentifier + : $this->stripFileExtension($fileIdentifier); + + return $this->normalizeCloudinaryPublicId($publicId); } public function computeCloudinaryFolderPath(string $folderIdentifier): string { - return $this->normalizeCloudinaryPath($folderIdentifier); + return $this->normalizeCloudinaryPublicId($folderIdentifier); } - public function normalizeCloudinaryPath(string $cloudinaryPath): string + public function normalizeCloudinaryPublicId(string $cloudinaryPublicId): string { - $normalizedCloudinaryPath = trim($cloudinaryPath, DIRECTORY_SEPARATOR); + $normalizedCloudinaryPath = trim($cloudinaryPublicId, DIRECTORY_SEPARATOR); $basePath = $this->getBasePath(); return $basePath ? trim($basePath . DIRECTORY_SEPARATOR . $normalizedCloudinaryPath, DIRECTORY_SEPARATOR) @@ -111,7 +116,7 @@ public function guessMimeType(array $cloudinaryResource): string protected function getCloudinaryResource(string $fileIdentifier): array { - $possiblePublicId = $this->stripExtension($fileIdentifier); + $possiblePublicId = $this->stripFileExtension($fileIdentifier); // We cache the resource for performance reasons. if (!isset($this->cachedCloudinaryResources[$possiblePublicId])) { @@ -130,7 +135,7 @@ protected function getCloudinaryResource(string $fileIdentifier): array $cloudinaryResource = $cloudinaryResourceService->getResource($fileIdentifier); } - // Houston, we have a real problem. The public id does not exist + // Houston, we have a problem. The public id does not exist, meaning the file does not exist. if (!$cloudinaryResource) { throw new \RuntimeException('Cloudinary resource not found for ' . $fileIdentifier, 1623157880); } @@ -141,12 +146,7 @@ protected function getCloudinaryResource(string $fileIdentifier): array return $this->cachedCloudinaryResources[$possiblePublicId]; } - /** - * @param $filename - * - * @return string - */ - protected function stripExtension(string $filename): string + protected function stripFileExtension(string $filename): string { $pathParts = PathUtility::pathinfo($filename); @@ -157,6 +157,12 @@ protected function stripExtension(string $filename): string return $pathParts['dirname'] . DIRECTORY_SEPARATOR . $pathParts['filename']; } + protected function getFileExtension(string $filename): string + { + $pathParts = PathUtility::pathinfo($filename); + return $pathParts['extension']; + } + public static function stripBasePathFromIdentifier(string $identifierWithBasePath, string $basePath): string { return preg_replace( From 463cd95772ee132cb90f1b23ef1b032cc38edcd9 Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Tue, 9 May 2023 14:12:04 +0200 Subject: [PATCH 46/54] [BUGFIX] Improve fileExists method to return true for processed files --- Classes/Driver/CloudinaryDriver.php | 43 +++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/Classes/Driver/CloudinaryDriver.php b/Classes/Driver/CloudinaryDriver.php index 1033444..f75427c 100644 --- a/Classes/Driver/CloudinaryDriver.php +++ b/Classes/Driver/CloudinaryDriver.php @@ -106,11 +106,8 @@ public function initialize(): void */ public function getPublicUrl($identifier): string { - // for processed file - $pattern = sprintf('/^PROCESSEDFILE\/(%s\/.*)/', $this->configurationService->get('cloudName')); - $matches = []; - if (preg_match($pattern, $identifier, $matches)) { - return 'https://res.cloudinary.com/' . $matches[1]; + if ($processedPath = $this->getProcessedPath($identifier)) { + return 'https://res.cloudinary.com/' . $processedPath; } $cloudinaryResource = $this->getCloudinaryResourceService()->getResource( @@ -155,6 +152,9 @@ public function getRootLevelFolder(): string */ public function getFileInfoByIdentifier($fileIdentifier, array $propertiesToExtract = []): array { + if ($this->isProcessedFile($fileIdentifier)) { + return []; + } $publicId = $this->getCloudinaryPathService()->computeCloudinaryPublicId($fileIdentifier); $cloudinaryResource = $this->getCloudinaryResourceService()->getResource($publicId); // We have a problem Hudson! @@ -208,15 +208,15 @@ protected function getResourceInfo(array $resource, string $name): string */ public function fileExists($fileIdentifier): bool { - try { - $cloudinaryResource = $this->getCloudinaryResourceService()->getResource( - $this->getCloudinaryPathService()->computeCloudinaryPublicId($fileIdentifier), - ); - } catch (\Exception $e) { - $fileIdentifier; - return false; + // Early return in case we have a processed file. + if ($this->isProcessedFile($fileIdentifier)) { + return true; } + $cloudinaryResource = $this->getCloudinaryResourceService()->getResource( + $this->getCloudinaryPathService()->computeCloudinaryPublicId($fileIdentifier), + ); + return !empty($cloudinaryResource); } @@ -1043,6 +1043,25 @@ public function getExplicitDataCacheRepository(): ExplicitDataCacheRepository return GeneralUtility::makeInstance(ExplicitDataCacheRepository::class); } + protected function getProcessedFilePattern(): string + { + return sprintf('/^PROCESSEDFILE\/(%s\/.*)/', $this->configurationService->get('cloudName')); + } + + protected function isProcessedFile(string $identifier): bool + { + return (bool)preg_match($this->getProcessedFilePattern(), $identifier); + } + + protected function getProcessedPath(string $identifier): string|null + { + $cloudinaryPath = null; + if (preg_match($this->getProcessedFilePattern(), $identifier, $matches)) { + [, $cloudinaryPath] = $matches; + } + return $cloudinaryPath; + } + protected function isFileIdentifier(string $newFileIdentifier): bool { return str_contains($newFileIdentifier, DIRECTORY_SEPARATOR); From a13eea3fdbd58cdbe6acc6399d270b782732e74b Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Tue, 9 May 2023 14:31:51 +0200 Subject: [PATCH 47/54] [CGL] phpstan --- Classes/Command/CloudinaryScanCommand.php | 1 + Classes/Driver/CloudinaryDriver.php | 2 +- .../BeforeFileProcessingEventHandler.php | 6 +- Classes/Services/CloudinaryPathService.php | 4 +- Classes/Services/FileMoveService.php | 6 +- phpstan-baseline.neon | 389 +----------------- 6 files changed, 27 insertions(+), 381 deletions(-) diff --git a/Classes/Command/CloudinaryScanCommand.php b/Classes/Command/CloudinaryScanCommand.php index b1cb26d..9f653de 100644 --- a/Classes/Command/CloudinaryScanCommand.php +++ b/Classes/Command/CloudinaryScanCommand.php @@ -62,6 +62,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->log('tail -f ' . $logFile); $this->log(); + /** @var string $expression */ $expression = $input->getOption('expression'); $result = $this->getCloudinaryScanService() diff --git a/Classes/Driver/CloudinaryDriver.php b/Classes/Driver/CloudinaryDriver.php index f75427c..44027b6 100644 --- a/Classes/Driver/CloudinaryDriver.php +++ b/Classes/Driver/CloudinaryDriver.php @@ -270,7 +270,7 @@ public function getFolderInFolder($folderName, $folderIdentifier): string * @param bool $removeOriginal if set the original file will be removed * after successful operation */ - public function addFile($localFilePath, $targetFolderIdentifier, $newFileName = '', $removeOriginal = true): bool + public function addFile($localFilePath, $targetFolderIdentifier, $newFileName = '', $removeOriginal = true): string { $fileName = $this->sanitizeFileName($newFileName !== '' ? $newFileName : PathUtility::basename($localFilePath)); diff --git a/Classes/EventHandlers/BeforeFileProcessingEventHandler.php b/Classes/EventHandlers/BeforeFileProcessingEventHandler.php index 2f421ca..9f9240c 100644 --- a/Classes/EventHandlers/BeforeFileProcessingEventHandler.php +++ b/Classes/EventHandlers/BeforeFileProcessingEventHandler.php @@ -33,7 +33,7 @@ public function __invoke(BeforeFileProcessingEvent $event): void return; } - if (str_starts_with($processedFile->getIdentifier() ?? '', 'PROCESSEDFILE')) { + if (str_starts_with($processedFile->getIdentifier(), 'PROCESSEDFILE')) { return; } @@ -56,15 +56,15 @@ public function __invoke(BeforeFileProcessingEvent $event): void $url = $explicitData['eager'][0]['secure_url']; $parts = parse_url($url); + $path = $parts['path'] ?? ''; $processedFile->setName(basename($url)); - $processedFile->setIdentifier('PROCESSEDFILE' . $parts['path']); + $processedFile->setIdentifier('PROCESSEDFILE' . $path); $processedFile->updateProperties([ 'width' => $explicitData['eager'][0]['width'], 'height' => $explicitData['eager'][0]['height'], ]); - /** @var $processedFileRepository ProcessedFileRepository */ $processedFileRepository = GeneralUtility::makeInstance(ProcessedFileRepository::class); $processedFileRepository->add($processedFile); } diff --git a/Classes/Services/CloudinaryPathService.php b/Classes/Services/CloudinaryPathService.php index d1672f1..c414678 100644 --- a/Classes/Services/CloudinaryPathService.php +++ b/Classes/Services/CloudinaryPathService.php @@ -159,8 +159,8 @@ protected function stripFileExtension(string $filename): string protected function getFileExtension(string $filename): string { - $pathParts = PathUtility::pathinfo($filename); - return $pathParts['extension']; + $pathInfo = PathUtility::pathinfo($filename); + return $pathInfo['extension'] ?? ''; } public static function stripBasePathFromIdentifier(string $identifierWithBasePath, string $basePath): string diff --git a/Classes/Services/FileMoveService.php b/Classes/Services/FileMoveService.php index 12f6256..6a5bb28 100644 --- a/Classes/Services/FileMoveService.php +++ b/Classes/Services/FileMoveService.php @@ -22,7 +22,7 @@ class FileMoveService { - protected $tableName = 'sys_file'; + protected string $tableName = 'sys_file'; protected ?CloudinaryPathService $cloudinaryPathService = null; @@ -75,7 +75,7 @@ public function fileExists(File $fileObject, ResourceStorage $targetStorage): bo # return $isUpdated && $isDeletedFromSourceStorage; #} - public function changeStorage(File $fileObject, ResourceStorage $targetStorage, $removeFile = true): bool + public function changeStorage(File $fileObject, ResourceStorage $targetStorage, bool $removeFile = true): bool { // Update the storage uid $isMigrated = (bool)$this->updateFile( @@ -178,7 +178,7 @@ protected function updateFile(File $fileObject, array $values): int ); } - protected function getCloudinaryPathService() + protected function getCloudinaryPathService(): CloudinaryPathService { return $this->cloudinaryPathService; } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 2949af0..12a5b0b 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,15 +1,5 @@ parameters: ignoreErrors: - - - message: "#^Call to an undefined method object\\:\\:get\\(\\)\\.$#" - count: 7 - path: Classes/Backend/Form/Container/InlineCloudinaryControlContainer.php - - - - message: "#^Call to an undefined method object\\:\\:getQueryBuilderForTable\\(\\)\\.$#" - count: 1 - path: Classes/Backend/Form/Container/InlineCloudinaryControlContainer.php - - message: "#^Call to method assignMultiple\\(\\) on an unknown class TYPO3\\\\CMS\\\\Fluid\\\\View\\\\StandaloneView\\.$#" count: 1 @@ -25,11 +15,6 @@ parameters: count: 1 path: Classes/Backend/Form/Container/InlineCloudinaryControlContainer.php - - - message: "#^Cannot call method fetchAllAssociativeIndexed\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#" - count: 1 - path: Classes/Backend/Form/Container/InlineCloudinaryControlContainer.php - - message: "#^Class TYPO3\\\\CMS\\\\Fluid\\\\View\\\\StandaloneView not found\\.$#" count: 1 @@ -55,11 +40,6 @@ parameters: count: 1 path: Classes/Backend/Form/Container/InlineCloudinaryControlContainer.php - - - message: "#^Call to an undefined method object\\:\\:get\\(\\)\\.$#" - count: 1 - path: Classes/CloudinaryFactory.php - - message: "#^Method Visol\\\\Cloudinary\\\\CloudinaryFactory\\:\\:getFolder\\(\\) should return TYPO3\\\\CMS\\\\Core\\\\Resource\\\\Folder but returns object\\.$#" count: 1 @@ -70,11 +50,6 @@ parameters: count: 1 path: Classes/CloudinaryFactory.php - - - message: "#^Cannot call method fetchAllAssociative\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#" - count: 1 - path: Classes/Command/AbstractCloudinaryCommand.php - - message: "#^Else branch is unreachable because previous condition is always true\\.$#" count: 1 @@ -225,11 +200,6 @@ parameters: count: 1 path: Classes/Command/CloudinaryCopyCommand.php - - - message: "#^Cannot call method fetchAllAssociative\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#" - count: 1 - path: Classes/Command/CloudinaryFixJpegCommand.php - - message: "#^Method Doctrine\\\\DBAL\\\\Driver\\\\Connection\\:\\:query\\(\\) invoked with 1 parameter, 0 required\\.$#" count: 1 @@ -255,26 +225,6 @@ parameters: count: 1 path: Classes/Command/CloudinaryFixJpegCommand.php - - - message: "#^Call to an undefined method object\\:\\:getAllSites\\(\\)\\.$#" - count: 1 - path: Classes/Command/CloudinaryMetadataCommand.php - - - - message: "#^Cannot call method fetchAllAssociative\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#" - count: 1 - path: Classes/Command/CloudinaryMetadataCommand.php - - - - message: "#^Method Visol\\\\Cloudinary\\\\Command\\\\CloudinaryMetadataCommand\\:\\:configure\\(\\) has no return type specified\\.$#" - count: 1 - path: Classes/Command/CloudinaryMetadataCommand.php - - - - message: "#^Method Visol\\\\Cloudinary\\\\Command\\\\CloudinaryMetadataCommand\\:\\:initialize\\(\\) has no return type specified\\.$#" - count: 1 - path: Classes/Command/CloudinaryMetadataCommand.php - - message: "#^Parameter \\#1 \\$fileIdentifier of method Visol\\\\Cloudinary\\\\Services\\\\CloudinaryPathService\\:\\:computeCloudinaryPublicId\\(\\) expects string, mixed given\\.$#" count: 1 @@ -285,11 +235,6 @@ parameters: count: 1 path: Classes/Command/CloudinaryMetadataCommand.php - - - message: "#^Property Visol\\\\Cloudinary\\\\Command\\\\CloudinaryMetadataCommand\\:\\:\\$cloudinaryPathService \\(Visol\\\\Cloudinary\\\\Services\\\\CloudinaryPathService\\) does not accept object\\.$#" - count: 1 - path: Classes/Command/CloudinaryMetadataCommand.php - - message: "#^Cannot call method exists\\(\\) on TYPO3\\\\CMS\\\\Core\\\\Resource\\\\File\\|TYPO3\\\\CMS\\\\Core\\\\Resource\\\\ProcessedFile\\|null\\.$#" count: 1 @@ -410,21 +355,11 @@ parameters: count: 1 path: Classes/Command/CloudinaryQueryCommand.php - - - message: "#^Method Visol\\\\Cloudinary\\\\Command\\\\CloudinaryScanCommand\\:\\:configure\\(\\) has no return type specified\\.$#" - count: 1 - path: Classes/Command/CloudinaryScanCommand.php - - message: "#^Method Visol\\\\Cloudinary\\\\Command\\\\CloudinaryScanCommand\\:\\:getCloudinaryScanService\\(\\) should return Visol\\\\Cloudinary\\\\Services\\\\CloudinaryScanService but returns object\\.$#" count: 1 path: Classes/Command/CloudinaryScanCommand.php - - - message: "#^Method Visol\\\\Cloudinary\\\\Command\\\\CloudinaryScanCommand\\:\\:initialize\\(\\) has no return type specified\\.$#" - count: 1 - path: Classes/Command/CloudinaryScanCommand.php - - message: "#^Parameter \\#1 \\$uid of method TYPO3\\\\CMS\\\\Core\\\\Resource\\\\ResourceFactory\\:\\:getStorageObject\\(\\) expects int\\|null, mixed given\\.$#" count: 1 @@ -435,21 +370,6 @@ parameters: count: 2 path: Classes/Controller/CloudinaryAjaxController.php - - - message: "#^Call to an undefined method object\\:\\:computeFileIdentifier\\(\\)\\.$#" - count: 1 - path: Classes/Controller/CloudinaryAjaxController.php - - - - message: "#^Call to an undefined method object\\:\\:get\\(\\)\\.$#" - count: 4 - path: Classes/Controller/CloudinaryAjaxController.php - - - - message: "#^Call to an undefined method object\\:\\:save\\(\\)\\.$#" - count: 1 - path: Classes/Controller/CloudinaryAjaxController.php - - message: "#^Cannot access offset 'cloudinaryIds' on array\\|object\\|null\\.$#" count: 1 @@ -470,16 +390,6 @@ parameters: count: 1 path: Classes/Controller/CloudinaryScanController.php - - - message: "#^Call to an undefined method object\\:\\:getPropertyFromAspect\\(\\)\\.$#" - count: 2 - path: Classes/Domain/Repository/ExplicitDataCacheRepository.php - - - - message: "#^Cannot call method fetchAssociative\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#" - count: 1 - path: Classes/Domain/Repository/ExplicitDataCacheRepository.php - - message: "#^PHPDoc tag @return with type object is not subtype of native type TYPO3\\\\CMS\\\\Core\\\\Database\\\\Connection\\.$#" count: 1 @@ -516,70 +426,45 @@ parameters: path: Classes/Driver/CloudinaryDriver.php - - message: "#^Call to an undefined method object\\:\\:computeCloudinaryFolderPath\\(\\)\\.$#" - count: 9 - path: Classes/Driver/CloudinaryDriver.php - - - - message: "#^Call to an undefined method object\\:\\:computeCloudinaryPublicId\\(\\)\\.$#" - count: 9 - path: Classes/Driver/CloudinaryDriver.php - - - - message: "#^Call to an undefined method object\\:\\:computeFileIdentifier\\(\\)\\.$#" + message: "#^Cannot access offset 0 on callable\\(\\)\\: mixed\\.$#" count: 1 path: Classes/Driver/CloudinaryDriver.php - - message: "#^Call to an undefined method object\\:\\:computeFolderIdentifier\\(\\)\\.$#" + message: "#^Cannot access offset 1 on callable\\(\\)\\: mixed\\.$#" count: 1 path: Classes/Driver/CloudinaryDriver.php - - message: "#^Call to an undefined method object\\:\\:delete\\(\\)\\.$#" - count: 1 + message: "#^Cannot cast mixed to int\\.$#" + count: 2 path: Classes/Driver/CloudinaryDriver.php - - message: "#^Call to an undefined method object\\:\\:getLogger\\(\\)\\.$#" + message: "#^Method Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:getCloudinaryFolderService\\(\\) should return Visol\\\\Cloudinary\\\\Services\\\\CloudinaryFolderService but returns object\\.$#" count: 1 path: Classes/Driver/CloudinaryDriver.php - - message: "#^Call to an undefined method object\\:\\:getResourceType\\(\\)\\.$#" - count: 5 - path: Classes/Driver/CloudinaryDriver.php - - - - message: "#^Call to an undefined method object\\:\\:guessMimeType\\(\\)\\.$#" + message: "#^Method Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:getCloudinaryPathService\\(\\) should return Visol\\\\Cloudinary\\\\Services\\\\CloudinaryPathService but returns object\\.$#" count: 1 path: Classes/Driver/CloudinaryDriver.php - - message: "#^Call to an undefined method object\\:\\:normalizeCloudinaryPath\\(\\)\\.$#" - count: 2 - path: Classes/Driver/CloudinaryDriver.php - - - - message: "#^Call to an undefined method object\\:\\:test\\(\\)\\.$#" + message: "#^Method Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:getCloudinaryResourceService\\(\\) should return Visol\\\\Cloudinary\\\\Services\\\\CloudinaryResourceService but returns object\\.$#" count: 1 path: Classes/Driver/CloudinaryDriver.php - - message: "#^Cannot access offset 0 on callable\\(\\)\\: mixed\\.$#" + message: "#^Method Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:getCloudinaryTestConnectionService\\(\\) should return Visol\\\\Cloudinary\\\\Services\\\\CloudinaryTestConnectionService but returns object\\.$#" count: 1 path: Classes/Driver/CloudinaryDriver.php - - message: "#^Cannot access offset 1 on callable\\(\\)\\: mixed\\.$#" + message: "#^Method Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:getExplicitDataCacheRepository\\(\\) should return Visol\\\\Cloudinary\\\\Domain\\\\Repository\\\\ExplicitDataCacheRepository but returns object\\.$#" count: 1 path: Classes/Driver/CloudinaryDriver.php - - - message: "#^Cannot cast mixed to int\\.$#" - count: 2 - path: Classes/Driver/CloudinaryDriver.php - - message: "#^Method Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:getFileContents\\(\\) should return string but returns string\\|false\\.$#" count: 1 @@ -590,21 +475,11 @@ parameters: count: 1 path: Classes/Driver/CloudinaryDriver.php - - - message: "#^Method Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:log\\(\\) has no return type specified\\.$#" - count: 1 - path: Classes/Driver/CloudinaryDriver.php - - message: "#^Method Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:sanitizeFileName\\(\\) should return string but returns string\\|null\\.$#" count: 1 path: Classes/Driver/CloudinaryDriver.php - - - message: "#^Negated boolean expression is always false\\.$#" - count: 3 - path: Classes/Driver/CloudinaryDriver.php - - message: "#^Offset 'extension' does not exist on array\\\\|string\\.$#" count: 1 @@ -636,29 +511,9 @@ parameters: path: Classes/Driver/CloudinaryDriver.php - - message: "#^Property Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:\\$charsetConversion \\(TYPO3\\\\CMS\\\\Core\\\\Charset\\\\CharsetConverter\\) does not accept object\\.$#" + message: "#^Method Visol\\\\Cloudinary\\\\EventHandlers\\\\BeforeFileProcessingEventHandler\\:\\:getCloudinaryImageService\\(\\) should return Visol\\\\Cloudinary\\\\Services\\\\CloudinaryImageService but returns object\\.$#" count: 1 - path: Classes/Driver/CloudinaryDriver.php - - - - message: "#^Property Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:\\$cloudinaryFolderService \\(Visol\\\\Cloudinary\\\\Services\\\\CloudinaryFolderService\\) does not accept object\\.$#" - count: 1 - path: Classes/Driver/CloudinaryDriver.php - - - - message: "#^Property Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:\\$cloudinaryPathService \\(Visol\\\\Cloudinary\\\\Services\\\\CloudinaryPathService\\) does not accept object\\.$#" - count: 1 - path: Classes/Driver/CloudinaryDriver.php - - - - message: "#^Property Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:\\$cloudinaryResourceService \\(Visol\\\\Cloudinary\\\\Services\\\\CloudinaryResourceService\\) does not accept object\\.$#" - count: 1 - path: Classes/Driver/CloudinaryDriver.php - - - - message: "#^Property Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:\\$configurationService \\(Visol\\\\Cloudinary\\\\Services\\\\ConfigurationService\\) does not accept object\\.$#" - count: 1 - path: Classes/Driver/CloudinaryDriver.php + path: Classes/EventHandlers/BeforeFileProcessingEventHandler.php - message: "#^Method Visol\\\\Cloudinary\\\\Filters\\\\RegularExpressionFilter\\:\\:filter\\(\\) should return bool but returns int\\|true\\.$#" @@ -680,38 +535,13 @@ parameters: count: 1 path: Classes/Filters/RegularExpressionFilter.php - - - message: "#^Call to an undefined method object\\:\\:delete\\(\\)\\.$#" - count: 1 - path: Classes/Hook/FileUploadHook.php - - - - message: "#^Call to an undefined method object\\:\\:getPublicIdForFile\\(\\)\\.$#" - count: 1 - path: Classes/Hook/FileUploadHook.php - - message: "#^Access to an undefined property Visol\\\\Cloudinary\\\\Services\\\\AbstractCloudinaryMediaService\\:\\:\\$explicitDataCacheRepository\\.$#" count: 2 path: Classes/Services/AbstractCloudinaryMediaService.php - - message: "#^Call to an undefined method object\\:\\:computeCloudinaryPublicId\\(\\)\\.$#" - count: 1 - path: Classes/Services/AbstractCloudinaryMediaService.php - - - - message: "#^Call to an undefined method object\\:\\:getLogger\\(\\)\\.$#" - count: 1 - path: Classes/Services/AbstractCloudinaryMediaService.php - - - - message: "#^Method Visol\\\\Cloudinary\\\\Services\\\\AbstractCloudinaryMediaService\\:\\:error\\(\\) has no return type specified\\.$#" - count: 1 - path: Classes/Services/AbstractCloudinaryMediaService.php - - - - message: "#^Method Visol\\\\Cloudinary\\\\Services\\\\AbstractCloudinaryMediaService\\:\\:getEmergencyPlaceholderFile\\(\\) should return TYPO3\\\\CMS\\\\Core\\\\Resource\\\\File but returns TYPO3\\\\CMS\\\\Core\\\\Resource\\\\FileInterface\\.$#" + message: "#^Method Visol\\\\Cloudinary\\\\Services\\\\AbstractCloudinaryMediaService\\:\\:getCloudinaryPathService\\(\\) should return Visol\\\\Cloudinary\\\\Services\\\\CloudinaryPathService but returns object\\.$#" count: 1 path: Classes/Services/AbstractCloudinaryMediaService.php @@ -730,21 +560,6 @@ parameters: count: 2 path: Classes/Services/CloudinaryFolderService.php - - - message: "#^Cannot call method fetchAllAssociative\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#" - count: 1 - path: Classes/Services/CloudinaryFolderService.php - - - - message: "#^Cannot call method fetchAssociative\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#" - count: 1 - path: Classes/Services/CloudinaryFolderService.php - - - - message: "#^Cannot call method fetchOne\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#" - count: 2 - path: Classes/Services/CloudinaryFolderService.php - - message: "#^Cannot cast mixed to int\\.$#" count: 2 @@ -795,11 +610,6 @@ parameters: count: 1 path: Classes/Services/CloudinaryImageService.php - - - message: "#^Property Visol\\\\Cloudinary\\\\Services\\\\CloudinaryImageService\\:\\:\\$explicitDataCacheRepository \\(Visol\\\\Cloudinary\\\\Domain\\\\Repository\\\\ExplicitDataCacheRepository\\) does not accept object\\.$#" - count: 1 - path: Classes/Services/CloudinaryImageService.php - - message: "#^Method Visol\\\\Cloudinary\\\\Services\\\\CloudinaryPathService\\:\\:stripBasePathFromIdentifier\\(\\) should return string but returns string\\|null\\.$#" count: 1 @@ -810,11 +620,6 @@ parameters: count: 1 path: Classes/Services/CloudinaryPathService.php - - - message: "#^Parameter \\#1 \\$string of function strtolower expects string, array\\\\|string given\\.$#" - count: 2 - path: Classes/Services/CloudinaryPathService.php - - message: "#^Call to an undefined method Doctrine\\\\DBAL\\\\Driver\\\\Connection\\:\\:delete\\(\\)\\.$#" count: 2 @@ -830,21 +635,6 @@ parameters: count: 2 path: Classes/Services/CloudinaryResourceService.php - - - message: "#^Cannot call method fetchAllAssociative\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#" - count: 1 - path: Classes/Services/CloudinaryResourceService.php - - - - message: "#^Cannot call method fetchAssociative\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#" - count: 1 - path: Classes/Services/CloudinaryResourceService.php - - - - message: "#^Cannot call method fetchOne\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#" - count: 2 - path: Classes/Services/CloudinaryResourceService.php - - message: "#^Cannot cast mixed to int\\.$#" count: 2 @@ -860,16 +650,6 @@ parameters: count: 2 path: Classes/Services/CloudinaryResourceService.php - - - message: "#^Call to an undefined method object\\:\\:getLogger\\(\\)\\.$#" - count: 1 - path: Classes/Services/CloudinaryScanService.php - - - - message: "#^Cannot call method fetchOne\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#" - count: 1 - path: Classes/Services/CloudinaryScanService.php - - message: "#^Method Visol\\\\Cloudinary\\\\Services\\\\CloudinaryScanService\\:\\:console\\(\\) has parameter \\$additionalBlankLine with no type specified\\.$#" count: 1 @@ -886,17 +666,12 @@ parameters: path: Classes/Services/CloudinaryScanService.php - - message: "#^Method Visol\\\\Cloudinary\\\\Services\\\\CloudinaryScanService\\:\\:getCloudinaryResourceService\\(\\) should return Visol\\\\Cloudinary\\\\Services\\\\CloudinaryResourceService but returns object\\.$#" + message: "#^Method Visol\\\\Cloudinary\\\\Services\\\\CloudinaryScanService\\:\\:getCloudinaryPathService\\(\\) should return Visol\\\\Cloudinary\\\\Services\\\\CloudinaryPathService but returns object\\.$#" count: 1 path: Classes/Services/CloudinaryScanService.php - - message: "#^Negated boolean expression is always false\\.$#" - count: 1 - path: Classes/Services/CloudinaryScanService.php - - - - message: "#^Property Visol\\\\Cloudinary\\\\Services\\\\CloudinaryScanService\\:\\:\\$cloudinaryPathService \\(Visol\\\\Cloudinary\\\\Services\\\\CloudinaryPathService\\) does not accept object\\.$#" + message: "#^Method Visol\\\\Cloudinary\\\\Services\\\\CloudinaryScanService\\:\\:getCloudinaryResourceService\\(\\) should return Visol\\\\Cloudinary\\\\Services\\\\CloudinaryResourceService but returns object\\.$#" count: 1 path: Classes/Services/CloudinaryScanService.php @@ -906,17 +681,7 @@ parameters: path: Classes/Services/CloudinaryTestConnectionService.php - - message: "#^Call to an undefined method object\\:\\:getLogger\\(\\)\\.$#" - count: 1 - path: Classes/Services/CloudinaryUploadService.php - - - - message: "#^Method Visol\\\\Cloudinary\\\\Services\\\\CloudinaryUploadService\\:\\:cleanUp\\(\\) has no return type specified\\.$#" - count: 1 - path: Classes/Services/CloudinaryUploadService.php - - - - message: "#^Method Visol\\\\Cloudinary\\\\Services\\\\CloudinaryUploadService\\:\\:error\\(\\) has no return type specified\\.$#" + message: "#^Method Visol\\\\Cloudinary\\\\Services\\\\CloudinaryUploadService\\:\\:uploadLocalFile\\(\\) should return TYPO3\\\\CMS\\\\Core\\\\Resource\\\\File but returns TYPO3\\\\CMS\\\\Core\\\\Resource\\\\FileInterface\\.$#" count: 1 path: Classes/Services/CloudinaryUploadService.php @@ -925,43 +690,18 @@ parameters: count: 1 path: Classes/Services/CloudinaryVideoService.php - - - message: "#^Call to an undefined method object\\:\\:computeCloudinaryPublicId\\(\\)\\.$#" - count: 1 - path: Classes/Services/Extractor/CloudinaryMetaDataExtractor.php - - - - message: "#^Call to an undefined method object\\:\\:get\\(\\)\\.$#" - count: 4 - path: Classes/Services/Extractor/CloudinaryMetaDataExtractor.php - - - - message: "#^Call to an undefined method object\\:\\:getResource\\(\\)\\.$#" - count: 1 - path: Classes/Services/Extractor/CloudinaryMetaDataExtractor.php - - message: "#^Call to an undefined method Doctrine\\\\DBAL\\\\Driver\\\\Connection\\:\\:update\\(\\)\\.$#" count: 1 path: Classes/Services/FileMoveService.php - - message: "#^Call to an undefined method object\\:\\:computeCloudinaryFolderPath\\(\\)\\.$#" - count: 1 - path: Classes/Services/FileMoveService.php - - - - message: "#^Call to an undefined method object\\:\\:computeCloudinaryPublicId\\(\\)\\.$#" - count: 2 - path: Classes/Services/FileMoveService.php - - - - message: "#^Call to an undefined method object\\:\\:getResourceType\\(\\)\\.$#" + message: "#^Method Visol\\\\Cloudinary\\\\Services\\\\FileMoveService\\:\\:ensureDirectoryExistence\\(\\) has no return type specified\\.$#" count: 1 path: Classes/Services/FileMoveService.php - - message: "#^Method Visol\\\\Cloudinary\\\\Services\\\\FileMoveService\\:\\:ensureDirectoryExistence\\(\\) has no return type specified\\.$#" + message: "#^Method Visol\\\\Cloudinary\\\\Services\\\\FileMoveService\\:\\:getCloudinaryPathService\\(\\) should return Visol\\\\Cloudinary\\\\Services\\\\CloudinaryPathService but returns Visol\\\\Cloudinary\\\\Services\\\\CloudinaryPathService\\|null\\.$#" count: 1 path: Classes/Services/FileMoveService.php @@ -975,61 +715,11 @@ parameters: count: 1 path: Classes/Services/FileMoveService.php - - - message: "#^PHPDoc tag @return with type object is not subtype of native type Doctrine\\\\DBAL\\\\Driver\\\\Connection\\.$#" - count: 1 - path: Classes/Services/FileMoveService.php - - - - message: "#^PHPDoc tag @return with type object is not subtype of native type TYPO3\\\\CMS\\\\Core\\\\Database\\\\Query\\\\QueryBuilder\\.$#" - count: 1 - path: Classes/Services/FileMoveService.php - - - - message: "#^Property Visol\\\\Cloudinary\\\\Services\\\\FileMoveService\\:\\:\\$cloudinaryPathService \\(Visol\\\\Cloudinary\\\\Services\\\\CloudinaryPathService\\) does not accept object\\.$#" - count: 1 - path: Classes/Services/FileMoveService.php - - message: "#^Variable \\$resource in empty\\(\\) always exists and is not falsy\\.$#" count: 1 path: Classes/Services/FileMoveService.php - - - message: "#^Call to an undefined method object\\:\\:add\\(\\)\\.$#" - count: 1 - path: Classes/Slots/FileProcessingSlot.php - - - - message: "#^Call to an undefined method object\\:\\:getExplicitData\\(\\)\\.$#" - count: 1 - path: Classes/Slots/FileProcessingSlot.php - - - - message: "#^Cannot access offset 'path' on array\\{scheme\\?\\: string, host\\?\\: string, port\\?\\: int\\<0, 65535\\>, user\\?\\: string, pass\\?\\: string, path\\?\\: string, query\\?\\: string, fragment\\?\\: string\\}\\|false\\.$#" - count: 1 - path: Classes/Slots/FileProcessingSlot.php - - - - message: "#^Expression on left side of \\?\\? is not nullable\\.$#" - count: 1 - path: Classes/Slots/FileProcessingSlot.php - - - - message: "#^Method Visol\\\\Cloudinary\\\\Slots\\\\FileProcessingSlot\\:\\:preFileProcess\\(\\) has no return type specified\\.$#" - count: 1 - path: Classes/Slots/FileProcessingSlot.php - - - - message: "#^Method Visol\\\\Cloudinary\\\\Slots\\\\FileProcessingSlot\\:\\:preFileProcess\\(\\) has parameter \\$taskType with no type specified\\.$#" - count: 1 - path: Classes/Slots/FileProcessingSlot.php - - - - message: "#^PHPDoc tag @var has invalid value \\(\\$processedFileRepository ProcessedFileRepository\\)\\: Unexpected token \"\\$processedFileRepository\", expected type at offset 9$#" - count: 1 - path: Classes/Slots/FileProcessingSlot.php - - message: "#^Method Visol\\\\Cloudinary\\\\Utility\\\\CloudinaryApiUtility\\:\\:initializeByConfiguration\\(\\) has no return type specified\\.$#" count: 1 @@ -1045,31 +735,6 @@ parameters: count: 1 path: Classes/ViewHelpers/CloudinaryImageDataViewHelper.php - - - message: "#^Call to an undefined method object\\:\\:computeCloudinaryPublicId\\(\\)\\.$#" - count: 1 - path: Classes/ViewHelpers/CloudinaryImageDataViewHelper.php - - - - message: "#^Call to an undefined method object\\:\\:generateOptionsFromSettings\\(\\)\\.$#" - count: 1 - path: Classes/ViewHelpers/CloudinaryImageDataViewHelper.php - - - - message: "#^Call to an undefined method object\\:\\:getImage\\(\\)\\.$#" - count: 3 - path: Classes/ViewHelpers/CloudinaryImageDataViewHelper.php - - - - message: "#^Call to an undefined method object\\:\\:getImageObjects\\(\\)\\.$#" - count: 1 - path: Classes/ViewHelpers/CloudinaryImageDataViewHelper.php - - - - message: "#^Call to an undefined method object\\:\\:getResponsiveBreakpointData\\(\\)\\.$#" - count: 1 - path: Classes/ViewHelpers/CloudinaryImageDataViewHelper.php - - message: "#^Cannot access offset 'path' on array\\{scheme\\?\\: string, host\\?\\: string, port\\?\\: int\\<0, 65535\\>, user\\?\\: string, pass\\?\\: string, path\\?\\: string, query\\?\\: string, fragment\\?\\: string\\}\\|false\\.$#" count: 1 @@ -1115,26 +780,6 @@ parameters: count: 1 path: Classes/ViewHelpers/CloudinaryImageViewHelper.php - - - message: "#^Call to an undefined method object\\:\\:generateOptionsFromSettings\\(\\)\\.$#" - count: 1 - path: Classes/ViewHelpers/CloudinaryImageViewHelper.php - - - - message: "#^Call to an undefined method object\\:\\:getResponsiveBreakpointData\\(\\)\\.$#" - count: 1 - path: Classes/ViewHelpers/CloudinaryImageViewHelper.php - - - - message: "#^Call to an undefined method object\\:\\:getSizesAttribute\\(\\)\\.$#" - count: 1 - path: Classes/ViewHelpers/CloudinaryImageViewHelper.php - - - - message: "#^Call to an undefined method object\\:\\:getSrcsetAttribute\\(\\)\\.$#" - count: 1 - path: Classes/ViewHelpers/CloudinaryImageViewHelper.php - - message: "#^Method Visol\\\\Cloudinary\\\\ViewHelpers\\\\CloudinaryImageViewHelper\\:\\:injectImageService\\(\\) has no return type specified\\.$#" count: 1 From 99f2264a01dd3c66e4565daf945cbd9e1e32c36b Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Tue, 9 May 2023 15:35:02 +0200 Subject: [PATCH 48/54] [STYLE] Add return type declarations to initialize and configure methods in Cloudinary commands --- Classes/Command/CloudinaryAcceptanceTestCommand.php | 4 ++-- Classes/Command/CloudinaryApiCommand.php | 4 ++-- Classes/Command/CloudinaryCopyCommand.php | 4 ++-- Classes/Command/CloudinaryFixJpegCommand.php | 4 ++-- Classes/Command/CloudinaryMoveCommand.php | 4 ++-- Classes/Command/CloudinaryQueryCommand.php | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Classes/Command/CloudinaryAcceptanceTestCommand.php b/Classes/Command/CloudinaryAcceptanceTestCommand.php index 5c5a606..a56d8dc 100644 --- a/Classes/Command/CloudinaryAcceptanceTestCommand.php +++ b/Classes/Command/CloudinaryAcceptanceTestCommand.php @@ -48,7 +48,7 @@ class CloudinaryAcceptanceTestCommand extends AbstractCloudinaryCommand /** * Configure the command by defining the name, options and arguments */ - protected function configure() + protected function configure(): void { $message = 'Run a suite of Acceptance Tests'; $this @@ -65,7 +65,7 @@ protected function configure() ); } - protected function initialize(InputInterface $input, OutputInterface $output) + protected function initialize(InputInterface $input, OutputInterface $output): void { $this->io = new SymfonyStyle($input, $output); } diff --git a/Classes/Command/CloudinaryApiCommand.php b/Classes/Command/CloudinaryApiCommand.php index 9e8de84..4362f36 100644 --- a/Classes/Command/CloudinaryApiCommand.php +++ b/Classes/Command/CloudinaryApiCommand.php @@ -44,7 +44,7 @@ class CloudinaryApiCommand extends AbstractCloudinaryCommand typo3 cloudinary:api [0-9] --expression=\'resource_type:image AND tags=kitten AND uploaded_at>1d\' ' ; - protected function initialize(InputInterface $input, OutputInterface $output) + protected function initialize(InputInterface $input, OutputInterface $output): void { $this->io = new SymfonyStyle($input, $output); @@ -53,7 +53,7 @@ protected function initialize(InputInterface $input, OutputInterface $output) $this->storage = $resourceFactory->getStorageObject($input->getArgument('storage')); } - protected function configure() + protected function configure(): void { $message = 'Interact with cloudinary API'; $this->setDescription($message) diff --git a/Classes/Command/CloudinaryCopyCommand.php b/Classes/Command/CloudinaryCopyCommand.php index ff824c8..f9b5b2e 100644 --- a/Classes/Command/CloudinaryCopyCommand.php +++ b/Classes/Command/CloudinaryCopyCommand.php @@ -29,7 +29,7 @@ class CloudinaryCopyCommand extends AbstractCloudinaryCommand protected ResourceStorage $targetStorage; - protected function initialize(InputInterface $input, OutputInterface $output) + protected function initialize(InputInterface $input, OutputInterface $output): void { $this->io = new SymfonyStyle($input, $output); @@ -44,7 +44,7 @@ protected function initialize(InputInterface $input, OutputInterface $output) /** * Configure the command by defining the name, options and arguments */ - protected function configure() + protected function configure(): void { $this->setDescription('Copy bunch of images from a local storage to a cloudinary storage') ->addOption('silent', 's', InputOption::VALUE_OPTIONAL, 'Mute output as much as possible', false) diff --git a/Classes/Command/CloudinaryFixJpegCommand.php b/Classes/Command/CloudinaryFixJpegCommand.php index 4d09df3..a99a3a3 100644 --- a/Classes/Command/CloudinaryFixJpegCommand.php +++ b/Classes/Command/CloudinaryFixJpegCommand.php @@ -25,7 +25,7 @@ class CloudinaryFixJpegCommand extends AbstractCloudinaryCommand protected string $tableName = 'sys_file'; - protected function initialize(InputInterface $input, OutputInterface $output) + protected function initialize(InputInterface $input, OutputInterface $output): void { $this->io = new SymfonyStyle($input, $output); @@ -39,7 +39,7 @@ protected function initialize(InputInterface $input, OutputInterface $output) /** * Configure the command by defining the name, options and arguments */ - protected function configure() + protected function configure(): void { $message = 'After "moving" files you should fix the jpeg extension. Consult README.md for more info.'; $this->setDescription($message) diff --git a/Classes/Command/CloudinaryMoveCommand.php b/Classes/Command/CloudinaryMoveCommand.php index 5f71c24..a5bb353 100644 --- a/Classes/Command/CloudinaryMoveCommand.php +++ b/Classes/Command/CloudinaryMoveCommand.php @@ -38,7 +38,7 @@ class CloudinaryMoveCommand extends AbstractCloudinaryCommand /** * Configure the command by defining the name, options and arguments */ - protected function configure() + protected function configure(): void { $message = 'Move bunch of images to a cloudinary storage. Consult the README.md for more info.'; $this->setDescription($message) @@ -54,7 +54,7 @@ protected function configure() ->setHelp('Usage: ./vendor/bin/typo3 cloudinary:move 1 2'); } - protected function initialize(InputInterface $input, OutputInterface $output) + protected function initialize(InputInterface $input, OutputInterface $output): void { $this->io = new SymfonyStyle($input, $output); diff --git a/Classes/Command/CloudinaryQueryCommand.php b/Classes/Command/CloudinaryQueryCommand.php index 2810c68..7841cab 100644 --- a/Classes/Command/CloudinaryQueryCommand.php +++ b/Classes/Command/CloudinaryQueryCommand.php @@ -26,7 +26,7 @@ class CloudinaryQueryCommand extends AbstractCloudinaryCommand { protected ResourceStorage $storage; - protected function initialize(InputInterface $input, OutputInterface $output) + protected function initialize(InputInterface $input, OutputInterface $output): void { $this->io = new SymfonyStyle($input, $output); @@ -60,7 +60,7 @@ protected function initialize(InputInterface $input, OutputInterface $output) /** * Configure the command by defining the name, options and arguments */ - protected function configure() + protected function configure(): void { $message = 'Query a given storage such a list, count files or folders'; $this->setDescription($message) From 1543f558654e4ade8f3d7f1d8d2fb7a037424f20 Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Tue, 9 May 2023 17:03:07 +0200 Subject: [PATCH 49/54] [FEATURE] Enable to recursively delete files from the command cloudinary:api --- Classes/Command/CloudinaryApiCommand.php | 81 ++++++++++++++++++++---- 1 file changed, 68 insertions(+), 13 deletions(-) diff --git a/Classes/Command/CloudinaryApiCommand.php b/Classes/Command/CloudinaryApiCommand.php index 4362f36..444546f 100644 --- a/Classes/Command/CloudinaryApiCommand.php +++ b/Classes/Command/CloudinaryApiCommand.php @@ -10,6 +10,7 @@ */ use Cloudinary\Api; +use Cloudinary\Search; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -20,6 +21,7 @@ use TYPO3\CMS\Core\Resource\ResourceFactory; use TYPO3\CMS\Core\Resource\ResourceStorage; use TYPO3\CMS\Core\Utility\GeneralUtility; +use Visol\Cloudinary\Driver\CloudinaryDriver; use Visol\Cloudinary\Services\CloudinaryPathService; use Visol\Cloudinary\Utility\CloudinaryApiUtility; @@ -30,18 +32,24 @@ class CloudinaryApiCommand extends AbstractCloudinaryCommand protected string $help = ' Usage: ./vendor/bin/typo3 cloudinary:api [storage-uid] -Examples +Examples: # Query by public id -typo3 cloudinary:api [0-9] --publicId=\'foo-bar\' +typo3 cloudinary:api [0-9] --publicId="foo-bar" -# Query by file uid -typo3 cloudinary:api --fileUid=\'[0-9]\' +# Query by file uid (will retrieve the public id from the file) +typo3 cloudinary:api --fileUid="[0-9]" # Query with an expression # @see https://cloudinary.com/documentation/search_api -typo3 cloudinary:api [0-9] --expression=\'public_id:foo-bar\' -typo3 cloudinary:api [0-9] --expression=\'resource_type:image AND tags=kitten AND uploaded_at>1d\' +typo3 cloudinary:api [0-9] --expression="public_id:foo-bar" +typo3 cloudinary:api [0-9] --expression="resource_type:image AND tags=kitten AND uploaded_at>1d" + +# List the resources instead of the whole resource +typo3 cloudinary:api [0-9] --expression="folder=fileadmin/_processed_/*" --list + +# Delete the resources according to the expression +typo3 cloudinary:api [0-9] --expression="folder=fileadmin/_processed_/*" --delete ' ; protected function initialize(InputInterface $input, OutputInterface $output): void @@ -60,7 +68,9 @@ protected function configure(): void ->addOption('silent', 's', InputOption::VALUE_OPTIONAL, 'Mute output as much as possible', false) ->addOption('fileUid', '', InputOption::VALUE_OPTIONAL, 'File uid', '') ->addOption('publicId', '', InputOption::VALUE_OPTIONAL, 'Cloudinary public id', '') - ->addOption('expression', '', InputOption::VALUE_OPTIONAL, 'Cloudinary search expression', '') + ->addOption('expression', '', InputOption::VALUE_OPTIONAL, 'Cloudinary search expression e.g --expression="folder=fileadmin/*"', '') + ->addOption('list', '', InputOption::VALUE_OPTIONAL, 'List instead of the whole resource --expression="folder=fileadmin/_processed_/*" --list', false) + ->addOption('delete', '', InputOption::VALUE_OPTIONAL, 'Delete the resources --expression="folder=fileadmin/*" --delete', false) ->addArgument('storage', InputArgument::OPTIONAL, 'Storage identifier') ->setHelp($this->help); } @@ -74,9 +84,20 @@ protected function execute(InputInterface $input, OutputInterface $output): int $publicId = $input->getOption('publicId'); $expression = $input->getOption('expression'); + $list = $input->getOption('list') === null; + $delete = $input->getOption('delete') === null; + + if ($delete) { + // ask the user whether it should continue + $continue = $this->io->confirm('Are you sure you want to delete the resources?'); + if (!$continue) { + $this->log('Aborting...'); + return Command::SUCCESS; + } + } - // @phpstan-ignore-next-line - $fileUid = (int)$input->getOption('fileUid'); + /** @var int $fileUid */ + $fileUid = $input->getOption('fileUid'); if ($fileUid) { /** @var ResourceFactory $resourceFactory */ $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class); @@ -92,10 +113,44 @@ protected function execute(InputInterface $input, OutputInterface $output): int $resource = $this->getApi()->resource($publicId); $this->log(var_export((array)$resource, true)); } elseif ($expression) { - $search = new \Cloudinary\Search(); - $search->expression($expression); - $response = $search->execute(); - $this->log(var_export((array)$response, true)); + + $counter = 0; + do { + $nextCursor = isset($response) + ? $response['next_cursor'] + : ''; + + /** @var Search $search */ + $search = new Search(); + + $response = $search + ->expression($expression) + ->sort_by('public_id', 'asc') + ->max_results(100) + ->next_cursor($nextCursor) + ->execute(); + + if (is_array($response['resources'])) { + $_resources = []; + foreach ($response['resources'] as $resource) { + if ($list || $delete) { + $this->log($resource['public_id']); + } else { + $this->log(var_export((array)$resource, true)); + } + + // collect resources in case of deletion. + $_resources[] = $resource['public_id']; + } + // delete the resource if told + if ($delete) { + $counter++; + $this->log("\nDeleting batch #$counter...\n"); + $this->getApi()->delete_resources($_resources); + } + } + } while (!empty($response) && isset($response['next_cursor'])); + } else { $this->log('Nothing to do...'); } From cdd25b68f642b4447882e0f1ec1a3a820bbc2e10 Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Tue, 9 May 2023 17:33:42 +0200 Subject: [PATCH 50/54] [ENHANCE] Add help text to cloudinary:scan command --- Classes/Command/CloudinaryScanCommand.php | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/Classes/Command/CloudinaryScanCommand.php b/Classes/Command/CloudinaryScanCommand.php index 9f653de..f11efb2 100644 --- a/Classes/Command/CloudinaryScanCommand.php +++ b/Classes/Command/CloudinaryScanCommand.php @@ -25,6 +25,25 @@ class CloudinaryScanCommand extends AbstractCloudinaryCommand { protected ResourceStorage $storage; + protected string $help = ' +Usage: ./vendor/bin/typo3 cloudinary:scan [0-9] + +Examples: + +# Query by public id +typo3 cloudinary:scan + +# Query with an additional expression +typo3 cloudinary:scan --expression="folder=fileadmin/* AND NOT folder:fileadmin/_processed_/*" + +Notice: + +You can search for an exact folder path with "folder=fileadmin/*" +or you can search for a folder prefix with "folder:fileadmin/*" +@see https://cloudinary.com/documentation/search_api + ' ; + + protected function initialize(InputInterface $input, OutputInterface $output): void { $this->io = new SymfonyStyle($input, $output); @@ -47,7 +66,7 @@ protected function configure(): void false ) ->addArgument('storage', InputArgument::REQUIRED, 'Storage identifier') - ->setHelp('Usage: ./vendor/bin/typo3 cloudinary:scan [0-9]'); + ->setHelp($this->help); } protected function execute(InputInterface $input, OutputInterface $output): int From b9beafc76f1ce464ff5bab7cbe8bcf5141cc1f75 Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Tue, 9 May 2023 18:38:27 +0200 Subject: [PATCH 51/54] [REFACTOR] Remove redundant prefix from log messages in CloudinaryDriver class --- Classes/Driver/CloudinaryDriver.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Classes/Driver/CloudinaryDriver.php b/Classes/Driver/CloudinaryDriver.php index 44027b6..61ac6a8 100644 --- a/Classes/Driver/CloudinaryDriver.php +++ b/Classes/Driver/CloudinaryDriver.php @@ -283,7 +283,7 @@ public function addFile($localFilePath, $targetFolderIdentifier, $newFileName = $cloudinaryPublicId = $this->getCloudinaryPathService()->computeCloudinaryPublicId($fileIdentifier); $this->log( - '[API][UPLOAD] Cloudinary\Uploader::upload() - add resource "%s"', + '[API] Cloudinary\Uploader::upload() - add resource "%s"', [$cloudinaryPublicId], ['addFile()'], ); @@ -388,7 +388,7 @@ public function deleteFile($fileIdentifier): bool { $cloudinaryPublicId = $this->getCloudinaryPathService()->computeCloudinaryPublicId($fileIdentifier); $this->log( - '[API][DELETE] Cloudinary\Api::delete_resources - delete resource "%s"', + '[API] Cloudinary\Api::delete_resources - delete resource "%s"', [$cloudinaryPublicId], ['deleteFile'], ); @@ -418,7 +418,7 @@ public function deleteFolder($folderIdentifier, $deleteRecursively = false): boo if ($deleteRecursively) { $this->log( - '[API][DELETE] Cloudinary\Api::delete_resources_by_prefix() - folder "%s"', + '[API] Cloudinary\Api::delete_resources_by_prefix() - folder "%s"', [$cloudinaryFolder], ['deleteFolder'], ); @@ -434,7 +434,7 @@ public function deleteFolder($folderIdentifier, $deleteRecursively = false): boo // We make sure the folder exists first. It will also delete sub-folder if those ones are empty. if ($this->folderExists($folderIdentifier)) { $this->log( - '[API][DELETE] Cloudinary\Api::delete_folder() - folder "%s"', + '[API] Cloudinary\Api::delete_folder() - folder "%s"', [$cloudinaryFolder], ['deleteFolder'], ); @@ -495,7 +495,7 @@ public function createFolder($newFolderName, $parentFolderIdentifier = '', $recu ); $cloudinaryFolder = $this->getCloudinaryPathService()->normalizeCloudinaryPublicId($canonicalFolderPath); - $this->log('[API][CREATE] Cloudinary\Api::createFolder() - folder "%s"', [$cloudinaryFolder], ['createFolder']); + $this->log('[API] Cloudinary\Api::createFolder() - folder "%s"', [$cloudinaryFolder], ['createFolder']); $response = $this->getApi()->create_folder($cloudinaryFolder); if (!$response['success']) { From d8e29f8060500530cc891962511d4de2f282a030 Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Tue, 9 May 2023 18:39:32 +0200 Subject: [PATCH 52/54] [ENHANCE] Avoid creating empty processed folder --- Classes/Driver/CloudinaryDriver.php | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/Classes/Driver/CloudinaryDriver.php b/Classes/Driver/CloudinaryDriver.php index 61ac6a8..27d2eec 100644 --- a/Classes/Driver/CloudinaryDriver.php +++ b/Classes/Driver/CloudinaryDriver.php @@ -225,6 +225,11 @@ public function fileExists($fileIdentifier): bool */ public function folderExists($folderIdentifier): bool { + // Early return in case we have a processed file. + if ($this->isProcessedFolder($folderIdentifier)) { + return true; + } + if ($folderIdentifier === self::ROOT_FOLDER_IDENTIFIER) { return true; } @@ -1053,7 +1058,20 @@ protected function isProcessedFile(string $identifier): bool return (bool)preg_match($this->getProcessedFilePattern(), $identifier); } - protected function getProcessedPath(string $identifier): string|null + protected function isProcessedFolder(string $identifier): bool + { + $storageRecord = $this->getStorageObject()->getStorageRecord(); + + // Example value for $storageRecord['processingfolder'] is "2:/_processed_" + // we want to remove the "2:" from the expression + $processedStorageFolderName = $storageRecord['processingfolder'] ?? '_processed_'; + $folderPath = preg_replace('/^[0-9]+:/', '', $processedStorageFolderName); + + // We detect if the identifier start with the value from $folderPath + return str_starts_with($identifier, $folderPath); + } + + protected function computeProcessedPath(string $identifier): string|null { $cloudinaryPath = null; if (preg_match($this->getProcessedFilePattern(), $identifier, $matches)) { From 228eda5b784ae3004c8a039cf7141dae4a88f570 Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Tue, 9 May 2023 18:40:06 +0200 Subject: [PATCH 53/54] [REFACTOR] Centralize getStorageObject() call --- Classes/Driver/CloudinaryDriver.php | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/Classes/Driver/CloudinaryDriver.php b/Classes/Driver/CloudinaryDriver.php index 27d2eec..a7021b7 100644 --- a/Classes/Driver/CloudinaryDriver.php +++ b/Classes/Driver/CloudinaryDriver.php @@ -59,8 +59,6 @@ class CloudinaryDriver extends AbstractHierarchicalFilesystemDriver protected ConfigurationService $configurationService; - protected ?ResourceStorage $storage = null; - protected CharsetConverter $charsetConversion; protected ?CloudinaryPathService $cloudinaryPathService = null; @@ -106,7 +104,7 @@ public function initialize(): void */ public function getPublicUrl($identifier): string { - if ($processedPath = $this->getProcessedPath($identifier)) { + if ($processedPath = $this->computeProcessedPath($identifier)) { return 'https://res.cloudinary.com/' . $processedPath; } @@ -1103,14 +1101,10 @@ protected function canonicalizeFolderIdentifierAndFileName(string $folderIdentif protected function getCloudinaryPathService(): CloudinaryPathService { if (!$this->cloudinaryPathService) { - if ($this->storageUid) { - $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class); - $storage = $resourceFactory->getStorageObject($this->storageUid); - } $this->cloudinaryPathService = GeneralUtility::makeInstance( CloudinaryPathService::class, $this->storageUid - ? $storage + ? $this->getStorageObject() : $this->configuration, ); } @@ -1118,15 +1112,20 @@ protected function getCloudinaryPathService(): CloudinaryPathService return $this->cloudinaryPathService; } + protected function getStorageObject(): ResourceStorage + { + /** @var ResourceFactory $resourceFactory */ + $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class); + return $resourceFactory->getStorageObject($this->storageUid); + } + protected function getCloudinaryResourceService(): CloudinaryResourceService { if (!$this->cloudinaryResourceService) { - /** @var ResourceFactory $resourceFactory */ - $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class); $this->cloudinaryResourceService = GeneralUtility::makeInstance( CloudinaryResourceService::class, - $resourceFactory->getStorageObject($this->storageUid), + $this->getStorageObject() ); } From 62b4d5bb19fb3eb61ed828f420ec6dae95eda9d0 Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Wed, 10 May 2023 11:10:36 +0200 Subject: [PATCH 54/54] [REFACTOR] Introduce MimeTypeUtility class to handle mime type guessing --- Classes/Driver/CloudinaryDriver.php | 20 +-- Classes/Services/CloudinaryPathService.php | 15 -- Classes/Utility/MimeTypeUtility.php | 152 +++++++++++++++++++++ 3 files changed, 154 insertions(+), 33 deletions(-) create mode 100644 Classes/Utility/MimeTypeUtility.php diff --git a/Classes/Driver/CloudinaryDriver.php b/Classes/Driver/CloudinaryDriver.php index a7021b7..e79954f 100644 --- a/Classes/Driver/CloudinaryDriver.php +++ b/Classes/Driver/CloudinaryDriver.php @@ -34,6 +34,7 @@ use Visol\Cloudinary\Services\CloudinaryTestConnectionService; use Visol\Cloudinary\Services\ConfigurationService; use Visol\Cloudinary\Utility\CloudinaryFileUtility; +use Visol\Cloudinary\Utility\MimeTypeUtility; class CloudinaryDriver extends AbstractHierarchicalFilesystemDriver { @@ -163,29 +164,12 @@ public function getFileInfoByIdentifier($fileIdentifier, array $propertiesToExtr ); } - $mimeType = $this->getCloudinaryPathService()->guessMimeType($cloudinaryResource); - if (!$mimeType) { - $this->log( - 'Just a notice! Time consuming action ahead. I am going to download a file "%s"', - [$fileIdentifier], - ['getFileInfoByIdentifier'], - ); - - // We are force to download the file in order to correctly find the mime type. - $localFile = $this->getFileForLocalProcessing($fileIdentifier); - - /** @var FileInfo $fileInfo */ - $fileInfo = GeneralUtility::makeInstance(FileInfo::class, $localFile); - - $mimeType = $fileInfo->getMimeType(); - } - return [ 'identifier_hash' => $this->hashIdentifier($fileIdentifier), 'folder_hash' => sha1($this->canonicalizeAndCheckFolderIdentifier(PathUtility::dirname($fileIdentifier))), 'creation_date' => strtotime($cloudinaryResource['created_at']), 'modification_date' => strtotime($cloudinaryResource['created_at']), - 'mime_type' => $mimeType, + 'mime_type' => MimeTypeUtility::guessMimeType($cloudinaryResource['format']), 'extension' => $this->getResourceInfo($cloudinaryResource, 'format'), 'size' => $this->getResourceInfo($cloudinaryResource, 'bytes'), 'width' => $this->getResourceInfo($cloudinaryResource, 'width'), diff --git a/Classes/Services/CloudinaryPathService.php b/Classes/Services/CloudinaryPathService.php index c414678..5ac4b2b 100644 --- a/Classes/Services/CloudinaryPathService.php +++ b/Classes/Services/CloudinaryPathService.php @@ -99,21 +99,6 @@ public function getResourceType(string $fileIdentifier): string return $cloudinaryResource['resource_type'] ?? 'unknown'; } - public function guessMimeType(array $cloudinaryResource): string - { - $mimeType = ''; - if ($cloudinaryResource['format'] === 'pdf') { - $mimeType = 'application/pdf'; - } elseif ($cloudinaryResource['format'] === 'jpg') { - $mimeType = 'image/jpeg'; - } elseif ($cloudinaryResource['format'] === 'png') { - $mimeType = 'image/png'; - } elseif ($cloudinaryResource['format'] === 'mp4') { - $mimeType = 'video/mp4'; - } - return $mimeType; - } - protected function getCloudinaryResource(string $fileIdentifier): array { $possiblePublicId = $this->stripFileExtension($fileIdentifier); diff --git a/Classes/Utility/MimeTypeUtility.php b/Classes/Utility/MimeTypeUtility.php new file mode 100644 index 0000000..9abf4f1 --- /dev/null +++ b/Classes/Utility/MimeTypeUtility.php @@ -0,0 +1,152 @@ + 'text/plain', + 'htm' => 'text/html', + 'html' => 'text/html', + 'php' => 'text/html', + 'css' => 'text/css', + 'js' => 'text/javascript', + 'csv' => 'text/comma-separated-values', + 'ics' => 'text/calendar', + 'log' => 'text/x-log', + 'zsh' => 'text/x-scriptzsh', + 'rtx' => 'text/richtext', + 'srt' => 'text/srt', + 'vcf' => 'text/x-vcard', + 'vtt' => 'text/vtt', + 'xsl' => 'text/xsl', + + // images + 'png' => 'image/png', + 'jpe' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'jpg' => 'image/jpeg', + 'gif' => 'image/gif', + 'bmp' => 'image/bmp', + 'ico' => 'image/vnd.microsoft.icon', + 'tiff' => 'image/tiff', + 'tif' => 'image/tiff', + 'svg' => 'image/svg+xml', + 'svgz' => 'image/svg+xml', + 'json' => 'text/json', + 'cdr' => 'image/cdr', + + // audio + 'mp3' => 'audio/mpeg', + 'qt' => 'video/quicktime', + 'aac' => 'audio/x-acc', + 'ac3' => 'audio/ac3', + 'aif' => 'audio/aiff', + 'au' => 'audio/x-au', + 'flac' => 'audio/x-flac', + 'm4a' => 'audio/x-m4a', + 'mid' => 'audio/midi', + 'ra' => 'audio/x-realaudio', + 'ram' => 'audio/x-pn-realaudio', + 'rpm' => 'audio/x-pn-realaudio-plugin', + 'wma' => 'audio/x-ms-wma', + + // video + 'youtube' => 'video/youtube', + 'vimeo' => 'video/vimeo', + 'mov' => 'video/quicktime', + 'movie' => 'video/x-sgi-movie', + 'mp4' => 'video/mp4', + 'mpeg' => 'video/mpeg', + 'ogg' => 'video/ogg', + 'rv' => 'video/vnd.rn-realvideo', + 'webm' => 'video/webm', + 'wmv' => 'video/x-ms-wmv', + '3g2' => 'video/3gpp2', + '3gp' => 'video/3gp', + 'avi' => 'video/avi', + 'f4v' => 'video/x-f4v', + 'flv' => 'video/x-flv', + 'jp2' => 'video/mj2', + + // adobe + 'pdf' => 'application/pdf', + 'psd' => 'image/vnd.adobe.photoshop', + 'ai' => 'application/postscript', + 'eps' => 'application/postscript', + 'ps' => 'application/postscript', + 'xml' => 'application/xml', + 'swf' => 'application/x-shockwave-flash', + + // ms office + 'doc' => 'application/msword', + 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'rtf' => 'application/rtf', + 'xls' => 'application/vnd.ms-excel', + 'xlsx' => 'application/vnd.ms-excel', + 'ppt' => 'application/vnd.ms-powerpoint', + 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + + // open office + 'odt' => 'application/vnd.oasis.opendocument.text', + 'ods' => 'application/vnd.oasis.opendocument.spreadsheet', + 'odp' => 'application/vnd.oasis.opendocument.presentation', + + // archives + 'zip' => 'application/zip', + 'rar' => 'application/x-rar-compressed', + 'exe' => 'application/x-msdownload', + 'msi' => 'application/x-msdownload', + 'cab' => 'application/vnd.ms-cab-compressed', + + // other + '7zip' => 'application/x-compressed', + 'cpt' => 'application/mac-compactpro', + 'dcr' => 'application/x-director', + 'dvi' => 'application/x-dvi', + 'gpg' => 'application/gpg-keys', + 'gtar' => 'application/x-gtar', + 'gzip' => 'application/x-gzip', + 'kml' => 'application/vnd.google-earth.kml+xml', + 'kmz' => 'application/vnd.google-earth.kmz', + 'm4u' => 'application/vnd.mpegurl', + 'mif' => 'application/vnd.mif', + 'p10' => 'application/pkcs10', + 'p12' => 'application/x-pkcs12', + 'p7a' => 'application/x-pkcs7-signature', + 'p7c' => 'application/pkcs7-mime', + 'p7r' => 'application/x-pkcs7-certreqresp', + 'p7s' => 'application/pkcs7-signature', + 'pem' => 'application/x-pem-file', + 'pgp' => 'application/pgp', + 'sit' => 'application/x-stuffit', + 'smil' => 'application/smil', + 'tar' => 'application/x-tar', + 'tgz' => 'application/x-gzip-compressed', + 'vlc' => 'application/videolan', + 'wbxml' => 'application/wbxml', + 'wmlc' => 'application/wmlc', + 'xhtml' => 'application/xhtml+xml', + 'xl' => 'application/excel', + 'xspf' => 'application/xspf+xml', + 'z' => 'application/x-compress', + + ]; + + return array_key_exists($fileExtension, $mimeTypes) + ? $mimeTypes[$fileExtension] + : 'application/octet-stream'; + } +}