From 1dd76dcbae57e2a4401f945267bab03ab5d56e02 Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Wed, 1 Mar 2023 15:10:00 +0000 Subject: [PATCH 01/99] [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/99] [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/99] [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/99] 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/99] [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/99] [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/99] [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/99] 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/99] [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/99] [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/99] [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/99] [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/99] [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/99] 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/99] [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/99] [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/99] [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/99] [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/99] [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/99] [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/99] [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/99] [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/99] [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/99] [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/99] [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/99] [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/99] [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/99] [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 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%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/99] [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/99] [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/99] [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/99] [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/99] 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/99] [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/99] [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/99] [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/99] [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/99] [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/99] [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/99] [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/99] [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/99] [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/99] [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/99] [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/99] [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/99] [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/99] [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/99] [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/99] [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/99] [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/99] [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/99] [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/99] [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/99] [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'; + } +} From d401f60240e33e626af8384bc8f100cbd4273697 Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Thu, 30 Mar 2023 14:42:25 +0200 Subject: [PATCH 55/99] [TASK] Update cloudinary php package to version 2 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 35e8ea9..2b82707 100644 --- a/composer.json +++ b/composer.json @@ -6,7 +6,7 @@ "GPL-2.0-or-later" ], "require": { - "cloudinary/cloudinary_php": "^1.15", + "cloudinary/cloudinary_php": "^2", "ext-json": "*", "php": "~8.0 || ~8.1", "typo3/cms-backend": "^11.5", From 35a45539a7dfcaef09a81905a0ea53d09da5f2ed Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Fri, 31 Mar 2023 18:05:55 +0200 Subject: [PATCH 56/99] [TASK] Replace deprecated Cloudinary API calls with v2 ones --- Classes/Command/CloudinaryApiCommand.php | 30 ++++---- Classes/Command/CloudinaryMetadataCommand.php | 8 +-- .../Controller/CloudinaryAjaxController.php | 32 ++++----- .../CloudinaryWebHookController.php | 8 +-- Classes/Driver/CloudinaryDriver.php | 69 ++++++------------- .../AbstractCloudinaryMediaService.php | 29 +++----- Classes/Services/CloudinaryImageService.php | 24 +++++-- Classes/Services/CloudinaryScanService.php | 38 ++++------ .../CloudinaryTestConnectionService.php | 17 ++--- Classes/Services/CloudinaryVideoService.php | 8 ++- .../Extractor/CloudinaryMetaDataExtractor.php | 29 +++----- Classes/Services/FileMoveService.php | 27 ++++---- Classes/Utility/CloudinaryApiUtility.php | 30 ++++++-- 13 files changed, 154 insertions(+), 195 deletions(-) diff --git a/Classes/Command/CloudinaryApiCommand.php b/Classes/Command/CloudinaryApiCommand.php index 444546f..2a38e70 100644 --- a/Classes/Command/CloudinaryApiCommand.php +++ b/Classes/Command/CloudinaryApiCommand.php @@ -9,8 +9,8 @@ * LICENSE.md file that was distributed with this source code. */ -use Cloudinary\Api; -use Cloudinary\Search; +use Cloudinary\Api\Admin\AdminApi; +use Cloudinary\Api\Search\SearchApi; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -107,10 +107,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int $publicId = $this->getPublicIdFromFile($file); } - $this->initializeApi(); try { if ($publicId) { - $resource = $this->getApi()->resource($publicId); + $resource = $this->getAdminApi()->asset($publicId); $this->log(var_export((array)$resource, true)); } elseif ($expression) { @@ -120,14 +119,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int ? $response['next_cursor'] : ''; - /** @var Search $search */ - $search = new Search(); - - $response = $search + $response = $this->getSearchApi() ->expression($expression) - ->sort_by('public_id', 'asc') - ->max_results(100) - ->next_cursor($nextCursor) + ->sortBy('public_id', 'asc') + ->maxResults(100) + ->nextCursor($nextCursor) ->execute(); if (is_array($response['resources'])) { @@ -146,7 +142,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($delete) { $counter++; $this->log("\nDeleting batch #$counter...\n"); - $this->getApi()->delete_resources($_resources); + $this->getAdminApi()->deleteAssets($_resources); } } } while (!empty($response) && isset($response['next_cursor'])); @@ -171,14 +167,14 @@ protected function getPublicIdFromFile(File $file): string return $cloudinaryPathService->computeCloudinaryPublicId($file->getIdentifier()); } - protected function getApi() + protected function getSearchApi(): SearchApi { - // create a new instance upon each API call to avoid driver confusion - return new Api(); + return CloudinaryApiUtility::getCloudinary($this->storage)->searchApi(); } - protected function initializeApi(): void + protected function getAdminApi(): AdminApi { - CloudinaryApiUtility::initializeByConfiguration($this->storage->getConfiguration()); + return CloudinaryApiUtility::getCloudinary($this->storage)->adminApi(); } + } diff --git a/Classes/Command/CloudinaryMetadataCommand.php b/Classes/Command/CloudinaryMetadataCommand.php index b9ba892..c29d755 100644 --- a/Classes/Command/CloudinaryMetadataCommand.php +++ b/Classes/Command/CloudinaryMetadataCommand.php @@ -9,6 +9,7 @@ * LICENSE.md file that was distributed with this source code. */ +use Cloudinary\Api\Upload\UploadApi; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -91,10 +92,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int } // 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( + $this->getUploadApi()->explicit( $publicId, [ 'type' => 'upload', @@ -114,9 +114,9 @@ public function getFirstSite(): Site return array_values($sites)[0]; } - protected function initializeApi(): void + protected function getUploadApi(): UploadApi { - CloudinaryApiUtility::initializeByConfiguration($this->storage->getConfiguration()); + return CloudinaryApiUtility::getCloudinary($this->storage)->uploadApi(); } } diff --git a/Classes/Controller/CloudinaryAjaxController.php b/Classes/Controller/CloudinaryAjaxController.php index 104a976..2381309 100644 --- a/Classes/Controller/CloudinaryAjaxController.php +++ b/Classes/Controller/CloudinaryAjaxController.php @@ -2,7 +2,7 @@ namespace Visol\Cloudinary\Controller; -use Cloudinary; +use Cloudinary\Api\Search\SearchApi; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use TYPO3\CMS\Core\Http\JsonResponse; @@ -11,14 +11,14 @@ use TYPO3\CMS\Core\Utility\GeneralUtility; use Visol\Cloudinary\Services\CloudinaryPathService; use Visol\Cloudinary\Services\CloudinaryResourceService; -use Visol\Cloudinary\Services\ConfigurationService; +use Visol\Cloudinary\Utility\CloudinaryApiUtility; class CloudinaryAjaxController { public function addFilesAction(ServerRequestInterface $request): ResponseInterface { - $storageUid = (int) $request->getParsedBody()['storageUid']; - $cloudinaryIds = (array) $request->getParsedBody()['cloudinaryIds']; + $storageUid = (int)$request->getParsedBody()['storageUid']; + $cloudinaryIds = (array)$request->getParsedBody()['cloudinaryIds']; /** @var ResourceFactory $resourceFactory */ $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class); @@ -29,7 +29,6 @@ public function addFilesAction(ServerRequestInterface $request): ResponseInterfa try { // Initialize objects $storage = $resourceFactory->getStorageObject($storageUid); - $this->initializeApi($storage); $cloudinaryPathService = GeneralUtility::makeInstance( CloudinaryPathService::class, $storage->getConfiguration(), @@ -39,10 +38,10 @@ public function addFilesAction(ServerRequestInterface $request): ResponseInterfa foreach ($cloudinaryIds as $publicId) { // We must retrieve the resources so that we can determine the format - $search = new \Cloudinary\Search(); - $search->expression('public_id:' . $publicId); - $search->max_results(1); - $response = $search->execute(); + $response = $this->getSearchApi($storage) + ->expression('public_id:' . $publicId) + ->maxResults(1) + ->execute(); if (empty($response['resources'])) { throw new \RuntimeException('Missing resources ' . $publicId, 1657125439); @@ -57,7 +56,7 @@ public function addFilesAction(ServerRequestInterface $request): ResponseInterfa //$cloudinaryPathService->computeFileIdentifier((array) $resource); // Save mirrored file - $cloudinaryResourceService->save((array) $resource); + $cloudinaryResourceService->save((array)$resource); // This will trigger a file indexation $files[] = $storage->getFile($identifier)->getUid(); @@ -73,16 +72,9 @@ public function addFilesAction(ServerRequestInterface $request): ResponseInterfa return $response; } - protected function initializeApi(ResourceStorage $storage): void + protected function getSearchApi(ResourceStorage $storage): SearchApi { - $configurationService = GeneralUtility::makeInstance(ConfigurationService::class, $storage->getConfiguration()); - - Cloudinary::config([ - 'cloud_name' => $configurationService->get('cloudName'), - 'api_key' => $configurationService->get('apiKey'), - 'api_secret' => $configurationService->get('apiSecret'), - 'timeout' => $configurationService->get('timeout'), - 'secure' => true, - ]); + return CloudinaryApiUtility::getCloudinary($storage)->searchApi(); } + } diff --git a/Classes/Controller/CloudinaryWebHookController.php b/Classes/Controller/CloudinaryWebHookController.php index 20cf259..9b05404 100644 --- a/Classes/Controller/CloudinaryWebHookController.php +++ b/Classes/Controller/CloudinaryWebHookController.php @@ -9,6 +9,7 @@ * LICENSE.md file that was distributed with this source code. */ +use Cloudinary\Api\Upload\UploadApi; use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Http\Message\ResponseInterface; use TYPO3\CMS\Core\Cache\CacheManager; @@ -109,7 +110,6 @@ public function processAction(): ResponseInterface $clearCachePages = []; self::getLogger()->debug(sprintf('Start flushing cache for file action "%s". ', $requestType)); - $this->initializeApi(); foreach ($publicIds as $publicId) { @@ -174,7 +174,7 @@ public function processAction(): ResponseInterface protected function flushCloudinaryCdn(string $publicId): void { // Invalidate CDN cache - \Cloudinary\Uploader::explicit( + $this->getUploadApi()->explicit( $publicId, [ 'type' => 'upload', @@ -405,9 +405,9 @@ protected static function getLogger(): Logger return $logger; } - protected function initializeApi(): void + protected function getUploadApi(): UploadApi { - CloudinaryApiUtility::initializeByConfiguration($this->storage->getConfiguration()); + return CloudinaryApiUtility::getCloudinary($this->storage)->uploadApi(); } } diff --git a/Classes/Driver/CloudinaryDriver.php b/Classes/Driver/CloudinaryDriver.php index e79954f..137b569 100644 --- a/Classes/Driver/CloudinaryDriver.php +++ b/Classes/Driver/CloudinaryDriver.php @@ -9,23 +9,20 @@ * LICENSE.md file that was distributed with this source code. */ +use Cloudinary\Api\Admin\AdminApi; +use Cloudinary\Api\Upload\UploadApi; use TYPO3\CMS\Core\Http\ApplicationType; use TYPO3\CMS\Core\Resource\Exception\InvalidFileNameException; -use Cloudinary; -use Cloudinary\Api; -use Cloudinary\Uploader; use RuntimeException; use TYPO3\CMS\Core\Charset\CharsetConverter; use TYPO3\CMS\Core\Core\Environment; use TYPO3\CMS\Core\Log\Logger; use TYPO3\CMS\Core\Resource\ResourceFactory; -use TYPO3\CMS\Core\Type\File\FileInfo; use TYPO3\CMS\Core\Log\LogLevel; use TYPO3\CMS\Core\Log\LogManager; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\Utility\PathUtility; use TYPO3\CMS\Core\Resource\Driver\AbstractHierarchicalFilesystemDriver; -use TYPO3\CMS\Core\Resource\Exception; use TYPO3\CMS\Core\Resource\ResourceStorage; use Visol\Cloudinary\Domain\Repository\ExplicitDataCacheRepository; use Visol\Cloudinary\Services\CloudinaryFolderService; @@ -33,6 +30,7 @@ use Visol\Cloudinary\Services\CloudinaryPathService; use Visol\Cloudinary\Services\CloudinaryTestConnectionService; use Visol\Cloudinary\Services\ConfigurationService; +use Visol\Cloudinary\Utility\CloudinaryApiUtility; use Visol\Cloudinary\Utility\CloudinaryFileUtility; use Visol\Cloudinary\Utility\MimeTypeUtility; @@ -270,16 +268,13 @@ public function addFile($localFilePath, $targetFolderIdentifier, $newFileName = $cloudinaryPublicId = $this->getCloudinaryPathService()->computeCloudinaryPublicId($fileIdentifier); $this->log( - '[API] Cloudinary\Uploader::upload() - add resource "%s"', + '[API] UploadApi - add resource "%s"', [$cloudinaryPublicId], ['addFile()'], ); - // Before calling API, make sure we are connected with the right "bucket" - $this->initializeApi(); - // Upload the file - $cloudinaryResource = Uploader::upload($localFilePath, [ + $cloudinaryResource = (array)$this->getUploadApi()->upload($localFilePath, [ 'public_id' => PathUtility::basename($cloudinaryPublicId), 'folder' => $this->getCloudinaryPathService()->computeCloudinaryFolderPath($targetFolderIdentifier), 'resource_type' => $this->getCloudinaryPathService()->getResourceType($fileIdentifier), @@ -314,10 +309,7 @@ public function copyFileWithinStorage($fileIdentifier, $targetFolderIdentifier, { $targetFileIdentifier = $this->canonicalizeFolderIdentifierAndFileName($targetFolderIdentifier, $fileName); - // Before calling API, make sure we are connected with the right "bucket" - $this->initializeApi(); - - $cloudinaryResource = Uploader::upload($this->getPublicUrl($fileIdentifier), [ + $cloudinaryResource = (array)$this->getUploadApi()->upload($this->getPublicUrl($fileIdentifier), [ 'public_id' => PathUtility::basename( $this->getCloudinaryPathService()->computeCloudinaryPublicId($targetFileIdentifier), ), @@ -347,11 +339,8 @@ public function replaceFile($fileIdentifier, $localFilePath): bool $this->getCloudinaryPathService()->computeCloudinaryPublicId($fileIdentifier), ); - // Before calling the API, make sure we are connected with the right "bucket" - $this->initializeApi(); - // Upload the file - $cloudinaryResource = Uploader::upload($localFilePath, [ + $cloudinaryResource = (array)$this->getUploadApi()->upload($localFilePath, [ 'public_id' => PathUtility::basename($cloudinaryPublicId), 'folder' => $this->getCloudinaryPathService()->computeCloudinaryFolderPath( PathUtility::dirname($fileIdentifier), @@ -375,12 +364,12 @@ public function deleteFile($fileIdentifier): bool { $cloudinaryPublicId = $this->getCloudinaryPathService()->computeCloudinaryPublicId($fileIdentifier); $this->log( - '[API] Cloudinary\Api::delete_resources - delete resource "%s"', + '[API] Delete resource "%s"', [$cloudinaryPublicId], ['deleteFile'], ); - $response = $this->getApi()->delete_resources($cloudinaryPublicId, [ + $response = $this->getAdminApi()->deleteAssets($cloudinaryPublicId, [ 'resource_type' => $this->getCloudinaryPathService()->getResourceType($fileIdentifier), ]); @@ -405,11 +394,11 @@ public function deleteFolder($folderIdentifier, $deleteRecursively = false): boo if ($deleteRecursively) { $this->log( - '[API] Cloudinary\Api::delete_resources_by_prefix() - folder "%s"', + '[API] Delete folder "%s"', [$cloudinaryFolder], ['deleteFolder'], ); - $response = $this->getApi()->delete_resources_by_prefix($cloudinaryFolder); + $response = $this->getAdminApi()->deleteAssetsByPrefix($cloudinaryFolder); foreach ($response['deleted'] as $publicId => $status) { if ($status === 'deleted') { @@ -421,11 +410,11 @@ 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] Cloudinary\Api::delete_folder() - folder "%s"', + '[API] Delete folder "%s"', [$cloudinaryFolder], ['deleteFolder'], ); - $response = $this->getApi()->delete_folder($cloudinaryFolder); + $response = $this->getAdminApi()->deleteFolder($cloudinaryFolder); foreach ($response['deleted'] as $folder) { $this->getCloudinaryFolderService()->delete($folder); @@ -482,8 +471,8 @@ public function createFolder($newFolderName, $parentFolderIdentifier = '', $recu ); $cloudinaryFolder = $this->getCloudinaryPathService()->normalizeCloudinaryPublicId($canonicalFolderPath); - $this->log('[API] Cloudinary\Api::createFolder() - folder "%s"', [$cloudinaryFolder], ['createFolder']); - $response = $this->getApi()->create_folder($cloudinaryFolder); + $this->log('[API] Create folder "%s"', [$cloudinaryFolder], ['createFolder']); + $response = $this->getAdminApi()->createFolder($cloudinaryFolder); if (!$response['success']) { throw new \Exception('Folder creation failed: ' . $cloudinaryFolder, 1591775050); @@ -532,11 +521,9 @@ public function renameFile($fileIdentifier, $newFileIdentifier): string $newCloudinaryPublicId = $this->getCloudinaryPathService()->computeCloudinaryPublicId($newFileIdentifier); if ($cloudinaryPublicId !== $newCloudinaryPublicId) { - // Before calling API, make sure we are connected with the right "bucket" - $this->initializeApi(); // Rename the file - $cloudinaryResource = Uploader::rename($cloudinaryPublicId, $newCloudinaryPublicId, [ + $cloudinaryResource = (array)$this->getUploadApi()->rename($cloudinaryPublicId, $newCloudinaryPublicId, [ 'resource_type' => $this->getCloudinaryPathService()->getResourceType($fileIdentifier), 'overwrite' => true, ]); @@ -582,9 +569,6 @@ public function renameFolder($folderIdentifier, $newFolderName): array $pathSegments[$numberOfSegments - 2] = $newFolderName; $newFolderIdentifier = implode('/', $pathSegments); - // Before calling the API, make sure we are connected with the right "bucket" - $this->initializeApi(); - $renamedFiles[$folderIdentifier] = $newFolderIdentifier; foreach ($this->getFilesInFolder($folderIdentifier, 0, -1, true) as $oldFileIdentifier) { @@ -1133,25 +1117,14 @@ protected function getCloudinaryFolderService(): CloudinaryFolderService return $this->cloudinaryFolderService; } - protected function initializeApi(): void + protected function getUploadApi(): UploadApi { - Cloudinary::config([ - 'cloud_name' => $this->configurationService->get('cloudName'), - 'api_key' => $this->configurationService->get('apiKey'), - 'api_secret' => $this->configurationService->get('apiSecret'), - 'timeout' => $this->configurationService->get('timeout'), - 'secure' => true, - ]); + return CloudinaryApiUtility::getCloudinary($this->configuration)->uploadApi(); } - protected function getApi(): Api + protected function getAdminApi(): AdminApi { - $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 CloudinaryApiUtility::getCloudinary($this->configuration)->adminApi(); } + } diff --git a/Classes/Services/AbstractCloudinaryMediaService.php b/Classes/Services/AbstractCloudinaryMediaService.php index 83fa5fc..76b394d 100644 --- a/Classes/Services/AbstractCloudinaryMediaService.php +++ b/Classes/Services/AbstractCloudinaryMediaService.php @@ -2,7 +2,7 @@ namespace Visol\Cloudinary\Services; -use Cloudinary\Uploader; +use Cloudinary\Api\Upload\UploadApi; use Doctrine\DBAL\Exception\UniqueConstraintViolationException; use TYPO3\CMS\Core\Log\Logger; use TYPO3\CMS\Core\Log\LogLevel; @@ -10,28 +10,10 @@ use TYPO3\CMS\Core\Resource\File; use TYPO3\CMS\Core\Resource\ResourceStorage; use TYPO3\CMS\Core\Utility\GeneralUtility; -use Visol\Cloudinary\Driver\CloudinaryDriver; use Visol\Cloudinary\Utility\CloudinaryApiUtility; abstract class AbstractCloudinaryMediaService { - /** - * @throws \Exception - */ - protected function initializeApi(ResourceStorage $storage): void - { - // Check the file is stored on the right storage - // If not we should trigger an exception - if ($storage->getDriverType() !== CloudinaryDriver::DRIVER_TYPE) { - $message = sprintf( - 'Wrong storage! Can not initialize with storage type "%s".', - $storage->getDriverType() - ); - throw new \Exception($message, 1590401459); - } - - CloudinaryApiUtility::initializeByConfiguration($storage->getConfiguration()); - } public function getExplicitData(File $file, array $options): array { @@ -40,8 +22,7 @@ public function getExplicitData(File $file, array $options): array $explicitData = $this->explicitDataCacheRepository->findByStorageAndPublicIdAndOptions($file->getStorage()->getUid(), $publicId, $options)['explicit_data']; if (!$explicitData) { - $this->initializeApi($file->getStorage()); - $explicitData = Uploader::explicit($publicId, $options); + $explicitData = $this->getUploadApi($file->getStorage())->explicit($publicId, $options); try { $this->explicitDataCacheRepository->save($file->getStorage()->getUid(), $publicId, $options, $explicitData); } catch (UniqueConstraintViolationException $e) { @@ -95,4 +76,10 @@ public function getPublicIdForFile(File $file): string ->getCloudinaryPathService($file->getStorage()) ->computeCloudinaryPublicId($file->getIdentifier()); } + + protected function getUploadApi(ResourceStorage $storage): UploadApi + { + return CloudinaryApiUtility::getCloudinary($storage)->uploadApi(); + } + } diff --git a/Classes/Services/CloudinaryImageService.php b/Classes/Services/CloudinaryImageService.php index b8f7840..dc107e0 100644 --- a/Classes/Services/CloudinaryImageService.php +++ b/Classes/Services/CloudinaryImageService.php @@ -9,12 +9,14 @@ namespace Visol\Cloudinary\Services; +use Cloudinary\Asset\Image; +use Cloudinary\Transformation\ImageTransformation; use TYPO3\CMS\Core\Resource\StorageRepository; -use Cloudinary\Uploader; use Doctrine\DBAL\Exception\UniqueConstraintViolationException; use TYPO3\CMS\Core\Resource\File; use TYPO3\CMS\Core\Utility\GeneralUtility; use Visol\Cloudinary\Domain\Repository\ExplicitDataCacheRepository; +use Visol\Cloudinary\Utility\CloudinaryApiUtility; class CloudinaryImageService extends AbstractCloudinaryMediaService { @@ -41,9 +43,18 @@ public function getExplicitData(File $file, array $options): array $explicitData = $this->explicitDataCacheRepository->findByStorageAndPublicIdAndOptions($file->getStorage()->getUid(), $publicId, $options)['explicit_data']; if (!$explicitData) { - $this->initializeApi($file->getStorage()); - $explicitData = Uploader::explicit($publicId, $options); + + // With Cloudinary API 2, we need to modify the way in which "responsive_breakpoints.transformation" are transmitted. + $apiOptions = $options; + if (isset($options['responsive_breakpoints']['transformation'])) { + $apiOptions['responsive_breakpoints']['transformation'] = []; // reset the value + foreach ($options['responsive_breakpoints']['transformation'] as $transformationParams) { + $apiOptions['responsive_breakpoints']['transformation'][] = ImageTransformation::fromParams($transformationParams); + } + } + try { + $explicitData = (array)$this->getUploadApi($file->getStorage())->explicit($publicId, $apiOptions); $this->explicitDataCacheRepository->save($file->getStorage()->getUid(), $publicId, $options, $explicitData); } catch (UniqueConstraintViolationException $e) { // ignore @@ -118,8 +129,10 @@ public function getImageUrl(File $file, array $options = []): string $publicId = $this->getPublicIdForFile($file); - $this->initializeApi($file->getStorage()); - return \Cloudinary::cloudinary_url($publicId, $options); + $configuration = CloudinaryApiUtility::getConfiguration($file->getStorage()); + return (string)Image::fromParams($publicId, $options) + ->configuration($configuration) + ->toUrl(); } public function getImageObjects(array $breakpoints): array @@ -206,4 +219,5 @@ public function injectStorageRepository(StorageRepository $storageRepository): v { $this->storageRepository = $storageRepository; } + } diff --git a/Classes/Services/CloudinaryScanService.php b/Classes/Services/CloudinaryScanService.php index 27840ad..1a16d73 100644 --- a/Classes/Services/CloudinaryScanService.php +++ b/Classes/Services/CloudinaryScanService.php @@ -9,10 +9,10 @@ * LICENSE.md file that was distributed with this source code. */ -use Cloudinary\Api; +use Cloudinary\Api\Admin\AdminApi; +use Cloudinary\Api\Search\SearchApi; use TYPO3\CMS\Core\Exception; use TYPO3\CMS\Core\Log\Logger; -use Cloudinary\Search; use Symfony\Component\Console\Style\SymfonyStyle; use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Database\Query\QueryBuilder; @@ -65,7 +65,7 @@ public function __construct(ResourceStorage $storage, SymfonyStyle $io = null) public function scanOne(string $publicId): array|null { try { - $resource = (array)$this->getApi()->resource($publicId); + $resource = (array)$this->getAdminApi()->asset($publicId); $result = $this->getCloudinaryResourceService()->save($resource); } catch (Exception $exception) { $result = null; @@ -77,9 +77,6 @@ public function scan(): array { $this->preScan(); - // Before calling the Search API, make sure we are connected with the right cloudinary account - $this->initializeApi(); - $cloudinaryFolder = $this->getCloudinaryPathService()->computeCloudinaryFolderPath(DIRECTORY_SEPARATOR); // We initialize the array. @@ -104,7 +101,7 @@ public function scan(): array : ''; $this->info( - '[API][SEARCH] Cloudinary\Search() - fetch resources from folder "%s" %s', + '[API] SearchApi - fetch resources from folder "%s" %s', [ $cloudinaryFolder, $nextCursor ? 'and cursor ' . $nextCursor : '', @@ -114,14 +111,11 @@ public function scan(): array ] ); - /** @var Search $search */ - $search = new Search(); - - $response = $search + $response = $this->getSearchApi() ->expression(implode(' AND ', $expressions)) - ->sort_by('public_id', 'asc') - ->max_results(500) - ->next_cursor($nextCursor) + ->sortBy('public_id', 'asc') + ->maxResults(500) + ->nextCursor($nextCursor) ->execute(); if (is_array($response['resources'])) { @@ -218,11 +212,6 @@ protected function getQueryBuilder(): QueryBuilder return $connectionPool->getQueryBuilderForTable('sys_file'); } - protected function initializeApi(): void - { - CloudinaryApiUtility::initializeByConfiguration($this->storage->getConfiguration()); - } - protected function getCloudinaryResourceService(): CloudinaryResourceService { return GeneralUtility::makeInstance(CloudinaryResourceService::class, $this->storage); @@ -245,13 +234,14 @@ protected function getCloudinaryPathService(): CloudinaryPathService return $this->cloudinaryPathService; } - protected function getApi() + protected function getSearchApi(): SearchApi { - // Initialize and configure the API for each call - $this->initializeApi(); + return CloudinaryApiUtility::getCloudinary($this->storage)->searchApi(); + } - // create a new instance upon each API call to avoid driver confusion - return new Api(); + protected function getAdminApi(): AdminApi + { + return CloudinaryApiUtility::getCloudinary($this->storage)->adminApi(); } protected function info(string $message, array $arguments = [], array $data = []): void diff --git a/Classes/Services/CloudinaryTestConnectionService.php b/Classes/Services/CloudinaryTestConnectionService.php index ab8f068..f5a9b9e 100644 --- a/Classes/Services/CloudinaryTestConnectionService.php +++ b/Classes/Services/CloudinaryTestConnectionService.php @@ -8,12 +8,12 @@ * For the full copyright and license information, please read the * LICENSE.md file that was distributed with this source code. */ -use Cloudinary\Search; + +use Cloudinary\Api\Search\SearchApi; use TYPO3\CMS\Core\Messaging\FlashMessageQueue; use TYPO3\CMS\Core\Messaging\FlashMessage; use TYPO3\CMS\Core\Messaging\FlashMessageService; use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Extbase\Object\ObjectManager; use TYPO3\CMS\Extbase\Utility\LocalizationUtility; use Visol\Cloudinary\Utility\CloudinaryApiUtility; @@ -51,10 +51,7 @@ public function test() $messageQueue = $this->getMessageQueue(); $localizationPrefix = $this->languageFile . ':driverConfiguration.message.'; try { - $this->initializeApi(); - - $search = new Search(); - $search + $search = $this->getSearchApi() ->expression('folder=/') ->execute(); @@ -88,13 +85,9 @@ protected function getMessageQueue() return $flashMessageService->getMessageQueueByIdentifier(); } - /** - * @return void - */ - protected function initializeApi() + protected function getSearchApi(): SearchApi { - CloudinaryApiUtility::initializeByConfiguration($this->configuration); + return CloudinaryApiUtility::getCloudinary($this->configuration)->searchApi(); } - } diff --git a/Classes/Services/CloudinaryVideoService.php b/Classes/Services/CloudinaryVideoService.php index 4e6dd35..be9df49 100644 --- a/Classes/Services/CloudinaryVideoService.php +++ b/Classes/Services/CloudinaryVideoService.php @@ -2,7 +2,9 @@ namespace Visol\Cloudinary\Services; +use Cloudinary\Asset\Video; use TYPO3\CMS\Core\Resource\File; +use Visol\Cloudinary\Utility\CloudinaryApiUtility; class CloudinaryVideoService extends AbstractCloudinaryMediaService { @@ -19,7 +21,9 @@ public function getVideoUrl(File $file, array $options = []): string $publicId = $this->getPublicIdForFile($file); - $this->initializeApi($file->getStorage()); - return \Cloudinary::cloudinary_url($publicId, $options); + $configuration = CloudinaryApiUtility::getConfiguration($file->getStorage()); + return Video::fromParams($publicId) + ->configuration($configuration) + ->toUrl(); } } diff --git a/Classes/Services/Extractor/CloudinaryMetaDataExtractor.php b/Classes/Services/Extractor/CloudinaryMetaDataExtractor.php index 825f7fe..67d8136 100644 --- a/Classes/Services/Extractor/CloudinaryMetaDataExtractor.php +++ b/Classes/Services/Extractor/CloudinaryMetaDataExtractor.php @@ -2,20 +2,15 @@ namespace Visol\Cloudinary\Services\Extractor; -use Cloudinary; -use Psr\Log\LoggerAwareInterface; -use Psr\Log\LoggerAwareTrait; -use Psr\Log\LogLevel; -use TYPO3\CMS\Core\Resource; +use Cloudinary\Api\Admin\AdminApi; use TYPO3\CMS\Core\Resource\File; use TYPO3\CMS\Core\Resource\Index\ExtractorInterface; use TYPO3\CMS\Core\Resource\ResourceStorage; -use TYPO3\CMS\Core\Type\File\ImageInfo; use TYPO3\CMS\Core\Utility\GeneralUtility; use Visol\Cloudinary\Driver\CloudinaryDriver; use Visol\Cloudinary\Services\CloudinaryPathService; use Visol\Cloudinary\Services\CloudinaryResourceService; -use Visol\Cloudinary\Services\ConfigurationService; +use Visol\Cloudinary\Utility\CloudinaryApiUtility; class CloudinaryMetaDataExtractor implements ExtractorInterface { @@ -78,11 +73,10 @@ public function extractMetaData(File $file, array $previousExtractedData = []): // We are force calling cloudinary API if (!$resource) { - // Ask cloudinary to fetch the resource for us - $this->initializeApi($file->getStorage()); - $api = new Cloudinary\Api(); - $resource = $api->resource($publicId); + // Ask cloudinary to fetch the resource for us + $resource = $this->getAdminApi($file->getStorage()) + ->asset($publicId); } return [ @@ -91,16 +85,9 @@ public function extractMetaData(File $file, array $previousExtractedData = []): ]; } - protected function initializeApi(ResourceStorage $storage): void + protected function getAdminApi(ResourceStorage $storage): AdminApi { - $configurationService = GeneralUtility::makeInstance(ConfigurationService::class, $storage->getConfiguration()); - - Cloudinary::config([ - 'cloud_name' => $configurationService->get('cloudName'), - 'api_key' => $configurationService->get('apiKey'), - 'api_secret' => $configurationService->get('apiSecret'), - 'timeout' => $configurationService->get('timeout'), - 'secure' => true, - ]); + return CloudinaryApiUtility::getCloudinary($storage)->adminApi(); } + } diff --git a/Classes/Services/FileMoveService.php b/Classes/Services/FileMoveService.php index 6a5bb28..705a96a 100644 --- a/Classes/Services/FileMoveService.php +++ b/Classes/Services/FileMoveService.php @@ -9,8 +9,8 @@ * LICENSE.md file that was distributed with this source code. */ -use Cloudinary\Api; -use Cloudinary\Uploader; +use Cloudinary\Api\Admin\AdminApi; +use Cloudinary\Api\Upload\UploadApi; use Doctrine\DBAL\Driver\Connection; use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Database\Query\QueryBuilder; @@ -28,7 +28,6 @@ class FileMoveService public function fileExists(File $fileObject, ResourceStorage $targetStorage): bool { - $this->initializeApi($targetStorage); $this->initializeCloudinaryService($targetStorage); // Retrieve the Public Id based on the file identifier @@ -36,8 +35,7 @@ public function fileExists(File $fileObject, ResourceStorage $targetStorage): bo ->computeCloudinaryPublicId($fileObject->getIdentifier()); try { - $api = new Api(); - $resource = $api->resource($publicId); + $resource = $this->getAdminApi($targetStorage)->asset($publicId); $fileExists = !empty($resource); } catch (\Exception $exception) { $fileExists = false; @@ -121,7 +119,6 @@ public function cloudinaryUploadFile( $this->ensureDirectoryExistence($fileObject); - $this->initializeApi($targetStorage); $fileIdentifier = $fileObject->getIdentifier(); $publicId = $this->getCloudinaryPathService() @@ -141,17 +138,12 @@ public function cloudinaryUploadFile( : $this->getAbsolutePath($fileObject); // Upload the file - $resource = Uploader::upload( + $this->getUploadApi($targetStorage)->upload( $fileNameAndPath, $options ); } - protected function initializeApi(ResourceStorage $targetStorage) - { - CloudinaryApiUtility::initializeByConfiguration($targetStorage->getConfiguration()); - } - protected function getQueryBuilder(): QueryBuilder { /** @var ConnectionPool $connectionPool */ @@ -190,4 +182,15 @@ protected function initializeCloudinaryService(ResourceStorage $storage) $storage ); } + + protected function getUploadApi(ResourceStorage $storage): UploadApi + { + return CloudinaryApiUtility::getCloudinary($storage)->uploadApi(); + } + + protected function getAdminApi(ResourceStorage $storage): AdminApi + { + return CloudinaryApiUtility::getCloudinary($storage)->adminApi(); + } + } diff --git a/Classes/Utility/CloudinaryApiUtility.php b/Classes/Utility/CloudinaryApiUtility.php index 9329aaf..1a82af0 100644 --- a/Classes/Utility/CloudinaryApiUtility.php +++ b/Classes/Utility/CloudinaryApiUtility.php @@ -8,7 +8,12 @@ * For the full copyright and license information, please read the * LICENSE.md file that was distributed with this source code. */ + +use Cloudinary\Cloudinary; +use Cloudinary\Configuration\Configuration; +use TYPO3\CMS\Core\Resource\ResourceStorage; use TYPO3\CMS\Core\Utility\GeneralUtility; +use Visol\Cloudinary\Driver\CloudinaryDriver; use Visol\Cloudinary\Services\ConfigurationService; /** @@ -17,16 +22,31 @@ class CloudinaryApiUtility { - public static function initializeByConfiguration(array $configuration) + public static function getCloudinary(ResourceStorage|array $storage): Cloudinary + { + return new Cloudinary(self::getConfiguration($storage)); + } + + public static function getConfiguration(ResourceStorage|array $storage): Configuration { + if ($storage->getDriverType() !== CloudinaryDriver::DRIVER_TYPE) { + // Check the file is stored on the right storage + // If not we should trigger an exception + $message = sprintf( + 'Wrong storage! Can not initialize with storage type "%s".', + $storage->getDriverType() + ); + throw new \Exception($message, 1590401459); + } + $storageConfiguration = $storage->getConfiguration(); + /** @var ConfigurationService $configurationService */ $configurationService = GeneralUtility::makeInstance( ConfigurationService::class, - $configuration + $storageConfiguration ); - \Cloudinary::config( - [ + return Configuration::instance([ 'cloud_name' => $configurationService->get('cloudName'), 'api_key' => $configurationService->get('apiKey'), 'api_secret' => $configurationService->get('apiSecret'), @@ -34,6 +54,6 @@ public static function initializeByConfiguration(array $configuration) 'secure' => true ] ); - } + } } From 9f1cf30580d7ddbb9f11cd0d3890a655bf2307ff Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Fri, 31 Mar 2023 18:15:16 +0200 Subject: [PATCH 57/99] [TASK] Update phpstan-baseline.neon --- phpstan-baseline.neon | 318 ++++++++++++++++++++++++++++-------------- phpstan.neon | 2 + 2 files changed, 216 insertions(+), 104 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 12a5b0b..5306f9e 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -40,11 +40,6 @@ parameters: count: 1 path: Classes/Backend/Form/Container/InlineCloudinaryControlContainer.php - - - message: "#^Method Visol\\\\Cloudinary\\\\CloudinaryFactory\\:\\:getFolder\\(\\) should return TYPO3\\\\CMS\\\\Core\\\\Resource\\\\Folder but returns object\\.$#" - count: 1 - path: Classes/CloudinaryFactory.php - - message: "#^PHPDoc tag @return with type object is not subtype of native type TYPO3\\\\CMS\\\\Core\\\\Resource\\\\Folder\\.$#" count: 1 @@ -91,37 +86,47 @@ parameters: path: Classes/Command/CloudinaryAcceptanceTestCommand.php - - message: "#^Method Visol\\\\Cloudinary\\\\Command\\\\CloudinaryAcceptanceTestCommand\\:\\:configure\\(\\) has no return type specified\\.$#" + message: "#^Method Visol\\\\Cloudinary\\\\Command\\\\CloudinaryAcceptanceTestCommand\\:\\:tearDown\\(\\) has no return type specified\\.$#" count: 1 path: Classes/Command/CloudinaryAcceptanceTestCommand.php - - message: "#^Method Visol\\\\Cloudinary\\\\Command\\\\CloudinaryAcceptanceTestCommand\\:\\:initialize\\(\\) has no return type specified\\.$#" + message: "#^Parameter \\#2 \\$string of static method TYPO3\\\\CMS\\\\Core\\\\Utility\\\\GeneralUtility\\:\\:trimExplode\\(\\) expects string, mixed given\\.$#" count: 1 path: Classes/Command/CloudinaryAcceptanceTestCommand.php - - message: "#^Method Visol\\\\Cloudinary\\\\Command\\\\CloudinaryAcceptanceTestCommand\\:\\:tearDown\\(\\) has no return type specified\\.$#" + message: "#^Call to method adminApi\\(\\) on an unknown class Cloudinary\\\\Cloudinary\\.$#" count: 1 - path: Classes/Command/CloudinaryAcceptanceTestCommand.php + path: Classes/Command/CloudinaryApiCommand.php - - message: "#^Parameter \\#2 \\$string of static method TYPO3\\\\CMS\\\\Core\\\\Utility\\\\GeneralUtility\\:\\:trimExplode\\(\\) expects string, mixed given\\.$#" + message: "#^Call to method asset\\(\\) on an unknown class Cloudinary\\\\Api\\\\Admin\\\\AdminApi\\.$#" count: 1 - path: Classes/Command/CloudinaryAcceptanceTestCommand.php + path: Classes/Command/CloudinaryApiCommand.php + + - + message: "#^Call to method deleteAssets\\(\\) on an unknown class Cloudinary\\\\Api\\\\Admin\\\\AdminApi\\.$#" + count: 1 + path: Classes/Command/CloudinaryApiCommand.php - - message: "#^Method Visol\\\\Cloudinary\\\\Command\\\\CloudinaryApiCommand\\:\\:configure\\(\\) has no return type specified\\.$#" + message: "#^Call to method expression\\(\\) on an unknown class Cloudinary\\\\Api\\\\Search\\\\SearchApi\\.$#" count: 1 path: Classes/Command/CloudinaryApiCommand.php - - message: "#^Method Visol\\\\Cloudinary\\\\Command\\\\CloudinaryApiCommand\\:\\:getApi\\(\\) has no return type specified\\.$#" + message: "#^Call to method searchApi\\(\\) on an unknown class Cloudinary\\\\Cloudinary\\.$#" count: 1 path: Classes/Command/CloudinaryApiCommand.php - - message: "#^Method Visol\\\\Cloudinary\\\\Command\\\\CloudinaryApiCommand\\:\\:initialize\\(\\) has no return type specified\\.$#" + message: "#^Method Visol\\\\Cloudinary\\\\Command\\\\CloudinaryApiCommand\\:\\:getAdminApi\\(\\) has invalid return type Cloudinary\\\\Api\\\\Admin\\\\AdminApi\\.$#" + count: 1 + path: Classes/Command/CloudinaryApiCommand.php + + - + message: "#^Method Visol\\\\Cloudinary\\\\Command\\\\CloudinaryApiCommand\\:\\:getSearchApi\\(\\) has invalid return type Cloudinary\\\\Api\\\\Search\\\\SearchApi\\.$#" count: 1 path: Classes/Command/CloudinaryApiCommand.php @@ -160,21 +165,11 @@ parameters: count: 1 path: Classes/Command/CloudinaryCopyCommand.php - - - message: "#^Method Visol\\\\Cloudinary\\\\Command\\\\CloudinaryCopyCommand\\:\\:configure\\(\\) has no return type specified\\.$#" - count: 1 - path: Classes/Command/CloudinaryCopyCommand.php - - message: "#^Method Visol\\\\Cloudinary\\\\Command\\\\CloudinaryCopyCommand\\:\\:ensureDirectoryExistence\\(\\) has no return type specified\\.$#" count: 1 path: Classes/Command/CloudinaryCopyCommand.php - - - message: "#^Method Visol\\\\Cloudinary\\\\Command\\\\CloudinaryCopyCommand\\:\\:initialize\\(\\) has no return type specified\\.$#" - count: 1 - path: Classes/Command/CloudinaryCopyCommand.php - - message: "#^Parameter \\#1 \\$fileObject of method Visol\\\\Cloudinary\\\\Command\\\\CloudinaryCopyCommand\\:\\:download\\(\\) expects TYPO3\\\\CMS\\\\Core\\\\Resource\\\\File, TYPO3\\\\CMS\\\\Core\\\\Resource\\\\File\\|TYPO3\\\\CMS\\\\Core\\\\Resource\\\\ProcessedFile\\|null given\\.$#" count: 1 @@ -206,24 +201,29 @@ parameters: path: Classes/Command/CloudinaryFixJpegCommand.php - - message: "#^Method Visol\\\\Cloudinary\\\\Command\\\\CloudinaryFixJpegCommand\\:\\:configure\\(\\) has no return type specified\\.$#" + message: "#^Parameter \\#1 \\$uid of method TYPO3\\\\CMS\\\\Core\\\\Resource\\\\ResourceFactory\\:\\:getStorageObject\\(\\) expects int\\|null, mixed given\\.$#" count: 1 path: Classes/Command/CloudinaryFixJpegCommand.php - - message: "#^Method Visol\\\\Cloudinary\\\\Command\\\\CloudinaryFixJpegCommand\\:\\:initialize\\(\\) has no return type specified\\.$#" + message: "#^Property Visol\\\\Cloudinary\\\\Command\\\\AbstractCloudinaryCommand\\:\\:\\$isSilent \\(bool\\) does not accept mixed\\.$#" count: 1 path: Classes/Command/CloudinaryFixJpegCommand.php - - message: "#^Parameter \\#1 \\$uid of method TYPO3\\\\CMS\\\\Core\\\\Resource\\\\ResourceFactory\\:\\:getStorageObject\\(\\) expects int\\|null, mixed given\\.$#" + message: "#^Call to method explicit\\(\\) on an unknown class Cloudinary\\\\Api\\\\Upload\\\\UploadApi\\.$#" count: 1 - path: Classes/Command/CloudinaryFixJpegCommand.php + path: Classes/Command/CloudinaryMetadataCommand.php - - message: "#^Property Visol\\\\Cloudinary\\\\Command\\\\AbstractCloudinaryCommand\\:\\:\\$isSilent \\(bool\\) does not accept mixed\\.$#" + message: "#^Call to method uploadApi\\(\\) on an unknown class Cloudinary\\\\Cloudinary\\.$#" count: 1 - path: Classes/Command/CloudinaryFixJpegCommand.php + path: Classes/Command/CloudinaryMetadataCommand.php + + - + message: "#^Method Visol\\\\Cloudinary\\\\Command\\\\CloudinaryMetadataCommand\\:\\:getUploadApi\\(\\) has invalid return type Cloudinary\\\\Api\\\\Upload\\\\UploadApi\\.$#" + count: 1 + path: Classes/Command/CloudinaryMetadataCommand.php - message: "#^Parameter \\#1 \\$fileIdentifier of method Visol\\\\Cloudinary\\\\Services\\\\CloudinaryPathService\\:\\:computeCloudinaryPublicId\\(\\) expects string, mixed given\\.$#" @@ -250,21 +250,6 @@ parameters: count: 1 path: Classes/Command/CloudinaryMoveCommand.php - - - message: "#^Method Visol\\\\Cloudinary\\\\Command\\\\CloudinaryMoveCommand\\:\\:configure\\(\\) has no return type specified\\.$#" - count: 1 - path: Classes/Command/CloudinaryMoveCommand.php - - - - message: "#^Method Visol\\\\Cloudinary\\\\Command\\\\CloudinaryMoveCommand\\:\\:getFileMoveService\\(\\) should return Visol\\\\Cloudinary\\\\Services\\\\FileMoveService but returns object\\.$#" - count: 1 - path: Classes/Command/CloudinaryMoveCommand.php - - - - message: "#^Method Visol\\\\Cloudinary\\\\Command\\\\CloudinaryMoveCommand\\:\\:initialize\\(\\) has no return type specified\\.$#" - 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 @@ -305,21 +290,6 @@ parameters: count: 1 path: Classes/Command/CloudinaryMoveCommand.php - - - message: "#^Method Visol\\\\Cloudinary\\\\Command\\\\CloudinaryQueryCommand\\:\\:configure\\(\\) has no return type specified\\.$#" - count: 1 - path: Classes/Command/CloudinaryQueryCommand.php - - - - message: "#^Method Visol\\\\Cloudinary\\\\Command\\\\CloudinaryQueryCommand\\:\\:getFolder\\(\\) should return TYPO3\\\\CMS\\\\Core\\\\Resource\\\\Folder but returns object\\.$#" - count: 1 - path: Classes/Command/CloudinaryQueryCommand.php - - - - message: "#^Method Visol\\\\Cloudinary\\\\Command\\\\CloudinaryQueryCommand\\:\\:initialize\\(\\) has no return type specified\\.$#" - count: 1 - path: Classes/Command/CloudinaryQueryCommand.php - - message: "#^Parameter \\#1 \\$folderIdentifier of method Visol\\\\Cloudinary\\\\Command\\\\CloudinaryQueryCommand\\:\\:getFolder\\(\\) expects string, mixed given\\.$#" count: 4 @@ -355,11 +325,6 @@ parameters: count: 1 path: Classes/Command/CloudinaryQueryCommand.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: "#^Parameter \\#1 \\$uid of method TYPO3\\\\CMS\\\\Core\\\\Resource\\\\ResourceFactory\\:\\:getStorageObject\\(\\) expects int\\|null, mixed given\\.$#" count: 1 @@ -370,6 +335,16 @@ parameters: count: 2 path: Classes/Controller/CloudinaryAjaxController.php + - + message: "#^Call to method expression\\(\\) on an unknown class Cloudinary\\\\Api\\\\Search\\\\SearchApi\\.$#" + count: 1 + path: Classes/Controller/CloudinaryAjaxController.php + + - + message: "#^Call to method searchApi\\(\\) on an unknown class Cloudinary\\\\Cloudinary\\.$#" + count: 1 + path: Classes/Controller/CloudinaryAjaxController.php + - message: "#^Cannot access offset 'cloudinaryIds' on array\\|object\\|null\\.$#" count: 1 @@ -380,6 +355,11 @@ parameters: count: 1 path: Classes/Controller/CloudinaryAjaxController.php + - + message: "#^Method Visol\\\\Cloudinary\\\\Controller\\\\CloudinaryAjaxController\\:\\:getSearchApi\\(\\) has invalid return type Cloudinary\\\\Api\\\\Search\\\\SearchApi\\.$#" + count: 1 + path: Classes/Controller/CloudinaryAjaxController.php + - message: "#^Parameter \\#1 \\$string of method Psr\\\\Http\\\\Message\\\\StreamInterface\\:\\:write\\(\\) expects string, string\\|false given\\.$#" count: 1 @@ -390,6 +370,21 @@ parameters: count: 1 path: Classes/Controller/CloudinaryScanController.php + - + message: "#^Call to method explicit\\(\\) on an unknown class Cloudinary\\\\Api\\\\Upload\\\\UploadApi\\.$#" + count: 1 + path: Classes/Controller/CloudinaryWebHookController.php + + - + message: "#^Call to method uploadApi\\(\\) on an unknown class Cloudinary\\\\Cloudinary\\.$#" + count: 1 + path: Classes/Controller/CloudinaryWebHookController.php + + - + message: "#^Method Visol\\\\Cloudinary\\\\Controller\\\\CloudinaryWebHookController\\:\\:getUploadApi\\(\\) has invalid return type Cloudinary\\\\Api\\\\Upload\\\\UploadApi\\.$#" + count: 1 + path: Classes/Controller/CloudinaryWebHookController.php + - message: "#^PHPDoc tag @return with type object is not subtype of native type TYPO3\\\\CMS\\\\Core\\\\Database\\\\Connection\\.$#" count: 1 @@ -421,99 +416,104 @@ parameters: path: Classes/Domain/Repository/ExplicitDataCacheRepository.php - - message: "#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#" - count: 3 + message: "#^Call to method adminApi\\(\\) on an unknown class Cloudinary\\\\Cloudinary\\.$#" + count: 1 path: Classes/Driver/CloudinaryDriver.php - - message: "#^Cannot access offset 0 on callable\\(\\)\\: mixed\\.$#" + message: "#^Call to method createFolder\\(\\) on an unknown class Cloudinary\\\\Api\\\\Admin\\\\AdminApi\\.$#" count: 1 path: Classes/Driver/CloudinaryDriver.php - - message: "#^Cannot access offset 1 on callable\\(\\)\\: mixed\\.$#" + message: "#^Call to method deleteAssets\\(\\) on an unknown class Cloudinary\\\\Api\\\\Admin\\\\AdminApi\\.$#" count: 1 path: Classes/Driver/CloudinaryDriver.php - - message: "#^Cannot cast mixed to int\\.$#" - count: 2 + message: "#^Call to method deleteAssetsByPrefix\\(\\) on an unknown class Cloudinary\\\\Api\\\\Admin\\\\AdminApi\\.$#" + count: 1 path: Classes/Driver/CloudinaryDriver.php - - message: "#^Method Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:getCloudinaryFolderService\\(\\) should return Visol\\\\Cloudinary\\\\Services\\\\CloudinaryFolderService but returns object\\.$#" + message: "#^Call to method deleteFolder\\(\\) on an unknown class Cloudinary\\\\Api\\\\Admin\\\\AdminApi\\.$#" count: 1 path: Classes/Driver/CloudinaryDriver.php - - message: "#^Method Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:getCloudinaryPathService\\(\\) should return Visol\\\\Cloudinary\\\\Services\\\\CloudinaryPathService but returns object\\.$#" + message: "#^Call to method rename\\(\\) on an unknown class Cloudinary\\\\Api\\\\Upload\\\\UploadApi\\.$#" count: 1 path: Classes/Driver/CloudinaryDriver.php - - message: "#^Method Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:getCloudinaryResourceService\\(\\) should return Visol\\\\Cloudinary\\\\Services\\\\CloudinaryResourceService but returns object\\.$#" - count: 1 + message: "#^Call to method upload\\(\\) on an unknown class Cloudinary\\\\Api\\\\Upload\\\\UploadApi\\.$#" + count: 3 path: Classes/Driver/CloudinaryDriver.php - - message: "#^Method Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:getCloudinaryTestConnectionService\\(\\) should return Visol\\\\Cloudinary\\\\Services\\\\CloudinaryTestConnectionService but returns object\\.$#" + message: "#^Call to method uploadApi\\(\\) on an unknown class Cloudinary\\\\Cloudinary\\.$#" count: 1 path: Classes/Driver/CloudinaryDriver.php - - message: "#^Method Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:getExplicitDataCacheRepository\\(\\) should return Visol\\\\Cloudinary\\\\Domain\\\\Repository\\\\ExplicitDataCacheRepository but returns object\\.$#" + message: "#^Cannot access offset 0 on callable\\(\\)\\: mixed\\.$#" count: 1 path: Classes/Driver/CloudinaryDriver.php - - message: "#^Method Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:getFileContents\\(\\) should return string but returns string\\|false\\.$#" + message: "#^Cannot access offset 1 on callable\\(\\)\\: mixed\\.$#" count: 1 path: Classes/Driver/CloudinaryDriver.php - - message: "#^Method Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:isFolderEmpty\\(\\) should return bool but returns int\\.$#" + message: "#^Cannot cast mixed to int\\.$#" + count: 2 + path: Classes/Driver/CloudinaryDriver.php + + - + message: "#^Method Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:getAdminApi\\(\\) has invalid return type Cloudinary\\\\Api\\\\Admin\\\\AdminApi\\.$#" count: 1 path: Classes/Driver/CloudinaryDriver.php - - message: "#^Method Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:sanitizeFileName\\(\\) should return string but returns string\\|null\\.$#" + message: "#^Method Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:getFileContents\\(\\) should return string but returns string\\|false\\.$#" count: 1 path: Classes/Driver/CloudinaryDriver.php - - message: "#^Offset 'extension' does not exist on array\\\\|string\\.$#" + message: "#^Method Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:getUploadApi\\(\\) has invalid return type Cloudinary\\\\Api\\\\Upload\\\\UploadApi\\.$#" count: 1 path: Classes/Driver/CloudinaryDriver.php - - message: "#^Offset 'filename' does not exist on array\\\\|string\\.$#" + message: "#^Method Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:isFolderEmpty\\(\\) should return bool but returns int\\.$#" count: 1 path: Classes/Driver/CloudinaryDriver.php - - message: "#^Offset 'type' does not exist on array\\{\\}\\.$#" + message: "#^Method Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:sanitizeFileName\\(\\) should return string but returns string\\|null\\.$#" count: 1 path: Classes/Driver/CloudinaryDriver.php - - message: "#^Parameter \\#1 \\$folder of method Visol\\\\Cloudinary\\\\Services\\\\CloudinaryFolderService\\:\\:delete\\(\\) expects string, mixed given\\.$#" + message: "#^Offset 'extension' does not exist on array\\\\|string\\.$#" count: 1 path: Classes/Driver/CloudinaryDriver.php - - message: "#^Parameter \\#1 \\$publicId of method Visol\\\\Cloudinary\\\\Services\\\\CloudinaryResourceService\\:\\:delete\\(\\) expects string, mixed given\\.$#" - count: 2 + message: "#^Offset 'filename' does not exist on array\\\\|string\\.$#" + count: 1 path: Classes/Driver/CloudinaryDriver.php - - message: "#^Parameter \\#1 \\$string of function rtrim expects string, string\\|null given\\.$#" + message: "#^Offset 'type' does not exist on array\\{\\}\\.$#" count: 1 path: Classes/Driver/CloudinaryDriver.php - - message: "#^Method Visol\\\\Cloudinary\\\\EventHandlers\\\\BeforeFileProcessingEventHandler\\:\\:getCloudinaryImageService\\(\\) should return Visol\\\\Cloudinary\\\\Services\\\\CloudinaryImageService but returns object\\.$#" + message: "#^Parameter \\#1 \\$string of function rtrim expects string, string\\|null given\\.$#" count: 1 - path: Classes/EventHandlers/BeforeFileProcessingEventHandler.php + path: Classes/Driver/CloudinaryDriver.php - message: "#^Method Visol\\\\Cloudinary\\\\Filters\\\\RegularExpressionFilter\\:\\:filter\\(\\) should return bool but returns int\\|true\\.$#" @@ -541,7 +541,17 @@ parameters: path: Classes/Services/AbstractCloudinaryMediaService.php - - message: "#^Method Visol\\\\Cloudinary\\\\Services\\\\AbstractCloudinaryMediaService\\:\\:getCloudinaryPathService\\(\\) should return Visol\\\\Cloudinary\\\\Services\\\\CloudinaryPathService but returns object\\.$#" + message: "#^Call to method explicit\\(\\) on an unknown class Cloudinary\\\\Api\\\\Upload\\\\UploadApi\\.$#" + count: 1 + path: Classes/Services/AbstractCloudinaryMediaService.php + + - + message: "#^Call to method uploadApi\\(\\) on an unknown class Cloudinary\\\\Cloudinary\\.$#" + count: 1 + path: Classes/Services/AbstractCloudinaryMediaService.php + + - + message: "#^Method Visol\\\\Cloudinary\\\\Services\\\\AbstractCloudinaryMediaService\\:\\:getUploadApi\\(\\) has invalid return type Cloudinary\\\\Api\\\\Upload\\\\UploadApi\\.$#" count: 1 path: Classes/Services/AbstractCloudinaryMediaService.php @@ -565,6 +575,21 @@ parameters: count: 2 path: Classes/Services/CloudinaryFolderService.php + - + message: "#^Call to method explicit\\(\\) on an unknown class Cloudinary\\\\Api\\\\Upload\\\\UploadApi\\.$#" + count: 1 + path: Classes/Services/CloudinaryImageService.php + + - + message: "#^Call to static method fromParams\\(\\) on an unknown class Cloudinary\\\\Asset\\\\Image\\.$#" + count: 1 + path: Classes/Services/CloudinaryImageService.php + + - + message: "#^Call to static method fromParams\\(\\) on an unknown class Cloudinary\\\\Transformation\\\\ImageTransformation\\.$#" + count: 1 + path: Classes/Services/CloudinaryImageService.php + - message: "#^Cannot access offset 'width' on mixed\\.$#" count: 2 @@ -640,41 +665,61 @@ parameters: count: 2 path: Classes/Services/CloudinaryResourceService.php - - - message: "#^Method Visol\\\\Cloudinary\\\\Services\\\\CloudinaryResourceService\\:\\:getCloudinaryFolderService\\(\\) should return Visol\\\\Cloudinary\\\\Services\\\\CloudinaryFolderService but returns object\\.$#" - count: 1 - path: Classes/Services/CloudinaryResourceService.php - - message: "#^Parameter \\#2 \\$timestamp of function date expects int\\|null, int\\|false given\\.$#" count: 2 path: Classes/Services/CloudinaryResourceService.php - - message: "#^Method Visol\\\\Cloudinary\\\\Services\\\\CloudinaryScanService\\:\\:console\\(\\) has parameter \\$additionalBlankLine with no type specified\\.$#" + message: "#^Call to method adminApi\\(\\) on an unknown class Cloudinary\\\\Cloudinary\\.$#" count: 1 path: Classes/Services/CloudinaryScanService.php - - message: "#^Method Visol\\\\Cloudinary\\\\Services\\\\CloudinaryScanService\\:\\:getApi\\(\\) has no return type specified\\.$#" + message: "#^Call to method asset\\(\\) on an unknown class Cloudinary\\\\Api\\\\Admin\\\\AdminApi\\.$#" count: 1 path: Classes/Services/CloudinaryScanService.php - - message: "#^Method Visol\\\\Cloudinary\\\\Services\\\\CloudinaryScanService\\:\\:getCloudinaryFolderService\\(\\) should return Visol\\\\Cloudinary\\\\Services\\\\CloudinaryFolderService but returns object\\.$#" + message: "#^Call to method expression\\(\\) on an unknown class Cloudinary\\\\Api\\\\Search\\\\SearchApi\\.$#" count: 1 path: Classes/Services/CloudinaryScanService.php - - message: "#^Method Visol\\\\Cloudinary\\\\Services\\\\CloudinaryScanService\\:\\:getCloudinaryPathService\\(\\) should return Visol\\\\Cloudinary\\\\Services\\\\CloudinaryPathService but returns object\\.$#" + message: "#^Call to method searchApi\\(\\) on an unknown class Cloudinary\\\\Cloudinary\\.$#" + count: 1 + path: Classes/Services/CloudinaryScanService.php + + - + message: "#^Method Visol\\\\Cloudinary\\\\Services\\\\CloudinaryScanService\\:\\:console\\(\\) has parameter \\$additionalBlankLine with no type specified\\.$#" count: 1 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\\:\\:getAdminApi\\(\\) has invalid return type Cloudinary\\\\Api\\\\Admin\\\\AdminApi\\.$#" count: 1 path: Classes/Services/CloudinaryScanService.php + - + message: "#^Method Visol\\\\Cloudinary\\\\Services\\\\CloudinaryScanService\\:\\:getSearchApi\\(\\) has invalid return type Cloudinary\\\\Api\\\\Search\\\\SearchApi\\.$#" + count: 1 + path: Classes/Services/CloudinaryScanService.php + + - + message: "#^Call to method expression\\(\\) on an unknown class Cloudinary\\\\Api\\\\Search\\\\SearchApi\\.$#" + count: 1 + path: Classes/Services/CloudinaryTestConnectionService.php + + - + message: "#^Call to method searchApi\\(\\) on an unknown class Cloudinary\\\\Cloudinary\\.$#" + count: 1 + path: Classes/Services/CloudinaryTestConnectionService.php + + - + message: "#^Method Visol\\\\Cloudinary\\\\Services\\\\CloudinaryTestConnectionService\\:\\:getSearchApi\\(\\) has invalid return type Cloudinary\\\\Api\\\\Search\\\\SearchApi\\.$#" + count: 1 + path: Classes/Services/CloudinaryTestConnectionService.php + - message: "#^Method Visol\\\\Cloudinary\\\\Services\\\\CloudinaryTestConnectionService\\:\\:test\\(\\) has no return type specified\\.$#" count: 1 @@ -685,28 +730,73 @@ parameters: count: 1 path: Classes/Services/CloudinaryUploadService.php + - + message: "#^Call to static method fromParams\\(\\) on an unknown class Cloudinary\\\\Asset\\\\Video\\.$#" + count: 1 + path: Classes/Services/CloudinaryVideoService.php + - message: "#^Property Visol\\\\Cloudinary\\\\Services\\\\CloudinaryVideoService\\:\\:\\$defaultOptions has no type specified\\.$#" count: 1 path: Classes/Services/CloudinaryVideoService.php + - + message: "#^Call to method adminApi\\(\\) on an unknown class Cloudinary\\\\Cloudinary\\.$#" + count: 1 + path: Classes/Services/Extractor/CloudinaryMetaDataExtractor.php + + - + message: "#^Call to method asset\\(\\) on an unknown class Cloudinary\\\\Api\\\\Admin\\\\AdminApi\\.$#" + count: 1 + path: Classes/Services/Extractor/CloudinaryMetaDataExtractor.php + + - + message: "#^Method Visol\\\\Cloudinary\\\\Services\\\\Extractor\\\\CloudinaryMetaDataExtractor\\:\\:getAdminApi\\(\\) has invalid return type Cloudinary\\\\Api\\\\Admin\\\\AdminApi\\.$#" + 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 method adminApi\\(\\) on an unknown class Cloudinary\\\\Cloudinary\\.$#" + count: 1 + path: Classes/Services/FileMoveService.php + + - + message: "#^Call to method asset\\(\\) on an unknown class Cloudinary\\\\Api\\\\Admin\\\\AdminApi\\.$#" + count: 1 + path: Classes/Services/FileMoveService.php + + - + message: "#^Call to method upload\\(\\) on an unknown class Cloudinary\\\\Api\\\\Upload\\\\UploadApi\\.$#" + count: 1 + path: Classes/Services/FileMoveService.php + + - + message: "#^Call to method uploadApi\\(\\) on an unknown class Cloudinary\\\\Cloudinary\\.$#" + count: 1 + path: Classes/Services/FileMoveService.php + - 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\\:\\:getAdminApi\\(\\) has invalid return type Cloudinary\\\\Api\\\\Admin\\\\AdminApi\\.$#" + count: 1 + path: Classes/Services/FileMoveService.php + - 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 - - message: "#^Method Visol\\\\Cloudinary\\\\Services\\\\FileMoveService\\:\\:initializeApi\\(\\) has no return type specified\\.$#" + message: "#^Method Visol\\\\Cloudinary\\\\Services\\\\FileMoveService\\:\\:getUploadApi\\(\\) has invalid return type Cloudinary\\\\Api\\\\Upload\\\\UploadApi\\.$#" count: 1 path: Classes/Services/FileMoveService.php @@ -716,12 +806,32 @@ parameters: path: Classes/Services/FileMoveService.php - - message: "#^Variable \\$resource in empty\\(\\) always exists and is not falsy\\.$#" + message: "#^Call to static method instance\\(\\) on an unknown class Cloudinary\\\\Configuration\\\\Configuration\\.$#" count: 1 - path: Classes/Services/FileMoveService.php + path: Classes/Utility/CloudinaryApiUtility.php + + - + message: "#^Cannot call method getConfiguration\\(\\) on array\\|TYPO3\\\\CMS\\\\Core\\\\Resource\\\\ResourceStorage\\.$#" + count: 1 + path: Classes/Utility/CloudinaryApiUtility.php + + - + message: "#^Cannot call method getDriverType\\(\\) on array\\|TYPO3\\\\CMS\\\\Core\\\\Resource\\\\ResourceStorage\\.$#" + count: 2 + path: Classes/Utility/CloudinaryApiUtility.php + + - + message: "#^Instantiated class Cloudinary\\\\Cloudinary not found\\.$#" + count: 1 + path: Classes/Utility/CloudinaryApiUtility.php + + - + message: "#^Method Visol\\\\Cloudinary\\\\Utility\\\\CloudinaryApiUtility\\:\\:getCloudinary\\(\\) has invalid return type Cloudinary\\\\Cloudinary\\.$#" + count: 1 + path: Classes/Utility/CloudinaryApiUtility.php - - message: "#^Method Visol\\\\Cloudinary\\\\Utility\\\\CloudinaryApiUtility\\:\\:initializeByConfiguration\\(\\) has no return type specified\\.$#" + message: "#^Method Visol\\\\Cloudinary\\\\Utility\\\\CloudinaryApiUtility\\:\\:getConfiguration\\(\\) has invalid return type Cloudinary\\\\Configuration\\\\Configuration\\.$#" count: 1 path: Classes/Utility/CloudinaryApiUtility.php diff --git a/phpstan.neon b/phpstan.neon index 44b0526..9356186 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -14,3 +14,5 @@ parameters: message: '#^Call to an undefined method object#' - message: '#^Cannot call method fetch.* on Doctrine\\DBAL\\Result\|int#' + - + message: '#should return .* but returns object.#' From e70bab7f7eb4c975d66b3921cacc389b0ea925dc Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Fri, 12 May 2023 14:13:31 +0200 Subject: [PATCH 58/99] [DOCS] Remove obsolete processed file config --- Documentation/driver-configuration-02.png | Bin 8429 -> 0 bytes README.md | 3 --- 2 files changed, 3 deletions(-) delete mode 100644 Documentation/driver-configuration-02.png diff --git a/Documentation/driver-configuration-02.png b/Documentation/driver-configuration-02.png deleted file mode 100644 index 3001b7fcc62ae3213c8427d15ca534330e1b4d30..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8429 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 Date: Fri, 12 May 2023 14:14:56 +0200 Subject: [PATCH 59/99] [STYLE] Reformat code for better readability --- Classes/Driver/CloudinaryDriver.php | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Classes/Driver/CloudinaryDriver.php b/Classes/Driver/CloudinaryDriver.php index 137b569..eae6399 100644 --- a/Classes/Driver/CloudinaryDriver.php +++ b/Classes/Driver/CloudinaryDriver.php @@ -274,12 +274,14 @@ public function addFile($localFilePath, $targetFolderIdentifier, $newFileName = ); // Upload the file - $cloudinaryResource = (array)$this->getUploadApi()->upload($localFilePath, [ - 'public_id' => PathUtility::basename($cloudinaryPublicId), - 'folder' => $this->getCloudinaryPathService()->computeCloudinaryFolderPath($targetFolderIdentifier), - 'resource_type' => $this->getCloudinaryPathService()->getResourceType($fileIdentifier), - 'overwrite' => true, - ]); + $cloudinaryResource = (array)$this->getUploadApi()->upload( + $localFilePath, [ + 'public_id' => PathUtility::basename($cloudinaryPublicId), + 'folder' => $this->getCloudinaryPathService()->computeCloudinaryFolderPath($targetFolderIdentifier), + 'resource_type' => $this->getCloudinaryPathService()->getResourceType($fileIdentifier), + 'overwrite' => true, + ] + ); $this->checkCloudinaryUploadStatus($cloudinaryResource, $fileIdentifier); From 1eec90e0ec6deeba0cb4f2ed6d981aa343482930 Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Fri, 12 May 2023 14:15:47 +0200 Subject: [PATCH 60/99] [REFACTOR] Simplify default orderings logic and use null coalescing operator --- Classes/Driver/CloudinaryDriver.php | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/Classes/Driver/CloudinaryDriver.php b/Classes/Driver/CloudinaryDriver.php index eae6399..e339539 100644 --- a/Classes/Driver/CloudinaryDriver.php +++ b/Classes/Driver/CloudinaryDriver.php @@ -739,17 +739,16 @@ public function getFilesInFolder( // Set default orderings $parameters = (array)GeneralUtility::_GP('SET'); - if ($parameters['sort'] === 'file') { - $parameters['sort'] = 'filename'; - } elseif ($parameters['sort'] === 'tstamp') { - $parameters['sort'] = 'created_at'; - } else { - $parameters['sort'] = 'filename'; - $parameters['reverse'] = 'ASC'; + + $orderField = $parameters['sort'] ?? 'filename'; + if ($orderField === 'file') { + $orderField = 'filename'; + } elseif ($orderField === 'tstamp') { + $orderField = 'created_at'; } $orderings = [ - 'fieldName' => $parameters['sort'], + 'fieldName' => $orderField, 'direction' => isset($parameters['reverse']) && (int)$parameters['reverse'] ? 'DESC' : 'ASC', ]; From 99de0d85ea80d80bc25c4c49773afa18232529aa Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Fri, 12 May 2023 14:16:15 +0200 Subject: [PATCH 61/99] [REFACTOR] Simplify file extension handling in cleanFileName() method --- Classes/Driver/CloudinaryDriver.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Classes/Driver/CloudinaryDriver.php b/Classes/Driver/CloudinaryDriver.php index e339539..f775b15 100644 --- a/Classes/Driver/CloudinaryDriver.php +++ b/Classes/Driver/CloudinaryDriver.php @@ -952,10 +952,11 @@ public function sanitizeFileName($fileName, $charset = ''): string } $pathParts = PathUtility::pathinfo($cleanFileName); + $fileExtension = $pathParts['extension'] ?? ''; $cleanFileName = str_replace('.', '_', $pathParts['filename']) . - ($pathParts['extension'] ? '.' . $pathParts['extension'] : ''); + ($fileExtension ? '.' . $fileExtension : ''); // Handle the special jpg case which does not correspond to the file extension. return preg_replace('/jpeg$/', 'jpg', $cleanFileName); From 123c2b3df6453933dc9467d870e46308b50f37f2 Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Fri, 12 May 2023 14:16:49 +0200 Subject: [PATCH 62/99] [BUGFIX] Add fallback mechanism to get resource type from file extension --- Classes/Services/CloudinaryPathService.php | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/Classes/Services/CloudinaryPathService.php b/Classes/Services/CloudinaryPathService.php index 5ac4b2b..ce45ddc 100644 --- a/Classes/Services/CloudinaryPathService.php +++ b/Classes/Services/CloudinaryPathService.php @@ -13,6 +13,7 @@ use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\Utility\PathUtility; use Visol\Cloudinary\Driver\CloudinaryDriver; +use Visol\Cloudinary\Utility\MimeTypeUtility; class CloudinaryPathService { @@ -95,7 +96,24 @@ public function getMimeType(array $fileInfo): string public function getResourceType(string $fileIdentifier): string { - $cloudinaryResource = $this->getCloudinaryResource($fileIdentifier); + try { + // Find the resource type from the cloudinary resource. + $cloudinaryResource = $this->getCloudinaryResource($fileIdentifier); + } catch (\RuntimeException $e) { + $fileExtension = $this->getFileExtension($fileIdentifier); + $mimeType = MimeTypeUtility::guessMimeType($fileExtension); + + // Get the primary resource type from the mime type such as image, video, audio, raw + $type = explode('/', $mimeType)[0]; + + // Equivalence table. + if ($type === 'application') { + $type = 'image'; + } elseif ($type === 'text') { + $type = 'raw'; + } + return $type; + } return $cloudinaryResource['resource_type'] ?? 'unknown'; } From f06fd1d813ca3a7fdd250e0979a367e6f7756230 Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Fri, 12 May 2023 14:17:41 +0200 Subject: [PATCH 63/99] [BUGFIX] Method getConfiguration and add support for array storage input --- Classes/Utility/CloudinaryApiUtility.php | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/Classes/Utility/CloudinaryApiUtility.php b/Classes/Utility/CloudinaryApiUtility.php index 1a82af0..949ade4 100644 --- a/Classes/Utility/CloudinaryApiUtility.php +++ b/Classes/Utility/CloudinaryApiUtility.php @@ -29,16 +29,20 @@ public static function getCloudinary(ResourceStorage|array $storage): Cloudinary public static function getConfiguration(ResourceStorage|array $storage): Configuration { - if ($storage->getDriverType() !== CloudinaryDriver::DRIVER_TYPE) { - // Check the file is stored on the right storage - // If not we should trigger an exception - $message = sprintf( - 'Wrong storage! Can not initialize with storage type "%s".', - $storage->getDriverType() - ); - throw new \Exception($message, 1590401459); + if (is_array($storage)) { + $storageConfiguration = $storage; + } else { + if ($storage->getDriverType() !== CloudinaryDriver::DRIVER_TYPE) { + // Check the file is stored on the right storage + // If not we should trigger an exception + $message = sprintf( + 'Wrong storage! Can not initialize with storage type "%s".', + $storage->getDriverType() + ); + throw new \Exception($message, 1590401459); + } + $storageConfiguration = $storage->getConfiguration(); } - $storageConfiguration = $storage->getConfiguration(); /** @var ConfigurationService $configurationService */ $configurationService = GeneralUtility::makeInstance( From d5c000de4b14e4ed21148f686fcb3a3d294e3132 Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Fri, 12 May 2023 14:18:48 +0200 Subject: [PATCH 64/99] [BUGFIX] Acceptance tests --- .../Command/CloudinaryAcceptanceTestCommand.php | 15 +++++---------- .../FileOperation/AddFileOperationTest.php | 1 + Tests/Acceptance/FileTestSuite.php | 11 ++++------- Tests/Acceptance/FolderTestSuite.php | 11 ++++------- 4 files changed, 14 insertions(+), 24 deletions(-) diff --git a/Classes/Command/CloudinaryAcceptanceTestCommand.php b/Classes/Command/CloudinaryAcceptanceTestCommand.php index a56d8dc..69631ca 100644 --- a/Classes/Command/CloudinaryAcceptanceTestCommand.php +++ b/Classes/Command/CloudinaryAcceptanceTestCommand.php @@ -61,7 +61,7 @@ protected function configure(): void 'The API configuration' ) ->setHelp( - 'Usage: ./vendor/bin/typo3 cloudinary:tests' + 'Usage: ./vendor/bin/typo3 cloudinary:tests bucket-name:my-api-key:my-api-secret' ); } @@ -76,12 +76,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int [$couldName, $apiKey, $apiSecret] = GeneralUtility::trimExplode(':', $input->getArgument('api-configuration')); if (!$couldName || !$apiKey || !$apiSecret) { // Everything must be defined! - $message = 'API configuration is incomplete. Format should be "cld-name:1234:abcd".' . LF . LF; - $message .= '"cld-name" is the name of the cloudinary bucket' . LF; - $message .= '"12345" is the API key' . LF; - $message .= '"abcd" is the API secret' . LF . LF; - $message .= 'https://cloudinary.com/console' . LF; - $message .= 'Strong advice! Take a free account to run the test suite'; + $message = 'API configuration is incomplete. Format should be "bucket-name:my-api-key:my-api-secret".'; $this->error($message); return Command::INVALID; } @@ -98,7 +93,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int return Command::FAILURE; } - // Test case for video file + // Test case for files $testSuite = new FileTestSuite($storageId, $this->io); $testSuite->runTests(); @@ -184,7 +179,7 @@ protected function tearDown(int $storageId) ); // Remove all cache - $db->truncate('cf_cloudinary'); - $db->truncate('cf_cloudinary_tags'); +// $db->truncate('cf_cloudinary'); +// $db->truncate('cf_cloudinary_tags'); } } diff --git a/Tests/Acceptance/FileOperation/AddFileOperationTest.php b/Tests/Acceptance/FileOperation/AddFileOperationTest.php index 4f1763d..72e59f2 100644 --- a/Tests/Acceptance/FileOperation/AddFileOperationTest.php +++ b/Tests/Acceptance/FileOperation/AddFileOperationTest.php @@ -15,6 +15,7 @@ class AddFileOperationTest extends AbstractCloudinaryFileOperationTest public function run() { $fixtureFile = $this->getFilePath($this->resourceName); + $file = $this->getStorage()->addFile( $fixtureFile, $this->getContainingFolder($this->resourceName), diff --git a/Tests/Acceptance/FileTestSuite.php b/Tests/Acceptance/FileTestSuite.php index cfec724..6d2082b 100644 --- a/Tests/Acceptance/FileTestSuite.php +++ b/Tests/Acceptance/FileTestSuite.php @@ -13,21 +13,18 @@ class FileTestSuite extends AbstractCloudinaryTestSuite { - /** - * @var - */ - protected $files = [ + protected array $files = [ 'sub-folder/image-jpeg.jpeg', 'sub-folder/image-tiff.tiff', 'image-jpg.jpg', 'image-png.png', - 'document.odt', + #'document.odt', 'document.pdf', - 'video.youtube', + #'video.youtube', 'video.mp4', ]; - public function runTests() + public function runTests(): void { // Basic access file such as read, write, delete diff --git a/Tests/Acceptance/FolderTestSuite.php b/Tests/Acceptance/FolderTestSuite.php index 37b3295..a9d423b 100644 --- a/Tests/Acceptance/FolderTestSuite.php +++ b/Tests/Acceptance/FolderTestSuite.php @@ -13,19 +13,16 @@ class FolderTestSuite extends AbstractCloudinaryTestSuite { - /** - * @var - */ - protected $files = [ + protected array $files = [ 'sub-folder/image-jpeg.jpeg', 'sub-folder/image-tiff.tiff', 'sub-folder/sub-sub-folder/image-jpeg.jpeg', 'sub-folder/sub-sub-folder/image-tiff.tiff', 'image-jpg.jpg', 'image-png.png', - 'document.odt', + #'document.odt', 'document.pdf', - 'video.youtube', + #'video.youtube', 'video.mp4', ]; @@ -41,7 +38,7 @@ public function runTests() // Count files $test = new CountFilesInFolderOperationTests($this, '/'); - $test->setExpectedNumberOfFiles(6) + $test->setExpectedNumberOfFiles(4) ->setExpectedNumberOfFolders(1) // _processed_ folder must be taken into consideration in the root folder ->run(); From 77dced361ccd7848e645a358ff960cd3baaf25f9 Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Fri, 12 May 2023 17:50:26 +0200 Subject: [PATCH 65/99] [BUGFIX] Catch all exceptions instead of just UniqueConstraintViolationException --- Classes/Services/CloudinaryImageService.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Classes/Services/CloudinaryImageService.php b/Classes/Services/CloudinaryImageService.php index dc107e0..7dad8cd 100644 --- a/Classes/Services/CloudinaryImageService.php +++ b/Classes/Services/CloudinaryImageService.php @@ -12,7 +12,6 @@ use Cloudinary\Asset\Image; use Cloudinary\Transformation\ImageTransformation; use TYPO3\CMS\Core\Resource\StorageRepository; -use Doctrine\DBAL\Exception\UniqueConstraintViolationException; use TYPO3\CMS\Core\Resource\File; use TYPO3\CMS\Core\Utility\GeneralUtility; use Visol\Cloudinary\Domain\Repository\ExplicitDataCacheRepository; @@ -56,7 +55,8 @@ public function getExplicitData(File $file, array $options): array try { $explicitData = (array)$this->getUploadApi($file->getStorage())->explicit($publicId, $apiOptions); $this->explicitDataCacheRepository->save($file->getStorage()->getUid(), $publicId, $options, $explicitData); - } catch (UniqueConstraintViolationException $e) { + } catch (\Exception $e) { + $explicitData = []; // ignore } } From fb53a413e7b5496946d91a3e6e5f3ab56b7a0a2b Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Fri, 12 May 2023 18:45:40 +0200 Subject: [PATCH 66/99] [ENHANCE] Add more descriptive help message for the command --- Classes/Command/CloudinaryFixJpegCommand.php | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/Classes/Command/CloudinaryFixJpegCommand.php b/Classes/Command/CloudinaryFixJpegCommand.php index a99a3a3..af79fd0 100644 --- a/Classes/Command/CloudinaryFixJpegCommand.php +++ b/Classes/Command/CloudinaryFixJpegCommand.php @@ -36,9 +36,6 @@ protected function initialize(InputInterface $input, OutputInterface $output): v $this->targetStorage = $resourceFactory->getStorageObject($input->getArgument('target')); } - /** - * Configure the command by defining the name, options and arguments - */ protected function configure(): void { $message = 'After "moving" files you should fix the jpeg extension. Consult README.md for more info.'; @@ -46,12 +43,9 @@ protected function configure(): void ->addOption('silent', 's', InputOption::VALUE_OPTIONAL, 'Mute output as much as possible', false) ->addOption('yes', 'y', InputOption::VALUE_OPTIONAL, 'Accept everything by default', false) ->addArgument('target', InputArgument::REQUIRED, 'Target storage identifier') - ->setHelp('Usage: ./vendor/bin/typo3 cloudinary:fix [0-9]'); + ->setHelp('Usage: ./vendor/bin/typo3 cloudinary:fix:image:after-move [0-9]'); } - /** - * Move file - */ protected function execute(InputInterface $input, OutputInterface $output): int { if (!$this->checkDriverType($this->targetStorage)) { From c96448bc095413aaa70c182e03b945153a260b73 Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Fri, 12 May 2023 18:46:18 +0200 Subject: [PATCH 67/99] [STYLE] Add more spaces --- Classes/Controller/CloudinaryWebHookController.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Classes/Controller/CloudinaryWebHookController.php b/Classes/Controller/CloudinaryWebHookController.php index 9b05404..47b6ed0 100644 --- a/Classes/Controller/CloudinaryWebHookController.php +++ b/Classes/Controller/CloudinaryWebHookController.php @@ -40,7 +40,9 @@ class CloudinaryWebHookController extends ActionController { protected const NOTIFICATION_TYPE_UPLOAD = 'upload'; + protected const NOTIFICATION_TYPE_RENAME = 'rename'; + protected const NOTIFICATION_TYPE_DELETE = 'delete'; protected CloudinaryResourceService $cloudinaryResourceService; From 9484249d62a6413f0efe10a117e597de1ad49d8b Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Fri, 12 May 2023 18:46:49 +0200 Subject: [PATCH 68/99] [FEATURE] Add command to fix missing width and height of image --- .../CloudinaryFixImageDimensionCommand.php | 146 ++++++++++++++++++ Configuration/Services.yaml | 8 +- 2 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 Classes/Command/CloudinaryFixImageDimensionCommand.php diff --git a/Classes/Command/CloudinaryFixImageDimensionCommand.php b/Classes/Command/CloudinaryFixImageDimensionCommand.php new file mode 100644 index 0000000..0a78bbf --- /dev/null +++ b/Classes/Command/CloudinaryFixImageDimensionCommand.php @@ -0,0 +1,146 @@ +io = new SymfonyStyle($input, $output); + + $this->isSilent = $input->getOption('silent'); + + /** @var ResourceFactory $resourceFactory */ + $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class); + $this->storage = $resourceFactory->getStorageObject($input->getArgument('target')); + + $this->cloudinaryResourceService = GeneralUtility::makeInstance( + CloudinaryResourceService::class, + $this->storage, + ); + + $this->cloudinaryPathService = GeneralUtility::makeInstance( + CloudinaryPathService::class, + $this->storage, + ); + + $this->metadataRepository = GeneralUtility::makeInstance(MetaDataRepository::class); + } + + protected function configure(): void + { + $this->setDescription('Fix missing width and height of image.') + ->addOption('silent', 's', InputOption::VALUE_OPTIONAL, 'Mute output as much as possible', false) + ->addOption('yes', 'y', InputOption::VALUE_OPTIONAL, 'Accept everything by default', false) + ->addArgument('target', InputArgument::REQUIRED, 'Target storage identifier') + ->setHelp('Usage: ./vendor/bin/typo3 cloudinary:fix:dimension [0-9]'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + if (!$this->checkDriverType($this->storage)) { + $this->log('Look out! target storage is not of type "cloudinary"'); + return Command::INVALID; + } + + $files = $this->getProblematicImages(); + + foreach ($files as $file) { + $fileObject = GeneralUtility::makeInstance(ResourceFactory::class)->getFileObject($file['uid']); + if ($fileObject->exists()) { + $this->log('Fixing %s (%s)', [$fileObject->getIdentifier(), $fileObject->getUid()]); + + // get the corresponding cloudinary resource + $publicId = $this->cloudinaryPathService->computeCloudinaryPublicId($fileObject->getIdentifier()); + + $cloudinaryResource = $this->cloudinaryResourceService->getResource($publicId); + + // we update the metadata + $this->metadataRepository->update( + $fileObject->getUid(), + [ + 'width' => $cloudinaryResource['width'], + 'height' => $cloudinaryResource['height'], + ] + ); + + } + } + + return Command::SUCCESS; + } + + protected function getProblematicImages(): array + { + $query = $this->getQueryBuilder($this->tableName); + $query + ->select('sys_file.uid') + ->from($this->tableName) + ->join( + $this->tableName, + 'sys_file_metadata', + 'metadata', + $query->expr()->eq( + 'metadata.file', + $this->tableName . '.uid' + ) + ) + ->where( + $query->expr()->eq( + 'storage', + $this->storage->getUid() + ), + $query->expr()->eq( + 'type', + File::FILETYPE_IMAGE + ), + $query->expr()->eq( + 'missing', + 0 + ), + $query->expr()->orX( + $query->expr()->eq( + 'metadata.width', + 0 + ), + $query->expr()->eq( + 'metadata.height', + 0 + ) + ) + ); + + return $query->execute()->fetchAllAssociative(); + } +} diff --git a/Configuration/Services.yaml b/Configuration/Services.yaml index 8bb65ae..3562a7a 100644 --- a/Configuration/Services.yaml +++ b/Configuration/Services.yaml @@ -35,10 +35,16 @@ services: schedulable: false description: Run a suite of Acceptance Tests. + Visol\Cloudinary\Command\CloudinaryFixImageDimensionCommand: + tags: + - name: 'console.command' + command: 'cloudinary:fix:image:dimension' + description: Fix missing width and height of image. + Visol\Cloudinary\Command\CloudinaryFixJpegCommand: tags: - name: 'console.command' - command: 'cloudinary:fix' + command: 'cloudinary:fix:image:after-move' schedulable: false description: After "moving" files you should fix the jpeg extension. Consult README.md for more info. From 92c591a1ec437e5f6bb8dbdd154ca3f5fc67e25e Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Wed, 17 May 2023 10:06:10 +0200 Subject: [PATCH 69/99] [BUGFIX] Try again with different resource types if necessary with command cloudinary:api --- Classes/Command/CloudinaryApiCommand.php | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/Classes/Command/CloudinaryApiCommand.php b/Classes/Command/CloudinaryApiCommand.php index 2a38e70..8605b72 100644 --- a/Classes/Command/CloudinaryApiCommand.php +++ b/Classes/Command/CloudinaryApiCommand.php @@ -21,7 +21,6 @@ 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; @@ -50,7 +49,7 @@ class CloudinaryApiCommand extends AbstractCloudinaryCommand # 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 { @@ -109,7 +108,16 @@ protected function execute(InputInterface $input, OutputInterface $output): int try { if ($publicId) { - $resource = $this->getAdminApi()->asset($publicId); + try { + $resource = $this->getAdminApi()->asset($publicId); + } catch (\Exception $e) { + // More attempts if we have a video or raw file + try { + $resource = $this->getAdminApi()->asset($publicId, ['resource_type' => 'video']); + } catch (\Exception $e) { + $resource = $this->getAdminApi()->asset($publicId, ['resource_type' => 'raw']); + } + } $this->log(var_export((array)$resource, true)); } elseif ($expression) { From 74f9bea4e4f4547d197775e53bc5e33804a153a8 Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Wed, 17 May 2023 10:47:45 +0200 Subject: [PATCH 70/99] [REFACTOR] Add support for type option to override resource type when querying by public id --- Classes/Command/CloudinaryApiCommand.php | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/Classes/Command/CloudinaryApiCommand.php b/Classes/Command/CloudinaryApiCommand.php index 8605b72..08520fb 100644 --- a/Classes/Command/CloudinaryApiCommand.php +++ b/Classes/Command/CloudinaryApiCommand.php @@ -35,6 +35,7 @@ class CloudinaryApiCommand extends AbstractCloudinaryCommand # Query by public id typo3 cloudinary:api [0-9] --publicId="foo-bar" +typo3 cloudinary:api [0-9] --publicId="foo-bar" --type="video" # Query by file uid (will retrieve the public id from the file) typo3 cloudinary:api --fileUid="[0-9]" @@ -67,6 +68,7 @@ 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('type', '', InputOption::VALUE_OPTIONAL, 'In combination with publicId, overrides the type. Possible value iamge, video or raw', 'image') ->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) @@ -108,16 +110,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int try { if ($publicId) { - try { - $resource = $this->getAdminApi()->asset($publicId); - } catch (\Exception $e) { - // More attempts if we have a video or raw file - try { - $resource = $this->getAdminApi()->asset($publicId, ['resource_type' => 'video']); - } catch (\Exception $e) { - $resource = $this->getAdminApi()->asset($publicId, ['resource_type' => 'raw']); - } - } + $resource = $this->getAdminApi()->asset($publicId, ['resource_type' => $input->getOption('type')]); $this->log(var_export((array)$resource, true)); } elseif ($expression) { From 062eea22b1a3323391c2f98f5eaffed43e5a1c7e Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Tue, 23 May 2023 09:05:12 +0200 Subject: [PATCH 71/99] [BUGFIX] defaultOptions variable must be transmitted to cloudinary api --- Classes/Services/CloudinaryVideoService.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Classes/Services/CloudinaryVideoService.php b/Classes/Services/CloudinaryVideoService.php index be9df49..971b457 100644 --- a/Classes/Services/CloudinaryVideoService.php +++ b/Classes/Services/CloudinaryVideoService.php @@ -8,7 +8,7 @@ class CloudinaryVideoService extends AbstractCloudinaryMediaService { - protected $defaultOptions = [ + protected array $defaultOptions = [ 'type' => 'upload', 'resource_type' => 'video', 'fetch_format' => 'auto', @@ -22,7 +22,8 @@ public function getVideoUrl(File $file, array $options = []): string $publicId = $this->getPublicIdForFile($file); $configuration = CloudinaryApiUtility::getConfiguration($file->getStorage()); - return Video::fromParams($publicId) + + return Video::fromParams($publicId, $options) ->configuration($configuration) ->toUrl(); } From a41c5845f8184d6a93a60b26ea7c0eb39273930f Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Tue, 23 May 2023 09:05:52 +0200 Subject: [PATCH 72/99] [FEATURE] Support new "cname" option to deliver resource with private cdn --- Classes/Services/ConfigurationService.php | 12 +++--------- Classes/Utility/CloudinaryApiUtility.php | 4 +++- Configuration/FlexForm/CloudinaryFlexForm.xml | 8 ++++++++ README.md | 2 +- Resources/Private/Language/backend.xlf | 7 +++++-- 5 files changed, 20 insertions(+), 13 deletions(-) diff --git a/Classes/Services/ConfigurationService.php b/Classes/Services/ConfigurationService.php index 32fa820..dd2d8d3 100644 --- a/Classes/Services/ConfigurationService.php +++ b/Classes/Services/ConfigurationService.php @@ -10,15 +10,9 @@ */ -/** - * Class ConfigurationService - */ class ConfigurationService { - /** - * @var array - */ - protected $configuration = []; + protected array $configuration = []; /** * ConfigurationService constructor. @@ -33,8 +27,8 @@ public function __construct(array $configuration) */ public function get(string $key): string { - $value = (string)$this->configuration[$key]; - if (preg_match('/^%(.*)%$/', $value, $matches)) { + $value = trim((string)$this->configuration[$key]); + if (preg_match('/^%\w+\((.*)\)%$/', $value, $matches) || preg_match('/^%(.*)%$/', $value, $matches)) { $value = getenv($matches[1]); if ($value === false) { diff --git a/Classes/Utility/CloudinaryApiUtility.php b/Classes/Utility/CloudinaryApiUtility.php index 949ade4..8b39892 100644 --- a/Classes/Utility/CloudinaryApiUtility.php +++ b/Classes/Utility/CloudinaryApiUtility.php @@ -55,7 +55,9 @@ public static function getConfiguration(ResourceStorage|array $storage): Configu 'api_key' => $configurationService->get('apiKey'), 'api_secret' => $configurationService->get('apiSecret'), 'timeout' => $configurationService->get('timeout'), - 'secure' => true + 'secure' => true, + 'private_cdn' => $configurationService->get('cname') !== '', + 'secure_distribution' => $configurationService->get('cname'), ] ); diff --git a/Configuration/FlexForm/CloudinaryFlexForm.xml b/Configuration/FlexForm/CloudinaryFlexForm.xml index 0a5d5e5..5a5cb5e 100644 --- a/Configuration/FlexForm/CloudinaryFlexForm.xml +++ b/Configuration/FlexForm/CloudinaryFlexForm.xml @@ -46,6 +46,14 @@ + + + + + input + + + diff --git a/README.md b/README.md index 16e6c03..8bba5cf 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ Notice the first time you click on a folder in the File list module, it will take some time since the images must be fetched and put into the cloudinary cache. Notice you can also use environment variable to configure the storage. -The environment variable should be surrounded by %. Example `%BUCKET_NAME%` +The environment variable should be surrounded by %. Example `%env(BUCKET_NAME%)` ![](Documentation/driver-configuration-01.png) diff --git a/Resources/Private/Language/backend.xlf b/Resources/Private/Language/backend.xlf index fa1760f..1389893 100644 --- a/Resources/Private/Language/backend.xlf +++ b/Resources/Private/Language/backend.xlf @@ -14,10 +14,13 @@ Cloudinary API Secret - A possible base path to store files in a parent directory. Example `production/` + A possible base path for storing files in a parent directory on cloudianry. Example `production/` + + + A possible CNAME to serve your assets from, instead of the default res.cloudinary.com domain name. - Email used for authentication (auto-login) in file reference fields in TCEForm. + Email address used for authentication (auto-login) in file reference fields within TCEForm. Cloudinary API timeout From 92cd1b68f736723ac69ecef2d7cd4b64c181876c Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Sun, 18 Jun 2023 20:14:58 +0200 Subject: [PATCH 73/99] [FEATURE] Add flag to force action without confirmation --- Classes/Command/CloudinaryApiCommand.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Classes/Command/CloudinaryApiCommand.php b/Classes/Command/CloudinaryApiCommand.php index 08520fb..73a6ad9 100644 --- a/Classes/Command/CloudinaryApiCommand.php +++ b/Classes/Command/CloudinaryApiCommand.php @@ -50,6 +50,7 @@ class CloudinaryApiCommand extends AbstractCloudinaryCommand # Delete the resources according to the expression typo3 cloudinary:api [0-9] --expression="folder=fileadmin/_processed_/*" --delete +typo3 cloudinary:api [0-9] --expression="folder=fileadmin/_processed_/*" --delete --force '; protected function initialize(InputInterface $input, OutputInterface $output): void @@ -66,6 +67,7 @@ protected function configure(): void $message = 'Interact with cloudinary API'; $this->setDescription($message) ->addOption('silent', 's', InputOption::VALUE_OPTIONAL, 'Mute output as much as possible', false) + ->addOption('force', 'f', InputOption::VALUE_OPTIONAL, 'Force the given action without further confirmation', false) ->addOption('fileUid', '', InputOption::VALUE_OPTIONAL, 'File uid', '') ->addOption('publicId', '', InputOption::VALUE_OPTIONAL, 'Cloudinary public id', '') ->addOption('type', '', InputOption::VALUE_OPTIONAL, 'In combination with publicId, overrides the type. Possible value iamge, video or raw', 'image') @@ -87,8 +89,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int $expression = $input->getOption('expression'); $list = $input->getOption('list') === null; $delete = $input->getOption('delete') === null; + $force = $input->getOption('force') === null; - if ($delete) { + if ($delete && !$force) { // ask the user whether it should continue $continue = $this->io->confirm('Are you sure you want to delete the resources?'); if (!$continue) { From e9bc1c7898f193ade681a554a7769db4252e400d Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Sun, 18 Jun 2023 20:33:22 +0200 Subject: [PATCH 74/99] [BUGFIX] Remove exception when no resources are find when deleting files --- Classes/Command/CloudinaryApiCommand.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Classes/Command/CloudinaryApiCommand.php b/Classes/Command/CloudinaryApiCommand.php index 73a6ad9..5d82b0e 100644 --- a/Classes/Command/CloudinaryApiCommand.php +++ b/Classes/Command/CloudinaryApiCommand.php @@ -155,6 +155,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->log('Nothing to do...'); } } catch (\Exception $exception) { + // Triggered when no resources are found when deleting files. + if ($exception->getMessage() === 'Missing required parameter - public_ids') { + $this->log("No resources found for expression '$expression'. Nothing to do..."); + return Command::SUCCESS; + } $this->error($exception->getMessage()); } From d568b1a593b3c75b19760558a4868494ed2b711e Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Fri, 14 Jul 2023 15:44:52 +0200 Subject: [PATCH 75/99] [FEATURE] Add command cloudinary:storage:list --- .../Command/CloudinaryStorageListCommand.php | 94 +++++++++++++++++++ Classes/Services/ConfigurationService.php | 3 +- Classes/Utility/CloudinaryApiUtility.php | 26 ++--- Configuration/Services.yaml | 7 ++ 4 files changed, 118 insertions(+), 12 deletions(-) create mode 100644 Classes/Command/CloudinaryStorageListCommand.php diff --git a/Classes/Command/CloudinaryStorageListCommand.php b/Classes/Command/CloudinaryStorageListCommand.php new file mode 100644 index 0000000..22fa400 --- /dev/null +++ b/Classes/Command/CloudinaryStorageListCommand.php @@ -0,0 +1,94 @@ +io = new SymfonyStyle($input, $output); + + } + + protected function configure(): void + { + $message = 'Display all available storages configured for cloudinary'; + $this->setDescription($message) + ->setHelp($this->help); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + + $cloudinaryRecords = $this->getCloudinaryRecords(); + foreach ($cloudinaryRecords as $cloudinaryRecord) { + + $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class); + $storage = $resourceFactory->getStorageObject($cloudinaryRecord['uid']); + + $this->log(chr(10) . '---'); + $this->log(sprintf('name: %s' , $storage->getName())); + $this->log(sprintf('uid: %s', $storage->getUid())); + $configuration = CloudinaryApiUtility::getArrayConfiguration($storage); + $this->log(sprintf('%s', var_export($configuration, true))); + } + + + return Command::SUCCESS; + } + + /** + * We retrieve all cloudinary storages + * + * @return array> + */ + protected function getCloudinaryRecords(): array + { + $q = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_file_storage'); + + $q->select('*') + ->from('sys_file_storage') + ->where( + $q->expr()->eq('driver', $q->createNamedParameter(CloudinaryDriver::DRIVER_TYPE)) + ); + + return $q->execute()->fetchAllAssociative(); + } + +} diff --git a/Classes/Services/ConfigurationService.php b/Classes/Services/ConfigurationService.php index dd2d8d3..35e0b12 100644 --- a/Classes/Services/ConfigurationService.php +++ b/Classes/Services/ConfigurationService.php @@ -27,7 +27,8 @@ public function __construct(array $configuration) */ public function get(string $key): string { - $value = trim((string)$this->configuration[$key]); + $rawValue = $this->configuration[$key] ?? ''; + $value = trim((string)$rawValue); if (preg_match('/^%\w+\((.*)\)%$/', $value, $matches) || preg_match('/^%(.*)%$/', $value, $matches)) { $value = getenv($matches[1]); diff --git a/Classes/Utility/CloudinaryApiUtility.php b/Classes/Utility/CloudinaryApiUtility.php index 8b39892..f2c00f3 100644 --- a/Classes/Utility/CloudinaryApiUtility.php +++ b/Classes/Utility/CloudinaryApiUtility.php @@ -28,6 +28,11 @@ public static function getCloudinary(ResourceStorage|array $storage): Cloudinary } public static function getConfiguration(ResourceStorage|array $storage): Configuration + { + return Configuration::instance(self::getArrayConfiguration($storage)); + } + + public static function getArrayConfiguration(ResourceStorage|array $storage): array { if (is_array($storage)) { $storageConfiguration = $storage; @@ -50,16 +55,15 @@ public static function getConfiguration(ResourceStorage|array $storage): Configu $storageConfiguration ); - return Configuration::instance([ - 'cloud_name' => $configurationService->get('cloudName'), - 'api_key' => $configurationService->get('apiKey'), - 'api_secret' => $configurationService->get('apiSecret'), - 'timeout' => $configurationService->get('timeout'), - 'secure' => true, - 'private_cdn' => $configurationService->get('cname') !== '', - 'secure_distribution' => $configurationService->get('cname'), - ] - ); - + return [ + 'cloud_name' => $configurationService->get('cloudName'), + 'api_key' => $configurationService->get('apiKey'), + 'api_secret' => $configurationService->get('apiSecret'), + 'timeout' => $configurationService->get('timeout'), + 'secure' => true, + 'private_cdn' => $configurationService->get('cname') !== '', + 'secure_distribution' => $configurationService->get('cname'), + ]; } + } diff --git a/Configuration/Services.yaml b/Configuration/Services.yaml index 3562a7a..e514373 100644 --- a/Configuration/Services.yaml +++ b/Configuration/Services.yaml @@ -55,6 +55,13 @@ services: schedulable: false description: Scan and warm up a cloudinary storage. + Visol\Cloudinary\Command\CloudinaryStorageListCommand: + tags: + - name: 'console.command' + command: 'cloudinary:storage:list' + schedulable: false + description: Will display all available storages configured for cloudinary + Visol\Cloudinary\Command\CloudinaryMetadataCommand: tags: - name: 'console.command' From 70180bef0e8ce6896a06c453e5f9716500b0c8c2 Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Fri, 14 Jul 2023 15:50:17 +0200 Subject: [PATCH 76/99] [TASK] Update phpstan baseline --- phpstan-baseline.neon | 287 ++++-------------------------------------- 1 file changed, 26 insertions(+), 261 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 5306f9e..6d84309 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -80,11 +80,6 @@ parameters: count: 2 path: Classes/Command/AbstractCloudinaryCommand.php - - - message: "#^Constant LF not found\\.$#" - count: 7 - path: Classes/Command/CloudinaryAcceptanceTestCommand.php - - message: "#^Method Visol\\\\Cloudinary\\\\Command\\\\CloudinaryAcceptanceTestCommand\\:\\:tearDown\\(\\) has no return type specified\\.$#" count: 1 @@ -96,42 +91,27 @@ parameters: path: Classes/Command/CloudinaryAcceptanceTestCommand.php - - message: "#^Call to method adminApi\\(\\) on an unknown class Cloudinary\\\\Cloudinary\\.$#" + message: "#^Parameter \\#1 \\$publicId of method Cloudinary\\\\Api\\\\Admin\\\\AdminApi\\:\\:asset\\(\\) expects string, mixed given\\.$#" count: 1 path: Classes/Command/CloudinaryApiCommand.php - - message: "#^Call to method asset\\(\\) on an unknown class Cloudinary\\\\Api\\\\Admin\\\\AdminApi\\.$#" - count: 1 - path: Classes/Command/CloudinaryApiCommand.php - - - - message: "#^Call to method deleteAssets\\(\\) on an unknown class Cloudinary\\\\Api\\\\Admin\\\\AdminApi\\.$#" - count: 1 - path: Classes/Command/CloudinaryApiCommand.php - - - - message: "#^Call to method expression\\(\\) on an unknown class Cloudinary\\\\Api\\\\Search\\\\SearchApi\\.$#" - count: 1 - path: Classes/Command/CloudinaryApiCommand.php - - - - message: "#^Call to method searchApi\\(\\) on an unknown class Cloudinary\\\\Cloudinary\\.$#" + 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: "#^Method Visol\\\\Cloudinary\\\\Command\\\\CloudinaryApiCommand\\:\\:getAdminApi\\(\\) has invalid return type Cloudinary\\\\Api\\\\Admin\\\\AdminApi\\.$#" + message: "#^Parameter \\#1 \\$value of method Cloudinary\\\\Api\\\\Search\\\\SearchApi\\:\\:nextCursor\\(\\) expects string, mixed given\\.$#" count: 1 path: Classes/Command/CloudinaryApiCommand.php - - message: "#^Method Visol\\\\Cloudinary\\\\Command\\\\CloudinaryApiCommand\\:\\:getSearchApi\\(\\) has invalid return type Cloudinary\\\\Api\\\\Search\\\\SearchApi\\.$#" + message: "#^Part \\$expression \\(mixed\\) of encapsed string cannot be cast to string\\.$#" count: 1 path: Classes/Command/CloudinaryApiCommand.php - - message: "#^Parameter \\#1 \\$uid of method TYPO3\\\\CMS\\\\Core\\\\Resource\\\\ResourceFactory\\:\\:getStorageObject\\(\\) expects int\\|null, mixed given\\.$#" + message: "#^Variable \\$response in empty\\(\\) always exists and is not falsy\\.$#" count: 1 path: Classes/Command/CloudinaryApiCommand.php @@ -195,35 +175,30 @@ parameters: count: 1 path: Classes/Command/CloudinaryCopyCommand.php - - - message: "#^Method Doctrine\\\\DBAL\\\\Driver\\\\Connection\\:\\:query\\(\\) invoked with 1 parameter, 0 required\\.$#" - count: 1 - path: Classes/Command/CloudinaryFixJpegCommand.php - - message: "#^Parameter \\#1 \\$uid of method TYPO3\\\\CMS\\\\Core\\\\Resource\\\\ResourceFactory\\:\\:getStorageObject\\(\\) expects int\\|null, mixed given\\.$#" count: 1 - path: Classes/Command/CloudinaryFixJpegCommand.php + path: Classes/Command/CloudinaryFixImageDimensionCommand.php - message: "#^Property Visol\\\\Cloudinary\\\\Command\\\\AbstractCloudinaryCommand\\:\\:\\$isSilent \\(bool\\) does not accept mixed\\.$#" count: 1 - path: Classes/Command/CloudinaryFixJpegCommand.php + path: Classes/Command/CloudinaryFixImageDimensionCommand.php - - message: "#^Call to method explicit\\(\\) on an unknown class Cloudinary\\\\Api\\\\Upload\\\\UploadApi\\.$#" + message: "#^Method Doctrine\\\\DBAL\\\\Driver\\\\Connection\\:\\:query\\(\\) invoked with 1 parameter, 0 required\\.$#" count: 1 - path: Classes/Command/CloudinaryMetadataCommand.php + path: Classes/Command/CloudinaryFixJpegCommand.php - - message: "#^Call to method uploadApi\\(\\) on an unknown class Cloudinary\\\\Cloudinary\\.$#" + message: "#^Parameter \\#1 \\$uid of method TYPO3\\\\CMS\\\\Core\\\\Resource\\\\ResourceFactory\\:\\:getStorageObject\\(\\) expects int\\|null, mixed given\\.$#" count: 1 - path: Classes/Command/CloudinaryMetadataCommand.php + path: Classes/Command/CloudinaryFixJpegCommand.php - - message: "#^Method Visol\\\\Cloudinary\\\\Command\\\\CloudinaryMetadataCommand\\:\\:getUploadApi\\(\\) has invalid return type Cloudinary\\\\Api\\\\Upload\\\\UploadApi\\.$#" + message: "#^Property Visol\\\\Cloudinary\\\\Command\\\\AbstractCloudinaryCommand\\:\\:\\$isSilent \\(bool\\) does not accept mixed\\.$#" count: 1 - path: Classes/Command/CloudinaryMetadataCommand.php + path: Classes/Command/CloudinaryFixJpegCommand.php - message: "#^Parameter \\#1 \\$fileIdentifier of method Visol\\\\Cloudinary\\\\Services\\\\CloudinaryPathService\\:\\:computeCloudinaryPublicId\\(\\) expects string, mixed given\\.$#" @@ -335,16 +310,6 @@ parameters: count: 2 path: Classes/Controller/CloudinaryAjaxController.php - - - message: "#^Call to method expression\\(\\) on an unknown class Cloudinary\\\\Api\\\\Search\\\\SearchApi\\.$#" - count: 1 - path: Classes/Controller/CloudinaryAjaxController.php - - - - message: "#^Call to method searchApi\\(\\) on an unknown class Cloudinary\\\\Cloudinary\\.$#" - count: 1 - path: Classes/Controller/CloudinaryAjaxController.php - - message: "#^Cannot access offset 'cloudinaryIds' on array\\|object\\|null\\.$#" count: 1 @@ -356,7 +321,7 @@ parameters: path: Classes/Controller/CloudinaryAjaxController.php - - message: "#^Method Visol\\\\Cloudinary\\\\Controller\\\\CloudinaryAjaxController\\:\\:getSearchApi\\(\\) has invalid return type Cloudinary\\\\Api\\\\Search\\\\SearchApi\\.$#" + message: "#^Cannot access offset 0 on mixed\\.$#" count: 1 path: Classes/Controller/CloudinaryAjaxController.php @@ -370,21 +335,6 @@ parameters: count: 1 path: Classes/Controller/CloudinaryScanController.php - - - message: "#^Call to method explicit\\(\\) on an unknown class Cloudinary\\\\Api\\\\Upload\\\\UploadApi\\.$#" - count: 1 - path: Classes/Controller/CloudinaryWebHookController.php - - - - message: "#^Call to method uploadApi\\(\\) on an unknown class Cloudinary\\\\Cloudinary\\.$#" - count: 1 - path: Classes/Controller/CloudinaryWebHookController.php - - - - message: "#^Method Visol\\\\Cloudinary\\\\Controller\\\\CloudinaryWebHookController\\:\\:getUploadApi\\(\\) has invalid return type Cloudinary\\\\Api\\\\Upload\\\\UploadApi\\.$#" - count: 1 - path: Classes/Controller/CloudinaryWebHookController.php - - message: "#^PHPDoc tag @return with type object is not subtype of native type TYPO3\\\\CMS\\\\Core\\\\Database\\\\Connection\\.$#" count: 1 @@ -416,45 +366,10 @@ parameters: path: Classes/Domain/Repository/ExplicitDataCacheRepository.php - - message: "#^Call to method adminApi\\(\\) on an unknown class Cloudinary\\\\Cloudinary\\.$#" - count: 1 - path: Classes/Driver/CloudinaryDriver.php - - - - message: "#^Call to method createFolder\\(\\) on an unknown class Cloudinary\\\\Api\\\\Admin\\\\AdminApi\\.$#" - count: 1 - path: Classes/Driver/CloudinaryDriver.php - - - - message: "#^Call to method deleteAssets\\(\\) on an unknown class Cloudinary\\\\Api\\\\Admin\\\\AdminApi\\.$#" - count: 1 - path: Classes/Driver/CloudinaryDriver.php - - - - message: "#^Call to method deleteAssetsByPrefix\\(\\) on an unknown class Cloudinary\\\\Api\\\\Admin\\\\AdminApi\\.$#" - count: 1 - path: Classes/Driver/CloudinaryDriver.php - - - - message: "#^Call to method deleteFolder\\(\\) on an unknown class Cloudinary\\\\Api\\\\Admin\\\\AdminApi\\.$#" - count: 1 - path: Classes/Driver/CloudinaryDriver.php - - - - message: "#^Call to method rename\\(\\) on an unknown class Cloudinary\\\\Api\\\\Upload\\\\UploadApi\\.$#" - count: 1 - path: Classes/Driver/CloudinaryDriver.php - - - - message: "#^Call to method upload\\(\\) on an unknown class Cloudinary\\\\Api\\\\Upload\\\\UploadApi\\.$#" + message: "#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#" count: 3 path: Classes/Driver/CloudinaryDriver.php - - - message: "#^Call to method uploadApi\\(\\) on an unknown class Cloudinary\\\\Cloudinary\\.$#" - count: 1 - path: Classes/Driver/CloudinaryDriver.php - - message: "#^Cannot access offset 0 on callable\\(\\)\\: mixed\\.$#" count: 1 @@ -470,44 +385,39 @@ parameters: count: 2 path: Classes/Driver/CloudinaryDriver.php - - - message: "#^Method Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:getAdminApi\\(\\) has invalid return type Cloudinary\\\\Api\\\\Admin\\\\AdminApi\\.$#" - count: 1 - path: Classes/Driver/CloudinaryDriver.php - - message: "#^Method Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:getFileContents\\(\\) should return string but returns string\\|false\\.$#" count: 1 path: Classes/Driver/CloudinaryDriver.php - - message: "#^Method Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:getUploadApi\\(\\) has invalid return type Cloudinary\\\\Api\\\\Upload\\\\UploadApi\\.$#" + message: "#^Method Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:isFolderEmpty\\(\\) should return bool but returns int\\.$#" count: 1 path: Classes/Driver/CloudinaryDriver.php - - message: "#^Method Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:isFolderEmpty\\(\\) should return bool but returns int\\.$#" + message: "#^Method Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:sanitizeFileName\\(\\) should return string but returns string\\|null\\.$#" count: 1 path: Classes/Driver/CloudinaryDriver.php - - message: "#^Method Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:sanitizeFileName\\(\\) should return string but returns string\\|null\\.$#" + message: "#^Offset 'filename' does not exist on array\\\\|string\\.$#" count: 1 path: Classes/Driver/CloudinaryDriver.php - - message: "#^Offset 'extension' does not exist on array\\\\|string\\.$#" + message: "#^Offset 'type' does not exist on array\\{\\}\\.$#" count: 1 path: Classes/Driver/CloudinaryDriver.php - - message: "#^Offset 'filename' does not exist on array\\\\|string\\.$#" + message: "#^Parameter \\#1 \\$folder of method Visol\\\\Cloudinary\\\\Services\\\\CloudinaryFolderService\\:\\:delete\\(\\) expects string, mixed given\\.$#" count: 1 path: Classes/Driver/CloudinaryDriver.php - - message: "#^Offset 'type' does not exist on array\\{\\}\\.$#" - count: 1 + message: "#^Parameter \\#1 \\$publicId of method Visol\\\\Cloudinary\\\\Services\\\\CloudinaryResourceService\\:\\:delete\\(\\) expects string, mixed given\\.$#" + count: 2 path: Classes/Driver/CloudinaryDriver.php - @@ -540,21 +450,6 @@ parameters: count: 2 path: Classes/Services/AbstractCloudinaryMediaService.php - - - message: "#^Call to method explicit\\(\\) on an unknown class Cloudinary\\\\Api\\\\Upload\\\\UploadApi\\.$#" - count: 1 - path: Classes/Services/AbstractCloudinaryMediaService.php - - - - message: "#^Call to method uploadApi\\(\\) on an unknown class Cloudinary\\\\Cloudinary\\.$#" - count: 1 - path: Classes/Services/AbstractCloudinaryMediaService.php - - - - message: "#^Method Visol\\\\Cloudinary\\\\Services\\\\AbstractCloudinaryMediaService\\:\\:getUploadApi\\(\\) has invalid return type Cloudinary\\\\Api\\\\Upload\\\\UploadApi\\.$#" - count: 1 - path: Classes/Services/AbstractCloudinaryMediaService.php - - message: "#^Call to an undefined method Doctrine\\\\DBAL\\\\Driver\\\\Connection\\:\\:delete\\(\\)\\.$#" count: 2 @@ -575,21 +470,6 @@ parameters: count: 2 path: Classes/Services/CloudinaryFolderService.php - - - message: "#^Call to method explicit\\(\\) on an unknown class Cloudinary\\\\Api\\\\Upload\\\\UploadApi\\.$#" - count: 1 - path: Classes/Services/CloudinaryImageService.php - - - - message: "#^Call to static method fromParams\\(\\) on an unknown class Cloudinary\\\\Asset\\\\Image\\.$#" - count: 1 - path: Classes/Services/CloudinaryImageService.php - - - - message: "#^Call to static method fromParams\\(\\) on an unknown class Cloudinary\\\\Transformation\\\\ImageTransformation\\.$#" - count: 1 - path: Classes/Services/CloudinaryImageService.php - - message: "#^Cannot access offset 'width' on mixed\\.$#" count: 2 @@ -670,56 +550,21 @@ parameters: count: 2 path: Classes/Services/CloudinaryResourceService.php - - - message: "#^Call to method adminApi\\(\\) on an unknown class Cloudinary\\\\Cloudinary\\.$#" - count: 1 - path: Classes/Services/CloudinaryScanService.php - - - - message: "#^Call to method asset\\(\\) on an unknown class Cloudinary\\\\Api\\\\Admin\\\\AdminApi\\.$#" - count: 1 - path: Classes/Services/CloudinaryScanService.php - - - - message: "#^Call to method expression\\(\\) on an unknown class Cloudinary\\\\Api\\\\Search\\\\SearchApi\\.$#" - count: 1 - path: Classes/Services/CloudinaryScanService.php - - - - message: "#^Call to method searchApi\\(\\) on an unknown class Cloudinary\\\\Cloudinary\\.$#" - count: 1 - path: Classes/Services/CloudinaryScanService.php - - message: "#^Method Visol\\\\Cloudinary\\\\Services\\\\CloudinaryScanService\\:\\:console\\(\\) has parameter \\$additionalBlankLine with no type specified\\.$#" count: 1 path: Classes/Services/CloudinaryScanService.php - - message: "#^Method Visol\\\\Cloudinary\\\\Services\\\\CloudinaryScanService\\:\\:getAdminApi\\(\\) has invalid return type Cloudinary\\\\Api\\\\Admin\\\\AdminApi\\.$#" + message: "#^Parameter \\#1 \\$value of method Cloudinary\\\\Api\\\\Search\\\\SearchApi\\:\\:nextCursor\\(\\) expects string, mixed given\\.$#" count: 1 path: Classes/Services/CloudinaryScanService.php - - message: "#^Method Visol\\\\Cloudinary\\\\Services\\\\CloudinaryScanService\\:\\:getSearchApi\\(\\) has invalid return type Cloudinary\\\\Api\\\\Search\\\\SearchApi\\.$#" + message: "#^Variable \\$response in empty\\(\\) always exists and is not falsy\\.$#" count: 1 path: Classes/Services/CloudinaryScanService.php - - - message: "#^Call to method expression\\(\\) on an unknown class Cloudinary\\\\Api\\\\Search\\\\SearchApi\\.$#" - count: 1 - path: Classes/Services/CloudinaryTestConnectionService.php - - - - message: "#^Call to method searchApi\\(\\) on an unknown class Cloudinary\\\\Cloudinary\\.$#" - count: 1 - path: Classes/Services/CloudinaryTestConnectionService.php - - - - message: "#^Method Visol\\\\Cloudinary\\\\Services\\\\CloudinaryTestConnectionService\\:\\:getSearchApi\\(\\) has invalid return type Cloudinary\\\\Api\\\\Search\\\\SearchApi\\.$#" - count: 1 - path: Classes/Services/CloudinaryTestConnectionService.php - - message: "#^Method Visol\\\\Cloudinary\\\\Services\\\\CloudinaryTestConnectionService\\:\\:test\\(\\) has no return type specified\\.$#" count: 1 @@ -730,110 +575,30 @@ parameters: count: 1 path: Classes/Services/CloudinaryUploadService.php - - - message: "#^Call to static method fromParams\\(\\) on an unknown class Cloudinary\\\\Asset\\\\Video\\.$#" - count: 1 - path: Classes/Services/CloudinaryVideoService.php - - - - message: "#^Property Visol\\\\Cloudinary\\\\Services\\\\CloudinaryVideoService\\:\\:\\$defaultOptions has no type specified\\.$#" - count: 1 - path: Classes/Services/CloudinaryVideoService.php - - - - message: "#^Call to method adminApi\\(\\) on an unknown class Cloudinary\\\\Cloudinary\\.$#" - count: 1 - path: Classes/Services/Extractor/CloudinaryMetaDataExtractor.php - - - - message: "#^Call to method asset\\(\\) on an unknown class Cloudinary\\\\Api\\\\Admin\\\\AdminApi\\.$#" - count: 1 - path: Classes/Services/Extractor/CloudinaryMetaDataExtractor.php - - - - message: "#^Method Visol\\\\Cloudinary\\\\Services\\\\Extractor\\\\CloudinaryMetaDataExtractor\\:\\:getAdminApi\\(\\) has invalid return type Cloudinary\\\\Api\\\\Admin\\\\AdminApi\\.$#" - 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 method adminApi\\(\\) on an unknown class Cloudinary\\\\Cloudinary\\.$#" - count: 1 - path: Classes/Services/FileMoveService.php - - - - message: "#^Call to method asset\\(\\) on an unknown class Cloudinary\\\\Api\\\\Admin\\\\AdminApi\\.$#" - count: 1 - path: Classes/Services/FileMoveService.php - - - - message: "#^Call to method upload\\(\\) on an unknown class Cloudinary\\\\Api\\\\Upload\\\\UploadApi\\.$#" - count: 1 - path: Classes/Services/FileMoveService.php - - - - message: "#^Call to method uploadApi\\(\\) on an unknown class Cloudinary\\\\Cloudinary\\.$#" - count: 1 - path: Classes/Services/FileMoveService.php - - 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\\:\\:getAdminApi\\(\\) has invalid return type Cloudinary\\\\Api\\\\Admin\\\\AdminApi\\.$#" - count: 1 - path: Classes/Services/FileMoveService.php - - 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 - - - message: "#^Method Visol\\\\Cloudinary\\\\Services\\\\FileMoveService\\:\\:getUploadApi\\(\\) has invalid return type Cloudinary\\\\Api\\\\Upload\\\\UploadApi\\.$#" - count: 1 - path: Classes/Services/FileMoveService.php - - message: "#^Method Visol\\\\Cloudinary\\\\Services\\\\FileMoveService\\:\\:initializeCloudinaryService\\(\\) has no return type specified\\.$#" count: 1 path: Classes/Services/FileMoveService.php - - message: "#^Call to static method instance\\(\\) on an unknown class Cloudinary\\\\Configuration\\\\Configuration\\.$#" - count: 1 - path: Classes/Utility/CloudinaryApiUtility.php - - - - message: "#^Cannot call method getConfiguration\\(\\) on array\\|TYPO3\\\\CMS\\\\Core\\\\Resource\\\\ResourceStorage\\.$#" + message: "#^Variable \\$resource in empty\\(\\) always exists and is not falsy\\.$#" count: 1 - path: Classes/Utility/CloudinaryApiUtility.php - - - - message: "#^Cannot call method getDriverType\\(\\) on array\\|TYPO3\\\\CMS\\\\Core\\\\Resource\\\\ResourceStorage\\.$#" - count: 2 - path: Classes/Utility/CloudinaryApiUtility.php - - - - message: "#^Instantiated class Cloudinary\\\\Cloudinary not found\\.$#" - count: 1 - path: Classes/Utility/CloudinaryApiUtility.php - - - - message: "#^Method Visol\\\\Cloudinary\\\\Utility\\\\CloudinaryApiUtility\\:\\:getCloudinary\\(\\) has invalid return type Cloudinary\\\\Cloudinary\\.$#" - count: 1 - path: Classes/Utility/CloudinaryApiUtility.php - - - - message: "#^Method Visol\\\\Cloudinary\\\\Utility\\\\CloudinaryApiUtility\\:\\:getConfiguration\\(\\) has invalid return type Cloudinary\\\\Configuration\\\\Configuration\\.$#" - count: 1 - path: Classes/Utility/CloudinaryApiUtility.php + path: Classes/Services/FileMoveService.php - message: "#^Method Visol\\\\Cloudinary\\\\Utility\\\\CloudinaryFileUtility\\:\\:getTemporaryFile\\(\\) has parameter \\$storageUid with no type specified\\.$#" From 30604ebe6593cd8cfb458668ec9dd92edb8cb64b Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Fri, 14 Jul 2023 17:36:36 +0200 Subject: [PATCH 77/99] [FEATURE] Improve storage list --- Classes/Command/CloudinaryStorageListCommand.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Classes/Command/CloudinaryStorageListCommand.php b/Classes/Command/CloudinaryStorageListCommand.php index 22fa400..efc0acd 100644 --- a/Classes/Command/CloudinaryStorageListCommand.php +++ b/Classes/Command/CloudinaryStorageListCommand.php @@ -62,11 +62,19 @@ protected function execute(InputInterface $input, OutputInterface $output): int $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class); $storage = $resourceFactory->getStorageObject($cloudinaryRecord['uid']); - $this->log(chr(10) . '---'); + $this->log('---'); $this->log(sprintf('name: %s' , $storage->getName())); $this->log(sprintf('uid: %s', $storage->getUid())); $configuration = CloudinaryApiUtility::getArrayConfiguration($storage); - $this->log(sprintf('%s', var_export($configuration, true))); + foreach ($configuration as $key => $value) { + if (is_bool($value)) { + $value = $value ? 'true' : 'false'; + } + if ($key === 'secure_distribution') { + $key .= ' (cname)'; + } + $this->log(sprintf('%s: %s', $key, $value)); + } } From daa48f2a66b572ee9c2f2529862f36b449f54744 Mon Sep 17 00:00:00 2001 From: Fabien Udriot Date: Fri, 14 Jul 2023 17:44:27 +0200 Subject: [PATCH 78/99] [REFACTOR] Fix order of log statements in foreach loop --- Classes/Command/CloudinaryStorageListCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/Command/CloudinaryStorageListCommand.php b/Classes/Command/CloudinaryStorageListCommand.php index efc0acd..2ba9b58 100644 --- a/Classes/Command/CloudinaryStorageListCommand.php +++ b/Classes/Command/CloudinaryStorageListCommand.php @@ -63,8 +63,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int $storage = $resourceFactory->getStorageObject($cloudinaryRecord['uid']); $this->log('---'); - $this->log(sprintf('name: %s' , $storage->getName())); $this->log(sprintf('uid: %s', $storage->getUid())); + $this->log(sprintf('name: %s' , $storage->getName())); $configuration = CloudinaryApiUtility::getArrayConfiguration($storage); foreach ($configuration as $key => $value) { if (is_bool($value)) { From 81deb5bc03887e7c6af6dced39529433f9ee53fb Mon Sep 17 00:00:00 2001 From: Daniel Huf Date: Mon, 11 Sep 2023 12:14:57 +0200 Subject: [PATCH 79/99] [REFACTOR] Code cleanup --- Classes/CloudinaryException.php | 4 +++- Classes/CloudinaryFactory.php | 3 ++- Classes/Command/CloudinaryApiCommand.php | 3 ++- Classes/Controller/CloudinaryAjaxController.php | 5 +++-- .../Controller/CloudinaryWebHookController.php | 3 ++- Classes/Driver/CloudinaryDriver.php | 10 ++++++---- Classes/Services/CloudinaryImageService.php | 3 ++- Classes/Services/CloudinaryPathService.php | 5 +++-- .../Services/CloudinaryTestConnectionService.php | 3 ++- Classes/Services/ConfigurationService.php | 4 +++- Classes/Services/FileMoveService.php | 3 ++- Classes/Utility/CloudinaryApiUtility.php | 3 ++- .../CloudinaryImageDataViewHelper.php | 13 +++++++++---- .../ViewHelpers/CloudinaryImageViewHelper.php | 13 +++++++++---- Configuration/Backend/AjaxRoutes.php | 4 +++- .../AbstractCloudinaryFileOperationTest.php | 16 +++++++++------- .../FileOperation/AddFileOperationTest.php | 3 ++- .../FileOperation/CopyFileOperationTest.php | 4 +++- .../FileOperation/CopyFolderOperationTests.php | 4 +++- .../CountFilesInFolderOperationTests.php | 4 +++- .../FileOperation/CreateFolderOperationTests.php | 4 +++- .../FileOperation/DeleteFileOperationTest.php | 4 +++- .../FileOperation/DeleteFolderOperationTest.php | 4 +++- .../FileOperation/MoveFileOperationTest.php | 4 +++- .../FileOperation/RenameFolderOperationTests.php | 4 +++- ext_emconf.php | 2 +- ext_localconf.php | 15 +++++++++------ 27 files changed, 98 insertions(+), 49 deletions(-) diff --git a/Classes/CloudinaryException.php b/Classes/CloudinaryException.php index 6acc908..f91291b 100644 --- a/Classes/CloudinaryException.php +++ b/Classes/CloudinaryException.php @@ -2,6 +2,8 @@ namespace Visol\Cloudinary; -class CloudinaryException extends \Exception +use Exception; + +class CloudinaryException extends Exception { } diff --git a/Classes/CloudinaryFactory.php b/Classes/CloudinaryFactory.php index c84992d..2b8f0d7 100644 --- a/Classes/CloudinaryFactory.php +++ b/Classes/CloudinaryFactory.php @@ -9,6 +9,7 @@ namespace Visol\Cloudinary; +use Exception; use TYPO3\CMS\Core\Configuration\ExtensionConfiguration; use TYPO3\CMS\Core\Resource\Folder; use TYPO3\CMS\Core\Resource\ResourceFactory; @@ -18,7 +19,7 @@ /** * Class CloudinaryFactory */ -class CloudinaryFactory extends \Exception +class CloudinaryFactory extends Exception { /** * @return ResourceStorage diff --git a/Classes/Command/CloudinaryApiCommand.php b/Classes/Command/CloudinaryApiCommand.php index 5d82b0e..d2c2860 100644 --- a/Classes/Command/CloudinaryApiCommand.php +++ b/Classes/Command/CloudinaryApiCommand.php @@ -11,6 +11,7 @@ use Cloudinary\Api\Admin\AdminApi; use Cloudinary\Api\Search\SearchApi; +use Exception; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -154,7 +155,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } else { $this->log('Nothing to do...'); } - } catch (\Exception $exception) { + } catch (Exception $exception) { // Triggered when no resources are found when deleting files. if ($exception->getMessage() === 'Missing required parameter - public_ids') { $this->log("No resources found for expression '$expression'. Nothing to do..."); diff --git a/Classes/Controller/CloudinaryAjaxController.php b/Classes/Controller/CloudinaryAjaxController.php index 2381309..0d94afb 100644 --- a/Classes/Controller/CloudinaryAjaxController.php +++ b/Classes/Controller/CloudinaryAjaxController.php @@ -5,6 +5,7 @@ use Cloudinary\Api\Search\SearchApi; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; +use RuntimeException; use TYPO3\CMS\Core\Http\JsonResponse; use TYPO3\CMS\Core\Resource\ResourceFactory; use TYPO3\CMS\Core\Resource\ResourceStorage; @@ -44,7 +45,7 @@ public function addFilesAction(ServerRequestInterface $request): ResponseInterfa ->execute(); if (empty($response['resources'])) { - throw new \RuntimeException('Missing resources ' . $publicId, 1657125439); + throw new RuntimeException('Missing resources ' . $publicId, 1657125439); } else { $resource = $response['resources'][0]; } @@ -62,7 +63,7 @@ public function addFilesAction(ServerRequestInterface $request): ResponseInterfa $files[] = $storage->getFile($identifier)->getUid(); } } - } catch (\RuntimeException $e) { + } catch (RuntimeException $e) { $result = 'ko'; $possibleError = $e->getMessage(); } diff --git a/Classes/Controller/CloudinaryWebHookController.php b/Classes/Controller/CloudinaryWebHookController.php index 47b6ed0..2481df7 100644 --- a/Classes/Controller/CloudinaryWebHookController.php +++ b/Classes/Controller/CloudinaryWebHookController.php @@ -12,6 +12,7 @@ use Cloudinary\Api\Upload\UploadApi; use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Http\Message\ResponseInterface; +use RuntimeException; use TYPO3\CMS\Core\Cache\CacheManager; use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Database\Query\QueryBuilder; @@ -384,7 +385,7 @@ 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); + throw new RuntimeException('Check your configuration while calling the cloudinary web hook. I am missing a storage id', 1677583654); } } diff --git a/Classes/Driver/CloudinaryDriver.php b/Classes/Driver/CloudinaryDriver.php index f775b15..c382da7 100644 --- a/Classes/Driver/CloudinaryDriver.php +++ b/Classes/Driver/CloudinaryDriver.php @@ -11,6 +11,7 @@ use Cloudinary\Api\Admin\AdminApi; use Cloudinary\Api\Upload\UploadApi; +use Exception; use TYPO3\CMS\Core\Http\ApplicationType; use TYPO3\CMS\Core\Resource\Exception\InvalidFileNameException; use RuntimeException; @@ -33,6 +34,7 @@ use Visol\Cloudinary\Utility\CloudinaryApiUtility; use Visol\Cloudinary\Utility\CloudinaryFileUtility; use Visol\Cloudinary\Utility\MimeTypeUtility; +use function str_starts_with; class CloudinaryDriver extends AbstractHierarchicalFilesystemDriver { @@ -156,7 +158,7 @@ public function getFileInfoByIdentifier($fileIdentifier, array $propertiesToExtr $cloudinaryResource = $this->getCloudinaryResourceService()->getResource($publicId); // We have a problem Hudson! if (!$cloudinaryResource) { - throw new \Exception( + throw new Exception( 'I could not find a corresponding cloudinary resource for file ' . $fileIdentifier, 1591775048, ); @@ -477,7 +479,7 @@ public function createFolder($newFolderName, $parentFolderIdentifier = '', $recu $response = $this->getAdminApi()->createFolder($cloudinaryFolder); if (!$response['success']) { - throw new \Exception('Folder creation failed: ' . $cloudinaryFolder, 1591775050); + throw new Exception('Folder creation failed: ' . $cloudinaryFolder, 1591775050); } $this->getCloudinaryFolderService()->save($cloudinaryFolder); @@ -682,7 +684,7 @@ public function isWithin($folderIdentifier, $identifier): bool $folderIdentifier .= DIRECTORY_SEPARATOR; } - return \str_starts_with($fileIdentifier, $folderIdentifier); + return str_starts_with($fileIdentifier, $folderIdentifier); } /** @@ -987,7 +989,7 @@ protected function applyFilterMethodsToDirectoryItem( return false; } if ($result === false) { - throw new \RuntimeException( + throw new RuntimeException( 'Could not apply file/folder name filter ' . $filter[0] . '::' . $filter[1], 1596795500, ); diff --git a/Classes/Services/CloudinaryImageService.php b/Classes/Services/CloudinaryImageService.php index 7dad8cd..76239b4 100644 --- a/Classes/Services/CloudinaryImageService.php +++ b/Classes/Services/CloudinaryImageService.php @@ -11,6 +11,7 @@ use Cloudinary\Asset\Image; use Cloudinary\Transformation\ImageTransformation; +use Exception; use TYPO3\CMS\Core\Resource\StorageRepository; use TYPO3\CMS\Core\Resource\File; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -55,7 +56,7 @@ public function getExplicitData(File $file, array $options): array try { $explicitData = (array)$this->getUploadApi($file->getStorage())->explicit($publicId, $apiOptions); $this->explicitDataCacheRepository->save($file->getStorage()->getUid(), $publicId, $options, $explicitData); - } catch (\Exception $e) { + } catch (Exception $e) { $explicitData = []; // ignore } diff --git a/Classes/Services/CloudinaryPathService.php b/Classes/Services/CloudinaryPathService.php index ce45ddc..595ff2c 100644 --- a/Classes/Services/CloudinaryPathService.php +++ b/Classes/Services/CloudinaryPathService.php @@ -9,6 +9,7 @@ * LICENSE.md file that was distributed with this source code. */ +use RuntimeException; use TYPO3\CMS\Core\Resource\ResourceStorage; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\Utility\PathUtility; @@ -99,7 +100,7 @@ public function getResourceType(string $fileIdentifier): string try { // Find the resource type from the cloudinary resource. $cloudinaryResource = $this->getCloudinaryResource($fileIdentifier); - } catch (\RuntimeException $e) { + } catch (RuntimeException $e) { $fileExtension = $this->getFileExtension($fileIdentifier); $mimeType = MimeTypeUtility::guessMimeType($fileExtension); @@ -140,7 +141,7 @@ protected function getCloudinaryResource(string $fileIdentifier): array // 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); + throw new RuntimeException('Cloudinary resource not found for ' . $fileIdentifier, 1623157880); } $this->cachedCloudinaryResources[$possiblePublicId] = $cloudinaryResource; diff --git a/Classes/Services/CloudinaryTestConnectionService.php b/Classes/Services/CloudinaryTestConnectionService.php index f5a9b9e..d696274 100644 --- a/Classes/Services/CloudinaryTestConnectionService.php +++ b/Classes/Services/CloudinaryTestConnectionService.php @@ -10,6 +10,7 @@ */ use Cloudinary\Api\Search\SearchApi; +use Exception; use TYPO3\CMS\Core\Messaging\FlashMessageQueue; use TYPO3\CMS\Core\Messaging\FlashMessage; use TYPO3\CMS\Core\Messaging\FlashMessageService; @@ -63,7 +64,7 @@ public function test() FlashMessage::OK ); $messageQueue->addMessage($message); - } catch (\Exception $exception) { + } catch (Exception $exception) { /** @var FlashMessage $message */ $message = GeneralUtility::makeInstance( FlashMessage::class, diff --git a/Classes/Services/ConfigurationService.php b/Classes/Services/ConfigurationService.php index 35e0b12..6026614 100644 --- a/Classes/Services/ConfigurationService.php +++ b/Classes/Services/ConfigurationService.php @@ -10,6 +10,8 @@ */ +use RuntimeException; + class ConfigurationService { protected array $configuration = []; @@ -33,7 +35,7 @@ public function get(string $key): string $value = getenv($matches[1]); if ($value === false) { - throw new \RuntimeException(sprintf('No value found for environment variable "%s"', $matches[1]), 1626948978); + throw new RuntimeException(sprintf('No value found for environment variable "%s"', $matches[1]), 1626948978); } $value = (string)$value; diff --git a/Classes/Services/FileMoveService.php b/Classes/Services/FileMoveService.php index 705a96a..0302915 100644 --- a/Classes/Services/FileMoveService.php +++ b/Classes/Services/FileMoveService.php @@ -12,6 +12,7 @@ use Cloudinary\Api\Admin\AdminApi; use Cloudinary\Api\Upload\UploadApi; use Doctrine\DBAL\Driver\Connection; +use Exception; use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Database\Query\QueryBuilder; use TYPO3\CMS\Core\Resource\File; @@ -37,7 +38,7 @@ public function fileExists(File $fileObject, ResourceStorage $targetStorage): bo try { $resource = $this->getAdminApi($targetStorage)->asset($publicId); $fileExists = !empty($resource); - } catch (\Exception $exception) { + } catch (Exception $exception) { $fileExists = false; } diff --git a/Classes/Utility/CloudinaryApiUtility.php b/Classes/Utility/CloudinaryApiUtility.php index f2c00f3..db67c09 100644 --- a/Classes/Utility/CloudinaryApiUtility.php +++ b/Classes/Utility/CloudinaryApiUtility.php @@ -11,6 +11,7 @@ use Cloudinary\Cloudinary; use Cloudinary\Configuration\Configuration; +use Exception; use TYPO3\CMS\Core\Resource\ResourceStorage; use TYPO3\CMS\Core\Utility\GeneralUtility; use Visol\Cloudinary\Driver\CloudinaryDriver; @@ -44,7 +45,7 @@ public static function getArrayConfiguration(ResourceStorage|array $storage): ar 'Wrong storage! Can not initialize with storage type "%s".', $storage->getDriverType() ); - throw new \Exception($message, 1590401459); + throw new Exception($message, 1590401459); } $storageConfiguration = $storage->getConfiguration(); } diff --git a/Classes/ViewHelpers/CloudinaryImageDataViewHelper.php b/Classes/ViewHelpers/CloudinaryImageDataViewHelper.php index 51146d4..16bf77d 100644 --- a/Classes/ViewHelpers/CloudinaryImageDataViewHelper.php +++ b/Classes/ViewHelpers/CloudinaryImageDataViewHelper.php @@ -8,10 +8,15 @@ * For the full copyright and license information, please read the * LICENSE.md file that was distributed with this source code. */ + +use Exception; +use InvalidArgumentException; +use RuntimeException; use TYPO3\CMS\Extbase\Service\ImageService; use TYPO3\CMS\Core\Resource\ResourceStorage; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper; +use UnexpectedValueException; use Visol\Cloudinary\Services\CloudinaryImageService; use Visol\Cloudinary\Services\CloudinaryPathService; use TYPO3\CMS\Core\Resource\Exception\ResourceDoesNotExistException; @@ -134,7 +139,7 @@ public function render(): string 'medianImage' => $this->getCloudinaryImageService()->getImage($breakpoints, 'median'), 'maxImage' => $this->getCloudinaryImageService()->getImage($breakpoints, 'max'), ]; - } catch (\Exception $e) { + } catch (Exception $e) { $responsiveImageData = [ 'images' => [ 1 => [ @@ -151,11 +156,11 @@ public function render(): string } } catch (ResourceDoesNotExistException $e) { // thrown if file does not exist - } catch (\UnexpectedValueException $e) { + } catch (UnexpectedValueException $e) { // thrown if a file has been replaced with a folder - } catch (\RuntimeException $e) { + } catch (RuntimeException $e) { // RuntimeException thrown if a file is outside of a storage - } catch (\InvalidArgumentException $e) { + } catch (InvalidArgumentException $e) { // thrown if file storage does not exist } diff --git a/Classes/ViewHelpers/CloudinaryImageViewHelper.php b/Classes/ViewHelpers/CloudinaryImageViewHelper.php index 9db76b5..7c507e8 100644 --- a/Classes/ViewHelpers/CloudinaryImageViewHelper.php +++ b/Classes/ViewHelpers/CloudinaryImageViewHelper.php @@ -8,9 +8,14 @@ * For the full copyright and license information, please read the * LICENSE.md file that was distributed with this source code. */ + +use Exception; +use InvalidArgumentException; +use RuntimeException; use TYPO3\CMS\Extbase\Service\ImageService; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper; +use UnexpectedValueException; use Visol\Cloudinary\Services\CloudinaryImageService; use TYPO3\CMS\Core\Resource\Exception\ResourceDoesNotExistException; use TYPO3\CMS\Core\Resource\FileInterface; @@ -120,7 +125,7 @@ public function render(): string $this->tag->addAttribute('sizes', $cloudinarySizes); $this->tag->addAttribute('srcset', $cloudinarySrcset); $this->tag->addAttribute('src', $image->getPublicUrl()); - } catch (\Exception $e) { + } catch (Exception $e) { $this->tag->addAttribute('src', $image->getPublicUrl()); } @@ -134,11 +139,11 @@ public function render(): string } } catch (ResourceDoesNotExistException $e) { // thrown if file does not exist - } catch (\UnexpectedValueException $e) { + } catch (UnexpectedValueException $e) { // thrown if a file has been replaced with a folder - } catch (\RuntimeException $e) { + } catch (RuntimeException $e) { // RuntimeException thrown if a file is outside of a storage - } catch (\InvalidArgumentException $e) { + } catch (InvalidArgumentException $e) { // thrown if file storage does not exist } diff --git a/Configuration/Backend/AjaxRoutes.php b/Configuration/Backend/AjaxRoutes.php index 9413ccc..2fb09ab 100644 --- a/Configuration/Backend/AjaxRoutes.php +++ b/Configuration/Backend/AjaxRoutes.php @@ -1,8 +1,10 @@ [ 'path' => '/cloudinary/add-files', - 'target' => \Visol\Cloudinary\Controller\CloudinaryAjaxController::class . '::addFilesAction', + 'target' => CloudinaryAjaxController::class . '::addFilesAction', ], ]; diff --git a/Tests/Acceptance/FileOperation/AbstractCloudinaryFileOperationTest.php b/Tests/Acceptance/FileOperation/AbstractCloudinaryFileOperationTest.php index 33f0841..5b88667 100644 --- a/Tests/Acceptance/FileOperation/AbstractCloudinaryFileOperationTest.php +++ b/Tests/Acceptance/FileOperation/AbstractCloudinaryFileOperationTest.php @@ -2,6 +2,8 @@ namespace Visol\Cloudinary\Tests\Acceptance\FileOperation; +use Exception; +use RuntimeException; use Symfony\Component\Console\Style\SymfonyStyle; use TYPO3\CMS\Core\Resource\Folder; use TYPO3\CMS\Core\Resource\ResourceStorage; @@ -135,7 +137,7 @@ protected function getFilePath(string $fileName): string $filePath = realpath($this->fixtureDirectory . DIRECTORY_SEPARATOR . $fileName); if (!$filePath) { - throw new \RuntimeException('Missing file ' . $fileName, 1591703650); + throw new RuntimeException('Missing file ' . $fileName, 1591703650); } return $filePath; } @@ -210,14 +212,14 @@ protected function getFolder($folderIdentifier): Folder * @param bool $expression * @param string $message * - * @throws \Exception + * @throws Exception */ protected function assertTrue(bool $expression, string $message) { if ($expression !== true) { $message .= chr(10) . chr(10) . 'Expected value: true'; $message .= chr(10) . 'Actual value: ' . var_export($expression, true); - throw new \Exception('AssertTrue! ' . $message, 1590757845); + throw new Exception('AssertTrue! ' . $message, 1590757845); } else { $this->getIo()->success($message); } @@ -227,14 +229,14 @@ protected function assertTrue(bool $expression, string $message) * @param bool $expression * @param string $message * - * @throws \Exception + * @throws Exception */ protected function assertFalse(bool $expression, string $message) { if ($expression !== false) { $message .= chr(10) . chr(10) . 'Expected value: false'; $message .= chr(10) . 'Actual value: ' . var_export($expression, true); - throw new \Exception('AssertFalse! ' . $message, 1590757846); + throw new Exception('AssertFalse! ' . $message, 1590757846); } else { $this->getIo()->success($message); } @@ -245,14 +247,14 @@ protected function assertFalse(bool $expression, string $message) * @param $actual * @param string $message * - * @throws \Exception + * @throws Exception */ protected function assert($expected, $actual, string $message) { if ($expected !== $actual) { $message .= chr(10) . chr(10) . 'Expected value: ' . $expected; $message .= chr(10) . 'Actual value: ' . $actual; - throw new \Exception('Assert! ' . $message, 1590757847); + throw new Exception('Assert! ' . $message, 1590757847); } else { $this->getIo()->success($message); } diff --git a/Tests/Acceptance/FileOperation/AddFileOperationTest.php b/Tests/Acceptance/FileOperation/AddFileOperationTest.php index 72e59f2..d850a2e 100644 --- a/Tests/Acceptance/FileOperation/AddFileOperationTest.php +++ b/Tests/Acceptance/FileOperation/AddFileOperationTest.php @@ -2,6 +2,7 @@ namespace Visol\Cloudinary\Tests\Acceptance\FileOperation; +use Exception; use TYPO3\CMS\Core\Resource\DuplicationBehavior; use TYPO3\CMS\Core\Resource\File; @@ -10,7 +11,7 @@ class AddFileOperationTest extends AbstractCloudinaryFileOperationTest /** * @return void - * @throws \Exception + * @throws Exception */ public function run() { diff --git a/Tests/Acceptance/FileOperation/CopyFileOperationTest.php b/Tests/Acceptance/FileOperation/CopyFileOperationTest.php index d771fff..3fe286d 100644 --- a/Tests/Acceptance/FileOperation/CopyFileOperationTest.php +++ b/Tests/Acceptance/FileOperation/CopyFileOperationTest.php @@ -2,6 +2,8 @@ namespace Visol\Cloudinary\Tests\Acceptance\FileOperation; +use Exception; + class CopyFileOperationTest extends AbstractCloudinaryFileOperationTest { @@ -12,7 +14,7 @@ class CopyFileOperationTest extends AbstractCloudinaryFileOperationTest /** * @return void - * @throws \Exception + * @throws Exception */ public function run() { diff --git a/Tests/Acceptance/FileOperation/CopyFolderOperationTests.php b/Tests/Acceptance/FileOperation/CopyFolderOperationTests.php index 5a9c44c..0a1e1b5 100644 --- a/Tests/Acceptance/FileOperation/CopyFolderOperationTests.php +++ b/Tests/Acceptance/FileOperation/CopyFolderOperationTests.php @@ -2,6 +2,8 @@ namespace Visol\Cloudinary\Tests\Acceptance\FileOperation; +use Exception; + class CopyFolderOperationTests extends AbstractCloudinaryFileOperationTest { @@ -12,7 +14,7 @@ class CopyFolderOperationTests extends AbstractCloudinaryFileOperationTest /** * @return void - * @throws \Exception + * @throws Exception */ public function run() { diff --git a/Tests/Acceptance/FileOperation/CountFilesInFolderOperationTests.php b/Tests/Acceptance/FileOperation/CountFilesInFolderOperationTests.php index bde450f..31ba7a2 100644 --- a/Tests/Acceptance/FileOperation/CountFilesInFolderOperationTests.php +++ b/Tests/Acceptance/FileOperation/CountFilesInFolderOperationTests.php @@ -2,6 +2,8 @@ namespace Visol\Cloudinary\Tests\Acceptance\FileOperation; +use Exception; + class CountFilesInFolderOperationTests extends AbstractCloudinaryFileOperationTest { @@ -17,7 +19,7 @@ class CountFilesInFolderOperationTests extends AbstractCloudinaryFileOperationTe /** * @return void - * @throws \Exception + * @throws Exception */ public function run() { diff --git a/Tests/Acceptance/FileOperation/CreateFolderOperationTests.php b/Tests/Acceptance/FileOperation/CreateFolderOperationTests.php index b9d52b9..a2b3694 100644 --- a/Tests/Acceptance/FileOperation/CreateFolderOperationTests.php +++ b/Tests/Acceptance/FileOperation/CreateFolderOperationTests.php @@ -2,12 +2,14 @@ namespace Visol\Cloudinary\Tests\Acceptance\FileOperation; +use Exception; + class CreateFolderOperationTests extends AbstractCloudinaryFileOperationTest { /** * @return void - * @throws \Exception + * @throws Exception */ public function run() { diff --git a/Tests/Acceptance/FileOperation/DeleteFileOperationTest.php b/Tests/Acceptance/FileOperation/DeleteFileOperationTest.php index 3a51b26..4f3b8ec 100644 --- a/Tests/Acceptance/FileOperation/DeleteFileOperationTest.php +++ b/Tests/Acceptance/FileOperation/DeleteFileOperationTest.php @@ -2,12 +2,14 @@ namespace Visol\Cloudinary\Tests\Acceptance\FileOperation; +use Exception; + class DeleteFileOperationTest extends AbstractCloudinaryFileOperationTest { /** * @return void - * @throws \Exception + * @throws Exception */ public function run() { diff --git a/Tests/Acceptance/FileOperation/DeleteFolderOperationTest.php b/Tests/Acceptance/FileOperation/DeleteFolderOperationTest.php index 2226cbb..49acb69 100644 --- a/Tests/Acceptance/FileOperation/DeleteFolderOperationTest.php +++ b/Tests/Acceptance/FileOperation/DeleteFolderOperationTest.php @@ -2,12 +2,14 @@ namespace Visol\Cloudinary\Tests\Acceptance\FileOperation; +use Exception; + class DeleteFolderOperationTest extends AbstractCloudinaryFileOperationTest { /** * @return void - * @throws \Exception + * @throws Exception */ public function run() { diff --git a/Tests/Acceptance/FileOperation/MoveFileOperationTest.php b/Tests/Acceptance/FileOperation/MoveFileOperationTest.php index 5588f59..7ad1ff3 100644 --- a/Tests/Acceptance/FileOperation/MoveFileOperationTest.php +++ b/Tests/Acceptance/FileOperation/MoveFileOperationTest.php @@ -2,6 +2,8 @@ namespace Visol\Cloudinary\Tests\Acceptance\FileOperation; +use Exception; + class MoveFileOperationTest extends AbstractCloudinaryFileOperationTest { @@ -12,7 +14,7 @@ class MoveFileOperationTest extends AbstractCloudinaryFileOperationTest /** * @return void - * @throws \Exception + * @throws Exception */ public function run() { diff --git a/Tests/Acceptance/FileOperation/RenameFolderOperationTests.php b/Tests/Acceptance/FileOperation/RenameFolderOperationTests.php index d4b0e5c..c723db7 100644 --- a/Tests/Acceptance/FileOperation/RenameFolderOperationTests.php +++ b/Tests/Acceptance/FileOperation/RenameFolderOperationTests.php @@ -2,6 +2,8 @@ namespace Visol\Cloudinary\Tests\Acceptance\FileOperation; +use Exception; + class RenameFolderOperationTests extends AbstractCloudinaryFileOperationTest { @@ -12,7 +14,7 @@ class RenameFolderOperationTests extends AbstractCloudinaryFileOperationTest /** * @return void - * @throws \Exception + * @throws Exception */ public function run() { diff --git a/ext_emconf.php b/ext_emconf.php index 3941664..509afa9 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -1,7 +1,7 @@ \Cloudinary::class, + 'title' => Cloudinary::class, 'description' => 'Cloudinary integration in TYPO3. Use automatic breakpoint generation for images.', 'state' => 'stable', 'author' => 'Jonas Renggli', diff --git a/ext_localconf.php b/ext_localconf.php index 2a5ea36..45fbd51 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -2,6 +2,8 @@ use TYPO3\CMS\Core\Core\Environment; use TYPO3\CMS\Core\Log\LogLevel; +use TYPO3\CMS\Core\Log\Writer\SyslogWriter; +use TYPO3\CMS\Core\Resource\Index\ExtractorRegistry; use TYPO3\CMS\Extbase\Utility\ExtensionUtility; use Visol\Cloudinary\Backend\Form\Container\InlineCloudinaryControlContainer; use TYPO3\CMS\Core\Resource\Driver\DriverRegistry; @@ -10,6 +12,7 @@ use Visol\Cloudinary\Driver\CloudinaryDriver; use TYPO3\CMS\Core\Log\Writer\FileWriter; use Visol\Cloudinary\Hook\FileUploadHook; +use Visol\Cloudinary\Services\Extractor\CloudinaryMetaDataExtractor; defined('TYPO3') || die('Access denied.'); call_user_func(callback: function () { @@ -22,7 +25,7 @@ ]; ExtensionUtility::configurePlugin( - \Cloudinary::class, + Cloudinary::class, 'WebHook', [ CloudinaryWebHookController::class => 'process', @@ -38,13 +41,13 @@ $driverRegistry->registerDriverClass( CloudinaryDriver::class, CloudinaryDriver::DRIVER_TYPE, - \Cloudinary::class, + Cloudinary::class, 'FILE:EXT:cloudinary/Configuration/FlexForm/CloudinaryFlexForm.xml', ); - /* @var \TYPO3\CMS\Core\Resource\Index\ExtractorRegistry $metaDataExtractorRegistry */ - $metaDataExtractorRegistry = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Resource\Index\ExtractorRegistry::class); - $metaDataExtractorRegistry->registerExtractionService(\Visol\Cloudinary\Services\Extractor\CloudinaryMetaDataExtractor::class); + /* @var ExtractorRegistry $metaDataExtractorRegistry */ + $metaDataExtractorRegistry = GeneralUtility::makeInstance(ExtractorRegistry::class); + $metaDataExtractorRegistry->registerExtractionService(CloudinaryMetaDataExtractor::class); // Log configuration for cloudinary web hook $GLOBALS['TYPO3_CONF_VARS']['LOG']['Visol']['Cloudinary']['Controller']['CloudinaryWebHookController']['writerConfiguration'] = [ @@ -57,7 +60,7 @@ // Configuration for WARNING severity, including all // levels with higher severity (ERROR, CRITICAL, EMERGENCY) LogLevel::WARNING => [ - \TYPO3\CMS\Core\Log\Writer\SyslogWriter::class => [], + SyslogWriter::class => [], ], ]; From ae6b6ee01d70d02959ef3c471882d2409db4d640 Mon Sep 17 00:00:00 2001 From: Hannes Lau Date: Fri, 22 Sep 2023 16:08:49 +0200 Subject: [PATCH 80/99] [BUGFIX] FIx rename webhook failing to delete processed files (#32) --- Classes/Driver/CloudinaryDriver.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Classes/Driver/CloudinaryDriver.php b/Classes/Driver/CloudinaryDriver.php index c382da7..0d2fbe9 100644 --- a/Classes/Driver/CloudinaryDriver.php +++ b/Classes/Driver/CloudinaryDriver.php @@ -366,6 +366,15 @@ public function replaceFile($fileIdentifier, $localFilePath): bool */ public function deleteFile($fileIdentifier): bool { + if ($this->isProcessedFile($fileIdentifier)) { + /* + * Processed files do not really exist in the Cloudinary file system + * Just remove the cache entry and confirm the deletion. + */ + $this->getCloudinaryResourceService()->delete($fileIdentifier); + return true; + } + $cloudinaryPublicId = $this->getCloudinaryPathService()->computeCloudinaryPublicId($fileIdentifier); $this->log( '[API] Delete resource "%s"', From bfe4526ccdf969c9d2e30959afe19b0dc747c141 Mon Sep 17 00:00:00 2001 From: Hannes Lau Date: Mon, 25 Sep 2023 13:11:39 +0200 Subject: [PATCH 81/99] [BUGFIX] Fix endless loading spinner in TYPO3 backend (#33) Fix a race condition in the TYPO3 BE that occurred on backend forms with in line elements. Load the external cloudinary js library via requireJS to ensure its availability when the custom JS in the TYPO3 backend tries to access the global object. --- .../Form/Container/InlineCloudinaryControlContainer.php | 5 ----- Resources/Public/JavaScript/CloudinaryMediaLibrary.js | 1 + 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/Classes/Backend/Form/Container/InlineCloudinaryControlContainer.php b/Classes/Backend/Form/Container/InlineCloudinaryControlContainer.php index 0e9dbd5..20c092d 100644 --- a/Classes/Backend/Form/Container/InlineCloudinaryControlContainer.php +++ b/Classes/Backend/Form/Container/InlineCloudinaryControlContainer.php @@ -20,11 +20,6 @@ class InlineCloudinaryControlContainer extends InlineControlContainer public function render() { - // We load here the cloudinary library - /** @var AssetCollector $assetCollector */ - $assetCollector = GeneralUtility::makeInstance(AssetCollector::class); - $assetCollector->addJavaScript('media_library_cloudinary', 'https://media-library.cloudinary.com/global/all.js', []); - /** @var PageRenderer $pageRenderer */ $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class); $pageRenderer->loadRequireJsModule('TYPO3/CMS/Cloudinary/CloudinaryMediaLibrary'); diff --git a/Resources/Public/JavaScript/CloudinaryMediaLibrary.js b/Resources/Public/JavaScript/CloudinaryMediaLibrary.js index bef168a..9f32f60 100644 --- a/Resources/Public/JavaScript/CloudinaryMediaLibrary.js +++ b/Resources/Public/JavaScript/CloudinaryMediaLibrary.js @@ -6,6 +6,7 @@ define([ 'TYPO3/CMS/Backend/Utility/MessageUtility', 'TYPO3/CMS/Backend/Modal', 'TYPO3/CMS/Backend/Severity', + '//media-library.cloudinary.com/global/all.js', ], function ($, NProgress, MessageUtility, Modal, Severity) { let irreNewTimout; From 0ec63634caaa2eca2085e1f8ab95d8d5b950188c Mon Sep 17 00:00:00 2001 From: Hannes Lau Date: Mon, 25 Sep 2023 22:58:59 +0200 Subject: [PATCH 82/99] [FEATURE] Update outdated resources when a cloudinary asset gets replaced (#34) Extend the webhook controller to flush the local caches if an image gets replaced (update override) in Cloudinary. --- .../CloudinaryWebHookController.php | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/Classes/Controller/CloudinaryWebHookController.php b/Classes/Controller/CloudinaryWebHookController.php index 2481df7..2ce5e20 100644 --- a/Classes/Controller/CloudinaryWebHookController.php +++ b/Classes/Controller/CloudinaryWebHookController.php @@ -9,6 +9,7 @@ * LICENSE.md file that was distributed with this source code. */ +use Cloudinary\Api\Admin\AdminApi; use Cloudinary\Api\Upload\UploadApi; use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Http\Message\ResponseInterface; @@ -27,6 +28,7 @@ use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\Utility\PathUtility; use TYPO3\CMS\Extbase\Mvc\Controller\ActionController; +use Visol\Cloudinary\Domain\Repository\ExplicitDataCacheRepository; use Visol\Cloudinary\Events\ClearCachePageEvent; use Visol\Cloudinary\Exceptions\CloudinaryNotFoundException; use Visol\Cloudinary\Exceptions\PublicIdMissingException; @@ -63,6 +65,11 @@ class CloudinaryWebHookController extends ActionController */ protected $eventDispatcher; + public function __construct( + protected ExplicitDataCacheRepository $explicitDataCacheRepository, + ) { + } + protected function initializeAction(): void { @@ -115,8 +122,14 @@ public function processAction(): ResponseInterface self::getLogger()->debug(sprintf('Start flushing cache for file action "%s". ', $requestType)); foreach ($publicIds as $publicId) { + if ($this->isRequestUploadOverwrite($payload)) { + // Update caches + $this->explicitDataCacheRepository->delete($this->storage->getUid(), $publicId); - if ($requestType === self::NOTIFICATION_TYPE_DELETE) { + $cloudinaryResource = (array)$this->getAdminApi()->asset($publicId); + $this->cloudinaryResourceService->save($cloudinaryResource); + } + elseif ($requestType === self::NOTIFICATION_TYPE_DELETE) { if (str_contains($publicId, '_processed_')) { $message = 'Processed file deleted. Nothing to do, stopping here...'; } else { @@ -408,6 +421,11 @@ protected static function getLogger(): Logger return $logger; } + protected function getAdminApi(): AdminApi + { + return CloudinaryApiUtility::getCloudinary($this->storage)->adminApi(); + } + protected function getUploadApi(): UploadApi { return CloudinaryApiUtility::getCloudinary($this->storage)->uploadApi(); From fd2682dfd0ba25725606d1951e2ad92adcd2d540 Mon Sep 17 00:00:00 2001 From: Hannes Lau Date: Wed, 27 Sep 2023 09:49:52 +0200 Subject: [PATCH 83/99] [BUGFIX] Avoid type exception for missing images (#35) The cloudinary extension tries to replace missing images with a default emergency fallback image. This may lead to invalid crop instructions for Cloudinary, as the emergency image may be smaller than the original image. Recover from these Cloudinary exceptions by returning an empty image instead of dying with a type excpetion. --- Classes/Services/CloudinaryImageService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/Services/CloudinaryImageService.php b/Classes/Services/CloudinaryImageService.php index 76239b4..38ad81a 100644 --- a/Classes/Services/CloudinaryImageService.php +++ b/Classes/Services/CloudinaryImageService.php @@ -69,7 +69,7 @@ public function getResponsiveBreakpointData(File $file, array $options): array { $explicitData = $this->getExplicitData($file, $options); - return $explicitData['responsive_breakpoints'][0]['breakpoints']; + return $explicitData['responsive_breakpoints'][0]['breakpoints'] ?? []; } public function getSrcsetAttribute(array $breakpoints): string From 45e3f8086bc5073c58dd9526f00f4f1b77346664 Mon Sep 17 00:00:00 2001 From: Daniel Huf <1814195+dhuf@users.noreply.github.com> Date: Thu, 5 Oct 2023 14:04:17 +0200 Subject: [PATCH 84/99] [FEATURE] Load Cloudinary script onclick --- .../InlineCloudinaryControlContainer.php | 37 ++-- .../Private/Standalone/MediaLibrary/Show.html | 12 +- .../JavaScript/CloudinaryMediaLibrary.js | 202 +++++++----------- 3 files changed, 106 insertions(+), 145 deletions(-) diff --git a/Classes/Backend/Form/Container/InlineCloudinaryControlContainer.php b/Classes/Backend/Form/Container/InlineCloudinaryControlContainer.php index 20c092d..020432b 100644 --- a/Classes/Backend/Form/Container/InlineCloudinaryControlContainer.php +++ b/Classes/Backend/Form/Container/InlineCloudinaryControlContainer.php @@ -27,26 +27,13 @@ public function render() return parent::render(); } - protected function renderPossibleRecordsSelectorTypeGroupDB(array $inlineConfiguration): string + protected function renderPossibleRecordsSelectorTypeGroupDB(array $inlineConfiguration): ?string { $typo3Buttons = parent::renderPossibleRecordsSelectorTypeGroupDB($inlineConfiguration); // We could have multiple cloudinary buttons / storages $cloudinaryButtons = $this->renderCloudinaryButtons($inlineConfiguration); - - // Inject button before help-block - if (strpos($typo3Buttons, '
') > 0) { - $typo3Buttons = str_replace( - '
', - $cloudinaryButtons . '
', - $typo3Buttons, - ); - // Try to inject it into the form-control container - } elseif (preg_match('/<\/div><\/div>$/i', $typo3Buttons)) { - $typo3Buttons = preg_replace('/<\/div><\/div>$/i', $cloudinaryButtons . '
', $typo3Buttons); - } else { - $typo3Buttons .= $cloudinaryButtons; - } + $typo3Buttons = $this->appendButtons($typo3Buttons, $cloudinaryButtons); return $typo3Buttons; } @@ -63,7 +50,7 @@ protected function renderCloudinaryButtons(array $inlineConfiguration): string $view = $this->initializeStandaloneView('EXT:cloudinary/Resources/Private/Standalone/MediaLibrary/Show.html'); $view->assignMultiple([ 'objectGroup' => $objectGroup, - 'cloudinaryCredentials' => json_encode($this->computeCloudinaryCredentials($storages)), + 'cloudinaryCredentials' => $this->computeCloudinaryCredentials($storages), ]); return $view->render(); @@ -150,4 +137,22 @@ protected function computeCloudinaryCredentials(array $storages): array return $cloudinaryCredentials; } + + protected function appendButtons(string $typo3Buttons, string $cloudinaryButtons): ?string + { + // Inject button before help-block + if (strpos($typo3Buttons, '
') > 0) { + $typo3Buttons = str_replace( + '
', + $cloudinaryButtons . '
', + $typo3Buttons, + ); + // Try to inject it into the form-control container + } elseif (preg_match('/<\/div><\/div>$/i', $typo3Buttons)) { + $typo3Buttons = preg_replace('/<\/div><\/div>$/i', $cloudinaryButtons . '
', $typo3Buttons); + } else { + $typo3Buttons .= $cloudinaryButtons; + } + return $typo3Buttons; + } } diff --git a/Resources/Private/Standalone/MediaLibrary/Show.html b/Resources/Private/Standalone/MediaLibrary/Show.html index 747c9cd..762c207 100644 --- a/Resources/Private/Standalone/MediaLibrary/Show.html +++ b/Resources/Private/Standalone/MediaLibrary/Show.html @@ -1 +1,11 @@ - + + + \ No newline at end of file diff --git a/Resources/Public/JavaScript/CloudinaryMediaLibrary.js b/Resources/Public/JavaScript/CloudinaryMediaLibrary.js index 9f32f60..d954f70 100644 --- a/Resources/Public/JavaScript/CloudinaryMediaLibrary.js +++ b/Resources/Public/JavaScript/CloudinaryMediaLibrary.js @@ -9,134 +9,80 @@ define([ '//media-library.cloudinary.com/global/all.js', ], function ($, NProgress, MessageUtility, Modal, Severity) { - let irreNewTimout; - let irreToggleTimout; - - // Click "new" irre - $('.t3js-create-new-button').click(function(e) { - const numberOfIrreObjects = $(this).parents('.form-group').find('.form-irre-object').length; - irreNewTimout = setTimeout(isNewIrreElementReady, 300, this, numberOfIrreObjects) - }) - - // Detect if the "new" irre is ready - function isNewIrreElementReady(element, numberOfIrreObjects) { - - const _numberOfIrreObjects = $(element).parents('.form-group').find('.form-irre-object').length; - - if (_numberOfIrreObjects > numberOfIrreObjects) { - clearTimeout(irreNewTimout) - initializeCloudinaryButtons() - } else { - irreToggleTimout = setTimeout(isNewIrreElementReady, 100, element, numberOfIrreObjects) - } - } - - // Click "toggle" irre box - $('.form-irre-header-cell').click(function(e) { - irreToggleTimout = setTimeout(isEditIrreElementReady, 300, this) - }) - - // Detect if the "toggle" irre is ready - function isEditIrreElementReady(element) { - - // 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) - initializeCloudinaryButtons() - } else { - irreToggleTimout = setTimeout(isEditIrreElementReady, 100, element) - } - } - - 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 cloudinary button - const mediaLibrary = cloudinary.createMediaLibrary( - { - cloud_name: credential.cloudName, - api_key: credential.apiKey, - username: credential.username, - timestamp: credential.timestamp, - signature: credential.signature, - button_class: - 'btn btn-default open-btn mx-1 btn-open-cloudinary btn-open-cloudinary-storage-' + credential.storageUid, - button_caption: ` - - - - Image or video from "${credential.storageName}"`, // todo translate me! - // search: { expression: 'resource_type:image' }, // todo we could have video, how to filter _processed_file - }, - { - insertHandler: function (data) { - NProgress.start(); - - const me = this; - const cloudinaryIds = data.assets.map((asset) => { - return asset.public_id; - }); - - $.post( - TYPO3.settings.ajaxUrls['cloudinary_add_files'], - { - cloudinaryIds: cloudinaryIds, - storageUid: me.storageUid, - }, - function (data) { - if (data.result === 'ok') { - data.files.map((fileUid) => { - MessageUtility.MessageUtility.send({ - actionName: 'typo3:foreignRelation:insert', - objectGroup: me.objectGroup, - table: 'sys_file', - uid: fileUid, - }); - }); - - NProgress.done(); - } else { - // error! - const $confirm = Modal.confirm('ERROR', data.error, Severity.error, [ - { - text: 'OK', // TYPO3.lang['file_upload.button.ok'] - btnClass: 'btn-' + Severity.getCssClass(Severity.error), - name: 'ok', - active: true, - }, - ]).on('confirm.button.ok', function () { - $confirm.modal('hide'); - }); - } - - NProgress.done(); - } - ); + let cloudinaryButtons = Array.from(document.getElementsByClassName('btn-cloudinary-media-library')); + + cloudinaryButtons.map((cloudinaryButton) => { + cloudinaryButton.addEventListener("click", function(event){ + event.preventDefault(); + let buttonClasses = $(this).attr('class'); + let buttonInnerHtml = $(this).prop("innerHTML"); + let objectGroup = $(this).data('objectGroup'); + let elementId = $(this).attr('id'); + openMediaLibrary(JSON.parse(cloudinaryButton.dataset.cloudinaryCredentials), objectGroup, elementId, buttonClasses, buttonInnerHtml); + }); + }); + + function openMediaLibrary(credential, objectGroup, elementId, buttonClasses, buttonInnerHtml) { + // Render the cloudinary button + const mediaLibrary = cloudinary.openMediaLibrary( + { + cloud_name: credential.cloudName, + api_key: credential.apiKey, + username: credential.username, + timestamp: credential.timestamp, + signature: credential.signature, + button_class: buttonClasses, + button_caption: buttonInnerHtml, + }, + { + insertHandler: function (data) { + NProgress.start(); + + const me = this; + const cloudinaryIds = data.assets.map((asset) => { + return asset.public_id; + }); + + $.post( + TYPO3.settings.ajaxUrls['cloudinary_add_files'], + { + cloudinaryIds: cloudinaryIds, + storageUid: me.storageUid, }, - }, - '#' + $(element).attr('id') - ); - mediaLibrary.storageUid = credential.storageUid; - mediaLibrary.objectGroup = $(element).data('objectGroup'); - }); - - // 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 for field id #' + $(element).attr('id')) - }) + function (data) { + if (data.result === 'ok') { + data.files.map((fileUid) => { + MessageUtility.MessageUtility.send({ + actionName: 'typo3:foreignRelation:insert', + objectGroup: me.objectGroup, + table: 'sys_file', + uid: fileUid, + }); + }); + + NProgress.done(); + } else { + // error! + const $confirm = Modal.confirm('ERROR', data.error, Severity.error, [ + { + text: 'OK', // TYPO3.lang['file_upload.button.ok'] + btnClass: 'btn-' + Severity.getCssClass(Severity.error), + name: 'ok', + active: true, + }, + ]).on('confirm.button.ok', function () { + $confirm.modal('hide'); + }); + } + + NProgress.done(); + } + ); + }, + }, + '#' + elementId + ); + mediaLibrary.storageUid = credential.storageUid; + mediaLibrary.objectGroup = objectGroup; } - - // We trigger a rendering for the normal case - initializeCloudinaryButtons () }); From 846cbf1eacb586444a2b142b46eaa7e76ffcbfc2 Mon Sep 17 00:00:00 2001 From: Daniel Huf <1814195+dhuf@users.noreply.github.com> Date: Wed, 8 Nov 2023 12:07:54 +0100 Subject: [PATCH 85/99] [FEATURE] Filter % or %env for env vars Co-authored-by: Hannes Lau --- Classes/Services/ConfigurationService.php | 5 ++--- Resources/Private/Language/backend.xlf | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Classes/Services/ConfigurationService.php b/Classes/Services/ConfigurationService.php index 6026614..51896f7 100644 --- a/Classes/Services/ConfigurationService.php +++ b/Classes/Services/ConfigurationService.php @@ -29,9 +29,8 @@ public function __construct(array $configuration) */ public function get(string $key): string { - $rawValue = $this->configuration[$key] ?? ''; - $value = trim((string)$rawValue); - if (preg_match('/^%\w+\((.*)\)%$/', $value, $matches) || preg_match('/^%(.*)%$/', $value, $matches)) { + $value = trim((string)$this->configuration[$key]); + if (preg_match('/^%env\((.*)\)%$/', $value, $matches) || preg_match('/^%(.*)%$/', $value, $matches)) { $value = getenv($matches[1]); if ($value === false) { diff --git a/Resources/Private/Language/backend.xlf b/Resources/Private/Language/backend.xlf index 1389893..da9a4c9 100644 --- a/Resources/Private/Language/backend.xlf +++ b/Resources/Private/Language/backend.xlf @@ -14,7 +14,7 @@ Cloudinary API Secret
- A possible base path for storing files in a parent directory on cloudianry. Example `production/` + A possible base path for storing files in a parent directory on cloudinary. Example `production/` A possible CNAME to serve your assets from, instead of the default res.cloudinary.com domain name. From df696eb19c8aca9e78178db88e50a0c18b213d14 Mon Sep 17 00:00:00 2001 From: Jonas Renggli Date: Tue, 21 Nov 2023 10:10:08 +0100 Subject: [PATCH 86/99] [BUGFIX] Use identifier_hash in CloudinaryScanService (#38) * [BUGFIX] Use identifier_hash in CloudinaryScanService The `fileExistsInStorage()` method currently performs a SQL query to count records, but suffers from slow performance due to the absence of an index on the `identifier` field. This results in a full table scan. To address this issue, this commit refactors the method to utilize the pre-existing `identifier_hash` field, specifically designed for efficient data searches. --------- Co-authored-by: Daniel Huf Co-authored-by: Hannes Lau --- Classes/Services/CloudinaryScanService.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Classes/Services/CloudinaryScanService.php b/Classes/Services/CloudinaryScanService.php index 1a16d73..cd9dbc5 100644 --- a/Classes/Services/CloudinaryScanService.php +++ b/Classes/Services/CloudinaryScanService.php @@ -193,8 +193,8 @@ protected function fileExistsInStorage(string $fileIdentifier): bool ->from('sys_file') ->where( $this->getQueryBuilder()->expr()->eq( - 'identifier', - $query->expr()->literal($fileIdentifier) + 'identifier_hash', + $query->expr()->literal($this->storage->hashFileIdentifier($fileIdentifier)), ), $this->getQueryBuilder()->expr()->eq( 'storage', From 3bd79c2a7feb7e9d39d224f9f03a8c55892fcfa7 Mon Sep 17 00:00:00 2001 From: Hannes Lau Date: Wed, 29 Nov 2023 09:38:39 +0100 Subject: [PATCH 87/99] [FEATURE] JB-1236 Scale down images before transformations (#40) Stay below the transformation megapixel limit by prepending a scale down transformation for large images. --- Classes/Services/CloudinaryImageService.php | 34 ++++++++++++++++++--- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/Classes/Services/CloudinaryImageService.php b/Classes/Services/CloudinaryImageService.php index 38ad81a..22bfcd6 100644 --- a/Classes/Services/CloudinaryImageService.php +++ b/Classes/Services/CloudinaryImageService.php @@ -11,6 +11,7 @@ use Cloudinary\Asset\Image; use Cloudinary\Transformation\ImageTransformation; +use Cloudinary\Transformation\Scale; use Exception; use TYPO3\CMS\Core\Resource\StorageRepository; use TYPO3\CMS\Core\Resource\File; @@ -20,6 +21,9 @@ class CloudinaryImageService extends AbstractCloudinaryMediaService { + // See "Max image megapixels" on https://cloudinary.com/pricing/compare-plans + const TRANSFORMATION_MAX_INPUT_PIXELS = 50_000_000; + protected ExplicitDataCacheRepository $explicitDataCacheRepository; protected ?StorageRepository $storageRepository = null; @@ -46,11 +50,13 @@ public function getExplicitData(File $file, array $options): array // With Cloudinary API 2, we need to modify the way in which "responsive_breakpoints.transformation" are transmitted. $apiOptions = $options; - if (isset($options['responsive_breakpoints']['transformation'])) { - $apiOptions['responsive_breakpoints']['transformation'] = []; // reset the value - foreach ($options['responsive_breakpoints']['transformation'] as $transformationParams) { - $apiOptions['responsive_breakpoints']['transformation'][] = ImageTransformation::fromParams($transformationParams); - } + if (isset($apiOptions['responsive_breakpoints']['transformation'])) { + // Check if we need to scale the image down, before applying image transformations + $prescaleTransformation = $this->getPrescaleTransformation($file); + $apiOptions['responsive_breakpoints']['transformation'] = array_map( + fn(array $parameters) => (new ImageTransformation($prescaleTransformation))->addActionFromQualifiers($parameters), + $apiOptions['responsive_breakpoints']['transformation'], + ); } try { @@ -221,4 +227,22 @@ public function injectStorageRepository(StorageRepository $storageRepository): v $this->storageRepository = $storageRepository; } + /** + * Check if cloudinary needs to scale down the image before applying + * transformations. This function will return the required scaling + * transformation or null if no scaling is required. + */ + protected function getPrescaleTransformation(File $file): ?Scale + { + $width = $file->getProperty('width') ?? 0; + $height = $file->getProperty('height') ?? 0; + + if ($width * $height <= self::TRANSFORMATION_MAX_INPUT_PIXELS) { + return null; + } + + // Calculate a width that allows the image to be processed + $maxWidth = (int)floor(sqrt(self::TRANSFORMATION_MAX_INPUT_PIXELS / ($height / $width))); + return Scale::limitFit($maxWidth); + } } From 7fd9ec09b43cbf247bc2a60c6bdb95dfdce21666 Mon Sep 17 00:00:00 2001 From: Hannes Lau Date: Tue, 5 Dec 2023 23:02:38 +0100 Subject: [PATCH 88/99] [BUGFIX] JB-1203 Fix user defined crops --- Classes/Services/CloudinaryImageService.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Classes/Services/CloudinaryImageService.php b/Classes/Services/CloudinaryImageService.php index 22bfcd6..16c1dc0 100644 --- a/Classes/Services/CloudinaryImageService.php +++ b/Classes/Services/CloudinaryImageService.php @@ -11,7 +11,6 @@ use Cloudinary\Asset\Image; use Cloudinary\Transformation\ImageTransformation; -use Cloudinary\Transformation\Scale; use Exception; use TYPO3\CMS\Core\Resource\StorageRepository; use TYPO3\CMS\Core\Resource\File; @@ -53,10 +52,11 @@ public function getExplicitData(File $file, array $options): array if (isset($apiOptions['responsive_breakpoints']['transformation'])) { // Check if we need to scale the image down, before applying image transformations $prescaleTransformation = $this->getPrescaleTransformation($file); - $apiOptions['responsive_breakpoints']['transformation'] = array_map( - fn(array $parameters) => (new ImageTransformation($prescaleTransformation))->addActionFromQualifiers($parameters), - $apiOptions['responsive_breakpoints']['transformation'], - ); + $transformation = new ImageTransformation($prescaleTransformation); + foreach($apiOptions['responsive_breakpoints']['transformation'] as $parameters) { + $transformation->addActionFromQualifiers($parameters); + } + $apiOptions['responsive_breakpoints']['transformation'] = $transformation; } try { @@ -165,10 +165,10 @@ public function generateOptionsFromSettings(array $settings, bool $enableRespons ) { $transformations[] = [ 'crop' => 'crop', - 'width' => $settings['width'], - 'height' => $settings['height'], - 'x' => $settings['x'], - 'y' => $settings['y'], + 'width' => (int)$settings['width'], + 'height' => (int)$settings['height'], + 'x' => (int)$settings['x'], + 'y' => (int)$settings['y'], ]; } From e957751f67a75fa3de0daf276c0310bc007e2632 Mon Sep 17 00:00:00 2001 From: Hannes Lau Date: Tue, 12 Dec 2023 20:01:22 +0100 Subject: [PATCH 89/99] [TASK] JB-1250 Webhook: Speed up file lookups (#43) --- Classes/Controller/CloudinaryWebHookController.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Classes/Controller/CloudinaryWebHookController.php b/Classes/Controller/CloudinaryWebHookController.php index 2ce5e20..342ad9c 100644 --- a/Classes/Controller/CloudinaryWebHookController.php +++ b/Classes/Controller/CloudinaryWebHookController.php @@ -221,13 +221,14 @@ protected function handleFileRename(string $previousFileIdentifier, string $next protected function getFile(array $cloudinaryResource): File { $fileIdentifier = $this->cloudinaryPathService->computeFileIdentifier($cloudinaryResource); + $fileIdentifierHash = $this->storage->hashFileIdentifier($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)) + $q->expr()->eq('identifier_hash', $q->expr()->literal($fileIdentifierHash)) ) ->execute() ->fetchAssociative(); From 4e1715c5cb4d6c09e4431d031d06cf1d7a9e9b29 Mon Sep 17 00:00:00 2001 From: Daniel Huf <1814195+dhuf@users.noreply.github.com> Date: Thu, 29 Feb 2024 10:37:41 +0100 Subject: [PATCH 90/99] feat: Refresh media library on irre elements (#44) --- .../Private/Standalone/MediaLibrary/Show.html | 1 + .../JavaScript/CloudinaryMediaLibrary.js | 37 ++++++++++++++----- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/Resources/Private/Standalone/MediaLibrary/Show.html b/Resources/Private/Standalone/MediaLibrary/Show.html index 762c207..f49a42f 100644 --- a/Resources/Private/Standalone/MediaLibrary/Show.html +++ b/Resources/Private/Standalone/MediaLibrary/Show.html @@ -4,6 +4,7 @@ class="btn-cloudinary-media-library btn btn-default open-btn mx-1 btn-open-cloudinary btn-open-cloudinary-storage-{cloudinaryCredential.storageUid}" data-cloudinary-credentials="{cloudinaryCredential -> f:format.json()}" data-object-group="{objectGroup}" + disabled > diff --git a/Resources/Public/JavaScript/CloudinaryMediaLibrary.js b/Resources/Public/JavaScript/CloudinaryMediaLibrary.js index d954f70..4484f96 100644 --- a/Resources/Public/JavaScript/CloudinaryMediaLibrary.js +++ b/Resources/Public/JavaScript/CloudinaryMediaLibrary.js @@ -10,17 +10,36 @@ define([ ], function ($, NProgress, MessageUtility, Modal, Severity) { let cloudinaryButtons = Array.from(document.getElementsByClassName('btn-cloudinary-media-library')); + setCloudinaryButtonEvent(cloudinaryButtons); - cloudinaryButtons.map((cloudinaryButton) => { - cloudinaryButton.addEventListener("click", function(event){ - event.preventDefault(); - let buttonClasses = $(this).attr('class'); - let buttonInnerHtml = $(this).prop("innerHTML"); - let objectGroup = $(this).data('objectGroup'); - let elementId = $(this).attr('id'); - openMediaLibrary(JSON.parse(cloudinaryButton.dataset.cloudinaryCredentials), objectGroup, elementId, buttonClasses, buttonInnerHtml); + $('.t3js-create-new-button').click(function(e) { + setTimedOutedCloudinaryButtonEvent(); + }) + + $('.form-irre-header-button').click(function(e) { + setTimedOutedCloudinaryButtonEvent(); + }) + + function setTimedOutedCloudinaryButtonEvent(){ + setTimeout(() => + {setCloudinaryButtonEvent(Array.from(document.getElementsByClassName('btn-cloudinary-media-library')))}, + 1000 + ); + } + + function setCloudinaryButtonEvent(cloudinaryButtons) { + cloudinaryButtons.map((cloudinaryButton) => { + cloudinaryButton.addEventListener("click", function(event){ + event.preventDefault(); + let buttonClasses = $(this).attr('class'); + let buttonInnerHtml = $(this).prop("innerHTML"); + let objectGroup = $(this).data('objectGroup'); + let elementId = $(this).attr('id'); + openMediaLibrary(JSON.parse(cloudinaryButton.dataset.cloudinaryCredentials), objectGroup, elementId, buttonClasses, buttonInnerHtml); + }); + cloudinaryButton.removeAttribute('disabled'); }); - }); + } function openMediaLibrary(credential, objectGroup, elementId, buttonClasses, buttonInnerHtml) { // Render the cloudinary button From 035280dbe9239983348f1eeeece6471c8d1921a0 Mon Sep 17 00:00:00 2001 From: Hannes Lau Date: Fri, 1 Mar 2024 10:46:54 +0100 Subject: [PATCH 91/99] [BUGFIX] Fix Cloudinary unable to render large images Fix a missing import that caused a bug when rendering large images via Cloudinary. References: 7fd9ec0 --- Classes/Services/CloudinaryImageService.php | 1 + 1 file changed, 1 insertion(+) diff --git a/Classes/Services/CloudinaryImageService.php b/Classes/Services/CloudinaryImageService.php index 16c1dc0..3355750 100644 --- a/Classes/Services/CloudinaryImageService.php +++ b/Classes/Services/CloudinaryImageService.php @@ -11,6 +11,7 @@ use Cloudinary\Asset\Image; use Cloudinary\Transformation\ImageTransformation; +use Cloudinary\Transformation\Scale; use Exception; use TYPO3\CMS\Core\Resource\StorageRepository; use TYPO3\CMS\Core\Resource\File; From 1ceb498c5f125664bab0c5639f5122dc1994022d Mon Sep 17 00:00:00 2001 From: Hannes Lau Date: Fri, 1 Mar 2024 10:37:58 +0100 Subject: [PATCH 92/99] [BUGFIX] JB-1301 Fix processed files on clouds with cnames Fix a bug where processed files would not be recognized if the cloudinary cloud uses a cname. Remove the cloud name from the processed file identifier --- Classes/Driver/CloudinaryDriver.php | 39 +++++++++++-------- .../BeforeFileProcessingEventHandler.php | 18 +++++++-- .../InvalidResourceUrlException.php | 15 +++++++ 3 files changed, 53 insertions(+), 19 deletions(-) create mode 100644 Classes/Exceptions/InvalidResourceUrlException.php diff --git a/Classes/Driver/CloudinaryDriver.php b/Classes/Driver/CloudinaryDriver.php index 0d2fbe9..a7b8787 100644 --- a/Classes/Driver/CloudinaryDriver.php +++ b/Classes/Driver/CloudinaryDriver.php @@ -44,6 +44,8 @@ class CloudinaryDriver extends AbstractHierarchicalFilesystemDriver protected const UNSAFE_FILENAME_CHARACTER_EXPRESSION = '\\x00-\\x2C\\/\\x3A-\\x3F\\x5B-\\x60\\x7B-\\xBF'; + public const PROCESSEDFILE_IDENTIFIER_PREFIX = 'PROCESSEDFILE'; + static public array $knownRawFormats = ['youtube', 'vimeo']; /** @@ -105,8 +107,8 @@ public function initialize(): void */ public function getPublicUrl($identifier): string { - if ($processedPath = $this->computeProcessedPath($identifier)) { - return 'https://res.cloudinary.com/' . $processedPath; + if ($this->isProcessedFile($identifier)) { + return $this->getPublicBaseUrl() . $this->getProcessedFileUri($identifier); } $cloudinaryResource = $this->getCloudinaryResourceService()->getResource( @@ -116,6 +118,17 @@ public function getPublicUrl($identifier): string return $cloudinaryResource ? $cloudinaryResource['secure_url'] : ''; } + public function getPublicBaseUrl(): string + { + $cname = $this->configurationService->get('cname'); + $publicBaseUrl = empty($cname) + ? 'https://res.cloudinary.com/' . $this->configurationService->get('cloudName') + : 'https://' . $this->configurationService->get('cname') + ; + + return $publicBaseUrl; + } + protected function log(string $message, array $arguments = [], array $data = []): void { /** @var Logger $logger */ @@ -1027,14 +1040,18 @@ public function getExplicitDataCacheRepository(): ExplicitDataCacheRepository return GeneralUtility::makeInstance(ExplicitDataCacheRepository::class); } - protected function getProcessedFilePattern(): string + protected function isProcessedFile(string $identifier): bool { - return sprintf('/^PROCESSEDFILE\/(%s\/.*)/', $this->configurationService->get('cloudName')); + return str_starts_with($identifier, self::PROCESSEDFILE_IDENTIFIER_PREFIX); } - protected function isProcessedFile(string $identifier): bool + protected function getProcessedFileUri(string $identifier): string { - return (bool)preg_match($this->getProcessedFilePattern(), $identifier); + if (! $this->isProcessedFile($identifier)) { + throw new \DomainException('Identifier does not belong to a processed file', 1709283844258); + } + + return substr($identifier, strlen(self::PROCESSEDFILE_IDENTIFIER_PREFIX)); } protected function isProcessedFolder(string $identifier): bool @@ -1050,15 +1067,6 @@ protected function isProcessedFolder(string $identifier): bool return str_starts_with($identifier, $folderPath); } - protected function computeProcessedPath(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); @@ -1139,5 +1147,4 @@ protected function getAdminApi(): AdminApi { return CloudinaryApiUtility::getCloudinary($this->configuration)->adminApi(); } - } diff --git a/Classes/EventHandlers/BeforeFileProcessingEventHandler.php b/Classes/EventHandlers/BeforeFileProcessingEventHandler.php index 9f9240c..28fecda 100644 --- a/Classes/EventHandlers/BeforeFileProcessingEventHandler.php +++ b/Classes/EventHandlers/BeforeFileProcessingEventHandler.php @@ -14,7 +14,9 @@ use TYPO3\CMS\Core\Resource\ProcessedFileRepository; use TYPO3\CMS\Core\Utility\GeneralUtility; use Visol\Cloudinary\Driver\CloudinaryDriver; +use Visol\Cloudinary\Exceptions\InvalidResourceUrlException; use Visol\Cloudinary\Services\CloudinaryImageService; +use Visol\Cloudinary\Services\ConfigurationService; final class BeforeFileProcessingEventHandler { @@ -54,11 +56,15 @@ public function __invoke(BeforeFileProcessingEvent $event): void ] ); $url = $explicitData['eager'][0]['secure_url']; + $publicBaseUrl = $driver->getPublicBaseUrl(); - $parts = parse_url($url); - $path = $parts['path'] ?? ''; + if (! str_starts_with($url, $publicBaseUrl)) { + throw new InvalidResourceUrlException($url, $publicBaseUrl, 1709284880259); + } + + $identifier = CloudinaryDriver::PROCESSEDFILE_IDENTIFIER_PREFIX . substr($url, strlen($publicBaseUrl)); $processedFile->setName(basename($url)); - $processedFile->setIdentifier('PROCESSEDFILE' . $path); + $processedFile->setIdentifier($identifier); $processedFile->updateProperties([ 'width' => $explicitData['eager'][0]['width'], @@ -73,4 +79,10 @@ public function getCloudinaryImageService(): CloudinaryImageService { return GeneralUtility::makeInstance(CloudinaryImageService::class); } + + protected function getCloudNameForFile(File $file): string + { + $configurationService = GeneralUtility::makeInstance(ConfigurationService::class, $file->getStorage()->getConfiguration()); + return $configurationService->get('cloudName'); + } } diff --git a/Classes/Exceptions/InvalidResourceUrlException.php b/Classes/Exceptions/InvalidResourceUrlException.php new file mode 100644 index 0000000..847b4ae --- /dev/null +++ b/Classes/Exceptions/InvalidResourceUrlException.php @@ -0,0 +1,15 @@ + Date: Fri, 1 Mar 2024 11:25:31 +0100 Subject: [PATCH 93/99] [CLEANUP] Remove dead code Remove unused protected method, accidentally introduced with f317925 --- Classes/EventHandlers/BeforeFileProcessingEventHandler.php | 7 ------- 1 file changed, 7 deletions(-) diff --git a/Classes/EventHandlers/BeforeFileProcessingEventHandler.php b/Classes/EventHandlers/BeforeFileProcessingEventHandler.php index 28fecda..4f337a4 100644 --- a/Classes/EventHandlers/BeforeFileProcessingEventHandler.php +++ b/Classes/EventHandlers/BeforeFileProcessingEventHandler.php @@ -16,7 +16,6 @@ use Visol\Cloudinary\Driver\CloudinaryDriver; use Visol\Cloudinary\Exceptions\InvalidResourceUrlException; use Visol\Cloudinary\Services\CloudinaryImageService; -use Visol\Cloudinary\Services\ConfigurationService; final class BeforeFileProcessingEventHandler { @@ -79,10 +78,4 @@ public function getCloudinaryImageService(): CloudinaryImageService { return GeneralUtility::makeInstance(CloudinaryImageService::class); } - - protected function getCloudNameForFile(File $file): string - { - $configurationService = GeneralUtility::makeInstance(ConfigurationService::class, $file->getStorage()->getConfiguration()); - return $configurationService->get('cloudName'); - } } From ab69ed71e0c7ca68a9522ee40bcc6fa61f077525 Mon Sep 17 00:00:00 2001 From: Hannes Lau Date: Fri, 1 Mar 2024 13:51:56 +0100 Subject: [PATCH 94/99] [BUGFIX] Avoid exception if cloudinary is unable to render backend preview --- .../BeforeFileProcessingEventHandler.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Classes/EventHandlers/BeforeFileProcessingEventHandler.php b/Classes/EventHandlers/BeforeFileProcessingEventHandler.php index 4f337a4..0c32299 100644 --- a/Classes/EventHandlers/BeforeFileProcessingEventHandler.php +++ b/Classes/EventHandlers/BeforeFileProcessingEventHandler.php @@ -54,14 +54,19 @@ public function __invoke(BeforeFileProcessingEvent $event): void ] ] ); - $url = $explicitData['eager'][0]['secure_url']; - $publicBaseUrl = $driver->getPublicBaseUrl(); + $url = $explicitData['eager'][0]['secure_url'] ?? null; + if (!isset($url)) { + // cloudinary is unable to render + return; + } + + $publicBaseUrl = $driver->getPublicBaseUrl(); if (! str_starts_with($url, $publicBaseUrl)) { throw new InvalidResourceUrlException($url, $publicBaseUrl, 1709284880259); } - $identifier = CloudinaryDriver::PROCESSEDFILE_IDENTIFIER_PREFIX . substr($url, strlen($publicBaseUrl)); + $processedFile->setName(basename($url)); $processedFile->setIdentifier($identifier); From 13a17a2ac976c225ff99289c2fc0ab627ee32225 Mon Sep 17 00:00:00 2001 From: Hannes Lau Date: Fri, 1 Mar 2024 14:18:25 +0100 Subject: [PATCH 95/99] [BUGFIX] JB-1305 Update local file name on remote rename When a file gets renamed in Cloudinary, do not only update the file identifier in the local sys_file table, but also the sys_file name. --- Classes/Controller/CloudinaryWebHookController.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Classes/Controller/CloudinaryWebHookController.php b/Classes/Controller/CloudinaryWebHookController.php index 342ad9c..ca759c3 100644 --- a/Classes/Controller/CloudinaryWebHookController.php +++ b/Classes/Controller/CloudinaryWebHookController.php @@ -204,6 +204,8 @@ protected function handleFileRename(string $previousFileIdentifier, string $next $nextFolderIdentifier = PathUtility::dirname($nextFileIdentifier); $nextFolderIdentifierHash = sha1($this->canonicalizeAndCheckFolderIdentifier($nextFolderIdentifier)); $nextFileIdentifierHash = sha1($this->canonicalizeAndCheckFileIdentifier($nextFileIdentifier)); + $nextName = PathUtility::basename($nextFileIdentifier); + $tableName = 'sys_file'; $q = $this->getQueryBuilder($tableName); $q->update($tableName) @@ -214,6 +216,7 @@ protected function handleFileRename(string $previousFileIdentifier, string $next ->set('identifier', $q->expr()->literal($nextFileIdentifier), false) ->set('identifier_hash', $q->expr()->literal($nextFileIdentifierHash), false) ->set('folder_hash', $q->expr()->literal($nextFolderIdentifierHash), false) + ->set('name', $q->expr()->literal($nextName), false) ->setMaxResults(1) ->executeStatement(); } From f65a1c3706f0843a61ff8c06f747b5a35d42f223 Mon Sep 17 00:00:00 2001 From: Hannes Lau Date: Fri, 1 Mar 2024 16:58:12 +0100 Subject: [PATCH 96/99] [TASK] Ensure trailing slashes in file identifier generation As defined in AbstractHierarchicalFilesystemDriver::canonicalizeAndCheckFileIdentifier --- Classes/Services/CloudinaryPathService.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Classes/Services/CloudinaryPathService.php b/Classes/Services/CloudinaryPathService.php index 595ff2c..ba73df3 100644 --- a/Classes/Services/CloudinaryPathService.php +++ b/Classes/Services/CloudinaryPathService.php @@ -40,10 +40,15 @@ public function computeFileIdentifier(array $cloudinaryResource): string ? $cloudinaryResource['public_id'] : $cloudinaryResource['public_id'] . '.' . $cloudinaryResource['format']; - return self::stripBasePathFromIdentifier( + $fileIdentifier = self::stripBasePathFromIdentifier( DIRECTORY_SEPARATOR . $fileIdentifier, $this->getBasePath() ); + + // ensure leading slash + $fileIdentifier = '/' . ltrim($fileIdentifier, './'); + + return $fileIdentifier; } public function computeFolderIdentifier(string $cloudinaryFolder): string From ff440b995480961398f9404fcc6dfbac4976b54b Mon Sep 17 00:00:00 2001 From: Hannes Lau Date: Fri, 8 Mar 2024 09:18:47 +0100 Subject: [PATCH 97/99] [TASK] Limit cloudinary file processing to image related tasks (#46) --- .../BeforeFileProcessingEventHandler.php | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/Classes/EventHandlers/BeforeFileProcessingEventHandler.php b/Classes/EventHandlers/BeforeFileProcessingEventHandler.php index 0c32299..3ee5307 100644 --- a/Classes/EventHandlers/BeforeFileProcessingEventHandler.php +++ b/Classes/EventHandlers/BeforeFileProcessingEventHandler.php @@ -11,6 +11,7 @@ use TYPO3\CMS\Core\Resource\Event\BeforeFileProcessingEvent; use TYPO3\CMS\Core\Resource\File; +use TYPO3\CMS\Core\Resource\ProcessedFile; use TYPO3\CMS\Core\Resource\ProcessedFileRepository; use TYPO3\CMS\Core\Utility\GeneralUtility; use Visol\Cloudinary\Driver\CloudinaryDriver; @@ -19,25 +20,32 @@ final class BeforeFileProcessingEventHandler { + public const ALLOWED_TASKS = [ + ProcessedFile::CONTEXT_IMAGECROPSCALEMASK, + ProcessedFile::CONTEXT_IMAGEPREVIEW, + ]; + public function __invoke(BeforeFileProcessingEvent $event): void { $driver = $event->getDriver(); - $processedFile = $event->getProcessedFile(); - /** @var File $file */ - $file = $event->getFile(); - if (!$driver instanceof CloudinaryDriver) { return; } + $processedFile = $event->getProcessedFile(); + if(! in_array($processedFile->getTaskIdentifier(), self::ALLOWED_TASKS)) { + return; + } if ($processedFile->isProcessed()) { return; } - if (str_starts_with($processedFile->getIdentifier(), 'PROCESSEDFILE')) { return; } + /** @var File $file */ + $file = $event->getFile(); + $explicitData = $this->getCloudinaryImageService()->getExplicitData( $file, [ From 09848b748c2e45d6de84a1de6cd092c8e5827742 Mon Sep 17 00:00:00 2001 From: Hannes Lau Date: Thu, 7 Mar 2024 19:04:13 +0100 Subject: [PATCH 98/99] [FEATURE] Render thumbnails for videos in the TYPO3 backend --- .../BeforeFileProcessingEventHandler.php | 30 +++++++++---------- .../AbstractCloudinaryMediaService.php | 5 ++++ Classes/Services/CloudinaryImageService.php | 2 +- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/Classes/EventHandlers/BeforeFileProcessingEventHandler.php b/Classes/EventHandlers/BeforeFileProcessingEventHandler.php index 3ee5307..c919b39 100644 --- a/Classes/EventHandlers/BeforeFileProcessingEventHandler.php +++ b/Classes/EventHandlers/BeforeFileProcessingEventHandler.php @@ -46,22 +46,20 @@ public function __invoke(BeforeFileProcessingEvent $event): void /** @var File $file */ $file = $event->getFile(); - $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', - ] - ] - ] - ); + $eagerOptions = [ + 'fetch_format' => 'auto', + 'quality' => 'auto:eco', + 'width' => 64, + 'height' => 64, + 'crop' => 'fit', + ]; + if ($file->getType() === $file::FILETYPE_VIDEO) { + $eagerOptions['format'] = 'png'; + } + $explicitData = $this->getCloudinaryImageService()->getExplicitData($file, [ + 'type' => 'upload', + 'eager' => [$eagerOptions] + ]); $url = $explicitData['eager'][0]['secure_url'] ?? null; if (!isset($url)) { diff --git a/Classes/Services/AbstractCloudinaryMediaService.php b/Classes/Services/AbstractCloudinaryMediaService.php index 76b394d..fb19555 100644 --- a/Classes/Services/AbstractCloudinaryMediaService.php +++ b/Classes/Services/AbstractCloudinaryMediaService.php @@ -77,6 +77,11 @@ public function getPublicIdForFile(File $file): string ->computeCloudinaryPublicId($file->getIdentifier()); } + public function getResourceTypeForFile(File $file): string + { + return $this->getCloudinaryPathService($file->getStorage())->getResourceType($file->getIdentifier()); + } + protected function getUploadApi(ResourceStorage $storage): UploadApi { return CloudinaryApiUtility::getCloudinary($storage)->uploadApi(); diff --git a/Classes/Services/CloudinaryImageService.php b/Classes/Services/CloudinaryImageService.php index 3355750..fe1f962 100644 --- a/Classes/Services/CloudinaryImageService.php +++ b/Classes/Services/CloudinaryImageService.php @@ -30,7 +30,6 @@ class CloudinaryImageService extends AbstractCloudinaryMediaService protected array $defaultOptions = [ 'type' => 'upload', - 'resource_type' => 'image', 'fetch_format' => 'auto', 'quality' => 'auto', ]; @@ -43,6 +42,7 @@ public function __construct() public function getExplicitData(File $file, array $options): array { $publicId = $this->getPublicIdForFile($file); + $options['resource_type'] = $this->getResourceTypeForFile($file); $explicitData = $this->explicitDataCacheRepository->findByStorageAndPublicIdAndOptions($file->getStorage()->getUid(), $publicId, $options)['explicit_data']; From c3cc2085739147408695e9ba7e66ba71cd81ff6e Mon Sep 17 00:00:00 2001 From: Hannes Lau Date: Wed, 10 Apr 2024 21:59:06 +0200 Subject: [PATCH 99/99] [FEATURE] Offer image processing via cloudinary --- .../BeforeFileProcessingEventHandler.php | 33 +++-- .../Exceptions/NoCloudinaryTransformation.php | 8 ++ Classes/Services/ProcessingTaskConverter.php | 116 ++++++++++++++++++ 3 files changed, 149 insertions(+), 8 deletions(-) create mode 100644 Classes/Exceptions/NoCloudinaryTransformation.php create mode 100644 Classes/Services/ProcessingTaskConverter.php diff --git a/Classes/EventHandlers/BeforeFileProcessingEventHandler.php b/Classes/EventHandlers/BeforeFileProcessingEventHandler.php index c919b39..c020369 100644 --- a/Classes/EventHandlers/BeforeFileProcessingEventHandler.php +++ b/Classes/EventHandlers/BeforeFileProcessingEventHandler.php @@ -9,6 +9,8 @@ * LICENSE.md file that was distributed with this source code. */ +use Cloudinary\Transformation\BaseAction; +use Cloudinary\Transformation\Format; use TYPO3\CMS\Core\Resource\Event\BeforeFileProcessingEvent; use TYPO3\CMS\Core\Resource\File; use TYPO3\CMS\Core\Resource\ProcessedFile; @@ -16,10 +18,16 @@ use TYPO3\CMS\Core\Utility\GeneralUtility; use Visol\Cloudinary\Driver\CloudinaryDriver; use Visol\Cloudinary\Exceptions\InvalidResourceUrlException; +use Visol\Cloudinary\Exceptions\NoCloudinaryTransformation; use Visol\Cloudinary\Services\CloudinaryImageService; +use Visol\Cloudinary\Services\ProcessingTaskConverter; final class BeforeFileProcessingEventHandler { + public function __construct( + protected ProcessingTaskConverter $processingInstructionConverter, + ) { } + public const ALLOWED_TASKS = [ ProcessedFile::CONTEXT_IMAGECROPSCALEMASK, ProcessedFile::CONTEXT_IMAGEPREVIEW, @@ -46,19 +54,28 @@ public function __invoke(BeforeFileProcessingEvent $event): void /** @var File $file */ $file = $event->getFile(); - $eagerOptions = [ + try { + $transformations = $this->processingInstructionConverter->convertProcessingConfiguration($event->getProcessedFile()->getTask()); + } catch (NoCloudinaryTransformation $e) { + // Cloudinary is unable to do the required processing + return; + } + + if ($file->getType() === $file::FILETYPE_VIDEO) { + $firstTransformation = $transformations[0]; + if ($firstTransformation instanceof BaseAction) { + $firstTransformation->addQualifiers(new Format('png')); + } + } + + $transformations[] = [ 'fetch_format' => 'auto', 'quality' => 'auto:eco', - 'width' => 64, - 'height' => 64, - 'crop' => 'fit', ]; - if ($file->getType() === $file::FILETYPE_VIDEO) { - $eagerOptions['format'] = 'png'; - } + $explicitData = $this->getCloudinaryImageService()->getExplicitData($file, [ 'type' => 'upload', - 'eager' => [$eagerOptions] + 'eager' => [$transformations], ]); $url = $explicitData['eager'][0]['secure_url'] ?? null; diff --git a/Classes/Exceptions/NoCloudinaryTransformation.php b/Classes/Exceptions/NoCloudinaryTransformation.php new file mode 100644 index 0000000..bb51f84 --- /dev/null +++ b/Classes/Exceptions/NoCloudinaryTransformation.php @@ -0,0 +1,8 @@ +getConfiguration(); + + if (isset($processingConfiguration['maskImages'])) { + throw new NoCloudinaryTransformation('Unable to maskImages', 1712760855588); + } + + $transformations = []; + + $cropTransformation = $this->getCropTransformation($task); + if (isset($cropTransformation)) { + $transformations[] = $cropTransformation; + } + + $scaleTransformation = $this->getScaleTransformation($task); + if (isset($scaleTransformation)) { + $transformations[] = $scaleTransformation; + } + + return $transformations; + } + + protected function getCropTransformation(TaskInterface $task): ?Crop + { + $processingConfiguration = $task->getConfiguration(); + $crop = $processingConfiguration['crop'] ?? null; + if (!isset($crop)) { + return null; + } + + if (!$crop instanceof Area) { + throw new NoCloudinaryTransformation('Crop is not of type area', 1712760863923); + } + + return new Crop( + CropMode::CROP, + (int)$crop->getWidth(), + (int)$crop->getHeight(), + null, + (int)$crop->getOffsetLeft(), + (int)$crop->getOffsetTop(), + ); + } + + protected function getScaleTransformation(TaskInterface $task): ?Scale + { + $processingConfiguration = $task->getConfiguration(); + + $setProperties = array_filter(array_intersect_key($processingConfiguration, array_flip(['width', 'height']))); + $maxProperties = array_filter(array_intersect_key($processingConfiguration, array_flip(['maxWidth', 'maxHeight']))); + $minProperties = array_filter(array_intersect_key($processingConfiguration, array_flip(['minWidth', 'minHeight']))); + + if (!empty($minProperties)) { + throw new NoCloudinaryTransformation('min* is not yet supported', 1712780611488); + } + if (empty(array_merge($maxProperties, $setProperties))) { + return null; + } + + foreach ($maxProperties as $propertyName => $value) { + $equivalentSetPropertyValue = sprintf('%sm', (int)$value); + $equivalentSetPropertyName = strtolower(str_replace('max', '', $propertyName)); + if (($setProperties[$equivalentSetPropertyName] ?? '') === $equivalentSetPropertyValue) { + unset($setProperties[$equivalentSetPropertyName]); + } + } + + if (!(empty($setProperties) || empty($maxProperties))) { + throw new NoCloudinaryTransformation('max* and width/height at the same time is not yet supported', 1712780615883); + } + + if (!empty($maxProperties)) { + $scaleTransformation = [ + 'crop' => count($maxProperties) >= 2 ? CropMode::FIT : CropMode:: FILL, + ]; + foreach ($maxProperties as $key => $value) { + $scaleTransformation[strtolower(str_replace('max', '', $key))] = (int)$value; + } + + return Scale::fromParams($scaleTransformation); + } + + $scaleTransformation = [ + 'crop' => CropMode::SCALE, + ]; + + foreach (['width', 'height'] as $property) { + $stringValue = $processingConfiguration[$property] ?? ''; + preg_match('/^(\d*)(c)?/', $stringValue, $matches); + $pixels = $matches[1] ?? 0; + if ((int)$pixels > 0) { + $scaleTransformation[$property] = $pixels; + } + if ($matches[2] === 'c') { + $scaleTransformation['crop'] = CropMode::FILL; + } + } + + return Scale::fromParams($scaleTransformation); + } +} \ No newline at end of file