Your IP : 216.73.217.13


Current Path : /var/www/surf/TYPO3/vendor/mask/mask/Classes/Controller/
Upload File :
Current File : /var/www/surf/TYPO3/vendor/mask/mask/Classes/Controller/AjaxController.php

<?php

declare(strict_types=1);

/*
 * This file is part of the TYPO3 CMS project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read the
 * LICENSE.txt file that was distributed with this source code.
 *
 * The TYPO3 project - inspiring people to share!
 */

namespace MASK\Mask\Controller;

use MASK\Mask\CodeGenerator\HtmlCodeGenerator;
use MASK\Mask\CodeGenerator\SqlCodeGenerator;
use MASK\Mask\CodeGenerator\TcaCodeGenerator;
use MASK\Mask\ConfigurationLoader\ConfigurationLoader;
use MASK\Mask\Definition\ElementTcaDefinition;
use MASK\Mask\Definition\TableDefinitionCollection;
use MASK\Mask\Definition\TcaFieldDefinition;
use MASK\Mask\Domain\Repository\BackendLayoutRepository;
use MASK\Mask\Domain\Repository\StorageRepository;
use MASK\Mask\Enumeration\FieldType;
use MASK\Mask\Enumeration\Tab;
use MASK\Mask\Event\MaskAfterElementDeletedEvent;
use MASK\Mask\Event\MaskAfterElementSavedEvent;
use MASK\Mask\Event\MaskAllowedFieldsEvent;
use MASK\Mask\Loader\LoaderInterface;
use MASK\Mask\Utility\AffixUtility;
use MASK\Mask\Utility\OverrideFieldsUtility;
use MASK\Mask\Utility\TemplatePathUtility;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Backend\View\BackendLayout\BackendLayout;
use TYPO3\CMS\Core\Cache\CacheManager;
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
use TYPO3\CMS\Core\Configuration\Features;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Http\HtmlResponse;
use TYPO3\CMS\Core\Http\JsonResponse;
use TYPO3\CMS\Core\Http\Response;
use TYPO3\CMS\Core\Http\ServerRequest;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Information\Typo3Version;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Messaging\AbstractMessage;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\OnlineMedia\Helpers\OnlineMediaHelperRegistry;
use TYPO3\CMS\Core\Resource\ProcessedFile;
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Fluid\View\StandaloneView;

/**
 * @internal
 */
class AjaxController
{
    protected StorageRepository $storageRepository;
    protected IconFactory $iconFactory;
    protected SqlCodeGenerator $sqlCodeGenerator;
    protected HtmlCodeGenerator $htmlCodeGenerator;
    protected BackendLayoutRepository $backendLayoutRepository;
    protected ResourceFactory $resourceFactory;
    protected ConfigurationLoader $configurationLoader;
    protected FlashMessageQueue $flashMessageQueue;
    protected OnlineMediaHelperRegistry $onlineMediaHelperRegistry;
    protected TableDefinitionCollection $tableDefinitionCollection;
    protected LoaderInterface $loader;
    protected EventDispatcherInterface $eventDispatcher;
    protected Features $features;

    /**
     * @var array<string, string>
     */
    protected array $maskExtensionConfiguration;

    /**
     * @var string[]
     */
    protected static array $folderPathKeys = [
        'content',
        'layouts',
        'partials',
        'backend',
        'layouts_backend',
        'partials_backend',
        'preview',
    ];

    public function __construct(
        StorageRepository $storageRepository,
        IconFactory $iconFactory,
        SqlCodeGenerator $sqlCodeGenerator,
        HtmlCodeGenerator $htmlCodeGenerator,
        BackendLayoutRepository $backendLayoutRepository,
        ResourceFactory $resourceFactory,
        ConfigurationLoader $configurationLoader,
        OnlineMediaHelperRegistry $onlineMediaHelperRegistry,
        TableDefinitionCollection $tableDefinitionCollection,
        LoaderInterface $loader,
        EventDispatcherInterface $eventDispatcher,
        Features $features,
        array $maskExtensionConfiguration
    ) {
        $this->storageRepository = $storageRepository;
        $this->iconFactory = $iconFactory;
        $this->sqlCodeGenerator = $sqlCodeGenerator;
        $this->htmlCodeGenerator = $htmlCodeGenerator;
        $this->backendLayoutRepository = $backendLayoutRepository;
        $this->resourceFactory = $resourceFactory;
        $this->configurationLoader = $configurationLoader;
        $this->onlineMediaHelperRegistry = $onlineMediaHelperRegistry;
        $this->tableDefinitionCollection = $tableDefinitionCollection;
        $this->maskExtensionConfiguration = $maskExtensionConfiguration;
        $this->loader = $loader;
        $this->eventDispatcher = $eventDispatcher;
        $this->features = $features;
        $this->flashMessageQueue = new FlashMessageQueue('mask');
    }

    public function setupComplete(ServerRequestInterface $request): Response
    {
        // If the loader identifier is NOT DEFINED OR "json" AND the path to the mask.json DOES NOT exist, the setup is incomplete.
        $loaderIdentifier = $this->maskExtensionConfiguration['loader_identifier'] ?? '';
        if (($loaderIdentifier === '' || $loaderIdentifier === 'json') && ($this->maskExtensionConfiguration['json'] ?? '') === '') {
            return new JsonResponse(['setupComplete' => 0, 'loader' => $loaderIdentifier]);
        }

        // If the loader identifier IS "json-split" AND the content elements' folder DOES NOT exist, the setup is incomplete.
        if ($loaderIdentifier === 'json-split' && ($this->maskExtensionConfiguration['content_elements_folder'] ?? '') === '') {
            return new JsonResponse(['setupComplete' => 0, 'loader' => $loaderIdentifier]);
        }

        return new JsonResponse(['setupComplete' => 1, 'loader' => $loaderIdentifier]);
    }

    public function autoConfigureSetup(ServerRequestInterface $request): Response
    {
        $parameters = $request->getParsedBody();
        $extensionKey = $parameters['extension'];
        $loader = $parameters['loader'];

        if ($extensionKey === '') {
            return new JsonResponse(['result' => ['error' => 'required!']]);
        }

        if (!ExtensionManagementUtility::isLoaded($extensionKey)) {
            return new JsonResponse(['result' => ['error' => 'must be loaded!']]);
        }

        $extensionPath = 'EXT:' . $extensionKey;
        $extensionConfiguration = new ExtensionConfiguration();
        $configuration = $extensionConfiguration->get('mask');
        $configuration['loader_identifier'] = $loader;
        if ($loader === 'json') {
            $configuration['json'] = $extensionPath . '/Configuration/Mask/mask.json';
        } else {
            $configuration['content_elements_folder'] = $extensionPath . '/Configuration/Mask/ContentElements';
            $configuration['backend_layouts_folder'] = $extensionPath . '/Configuration/Mask/BackendLayouts';
        }
        $configuration['content'] = $extensionPath . '/Resources/Private/Mask/Frontend/Templates';
        $configuration['layouts'] = $extensionPath . '/Resources/Private/Mask/Frontend/Layouts';
        $configuration['partials'] = $extensionPath . '/Resources/Private/Mask/Frontend/Partials';
        $configuration['backend'] = $extensionPath . '/Resources/Private/Mask/Backend/Templates';
        $configuration['layouts_backend'] = $extensionPath . '/Resources/Private/Mask/Backend/Layouts';
        $configuration['partials_backend'] = $extensionPath . '/Resources/Private/Mask/Backend/Partials';
        $configuration['preview'] = $extensionPath . '/Resources/Public/Mask/';

        $extensionConfiguration->set('mask', $configuration);

        return new JsonResponse(['result' => ['error' => '']]);
    }

    public function missingFilesOrFolders(ServerRequestInterface $request): Response
    {
        $missing = 0;
        $missingFolders = $this->getMissingFolders();
        if ($this->getMissingFolders() !== []) {
            $missing = 1;
        }

        // If no elements exist, there can't be any missing templates.
        if (!$this->tableDefinitionCollection->hasTable('tt_content')) {
            return new JsonResponse(['missing' => $missing, 'missingFolders' => $missingFolders, 'missingTemplates' => []]);
        }

        // Loop through each element and check if template exists.
        $missingTemplates = [];
        foreach ($this->tableDefinitionCollection->getTable('tt_content')->elements as $element) {
            if (!$this->contentElementTemplateExists($element->key)) {
                $missingTemplates[$element->key] = TemplatePathUtility::getTemplatePath($this->maskExtensionConfiguration, $element->key, true);
            }
        }

        if ($missingTemplates !== []) {
            return new JsonResponse(['missing' => 1, 'missingFolders' => $missingFolders, 'missingTemplates' => $missingTemplates]);
        }

        return new JsonResponse(['missing' => $missing, 'missingFolders' => $missingFolders, 'missingTemplates' => $missingTemplates]);
    }

    public function fixMissingFilesOrFolders(ServerRequestInterface $request): Response
    {
        foreach ($this->getMissingFolders() as $missingFolderPath) {
            if ($this->createFolder($missingFolderPath)) {
                $this->addFlashMessage('Successfully created directory: ' . $missingFolderPath);
            } else {
                $this->addFlashMessage('Failed to create directory: ' . $missingFolderPath, '', AbstractMessage::ERROR);
            }
        }

        if (!$this->tableDefinitionCollection->hasTable('tt_content')) {
            return new JsonResponse(['messages' => $this->flashMessageQueue->getAllMessagesAndFlush()]);
        }

        $success = true;
        $messages = [];
        $numberTemplateFilesCreated = 0;
        foreach ($this->tableDefinitionCollection->getTable('tt_content')->elements as $element) {
            if (!$this->contentElementTemplateExists($element->key)) {
                try {
                    $success &= $this->createHtml($element->key);
                    $numberTemplateFilesCreated++;
                } catch (\Exception $e) {
                    $success = false;
                    $messages[] = $e->getMessage();
                }
            }
        }

        if (!$success) {
            $this->addFlashMessage('Failed to create template files. See errors below.', '', AbstractMessage::ERROR);
            foreach ($messages as $message) {
                $this->addFlashMessage($message, '', AbstractMessage::ERROR);
            }
        }

        if ($numberTemplateFilesCreated > 0 && $success) {
            $this->addFlashMessage('Successfully created ' . $numberTemplateFilesCreated . ' template files.');
        }

        return new JsonResponse(['messages' => $this->flashMessageQueue->getAllMessagesAndFlush()]);
    }

    /**
     * Generates Fluid HTML for Content elements
     */
    public function showHtmlAction(ServerRequestInterface $request): Response
    {
        $params = $request->getQueryParams();
        $html = $this->htmlCodeGenerator->generateHtml($params['key'], $params['table']);
        $view = GeneralUtility::makeInstance(StandaloneView::class);
        $view->getRenderingContext()->getTemplatePaths()->fillDefaultsByPackageName('mask');
        $view->setTemplate('Ajax/ShowHtml');
        $view->assign('html', $html);
        return new HtmlResponse($view->render());
    }

    public function save(ServerRequestInterface $request): Response
    {
        $params = $request->getParsedBody();
        $isNew = (bool)$params['isNew'];
        $elementKey = $params['element']['key'];
        $fields = json_decode($params['fields'], true);
        try {
            $tableDefinitionCollection = $this->storageRepository->update($params['element'], $fields, $params['type'], $isNew);
        } catch (\Exception $e) {
            $this->addFlashMessage($e->getMessage(), '', AbstractMessage::ERROR);
            return new JsonResponse(['messages' => $this->flashMessageQueue->getAllMessagesAndFlush(), 'hasError' => 1]);
        }
        $this->generateAction($tableDefinitionCollection);
        if ($params['type'] === 'tt_content') {
            try {
                $this->createHtml($elementKey);
            } catch (\Exception $e) {
                $this->addFlashMessage('Creating template file has failed. See error below.', '', AbstractMessage::ERROR);
                $this->addFlashMessage($e->getMessage(), '', AbstractMessage::ERROR);
            }
        }
        if ($isNew) {
            $this->addFlashMessage($this->translateLabel('tx_mask.content.newcontentelement'));
        } else {
            $this->addFlashMessage($this->translateLabel('tx_mask.content.updatedcontentelement'));
        }

        $this->eventDispatcher->dispatch(
            new MaskAfterElementSavedEvent($tableDefinitionCollection, $elementKey, $isNew)
        );

        return new JsonResponse(['messages' => $this->flashMessageQueue->getAllMessagesAndFlush(), 'hasError' => 0]);
    }

    /**
     * Delete a content element
     */
    public function delete(ServerRequestInterface $request): Response
    {
        $params = $request->getParsedBody();
        if ($params['purge']) {
            $this->deleteHtml($params['key']);
        }
        $tableDefinitionCollection = $this->storageRepository->persist($this->storageRepository->remove('tt_content', $params['key']));
        $this->generateAction($tableDefinitionCollection);
        $this->addFlashMessage($this->translateLabel('tx_mask.content.deletedcontentelement'));

        $this->eventDispatcher->dispatch(
            new MaskAfterElementDeletedEvent($tableDefinitionCollection, $params['key'])
        );

        return new JsonResponse($this->flashMessageQueue->getAllMessagesAndFlush());
    }

    public function toggleVisibility(ServerRequestInterface $request): Response
    {
        $params = $request->getParsedBody();
        if ((int)$params['element']['hidden'] === 1) {
            $tableDefinitionCollection = $this->storageRepository->activate('tt_content', $params['element']['key']);
            $this->addFlashMessage($this->translateLabel('tx_mask.content.activatedcontentelement'));
        } else {
            $tableDefinitionCollection = $this->storageRepository->hide('tt_content', $params['element']['key']);
            $this->addFlashMessage($this->translateLabel('tx_mask.content.hiddencontentelement'));
        }
        $this->generateAction($tableDefinitionCollection);
        return new JsonResponse($this->flashMessageQueue->getAllMessagesAndFlush());
    }

    public function backendLayouts(ServerRequestInterface $request): Response
    {
        $backendLayouts = $this->backendLayoutRepository->findAll(GeneralUtility::trimExplode(',', $this->maskExtensionConfiguration['backendlayout_pids'] ?? ''));
        $json['backendLayouts'] = [];
        /** @var BackendLayout $backendLayout */
        foreach ($backendLayouts as $key => $backendLayout) {
            $iconPath = $backendLayout->getIconPath();
            if ($iconPath) {
                $image = $this->resourceFactory->retrieveFileOrFolderObject($iconPath);
                if ($image instanceof File) {
                    $processingInstructions = [
                        'width' => '32',
                        'height' => '32c',
                    ];
                    $processedImage = $image->process(ProcessedFile::CONTEXT_IMAGECROPSCALEMASK, $processingInstructions);
                    $publicUrl = $processedImage->getPublicUrl();
                    $backendLayout->setIconPath($publicUrl);
                }
            }
            $json['backendLayouts'][] = [
                'key' => $key,
                'title' => $backendLayout->getTitle(),
                'description' => $backendLayout->getDescription(),
                'icon' => $backendLayout->getIconPath(),
            ];
        }
        return new JsonResponse($json);
    }

    public function elements(ServerRequestInterface $request): Response
    {
        if (!$this->tableDefinitionCollection->hasTable('tt_content')) {
            return new JsonResponse(['elements' => []]);
        }

        $elements = [];
        foreach ($this->tableDefinitionCollection->getTable('tt_content')->elements as $element) {
            $overlay = $element->hidden ? 'overlay-hidden' : null;
            $translatedLabel = $GLOBALS['LANG']->sl($element->label);
            $translatedDescription = $GLOBALS['LANG']->sl($element->description);
            $elements[$element->key] = [
                'color' => $element->color,
                'colorOverlay' => $element->colorOverlay,
                'description' => $element->description,
                'translatedDescription' => $translatedDescription !== $element->description ? $translatedDescription : '',
                'icon' => $element->icon,
                'iconOverlay' => $element->iconOverlay,
                'key' => $element->key,
                'label' => $element->label,
                'translatedLabel' => $translatedLabel !== $element->label ? $translatedLabel : '',
                'shortLabel' => $element->shortLabel,
                'iconMarkup' => $this->iconFactory->getIcon('mask-ce-' . $element->key, (new Typo3Version())->getMajorVersion() > 11 ? 'medium' : 'default', $overlay)->render(),
                'templateExists' => $this->contentElementTemplateExists($element->key) ? 1 : 0,
                'hidden' => $element->hidden ? 1 : 0,
                'count' => $this->getElementCount($element->key),
                'sorting' => $element->sorting,
                'saveAndClose' => $element->saveAndClose ? 1 : 0,
            ];
        }
        $json['elements'] = $elements;
        return new JsonResponse($json);
    }

    protected function getElementCount($elementKey): int
    {
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
            ->getQueryBuilderForTable('tt_content');

        return (int)$queryBuilder
            ->select('uid')
            ->from('tt_content')
            ->where($queryBuilder->expr()->eq('CType', $queryBuilder->createNamedParameter(AffixUtility::addMaskCTypePrefix($elementKey))))
            ->executeQuery()
            ->rowCount();
    }

    public function fieldTypes(ServerRequestInterface $request): Response
    {
        $json = [];
        $availability = [
            FieldType::EMAIL => 12,
            FieldType::FOLDER => 12,
        ];
        $typo3Version = new Typo3Version();
        $defaults = $this->configurationLoader->loadDefaults();
        $grouping = $this->configurationLoader->loadFieldGroups();
        foreach (FieldType::getConstants() as $type) {
            if (isset($availability[$type]) && $typo3Version->getMajorVersion() < $availability[$type]) {
                continue;
            }

            $config = [
                'name' => $type,
                'icon' => $this->iconFactory->getIcon('mask-fieldtype-' . $type)->getMarkup(),
                'fields' => [],
                'key' => '',
                'label' => '',
                'description' => '',
                'translatedLabel' => '',
                'itemLabel' => $this->translateLabel('tx_mask.field.' . $type),
                'parent' => [],
                'group' => $grouping[$type],
                'newField' => true,
                'tca' => [
                    'l10n_mode' => '',
                ],
            ];

            if (isset($defaults[$type]['tca_in'])) {
                foreach ($defaults[$type]['tca_in'] as $tcaKey => $value) {
                    $config['tca'][$tcaKey] = $value;
                }
            }
            $json[] = $config;
        }
        return new JsonResponse($json);
    }

    public function fieldGroups(ServerRequestInterface $request): Response
    {
        return new JsonResponse(
            [
                'groups' => [
                    [
                        'name' => 'input',
                        'label' => $this->translateLabel('tx_mask.input'),
                    ],
                    [
                        'name' => 'text',
                        'label' => $this->translateLabel('tx_mask.text'),
                    ],
                    [
                        'name' => 'date',
                        'label' => $this->translateLabel('tx_mask.date'),
                    ],
                    [
                        'name' => 'choice',
                        'label' => $this->translateLabel('tx_mask.choice'),
                    ],
                    [
                        'name' => 'special',
                        'label' => $this->translateLabel('tx_mask.special'),
                    ],
                    [
                        'name' => 'repeating',
                        'label' => $this->translateLabel('tx_mask.repeating'),
                    ],
                    [
                        'name' => 'structure',
                        'label' => $this->translateLabel('tx_mask.structure'),
                    ],
                ],
            ]
        );
    }

    public function multiUse(ServerRequestInterface $request): Response
    {
        $params = $request->getQueryParams();
        $key = $params['key'];
        $newField = (int)$params['newField'];
        $elementKey = '';
        if ($newField === 0) {
            $elementKey = $params['elementKey'];
        }
        $json['multiUseElements'] = $this->getMultiUseForField($key, $elementKey);
        return new JsonResponse($json);
    }

    /**
     * Checks all fields of an element for multi usage.
     * These fields CAN NOT be shared: inline, palette, tab, fields in inline.
     * These fields CAN be shared: all other first level fields, all other fields in first level palettes.
     */
    public function loadAllMultiUse(ServerRequestInterface $request): Response
    {
        $params = $request->getQueryParams();
        $table = $params['table'];
        $elementKey = $params['elementKey'];
        $element = $this->tableDefinitionCollection->loadElement($table, $elementKey);

        if (!$element) {
            return new JsonResponse(['multiUseElements' => []]);
        }

        $multiUseElements = [];
        foreach ($element->getRootTcaFields() as $field) {
            if ($field->isCoreField) {
                continue;
            }

            // Get fields in palette
            if ($field->getFieldType()->equals(FieldType::PALETTE)) {
                foreach ($this->tableDefinitionCollection->loadInlineFields($field->fullKey, $elementKey, $element->elementDefinition) as $paletteField) {
                    if ($paletteField->isCoreField || !$paletteField->getFieldType()->canBeShared()) {
                        continue;
                    }
                    $multiUseElements[$paletteField->fullKey] = $this->getMultiUseForField($paletteField->fullKey, $elementKey);
                }
                continue;
            }

            if (!$field->getFieldType()->canBeShared()) {
                continue;
            }

            $multiUseElements[$field->fullKey] = $this->getMultiUseForField($field->fullKey, $elementKey);
        }

        return new JsonResponse(['multiUseElements' => $multiUseElements]);
    }

    protected function getMultiUseForField(string $key, string $elementKey): array
    {
        $type = $this->tableDefinitionCollection->getTableByField($key, $elementKey);
        $multiUseElements = $this->tableDefinitionCollection->getElementsWhichUseField($key, $type)->toArray();

        // Filter elements with same element key
        $multiUseElements = array_filter(
            $multiUseElements,
            static function ($item) use ($elementKey) {
                return $item['key'] !== $elementKey;
            }
        );

        $multiUseElements = array_map(
            static function ($item) {
                return [
                    'key' => $item['key'],
                    'label' => $item['label'],
                ];
            },
            $multiUseElements
        );

        // Reset keys and return values
        return array_values($multiUseElements);
    }

    public function migrationsDone(ServerRequestInterface $request): Response
    {
        return new JsonResponse(['migrationsDone' => (int)$this->tableDefinitionCollection->getMigrationDone()]);
    }

    public function persistMaskDefinition(ServerRequestInterface $request): Response
    {
        try {
            $this->loader->write($this->tableDefinitionCollection);
            return new JsonResponse(['status' => 'ok', 'title' => $this->translateLabel('tx_mask.update_complete.title'), 'message' => $this->translateLabel('tx_mask.update_complete.message')]);
        } catch (\Throwable $e) {
            return new JsonResponse(['status' => 'error', 'title' => $this->translateLabel('tx_mask.update_failed.title'), 'message' => $this->translateLabel('tx_mask.update_failed.message')]);
        }
    }

    public function restructuringNeeded(ServerRequestInterface $request): JsonResponse
    {
        $overrideSharedFields = $this->features->isFeatureEnabled('overrideSharedFields');
        if (!$overrideSharedFields) {
            return new JsonResponse(['restructuringNeeded' => 0]);
        }
        $needsRestructure = $this->tableDefinitionCollection->isRestructuringNeeded();
        return new JsonResponse(['restructuringNeeded' => (int)$needsRestructure]);
    }

    public function executeRestructuring(ServerRequestInterface $request): Response
    {
        $restructuredTableDefinitionCollection = OverrideFieldsUtility::restructureTcaDefinitions($this->tableDefinitionCollection);
        try {
            $this->loader->write($restructuredTableDefinitionCollection);
            return new JsonResponse(['status' => 'ok', 'title' => $this->translateLabel('tx_mask.update_complete.title'), 'message' => $this->translateLabel('tx_mask.update_complete.message')]);
        } catch (\Throwable $e) {
            return new JsonResponse(['status' => 'error', 'title' => $this->translateLabel('tx_mask.update_failed.title'), 'message' => $this->translateLabel('tx_mask.update_failed.message')]);
        }
    }

    public function icons(ServerRequestInterface $request): Response
    {
        $icons = [
            'Web Application' => ['address-book', 'address-book-o', 'address-card', 'address-card-o', 'adjust', 'anchor', 'archive', 'asterisk', 'at', 'balance-scale', 'ban', 'bank', 'barcode', 'bars', 'bath', 'bathtub', 'battery', 'battery-0', 'battery-1', 'battery-2', 'battery-3', 'battery-4', 'battery-empty', 'battery-full', 'battery-half', 'battery-quarter', 'battery-three-quarters', 'bed', 'beer', 'bell', 'bell-o', 'bell-slash', 'bell-slash-o', 'binoculars', 'birthday-cake', 'bolt', 'bomb', 'book', 'bookmark', 'bookmark-o', 'briefcase', 'bug', 'building', 'building-o', 'bullhorn', 'bullseye', 'calculator', 'calendar', 'calendar-check-o', 'calendar-minus-o', 'calendar-o', 'calendar-plus-o', 'calendar-times-o', 'camera', 'camera-retro', 'cart-arrow-down', 'cart-plus', 'certificate', 'check', 'check-circle', 'check-circle-o', 'child', 'circle-thin', 'clock-o', 'clone', 'close', 'cloud', 'cloud-download', 'cloud-upload', 'code', 'code-fork', 'coffee', 'cogs', 'comment', 'comment-o', 'commenting', 'commenting-o', 'comments', 'comments-o', 'compass', 'copyright', 'creative-commons', 'crop', 'crosshairs', 'cube', 'cubes', 'cutlery', 'dashboard', 'database', 'desktop', 'diamond', 'download', 'drivers-license', 'drivers-license-o', 'edit', 'ellipsis-h', 'ellipsis-v', 'envelope', 'envelope-o', 'envelope-open', 'envelope-open-o', 'envelope-square', 'exclamation', 'exclamation-circle', 'exclamation-triangle', 'external-link', 'external-link-square', 'eye', 'eye-slash', 'eyedropper', 'fax', 'feed', 'female', 'film', 'filter', 'fire', 'fire-extinguisher', 'flag', 'flag-checkered', 'flag-o', 'flash', 'flask', 'folder', 'folder-o', 'folder-open', 'folder-open-o', 'frown-o', 'futbol-o', 'gamepad', 'gavel', 'gears', 'gift', 'glass', 'globe', 'graduation-cap', 'group', 'handshake-o', 'hashtag', 'hdd-o', 'headphones', 'history', 'home', 'hotel', 'hourglass', 'hourglass-1', 'hourglass-2', 'hourglass-3', 'hourglass-end', 'hourglass-half', 'hourglass-o', 'hourglass-start', 'i-cursor', 'id-badge', 'id-card', 'id-card-o', 'image', 'inbox', 'industry', 'info', 'info-circle', 'institution', 'key', 'keyboard-o', 'language', 'laptop', 'leaf', 'legal', 'lemon-o', 'level-down', 'level-up', 'life-bouy', 'life-buoy', 'life-ring', 'life-saver', 'lightbulb-o', 'location-arrow', 'lock', 'magic', 'magnet', 'mail-forward', 'mail-reply', 'mail-reply-all', 'male', 'map', 'map-marker', 'map-o', 'map-pin', 'map-signs', 'meh-o', 'microchip', 'microphone', 'microphone-slash', 'minus', 'minus-circle', 'mobile', 'mobile-phone', 'moon-o', 'mortar-board', 'mouse-pointer', 'music', 'navicon', 'newspaper-o', 'object-group', 'object-ungroup', 'paint-brush', 'paper-plane', 'paper-plane-o', 'paw', 'pencil', 'pencil-square', 'pencil-square-o', 'percent', 'phone', 'phone-square', 'photo', 'picture-o', 'plug', 'plus', 'plus-circle', 'podcast', 'power-off', 'print', 'puzzle-piece', 'qrcode', 'question', 'question-circle', 'quote-left', 'quote-right', 'recycle', 'registered', 'remove', 'reorder', 'reply', 'reply-all', 'retweet', 'road', 'rss', 'rss-square', 's15', 'search', 'search-minus', 'search-plus', 'send', 'send-o', 'server', 'share', 'share-square', 'share-square-o', 'shield', 'shopping-bag', 'shopping-basket', 'shopping-cart', 'shower', 'sign-in', 'sign-out', 'signal', 'sitemap', 'sliders', 'smile-o', 'snowflake-o', 'soccer-ball-o', 'sort', 'sort-alpha-asc', 'sort-alpha-desc', 'sort-amount-asc', 'sort-amount-desc', 'sort-asc', 'sort-desc', 'sort-down', 'sort-numeric-asc', 'sort-numeric-desc', 'sort-up', 'spoon', 'star', 'star-half', 'star-half-empty', 'star-half-full', 'star-half-o', 'star-o', 'sticky-note', 'sticky-note-o', 'street-view', 'suitcase', 'sun-o', 'support', 'tablet', 'tachometer', 'tag', 'tags', 'tasks', 'television', 'terminal', 'thermometer', 'thermometer-0', 'thermometer-1', 'thermometer-2', 'thermometer-3', 'thermometer-4', 'thermometer-empty', 'thermometer-full', 'thermometer-half', 'thermometer-quarter', 'thermometer-three-quarters', 'thumb-tack', 'ticket', 'times', 'times-circle', 'times-circle-o', 'times-rectangle', 'times-rectangle-o', 'tint', 'toggle-off', 'toggle-on', 'trademark', 'trash', 'trash-o', 'tree', 'trophy', 'tv', 'umbrella', 'university', 'unlock', 'unlock-alt', 'unsorted', 'upload', 'user', 'user-circle', 'user-circle-o', 'user-o', 'user-plus', 'user-secret', 'user-times', 'users', 'vcard', 'vcard-o', 'video-camera', 'volume-down', 'volume-off', 'volume-up', 'warning', 'wifi', 'window-close', 'window-close-o', 'window-maximize', 'window-minimize', 'window-restore', 'wrench'],
            'Accessibility' => ['american-sign-language-interpreting', 'asl-interpreting', 'assistive-listening-systems', 'audio-description', 'blind', 'braille', 'cc', 'deaf', 'deafness', 'hard-of-hearing', 'low-vision', 'question-circle-o', 'sign-language', 'signing', 'tty', 'universal-access', 'volume-control-phone', 'wheelchair', 'wheelchair-alt'],
            'Hand' => ['hand-grab-o', 'hand-lizard-o', 'hand-o-down', 'hand-o-left', 'hand-o-right', 'hand-o-up', 'hand-paper-o', 'hand-peace-o', 'hand-pointer-o', 'hand-rock-o', 'hand-scissors-o', 'hand-spock-o', 'hand-stop-o', 'thumbs-down', 'thumbs-o-down', 'thumbs-o-up', 'thumbs-up'],
            'Transportation' => ['automobile', 'bicycle', 'bus', 'cab', 'car', 'fighter-jet', 'motorcycle', 'plane', 'rocket', 'ship', 'space-shuttle', 'subway', 'taxi', 'train', 'truck'],
            'Gender' => ['genderless', 'intersex', 'mars', 'mars-double', 'mars-stroke', 'mars-stroke-h', 'mars-stroke-v', 'mercury', 'neuter', 'transgender', 'transgender-alt', 'venus', 'venus-double', 'venus-mars'],
            'File Type' => ['file', 'file-archive-o', 'file-audio-o', 'file-code-o', 'file-excel-o', 'file-image-o', 'file-movie-o', 'file-o', 'file-pdf-o', 'file-photo-o', 'file-picture-o', 'file-powerpoint-o', 'file-sound-o', 'file-text', 'file-text-o', 'file-video-o', 'file-word-o', 'file-zip-o'],
            'Spinner' => ['circle-o-notch', 'cog', 'gear', 'refresh', 'spinner'],
            'Form Control' => ['check-square', 'check-square-o', 'circle', 'circle-o', 'dot-circle-o', 'minus-square', 'minus-square-o', 'plus-square', 'plus-square-o', 'square', 'square-o'],
            'Payment' => ['cc-amex', 'cc-diners-club', 'cc-discover', 'cc-jcb', 'cc-mastercard', 'cc-paypal', 'cc-stripe', 'cc-visa', 'credit-card', 'credit-card-alt', 'google-wallet'],
            'Chart' => ['area-chart', 'bar-chart', 'bar-chart-o', 'line-chart', 'pie-chart'],
            'Currency' => ['btc', 'cny', 'dollar', 'eur', 'euro', 'gbp', 'gg-circle', 'ils', 'inr', 'jpy', 'krw', 'money', 'rmb', 'rouble', 'rub', 'ruble', 'rupee', 'shekel', 'sheqel', 'try', 'turkish-lira', 'usd', 'viacoin', 'won', 'yen'],
            'Text Editor' => ['align-center', 'align-justify', 'align-left', 'align-right', 'bold', 'chain', 'chain-broken', 'clipboard', 'columns', 'copy', 'cut', 'dedent', 'eraser', 'files-o', 'floppy-o', 'font', 'header', 'indent', 'italic', 'link', 'list', 'list-alt', 'list-ol', 'list-ul', 'outdent', 'paperclip', 'paragraph', 'paste', 'repeat', 'rotate-left', 'rotate-right', 'save', 'scissors', 'strikethrough', 'subscript', 'superscript', 'table', 'text-height', 'text-width', 'th', 'th-large', 'th-list', 'underline', 'undo', 'unlink'],
            'Directional' => ['angle-double-down', 'angle-double-left', 'angle-double-right', 'angle-double-up', 'angle-down', 'angle-left', 'angle-right', 'angle-up', 'arrow-circle-down', 'arrow-circle-left', 'arrow-circle-o-down', 'arrow-circle-o-left', 'arrow-circle-o-right', 'arrow-circle-o-up', 'arrow-circle-right', 'arrow-circle-up', 'arrow-down', 'arrow-left', 'arrow-right', 'arrow-up', 'arrows', 'arrows-h', 'arrows-v', 'caret-down', 'caret-left', 'caret-right', 'caret-square-o-down', 'caret-square-o-left', 'caret-square-o-right', 'caret-square-o-up', 'caret-up', 'chevron-circle-down', 'chevron-circle-left', 'chevron-circle-right', 'chevron-circle-up', 'chevron-down', 'chevron-left', 'chevron-right', 'chevron-up', 'exchange', 'long-arrow-down', 'long-arrow-left', 'long-arrow-right', 'long-arrow-up', 'toggle-down', 'toggle-left', 'toggle-right', 'toggle-up'],
            'Video Player' => ['arrows-alt', 'backward', 'compress', 'eject', 'expand', 'fast-backward', 'fast-forward', 'forward', 'pause', 'pause-circle', 'pause-circle-o', 'play', 'play-circle', 'play-circle-o', 'random', 'step-backward', 'step-forward', 'stop', 'stop-circle', 'stop-circle-o'],
            'Brand' => ['500px', 'adn', 'amazon', 'android', 'angellist', 'apple', 'bandcamp', 'behance', 'behance-square', 'bitbucket', 'bitbucket-square', 'bitcoin', 'black-tie', 'bluetooth', 'bluetooth-b', 'buysellads', 'paypal', 'chrome', 'codepen', 'codiepie', 'connectdevelop', 'contao', 'css3', 'dashcube', 'delicious', 'deviantart', 'digg', 'dribbble', 'dropbox', 'drupal', 'edge', 'eercast', 'empire', 'envira', 'etsy', 'expeditedssl', 'fa', 'facebook', 'facebook-f', 'facebook-official', 'facebook-square', 'firefox', 'first-order', 'flickr', 'font-awesome', 'fonticons', 'fort-awesome', 'forumbee', 'foursquare', 'free-code-camp', 'ge', 'get-pocket', 'gg', 'git', 'git-square', 'github', 'github-alt', 'github-square', 'gitlab', 'gittip', 'glide', 'glide-g', 'google', 'google-plus', 'google-plus-circle', 'google-plus-official', 'google-plus-square', 'gratipay', 'grav', 'hacker-news', 'houzz', 'html5', 'imdb', 'instagram', 'internet-explorer', 'ioxhost', 'joomla', 'jsfiddle', 'lastfm', 'lastfm-square', 'leanpub', 'linkedin', 'linkedin-square', 'linode', 'linux', 'maxcdn', 'meanpath', 'medium', 'meetup', 'mixcloud', 'modx', 'odnoklassniki', 'odnoklassniki-square', 'opencart', 'openid', 'opera', 'optin-monster', 'pagelines', 'pied-piper', 'pied-piper-alt', 'pied-piper-pp', 'pinterest', 'pinterest-p', 'pinterest-square', 'product-hunt', 'qq', 'quora', 'ra', 'ravelry', 'rebel', 'reddit', 'reddit-alien', 'reddit-square', 'renren', 'resistance', 'safari', 'scribd', 'sellsy', 'share-alt', 'share-alt-square', 'shirtsinbulk', 'simplybuilt', 'skyatlas', 'skype', 'slack', 'slideshare', 'snapchat', 'snapchat-ghost', 'snapchat-square', 'soundcloud', 'spotify', 'stack-exchange', 'stack-overflow', 'steam', 'steam-square', 'stumbleupon', 'stumbleupon-circle', 'superpowers', 'telegram', 'tencent-weibo', 'themeisle', 'trello', 'tripadvisor', 'tumblr', 'tumblr-square', 'twitch', 'twitter', 'twitter-square', 'usb', 'viadeo', 'viadeo-square', 'vimeo', 'vimeo-square', 'vine', 'vk', 'wechat', 'weibo', 'weixin', 'whatsapp', 'wikipedia-w', 'windows', 'wordpress', 'wpbeginner', 'wpexplorer', 'wpforms', 'xing', 'xing-square', 'y-combinator', 'y-combinator-square', 'yahoo', 'yc', 'yc-square', 'yelp', 'yoast', 'youtube', 'youtube-play', 'youtube-square'],
            'Medical' => ['ambulance', 'h-square', 'heart', 'heart-o', 'heartbeat', 'hospital-o', 'medkit', 'stethoscope', 'user-md'],
        ];
        foreach ($icons as $category => $values) {
            $icons[$category] = array_map(function ($item) {
                return 'fa-' . $item;
            }, $values);
        }
        return new JsonResponse($icons);
    }

    public function existingTca(ServerRequestInterface $request): Response
    {
        $allowedFields = [
            'tt_content' => [
                'header',
                'header_layout',
                'header_position',
                'date',
                'header_link',
                'subheader',
                'bodytext',
                'assets',
                'image',
                'media',
                'imagewidth',
                'imageheight',
                'imageborder',
                'imageorient',
                'imagecols',
                'image_zoom',
                'bullets_type',
                'table_delimiter',
                'table_enclosure',
                'table_caption',
                'file_collections',
                'filelink_sorting',
                'filelink_sorting_direction',
                'target',
                'filelink_size',
                'uploads_description',
                'uploads_type',
                'pages',
                'selected_categories',
                'category_field',
            ],
        ];

        /** @var MaskAllowedFieldsEvent $allowedFieldsEvent */
        $allowedFieldsEvent = $this->eventDispatcher->dispatch(
            new MaskAllowedFieldsEvent($allowedFields)
        );
        $allowedFields = $allowedFieldsEvent->getAllowedFields();

        $table = $request->getQueryParams()['table'];
        $type = $request->getQueryParams()['type'];
        $fields = ['mask' => [], 'core' => []];
        $searchFieldType = FieldType::cast($type);

        // Return empty result for non-shareable fields.
        if (!$searchFieldType->canBeShared()) {
            return new JsonResponse($fields);
        }

        foreach (($GLOBALS['TCA'][$table]['columns'] ?? []) as $tcaField => $tcaConfig) {
            $isMaskField = AffixUtility::hasMaskPrefix($tcaField);
            // Skip the field, if it is not a mask field, AND it is not in the allow-list
            // AND it is also not an already defined field in the configuration.
            // This may be an extension field or a core field (which was allowed in former Mask versions).
            if (
                !$isMaskField
                && !in_array($tcaField, $allowedFields[$table] ?? [], true)
                && !$this->tableDefinitionCollection->loadField($table, $tcaField) instanceof TcaFieldDefinition
            ) {
                continue;
            }

            if (($GLOBALS['TCA'][$table]['columns'][$tcaField]['config']['type'] ?? '') === 'passthrough') {
                continue;
            }

            // Add the field, if the field type matches with the search type
            // OR the field is the bodytext field, and we search for a text or textarea field.
            $fieldType = $this->tableDefinitionCollection->getFieldType($tcaField, $table);
            if (
                $fieldType->equals($type)
                || (
                    $tcaField === 'bodytext' && $searchFieldType->isTextareaField()
                )
            ) {
                $key = $isMaskField ? 'mask' : 'core';
                if ($isMaskField) {
                    $label = $this->tableDefinitionCollection->findFirstNonEmptyLabel($table, $tcaField);
                } elseif ('' === $label = $GLOBALS['LANG']->sL($tcaConfig['label'])) {
                    $label = $tcaConfig['label'];
                }
                $fields[$key][] = [
                    'field' => $tcaField,
                    'label' => $label,
                ];
            }
        }

        return new JsonResponse($fields);
    }

    public function tcaFields(ServerRequestInterface $request): Response
    {
        $tcaFields = $this->configurationLoader->loadTcaFields();
        foreach ($tcaFields as $key => $field) {
            if ($field['collision'] ?? false) {
                unset($field['collision']);
                foreach ($field as $type => $typeField) {
                    $tcaFields[$key] = $this->translateTcaFieldLabels($type, $typeField, $tcaFields[$key]);
                }
            } else {
                $tcaFields = $this->translateTcaFieldLabels($key, $field, $tcaFields);
            }
        }
        return new JsonResponse($tcaFields);
    }

    public function tables(ServerRequestInterface $request): Response
    {
        $items = ['' => ''];
        $tables = $GLOBALS['TCA'] ?? [];
        foreach ($tables as $tableKey => $table) {
            // Hidden tables should usually not be used as references.
            if ($table['ctrl']['hideTable'] ?? false) {
                continue;
            }
            $items[$tableKey] = $this->getLanguageService()->sL($table['ctrl']['title'] ?? $tableKey);
        }

        ksort($items);
        $json['foreignTables'] = $items;
        return new JsonResponse($json);
    }

    /**
     * @param array<string, array<string, string|array<string, mixed>>> $tcaFields
     * @return array<string, array<string, string|array<string, mixed>>>
     */
    protected function translateTcaFieldLabels(string $key, array $field, array $tcaFields): array
    {
        $tcaFields[$key]['label'] = $this->translateLabel($field['label']);
        if (isset($field['placeholder'])) {
            $tcaFields[$key]['placeholder'] = $this->translateLabel($field['placeholder']);
        }
        if (isset($field['description'])) {
            $tcaFields[$key]['description'] = $this->translateLabel($field['description']);
        }
        if (isset($field['keyValueLabels'])) {
            $tcaFields[$key]['keyValueLabels']['key'] = $this->translateLabel($field['keyValueLabels']['key']);
            $tcaFields[$key]['keyValueLabels']['value'] = $this->translateLabel($field['keyValueLabels']['value']);
        }
        if (isset($field['keyValueSelectItems'])) {
            foreach (['key', 'value'] as $keyValue) {
                foreach ($field['keyValueSelectItems'][$keyValue] as $selectItemIndex => $selectItem) {
                    $tcaFields[$key]['keyValueSelectItems'][$keyValue][$selectItemIndex]['label'] = $this->translateLabel($selectItem['label']);
                }
            }
        }
        if (isset($field['properties'])) {
            foreach ($field['properties'] as $propertyKey => $property) {
                $tcaFields[$key]['properties'][$propertyKey]['label'] = $this->translateLabel($property['label']);
            }
        }
        if (isset($tcaFields[$key]['items'])) {
            foreach ($tcaFields[$key]['items'] as $itemKey => $item) {
                $tcaFields[$key]['items'][$itemKey] = $this->translateLabel($item);
            }
        }
        return $tcaFields;
    }

    public function cTypes(ServerRequestInterface $request): Response
    {
        $items = [];
        $cTypes = $GLOBALS['TCA']['tt_content']['columns']['CType']['config']['items'];
        foreach ($cTypes ?? [] as $type) {
            $label = $type[0] ?? $type['label'];
            $value = $type[1] ?? $type['value'];
            if ($value !== '--div--') {
                $items[$value] = $this->getLanguageService()->sL($label);
            }
        }
        $json['ctypes'] = $items;
        return new JsonResponse($json);
    }

    public function tabs(ServerRequestInterface $request): Response
    {
        $availability = [
            FieldType::CATEGORY => 11,
        ];
        $typo3Version = new Typo3Version();
        $tabs = [];
        $availableTcaFields = $this->configurationLoader->loadTcaFields();
        foreach (FieldType::getConstants() as $type) {
            if (isset($availability[$type]) && $typo3Version->getMajorVersion() < $availability[$type]) {
                continue;
            }
            $tabs[$type] = $this->configurationLoader->loadTab($type);
            // Remove unavailable TCA options
            foreach ($tabs[$type] as $tabType => $rows) {
                foreach ($rows as $rowIndex => $fields) {
                    foreach ($fields as $tcaField => $ize) {
                        if (!array_key_exists($tcaField, $availableTcaFields)) {
                            unset($tabs[$type][$tabType][$rowIndex][$tcaField]);
                        }
                    }
                    // Remove empty rows
                    if (empty($tabs[$type][$tabType][$rowIndex])) {
                        unset($tabs[$type][$tabType][$rowIndex]);
                    }
                }
            }
        }
        return new JsonResponse($tabs);
    }

    public function language(ServerRequestInterface $request): Response
    {
        $language = [];
        $tabs = [
            Tab::GENERAL => 'tx_mask.tabs.default',
            Tab::APPEARANCE => 'tx_mask.tabs.appearance',
            Tab::DATABASE => 'tx_mask.tabs.database',
            Tab::EXTENDED => 'tx_mask.tabs.extended',
            Tab::FIELD_CONTROL => 'tx_mask.tabs.fieldControl',
            Tab::FILES => 'tx_mask.tabs.files',
            Tab::LOCALIZATION => 'tx_mask.tabs.localization',
            Tab::VALIDATION => 'tx_mask.tabs.validation',
            Tab::WIZARDS => 'tx_mask.tabs.wizards',
            Tab::GENERATOR => 'tx_mask.tabs.generator',
            Tab::ITEM_GROUP_SORTING => 'tx_mask.tabs.itemGroupSorting',
            Tab::VALUE_PICKER => 'tx_mask.tabs.valuePicker',
            Tab::ENABLED_CONTROLS => 'tx_mask.tabs.enabledControls',
        ];

        foreach ($tabs as $key => $tab) {
            $tabs[$key] = $this->translateLabel($tab);
        }
        $language['tabs'] = $tabs;

        $language['ok'] = $this->translateLabel('tx_mask.ok');
        $language['close'] = $this->translateLabel('tx_mask.close');
        $language['alert'] = $this->translateLabel('tx_mask.alert');
        $language['fieldsMissing'] = $this->translateLabel('tx_mask.fieldsMissing');
        $language['missingCreated'] = $this->translateLabel('tx_mask.all.createdmissingfolders');
        $language['reset'] = $this->translateLabel('tx_mask.reset_settings_success');
        $language['create'] = $this->translateLabel('tx_mask.all.create');
        $language['add'] = $this->translateLabel('tx_mask.all.add');
        $language['delete'] = $this->translateLabel('tx_mask.all.delete');
        $language['drag'] = $this->translateLabel('tx_mask.all.drag');
        $language['noGroup'] = $this->translateLabel('tx_mask.noGroup');

        $language['deleteModal'] = [
            'title' => $this->translateLabel('tx_mask.field.titleDelete'),
            'content' => $this->translateLabel('tx_mask.all.confirmdelete'),
            'close' => $this->translateLabel('tx_mask.all.abort'),
            'delete' => $this->translateLabel('tx_mask.all.delete'),
            'purge' => $this->translateLabel('tx_mask.all.purge'),
        ];

        $language['tooltip'] = [
            'editElement' => $this->translateLabel('tx_mask.tooltip.edit_element'),
            'deleteElement' => $this->translateLabel('tx_mask.tooltip.delete_element'),
            'enableElement' => $this->translateLabel('tx_mask.tooltip.enable_element'),
            'disableElement' => $this->translateLabel('tx_mask.tooltip.disable_element'),
            'html' => $this->translateLabel('tx_mask.tooltip.html'),
            'deleteField' => $this->translateLabel('tx_mask.field.delete'),
        ];

        $language['deleted'] = $this->translateLabel('tx_mask.content.deletedcontentelement');
        $language['icon'] = $this->translateLabel('tx_mask.all.icon');
        $language['iconOverlay'] = $this->translateLabel('tx_mask.all.iconOverlay');
        $language['color'] = $this->translateLabel('tx_mask.all.color');
        $language['colorOverlay'] = $this->translateLabel('tx_mask.all.colorOverlay');
        $language['usage'] = $this->translateLabel('tx_mask.content.count');
        $language['elementKey'] = $this->translateLabel('tx_mask.all.fieldkey');
        $language['elementLabel'] = $this->translateLabel('tx_mask.all.fieldLabel');

        $language['multistep'] = [
            'chooseKey' => $this->translateLabel('tx_mask.multistep.chooseKey'),
            'chooseLabel' => $this->translateLabel('tx_mask.multistep.chooseKey'),
            'text1' => $this->translateLabel('tx_mask.multistep.text1'),
            'text2' => $this->translateLabel('tx_mask.multistep.text2'),
            'placeholder1' => $this->translateLabel('tx_mask.multistep.placeholder1'),
            'placeholder2' => $this->translateLabel('tx_mask.multistep.placeholder2'),
        ];

        $language['createMissingFilesOrFolders'] = $this->translateLabel('tx_mask.all.createmissingfolders');
        $language['missingFolders'] = $this->translateLabel('tx_mask.all.missingFolders');
        $language['missingTemplates'] = $this->translateLabel('tx_mask.all.missingTemplates');
        $language['migrationsPerformedTitle'] = $this->translateLabel('tx_mask.migrations_performed.title');
        $language['migrationsPerformedMessage'] = $this->translateLabel('tx_mask.migrations_performed.message');
        $language['updateMaskDefinition'] = $this->translateLabel('tx_mask.update_mask_definition');
        $language['restructuringNeededTitle'] = $this->translateLabel('tx_mask.restructuring_needed.title');
        $language['restructuringNeededMessage'] = $this->translateLabel('tx_mask.restructuring_needed.message');
        $language['executeRestructuring'] = $this->translateLabel('tx_mask.execute_restructuring');
        $language['selectedItems'] = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.selected');
        $language['availableItems'] = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.items');
        $language['multiuseTitle'] = $this->translateLabel('tx_mask.content.multiuse');
        $language['multiuseMessage'] = $this->translateLabel('tx_mask.content.multiuse.description');

        return new JsonResponse($language);
    }

    public function nonOverrideableOptions(ServerRequestInterface $request): Response
    {
        return new JsonResponse(TcaFieldDefinition::NON_OVERRIDEABLE_OPTIONS);
    }

    public function features(ServerRequestInterface $request): Response
    {
        $featuresList['overrideSharedFields'] = [
            'title' => $this->translateLabel('tx_mask.features.overrideSharedFields'),
            'state' => (int)$this->features->isFeatureEnabled('overrideSharedFields'),
            'documentation' => 'https://docs.typo3.org/p/mask/mask/main/en-us/ChangeLog/8.2/Index.html',
        ];

        return new JsonResponse($featuresList);
    }

    public function richtextConfiguration(ServerRequestInterface $request): Response
    {
        $config[''] = $this->translateLabel('tx_mask.config.richtextConfiguration.none');
        $presets = array_keys($GLOBALS['TYPO3_CONF_VARS']['RTE']['Presets'] ?? []);
        $presets = array_filter($presets, function ($item) {
            return $item !== 'sys_news';
        });
        $presets = array_combine($presets, $presets);
        $config = array_merge($config, $presets);
        return new JsonResponse($config);
    }

    public function linkHandler(ServerRequestInterface $request): Response
    {
        $linkHandlerList = (array)(BackendUtility::getPagesTSconfig(0)['TCEMAIN.']['linkHandler.'] ?? []);
        $linkHandlerResponse = [];
        foreach ($linkHandlerList as $identifier => $linkHandler) {
            $linkHandlerResponse[] = [
                'identifier' => rtrim($identifier, '.'),
                'label' => $this->getLanguageService()->sL($linkHandler['label']),
            ];
        }
        if ((new Typo3Version())->getMajorVersion() > 11) {
            $linkHandlerResponse[] = [
                'identifier' => 'record',
                'label' => $this->translateLabel('tx_mask.genericLinkhandlerRecord'),
            ];
        }

        return new JsonResponse($linkHandlerResponse);
    }

    public function optionalExtensionStatus(ServerRequestInterface $request): Response
    {
        $optionalExtensions = ['rte_ckeditor'];
        $optionalExtensionStatus = [];
        foreach ($optionalExtensions as $optionalExtension) {
            $optionalExtensionStatus[$optionalExtension] = (int)ExtensionManagementUtility::isLoaded($optionalExtension);
        }
        return new JsonResponse($optionalExtensionStatus);
    }

    public function versions(ServerRequestInterface $request): Response
    {
        $typo3Version = GeneralUtility::makeInstance(Typo3Version::class);
        return new JsonResponse(
            [
                'typo3' => $typo3Version->getMajorVersion(),
                'mask' => ltrim(ExtensionManagementUtility::getExtensionVersion('mask'), 'v'),
            ]
        );
    }

    public function availableOnlineMedia(ServerRequestInterface $request): Response
    {
        return new JsonResponse($this->onlineMediaHelperRegistry->getSupportedFileExtensions());
    }

    /**
     * Generates all the necessary files
     */
    protected function generateAction(TableDefinitionCollection $tableDefinitionCollection): void
    {
        // Set TCA to enable DefaultTcaSchema
        $tcaCodeGenerator = GeneralUtility::makeInstance(TcaCodeGenerator::class);
        $tcaCodeGenerator->setInlineTca($tableDefinitionCollection);
        foreach ($tableDefinitionCollection as $tableDefinition) {
            if (!AffixUtility::hasMaskPrefix($tableDefinition->table)) {
                $fieldTca = $tcaCodeGenerator->generateFieldsTca($tableDefinition->table);
                if ($fieldTca === []) {
                    continue;
                }
                ExtensionManagementUtility::addTCAcolumns($tableDefinition->table, $fieldTca);
            }
        }

        // Update Database
        $result = $this->sqlCodeGenerator->updateDatabase();
        if (array_key_exists('error', $result)) {
            $this->addFlashMessage($result['error'], '', AbstractMessage::ERROR);
        }

        // Clear system cache to force new TCA caching
        $cacheManager = GeneralUtility::makeInstance(CacheManager::class);
        $cacheManager->flushCachesInGroup('system');
    }

    /**
     * Saves Fluid HTML for Contentelements, if File not exists
     */
    protected function saveHtml(string $key, string $html): bool
    {
        // fallback to prevent breaking change
        $path = TemplatePathUtility::getTemplatePath($this->maskExtensionConfiguration, $key);
        // Do not override existing files.
        if (file_exists($path)) {
            return false;
        }
        return GeneralUtility::writeFile($path, $html);
    }

    /**
     * Checks if a key for an element is available
     */
    public function checkElementKey(ServerRequest $request): Response
    {
        $elementKey = $request->getQueryParams()['key'];
        $elementTcaDefinition = $this->tableDefinitionCollection->loadElement('tt_content', $elementKey);
        $isAvailable = !$elementTcaDefinition instanceof ElementTcaDefinition;

        return new JsonResponse(['isAvailable' => $isAvailable]);
    }

    /**
     * Checks if a key for a field is available.
     * Inline fields and content fields must not be used across elements.
     * Other "normal" fields can be used in different elements, but changes are applied for both.
     */
    public function checkFieldKey(ServerRequest $request): Response
    {
        $queryParams = $request->getQueryParams();
        $table = $queryParams['table'];
        $fieldKey = $queryParams['key'];
        $type = $queryParams['type'];
        $elementKey = $queryParams['elementKey'];

        // Check if an inline table with the same key already exists.
        if ($type === FieldType::INLINE) {
            return new JsonResponse(['isAvailable' => !$this->tableDefinitionCollection->hasTable($fieldKey)]);
        }

        // All other fields must be unique per table. Exception: If a field
        // with the same field type exists, it may be shared.
        $field = $this->tableDefinitionCollection->loadField($table, $fieldKey);

        return new JsonResponse(['isAvailable' => (!$field instanceof TcaFieldDefinition || !$field->hasFieldType($elementKey) || $field->getFieldType($elementKey)->equals($type))]);
    }

    /**
     * Creates a Message object and adds it to the FlashMessageQueue.
     *
     * @param string $messageBody The message
     * @param string $messageTitle Optional message title
     * @param int $severity Optional severity, must be one of \TYPO3\CMS\Core\Messaging\FlashMessage constants
     * @param bool $storeInSession Optional, defines whether the message should be stored in the session (default) or not
     * @throws \TYPO3\CMS\Core\Exception
     * @see \TYPO3\CMS\Core\Messaging\FlashMessage
     */
    protected function addFlashMessage(string $messageBody, string $messageTitle = '', int $severity = AbstractMessage::OK, bool $storeInSession = true): void
    {
        $flashMessage = GeneralUtility::makeInstance(
            FlashMessage::class,
            $messageBody,
            $messageTitle,
            $this->getCompatibleSeverity($severity),
            $storeInSession
        );
        $this->flashMessageQueue->enqueue($flashMessage);
    }

    /**
     * @return int|ContextualFeedbackSeverity
     */
    protected function getCompatibleSeverity(int $code)
    {
        $useEnum = (new Typo3Version())->getMajorVersion() > 11;
        switch ($code) {
            case AbstractMessage::NOTICE:
                return $useEnum ? ContextualFeedbackSeverity::NOTICE : AbstractMessage::NOTICE;
            case AbstractMessage::INFO:
                return $useEnum ? ContextualFeedbackSeverity::INFO : AbstractMessage::INFO;
            case AbstractMessage::OK:
                return $useEnum ? ContextualFeedbackSeverity::OK : AbstractMessage::OK;
            case AbstractMessage::WARNING:
                return $useEnum ? ContextualFeedbackSeverity::WARNING : AbstractMessage::WARNING;
            case AbstractMessage::ERROR:
                return $useEnum ? ContextualFeedbackSeverity::ERROR : AbstractMessage::ERROR;
        }
        return $useEnum ? ContextualFeedbackSeverity::OK : AbstractMessage::OK;
    }

    /**
     * Check if template file exists.
     */
    protected function contentElementTemplateExists(string $key): bool
    {
        $templatePath = TemplatePathUtility::getTemplatePath($this->maskExtensionConfiguration, $key);
        return file_exists($templatePath) && is_file($templatePath);
    }

    /**
     * Creates a folder.
     */
    protected function createFolder(string $path): bool
    {
        $success = true;
        $path = GeneralUtility::getFileAbsFileName($path);
        if (!file_exists($path)) {
            $success = mkdir(
                $path,
                octdec($GLOBALS['TYPO3_CONF_VARS']['SYS']['folderCreateMask']),
                true
            );
        }
        return $success;
    }

    /**
     * Writes the generated html for the content element into the template file.
     */
    protected function createHtml(string $key): bool
    {
        $html = $this->htmlCodeGenerator->generateHtml($key, 'tt_content');
        return $this->saveHtml($key, $html);
    }

    /**
     * Deletes Fluid html, if file exists
     */
    protected function deleteHtml(string $key): void
    {
        $paths = [];
        $paths[] = TemplatePathUtility::getTemplatePath($this->maskExtensionConfiguration, $key);
        $paths[] = TemplatePathUtility::getTemplatePath($this->maskExtensionConfiguration, $key, false, $this->maskExtensionConfiguration['backend'] ?? '');
        foreach ($paths as $path) {
            @unlink($path);
        }
    }

    protected function getMissingFolders(): array
    {
        $missingFolders = [];
        foreach (self::$folderPathKeys as $key) {
            if (!isset($this->maskExtensionConfiguration[$key])) {
                continue;
            }
            $path = GeneralUtility::getFileAbsFileName($this->maskExtensionConfiguration[$key]);
            if ($path === '') {
                continue;
            }
            if (!file_exists($path)) {
                $missingFolders[$key] = $this->maskExtensionConfiguration[$key];
            }
        }

        return $missingFolders;
    }

    protected function translateLabel(string $key): string
    {
        return $this->getLanguageService()->sL('LLL:EXT:mask/Resources/Private/Language/locallang.xlf:' . $key);
    }

    protected function getLanguageService(): LanguageService
    {
        return $GLOBALS['LANG'];
    }
}