diff --git a/.gitignore b/.gitignore index 7595791..a010026 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ .prettierrc package.json yarn.lock +composer.lock +/public/* +/vendor/* diff --git a/Classes/Backend/Form/Container/InlineCloudinaryControlContainer.php b/Classes/Backend/Form/Container/InlineCloudinaryControlContainer.php index 5d4cac1..0e9dbd5 100644 --- a/Classes/Backend/Form/Container/InlineCloudinaryControlContainer.php +++ b/Classes/Backend/Form/Container/InlineCloudinaryControlContainer.php @@ -12,14 +12,14 @@ 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 { - 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); @@ -107,7 +103,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/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/Command/AbstractCloudinaryCommand.php b/Classes/Command/AbstractCloudinaryCommand.php index 6a309fc..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) @@ -110,13 +95,9 @@ protected function getFiles(ResourceStorage $storage, InputInterface $input): ar } } - return $query->execute()->fetchAll(); + 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..a56d8dc 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; @@ -41,15 +43,12 @@ function ($className) { } ); -/** - * Class CloudinaryAcceptanceTestCommand - */ 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 @@ -66,21 +65,11 @@ protected function configure() ); } - /** - * @param InputInterface $input - * @param OutputInterface $output - */ - protected function initialize(InputInterface $input, OutputInterface $output) + protected function initialize(InputInterface $input, OutputInterface $output): void { $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 +83,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 +107,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 +160,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/CloudinaryApiCommand.php b/Classes/Command/CloudinaryApiCommand.php new file mode 100644 index 0000000..444546f --- /dev/null +++ b/Classes/Command/CloudinaryApiCommand.php @@ -0,0 +1,184 @@ +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 + { + $this->io = new SymfonyStyle($input, $output); + + /** @var ResourceFactory $resourceFactory */ + $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class); + $this->storage = $resourceFactory->getStorageObject($input->getArgument('storage')); + } + + 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('fileUid', '', InputOption::VALUE_OPTIONAL, 'File uid', '') + ->addOption('publicId', '', InputOption::VALUE_OPTIONAL, 'Cloudinary public id', '') + ->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); + } + + 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'); + $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; + } + } + + /** @var int $fileUid */ + $fileUid = $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)); + } elseif ($expression) { + + $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...'); + } + } catch (\Exception $exception) { + $this->error($exception->getMessage()); + } + + return Command::SUCCESS; + } + + protected function getPublicIdFromFile(File $file): string + { + /** @var CloudinaryPathService $cloudinaryPathService */ + $cloudinaryPathService = GeneralUtility::makeInstance( + CloudinaryPathService::class, + $file->getStorage(), + ); + return $cloudinaryPathService->computeCloudinaryPublicId($file->getIdentifier()); + } + + 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/Classes/Command/CloudinaryCopyCommand.php b/Classes/Command/CloudinaryCopyCommand.php index dba3ca2..f9b5b2e 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; @@ -20,31 +21,15 @@ use TYPO3\CMS\Core\Resource\ResourceStorage; use TYPO3\CMS\Core\Utility\GeneralUtility; -/** - * Class CloudinaryCopyCommand - */ 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) + protected function initialize(InputInterface $input, OutputInterface $output): void { $this->io = new SymfonyStyle($input, $output); @@ -59,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) @@ -74,22 +59,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 +87,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if (!$response) { $this->log('Script aborted'); - return 0; + return Command::SUCCESS; } } @@ -148,28 +129,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 +148,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 2b5f1fb..a99a3a3 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; @@ -18,21 +19,13 @@ use TYPO3\CMS\Core\Resource\ResourceStorage; use TYPO3\CMS\Core\Utility\GeneralUtility; -/** - * Class CloudinaryFixJpegCommand - */ class CloudinaryFixJpegCommand extends AbstractCloudinaryCommand { - /** - * @var ResourceStorage - */ - protected $targetStorage; + protected ResourceStorage $targetStorage; - /** - * @param InputInterface $input - * @param OutputInterface $output - */ - protected function initialize(InputInterface $input, OutputInterface $output) + protected string $tableName = 'sys_file'; + + protected function initialize(InputInterface $input, OutputInterface $output): void { $this->io = new SymfonyStyle($input, $output); @@ -46,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) @@ -58,24 +51,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 +78,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if (!$response) { $this->log('Script aborted'); - return 0; + return Command::SUCCESS; } } @@ -106,20 +94,18 @@ 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) ->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/Command/CloudinaryMetadataCommand.php b/Classes/Command/CloudinaryMetadataCommand.php new file mode 100644 index 0000000..b9ba892 --- /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, + ); + } + + protected function configure(): void + { + $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/Classes/Command/CloudinaryMoveCommand.php b/Classes/Command/CloudinaryMoveCommand.php index bea50ea..a5bb353 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; @@ -22,40 +23,22 @@ use TYPO3\CMS\Core\Resource\ResourceFactory; use TYPO3\CMS\Core\Utility\GeneralUtility; -/** - * Class CloudinaryMoveCommand - */ 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 */ - 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) @@ -71,11 +54,7 @@ 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) + protected function initialize(InputInterface $input, OutputInterface $output): void { $this->io = new SymfonyStyle($input, $output); @@ -88,26 +67,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 +95,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int if (!$response) { $this->log('Script aborted'); - return 0; + + return Command::SUCCESS; } } @@ -194,14 +166,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 +186,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..7841cab 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; @@ -21,40 +22,11 @@ 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 { - /** - * @var ResourceStorage - */ - protected $storage; + protected ResourceStorage $storage; - /** - * @param InputInterface $input - * @param OutputInterface $output - */ - protected function initialize(InputInterface $input, OutputInterface $output) + protected function initialize(InputInterface $input, OutputInterface $output): void { $this->io = new SymfonyStyle($input, $output); @@ -64,10 +36,31 @@ 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 [0-9] --path=/foo/ + +# List of files withing a folder with recursive flag +typo3 cloudinary:query [0-9] --path=/foo/ --recursive + +# List of files withing a folder with filter flag +typo3 cloudinary:query [0-9] --path=/foo/ --filter=\'[0-9,a-z]\.jpg\' + + # Count files / folder +typo3 cloudinary:query [0-9] --count + + # List of folders instead of files +typo3 cloudinary:query [0-9] --folder + ' ; + /** * 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) @@ -79,18 +72,14 @@ 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); } - /** - * @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 +130,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 +144,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 +154,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 +161,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 +168,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 41abbc1..f11efb2 100644 --- a/Classes/Command/CloudinaryScanCommand.php +++ b/Classes/Command/CloudinaryScanCommand.php @@ -9,31 +9,42 @@ * 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; use Visol\Cloudinary\Services\CloudinaryScanService; -/** - * Class CloudinaryScanCommand - */ class CloudinaryScanCommand extends AbstractCloudinaryCommand { - /** - * @var ResourceStorage - */ protected ResourceStorage $storage; - /** - * @param InputInterface $input - * @param OutputInterface $output - */ - protected function initialize(InputInterface $input, OutputInterface $output) + 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); @@ -42,48 +53,45 @@ 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() + 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( - 'empty', - 'e', + 'expression', + '', InputOption::VALUE_OPTIONAL, - 'Before scanning empty all resources for a given storage', - false, + '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]'); + ->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 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()->empty(); + 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(); + /** @var string $expression */ + $expression = $input->getOption('expression'); + + $result = $this->getCloudinaryScanService() + ->setAdditionalExpression($expression) + ->scan(); $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']], ); } @@ -99,7 +107,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $result['folder_deleted'], ]); - return 0; + return Command::SUCCESS; } protected function getCloudinaryScanService(): CloudinaryScanService diff --git a/Classes/Controller/CloudinaryWebHookController.php b/Classes/Controller/CloudinaryWebHookController.php new file mode 100644 index 0000000..20cf259 --- /dev/null +++ b/Classes/Controller/CloudinaryWebHookController.php @@ -0,0 +1,413 @@ +checkEnvironment(); + + /** @var ResourceFactory $resourceFactory */ + $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, + ); + + $this->scanService = GeneralUtility::makeInstance( + CloudinaryScanService::class, + $storage + ); + + $this->cloudinaryPathService = GeneralUtility::makeInstance( + CloudinaryPathService::class, + $storage + ); + + $this->storage = $storage; + + $this->processedFileRepository = GeneralUtility::makeInstance(ProcessedFileRepository::class); + + $this->packageManager = GeneralUtility::makeInstance(PackageManager::class); + } + + public function processAction(): ResponseInterface + { + $parsedBody = (string)file_get_contents('php://input'); + $payload = (array)json_decode($parsedBody, true); + self::getLogger()->debug($parsedBody); + + if ($this->shouldStopProcessing($payload)) { + return $this->sendResponse(['result' => true, '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(); + + foreach ($publicIds as $publicId) { + + if ($requestType === self::NOTIFICATION_TYPE_DELETE) { + 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); + } + + // early return + return $this->sendResponse(['result' => true, '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 + /** @var string $nextPublicId */ + $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); + + // #. flush the process files + $this->clearProcessedFiles($file); + + // #. clean up local temporary file - var/variant folder + $this->cleanUpTemporaryFile($file); + + // #. flush cache pages + $clearCachePages = $this->clearCachePages($file); + } + } catch (\Exception $e) { + return $this->sendResponse([ + 'result' => false, + 'message' => $e->getMessage(), + ]); + } + + $message = $clearCachePages + ? 'Success! Cache flushed for pages ' . implode(',', $clearCachePages) + : 'Success! Job done'; + return $this->sendResponse(['result' => true, 'message' => $message]); + } + + protected function flushCloudinaryCdn(string $publicId): void + { + // Invalidate CDN cache + \Cloudinary\Uploader::explicit( + $publicId, + [ + 'type' => 'upload', + 'invalidate' => true + ] + ); + } + + 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) + ->where( + $q->expr()->eq('storage', $this->storage->getUid()), + $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); + $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 (!$fileRecord) { + throw new Exception('No indexed file could be fine for public id ' . $cloudinaryResource['public_id']); + } + + $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class); + return $resourceFactory->getFileObject($fileRecord['uid']); + } + + 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']]; + } 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); + + foreach ($processedFiles as $processedFile) { + $processedFile->getStorage()->setEvaluatePermissions(false); + $processedFile->delete(); + } + } + + protected function cleanUpTemporaryFile(File $file): void + { + $temporaryFileNameAndPath = CloudinaryFileUtility::getTemporaryFile($file->getStorage()->getUid(), $file->getIdentifier()); + if (is_file($temporaryFileNameAndPath)) { + self::getLogger()->debug($temporaryFileNameAndPath); + unlink($temporaryFileNameAndPath); + } + } + + protected function clearCachePages(File $file): array + { + $tags = []; + foreach ($this->findPagesWithFileReferences($file) as $page) { + $tags[$page['pid']] = 'pageId_' . $page['pid']; + } + + GeneralUtility::makeInstance(CacheManager::class) + ->flushCachesInGroupByTags('pages', $tags); + + $this->eventDispatcher->dispatch( + new ClearCachePageEvent($tags) + ); + + return array_keys($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') + //->groupBy('pid') // no support for distinct + ->andWhere( + 'pid > 0', + 'uid_local = ' . $file->getUid() + ) + ->execute() + ->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 + * 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) || + $this->isRequestDelete($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( + (string)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(string $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; + // @phpstan-ignore-next-line + if ($logger === null) { + /** @var LogManager $logger */ + $logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__); + } + return $logger; + } + + protected function initializeApi(): void + { + CloudinaryApiUtility::initializeByConfiguration($this->storage->getConfiguration()); + } + +} 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; diff --git a/Classes/Driver/CloudinaryDriver.php b/Classes/Driver/CloudinaryDriver.php index 127570c..e79954f 100644 --- a/Classes/Driver/CloudinaryDriver.php +++ b/Classes/Driver/CloudinaryDriver.php @@ -8,116 +8,66 @@ * 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\Charset\CharsetConverter; -use TYPO3\CMS\Core\Log\Logger; use TYPO3\CMS\Core\Resource\Exception\InvalidFileNameException; -use Cloudinary\Api\NotFound; -use TYPO3\CMS\Core\Messaging\FlashMessageQueue; +use Cloudinary; use Cloudinary\Api; -use Cloudinary\Search; 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 Visol\Cloudinary\Cache\CloudinaryTypo3Cache; use TYPO3\CMS\Core\Log\LogLevel; use TYPO3\CMS\Core\Log\LogManager; -use TYPO3\CMS\Core\Messaging\FlashMessage; -use TYPO3\CMS\Core\Messaging\FlashMessageService; 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 TYPO3\CMS\Extbase\Object\ObjectManager; -use TYPO3\CMS\Extbase\SignalSlot\Dispatcher; -use TYPO3\CMS\Extbase\Utility\LocalizationUtility; +use Visol\Cloudinary\Domain\Repository\ExplicitDataCacheRepository; +use Visol\Cloudinary\Services\CloudinaryFolderService; +use Visol\Cloudinary\Services\CloudinaryResourceService; use Visol\Cloudinary\Services\CloudinaryPathService; -use Visol\Cloudinary\Utility\CloudinaryApiUtility; +use Visol\Cloudinary\Services\CloudinaryTestConnectionService; +use Visol\Cloudinary\Services\ConfigurationService; +use Visol\Cloudinary\Utility\CloudinaryFileUtility; +use Visol\Cloudinary\Utility\MimeTypeUtility; -/** - * Class CloudinaryDriver - * - * @obsolete - */ 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'; - /** - * 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 const ROOT_FOLDER_IDENTIFIER = '/'; - /** - * @var array[] - */ - protected $cachedCloudinaryResources = []; + protected const UNSAFE_FILENAME_CHARACTER_EXPRESSION = '\\x00-\\x2C\\/\\x3A-\\x3F\\x5B-\\x60\\x7B-\\xBF'; + + static public array $knownRawFormats = ['youtube', 'vimeo']; /** - * @var array + * 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 */ - protected $cachedFolders = []; + protected string $baseUrl = ''; /** * Object permissions are cached here in subarrays like: * $identifier => ['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; + protected array $cachedPermissions = []; - /** - * @var string - */ - protected $languageFile = 'LLL:EXT:cloudinary/Resources/Private/Language/backend.xlf'; + protected ConfigurationService $configurationService; - /** - * @var Dispatcher - */ - protected $signalSlotDispatcher; + protected CharsetConverter $charsetConversion; - /** - * @var Api $api - */ - protected $api; + protected ?CloudinaryPathService $cloudinaryPathService = null; - /** - * @var CloudinaryTypo3Cache - */ - protected $cloudinaryTypo3Cache; + protected ?CloudinaryResourceService $cloudinaryResourceService = null; - /** - * @var CloudinaryPathService - */ - protected $cloudinaryPathService; + protected ?CloudinaryFolderService $cloudinaryFolderService = null; - /** - * @param array $configuration - */ public function __construct(array $configuration = []) { $this->configuration = $configuration; @@ -128,44 +78,45 @@ public function __construct(array $configuration = []) ResourceStorage::CAPABILITY_BROWSABLE | ResourceStorage::CAPABILITY_PUBLIC | ResourceStorage::CAPABILITY_WRITABLE; + + $this->configurationService = GeneralUtility::makeInstance(ConfigurationService::class, $this->configuration); + + $this->charsetConversion = GeneralUtility::makeInstance(CharsetConverter::class); } - /** - * @return void - */ - public function processConfiguration() + 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 (ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isBackend() && !empty($_GET['edit']['sys_file_storage'])) { - $this->testConnection(); + if ( + !Environment::isCli() && + ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isBackend() && + !empty($_GET['edit']['sys_file_storage']) + ) { + $this->getCloudinaryTestConnectionService()->test(); } } /** - * @param string $fileIdentifier - * - * @return string + * @param string $identifier */ - public function getPublicUrl($fileIdentifier) + public function getPublicUrl($identifier): string { - return $this->resourceExists($fileIdentifier) - ? $this->getCachedCloudinaryResource($fileIdentifier)['secure_url'] - : ''; + if ($processedPath = $this->computeProcessedPath($identifier)) { + return 'https://res.cloudinary.com/' . $processedPath; + } + + $cloudinaryResource = $this->getCloudinaryResourceService()->getResource( + $this->getCloudinaryPathService()->computeCloudinaryPublicId($identifier), + ); + + 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,88 +128,49 @@ 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; } /** - * 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 { - $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 ($this->isProcessedFile($fileIdentifier)) { + return []; + } + $publicId = $this->getCloudinaryPathService()->computeCloudinaryPublicId($fileIdentifier); + $cloudinaryResource = $this->getCloudinaryResourceService()->getResource($publicId); + // We have a problem Hudson! 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, - ); - } + 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 = [ + return [ 'identifier_hash' => $this->hashIdentifier($fileIdentifier), - 'folder_hash' => sha1($canonicalFolderIdentifier), + 'folder_hash' => sha1($this->canonicalizeAndCheckFolderIdentifier(PathUtility::dirname($fileIdentifier))), 'creation_date' => strtotime($cloudinaryResource['created_at']), 'modification_date' => strtotime($cloudinaryResource['created_at']), - 'mime_type' => $mimeType, - 'extension' => $extension, + 'mime_type' => MimeTypeUtility::guessMimeType($cloudinaryResource['format']), + 'extension' => $this->getResourceInfo($cloudinaryResource, 'format'), 'size' => $this->getResourceInfo($cloudinaryResource, 'bytes'), 'width' => $this->getResourceInfo($cloudinaryResource, 'width'), 'height' => $this->getResourceInfo($cloudinaryResource, 'height'), @@ -266,89 +178,74 @@ public function getFileInfoByIdentifier($fileIdentifier, array $propertiesToExtr '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 + * @param string $fileIdentifier */ - public function fileExists($identifier) + public function fileExists($fileIdentifier): bool { - if (substr($identifier, -1) === DIRECTORY_SEPARATOR || $identifier === '') { - return false; + // Early return in case we have a processed file. + if ($this->isProcessedFile($fileIdentifier)) { + return true; } - return $this->resourceExists($identifier); + + $cloudinaryResource = $this->getCloudinaryResourceService()->getResource( + $this->getCloudinaryPathService()->computeCloudinaryPublicId($fileIdentifier), + ); + + return !empty($cloudinaryResource); } /** - * Checks if a folder exists - * * @param string $folderIdentifier - * - * @return bool */ - public function folderExists($folderIdentifier) + public function folderExists($folderIdentifier): bool { - try { - // Will trigger an exception if the folder identifier does not exist. - $subFolders = $this->getFoldersInFolder($folderIdentifier); - } catch (\Exception $e) { - return false; + // Early return in case we have a processed file. + if ($this->isProcessedFolder($folderIdentifier)) { + return true; + } + + if ($folderIdentifier === self::ROOT_FOLDER_IDENTIFIER) { + return true; } - return is_array($subFolders); + $cloudinaryFolder = $this->getCloudinaryFolderService()->getFolder( + $this->getCloudinaryPathService()->computeCloudinaryFolderPath($folderIdentifier), + ); + return !empty($cloudinaryFolder); } /** * @param string $fileName * @param string $folderIdentifier - * - * @return bool */ - public function fileExistsInFolder($fileName, $folderIdentifier) + public function fileExistsInFolder($fileName, $folderIdentifier): bool { - $fileIdentifier = $folderIdentifier . $fileName; - return $this->resourceExists($fileIdentifier); + $fileIdentifier = $this->canonicalizeFolderIdentifierAndFileName($folderIdentifier, $fileName); + + return $this->fileExists($fileIdentifier); } /** - * 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 { - $canonicalFolderPath = $this->canonicalizeAndCheckFolderIdentifierAndFolderName($folderIdentifier, $folderName); - return $this->folderExists($canonicalFolderPath); + 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; } @@ -359,26 +256,21 @@ 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): string { $fileName = $this->sanitizeFileName($newFileName !== '' ? $newFileName : PathUtility::basename($localFilePath)); - $fileIdentifier = $this->canonicalizeAndCheckFileIdentifier( - $this->canonicalizeAndCheckFolderIdentifier($targetFolderIdentifier) . $fileName, - ); + $fileIdentifier = $this->canonicalizeFolderIdentifierAndFileName($targetFolderIdentifier, $fileName); - // Necessary to happen in an early stage. - $this->log('[CACHE] Flushed as adding file', [], ['addFile']); - $this->flushFileCache(); + // We remove a possible existing transient file to avoid bad surprise. + $this->cleanUpTemporaryFile($fileIdentifier); + // We compute the cloudinary public id $cloudinaryPublicId = $this->getCloudinaryPathService()->computeCloudinaryPublicId($fileIdentifier); $this->log( - '[API][UPLOAD] Cloudinary\Uploader::upload() - add resource "%s"', + '[API] Cloudinary\Uploader::upload() - add resource "%s"', [$cloudinaryPublicId], ['addFile()'], ); @@ -387,16 +279,17 @@ public function addFile($localFilePath, $targetFolderIdentifier, $newFileName = $this->initializeApi(); // Upload the file - $resource = Uploader::upload($localFilePath, [ + $cloudinaryResource = 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); - } + $this->checkCloudinaryUploadStatus($cloudinaryResource, $fileIdentifier); + + // We persist the uploaded resource. + $this->getCloudinaryResourceService()->save($cloudinaryResource); return $fileIdentifier; } @@ -405,103 +298,84 @@ 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 { - // Flush the file cache entries - $this->log('[CACHE] Flushed as copying file', [], ['copyFileWithinStorage']); - $this->flushFileCache(); + $targetFileIdentifier = $this->canonicalizeFolderIdentifierAndFileName($targetFolderIdentifier, $fileName); // Before calling API, make sure we are connected with the right "bucket" $this->initializeApi(); - Uploader::upload($this->getPublicUrl($fileIdentifier), [ + $cloudinaryResource = Uploader::upload($this->getPublicUrl($fileIdentifier), [ 'public_id' => PathUtility::basename( - $this->getCloudinaryPathService()->computeCloudinaryPublicId($fileName), + $this->getCloudinaryPathService()->computeCloudinaryPublicId($targetFileIdentifier), ), 'folder' => $this->getCloudinaryPathService()->computeCloudinaryFolderPath($targetFolderIdentifier), 'resource_type' => $this->getCloudinaryPathService()->getResourceType($fileIdentifier), 'overwrite' => true, ]); - $targetIdentifier = $targetFolderIdentifier . $fileName; - return $targetIdentifier; + $this->checkCloudinaryUploadStatus($cloudinaryResource, $fileIdentifier); + + // We persist the uploaded resource + $this->getCloudinaryResourceService()->save($cloudinaryResource); + + return $targetFileIdentifier; } /** - * 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); + $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); + $cloudinaryResource = Uploader::upload($localFilePath, [ + 'public_id' => PathUtility::basename($cloudinaryPublicId), + 'folder' => $this->getCloudinaryPathService()->computeCloudinaryFolderPath( + PathUtility::dirname($fileIdentifier), + ), + 'resource_type' => $this->getCloudinaryPathService()->getResourceType($fileIdentifier), + 'overwrite' => true, + ]); + + $this->checkCloudinaryUploadStatus($cloudinaryResource, $fileIdentifier); + + // We persist the uploaded resource. + $this->getCloudinaryResourceService()->save($cloudinaryResource); 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) + public function deleteFile($fileIdentifier): bool { - // 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"', + '[API] Cloudinary\Api::delete_resources - delete resource "%s"', [$cloudinaryPublicId], ['deleteFile'], ); @@ -510,47 +384,53 @@ public function deleteFile($fileIdentifier) 'resource_type' => $this->getCloudinaryPathService()->getResourceType($fileIdentifier), ]); - $key = is_array($response['deleted']) ? key($response['deleted']) : ''; + $isDeleted = false; + + foreach ($response['deleted'] as $publicId => $status) { + if ($status === 'deleted') { + $isDeleted = (bool)$this->getCloudinaryResourceService()->delete($publicId); + } + } - return is_array($response['deleted']) && - isset($response['deleted'][$key]) && - $response['deleted'][$key] === 'deleted'; + return $isDeleted; } /** - * 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); + 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'], ); - $this->getApi()->delete_resources_by_prefix($cloudinaryFolder); + $response = $this->getApi()->delete_resources_by_prefix($cloudinaryFolder); + + foreach ($response['deleted'] as $publicId => $status) { + if ($status === 'deleted') { + $this->getCloudinaryResourceService()->delete($publicId); + } + } } // 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'], ); - $this->getApi()->delete_folder($cloudinaryFolder); - } + $response = $this->getApi()->delete_folder($cloudinaryFolder); - // Flush the folder cache entries - $this->log('[CACHE][FOLDER] Flushed as deleting folder', [], ['deleteFolder']); - $this->flushFolderCache(); + foreach ($response['deleted'] as $folder) { + $this->getCloudinaryFolderService()->delete($folder); + } + } return true; } @@ -558,10 +438,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); @@ -572,73 +450,53 @@ public function getFileForLocalProcessing($fileIdentifier, $writable = true) ['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, - ); - } - + file_put_contents($temporaryPath, file_get_contents($this->getPublicUrl($fileIdentifier))); $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) + public function createFile($fileName, $parentFolderIdentifier): string { - throw new \RuntimeException( + 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) + public function createFolder($newFolderName, $parentFolderIdentifier = '', $recursive = false): string { - $canonicalFolderPath = $this->canonicalizeAndCheckFolderIdentifierAndFolderName( + $canonicalFolderPath = $this->canonicalizeFolderIdentifierAndFolderName( $parentFolderIdentifier, $newFolderName, ); - $cloudinaryFolder = $this->getCloudinaryPathService()->normalizeCloudinaryPath($canonicalFolderPath); + $cloudinaryFolder = $this->getCloudinaryPathService()->normalizeCloudinaryPublicId($canonicalFolderPath); - $this->log('[API][CREATE] Cloudinary\Api::createFolder() - folder "%s"', [$cloudinaryFolder], ['createFolder']); - $this->getApi()->create_folder($cloudinaryFolder); + $this->log('[API] Cloudinary\Api::createFolder() - folder "%s"', [$cloudinaryFolder], ['createFolder']); + $response = $this->getApi()->create_folder($cloudinaryFolder); - // Flush the folder cache entries - $this->log('[CACHE][FOLDER] Flushed as creating folder', [], ['createFolder']); - $this->flushFolderCache(); + if (!$response['success']) { + throw new \Exception('Folder creation failed: ' . $cloudinaryFolder, 1591775050); + } + $this->getCloudinaryFolderService()->save($cloudinaryFolder); return $canonicalFolderPath; } /** * @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); @@ -650,23 +508,17 @@ public function getFileContents($fileIdentifier) * * @param string $fileIdentifier * @param string $contents - * - * @return int */ public function setFileContents($fileIdentifier, $contents) { - throw new \RuntimeException('setFileContents: not implemented action!', 1570728106); + 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) + public function renameFile($fileIdentifier, $newFileIdentifier): string { if (!$this->isFileIdentifier($newFileIdentifier)) { $sanitizedFileName = $this->sanitizeFileName(PathUtility::basename($newFileIdentifier)); @@ -680,70 +532,72 @@ public function renameFile($fileIdentifier, $newFileIdentifier) $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, [ + $cloudinaryResource = Uploader::rename($cloudinaryPublicId, $newCloudinaryPublicId, [ 'resource_type' => $this->getCloudinaryPathService()->getResourceType($fileIdentifier), + 'overwrite' => true, ]); + + $this->checkCloudinaryUploadStatus($cloudinaryResource, $fileIdentifier); + + // We remove the old public id + $this->getCloudinaryResourceService()->delete($cloudinaryPublicId); + + // ... and insert the new cloudinary resource + $this->getCloudinaryResourceService()->save($cloudinaryResource); } return $newFileIdentifier; } /** - * Renames a folder in this storage. - * + * @param array $cloudinaryResource + * @param string $fileIdentifier + */ + protected function checkCloudinaryUploadStatus(array $cloudinaryResource, $fileIdentifier): void + { + if (!$cloudinaryResource && $cloudinaryResource['type'] !== 'upload') { + throw new RuntimeException('Cloudinary upload failed for ' . $fileIdentifier, 1591954950); + } + } + + /** * @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 = []; - 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; + $pathSegments = GeneralUtility::trimExplode('/', $folderIdentifier); + $numberOfSegments = count($pathSegments); + + if ($numberOfSegments > 1) { + // Replace last folder name by the new folder name + $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) { + $newFileIdentifier = str_replace($folderIdentifier, $newFolderIdentifier, $oldFileIdentifier); + + if ($oldFileIdentifier !== $newFileIdentifier) { + $renamedFiles[$oldFileIdentifier] = $this->renameFile($oldFileIdentifier, $newFileIdentifier); } } - } - // After working so hard, delete the old empty folder. - $this->deleteFolder($folderIdentifier); + // After working so hard, delete the old empty folder. + $this->deleteFolder($folderIdentifier); + } return $renamedFiles; } @@ -755,10 +609,14 @@ 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 = $targetFolderIdentifier . $newFolderName . DIRECTORY_SEPARATOR; + $newTargetFolderIdentifier = $this->canonicalizeFolderIdentifierAndFolderName( + $targetFolderIdentifier, + $newFolderName, + ); + if (!$this->folderExists($newTargetFolderIdentifier)) { $this->createFolder($newTargetFolderIdentifier); } @@ -783,13 +641,11 @@ 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->canonicalizeAndCheckFolderIdentifierAndFolderName( + $newTargetFolderIdentifier = $this->canonicalizeFolderIdentifierAndFolderName( $targetFolderIdentifier, $newFolderName, ); @@ -798,11 +654,13 @@ public function copyFolderWithinStorage($sourceFolderIdentifier, $targetFolderId $this->createFolder($newTargetFolderIdentifier); } - $files = $this->getFilesInFolder($sourceFolderIdentifier, 0, -1); + $files = $this->getFilesInFolder($sourceFolderIdentifier, 0, -1, true); foreach ($files as $fileIdentifier) { + $newFileIdentifier = str_replace($sourceFolderIdentifier, $newTargetFolderIdentifier, $fileIdentifier); + $this->copyFileWithinStorage( $fileIdentifier, - $newTargetFolderIdentifier, + GeneralUtility::dirname($newFileIdentifier), PathUtility::basename($fileIdentifier), ); } @@ -814,42 +672,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 { - $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']); + 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. - * 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) + public function isWithin($folderIdentifier, $identifier): bool { $folderIdentifier = $this->canonicalizeAndCheckFileIdentifier($folderIdentifier); $fileIdentifier = $this->canonicalizeAndCheckFileIdentifier($identifier); @@ -867,215 +700,214 @@ 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 [ 'identifier' => $canonicalFolderIdentifier, 'name' => PathUtility::basename( - $this->getCloudinaryPathService()->normalizeCloudinaryPath($canonicalFolderIdentifier), + $this->getCloudinaryPathService()->normalizeCloudinaryPublicId($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) + 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 * @param bool $recursive - * @param array $filenameFilterCallbacks callbacks for filtering the items + * @param array $filterCallbacks 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 = [], + array $filterCallbacks = [], $sort = '', $sortRev = false - ) { - if ($folderIdentifier === '') { - throw new \RuntimeException( - 'Something went wrong in method "getFilesInFolder"! $folderIdentifier can not be empty', - 1574754623, - ); + ): array + { + $cloudinaryFolder = $this->getCloudinaryPathService()->computeCloudinaryFolderPath( + $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier), + ); + + // 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'; } - if (!isset($this->cachedCloudinaryResources[$folderIdentifier])) { - // Try to fetch from the cache - $this->cachedCloudinaryResources[$folderIdentifier] = $this->getCache()->getCachedFiles($folderIdentifier); + $orderings = [ + 'fieldName' => $parameters['sort'], + 'direction' => isset($parameters['reverse']) && (int)$parameters['reverse'] ? 'DESC' : 'ASC', + ]; - // If not found in TYPO3 cache, ask Cloudinary - if (!is_array($this->cachedCloudinaryResources[$folderIdentifier])) { - $this->cachedCloudinaryResources[$folderIdentifier] = $this->getCloudinaryResources($folderIdentifier); - } - } + $pagination = [ + 'maxResult' => $numberOfItems, + 'firstResult' => (int)GeneralUtility::_GP('pointer'), + ]; - // Set default sorting - $parameters = (array) GeneralUtility::_GP('SET'); - if (empty($parameters)) { - $parameters['sort'] = 'file'; - $parameters['reverse'] = 0; - } + $cloudinaryResources = $this->getCloudinaryResourceService()->getResources( + $cloudinaryFolder, + $orderings, + $pagination, + $recursive, + ); - // 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', - ); - } - } + // Generate list of folders for the file module. + $files = []; + foreach ($cloudinaryResources as $cloudinaryResource) { + // Compute file identifier + $fileIdentifier = $this->canonicalizeAndCheckFileIdentifier( + $this->getCloudinaryPathService()->computeFileIdentifier($cloudinaryResource), + ); - // Pagination - if ($numberOfItems > 0) { - $files = array_slice( - $this->cachedCloudinaryResources[$folderIdentifier], - (int) GeneralUtility::_GP('pointer'), - $numberOfItems, + $result = $this->applyFilterMethodsToDirectoryItem( + $filterCallbacks, + basename($fileIdentifier), + $fileIdentifier, + dirname($fileIdentifier), ); - } else { - $files = $this->cachedCloudinaryResources[$folderIdentifier]; + + if ($result) { + $files[] = $fileIdentifier; + } } - return array_keys($files); + return $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 + * @param array $filterCallbacks callbacks for filtering the items */ - public function countFilesInFolder($folderIdentifier, $recursive = false, array $filenameFilterCallbacks = []) + public function countFilesInFolder($folderIdentifier, $recursive = false, array $filterCallbacks = []): int { - if (!isset($this->cachedCloudinaryResources[$folderIdentifier])) { - $this->getFilesInFolder($folderIdentifier, 0, -1, $recursive, $filenameFilterCallbacks); + $folderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier); + + // true means we have non-core filters that has been added and we must filter on the PHP side. + if (count($filterCallbacks) > 1) { + $files = $this->getFilesInFolder($folderIdentifier, 0, 0, $recursive, $filterCallbacks); + $result = count($files); + } else { + $result = $this->getCloudinaryResourceService()->count( + $this->getCloudinaryPathService()->computeCloudinaryFolderPath($folderIdentifier), + $recursive, + ); } - return count($this->cachedCloudinaryResources[$folderIdentifier]); + return $result; } /** - * 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 array $filterCallbacks * @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 = [], + array $filterCallbacks = [], $sort = '', $sortRev = false - ) { - $folderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier); + ): array + { + $parameters = (array)GeneralUtility::_GP('SET'); - if (!isset($this->cachedFolders[$folderIdentifier])) { - // Try to fetch from the cache - $this->cachedFolders[$folderIdentifier] = $this->getCache()->getCachedFolders($folderIdentifier); + $cloudinaryFolder = $this->getCloudinaryPathService()->computeCloudinaryFolderPath( + $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier), + ); - // If not found in TYPO3 cache, ask Cloudinary - if (!is_array($this->cachedFolders[$folderIdentifier])) { - $this->cachedFolders[$folderIdentifier] = $this->getCloudinaryFolders($folderIdentifier); - } - } + $cloudinaryFolders = $this->getCloudinaryFolderService()->getSubFolders( + $cloudinaryFolder, + [ + 'fieldName' => 'folder', + 'direction' => isset($parameters['reverse']) && (int)$parameters['reverse'] ? 'DESC' : 'ASC', + ], + $recursive, + ); + + // Generate list of folders for the file module. + $folders = []; + foreach ($cloudinaryFolders as $cloudinaryFolder) { + $folderIdentifier = $this->getCloudinaryPathService()->computeFolderIdentifier($cloudinaryFolder['folder']); + + $result = $this->applyFilterMethodsToDirectoryItem( + $filterCallbacks, + basename($folderIdentifier), + $folderIdentifier, + dirname($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]); + if ($result) { + $folders[] = $folderIdentifier; + } } - return $this->cachedFolders[$folderIdentifier]; + return $folders; } /** - * 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 + * @param array $filterCallbacks */ - public function countFoldersInFolder($folderIdentifier, $recursive = false, array $folderNameFilterCallbacks = []) + public function countFoldersInFolder($folderIdentifier, $recursive = false, array $filterCallbacks = []): int { - return count($this->getFoldersInFolder($folderIdentifier, 0, -1, $recursive, $folderNameFilterCallbacks)); + // true means we have non-core filters that has been added and we must filter on the PHP side. + if (count($filterCallbacks) > 1) { + $folders = $this->getFoldersInFolder($folderIdentifier, 0, 0, $recursive, $filterCallbacks); + $result = count($folders); + } else { + $cloudinaryFolder = $this->getCloudinaryPathService()->computeCloudinaryFolderPath( + $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier), + ); + + $result = $this->getCloudinaryFolderService()->countSubFolders($cloudinaryFolder, $recursive); + } + + return $result; } /** * @param string $identifier - * - * @return string */ - public function dumpFileContents($identifier) + public function dumpFileContents($identifier): string { return $this->getFileContents($identifier); } @@ -1085,10 +917,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 @@ -1104,10 +934,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; @@ -1120,13 +948,10 @@ 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->getCharsetConversion()->specCharsToASCII('utf-8', $fileName); + $fileName = $this->charsetConversion->specCharsToASCII('utf-8', $fileName); // Replace unwanted characters by underscores $cleanFileName = preg_replace( @@ -1141,316 +966,185 @@ public function sanitizeFileName($fileName, $charset = '') throw new InvalidFileNameException('File name "' . $fileName . '" is invalid.', 1320288991); } + $pathParts = PathUtility::pathinfo($cleanFileName); + + $cleanFileName = + str_replace('.', '_', $pathParts['filename']) . + ($pathParts['extension'] ? '.' . $pathParts['extension'] : ''); + // 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 + * Applies a set of filter methods to a file name to find out if it should be used or not. This is e.g. used by + * directory listings. * - * @return string + * @param array $filterMethods The filter methods to use + * @param string $itemName + * @param string $itemIdentifier + * @param string $parentIdentifier */ - protected function getTemporaryPathForFile($fileIdentifier): string + protected function applyFilterMethodsToDirectoryItem( + array $filterMethods, + $itemName, + $itemIdentifier, + $parentIdentifier + ): bool { - $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); + foreach ($filterMethods as $filter) { + if (is_callable($filter)) { + $result = call_user_func($filter, $itemName, $itemIdentifier, $parentIdentifier, [], $this); + // We have to use -1 as the „don't include“ return value, as call_user_func() will return FALSE + // If calling the method succeeded and thus we can't use that as a return value. + if ($result === -1) { + return false; + } + if ($result === false) { + throw new \RuntimeException( + 'Could not apply file/folder name filter ' . $filter[0] . '::' . $filter[1], + 1596795500, + ); + } + } } - return $temporaryFileNameAndPath; + return true; } /** - * @param string $newFileIdentifier - * - * @return bool + * We want to remove the local temporary file */ - protected function isFileIdentifier(string $newFileIdentifier): bool + protected function cleanUpTemporaryFile(string $fileIdentifier): void { - return false !== strpos($newFileIdentifier, DIRECTORY_SEPARATOR); - } + $temporaryLocalFile = CloudinaryFileUtility::getTemporaryFile($this->storageUid, $fileIdentifier); + if (is_file($temporaryLocalFile)) { + unlink($temporaryLocalFile); + } - /** - * @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), - ); + // very coupled.... via signal slot? + $this->getExplicitDataCacheRepository()->delete($this->storageUid, $fileIdentifier); } - /** - * @param string $folderIdentifier - * - * @return array - * @throws Api\GeneralError - */ - protected function getCloudinaryFolders(string $folderIdentifier): array + public function getExplicitDataCacheRepository(): ExplicitDataCacheRepository { - $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; + return GeneralUtility::makeInstance(ExplicitDataCacheRepository::class); } - /** - * @param string $folderIdentifier - * - * @return array - */ - protected function getCloudinaryResources(string $folderIdentifier): array + protected function getProcessedFilePattern(): string { - $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), - ); + return sprintf('/^PROCESSEDFILE\/(%s\/.*)/', $this->configurationService->get('cloudName')); + } - // Compute folder identifier - #$computedFolderIdentifier = $this->canonicalizeAndCheckFolderIdentifier( - # GeneralUtility::dirname($fileIdentifier) - #); + protected function isProcessedFile(string $identifier): bool + { + return (bool)preg_match($this->getProcessedFilePattern(), $identifier); + } - // 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)); + protected function isProcessedFolder(string $identifier): bool + { + $storageRecord = $this->getStorageObject()->getStorageRecord(); - // Add result into typo3 cache to spare API calls next time... - $this->getCache()->setCachedFiles($folderIdentifier, $cloudinaryResources); + // 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); - return $cloudinaryResources; + // We detect if the identifier start with the value from $folderPath + return str_starts_with($identifier, $folderPath); } - /** - * @param string $fileIdentifier - * - * @return array|null - */ - protected function getCloudinaryResource(string $fileIdentifier) + protected function computeProcessedPath(string $identifier): string|null { - $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; + $cloudinaryPath = null; + if (preg_match($this->getProcessedFilePattern(), $identifier, $matches)) { + [, $cloudinaryPath] = $matches; } - return $cloudinaryResource; + return $cloudinaryPath; } - /** - * @param string $fileIdentifier - * - * @return array|false - */ - protected function getCachedCloudinaryResource(string $fileIdentifier) + protected function isFileIdentifier(string $newFileIdentifier): bool { - $folderIdentifier = $this->canonicalizeAndCheckFolderIdentifier(GeneralUtility::dirname($fileIdentifier)); + return str_contains($newFileIdentifier, DIRECTORY_SEPARATOR); + } - // Warm up the cache! - if (!isset($this->cachedCloudinaryResources[$folderIdentifier][$fileIdentifier])) { - $this->getFilesInFolder($folderIdentifier, 0, -1); - } + protected function canonicalizeFolderIdentifierAndFolderName(string $folderIdentifier, string $folderName): string + { + $canonicalFolderPath = $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier); + return $this->canonicalizeAndCheckFolderIdentifier( + $canonicalFolderPath . trim($folderName, DIRECTORY_SEPARATOR), + ); + } - return isset($this->cachedCloudinaryResources[$folderIdentifier][$fileIdentifier]) - ? $this->cachedCloudinaryResources[$folderIdentifier][$fileIdentifier] - : false; + protected function canonicalizeFolderIdentifierAndFileName(string $folderIdentifier, string $fileName): string + { + return $this->canonicalizeAndCheckFileIdentifier( + $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier) . $fileName, + ); } - /** - * @return CloudinaryPathService - */ - protected function getCloudinaryPathService() + protected function getCloudinaryPathService(): CloudinaryPathService { if (!$this->cloudinaryPathService) { $this->cloudinaryPathService = GeneralUtility::makeInstance( CloudinaryPathService::class, - $this->configuration, + $this->storageUid + ? $this->getStorageObject() + : $this->configuration, ); } return $this->cloudinaryPathService; } - /** - * Test the connection - */ - protected function testConnection() + protected function getStorageObject(): ResourceStorage { - $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); - } + /** @var ResourceFactory $resourceFactory */ + $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class); + return $resourceFactory->getStorageObject($this->storageUid); } - /** - * @return FlashMessageQueue - */ - protected function getMessageQueue() + protected function getCloudinaryResourceService(): CloudinaryResourceService { - /** @var FlashMessageService $flashMessageService */ - $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class); - return $flashMessageService->getMessageQueueByIdentifier(); - } + if (!$this->cloudinaryResourceService) { - /** - * 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()']); + $this->cloudinaryResourceService = GeneralUtility::makeInstance( + CloudinaryResourceService::class, + $this->getStorageObject() + ); } - return !empty($cloudinaryResource); - } - /** - * @return void - */ - protected function flushCache(): void - { - $this->flushFolderCache(); - $this->flushFileCache(); + return $this->cloudinaryResourceService; } - /** - * @return void - */ - protected function flushFileCache(): void + protected function getCloudinaryTestConnectionService(): CloudinaryTestConnectionService { - // Flush the file cache entries - $this->getCache()->flushFileCache(); - - $this->cachedCloudinaryResources = []; + return GeneralUtility::makeInstance(CloudinaryTestConnectionService::class, $this->configuration); } - /** - * @return void - */ - protected function flushFolderCache(): void + protected function getCloudinaryFolderService(): CloudinaryFolderService { - // Flush the file cache entries - $this->getCache()->flushFolderCache(); + if (!$this->cloudinaryFolderService) { + $this->cloudinaryFolderService = GeneralUtility::makeInstance( + CloudinaryFolderService::class, + $this->storageUid, + ); + } - $this->cachedFolders = []; + return $this->cloudinaryFolderService; } - /** - * @return void - */ - protected function initializeApi() + protected function initializeApi(): void { - CloudinaryApiUtility::initializeByConfiguration($this->configuration); + 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 Api - */ - protected function getApi() + protected function getApi(): Api { $this->initializeApi(); @@ -1460,18 +1154,4 @@ protected function getApi() // 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/Classes/Driver/CloudinaryFastDriver.php b/Classes/Driver/CloudinaryFastDriver.php deleted file mode 100644 index 5e5a49c..0000000 --- a/Classes/Driver/CloudinaryFastDriver.php +++ /dev/null @@ -1,1357 +0,0 @@ - ['r' => bool, 'w' => bool] - * - * @var array - */ - protected $cachedPermissions = []; - - /** @var ConfigurationService */ - protected $configurationService; - - /** - * @var ResourceStorage - */ - protected $storage = null; - - /** - * @var CharsetConverter - */ - protected $charsetConversion = null; - - /** - * @var CloudinaryPathService - */ - protected $cloudinaryPathService; - - /** - * @var CloudinaryResourceService - */ - protected $cloudinaryResourceService; - - /** - * @var CloudinaryFolderService - */ - protected $cloudinaryFolderService; - - /** - * @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; - - $this->configurationService = GeneralUtility::makeInstance(ConfigurationService::class, $this->configuration); - - $this->charsetConversion = GeneralUtility::makeInstance(CharsetConverter::class); - } - - /** - * @return void - */ - public function processConfiguration() - { - } - - /** - * @return void - */ - public function initialize() - { - // Test connection if we are in the edit view of this storage - if ( - !Environment::isCli() && - ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isBackend() && - !empty($_GET['edit']['sys_file_storage']) - ) { - $this->getCloudinaryTestConnectionService()->test(); - } - } - - /** - * @param string $identifier - * - * @return string - */ - 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]; - } - - $cloudinaryResource = $this->getCloudinaryResourceService()->getResource( - $this->getCloudinaryPathService()->computeCloudinaryPublicId($identifier), - ); - - return $cloudinaryResource ? $cloudinaryResource['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 = []) - { - $publicId = $this->getCloudinaryPathService()->computeCloudinaryPublicId($fileIdentifier); - $cloudinaryResource = $this->getCloudinaryResourceService()->getResource($publicId); - // We have a problem Hudson! - if (!$cloudinaryResource) { - throw new \Exception( - 'I could not find a corresponding cloudinary resource for file ' . $fileIdentifier, - 1591775048, - ); - } - - $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, - 'extension' => $this->getResourceInfo($cloudinaryResource, 'format'), - 'size' => $this->getResourceInfo($cloudinaryResource, 'bytes'), - 'width' => $this->getResourceInfo($cloudinaryResource, 'width'), - 'height' => $this->getResourceInfo($cloudinaryResource, 'height'), - 'storage' => $this->storageUid, - 'identifier' => $fileIdentifier, - 'name' => PathUtility::basename($fileIdentifier), - ]; - } - - /** - * @param array $resource - * @param string $name - * - * @return string - */ - protected function getResourceInfo(array $resource, string $name): string - { - return isset($resource[$name]) ? $resource[$name] : ''; - } - - /** - * Checks if a file exists - * - * @param string $fileIdentifier - * - * @return bool - */ - public function fileExists($fileIdentifier) - { - $cloudinaryResource = $this->getCloudinaryResourceService()->getResource( - $this->getCloudinaryPathService()->computeCloudinaryPublicId($fileIdentifier), - ); - - return !empty($cloudinaryResource); - } - - /** - * Checks if a folder exists - * - * @param string $folderIdentifier - * - * @return bool - */ - public function folderExists($folderIdentifier) - { - if ($folderIdentifier === self::ROOT_FOLDER_IDENTIFIER) { - return true; - } - $cloudinaryFolder = $this->getCloudinaryFolderService()->getFolder( - $this->getCloudinaryPathService()->computeCloudinaryFolderPath($folderIdentifier), - ); - return !empty($cloudinaryFolder); - } - - /** - * @param string $fileName - * @param string $folderIdentifier - * - * @return bool - */ - public function fileExistsInFolder($fileName, $folderIdentifier) - { - $fileIdentifier = $this->canonicalizeFolderIdentifierAndFileName($folderIdentifier, $fileName); - - return $this->fileExists($fileIdentifier); - } - - /** - * Checks if a folder exists inside a storage folder - * - * @param string $folderName - * @param string $folderIdentifier - * - * @return bool - */ - public function folderExistsInFolder($folderName, $folderIdentifier) - { - 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) - { - 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->canonicalizeFolderIdentifierAndFileName($targetFolderIdentifier, $fileName); - - // We remove a possible existing transient file to avoid bad surprise. - $this->cleanUpTemporaryFile($fileIdentifier); - - // We compute the cloudinary public id - $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 - $cloudinaryResource = Uploader::upload($localFilePath, [ - 'public_id' => PathUtility::basename($cloudinaryPublicId), - 'folder' => $this->getCloudinaryPathService()->computeCloudinaryFolderPath($targetFolderIdentifier), - 'resource_type' => $this->getCloudinaryPathService()->getResourceType($fileIdentifier), - 'overwrite' => true, - ]); - - $this->checkCloudinaryUploadStatus($cloudinaryResource, $fileIdentifier); - - // We persist the uploaded resource. - $this->getCloudinaryResourceService()->save($cloudinaryResource); - - 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) - { - $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), [ - 'public_id' => PathUtility::basename( - $this->getCloudinaryPathService()->computeCloudinaryPublicId($targetFileIdentifier), - ), - 'folder' => $this->getCloudinaryPathService()->computeCloudinaryFolderPath($targetFolderIdentifier), - 'resource_type' => $this->getCloudinaryPathService()->getResourceType($fileIdentifier), - 'overwrite' => true, - ]); - - $this->checkCloudinaryUploadStatus($cloudinaryResource, $fileIdentifier); - - // We persist the uploaded resource - $this->getCloudinaryResourceService()->save($cloudinaryResource); - - return $targetFileIdentifier; - } - - /** - * Replaces a file with file in local file system. - * - * @param string $fileIdentifier - * @param string $localFilePath - * - * @return bool - */ - public function replaceFile($fileIdentifier, $localFilePath) - { - // We remove a possible existing transient file to avoid bad surprise. - $this->cleanUpTemporaryFile($fileIdentifier); - - $cloudinaryPublicId = PathUtility::basename( - $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, [ - 'public_id' => PathUtility::basename($cloudinaryPublicId), - 'folder' => $this->getCloudinaryPathService()->computeCloudinaryFolderPath( - PathUtility::dirname($fileIdentifier), - ), - 'resource_type' => $this->getCloudinaryPathService()->getResourceType($fileIdentifier), - 'overwrite' => true, - ]); - - $this->checkCloudinaryUploadStatus($cloudinaryResource, $fileIdentifier); - - // We persist the uploaded resource. - $this->getCloudinaryResourceService()->save($cloudinaryResource); - - 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) - { - $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), - ]); - - $isDeleted = false; - - foreach ($response['deleted'] as $publicId => $status) { - if ($status === 'deleted') { - $isDeleted = (bool) $this->getCloudinaryResourceService()->delete($publicId); - } - } - - return $isDeleted; - } - - /** - * 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'], - ); - $response = $this->getApi()->delete_resources_by_prefix($cloudinaryFolder); - - foreach ($response['deleted'] as $publicId => $status) { - if ($status === 'deleted') { - $this->getCloudinaryResourceService()->delete($publicId); - } - } - } - - // 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'], - ); - $response = $this->getApi()->delete_folder($cloudinaryFolder); - - foreach ($response['deleted'] as $folder) { - $this->getCloudinaryFolderService()->delete($folder); - } - } - - 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'], - ); - - file_put_contents($temporaryPath, file_get_contents($this->getPublicUrl($fileIdentifier))); - $this->log('File downloaded into "%s"', [$temporaryPath], ['getFileForLocalProcessing']); - } - - 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->canonicalizeFolderIdentifierAndFolderName( - $parentFolderIdentifier, - $newFolderName, - ); - $cloudinaryFolder = $this->getCloudinaryPathService()->normalizeCloudinaryPath($canonicalFolderPath); - - $this->log('[API][CREATE] Cloudinary\Api::createFolder() - folder "%s"', [$cloudinaryFolder], ['createFolder']); - $response = $this->getApi()->create_folder($cloudinaryFolder); - - if (!$response['success']) { - throw new \Exception('Folder creation failed: ' . $cloudinaryFolder, 1591775050); - } - $this->getCloudinaryFolderService()->save($cloudinaryFolder); - - 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) { - // Before calling API, make sure we are connected with the right "bucket" - $this->initializeApi(); - - // Rename the file - $cloudinaryResource = Uploader::rename($cloudinaryPublicId, $newCloudinaryPublicId, [ - 'resource_type' => $this->getCloudinaryPathService()->getResourceType($fileIdentifier), - 'overwrite' => true, - ]); - - $this->checkCloudinaryUploadStatus($cloudinaryResource, $fileIdentifier); - - // We remove the old public id - $this->getCloudinaryResourceService()->delete($cloudinaryPublicId); - - // ... and insert the new cloudinary resource - $this->getCloudinaryResourceService()->save($cloudinaryResource); - } - - return $newFileIdentifier; - } - - /** - * @param array $cloudinaryResource - * @param string $fileIdentifier - * - * @throws Api\GeneralError - */ - protected function checkCloudinaryUploadStatus(array $cloudinaryResource, $fileIdentifier): void - { - if (!$cloudinaryResource && $cloudinaryResource['type'] !== 'upload') { - throw new RuntimeException('Cloudinary upload failed for ' . $fileIdentifier, 1591954950); - } - } - - /** - * 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 = []; - - $pathSegments = GeneralUtility::trimExplode('/', $folderIdentifier); - $numberOfSegments = count($pathSegments); - - if ($numberOfSegments > 1) { - // Replace last folder name by the new folder name - $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) { - $newFileIdentifier = str_replace($folderIdentifier, $newFolderIdentifier, $oldFileIdentifier); - - if ($oldFileIdentifier !== $newFileIdentifier) { - $renamedFiles[$oldFileIdentifier] = $this->renameFile($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 = $this->canonicalizeFolderIdentifierAndFolderName( - $targetFolderIdentifier, - $newFolderName, - ); - - 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->canonicalizeFolderIdentifierAndFolderName( - $targetFolderIdentifier, - $newFolderName, - ); - - if (!$this->folderExists($newTargetFolderIdentifier)) { - $this->createFolder($newTargetFolderIdentifier); - } - - $files = $this->getFilesInFolder($sourceFolderIdentifier, 0, -1, true); - foreach ($files as $fileIdentifier) { - $newFileIdentifier = str_replace($sourceFolderIdentifier, $newTargetFolderIdentifier, $fileIdentifier); - - $this->copyFileWithinStorage( - $fileIdentifier, - GeneralUtility::dirname($newFileIdentifier), - 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) - { - 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) - { - $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 - */ - 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 $filterCallbacks 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 $filterCallbacks = [], - $sort = '', - $sortRev = false - ) { - $cloudinaryFolder = $this->getCloudinaryPathService()->computeCloudinaryFolderPath( - $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier), - ); - - // 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'; - } - - $orderings = [ - 'fieldName' => $parameters['sort'], - 'direction' => isset($parameters['reverse']) && (int) $parameters['reverse'] ? 'DESC' : 'ASC', - ]; - - $pagination = [ - 'maxResult' => $numberOfItems, - 'firstResult' => (int) GeneralUtility::_GP('pointer'), - ]; - - $cloudinaryResources = $this->getCloudinaryResourceService()->getResources( - $cloudinaryFolder, - $orderings, - $pagination, - $recursive, - ); - - // Generate list of folders for the file module. - $files = []; - foreach ($cloudinaryResources as $cloudinaryResource) { - // Compute file identifier - $fileIdentifier = $this->canonicalizeAndCheckFileIdentifier( - $this->getCloudinaryPathService()->computeFileIdentifier($cloudinaryResource), - ); - - $result = $this->applyFilterMethodsToDirectoryItem( - $filterCallbacks, - basename($fileIdentifier), - $fileIdentifier, - dirname($fileIdentifier), - ); - - if ($result) { - $files[] = $fileIdentifier; - } - } - - return $files; - } - - /** - * 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 = []) - { - $folderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier); - - // true means we have non-core filters that has been added and we must filter on the PHP side. - if (count($filterCallbacks) > 1) { - $files = $this->getFilesInFolder($folderIdentifier, 0, 0, $recursive, $filterCallbacks); - $result = count($files); - } else { - $result = $this->getCloudinaryResourceService()->count( - $this->getCloudinaryPathService()->computeCloudinaryFolderPath($folderIdentifier), - $recursive, - ); - } - return $result; - } - - /** - * Returns a list of folders inside the specified path - * - * @param string $folderIdentifier - * @param int $start - * @param int $numberOfItems - * @param bool $recursive - * @param array $filterCallbacks - * @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 $filterCallbacks = [], - $sort = '', - $sortRev = false - ) { - $parameters = (array) GeneralUtility::_GP('SET'); - - $cloudinaryFolder = $this->getCloudinaryPathService()->computeCloudinaryFolderPath( - $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier), - ); - - $cloudinaryFolders = $this->getCloudinaryFolderService()->getSubFolders( - $cloudinaryFolder, - [ - 'fieldName' => 'folder', - 'direction' => isset($parameters['reverse']) && (int) $parameters['reverse'] ? 'DESC' : 'ASC', - ], - $recursive, - ); - - // Generate list of folders for the file module. - $folders = []; - foreach ($cloudinaryFolders as $cloudinaryFolder) { - $folderIdentifier = $this->getCloudinaryPathService()->computeFolderIdentifier($cloudinaryFolder['folder']); - - $result = $this->applyFilterMethodsToDirectoryItem( - $filterCallbacks, - basename($folderIdentifier), - $folderIdentifier, - dirname($folderIdentifier), - ); - - if ($result) { - $folders[] = $folderIdentifier; - } - } - - return $folders; - } - - /** - * 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 = []) - { - // true means we have non-core filters that has been added and we must filter on the PHP side. - if (count($filterCallbacks) > 1) { - $folders = $this->getFoldersInFolder($folderIdentifier, 0, 0, $recursive, $filterCallbacks); - $result = count($folders); - } else { - $cloudinaryFolder = $this->getCloudinaryPathService()->computeCloudinaryFolderPath( - $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier), - ); - - $result = $this->getCloudinaryFolderService()->countSubFolders($cloudinaryFolder, $recursive); - } - - return $result; - } - - /** - * @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->charsetConversion->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); - } - - $pathParts = PathUtility::pathinfo($cleanFileName); - - $cleanFileName = - str_replace('.', '_', $pathParts['filename']) . - ($pathParts['extension'] ? '.' . $pathParts['extension'] : ''); - - // Handle the special jpg case which does not correspond to the file extension. - return preg_replace('/jpeg$/', 'jpg', $cleanFileName); - } - - /** - * Applies a set of filter methods to a file name to find out if it should be used or not. This is e.g. used by - * directory listings. - * - * @param array $filterMethods The filter methods to use - * @param string $itemName - * @param string $itemIdentifier - * @param string $parentIdentifier - * - * @return bool - * @throws \RuntimeException - */ - protected function applyFilterMethodsToDirectoryItem( - array $filterMethods, - $itemName, - $itemIdentifier, - $parentIdentifier - ) { - foreach ($filterMethods as $filter) { - if (is_callable($filter)) { - $result = call_user_func($filter, $itemName, $itemIdentifier, $parentIdentifier, [], $this); - // We have to use -1 as the „don't include“ return value, as call_user_func() will return FALSE - // If calling the method succeeded and thus we can't use that as a return value. - if ($result === -1) { - return false; - } - if ($result === false) { - throw new \RuntimeException( - 'Could not apply file/folder name filter ' . $filter[0] . '::' . $filter[1], - 1596795500, - ); - } - } - } - 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); - if (is_file($temporaryLocalFile)) { - unlink($temporaryLocalFile); - } - - // very coupled.... via signal slot? - $this->getExplicitDataCacheRepository()->delete($this->storageUid, $fileIdentifier); - } - - /** - * @return object|ExplicitDataCacheRepository - */ - public function getExplicitDataCacheRepository() - { - return GeneralUtility::makeInstance(ExplicitDataCacheRepository::class); - } - - /** - * @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 canonicalizeFolderIdentifierAndFolderName(string $folderIdentifier, string $folderName): string - { - $canonicalFolderPath = $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier); - return $this->canonicalizeAndCheckFolderIdentifier( - $canonicalFolderPath . trim($folderName, DIRECTORY_SEPARATOR), - ); - } - - /** - * @param string $folderIdentifier - * @param string $fileName - * - * @return string - */ - protected function canonicalizeFolderIdentifierAndFileName(string $folderIdentifier, string $fileName): string - { - return $this->canonicalizeAndCheckFileIdentifier( - $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier) . $fileName, - ); - } - - /** - * @return object|CloudinaryPathService - */ - protected function getCloudinaryPathService() - { - if (!$this->cloudinaryPathService) { - $this->cloudinaryPathService = GeneralUtility::makeInstance( - CloudinaryPathService::class, - $this->configuration, - ); - } - - return $this->cloudinaryPathService; - } - - /** - * @return CloudinaryResourceService - */ - protected function getCloudinaryResourceService() - { - if (!$this->cloudinaryResourceService) { - /** @var ResourceFactory $resourceFactory */ - $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class); - - $this->cloudinaryResourceService = GeneralUtility::makeInstance( - CloudinaryResourceService::class, - $resourceFactory->getStorageObject($this->storageUid), - ); - } - - return $this->cloudinaryResourceService; - } - - /** - * @return object|CloudinaryTestConnectionService - */ - protected function getCloudinaryTestConnectionService() - { - return GeneralUtility::makeInstance(CloudinaryTestConnectionService::class, $this->configuration); - } - - /** - * @return CloudinaryFolderService - */ - protected function getCloudinaryFolderService() - { - if (!$this->cloudinaryFolderService) { - $this->cloudinaryFolderService = GeneralUtility::makeInstance( - CloudinaryFolderService::class, - $this->storageUid, - ); - } - - return $this->cloudinaryFolderService; - } - - /** - * @return void - */ - protected function initializeApi() - { - 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 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(); - } -} diff --git a/Classes/EventHandlers/BeforeFileProcessingEventHandler.php b/Classes/EventHandlers/BeforeFileProcessingEventHandler.php new file mode 100644 index 0000000..9f9240c --- /dev/null +++ b/Classes/EventHandlers/BeforeFileProcessingEventHandler.php @@ -0,0 +1,76 @@ +getDriver(); + $processedFile = $event->getProcessedFile(); + /** @var File $file */ + $file = $event->getFile(); + + if (!$driver instanceof CloudinaryDriver) { + return; + } + + if ($processedFile->isProcessed()) { + return; + } + + if (str_starts_with($processedFile->getIdentifier(), 'PROCESSEDFILE')) { + return; + } + + $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', + ] + ] + ] + ); + $url = $explicitData['eager'][0]['secure_url']; + + $parts = parse_url($url); + $path = $parts['path'] ?? ''; + $processedFile->setName(basename($url)); + $processedFile->setIdentifier('PROCESSEDFILE' . $path); + + $processedFile->updateProperties([ + 'width' => $explicitData['eager'][0]['width'], + 'height' => $explicitData['eager'][0]['height'], + ]); + + $processedFileRepository = GeneralUtility::makeInstance(ProcessedFileRepository::class); + $processedFileRepository->add($processedFile); + } + + public function getCloudinaryImageService(): CloudinaryImageService + { + return GeneralUtility::makeInstance(CloudinaryImageService::class); + } +} 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/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 @@ +getStorage()->getDriverType() !== CloudinaryFastDriver::DRIVER_TYPE) { + if ($file->getStorage()->getDriverType() !== CloudinaryDriver::DRIVER_TYPE) { return; } $cloudinaryImageService = GeneralUtility::makeInstance(CloudinaryImageService::class); diff --git a/Classes/Services/AbstractCloudinaryMediaService.php b/Classes/Services/AbstractCloudinaryMediaService.php index fcbf0ae..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,32 +63,21 @@ 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, - $storage->getConfiguration() + $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/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/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 = []; diff --git a/Classes/Services/CloudinaryPathService.php b/Classes/Services/CloudinaryPathService.php index a61c42d..5ac4b2b 100644 --- a/Classes/Services/CloudinaryPathService.php +++ b/Classes/Services/CloudinaryPathService.php @@ -9,55 +9,41 @@ * 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; +use Visol\Cloudinary\Driver\CloudinaryDriver; -/** - * 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 { - $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() ); } - /** - * @param string $cloudinaryFolder - * - * @return string - */ public function computeFolderIdentifier(string $cloudinaryFolder): string { return self::stripBasePathFromIdentifier( @@ -69,7 +55,6 @@ public function computeFolderIdentifier(string $cloudinaryFolder): string /** * Return the basePath. * The basePath never has a trailing slash - * @return string */ protected function getBasePath(): string { @@ -79,153 +64,74 @@ 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; + $fileExtension = $this->getFileExtension($fileIdentifier); + $publicId = in_array($fileExtension, CloudinaryDriver::$knownRawFormats) + ? $fileIdentifier + : $this->stripFileExtension($fileIdentifier); - return $this->normalizeCloudinaryPath($normalizedFileIdentifier); + return $this->normalizeCloudinaryPublicId($publicId); } - /** - * FAL to Cloudinary identifier - * - * @param string $folderIdentifier - * - * @return string - */ public function computeCloudinaryFolderPath(string $folderIdentifier): string { - return $this->normalizeCloudinaryPath($folderIdentifier); + return $this->normalizeCloudinaryPublicId($folderIdentifier); } - /** - * @param string $cloudinaryPath - * - * @return string - */ - 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) : $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 + protected function getCloudinaryResource(string $fileIdentifier): array { - $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; - } + $possiblePublicId = $this->stripFileExtension($fileIdentifier); - /** - * @param string $fileIdentifier - * - * @return bool - */ - protected function guessIsVideo(string $fileIdentifier) - { - $extension = strtolower(PathUtility::pathinfo($fileIdentifier, PATHINFO_EXTENSION)); - $rawExtensions = [ - 'mp4', - 'mov', + // We cache the resource for performance reasons. + if (!isset($this->cachedCloudinaryResources[$possiblePublicId])) { - 'mp3', // As documented @see https://cloudinary.com/documentation/image_upload_api_reference - ]; + // We need to check whether the public id really exists. + $cloudinaryResourceService = GeneralUtility::makeInstance( + CloudinaryResourceService::class, + $this->storage + ); - return in_array($extension, $rawExtensions, true); - } + $cloudinaryResource = $cloudinaryResourceService->getResource($possiblePublicId); - /** - * 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); + // 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 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); + } + + $this->cachedCloudinaryResources[$possiblePublicId] = $cloudinaryResource; + } + + return $this->cachedCloudinaryResources[$possiblePublicId]; } - /** - * @param $filename - * - * @return string - */ - protected function stripExtension(string $filename): string + protected function stripFileExtension(string $filename): string { $pathParts = PathUtility::pathinfo($filename); @@ -236,6 +142,12 @@ protected function stripExtension(string $filename): string return $pathParts['dirname'] . DIRECTORY_SEPARATOR . $pathParts['filename']; } + protected function getFileExtension(string $filename): string + { + $pathInfo = PathUtility::pathinfo($filename); + return $pathInfo['extension'] ?? ''; + } + public static function stripBasePathFromIdentifier(string $identifierWithBasePath, string $basePath): string { return preg_replace( diff --git a/Classes/Services/CloudinaryResourceService.php b/Classes/Services/CloudinaryResourceService.php index 8cfafa9..a223b8b 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_cache_resources'; + + 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,23 +49,16 @@ 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 = [], - array $pagination = [], - bool $recursive = false - ): array { + array $orderings = [], + array $pagination = [], + bool $recursive = false + ): array + { $query = $this->getQueryBuilder(); $query ->select('*') @@ -92,28 +66,22 @@ 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']); } - 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()->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 +96,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 +106,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); @@ -169,27 +122,24 @@ 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, + ] + ); } - /** - * @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 +148,6 @@ protected function update(array $cloudinaryResource, string $publicIdHash): int ]); } - /** - * @param string $publicIdHash - * - * @return int - */ protected function exists(string $publicIdHash): int { $query = $this->getQueryBuilder(); @@ -214,14 +159,9 @@ 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); } - /** - * @param array $cloudinaryResource - * - * @return array - */ protected function getValues(array $cloudinaryResource): array { $publicIdHash = $this->getPublicIdHash($cloudinaryResource); @@ -232,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), @@ -255,85 +195,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] : ''; + 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 +243,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..27840ad 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,15 @@ 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 = null; + + protected string $processedFolder = '_processed_'; + + protected string $additionalExpression = ''; + + protected array $statistics = [ self::CREATED => 0, self::UPDATED => 0, self::DELETED => 0, @@ -61,19 +51,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 +62,17 @@ public function __construct(ResourceStorage $storage, SymfonyStyle $io = null) $this->io = $io; } - /** - * @return void - */ - public function empty(): void + public function scanOne(string $publicId): array|null { - $this->getCloudinaryResourceService()->deleteAll(); - $this->getCloudinaryFolderService()->deleteAll(); + try { + $resource = (array)$this->getApi()->resource($publicId); + $result = $this->getCloudinaryResourceService()->save($resource); + } catch (Exception $exception) { + $result = null; + } + return $result; } - /** - * @return array - */ public function scan(): array { $this->preScan(); @@ -104,25 +82,28 @@ 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->io) { - $this->io->writeln('Mirroring...' . chr(10)); + if ($this->additionalExpression) { + $expressions[] = $this->additionalExpression; } + $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, @@ -146,21 +127,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'], 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 + $this->console('Skipped unknown raw file ' . $fileIdentifier); + continue; + } + try { - if ($this->io) { - $this->io->writeln($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)) { - if ($this->io) { - $this->io->writeln('Indexing new file: ' . $fileIdentifier); - $this->io->writeln(''); - } + $this->console('New file will be indexed in typo3 ' . $fileIdentifier, true); // This will trigger a file indexation $this->storage->getFile($fileIdentifier); @@ -173,12 +165,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 +179,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 +208,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,51 +218,43 @@ 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) { $this->cloudinaryPathService = GeneralUtility::makeInstance( CloudinaryPathService::class, - $this->storage->getConfiguration() + $this->storage ); } 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 +264,20 @@ 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(''); + } + } + } + + public function setAdditionalExpression(string $additionalExpression): CloudinaryScanService + { + $this->additionalExpression = $additionalExpression; + return $this; + } } 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__); diff --git a/Classes/Services/Extractor/CloudinaryMetaDataExtractor.php b/Classes/Services/Extractor/CloudinaryMetaDataExtractor.php index 9b4a564..825f7fe 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]; } /** @@ -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/Services/FileMoveService.php b/Classes/Services/FileMoveService.php index d7cb39f..6a5bb28 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 string $tableName = 'sys_file'; + + 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,14 +75,7 @@ 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 + public function changeStorage(File $fileObject, ResourceStorage $targetStorage, bool $removeFile = true): bool { // Update the storage uid $isMigrated = (bool)$this->updateFile( @@ -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() + protected function getCloudinaryPathService(): CloudinaryPathService { return $this->cloudinaryPathService; } - /** - * @param ResourceStorage $storage - */ protected function initializeCloudinaryService(ResourceStorage $storage) { $this->cloudinaryPathService = GeneralUtility::makeInstance( CloudinaryPathService::class, - $storage->getStorageRecord() + $storage ); } } diff --git a/Classes/Slots/FileProcessingSlot.php b/Classes/Slots/FileProcessingSlot.php deleted file mode 100644 index ca25c6e..0000000 --- a/Classes/Slots/FileProcessingSlot.php +++ /dev/null @@ -1,81 +0,0 @@ -isProcessed()) { - return; - } - - if (strpos($processedFile->getIdentifier() ?? '', 'PROCESSEDFILE' ) === 0) { - 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, $options); - $url = $explicitData['eager'][0]['secure_url']; - - $parts = parse_url($url); - $processedFile->setName(basename($url)); - $processedFile->setIdentifier('PROCESSEDFILE' . $parts['path']); - - $processedFile->updateProperties([ - 'width' => $explicitData['eager'][0]['width'], - 'height' => $explicitData['eager'][0]['height'], - ]); - - /** @var $processedFileRepository ProcessedFileRepository */ - $processedFileRepository = GeneralUtility::makeInstance(ProcessedFileRepository::class); - $processedFileRepository->add($processedFile); - } - - /** - * @return object|CloudinaryImageService - */ - public function getCloudinaryImageService() - { - return GeneralUtility::makeInstance(CloudinaryImageService::class); - } - -} diff --git a/Classes/Utility/CloudinaryFileUtility.php b/Classes/Utility/CloudinaryFileUtility.php new file mode 100644 index 0000000..388f317 --- /dev/null +++ b/Classes/Utility/CloudinaryFileUtility.php @@ -0,0 +1,37 @@ + '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'; + } +} 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 ); } diff --git a/Configuration/Services.yaml b/Configuration/Services.yaml index 7acc094..8bb65ae 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' @@ -42,9 +49,22 @@ 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' 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 diff --git a/Configuration/TypoScript/setup.typoscript b/Configuration/TypoScript/setup.webhook.example.typoscript similarity index 75% rename from Configuration/TypoScript/setup.typoscript rename to Configuration/TypoScript/setup.webhook.example.typoscript index 757cf4b..bb27603 100644 --- a/Configuration/TypoScript/setup.typoscript +++ b/Configuration/TypoScript/setup.webhook.example.typoscript @@ -9,7 +9,6 @@ page_1573555440 { xhtml_cleaning = 0 admPanel = 0 disableAllHeaderCode = 1 - additionalHeaders.10.header = Content-type:text/html } 10 = COA_INT 10 { @@ -18,10 +17,13 @@ page_1573555440 { userFunc = TYPO3\CMS\Extbase\Core\Bootstrap->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/Documentation/backend-cloudinary-integration-01.png b/Documentation/backend-cloudinary-integration-01.png new file mode 100644 index 0000000..6c7581b Binary files /dev/null and b/Documentation/backend-cloudinary-integration-01.png differ diff --git a/Documentation/driver-configuration-03.png b/Documentation/driver-configuration-03.png new file mode 100644 index 0000000..4c54e79 Binary files /dev/null and b/Documentation/driver-configuration-03.png differ diff --git a/Documentation/extension-configuration-01.png b/Documentation/extension-configuration-01.png new file mode 100644 index 0000000..af213bb Binary files /dev/null and b/Documentation/extension-configuration-01.png differ 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 ####################### diff --git a/README.md b/README.md index 81e98a0..0f7eb62 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A TYPO3 extension that connect TYPO3 with [Cloudinary](cloudinary.com) services by the means of a **Cloudinary Driver for FAL**. The extension also provides various View Helpers to render images on the Frontend. Cloudinary is a service provider dealing with images and videos. -It offers various services among other: +It offers various services among others: * CDN for fast images and videos delivering * Manipulation of images and videos such as cropping, resizing and much more... @@ -57,6 +57,52 @@ The environment variable should be surrounded by %. Example `%BUCKET_NAME%` ![](Documentation/driver-configuration-01.png) +Cloudinary integration as file picker +------------------------------------- + +The extension is providing an integration with Cloudinary so that the editor can directly interact with the cloudinary library in the backend. +When clicking on a button, a modal window will open up displaying the Cloudinary files directly. +From there, the files can be inserted directly as file references. + +![](Documentation/backend-cloudinary-integration-01.png) + +To enable this button, we should first configure the extension settings to display the +desired button and storage. + +![](Documentation/extension-configuration-01.png) + + +We can even take it a step further by enabling auto-login. +A new field called authenticationEmail has been added to the storage configuration. +By providing a configured email in Cloudinary, we can automatically log +in when clicking on the button. Magic! + +![](Documentation/driver-configuration-03.png) + + +Configuration TCEFORM +--------------------- + +We can configure the form in the backend to hide the default TYPO3 button, +thus limiting backend user interaction with the Cloudinary library. +Here is an example of such a configuration: + +``` +TCEFORM { + pages { + media { + config { + appearance { + fileUploadAllowed = 0 + fileByUrlAllowed = 0 + elementBrowserEnabled = 0 + } + } + } + } +} +``` + Logging ------- @@ -69,7 +115,7 @@ tail -f public/typo3temp/var/logs/cloudinary.log To decide: we now have log level INFO. We might consider "increasing" the level to "DEBUG". -Caveats and trouble shooting +Caveats and troubleshooting ---------------------------- * **Free** Cloudinary account allows 500 API request per day @@ -156,7 +202,7 @@ many resources at once. cld sync --push localFolder remoteFolder ``` -The extension provides also a tool to copy a bunch of files (restricted to images) from one storage to an another. +The extension provides also a tool to copy a bunch of files (restricted to images) from one storage to another. This can be achieved with this command: ```shell script @@ -164,7 +210,7 @@ This can be achieved with this command: # where 1 is the source storage (local) # and 2 is the target storage (cloudinary) -# Ouptut: +# Output: Copying 64 files from storage "fileadmin/ (auto-created)" (1) to "Cloudinary Storage (fabidule)" (2) Copying /introduction/images/typo3-book-backend-login.png Copying /introduction/images/content/content-quote.png @@ -198,20 +244,30 @@ 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 overriding +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 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 --------------------- -Adapter for theleague php flysystem for Cloudinary +Adapter for php flysystem for Cloudinary https://github.com/flownative/flow-google-cloudstorage 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 - 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')) }) } diff --git a/ext_conf_template.txt b/ext_conf_template.txt index 9f64f4f..cc9625f 100644 --- a/ext_conf_template.txt +++ b/ext_conf_template.txt @@ -1,5 +1,5 @@ # cat=cloudinary; type=int; label=Default cloudinary storage where to upload "local" file (files usually stored on storage = 0) default_cloudinary_storage = 0 -# cat=cloudinary; type=string; label=CSV values of storage to be displayed in file references fields in TCForm. If empty all cloudinary file storages will be displayed. -tceform_cludinary_storage = +# cat=cloudinary; type=int; label=Cloudinary storage to be displayed in file references fields in TCForm. +tceform_cludinary_storage = 0 diff --git a/ext_localconf.php b/ext_localconf.php index 32683a3..2a5ea36 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -1,24 +1,18 @@ ', - ); +call_user_func(callback: function () { // Override default class to add cloudinary button $GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['nodeRegistry'][1652423292] = [ @@ -29,21 +23,21 @@ ExtensionUtility::configurePlugin( \Cloudinary::class, - 'Cache', + 'WebHook', [ - CloudinaryScanController::class => 'scan', + CloudinaryWebHookController::class => 'process', ], // non-cacheable actions [ - CloudinaryScanController::class => 'scan', + CloudinaryWebHookController::class => 'process', ], ); /** @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', ); @@ -52,27 +46,36 @@ $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', ], ], ]; - 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; 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, diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 5fed779..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,36 +40,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 - path: Classes/CloudinaryFactory.php - - message: "#^Method Visol\\\\Cloudinary\\\\CloudinaryFactory\\:\\:getFolder\\(\\) should return TYPO3\\\\CMS\\\\Core\\\\Resource\\\\Folder but returns object\\.$#" count: 1 @@ -95,11 +50,6 @@ parameters: count: 1 path: Classes/CloudinaryFactory.php - - - message: "#^Cannot call method fetchAll\\(\\) 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 @@ -130,16 +80,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 +110,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 @@ -240,11 +200,6 @@ parameters: count: 1 path: Classes/Command/CloudinaryCopyCommand.php - - - message: "#^Cannot call method fetchAll\\(\\) 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 @@ -270,6 +225,16 @@ parameters: count: 1 path: Classes/Command/CloudinaryFixJpegCommand.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: "#^Cannot call method exists\\(\\) on TYPO3\\\\CMS\\\\Core\\\\Resource\\\\File\\|TYPO3\\\\CMS\\\\Core\\\\Resource\\\\ProcessedFile\\|null\\.$#" count: 1 @@ -300,11 +265,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 +320,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 @@ -400,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 @@ -425,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 @@ -460,21 +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 fetch\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#" - count: 1 - path: Classes/Domain/Repository/ExplicitDataCacheRepository.php - - - - message: "#^Offset 'explicit_data' does not exist on array\\{options\\: mixed\\}\\.$#" - 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 @@ -491,77 +406,62 @@ parameters: path: Classes/Domain/Repository/ExplicitDataCacheRepository.php - - message: "#^Parameter \\#1 \\$string of function sha1 expects string, string\\|false given\\.$#" - count: 1 + message: "#^Parameter \\#1 \\$json of function json_decode expects string, mixed given\\.$#" + count: 2 path: Classes/Domain/Repository/ExplicitDataCacheRepository.php - - message: "#^Property Visol\\\\Cloudinary\\\\Domain\\\\Repository\\\\ExplicitDataCacheRepository\\:\\:\\$tableName has no type specified\\.$#" + message: "#^Parameter \\#1 \\$string of function sha1 expects string, string\\|false given\\.$#" count: 1 path: Classes/Domain/Repository/ExplicitDataCacheRepository.php - - message: "#^Call to an undefined method Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:getCharsetConversion\\(\\)\\.$#" - count: 1 - path: Classes/Driver/CloudinaryDriver.php - - - - message: "#^Call to an undefined method object\\:\\:flushFileCache\\(\\)\\.$#" - count: 1 - path: Classes/Driver/CloudinaryDriver.php - - - - message: "#^Call to an undefined method object\\:\\:flushFolderCache\\(\\)\\.$#" - count: 1 - path: Classes/Driver/CloudinaryDriver.php - - - - message: "#^Call to an undefined method object\\:\\:getCachedFiles\\(\\)\\.$#" + 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: "#^Call to an undefined method object\\:\\:getCachedFolders\\(\\)\\.$#" - count: 1 + message: "#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#" + count: 3 path: Classes/Driver/CloudinaryDriver.php - - message: "#^Call to an undefined method object\\:\\:getLogger\\(\\)\\.$#" + message: "#^Cannot access offset 0 on callable\\(\\)\\: mixed\\.$#" count: 1 path: Classes/Driver/CloudinaryDriver.php - - message: "#^Call to an undefined method object\\:\\:setCachedFiles\\(\\)\\.$#" + message: "#^Cannot access offset 1 on callable\\(\\)\\: mixed\\.$#" count: 1 path: Classes/Driver/CloudinaryDriver.php - - message: "#^Call to an undefined method object\\:\\:setCachedFolders\\(\\)\\.$#" - count: 1 + message: "#^Cannot cast mixed to int\\.$#" + count: 2 path: Classes/Driver/CloudinaryDriver.php - - message: "#^Cannot access offset 'format' on array\\|false\\.$#" + message: "#^Method Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:getCloudinaryFolderService\\(\\) should return Visol\\\\Cloudinary\\\\Services\\\\CloudinaryFolderService but returns object\\.$#" count: 1 path: Classes/Driver/CloudinaryDriver.php - - message: "#^Cannot access offset 'public_id' on array\\|false\\.$#" + message: "#^Method Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:getCloudinaryPathService\\(\\) should return Visol\\\\Cloudinary\\\\Services\\\\CloudinaryPathService but returns object\\.$#" count: 1 path: Classes/Driver/CloudinaryDriver.php - - message: "#^Cannot access offset 'secure_url' on array\\|false\\.$#" + 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 'type' on 0\\|0\\.0\\|''\\|'0'\\|array\\{\\}\\|false\\|null\\.$#" + 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 cast mixed to int\\.$#" + message: "#^Method Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:getExplicitDataCacheRepository\\(\\) should return Visol\\\\Cloudinary\\\\Domain\\\\Repository\\\\ExplicitDataCacheRepository but returns object\\.$#" count: 1 path: Classes/Driver/CloudinaryDriver.php @@ -571,7 +471,7 @@ parameters: path: Classes/Driver/CloudinaryDriver.php - - message: "#^Method Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:log\\(\\) has no return type specified\\.$#" + message: "#^Method Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:isFolderEmpty\\(\\) should return bool but returns int\\.$#" count: 1 path: Classes/Driver/CloudinaryDriver.php @@ -580,185 +480,40 @@ parameters: count: 1 path: Classes/Driver/CloudinaryDriver.php - - - message: "#^Method Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryDriver\\:\\:testConnection\\(\\) has no return type specified\\.$#" - count: 1 - path: Classes/Driver/CloudinaryDriver.php - - - - message: "#^Negated boolean expression is always false\\.$#" - count: 1 - path: Classes/Driver/CloudinaryDriver.php - - - - message: "#^Parameter \\#1 \\$cloudinaryResource of method Visol\\\\Cloudinary\\\\Services\\\\CloudinaryPathService\\:\\:computeFileIdentifier\\(\\) expects array, array\\|false given\\.$#" - count: 1 - path: Classes/Driver/CloudinaryDriver.php - - - - message: "#^Parameter \\#1 \\$string of function rtrim expects string, string\\|null given\\.$#" - 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\\:\\:\\$cloudinaryTypo3Cache \\(Visol\\\\Cloudinary\\\\Cache\\\\CloudinaryTypo3Cache\\) does not accept object\\.$#" - count: 1 - path: Classes/Driver/CloudinaryDriver.php - - - - message: "#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#" - count: 3 - path: Classes/Driver/CloudinaryFastDriver.php - - - - message: "#^Call to an undefined method object\\:\\:computeCloudinaryFolderPath\\(\\)\\.$#" - count: 9 - path: Classes/Driver/CloudinaryFastDriver.php - - - - message: "#^Call to an undefined method object\\:\\:computeCloudinaryPublicId\\(\\)\\.$#" - count: 9 - path: Classes/Driver/CloudinaryFastDriver.php - - - - message: "#^Call to an undefined method object\\:\\:computeFileIdentifier\\(\\)\\.$#" - count: 1 - path: Classes/Driver/CloudinaryFastDriver.php - - - - message: "#^Call to an undefined method object\\:\\:computeFolderIdentifier\\(\\)\\.$#" - count: 1 - path: Classes/Driver/CloudinaryFastDriver.php - - - - message: "#^Call to an undefined method object\\:\\:delete\\(\\)\\.$#" - count: 1 - path: Classes/Driver/CloudinaryFastDriver.php - - - - message: "#^Call to an undefined method object\\:\\:getLogger\\(\\)\\.$#" - count: 1 - path: Classes/Driver/CloudinaryFastDriver.php - - - - message: "#^Call to an undefined method object\\:\\:getResourceType\\(\\)\\.$#" - count: 5 - path: Classes/Driver/CloudinaryFastDriver.php - - - - message: "#^Call to an undefined method object\\:\\:guessMimeType\\(\\)\\.$#" - count: 1 - path: Classes/Driver/CloudinaryFastDriver.php - - - - message: "#^Call to an undefined method object\\:\\:normalizeCloudinaryPath\\(\\)\\.$#" - count: 2 - path: Classes/Driver/CloudinaryFastDriver.php - - - - message: "#^Call to an undefined method object\\:\\:test\\(\\)\\.$#" - count: 1 - path: Classes/Driver/CloudinaryFastDriver.php - - - - message: "#^Cannot access offset 0 on callable\\(\\)\\: mixed\\.$#" - count: 1 - path: Classes/Driver/CloudinaryFastDriver.php - - - - message: "#^Cannot access offset 1 on callable\\(\\)\\: mixed\\.$#" - count: 1 - path: Classes/Driver/CloudinaryFastDriver.php - - - - message: "#^Cannot cast mixed to int\\.$#" - count: 2 - path: Classes/Driver/CloudinaryFastDriver.php - - - - message: "#^Method Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryFastDriver\\:\\:getFileContents\\(\\) should return string but returns string\\|false\\.$#" - count: 1 - path: Classes/Driver/CloudinaryFastDriver.php - - - - message: "#^Method Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryFastDriver\\:\\:isFolderEmpty\\(\\) should return bool but returns int\\.$#" - count: 1 - path: Classes/Driver/CloudinaryFastDriver.php - - - - message: "#^Method Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryFastDriver\\:\\:log\\(\\) has no return type specified\\.$#" - count: 1 - path: Classes/Driver/CloudinaryFastDriver.php - - - - message: "#^Method Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryFastDriver\\:\\:sanitizeFileName\\(\\) should return string but returns string\\|null\\.$#" - count: 1 - path: Classes/Driver/CloudinaryFastDriver.php - - - - message: "#^Negated boolean expression is always false\\.$#" - count: 3 - path: Classes/Driver/CloudinaryFastDriver.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 - - - - message: "#^Property Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryFastDriver\\:\\:\\$charsetConversion \\(TYPO3\\\\CMS\\\\Core\\\\Charset\\\\CharsetConverter\\) does not accept object\\.$#" - count: 1 - path: Classes/Driver/CloudinaryFastDriver.php - - - - message: "#^Property Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryFastDriver\\:\\:\\$cloudinaryFolderService \\(Visol\\\\Cloudinary\\\\Services\\\\CloudinaryFolderService\\) does not accept object\\.$#" - count: 1 - path: Classes/Driver/CloudinaryFastDriver.php - - - - message: "#^Property Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryFastDriver\\:\\:\\$cloudinaryPathService \\(Visol\\\\Cloudinary\\\\Services\\\\CloudinaryPathService\\) does not accept object\\.$#" - count: 1 - path: Classes/Driver/CloudinaryFastDriver.php - - - - message: "#^Property Visol\\\\Cloudinary\\\\Driver\\\\CloudinaryFastDriver\\:\\:\\$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: "#^Method Visol\\\\Cloudinary\\\\EventHandlers\\\\BeforeFileProcessingEventHandler\\:\\:getCloudinaryImageService\\(\\) should return Visol\\\\Cloudinary\\\\Services\\\\CloudinaryImageService but returns object\\.$#" count: 1 - path: Classes/Driver/CloudinaryFastDriver.php + path: Classes/EventHandlers/BeforeFileProcessingEventHandler.php - message: "#^Method Visol\\\\Cloudinary\\\\Filters\\\\RegularExpressionFilter\\:\\:filter\\(\\) should return bool but returns int\\|true\\.$#" @@ -780,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 @@ -831,30 +561,10 @@ parameters: path: Classes/Services/CloudinaryFolderService.php - - message: "#^Cannot call method fetch\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#" - count: 1 - path: Classes/Services/CloudinaryFolderService.php - - - - message: "#^Cannot call method fetchAll\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#" - count: 1 - path: Classes/Services/CloudinaryFolderService.php - - - - message: "#^Cannot call method fetchColumn\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#" + message: "#^Cannot cast mixed to 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 - path: Classes/Services/CloudinaryFolderService.php - - message: "#^Cannot access offset 'width' on mixed\\.$#" count: 2 @@ -900,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 @@ -915,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 @@ -935,29 +635,9 @@ parameters: count: 2 path: Classes/Services/CloudinaryResourceService.php - - - message: "#^Cannot call method fetchAll\\(\\) 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 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 - path: Classes/Services/CloudinaryResourceService.php - - message: "#^Cannot cast mixed to int\\.$#" - count: 1 + count: 2 path: Classes/Services/CloudinaryResourceService.php - @@ -965,33 +645,18 @@ 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 path: Classes/Services/CloudinaryResourceService.php - - message: "#^Call to an undefined method object\\:\\:getLogger\\(\\)\\.$#" + message: "#^Method Visol\\\\Cloudinary\\\\Services\\\\CloudinaryScanService\\:\\:console\\(\\) has parameter \\$additionalBlankLine with no type specified\\.$#" count: 1 path: Classes/Services/CloudinaryScanService.php - - message: "#^Cannot call method fetchColumn\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#" + message: "#^Method Visol\\\\Cloudinary\\\\Services\\\\CloudinaryScanService\\:\\:getApi\\(\\) has no return type specified\\.$#" count: 1 path: Classes/Services/CloudinaryScanService.php @@ -1001,37 +666,12 @@ parameters: path: Classes/Services/CloudinaryScanService.php - - message: "#^Method Visol\\\\Cloudinary\\\\Services\\\\CloudinaryScanService\\:\\:getCloudinaryResourceService\\(\\) should return Visol\\\\Cloudinary\\\\Services\\\\CloudinaryResourceService but returns object\\.$#" - count: 1 - path: Classes/Services/CloudinaryScanService.php - - - - message: "#^Method Visol\\\\Cloudinary\\\\Services\\\\CloudinaryScanService\\:\\:log\\(\\) has no return type specified\\.$#" - count: 1 - path: Classes/Services/CloudinaryScanService.php - - - - message: "#^Negated boolean expression is always false\\.$#" - 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\\:\\:getCloudinaryPathService\\(\\) should return Visol\\\\Cloudinary\\\\Services\\\\CloudinaryPathService 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\\.$#" - count: 1 - path: Classes/Services/CloudinaryScanService.php - - - - message: "#^PHPDoc tag @return with type object is not subtype of native type Visol\\\\Cloudinary\\\\Services\\\\CloudinaryResourceService\\.$#" - 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 @@ -1041,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 @@ -1060,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\\(\\)\\.$#" + message: "#^Method Visol\\\\Cloudinary\\\\Services\\\\FileMoveService\\:\\:ensureDirectoryExistence\\(\\) has no return type specified\\.$#" 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\\(\\)\\.$#" - 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 @@ -1110,65 +715,20 @@ 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$#" + message: "#^Method Visol\\\\Cloudinary\\\\Utility\\\\CloudinaryApiUtility\\:\\:initializeByConfiguration\\(\\) has no return type specified\\.$#" count: 1 - path: Classes/Slots/FileProcessingSlot.php + path: Classes/Utility/CloudinaryApiUtility.php - - message: "#^Method Visol\\\\Cloudinary\\\\Utility\\\\CloudinaryApiUtility\\:\\:initializeByConfiguration\\(\\) has no return type specified\\.$#" + message: "#^Method Visol\\\\Cloudinary\\\\Utility\\\\CloudinaryFileUtility\\:\\:getTemporaryFile\\(\\) has parameter \\$storageUid with no type specified\\.$#" count: 1 - path: Classes/Utility/CloudinaryApiUtility.php + path: Classes/Utility/CloudinaryFileUtility.php - message: "#^Call to an undefined method TYPO3\\\\CMS\\\\Core\\\\Resource\\\\FileInterface\\:\\:getOriginalFile\\(\\)\\.$#" @@ -1176,42 +736,42 @@ parameters: path: Classes/ViewHelpers/CloudinaryImageDataViewHelper.php - - message: "#^Call to an undefined method object\\:\\:computeCloudinaryPublicId\\(\\)\\.$#" + 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/ViewHelpers/CloudinaryImageDataViewHelper.php - - message: "#^Call to an undefined method object\\:\\:generateOptionsFromSettings\\(\\)\\.$#" + message: "#^Method Visol\\\\Cloudinary\\\\ViewHelpers\\\\CloudinaryImageDataViewHelper\\:\\:injectImageService\\(\\) has no return type specified\\.$#" count: 1 path: Classes/ViewHelpers/CloudinaryImageDataViewHelper.php - - message: "#^Call to an undefined method object\\:\\:getImage\\(\\)\\.$#" - count: 3 + message: "#^Parameter \\#1 \\$identifier of method TYPO3Fluid\\\\Fluid\\\\Core\\\\Variables\\\\VariableProviderInterface\\:\\:add\\(\\) expects string, mixed given\\.$#" + count: 1 path: Classes/ViewHelpers/CloudinaryImageDataViewHelper.php - - message: "#^Call to an undefined method object\\:\\:getImageObjects\\(\\)\\.$#" + message: "#^Parameter \\#1 \\$identifier of method TYPO3Fluid\\\\Fluid\\\\Core\\\\Variables\\\\VariableProviderInterface\\:\\:remove\\(\\) expects string, mixed given\\.$#" count: 1 path: Classes/ViewHelpers/CloudinaryImageDataViewHelper.php - - message: "#^Call to an undefined method object\\:\\:getResponsiveBreakpointData\\(\\)\\.$#" + 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: "#^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\\.$#" + message: "#^Parameter \\#1 \\$url of function parse_url expects string, mixed given\\.$#" count: 1 path: Classes/ViewHelpers/CloudinaryImageDataViewHelper.php - - message: "#^Method Visol\\\\Cloudinary\\\\ViewHelpers\\\\CloudinaryImageDataViewHelper\\:\\:injectImageService\\(\\) has no return type specified\\.$#" + 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 \\#1 \\$src of method TYPO3\\\\CMS\\\\Extbase\\\\Service\\\\ImageService\\:\\:getImage\\(\\) expects string, int\\|string given\\.$#" + message: "#^Parameter \\#3 \\$treatIdAsReference of method TYPO3\\\\CMS\\\\Extbase\\\\Service\\\\ImageService\\:\\:getImage\\(\\) expects bool, mixed given\\.$#" count: 1 path: Classes/ViewHelpers/CloudinaryImageDataViewHelper.php @@ -1221,26 +781,21 @@ parameters: 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\\(\\)\\.$#" + message: "#^Method Visol\\\\Cloudinary\\\\ViewHelpers\\\\CloudinaryImageViewHelper\\:\\:injectImageService\\(\\) has no return type specified\\.$#" count: 1 path: Classes/ViewHelpers/CloudinaryImageViewHelper.php - - message: "#^Call to an undefined method object\\:\\:getSizesAttribute\\(\\)\\.$#" + message: "#^Parameter \\#1 \\$src of method TYPO3\\\\CMS\\\\Extbase\\\\Service\\\\ImageService\\:\\:getImage\\(\\) expects string, mixed given\\.$#" count: 1 path: Classes/ViewHelpers/CloudinaryImageViewHelper.php - - message: "#^Call to an undefined method object\\:\\:getSrcsetAttribute\\(\\)\\.$#" + 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: "#^Method Visol\\\\Cloudinary\\\\ViewHelpers\\\\CloudinaryImageViewHelper\\:\\:injectImageService\\(\\) has no return type specified\\.$#" + message: "#^Parameter \\#3 \\$treatIdAsReference of method TYPO3\\\\CMS\\\\Extbase\\\\Service\\\\ImageService\\:\\:getImage\\(\\) expects bool, mixed given\\.$#" count: 1 path: Classes/ViewHelpers/CloudinaryImageViewHelper.php diff --git a/phpstan.neon b/phpstan.neon index 0bca2fa..44b0526 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -7,3 +7,10 @@ parameters: - Configuration checkMissingIterableValueType: false reportUnmatchedIgnoredErrors: false + ignoreErrors: + - + message: '#does not accept object.$#' + - + message: '#^Call to an undefined method object#' + - + message: '#^Cannot call method fetch.* on Doctrine\\DBAL\\Result\|int#'