Skip to content

Commit

Permalink
Merge branch '11.5-bug-context-menu-actions' into '11.5'
Browse files Browse the repository at this point in the history
Options in context menu in file list should be checked for availability

See merge request typo3-commons/mkcontentai!31
  • Loading branch information
hannesbochmann committed Mar 25, 2024
2 parents ab422e7 + 819793e commit dfe8b4a
Show file tree
Hide file tree
Showing 2 changed files with 193 additions and 60 deletions.
145 changes: 120 additions & 25 deletions Classes/Backend/Hooks/FileListButtonHook.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,21 @@
use TYPO3\CMS\Core\Http\ServerRequestFactory;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\FileInterface;
use TYPO3\CMS\Core\Resource\Folder;
use TYPO3\CMS\Core\Resource\ResourceInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
use TYPO3\CMS\Filelist\FileListEditIconHookInterface;

class FileListButtonHook implements FileListEditIconHookInterface
{
protected ContentAiItemProvider $contentAiItemProvider;
protected UriBuilder $uriBuilder;

public function __construct()
{
$this->contentAiItemProvider = GeneralUtility::makeInstance(ContentAiItemProvider::class, '', '');
$this->uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
}

/**
Expand All @@ -43,54 +47,78 @@ public function __construct()
public function manipulateEditIcons(&$cells, &$parentObject): void
{
$request = ServerRequestFactory::fromGlobals();
$resource = $cells['__fileOrFolderObject'];

if (!$resource instanceof File) {
/** @var string $route */
$route = $request->getQueryParams()['route'] ?? '';
$currentUri = $request->getUri()->getPath();

if (!$this->isFileListAction($currentUri, $route)) {
return;
}

foreach ($this->contentAiItemProvider->getItemsConfiguration() as $actionName) {
$route = $request->getQueryParams()['route'] ?? null;
$currentUri = $request->getUri()->getPath();
$resource = null;
$actions = [];

if ('/typo3/module/file/FilelistList' === $currentUri || '/module/file/FilelistList' === $route) {
('alt' === $actionName['callbackAction'])
? $url = $this->buildUriToControllerAction($resource, $this->mapAction($actionName['callbackAction']), 'AiText')
: $url = $this->buildUriToControllerAction($resource, $this->mapAction($actionName['callbackAction']), 'AiImage');
if ($cells['__fileOrFolderObject'] instanceof File) {
$resource = $cells['__fileOrFolderObject'];
$actions = $this->contentAiItemProvider->getItemsConfiguration();
$this->contentAiItemProvider->setContext('sys_file', $this->buildFileIdentifier($resource));
}

$htmlCode = '<a href="'.$url.'" class="btn btn-default" title="'.(LocalizationUtility::translate($actionName['label']) ?? '').'"><span class="t3js-icon icon icon-size-small icon-state-default">
<span class="icon-markup">
<svg class="icon-color"><use xlink:href="/typo3/sysext/core/Resources/Public/Icons/T3Icons/sprites/actions.svg#actions-rocket" /></svg>
</span></span></a>';
$button = new CustomButton();
$cells[$actionName['callbackAction']] = $button->setHtmlSource($htmlCode);
if ($cells['__fileOrFolderObject'] instanceof Folder) {
$resource = $cells['__fileOrFolderObject'];
$actions = $this->contentAiItemProvider->getItemsConfigurationForFolder();
$this->contentAiItemProvider->setContext('sys_file_storage', $this->buildFolderIdentifier($resource));
}

/**
* @var string $key
*/
foreach ($actions as $key => $actionName) {
if (!$this->canRenderButton($key, $actionName['type'])) {
continue;
}

$targetControllerClassName = $this->determineTargetControllerClassName($actionName['callbackAction']);
$url = $this->generateUrl($resource, $actionName['callbackAction'], $targetControllerClassName);
$cells[$actionName['callbackAction']] =
(new CustomButton())->setHtmlSource($this->generateHtmlButton($url, $actionName['label']));
}
}

public function buildUriToControllerAction(FileInterface $file, string $actionName, string $controllerName): string
private function buildUriToControllerActionFile(File $file, string $actionName, string $controllerName): string
{
/**
* @var UriBuilder $uriBuilder
*/
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
$promptUrl = $uriBuilder->buildUriFromRoutePath(
$promptUrl = $this->uriBuilder->buildUriFromRoutePath(
'/module/system/MkcontentaiContentai',
[
'tx_mkcontentai_system_mkcontentaicontentai' => [
'action' => $actionName,
'controller' => $controllerName,
'file' => $file->getStorage()->getUid().':'.$file->getIdentifier(),
'file' => $this->buildFileIdentifier($file),
],
]
);

$url = $promptUrl->__toString();
return $promptUrl->__toString();
}

private function buildUriToControllerActionFolder(Folder $folder, string $actionName, string $controllerName): string
{
$promptUrl = $this->uriBuilder->buildUriFromRoutePath(
'/module/system/MkcontentaiContentai',
[
'tx_mkcontentai_system_mkcontentaicontentai' => [
'action' => $actionName,
'controller' => $controllerName,
'folderName' => $this->buildFolderIdentifier($folder),
],
]
);

return $url;
return $promptUrl->__toString();
}

public function mapAction(string $actionName): string
private function mapAction(string $actionName): string
{
if ('extend' === $actionName) {
return 'cropAndExtend';
Expand All @@ -102,4 +130,71 @@ public function mapAction(string $actionName): string

return $actionName;
}

private function buildFileIdentifier(FileInterface $file): string
{
return $file->getStorage()->getUid().':'.$file->getIdentifier();
}

private function buildFolderIdentifier(Folder $folder): string
{
return $folder->getCombinedIdentifier();
}

private function isFileListAction(string $currentUri, string $route): bool
{
return '/typo3/module/file/FilelistList' === $currentUri || '/module/file/FilelistList' === $route;
}

private function canRenderButton(string $key, string $type): bool
{
return $this->contentAiItemProvider->canRender($key, $type);
}

/**
* @param File $resource
*/
private function generateUriFile(ResourceInterface $resource, string $actionName, string $targetControllerClassName): string
{
return $this->buildUriToControllerActionFile($resource, $this->mapAction($actionName), $targetControllerClassName);
}

/**
* @param Folder $resource
*/
private function generateUriFolder(ResourceInterface $resource, string $actionName, string $targetControllerClassName): string
{
return $this->buildUriToControllerActionFolder($resource, $this->mapAction($actionName), $targetControllerClassName);
}

/**
* @throws \InvalidArgumentException
*/
private function generateUrl(?ResourceInterface $resource, string $callbackAction, string $targetControllerClassName): string
{
$resourceIdentifier = null === $resource ? 'unknown' : $resource->getIdentifier();

if ($resource instanceof File) {
return $this->generateUriFile($resource, $this->mapAction($callbackAction), $targetControllerClassName);
}

if ($resource instanceof Folder) {
return $this->generateUriFolder($resource, $this->mapAction($callbackAction), $targetControllerClassName);
}

throw new \InvalidArgumentException(sprintf('Provided resource is not file or folder: %s', $resourceIdentifier));
}

private function generateHtmlButton(string $url, string $label): string
{
return '<a href="'.$url.'" class="btn btn-default" title="'.(LocalizationUtility::translate($label) ?? '').'"><span class="t3js-icon icon icon-size-small icon-state-default">
<span class="icon-markup">
<svg class="icon-color"><use xlink:href="/typo3/sysext/core/Resources/Public/Icons/T3Icons/sprites/actions.svg#actions-rocket" /></svg>
</span></span></a>';
}

private function determineTargetControllerClassName(string $callbackAction): string
{
return 'alt' === $callbackAction || 'altTexts' === $callbackAction ? 'AiText' : 'AiImage';
}
}
108 changes: 73 additions & 35 deletions Classes/ContextMenu/ContentAiItemProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@
* The TYPO3 project - inspiring people to share!
*/

use DMK\MkContentAi\Controller\AiImageController;
use DMK\MkContentAi\Controller\SettingsController;
use DMK\MkContentAi\Http\Client\OpenAiClient;
use DMK\MkContentAi\Http\Client\StabilityAiClient;
use TYPO3\CMS\Backend\ContextMenu\ItemProviders\AbstractProvider;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Core\Http\Uri;
Expand Down Expand Up @@ -102,6 +106,46 @@ public function getItemsConfiguration(): array
return $this->itemsConfiguration;
}

/**
* @return array<string, array{
* type: string,
* label: string,
* iconIdentifier: string,
* callbackAction: string
* }>
*/
public function getItemsConfigurationForFolder(): array
{
return array_filter($this->itemsConfiguration, fn (array $item) => 'altTexts' === $item['callbackAction']);
}

/**
* This method is called for each item this provider adds and checks if given item can be added.
*/
public function canRender(string $itemName, string $type): bool
{
$imageAiEngine = SettingsController::getImageAiEngine();

if ('item' !== $type) {
return false;
}

$canRender = false;

if (
in_array($itemName, ['fileUpscale', 'fileExtend']) && true === $this->checkAllowedOperationsByClient($itemName, $imageAiEngine)
|| 'fileAlt' === $itemName
) {
return $this->isImage();
}

if ('folderAltTexts' === $itemName) {
return $this->isFolder();
}

return $canRender;
}

/**
* @return array<string>
*
Expand All @@ -117,6 +161,25 @@ protected function getAdditionalAttributes(string $itemName): array
];
}

/**
* Helper method implementing e.g. access check for certain item.
*/
protected function isImage(): bool
{
return 'sys_file' === $this->table && preg_match('/\.(png|jpg)$/', $this->identifier);
}

/**
* Helper method checking if resource is a folder and exist in the storage.
*/
protected function isFolder(): bool
{
$resourceStorage = GeneralUtility::makeInstance(ResourceFactory::class);
$object = $resourceStorage->retrieveFileOrFolderObject($this->identifier);

return $object instanceof Folder;
}

private function generateUrl(string $itemName): Uri
{
$pathInfo = '/module/system/MkcontentaiContentai';
Expand Down Expand Up @@ -156,44 +219,19 @@ private function generateUrl(string $itemName): Uri
}

/**
* This method is called for each item this provider adds and checks if given item can be added.
*/
public function canRender(string $itemName, string $type): bool
{
if ('item' !== $type) {
return false;
}
$canRender = false;
switch ($itemName) {
case 'fileUpscale':
case 'fileExtend':
case 'fileAlt':
$canRender = $this->isImage();
break;
case 'folderAltTexts':
$canRender = $this->isFolder();
break;
}

return $canRender;
}

/**
* Helper method implementing e.g. access check for certain item.
* Helper method checking if current AI Client has permissions for a given operation.
*/
protected function isImage(): bool
private function checkAllowedOperationsByClient(string $itemName, int $imageAiEngine): bool
{
return 'sys_file' === $this->table && preg_match('/\.(png|jpg)$/', $this->identifier);
}
$stabilityAiClient = GeneralUtility::makeInstance(StabilityAiClient::class);
$openAiClient = GeneralUtility::makeInstance(OpenAiClient::class);

/**
* Helper method checking if resource is a folder and exist in the storage.
*/
protected function isFolder(): bool
{
$resourceStorage = GeneralUtility::makeInstance(ResourceFactory::class);
$object = $resourceStorage->retrieveFileOrFolderObject($this->identifier);
foreach ([$stabilityAiClient, $openAiClient] as $aiClient) {
if (get_class($aiClient) === AiImageController::GENERATOR_ENGINE[$imageAiEngine] && in_array(strtolower(str_replace('file', '', $itemName)), $aiClient->getAllowedOperations())) {
return true;
}
}

return $object instanceof Folder;
return false;
}
}

0 comments on commit dfe8b4a

Please sign in to comment.