711 lines
33 KiB
PHP
711 lines
33 KiB
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 TYPO3\CMS\Backend\Controller\ContentElement;
|
|
|
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
|
use Psr\Http\Message\ResponseInterface;
|
|
use Psr\Http\Message\ServerRequestInterface;
|
|
use TYPO3\CMS\Backend\Attribute\AsController;
|
|
use TYPO3\CMS\Backend\Controller\Event\ModifyNewContentElementWizardItemsEvent;
|
|
use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
|
|
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
|
use TYPO3\CMS\Backend\Tree\View\ContentCreationPagePositionMap;
|
|
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
|
use TYPO3\CMS\Backend\View\BackendLayoutView;
|
|
use TYPO3\CMS\Backend\View\BackendViewFactory;
|
|
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
|
use TYPO3\CMS\Core\Http\HtmlResponse;
|
|
use TYPO3\CMS\Core\Localization\LanguageService;
|
|
use TYPO3\CMS\Core\Schema\Struct\SelectItem;
|
|
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
|
|
use TYPO3\CMS\Core\Service\DependencyOrderingService;
|
|
use TYPO3\CMS\Core\Type\Bitmask\Permission;
|
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
|
use TYPO3\CMS\Core\Utility\StringUtility;
|
|
|
|
/**
|
|
* New Content element wizard. This is the modal that pops up when clicking "+content" in page module, which
|
|
* will trigger wizardAction() since there is a colPos given. Method positionMapAction() is triggered for
|
|
* instance from the records module "+content" on tt_content table header, and from records module doc-header "+"
|
|
* and then "Click here for wizard".
|
|
*
|
|
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
|
*/
|
|
#[AsController]
|
|
class NewContentElementController
|
|
{
|
|
protected int $id = 0;
|
|
protected int $uid_pid = 0;
|
|
protected array $pageInfo = [];
|
|
protected int $sys_language = 0;
|
|
protected string $returnUrl = '';
|
|
|
|
/**
|
|
* If set, the content is destined for a specific column.
|
|
*/
|
|
protected ?int $colPos = null;
|
|
|
|
public function __construct(
|
|
protected readonly UriBuilder $uriBuilder,
|
|
protected readonly BackendViewFactory $backendViewFactory,
|
|
protected readonly EventDispatcherInterface $eventDispatcher,
|
|
protected readonly DependencyOrderingService $dependencyOrderingService,
|
|
protected readonly TcaSchemaFactory $tcaSchemaFactory,
|
|
protected readonly BackendLayoutView $backendLayoutView,
|
|
) {}
|
|
|
|
/**
|
|
* Process incoming request and dispatch to the requested action
|
|
*/
|
|
public function handleRequest(ServerRequestInterface $request): ResponseInterface
|
|
{
|
|
$parsedBody = $request->getParsedBody();
|
|
$queryParams = $request->getQueryParams();
|
|
|
|
// Setting internal vars:
|
|
$this->id = (int)($parsedBody['id'] ?? $queryParams['id'] ?? 0);
|
|
$this->sys_language = (int)($parsedBody['language_tag'] ?? $queryParams['language_tag'] ?? 0);
|
|
$this->returnUrl = GeneralUtility::sanitizeLocalUrl($parsedBody['returnUrl'] ?? $queryParams['returnUrl'] ?? '', $request);
|
|
$colPos = $parsedBody['colPos'] ?? $queryParams['colPos'] ?? null;
|
|
$this->colPos = $colPos === null ? null : (int)$colPos;
|
|
$this->uid_pid = (int)($parsedBody['uid_pid'] ?? $queryParams['uid_pid'] ?? 0);
|
|
|
|
// Getting the current page and receiving access information
|
|
$this->pageInfo = BackendUtility::readPageAccess($this->id, $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW)) ?: [];
|
|
|
|
$action = (string)($parsedBody['action'] ?? $queryParams['action'] ?? 'wizard');
|
|
if ($action === 'wizard') {
|
|
return $this->wizardAction($request);
|
|
}
|
|
if ($action === 'positionMap') {
|
|
return $this->positionMapAction($request);
|
|
}
|
|
return new HtmlResponse('Action not allowed', 400);
|
|
}
|
|
|
|
/**
|
|
* Renders the wizard
|
|
*/
|
|
protected function wizardAction(ServerRequestInterface $request): ResponseInterface
|
|
{
|
|
if (!$this->id || $this->pageInfo === []) {
|
|
// No pageId or no access.
|
|
return new HtmlResponse('No Access');
|
|
}
|
|
// Whether position selection must be performed (no colPos was yet defined)
|
|
$positionSelection = $this->colPos === null;
|
|
|
|
// Get processed and modified wizard items
|
|
$wizardItems = $this->eventDispatcher->dispatch(
|
|
new ModifyNewContentElementWizardItemsEvent(
|
|
$this->getWizards($request),
|
|
$this->pageInfo,
|
|
$this->colPos,
|
|
$this->sys_language,
|
|
$this->uid_pid,
|
|
$request,
|
|
)
|
|
)->getWizardItems();
|
|
|
|
$key = 'common';
|
|
$categories = [];
|
|
foreach ($wizardItems as $wizardKey => $wizardItem) {
|
|
// An item is either a header or an item rendered with title/description and icon:
|
|
if (isset($wizardItem['header'])) {
|
|
$key = $wizardKey;
|
|
$categories[$key] = [
|
|
'identifier' => $key,
|
|
'label' => $wizardItem['header'] ?: '-',
|
|
'items' => [],
|
|
];
|
|
} else {
|
|
// Get default values for the wizard item
|
|
$defaultValues = (array)($wizardItem['defaultValues'] ?? []);
|
|
|
|
// Initialize the view variables for the item
|
|
$item = [
|
|
'identifier' => $wizardKey,
|
|
'icon' => $wizardItem['iconIdentifier'] ?? '',
|
|
'iconOverlay' => $wizardItem['iconOverlay'] ?? '',
|
|
'label' => $wizardItem['title'] ?? '',
|
|
'description' => $wizardItem['description'] ?? '',
|
|
'defaultValues' => $defaultValues,
|
|
];
|
|
// If the URL was already created (e.g. via the PSR-14 event) this needs to be
|
|
// kept and not overwritten
|
|
if (isset($wizardItem['url'])) {
|
|
$item['url'] = $wizardItem['url'];
|
|
if ($positionSelection) {
|
|
$item['requestType'] = 'ajax';
|
|
$item['saveAndClose'] = (bool)($wizardItem['saveAndClose'] ?? false);
|
|
}
|
|
} elseif ($positionSelection) {
|
|
$item['url'] = (string)$this->uriBuilder
|
|
->buildUriFromRoute(
|
|
'new_content_element_wizard',
|
|
[
|
|
'action' => 'positionMap',
|
|
'id' => $this->id,
|
|
'sys_language_uid' => $this->sys_language,
|
|
'returnUrl' => $this->returnUrl,
|
|
]
|
|
);
|
|
$item['requestType'] = 'ajax';
|
|
$item['saveAndClose'] = (bool)($wizardItem['saveAndClose'] ?? false);
|
|
} else {
|
|
// In case no position has to be selected, we can just add the target
|
|
if ($wizardItem['saveAndClose'] ?? false) {
|
|
// Go to DataHandler directly instead of FormEngine
|
|
$item['url'] = (string)$this->uriBuilder->buildUriFromRoute('tce_db', [
|
|
'data' => [
|
|
'tt_content' => [
|
|
StringUtility::getUniqueId('NEW') => array_replace($defaultValues, [
|
|
'colPos' => $this->colPos,
|
|
'pid' => $this->uid_pid,
|
|
'sys_language_uid' => $this->sys_language,
|
|
]),
|
|
],
|
|
],
|
|
'redirect' => $this->returnUrl,
|
|
]);
|
|
} else {
|
|
$item['url'] = (string)$this->uriBuilder->buildUriFromRoute('record_edit', [
|
|
'edit' => [
|
|
'tt_content' => [
|
|
$this->uid_pid => 'new',
|
|
],
|
|
],
|
|
'module' => '_CURRENT_MODULE_',
|
|
'returnUrl' => $this->returnUrl,
|
|
'defVals' => [
|
|
'tt_content' => array_replace($defaultValues, [
|
|
'colPos' => $this->colPos,
|
|
'sys_language_uid' => $this->sys_language,
|
|
]),
|
|
],
|
|
]);
|
|
}
|
|
}
|
|
$categories[$key]['items'][] = $item;
|
|
}
|
|
}
|
|
|
|
// Unset empty categories
|
|
foreach ($categories as $key => $category) {
|
|
if ($category['items'] === []) {
|
|
unset($categories[$key]);
|
|
}
|
|
}
|
|
|
|
$view = $this->backendViewFactory->create($request);
|
|
$view->assignMultiple([
|
|
'positionSelection' => $positionSelection,
|
|
'categoriesJson' => GeneralUtility::jsonEncodeForHtmlAttribute($categories, false),
|
|
]);
|
|
return new HtmlResponse($view->render('NewContentElement/Wizard'));
|
|
}
|
|
|
|
/**
|
|
* Renders the position map
|
|
*/
|
|
protected function positionMapAction(ServerRequestInterface $request): ResponseInterface
|
|
{
|
|
$pageInfo = BackendUtility::readPageAccess($this->id, $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW));
|
|
|
|
$posMap = GeneralUtility::makeInstance(ContentCreationPagePositionMap::class);
|
|
$posMap->cur_sys_language = $this->sys_language;
|
|
$posMap->defVals = (array)($request->getParsedBody()['defVals'] ?? []);
|
|
$posMap->saveAndClose = (bool)($request->getParsedBody()['saveAndClose'] ?? false);
|
|
$posMap->R_URI = $this->returnUrl;
|
|
$view = $this->backendViewFactory->create($request);
|
|
$view->assign('posMap', $posMap->printContentElementColumns($this->id, $pageInfo, $request));
|
|
return new HtmlResponse($view->render('NewContentElement/PositionMap'));
|
|
}
|
|
|
|
/**
|
|
* Returns the array of elements in the wizard display.
|
|
* For the plugin section there is support for adding elements there from a global variable.
|
|
*/
|
|
protected function getWizards(ServerRequestInterface $request): array
|
|
{
|
|
$wizards = $this->loadAvailableWizards();
|
|
$newContentElementWizardTsConfig = BackendUtility::getPagesTSconfig($this->id)['mod.']['wizards.']['newContentElement.'] ?? [];
|
|
$wizardsFromPageTSConfig = $this->migrateCommonGroupToDefault($newContentElementWizardTsConfig['wizardItems.'] ?? []);
|
|
$wizardsFromPageTSConfig = $this->migratePositionalCommonGroupToDefault($wizardsFromPageTSConfig);
|
|
$wizards = $this->mergeContentElementWizardsWithPageTSConfigWizards($wizards, $wizardsFromPageTSConfig);
|
|
$wizards = $this->removeWizardsByPageTs($wizards, $newContentElementWizardTsConfig);
|
|
$wizards = $this->removeWizardsByBackendLayoutColPosRestriction($wizards, $this->pageInfo, $this->colPos, $request);
|
|
if ($wizards === []) {
|
|
return [];
|
|
}
|
|
$wizardItems = [];
|
|
foreach ($wizards as $groupKey => $wizardGroup) {
|
|
$wizards[$groupKey] = $this->prepareDependencyOrdering($wizards[$groupKey], 'before');
|
|
$wizards[$groupKey] = $this->prepareDependencyOrdering($wizards[$groupKey], 'after');
|
|
}
|
|
$orderedWizards = $this->orderWizards($wizards);
|
|
foreach ($orderedWizards as $groupKey => $wizardGroup) {
|
|
$groupKey = rtrim($groupKey, '.');
|
|
$groupItems = [];
|
|
$wizardElements = $wizardGroup['elements.'] ?? [];
|
|
if (is_array($wizardElements)) {
|
|
$wizardElements = $this->orderElements($wizardElements);
|
|
foreach ($wizardElements as $itemKey => $itemConf) {
|
|
$itemKey = rtrim($itemKey, '.');
|
|
if ($itemConf !== []) {
|
|
$groupItems[$groupKey . '_' . $itemKey] = $this->prepareWizardItem($itemConf);
|
|
}
|
|
}
|
|
}
|
|
if (!empty($groupItems)) {
|
|
$wizardItems[$groupKey]['header'] = $this->getLanguageService()->sL($wizardGroup['header'] ?? '');
|
|
$wizardItems = array_merge($wizardItems, $groupItems);
|
|
}
|
|
}
|
|
|
|
// Remove elements where preset values are not allowed:
|
|
return $this->removeInvalidWizardItems($wizardItems);
|
|
}
|
|
|
|
protected function loadAvailableWizards(): array
|
|
{
|
|
$schema = $this->tcaSchemaFactory->get('tt_content');
|
|
// Foreign table support for TypeInformation is not supported in tt_content
|
|
$typeField = $schema->getSubSchemaTypeInformation()->getFieldName();
|
|
$fieldConfig = $schema->hasField($typeField) ? $schema->getField($typeField)->getConfiguration() : [];
|
|
$items = $fieldConfig['items'] ?? [];
|
|
$itemGroups = $fieldConfig['itemGroups'] ?? [];
|
|
$groupedWizardItems = [];
|
|
foreach (array_keys($itemGroups) as $groupIdentifier) {
|
|
$groupedWizardItems[$groupIdentifier . '.']['header'] = $itemGroups[$groupIdentifier];
|
|
}
|
|
foreach ($items as $item) {
|
|
$selectItem = SelectItem::fromTcaItemArray($item);
|
|
if ($selectItem->isDivider()) {
|
|
continue;
|
|
}
|
|
$recordType = $selectItem->getValue();
|
|
$groupIdentifier = $selectItem->getGroup();
|
|
$groupedWizardItems[$groupIdentifier . '.']['elements.'] ??= [];
|
|
// In case this group is not defined in itemGroups, use the group identifier as label.
|
|
$groupedWizardItems[$groupIdentifier . '.']['header'] ??= $groupIdentifier;
|
|
$itemDescription = $selectItem->getDescription();
|
|
$wizardEntry = [
|
|
'iconIdentifier' => $selectItem->getIcon(),
|
|
'iconOverlay' => $selectItem->getIconOverlay(),
|
|
'title' => $selectItem->getLabel(),
|
|
'description' => $itemDescription['description'] ?? ($itemDescription ?? ''),
|
|
'defaultValues' => [
|
|
'CType' => $recordType,
|
|
],
|
|
];
|
|
if ($schema->hasSubSchema($recordType)) {
|
|
$wizardEntry = array_replace_recursive($wizardEntry, $schema->getSubSchema($recordType)->getRawConfiguration()['creationOptions'] ?? []);
|
|
}
|
|
$groupedWizardItems[$groupIdentifier . '.']['elements.'][$recordType . '.'] = $wizardEntry;
|
|
}
|
|
return $groupedWizardItems;
|
|
}
|
|
|
|
/**
|
|
* This method merges Content Element wizards defined by TCA with wizards defined in PageTSConfig.
|
|
* PageTS has precedence.
|
|
* It might happen that both TCA and PageTS define an entry with exactly the same default values.
|
|
* In such a case, the automatically added TCA entry is dropped.
|
|
*/
|
|
protected function mergeContentElementWizardsWithPageTSConfigWizards(array $contentElementWizards, array $pageTsConfigWizards): array
|
|
{
|
|
$uniqueDefaultValuesInPageTsWizards = [];
|
|
foreach ($pageTsConfigWizards as $wizard) {
|
|
foreach ($wizard['elements.'] ?? [] as $elementConfig) {
|
|
$defaultValues = $elementConfig['tt_content_defValues.'] ?? [];
|
|
if ($defaultValues === []) {
|
|
continue;
|
|
}
|
|
ksort($defaultValues);
|
|
$uniqueDefaultValuesInPageTsWizards[] = $defaultValues;
|
|
}
|
|
}
|
|
foreach ($contentElementWizards as $group => $wizard) {
|
|
foreach ($wizard['elements.'] ?? [] as $key => $elementConfig) {
|
|
// Remove duplicated entry.
|
|
$defaultValues = $elementConfig['defaultValues'];
|
|
ksort($defaultValues);
|
|
if (in_array($defaultValues, $uniqueDefaultValuesInPageTsWizards, true)) {
|
|
unset($contentElementWizards[$group]['elements.'][$key]);
|
|
}
|
|
}
|
|
}
|
|
$mergedWizards = array_replace_recursive($contentElementWizards, $pageTsConfigWizards);
|
|
return $mergedWizards;
|
|
}
|
|
|
|
/**
|
|
* Orders elements within a wizard group using before/after configuration.
|
|
* Similar to orderWizards() but for individual content elements.
|
|
*/
|
|
protected function orderElements(array $elements): array
|
|
{
|
|
// Check if any element has before/after configuration
|
|
// and return early if no reordering is required.
|
|
if (!$this->hasPositionalArguments($elements)) {
|
|
return $elements;
|
|
}
|
|
|
|
// Prepare elements for dependency ordering.
|
|
// Create implicit chain based on initial order for consecutive elements
|
|
// without explicit dependencies, preserving relative order while allowing
|
|
// explicit positioning.
|
|
$preparedElements = [];
|
|
|
|
// First pass: prepare all elements with their explicit dependencies
|
|
foreach ($elements as $elementKey => $element) {
|
|
$preparedElement = $element;
|
|
// Prepare before/after values (they might be comma-separated strings)
|
|
$preparedElement = $this->prepareDependencyOrdering($preparedElement, 'before');
|
|
$preparedElement = $this->prepareDependencyOrdering($preparedElement, 'after');
|
|
$preparedElements[$elementKey] = $preparedElement;
|
|
}
|
|
|
|
// Second pass: add implicit chain for consecutive elements without explicit dependencies
|
|
// This preserves relative order within blocks, while explicit dependencies can reorder them
|
|
$previousIndependentElementKey = null;
|
|
foreach ($elements as $elementKey => $element) {
|
|
$isIndependent = empty($element['before']) && empty($element['after']);
|
|
if ($isIndependent) {
|
|
// Element without explicit dependency: chain with previous independent element
|
|
if ($previousIndependentElementKey !== null) {
|
|
$existingAfter = $preparedElements[$elementKey]['after'] ?? [];
|
|
if (!in_array($previousIndependentElementKey, $existingAfter, true)) {
|
|
$preparedElements[$elementKey]['after'] = array_merge($existingAfter, [$previousIndependentElementKey]);
|
|
}
|
|
}
|
|
$previousIndependentElementKey = $elementKey;
|
|
}
|
|
}
|
|
// Use dependency ordering service to order elements
|
|
return $this->dependencyOrderingService->orderByDependencies($preparedElements);
|
|
}
|
|
|
|
protected function hasPositionalArguments(array $elements): bool
|
|
{
|
|
foreach ($elements as $element) {
|
|
if (!empty($element['before']) || !empty($element['after'])) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* There are two separate ordering systems for wizard groups:
|
|
* 1. TCA itemGroup sorting by associative array item order.
|
|
* 2. PageTS defined order by "before" and "after".
|
|
*
|
|
* System 1. has a well-defined order, where every item defines "after" (linked list).
|
|
* Due to this, the two system cannot be combined.
|
|
* As soon as system 2 defines at least one "before" or "after" it takes over.
|
|
*/
|
|
protected function orderWizards(array $wizards): array
|
|
{
|
|
// First round: Order by TCA defined sorting.
|
|
$hasAtLeastOnePositionalArgument = false;
|
|
foreach ($wizards as $group => $wizard) {
|
|
if (isset($wizard['before'])) {
|
|
$hasAtLeastOnePositionalArgument = true;
|
|
$wizards[$group]['pageTsBefore'] = $wizard['before'];
|
|
unset($wizards[$group]['before']);
|
|
}
|
|
if (isset($wizard['after'])) {
|
|
$hasAtLeastOnePositionalArgument = true;
|
|
$wizards[$group]['pageTsAfter'] = $wizard['after'];
|
|
unset($wizards[$group]['after']);
|
|
}
|
|
}
|
|
// No order defined by pageTS. Use TCA sorting.
|
|
if (!$hasAtLeastOnePositionalArgument) {
|
|
$schema = $this->tcaSchemaFactory->get('tt_content');
|
|
// Foreign table support for TypeInformation is not supported in tt_content
|
|
$typeField = $schema->getSubSchemaTypeInformation()->getFieldName();
|
|
$fieldConfig = $schema->hasField($typeField) ? $schema->getField($typeField)->getConfiguration() : [];
|
|
$itemGroups = $fieldConfig['itemGroups'] ?? [];
|
|
// Auto-set positional information based on TCA itemGroups sorting.
|
|
$lastGroup = null;
|
|
foreach (array_keys($itemGroups) as $groupIdentifier) {
|
|
if (!array_key_exists($groupIdentifier . '.', $wizards)) {
|
|
continue;
|
|
}
|
|
if ($lastGroup !== null) {
|
|
$wizards[$groupIdentifier . '.']['after'] = [$lastGroup . '.'];
|
|
}
|
|
$lastGroup = $groupIdentifier;
|
|
}
|
|
return $this->dependencyOrderingService->orderByDependencies($wizards);
|
|
}
|
|
// Override order by pageTsConfig.
|
|
foreach ($wizards as $group => $wizard) {
|
|
// Unset "after" previously set by Content Element wizards.
|
|
unset($wizards[$group]['after']);
|
|
if (isset($wizard['pageTsBefore'])) {
|
|
$wizards[$group]['before'] = $wizard['pageTsBefore'];
|
|
unset($wizards[$group]['pageTsBefore']);
|
|
}
|
|
if (isset($wizard['pageTsAfter'])) {
|
|
$wizards[$group]['after'] = $wizard['pageTsAfter'];
|
|
unset($wizards[$group]['pageTsAfter']);
|
|
}
|
|
}
|
|
return $this->dependencyOrderingService->orderByDependencies($wizards);
|
|
}
|
|
|
|
/**
|
|
* This method returns the wizard items, defined in Page TSconfig for b/w
|
|
* compatibility.
|
|
*
|
|
* Additionally, it migrates previously defined wizard items in the
|
|
* `common` group to the new `default` group, which is defined in TCA.
|
|
*
|
|
* @param array<string, array> $wizardsFromPageTs
|
|
* @return array<string, array>
|
|
*/
|
|
protected function migrateCommonGroupToDefault(array $wizardsFromPageTs): array
|
|
{
|
|
if (!array_key_exists('common.', $wizardsFromPageTs)) {
|
|
// In case "common." is not defined, just return the wizards, which are still defined via Page TSconfig
|
|
return $wizardsFromPageTs;
|
|
}
|
|
|
|
// Prepare "removeItems" to be merged
|
|
if ($wizardsFromPageTs['default.']['elements.']['removeItems'] ?? false) {
|
|
$wizardsFromPageTs['default.']['removeItems'] = GeneralUtility::trimExplode(',', $wizardsFromPageTs['default.']['elements.']['removeItems'] ?? '', true);
|
|
} elseif ($wizardsFromPageTs['default.']['removeItems'] ?? false) {
|
|
$wizardsFromPageTs['default.']['removeItems'] = GeneralUtility::trimExplode(',', $wizardsFromPageTs['default.']['removeItems'], true);
|
|
}
|
|
|
|
if ($wizardsFromPageTs['common.']['elements.']['removeItems'] ?? false) {
|
|
$wizardsFromPageTs['common.']['removeItems'] = GeneralUtility::trimExplode(',', $wizardsFromPageTs['common.']['elements.']['removeItems'] ?? '', true);
|
|
} elseif ($wizardsFromPageTs['common.']['removeItems'] ?? false) {
|
|
$wizardsFromPageTs['common.']['removeItems'] = GeneralUtility::trimExplode(',', $wizardsFromPageTs['common.']['removeItems'], true);
|
|
}
|
|
|
|
$defaultItems = array_merge_recursive($wizardsFromPageTs['default.'] ?? [], $wizardsFromPageTs['common.']);
|
|
unset($wizardsFromPageTs['common.']);
|
|
|
|
if ($defaultItems !== []) {
|
|
$wizardsFromPageTs['default.'] = $defaultItems;
|
|
}
|
|
|
|
return $wizardsFromPageTs;
|
|
}
|
|
|
|
protected function migratePositionalCommonGroupToDefault(array $wizards): array
|
|
{
|
|
foreach ($wizards as $group => $wizard) {
|
|
if (($wizard['before'] ?? '') === 'common') {
|
|
$wizards[$group]['before'] = 'default';
|
|
}
|
|
if (($wizard['after'] ?? '') === 'common') {
|
|
$wizards[$group]['after'] = 'default';
|
|
}
|
|
}
|
|
return $wizards;
|
|
}
|
|
|
|
protected function prepareWizardItem(array $itemConf): array
|
|
{
|
|
// Just replace the "known" keys of $itemConf. This way extensions are able to set custom keys, which are not
|
|
// used by the controller, but might be evaluated by listeners of the ModifyNewContentElementWizardItemsEvent.
|
|
$itemConf = array_replace_recursive(
|
|
$itemConf,
|
|
[
|
|
'title' => trim($this->getLanguageService()->sL($itemConf['title'] ?? '')),
|
|
'description' => trim($this->getLanguageService()->sL($itemConf['description'] ?? '')),
|
|
'iconIdentifier' => $itemConf['iconIdentifier'] ?? null,
|
|
'saveAndClose' => (bool)($itemConf['saveAndClose'] ?? false),
|
|
'defaultValues' => array_replace_recursive(
|
|
$itemConf['tt_content_defValues'] ?? [],
|
|
$itemConf['tt_content_defValues.'] ?? [],
|
|
$itemConf['defaultValues'] ?? []
|
|
),
|
|
]
|
|
);
|
|
unset($itemConf['tt_content_defValues'], $itemConf['tt_content_defValues.']);
|
|
return $itemConf;
|
|
}
|
|
|
|
protected function removeWizardsByPageTs(array $wizards, mixed $wizardsItemsPageTs): array
|
|
{
|
|
$removeWizardItems = $wizardsItemsPageTs['wizardItems.']['removeItems'] ?? [];
|
|
if (is_string($removeWizardItems)) {
|
|
$removeWizardItems = GeneralUtility::trimExplode(',', $removeWizardItems, true);
|
|
}
|
|
|
|
foreach ($wizards as $key => &$wizard) {
|
|
// Leave out removeItems etc.
|
|
if (is_string($wizard)) {
|
|
unset($wizards[$key]);
|
|
continue;
|
|
}
|
|
if (in_array(rtrim((string)$key, '.'), $removeWizardItems, true)) {
|
|
unset($wizards[$key]);
|
|
continue;
|
|
}
|
|
$removeWizardElements = $wizardsItemsPageTs['wizardItems.'][$key]['removeItems'] ?? [];
|
|
if (is_string($removeWizardElements)) {
|
|
$removeWizardElements = GeneralUtility::trimExplode(',', $removeWizardElements, true);
|
|
}
|
|
foreach ($wizard['elements.'] ?? [] as $identifier => $element) {
|
|
if (in_array(rtrim((string)$identifier, '.'), $removeWizardElements, true)) {
|
|
unset($wizard['elements.'][$identifier]);
|
|
}
|
|
}
|
|
}
|
|
|
|
return $wizards;
|
|
}
|
|
|
|
protected function removeWizardsByBackendLayoutColPosRestriction(array $wizardGroups, array $pageInfo, ?int $colPos, ServerRequestInterface $request): array
|
|
{
|
|
// Force colPos to 0 if null to apply restrictions for 0 by default.
|
|
$colPos = (int)$colPos;
|
|
// This is the page uid of a workspace overlay already so backend layouts of workspace
|
|
// changed or moved pages should be considered correctly.
|
|
$pid = (int)$pageInfo['uid'];
|
|
$backendLayout = $this->backendLayoutView->getBackendLayoutForPage($pid);
|
|
$columnConfiguration = $this->backendLayoutView->getColPosConfigurationForPage($backendLayout, $colPos, $pid, $request);
|
|
if (!empty($columnConfiguration['allowedContentTypes'])) {
|
|
$allowedContentTypes = GeneralUtility::trimExplode(',', $columnConfiguration['allowedContentTypes'], true);
|
|
foreach ($wizardGroups as $wizardGroupName => $wizards) {
|
|
foreach (($wizards['elements.'] ?? []) as $wizardKey => $wizard) {
|
|
$cType = $wizard['defaultValues']['CType'] ?? $wizard['tt_content_defValues.']['CType'] ?? '';
|
|
if (empty($cType)) {
|
|
continue;
|
|
}
|
|
if (!in_array(trim($cType), $allowedContentTypes, true)) {
|
|
unset($wizardGroups[$wizardGroupName]['elements.'][$wizardKey]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (!empty($columnConfiguration['disallowedContentTypes'])) {
|
|
$disAllowedContentTypes = GeneralUtility::trimExplode(',', $columnConfiguration['disallowedContentTypes'], true);
|
|
foreach ($wizardGroups as $wizardGroupName => $wizards) {
|
|
foreach (($wizards['elements.'] ?? []) as $wizardKey => $wizard) {
|
|
$cType = $wizard['defaultValues']['CType'] ?? $wizard['tt_content_defValues.']['CType'] ?? '';
|
|
if (empty($cType)) {
|
|
continue;
|
|
}
|
|
if (in_array(trim($cType), $disAllowedContentTypes, true)) {
|
|
unset($wizardGroups[$wizardGroupName]['elements.'][$wizardKey]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return $wizardGroups;
|
|
}
|
|
|
|
/**
|
|
* Checks the array for elements which might contain invalid default values and will unset them!
|
|
* Looks for the "defaultValues" key in each element and if found it will traverse that array
|
|
* as fieldname / value pairs and check.
|
|
*/
|
|
protected function removeInvalidWizardItems(array $wizardItems): array
|
|
{
|
|
$schema = $this->tcaSchemaFactory->get('tt_content');
|
|
$removeItems = [];
|
|
$keepItems = [];
|
|
// Get TCEFORM from TSconfig of current page
|
|
$TCEFORM_TSconfig = FormEngineUtility::getTCEFORM_TSconfig('tt_content', ['pid' => $this->id]);
|
|
$backendUser = $this->getBackendUser();
|
|
// Traverse wizard items:
|
|
foreach ($wizardItems as $key => $cfg) {
|
|
if (!is_array($cfg['defaultValues'] ?? false)) {
|
|
continue;
|
|
}
|
|
|
|
// This is not a group; this is likely broken configuration
|
|
if ($cfg['defaultValues'] === []) {
|
|
unset($wizardItems[$key]);
|
|
}
|
|
|
|
// If defaultValues are defined, check access by traversing all fields with default values:
|
|
foreach ($cfg['defaultValues'] as $fieldName => $value) {
|
|
if (!$schema->hasField($fieldName)) {
|
|
continue;
|
|
}
|
|
// Get information about if the field value is OK:
|
|
$config = $schema->getField($fieldName)->getConfiguration();
|
|
$userNotAllowedToAccess = ($config['type'] ?? '') === 'select' && ($config['authMode'] ?? false)
|
|
&& !$backendUser->checkAuthMode('tt_content', $fieldName, $value);
|
|
// Check removeItems
|
|
if (!isset($removeItems[$fieldName]) && ($TCEFORM_TSconfig[$fieldName]['removeItems'] ?? false)) {
|
|
$removeItems[$fieldName] = array_flip(GeneralUtility::trimExplode(
|
|
',',
|
|
$TCEFORM_TSconfig[$fieldName]['removeItems'],
|
|
true
|
|
));
|
|
}
|
|
// Check keepItems
|
|
if (!isset($keepItems[$fieldName]) && ($TCEFORM_TSconfig[$fieldName]['keepItems'] ?? false)) {
|
|
$keepItems[$fieldName] = array_flip(GeneralUtility::trimExplode(
|
|
',',
|
|
$TCEFORM_TSconfig[$fieldName]['keepItems'],
|
|
true
|
|
));
|
|
}
|
|
$isNotInKeepItems = !empty($keepItems[$fieldName]) && !isset($keepItems[$fieldName][$value]);
|
|
if ($userNotAllowedToAccess || ($fieldName === 'CType' && (isset($removeItems[$fieldName][$value]) || $isNotInKeepItems))) {
|
|
// Remove element all together:
|
|
unset($wizardItems[$key]);
|
|
break;
|
|
}
|
|
// Add the parameter:
|
|
$wizardItems[$key]['defaultValues'][$fieldName] = $this->getLanguageService()->sL($value);
|
|
}
|
|
}
|
|
return $wizardItems;
|
|
}
|
|
|
|
/**
|
|
* Prepare a wizard tab configuration for sorting.
|
|
*/
|
|
protected function prepareDependencyOrdering(array $wizardGroup, string $key): array
|
|
{
|
|
if (is_string($wizardGroup[$key] ?? null)) {
|
|
$wizardGroup[$key] = GeneralUtility::trimExplode(',', $wizardGroup[$key], true);
|
|
}
|
|
if (is_array($wizardGroup[$key] ?? null)) {
|
|
$wizardGroup[$key] = array_map(
|
|
static fn(string $s): string => rtrim($s, '.') . '.',
|
|
$wizardGroup[$key]
|
|
);
|
|
}
|
|
return $wizardGroup;
|
|
}
|
|
|
|
protected function getLanguageService(): LanguageService
|
|
{
|
|
return $GLOBALS['LANG'];
|
|
}
|
|
|
|
protected function getBackendUser(): BackendUserAuthentication
|
|
{
|
|
return $GLOBALS['BE_USER'];
|
|
}
|
|
}
|