diff --git a/Classes/AssetSource/CantoAssetProxy.php b/Classes/AssetSource/CantoAssetProxy.php index a2d1dd5..97ab72b 100644 --- a/Classes/AssetSource/CantoAssetProxy.php +++ b/Classes/AssetSource/CantoAssetProxy.php @@ -15,10 +15,13 @@ use Exception; use Flownative\Canto\Exception\AuthenticationFailedException; +use Flownative\Canto\Exception\MissingClientSecretException; use Flownative\OAuth2\Client\OAuthClientException; use GuzzleHttp\Exception\GuzzleException; use GuzzleHttp\Psr7\Uri; +use League\OAuth2\Client\Provider\Exception\IdentityProviderException; use Neos\Flow\Annotations as Flow; +use Neos\Flow\Mvc\Routing\Exception\MissingActionNameException; use Neos\Media\Domain\Model\AssetSource\AssetProxy\AssetProxyInterface; use Neos\Media\Domain\Model\AssetSource\AssetProxy\HasRemoteOriginalInterface; use Neos\Media\Domain\Model\AssetSource\AssetProxy\SupportsIptcMetadataInterface; @@ -36,65 +39,18 @@ */ final class CantoAssetProxy implements AssetProxyInterface, HasRemoteOriginalInterface, SupportsIptcMetadataInterface { - /** - * @var CantoAssetSource - */ - private $assetSource; - - /** - * @var string - */ - private $identifier; - - /** - * @var string - */ - private $label; - - /** - * @var string - */ - private $filename; - - /** - * @var \DateTime - */ - private $lastModified; - - /** - * @var int - */ - private $fileSize; - - /** - * @var string - */ - private $mediaType; - - /** - * @var array - */ - private $iptcProperties = []; - - /** - * @var string - */ - private $previewUri; - - /** - * @var int - */ - private $widthInPixels; - - /** - * @var int - */ - private $heightInPixels; - - /** - * @var array - */ - private $tags = []; + private CantoAssetSource $assetSource; + private string $identifier; + private string $label; + private string $filename; + private \DateTime $lastModified; + private int $fileSize; + private string $mediaType; + private array $iptcProperties = []; + private string $previewUri; + private int $widthInPixels; + private int $heightInPixels; + private array $tags = []; /** * @Flow\Inject @@ -198,7 +154,6 @@ public function getHeightInPixels(): ?int } /** - * @return UriInterface * @throws ThumbnailServiceException */ public function getThumbnailUri(): ?UriInterface @@ -206,13 +161,12 @@ public function getThumbnailUri(): ?UriInterface $thumbnailConfiguration = $this->thumbnailService->getThumbnailConfigurationForPreset('Neos.Media.Browser:Thumbnail'); return new Uri(sprintf( '%s/%d', - preg_replace('|/[0-9]+$|', '', $this->previewUri), + preg_replace('|/\d+$|', '', $this->previewUri), max($thumbnailConfiguration->getMaximumWidth(), $thumbnailConfiguration->getMaximumHeight()) )); } /** - * @return UriInterface * @throws ThumbnailServiceException */ public function getPreviewUri(): ?UriInterface @@ -220,7 +174,7 @@ public function getPreviewUri(): ?UriInterface $previewConfiguration = $this->thumbnailService->getThumbnailConfigurationForPreset('Neos.Media.Browser:Preview'); return new Uri(sprintf( '%s/%d', - preg_replace('|/[0-9]+$|', '', $this->previewUri), + preg_replace('|/\d+$|', '', $this->previewUri), max($previewConfiguration->getMaximumWidth(), $previewConfiguration->getMaximumHeight()) )); } @@ -228,17 +182,18 @@ public function getPreviewUri(): ?UriInterface /** * @return resource * @throws AuthenticationFailedException - * @throws OAuthClientException * @throws GuzzleException + * @throws OAuthClientException + * @throws MissingClientSecretException + * @throws IdentityProviderException + * @throws \Neos\Flow\Http\Exception + * @throws MissingActionNameException */ public function getImportStream() { return fopen((string)$this->assetSource->getCantoClient()->directUri($this->identifier), 'rb'); } - /** - * @return string - */ public function getLocalAssetIdentifier(): ?string { $importedAsset = $this->importedAssetRepository->findOneByAssetSourceIdentifierAndRemoteAssetIdentifier($this->assetSource->getIdentifier(), $this->identifier); diff --git a/Classes/AssetSource/CantoAssetProxyQuery.php b/Classes/AssetSource/CantoAssetProxyQuery.php index 1406ece..1b58e0a 100644 --- a/Classes/AssetSource/CantoAssetProxyQuery.php +++ b/Classes/AssetSource/CantoAssetProxyQuery.php @@ -14,12 +14,16 @@ */ use Flownative\Canto\Exception\AuthenticationFailedException; +use Flownative\Canto\Exception\MissingClientSecretException; use Flownative\OAuth2\Client\OAuthClientException; use GuzzleHttp\Exception\GuzzleException; use GuzzleHttp\Psr7\Response; +use League\OAuth2\Client\Provider\Exception\IdentityProviderException; use Neos\Cache\Exception as CacheException; use Neos\Cache\Exception\InvalidDataException; use Neos\Flow\Annotations as Flow; +use Neos\Flow\Http\Exception; +use Neos\Flow\Mvc\Routing\Exception\MissingActionNameException; use Neos\Media\Domain\Model\AssetCollection; use Neos\Media\Domain\Model\AssetSource\AssetProxyQueryInterface; use Neos\Media\Domain\Model\AssetSource\AssetProxyQueryResultInterface; @@ -31,51 +35,19 @@ */ final class CantoAssetProxyQuery implements AssetProxyQueryInterface { - /** - * @var string - */ - private $searchTerm = ''; - - /** - * @var Tag - */ - private $activeTag; - - /** - * @var string - */ - private $tagQuery = ''; - - /** - * @var AssetCollection - */ - private $activeAssetCollection; - - /** - * @var string - */ - private $assetTypeFilter = 'All'; - - /** - * @var array - */ - private $orderings = []; - - /** - * @var int - */ - private $offset = 0; - - /** - * @var int - */ - private $limit = 30; + private string $searchTerm = ''; + private ?Tag $activeTag = null; + private string $tagQuery = ''; + private ?AssetCollection $activeAssetCollection = null; + private string $assetTypeFilter = 'All'; + private array $orderings = []; + private int $offset = 0; + private int $limit = 30; /** * @Flow\InjectConfiguration(path="mapping", package="Flownative.Canto") - * @var array */ - protected $mapping = []; + protected array $mapping = []; /** * @Flow\Inject @@ -83,9 +55,6 @@ final class CantoAssetProxyQuery implements AssetProxyQueryInterface */ protected $logger; - /** - * @param CantoAssetSource $assetSource - */ public function __construct(private CantoAssetSource $assetSource) { } @@ -169,9 +138,14 @@ public function execute(): AssetProxyQueryResultInterface } /** - * @throws OAuthClientException - * @throws GuzzleException * @throws AuthenticationFailedException + * @throws Exception + * @throws GuzzleException + * @throws IdentityProviderException + * @throws MissingActionNameException + * @throws MissingClientSecretException + * @throws OAuthClientException + * @throws \JsonException */ public function count(): int { @@ -188,6 +162,8 @@ public function count(): int * @throws CacheException * @throws InvalidDataException * @throws AuthenticationFailedException + * @throws \JsonException + * @throws \Exception */ public function getArrayResult(): array { @@ -209,9 +185,14 @@ public function getArrayResult(): array } /** - * @throws OAuthClientException - * @throws GuzzleException * @throws AuthenticationFailedException + * @throws Exception + * @throws GuzzleException + * @throws IdentityProviderException + * @throws MissingActionNameException + * @throws MissingClientSecretException + * @throws OAuthClientException + * @throws \JsonException */ private function sendSearchRequest(int $limit, array $orderings): Response { @@ -221,31 +202,25 @@ private function sendSearchRequest(int $limit, array $orderings): Response $searchTerm = $this->searchTerm; - switch ($this->assetTypeFilter) { - case 'Image': - $formatTypes = ['image']; - break; - case 'Video': - $formatTypes = ['video']; - break; - case 'Audio': - $formatTypes = ['audio']; - break; - case 'Document': - $formatTypes = ['document']; - break; - case 'All': - default: - $formatTypes = ['image', 'video', 'audio', 'document', 'presentation', 'other']; - break; - } + $formatTypes = match ($this->assetTypeFilter) { + 'Image' => ['image'], + 'Video' => ['video'], + 'Audio' => ['audio'], + 'Document' => ['document'], + default => ['image', 'video', 'audio', 'document', 'presentation', 'other'], + }; return $this->assetSource->getCantoClient()->search($searchTerm, $formatTypes, $this->tagQuery, $this->offset, $limit, $orderings); } /** - * @throws OAuthClientException - * @throws GuzzleException * @throws AuthenticationFailedException + * @throws Exception + * @throws GuzzleException + * @throws IdentityProviderException + * @throws MissingActionNameException + * @throws MissingClientSecretException + * @throws OAuthClientException + * @throws \JsonException */ public function prepareTagQuery(): void { @@ -276,9 +251,14 @@ public function prepareTagQuery(): void } /** - * @throws OAuthClientException - * @throws GuzzleException * @throws AuthenticationFailedException + * @throws GuzzleException + * @throws OAuthClientException + * @throws MissingClientSecretException + * @throws \JsonException + * @throws IdentityProviderException + * @throws Exception + * @throws MissingActionNameException */ public function prepareUntaggedQuery(): void { diff --git a/Classes/AssetSource/CantoAssetProxyQueryResult.php b/Classes/AssetSource/CantoAssetProxyQueryResult.php index c03dcfa..844948e 100644 --- a/Classes/AssetSource/CantoAssetProxyQueryResult.php +++ b/Classes/AssetSource/CantoAssetProxyQueryResult.php @@ -27,24 +27,10 @@ */ class CantoAssetProxyQueryResult implements AssetProxyQueryResultInterface { - /** - * @var array - */ - private $assetProxies; - - /** - * @var int - */ - private $numberOfAssetProxies; + private ?array $assetProxies = null; + private ?int $numberOfAssetProxies = null; + private \ArrayIterator $assetProxiesIterator; - /** - * @var \ArrayIterator - */ - private $assetProxiesIterator; - - /** - * @param CantoAssetProxyQuery $query - */ public function __construct(private CantoAssetProxyQuery $query) { } @@ -96,48 +82,104 @@ public function toArray(): array return $this->assetProxies; } + /** + * @throws AuthenticationFailedException + * @throws OAuthClientException + * @throws CacheException + * @throws GuzzleException + * @throws InvalidDataException + */ public function current() { $this->initialize(); return $this->assetProxiesIterator->current(); } + /** + * @throws OAuthClientException + * @throws AuthenticationFailedException + * @throws CacheException + * @throws GuzzleException + * @throws InvalidDataException + */ public function next() { $this->initialize(); $this->assetProxiesIterator->next(); } + /** + * @throws OAuthClientException + * @throws AuthenticationFailedException + * @throws CacheException + * @throws GuzzleException + * @throws InvalidDataException + */ public function key() { $this->initialize(); return $this->assetProxiesIterator->key(); } + /** + * @throws OAuthClientException + * @throws AuthenticationFailedException + * @throws CacheException + * @throws GuzzleException + * @throws InvalidDataException + */ public function valid(): bool { $this->initialize(); return $this->assetProxiesIterator->valid(); } + /** + * @throws OAuthClientException + * @throws AuthenticationFailedException + * @throws CacheException + * @throws GuzzleException + * @throws InvalidDataException + */ public function rewind() { $this->initialize(); $this->assetProxiesIterator->rewind(); } + /** + * @throws AuthenticationFailedException + * @throws OAuthClientException + * @throws CacheException + * @throws GuzzleException + * @throws InvalidDataException + */ public function offsetExists($offset): bool { $this->initialize(); return $this->assetProxiesIterator->offsetExists($offset); } + /** + * @throws AuthenticationFailedException + * @throws OAuthClientException + * @throws CacheException + * @throws GuzzleException + * @throws InvalidDataException + */ public function offsetGet($offset) { $this->initialize(); return $this->assetProxiesIterator->offsetGet($offset); } + /** + * @throws AuthenticationFailedException + * @throws OAuthClientException + * @throws CacheException + * @throws GuzzleException + * @throws InvalidDataException + */ public function offsetSet($offset, $value) { $this->initialize(); diff --git a/Classes/AssetSource/CantoAssetProxyRepository.php b/Classes/AssetSource/CantoAssetProxyRepository.php index 60d2080..db6d7ca 100644 --- a/Classes/AssetSource/CantoAssetProxyRepository.php +++ b/Classes/AssetSource/CantoAssetProxyRepository.php @@ -15,11 +15,16 @@ use Flownative\Canto\Exception\AssetNotFoundException; use Flownative\Canto\Exception\AuthenticationFailedException; +use Flownative\Canto\Exception\MissingClientSecretException; use Flownative\OAuth2\Client\OAuthClientException; use GuzzleHttp\Exception\GuzzleException; use GuzzleHttp\Utils; +use League\OAuth2\Client\Provider\Exception\IdentityProviderException; use Neos\Cache\Exception as CacheException; use Neos\Cache\Exception\InvalidDataException; +use Neos\Cache\Frontend\VariableFrontend; +use Neos\Flow\Http\Exception; +use Neos\Flow\Mvc\Routing\Exception\MissingActionNameException; use Neos\Media\Domain\Model\AssetCollection; use Neos\Media\Domain\Model\AssetSource\AssetProxy\AssetProxyInterface; use Neos\Media\Domain\Model\AssetSource\AssetProxyQueryResultInterface; @@ -36,28 +41,19 @@ */ class CantoAssetProxyRepository implements AssetProxyRepositoryInterface, SupportsSortingInterface, SupportsTaggingInterface, SupportsCollectionsInterface { - /** - * @var AssetCollection - */ - private $activeAssetCollection; + private ?AssetCollection $activeAssetCollection = null; + private string $assetTypeFilter = 'All'; + private array $orderings = []; /** - * @param CantoAssetSource $assetSource + * @var VariableFrontend $apiResponsesCache */ + protected $apiResponsesCache; + public function __construct(private CantoAssetSource $assetSource) { } - /** - * @var string - */ - private $assetTypeFilter = 'All'; - - /** - * @var array - */ - private $orderings = []; - /** * @throws AssetNotFoundException * @throws OAuthClientException @@ -90,9 +86,6 @@ public function getAssetProxy(string $identifier): AssetProxyInterface return CantoAssetProxy::fromJsonObject($responseObject, $this->assetSource); } - /** - * @param AssetTypeFilter|null $assetType - */ public function filterByType(AssetTypeFilter $assetType = null): void { $this->assetTypeFilter = (string)$assetType ?: 'All'; @@ -126,9 +119,15 @@ public function findByTag(Tag $tag): AssetProxyQueryResultInterface } /** - * @throws OAuthClientException - * @throws GuzzleException + * @return AssetProxyQueryResultInterface * @throws AuthenticationFailedException + * @throws Exception + * @throws GuzzleException + * @throws IdentityProviderException + * @throws MissingActionNameException + * @throws MissingClientSecretException + * @throws OAuthClientException + * @throws \JsonException */ public function findUntagged(): AssetProxyQueryResultInterface { @@ -141,14 +140,18 @@ public function findUntagged(): AssetProxyQueryResultInterface } /** - * @throws OAuthClientException - * @throws GuzzleException * @throws AuthenticationFailedException + * @throws Exception + * @throws GuzzleException + * @throws IdentityProviderException + * @throws MissingActionNameException + * @throws MissingClientSecretException + * @throws OAuthClientException + * @throws \JsonException */ public function countAll(): int { - $query = new CantoAssetProxyQuery($this->assetSource); - return $query->count(); + return (new CantoAssetProxyQuery($this->assetSource))->count(); } /** @@ -167,9 +170,14 @@ public function orderBy(array $orderings): void } /** - * @throws OAuthClientException - * @throws GuzzleException * @throws AuthenticationFailedException + * @throws GuzzleException + * @throws OAuthClientException + * @throws MissingClientSecretException + * @throws \JsonException + * @throws IdentityProviderException + * @throws Exception + * @throws MissingActionNameException */ public function countUntagged(): int { @@ -181,22 +189,23 @@ public function countUntagged(): int return $query->count(); } - /** - * @param Tag $tag - * @return int - */ public function countByTag(Tag $tag): int { + $identifier = 'countByTag-' . sha1($tag->getLabel()); + $cacheEntry = $this->apiResponsesCache->get($identifier); + if ($cacheEntry !== false) { + return $cacheEntry; + } + try { - return ($this->findByTag($tag))->count(); - } catch (AuthenticationFailedException|OAuthClientException|GuzzleException $e) { - return 0; + $count = $this->findByTag($tag)->count(); + $this->apiResponsesCache->set($identifier, $count, ['countByTag']); + return $count; + } catch (AuthenticationFailedException|OAuthClientException|GuzzleException) { } + return 0; } - /** - * @param AssetCollection|null $assetCollection - */ public function filterByCollection(AssetCollection $assetCollection = null): void { $this->activeAssetCollection = $assetCollection; diff --git a/Classes/AssetSource/CantoAssetSource.php b/Classes/AssetSource/CantoAssetSource.php index 67255e8..e319d1b 100644 --- a/Classes/AssetSource/CantoAssetSource.php +++ b/Classes/AssetSource/CantoAssetSource.php @@ -27,55 +27,16 @@ class CantoAssetSource implements AssetSourceInterface { public const ASSET_SOURCE_IDENTIFIER = 'flownative-canto'; - /** - * @var bool - */ - private $autoTaggingEnabled = false; - - /** - * @var string - */ - private $autoTaggingInUseTag = 'used-by-neos'; - - /** - * @var string - */ - private $assetSourceIdentifier; - - /** - * @var CantoAssetProxyRepository - */ - private $assetProxyRepository; - - /** - * @var string - */ - private $apiBaseUri; - - /** - * @var string - */ - private $appId; - - /** - * @var string - */ - private $appSecret; - - /** - * @var CantoClient - */ - private $cantoClient; - - /** - * @var string - */ - private $iconPath; - - /** - * @var string - */ - private $description; + private bool $autoTaggingEnabled = false; + private string $autoTaggingInUseTag = 'used-by-neos'; + private string $assetSourceIdentifier; + private ?CantoAssetProxyRepository $assetProxyRepository = null; + private string $apiBaseUri; + private string $appId = ''; + private string $appSecret = ''; + private ?CantoClient $cantoClient = null; + private string $iconPath; + private string $description; /** * @Flow\Inject @@ -88,10 +49,6 @@ class CantoAssetSource implements AssetSourceInterface */ protected $assetProxyCache; - /** - * @param string $assetSourceIdentifier - * @param array $assetSourceOptions - */ public function __construct(string $assetSourceIdentifier, private array $assetSourceOptions) { if (preg_match('/^[a-z][a-z0-9-]{0,62}[a-z]$/', $assetSourceIdentifier) !== 1) { @@ -139,7 +96,7 @@ public function __construct(string $assetSourceIdentifier, private array $assetS } } - if ($this->appId === null || $this->appSecret === null) { + if ($this->appId === '' || $this->appSecret === '') { throw new \InvalidArgumentException(sprintf('No app id or app secret specified for Canto asset source "%s".', $assetSourceIdentifier), 1632468673); } } @@ -159,10 +116,7 @@ public function getLabel(): string return 'Canto'; } - /** - * @return CantoAssetProxyRepository - */ - public function getAssetProxyRepository(): AssetProxyRepositoryInterface + public function getAssetProxyRepository(): AssetProxyRepositoryInterface|CantoAssetProxyRepository { if ($this->assetProxyRepository === null) { $this->assetProxyRepository = new CantoAssetProxyRepository($this); diff --git a/Classes/Command/CantoCommandController.php b/Classes/Command/CantoCommandController.php index 519e6ef..334ed1c 100644 --- a/Classes/Command/CantoCommandController.php +++ b/Classes/Command/CantoCommandController.php @@ -7,11 +7,15 @@ use Flownative\Canto\AssetSource\CantoAssetProxyRepository; use Flownative\Canto\AssetSource\CantoAssetSource; use Flownative\Canto\Exception\AuthenticationFailedException; +use Flownative\Canto\Exception\MissingClientSecretException; use Flownative\OAuth2\Client\OAuthClientException; use GuzzleHttp\Exception\GuzzleException; +use League\OAuth2\Client\Provider\Exception\IdentityProviderException; use Neos\Flow\Annotations as Flow; use Neos\Flow\Cli\CommandController; use Neos\Flow\Cli\Exception\StopCommandException; +use Neos\Flow\Http\Exception; +use Neos\Flow\Mvc\Routing\Exception\MissingActionNameException; use Neos\Flow\Persistence\Exception\IllegalObjectTypeException; use Neos\Media\Domain\Model\Asset; use Neos\Media\Domain\Model\AssetCollection; @@ -51,7 +55,7 @@ class CantoCommandController extends CommandController * @Flow\InjectConfiguration(path="mapping", package="Flownative.Canto") * @var array */ - protected $mapping = []; + protected array $mapping = []; /** * Tag used assets @@ -76,8 +80,6 @@ public function tagUsedAssetsCommand(string $assetSource = CantoAssetSource::ASS $this->quit(1); } - $assetProxyRepository = $cantoAssetSource->getAssetProxyRepository(); - assert($assetProxyRepository instanceof CantoAssetProxyRepository); $cantoAssetSource->getAssetProxyCache()->flush(); foreach ($this->assetRepository->iterate($iterator) as $asset) { @@ -128,11 +130,16 @@ public function tagUsedAssetsCommand(string $assetSource = CantoAssetSource::ASS * * @param string $assetSourceIdentifier Name of the canto asset source * @param bool $quiet If set, only errors will be displayed. + * @throws AuthenticationFailedException * @throws GuzzleException * @throws IllegalObjectTypeException * @throws OAuthClientException * @throws StopCommandException - * @throws AuthenticationFailedException + * @throws MissingClientSecretException + * @throws \JsonException + * @throws IdentityProviderException + * @throws Exception + * @throws MissingActionNameException */ public function importCustomFieldsAsCollectionsAndTagsCommand(string $assetSourceIdentifier = CantoAssetSource::ASSET_SOURCE_IDENTIFIER, bool $quiet = true): void { diff --git a/Classes/Controller/AuthorizationController.php b/Classes/Controller/AuthorizationController.php index 86259c5..afdd6e5 100644 --- a/Classes/Controller/AuthorizationController.php +++ b/Classes/Controller/AuthorizationController.php @@ -18,7 +18,11 @@ use Flownative\OAuth2\Client\OAuthClientException; use GuzzleHttp\Psr7\Uri; use Neos\Flow\Annotations as Flow; +use Neos\Flow\Http\Exception; use Neos\Flow\Mvc\Controller\ActionController; +use Neos\Flow\Mvc\Exception\UnsupportedRequestTypeException; +use Neos\Flow\Mvc\Routing\Exception\MissingActionNameException; +use Neos\Flow\Persistence\Exception\IllegalObjectTypeException; use Neos\Flow\Security\Context; use Neos\Media\Domain\Service\AssetSourceService; @@ -36,12 +40,23 @@ class AuthorizationController extends ActionController */ protected $assetSourceService; + /** + * @throws Exception + * @throws MissingActionNameException + */ public function neededAction(string $returnUri): void { $this->view->assign('startUri', $this->uriBuilder->uriFor('start')); $this->view->assign('returnUri', $returnUri); } + /** + * @throws OAuthClientException + * @throws MissingActionNameException + * @throws Exception + * @throws UnsupportedRequestTypeException + * @throws \JsonException + */ public function startAction(): void { $appId = $this->assetSourceService->getAssetSources()[CantoAssetSource::ASSET_SOURCE_IDENTIFIER]->getAppId(); @@ -73,6 +88,8 @@ public function startAction(): void * Finish OAuth2 authorization * * @throws OAuthClientException + * @throws \JsonException + * @throws IllegalObjectTypeException */ public function finishAction(string $state, string $code, string $scope = ''): void { diff --git a/Classes/Domain/Model/AccountAuthorization.php b/Classes/Domain/Model/AccountAuthorization.php index 2c0b81f..f251075 100644 --- a/Classes/Domain/Model/AccountAuthorization.php +++ b/Classes/Domain/Model/AccountAuthorization.php @@ -25,12 +25,12 @@ class AccountAuthorization * @Identity() * @var string */ - protected $flowAccountIdentifier; + protected string $flowAccountIdentifier; /** * @var string */ - protected $authorizationId; + protected string $authorizationId; public function getFlowAccountIdentifier(): string { diff --git a/Classes/Package.php b/Classes/Package.php index 2ee5294..a462725 100644 --- a/Classes/Package.php +++ b/Classes/Package.php @@ -27,7 +27,7 @@ class Package extends BasePackage * @param Bootstrap $bootstrap The current bootstrap * @return void */ - public function boot(Bootstrap $bootstrap) + public function boot(Bootstrap $bootstrap): void { $dispatcher = $bootstrap->getSignalSlotDispatcher(); $dispatcher->connect( diff --git a/Classes/Service/AssetUpdateService.php b/Classes/Service/AssetUpdateService.php index 181db6c..688e91b 100644 --- a/Classes/Service/AssetUpdateService.php +++ b/Classes/Service/AssetUpdateService.php @@ -14,13 +14,18 @@ */ use Flownative\Canto\AssetSource\CantoAssetSource; +use Flownative\Canto\Exception\AssetNotFoundException; +use Flownative\Canto\Exception\AuthenticationFailedException; +use Flownative\OAuth2\Client\OAuthClientException; +use GuzzleHttp\Exception\GuzzleException; +use Neos\Cache\Exception; +use Neos\Cache\Exception\InvalidDataException; use Neos\Flow\Annotations as Flow; use Neos\Flow\Log\ThrowableStorageInterface; use Neos\Flow\Log\Utility\LogEnvironment; use Neos\Flow\Persistence\PersistenceManagerInterface; use Neos\Flow\ResourceManagement\ResourceManager; use Neos\Media\Domain\Model\AssetInterface; -use Neos\Media\Domain\Model\AssetSource\AssetSourceInterface; use Neos\Media\Domain\Model\ImageVariant; use Neos\Media\Domain\Repository\AssetRepository; use Neos\Media\Domain\Repository\ImportedAssetRepository; @@ -121,7 +126,7 @@ public function handleAssetMetadataUpdated(array $payload): bool $this->persistenceManager->persistAll(); return true; - } catch (\Exception) { + } catch (\Throwable) { return false; } } @@ -142,11 +147,20 @@ public function handleNewAssetVersionAdded(array $payload): bool $this->persistenceManager->persistAll(); return true; - } catch (\Exception) { + } catch (\Throwable) { return false; } } + /** + * @throws OAuthClientException + * @throws AuthenticationFailedException + * @throws Exception + * @throws AssetNotFoundException + * @throws \Neos\Flow\ResourceManagement\Exception + * @throws GuzzleException + * @throws InvalidDataException + */ private function replaceAsset(string $identifier): void { $importedAsset = $this->importedAssetRepository->findOneByAssetSourceIdentifierAndRemoteAssetIdentifier(CantoAssetSource::ASSET_SOURCE_IDENTIFIER, $identifier); @@ -166,7 +180,7 @@ private function replaceAsset(string $identifier): void $proxy = $this->getAssetSource()->getAssetProxyRepository()->getAssetProxy($identifier); $newResource = $this->resourceManager->importResource($proxy->getImportStream()); } catch (\Exception $e) { - $this->logger->debug(sprintf('Could not import resource for asset %s from %s, exception: %s', $localAssetIdentifier, $identifier, $this->throwableStorage->logThrowable($e)), LogEnvironment::fromMethodName(__METHOD__));; + $this->logger->debug(sprintf('Could not import resource for asset %s from %s, exception: %s', $localAssetIdentifier, $identifier, $this->throwableStorage->logThrowable($e)), LogEnvironment::fromMethodName(__METHOD__)); throw $e; } $newResource->setFilename($proxy->getFilename()); @@ -174,7 +188,7 @@ private function replaceAsset(string $identifier): void $this->logger->debug(sprintf('Replaced resource %s with %s on asset %s from Canto asset %s', $localAsset->getResource()->getSha1(), $newResource->getSha1(), $localAssetIdentifier, $identifier), LogEnvironment::fromMethodName(__METHOD__)); // … to delete it here! - $this->logger->debug(sprintf('Trying to delete replaced resource: %s (%s)', $previousResource->getFilename(), $previousResource->getSha1()), LogEnvironment::fromMethodName(__METHOD__));; + $this->logger->debug(sprintf('Trying to delete replaced resource: %s (%s)', $previousResource->getFilename(), $previousResource->getSha1()), LogEnvironment::fromMethodName(__METHOD__)); $this->resourceManager->deleteResource($previousResource); } @@ -195,7 +209,7 @@ private function flushProxyForAsset(string $identifier): void } } - private function getAssetSource(): AssetSourceInterface + private function getAssetSource(): CantoAssetSource { /** @var CantoAssetSource $assetSource */ $assetSource = $this->assetSourceService->getAssetSources()[CantoAssetSource::ASSET_SOURCE_IDENTIFIER]; diff --git a/Classes/Service/CantoClient.php b/Classes/Service/CantoClient.php index c1cd7e2..cc1213d 100644 --- a/Classes/Service/CantoClient.php +++ b/Classes/Service/CantoClient.php @@ -26,6 +26,8 @@ use GuzzleHttp\Psr7\ServerRequest; use GuzzleHttp\Psr7\Uri; use League\OAuth2\Client\Provider\Exception\IdentityProviderException; +use Neos\Cache\Exception; +use Neos\Cache\Frontend\VariableFrontend; use Neos\Flow\Annotations as Flow; use Neos\Flow\Core\Bootstrap; use Neos\Flow\Http\Exception as HttpException; @@ -47,6 +49,9 @@ final class CantoClient { protected bool $allowClientCredentialsAuthentication = false; + private ?Authorization $authorization = null; + private Client $httpClient; + /** * @Flow\Inject * @var Bootstrap @@ -71,22 +76,6 @@ final class CantoClient */ protected $accountAuthorizationRepository; - /** - * @var Authorization - */ - private $authorization; - - /** - * @var Client - */ - private $httpClient; - - /** - * @param string $apiBaseUri - * @param string $appId - * @param string $appSecret - * @param string $serviceName - */ public function __construct(private string $apiBaseUri, protected string $appId, protected string $appSecret, private string $serviceName) { $this->httpClient = new Client(['allow_redirects' => true]); @@ -165,6 +154,7 @@ private function redirectToUri(string $uri): void * @throws MissingClientSecretException * @throws OAuthClientException * @throws IdentityProviderException + * @throws \JsonException */ public function getFile(string $assetProxyId): ResponseInterface { @@ -188,6 +178,7 @@ public function updateFile(string $id, array $metadata): ResponseInterface * @throws MissingActionNameException * @throws MissingClientSecretException * @throws OAuthClientException + * @throws \JsonException */ public function search(string $keyword, array $formatTypes, string $customQueryPart = '', int $offset = 0, int $limit = 50, array $orderings = []): ResponseInterface { @@ -225,13 +216,21 @@ public function search(string $keyword, array $formatTypes, string $customQueryP * @throws MissingActionNameException * @throws MissingClientSecretException * @throws OAuthClientException - * @todo perhaps cache the result + * @throws \JsonException + * @throws Exception */ public function getCustomFields(): array { + $cacheEntry = $this->apiResponsesCache->get('customFields'); + if ($cacheEntry !== false) { + return $cacheEntry; + } + $response = $this->sendAuthenticatedRequest('custom/field'); if ($response->getStatusCode() === 200) { - return json_decode($response->getBody()->getContents(), false, 512, JSON_THROW_ON_ERROR); + $customFields = json_decode($response->getBody()->getContents(), false, 512, JSON_THROW_ON_ERROR); + $this->apiResponsesCache->set('customFields', $customFields); + return $customFields; } return []; } @@ -244,6 +243,7 @@ public function getCustomFields(): array * @throws MissingActionNameException * @throws MissingClientSecretException * @throws OAuthClientException + * @throws \JsonException */ public function user(): array { @@ -262,6 +262,7 @@ public function user(): array * @throws MissingActionNameException * @throws MissingClientSecretException * @throws OAuthClientException + * @throws \JsonException */ public function tree(): array { @@ -316,11 +317,8 @@ public function directUri(string $assetProxyId): ?Uri /** * Returns a prepared request to an OAuth 2.0 service provider using Bearer token authentication * - * @param Authorization $authorization - * @param string $uriPathAndQuery A relative URI of the web server, prepended by the base URI - * @param string $method The HTTP method, for example "GET" or "POST" - * @param array $bodyFields Associative array of body fields to send (optional) * @throws OAuthClientException + * @throws \JsonException */ private function getAuthenticatedRequest(Authorization $authorization, string $uriPathAndQuery, string $method = 'GET', array $bodyFields = []): RequestInterface { @@ -350,6 +348,7 @@ private function getAuthenticatedRequest(Authorization $authorization, string $u * @throws MissingActionNameException * @throws MissingClientSecretException * @throws OAuthClientException + * @throws \JsonException */ public function sendAuthenticatedRequest(string $uriPathAndQuery, string $method = 'GET', array $bodyFields = []): Response { diff --git a/Classes/Service/CantoOAuthClient.php b/Classes/Service/CantoOAuthClient.php index 2f1f260..8d3a120 100644 --- a/Classes/Service/CantoOAuthClient.php +++ b/Classes/Service/CantoOAuthClient.php @@ -20,8 +20,12 @@ use Flownative\OAuth2\Client\OAuthClientException; use League\OAuth2\Client\Provider\GenericProvider; use Neos\Flow\Annotations as Flow; +use Neos\Flow\Http\Exception; use Neos\Flow\Http\Helper\UriHelper; +use Neos\Flow\Http\RequestHandler; use Neos\Flow\Mvc\ActionRequest; +use Neos\Flow\Mvc\Routing\Exception\MissingActionNameException; +use Neos\Flow\Persistence\Exception\IllegalObjectTypeException; use Neos\Flow\Persistence\PersistenceManagerInterface; use Neos\Flow\Security\Context; use Psr\Http\Message\UriInterface; @@ -86,6 +90,10 @@ public function getClientId(): string throw new \RuntimeException('not implemented'); } + /** + * @throws Exception + * @throws MissingActionNameException + */ protected function createOAuthProvider(string $clientId, string $clientSecret): GenericProvider { return new CantoOAuthProvider([ @@ -100,6 +108,10 @@ protected function createOAuthProvider(string $clientId, string $clientSecret): ]); } + /** + * @throws Exception + * @throws MissingActionNameException + */ public function renderFinishAuthorizationUri(): string { if (FLOW_SAPITYPE === 'CLI') { @@ -122,11 +134,16 @@ public function renderFinishAuthorizationUri(): string ); } + /** + * @throws OAuthClientException + * @throws IllegalObjectTypeException + * @throws \JsonException + */ public function finishAuthorization(string $stateIdentifier, string $code, string $scope): UriInterface { $stateFromCache = $this->stateCache->get($stateIdentifier); if (empty($stateFromCache)) { - throw new OAuthClientException(sprintf('OAuth2 (%s): Finishing authorization failed because oAuth state %s could not be retrieved from the state cache.', CantoOAuthClient::getServiceType(), $stateIdentifier), 1627046882); + throw new OAuthClientException(sprintf('OAuth2 (%s): Finishing authorization failed because oAuth state %s could not be retrieved from the state cache.', self::getServiceType(), $stateIdentifier), 1627046882); } $authorizationId = $stateFromCache['authorizationId']; @@ -149,7 +166,7 @@ public function finishAuthorization(string $stateIdentifier, string $code, strin $accountAuthorization->setAuthorizationId($authorizationId); $this->persistenceManager->allowObject($accountAuthorization); - $queryParameterName = CantoOAuthClient::generateAuthorizationIdQueryParameterName(CantoAssetSource::ASSET_SOURCE_IDENTIFIER); + $queryParameterName = self::generateAuthorizationIdQueryParameterName(CantoAssetSource::ASSET_SOURCE_IDENTIFIER); $queryParameters = UriHelper::parseQueryIntoArguments($returnUri); unset($queryParameters[$queryParameterName]); return UriHelper::uriWithArguments($returnUri, $queryParameters); diff --git a/Classes/Service/CantoOAuthProvider.php b/Classes/Service/CantoOAuthProvider.php index b6beeba..5e3c889 100644 --- a/Classes/Service/CantoOAuthProvider.php +++ b/Classes/Service/CantoOAuthProvider.php @@ -27,7 +27,6 @@ final class CantoOAuthProvider extends GenericProvider * Requests an access token using a specified grant and option set. * * @param mixed $grant - * @return AccessTokenInterface * @throws IdentityProviderException */ public function getAccessToken($grant, array $options = []): AccessTokenInterface diff --git a/Configuration/Caches.yaml b/Configuration/Caches.yaml index 687cabe..328b04d 100644 --- a/Configuration/Caches.yaml +++ b/Configuration/Caches.yaml @@ -3,3 +3,9 @@ Flownative_Canto_AssetProxy: backend: Neos\Cache\Backend\FileBackend backendOptions: defaultLifetime: 0 + +Flownative_Canto_ApiResponses: + frontend: Neos\Cache\Frontend\VariableFrontend + backend: Neos\Cache\Backend\FileBackend + backendOptions: + defaultLifetime: 3600 diff --git a/Configuration/Objects.yaml b/Configuration/Objects.yaml index 9c6cd6f..e6ec5c6 100644 --- a/Configuration/Objects.yaml +++ b/Configuration/Objects.yaml @@ -8,6 +8,26 @@ Flownative\Canto\AssetSource\CantoAssetSource: 1: value: Flownative_Canto_AssetProxy +Flownative\Canto\AssetSource\CantoAssetProxyRepository: + properties: + apiResponsesCache: + object: + factoryObjectName: Neos\Flow\Cache\CacheManager + factoryMethodName: getCache + arguments: + 1: + value: Flownative_Canto_ApiResponses + +Flownative\Canto\Service\CantoClient: + properties: + apiResponsesCache: + object: + factoryObjectName: Neos\Flow\Cache\CacheManager + factoryMethodName: getCache + arguments: + 1: + value: Flownative_Canto_ApiResponses + Flownative\Canto\Service\CantoOAuthClient: properties: stateCache: