TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,554 @@
|
||||
<?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\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Attribute\AsController;
|
||||
use TYPO3\CMS\Backend\History\RecordHistory;
|
||||
use TYPO3\CMS\Backend\History\RecordHistoryRollback;
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Backend\Template\Components\ButtonBar;
|
||||
use TYPO3\CMS\Backend\Template\Components\ComponentFactory;
|
||||
use TYPO3\CMS\Backend\Template\ModuleTemplate;
|
||||
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Backend\View\ValueFormatter\FlexFormValueFormatter;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\DataHandling\History\RecordHistoryStore;
|
||||
use TYPO3\CMS\Core\DataHandling\TableColumnType;
|
||||
use TYPO3\CMS\Core\Domain\DateTimeFactory;
|
||||
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Imaging\IconSize;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
|
||||
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
|
||||
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
|
||||
use TYPO3\CMS\Core\Site\SiteFinder;
|
||||
use TYPO3\CMS\Core\Type\Bitmask\Permission;
|
||||
use TYPO3\CMS\Core\Utility\DiffGranularity;
|
||||
use TYPO3\CMS\Core\Utility\DiffUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Controller for showing the history module of TYPO3s backend.
|
||||
*
|
||||
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
#[AsController]
|
||||
class ElementHistoryController
|
||||
{
|
||||
protected RecordHistory $historyObject;
|
||||
|
||||
/**
|
||||
* Display inline differences or not
|
||||
*/
|
||||
protected bool $showDiff = true;
|
||||
protected array $recordCache = [];
|
||||
|
||||
protected ModuleTemplate $view;
|
||||
|
||||
protected string $returnUrl = '';
|
||||
|
||||
public function __construct(
|
||||
protected readonly IconFactory $iconFactory,
|
||||
protected readonly UriBuilder $uriBuilder,
|
||||
protected readonly ModuleTemplateFactory $moduleTemplateFactory,
|
||||
private readonly DiffUtility $diffUtility,
|
||||
private readonly FlexFormValueFormatter $flexFormValueFormatter,
|
||||
private readonly TcaSchemaFactory $tcaSchemaFactory,
|
||||
private readonly ComponentFactory $componentFactory,
|
||||
private readonly SiteFinder $siteFinder,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Injects the request object for the current request or sub request
|
||||
* As this controller goes only through the main() method, it is rather simple for now
|
||||
*/
|
||||
public function mainAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$this->view = $this->moduleTemplateFactory->create($request);
|
||||
$backendUser = $this->getBackendUser();
|
||||
$this->view->getDocHeaderComponent()->setPageBreadcrumb([]);
|
||||
|
||||
$parsedBody = $request->getParsedBody();
|
||||
$queryParams = $request->getQueryParams();
|
||||
|
||||
$this->returnUrl = GeneralUtility::sanitizeLocalUrl($parsedBody['returnUrl'] ?? $queryParams['returnUrl'] ?? '', $request);
|
||||
|
||||
$lastHistoryEntry = (int)($parsedBody['historyEntry'] ?? $queryParams['historyEntry'] ?? 0);
|
||||
$rollbackFields = $parsedBody['rollbackFields'] ?? $queryParams['rollbackFields'] ?? null;
|
||||
$element = $parsedBody['element'] ?? $queryParams['element'] ?? null;
|
||||
$moduleSettings = $this->processSettings($request);
|
||||
$this->view->assign('isUserInWorkspace', $backendUser->workspace > 0);
|
||||
|
||||
$this->showDiff = (bool)$moduleSettings['showDiff'];
|
||||
|
||||
// Start history object
|
||||
$this->historyObject = GeneralUtility::makeInstance(RecordHistory::class, $element);
|
||||
$this->historyObject->setShowSubElements((bool)$moduleSettings['showSubElements']);
|
||||
$this->historyObject->setLastHistoryEntryNumber($lastHistoryEntry);
|
||||
if ($moduleSettings['maxSteps']) {
|
||||
$this->historyObject->setMaxSteps((int)$moduleSettings['maxSteps']);
|
||||
}
|
||||
|
||||
// Do the actual logic now (rollback, show a diff for certain changes,
|
||||
// or show the full history of a page or a specific record)
|
||||
$changeLog = $this->historyObject->getChangeLog();
|
||||
if (!empty($changeLog)) {
|
||||
if ($rollbackFields !== null) {
|
||||
$diff = $this->historyObject->getDiff($changeLog);
|
||||
GeneralUtility::makeInstance(RecordHistoryRollback::class)->performRollback($rollbackFields, $diff);
|
||||
} elseif ($lastHistoryEntry) {
|
||||
$completeDiff = $this->historyObject->getDiff($changeLog);
|
||||
$this->displayMultipleDiff($completeDiff);
|
||||
$button = $this->componentFactory->createLinkButton()
|
||||
->setHref($this->buildUrl(['historyEntry' => '']))
|
||||
->setIcon($this->iconFactory->getIcon('actions-view-go-back', IconSize::SMALL))
|
||||
->setTitle($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_show_rechis.xlf:fullView'))
|
||||
->setShowLabelText(true);
|
||||
$this->view->addButtonToButtonBar($button);
|
||||
}
|
||||
if ($this->historyObject->getElementString() !== '') {
|
||||
$this->displayHistory($changeLog);
|
||||
}
|
||||
}
|
||||
|
||||
$elementData = $this->historyObject->getElementInformation();
|
||||
$editLock = false;
|
||||
if (!empty($elementData)) {
|
||||
[$elementTable, $elementUid] = $elementData;
|
||||
$elementUid = (int)$elementUid;
|
||||
$this->setPagePath($elementTable, $elementUid);
|
||||
$editLock = $this->getEditLockFromElement($elementTable, $elementUid);
|
||||
// Get link to page history if the element history is shown
|
||||
if ($elementTable !== 'pages') {
|
||||
$parentPage = BackendUtility::getRecord($elementTable, $elementUid, '*', '', false);
|
||||
if ($parentPage['pid'] > 0 && BackendUtility::readPageAccess($parentPage['pid'], $backendUser->getPagePermsClause(Permission::PAGE_SHOW))) {
|
||||
$button = $this->componentFactory->createLinkButton()
|
||||
->setHref($this->buildUrl([
|
||||
'element' => 'pages:' . $parentPage['pid'],
|
||||
'historyEntry' => '',
|
||||
]))
|
||||
->setIcon($this->iconFactory->getIcon('apps-pagetree-page-default', IconSize::SMALL))
|
||||
->setTitle($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_show_rechis.xlf:elementHistory_link'))
|
||||
->setShowLabelText(true);
|
||||
$this->view->addButtonToButtonBar($button, ButtonBar::BUTTON_POSITION_LEFT, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($element !== null) {
|
||||
$this->addLanguageSwitcher($request, $backendUser, $element);
|
||||
}
|
||||
|
||||
$this->view->assign('editLock', $editLock);
|
||||
$this->view->assign('moduleSettings', $moduleSettings);
|
||||
$this->view->assign('settingsFormUrl', $this->buildUrl());
|
||||
|
||||
// Setting up the buttons and markers for docheader
|
||||
$this->getButtons();
|
||||
|
||||
return $this->view->renderResponse('RecordHistory/Main');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the correct path to the current record
|
||||
*/
|
||||
protected function setPagePath(string $table, int $uid): void
|
||||
{
|
||||
$record = BackendUtility::getRecord($table, $uid, '*', '', false);
|
||||
if ($table === 'pages') {
|
||||
$pageId = $uid;
|
||||
} else {
|
||||
$pageId = $record['pid'];
|
||||
}
|
||||
|
||||
$pageAccess = BackendUtility::readPageAccess($pageId, $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW));
|
||||
if (is_array($pageAccess)) {
|
||||
$this->view->getDocHeaderComponent()->setPageBreadcrumb($pageAccess);
|
||||
}
|
||||
|
||||
$schema = $this->tcaSchemaFactory->get($table);
|
||||
$this->view->assignMultiple([
|
||||
'recordTable' => $table,
|
||||
'recordTableReadable' => $schema->getTitle($this->getLanguageService()->sL(...)),
|
||||
'recordUid' => $uid,
|
||||
'recordTitle' => $this->generateTitle($table, (string)$uid),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getButtons(): void
|
||||
{
|
||||
if ($this->returnUrl) {
|
||||
$backButton = $this->componentFactory->createLinkButton()
|
||||
->setHref($this->returnUrl)
|
||||
->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.closeDoc'))
|
||||
->setShowLabelText(true)
|
||||
->setIcon($this->iconFactory->getIcon('actions-close', IconSize::SMALL));
|
||||
$this->view->addButtonToButtonBar($backButton);
|
||||
}
|
||||
}
|
||||
|
||||
protected function processSettings(ServerRequestInterface $request): array
|
||||
{
|
||||
// Get current selection from UC, merge data, write it back to UC
|
||||
$currentSelection = $this->getBackendUser()->getModuleData('history');
|
||||
if (!is_array($currentSelection)) {
|
||||
$currentSelection = ['maxSteps' => '', 'showDiff' => 1, 'showSubElements' => 1];
|
||||
}
|
||||
$currentSelectionOverride = $request->getParsedBody()['settings'] ?? null;
|
||||
if (is_array($currentSelectionOverride) && !empty($currentSelectionOverride)) {
|
||||
$currentSelection = array_merge($currentSelection, $currentSelectionOverride);
|
||||
$this->getBackendUser()->pushModuleData('history', $currentSelection);
|
||||
}
|
||||
return $currentSelection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a translation selection dropdown if the record is language aware.
|
||||
*/
|
||||
protected function addLanguageSwitcher(
|
||||
ServerRequestInterface $request,
|
||||
BackendUserAuthentication $backendUser,
|
||||
string $element,
|
||||
): void {
|
||||
$translations = $this->historyObject->getTranslations($element);
|
||||
if ($translations === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$languageDropDownButton = $this->componentFactory->createDropDownButton()
|
||||
->setLabel($this->getLanguageService()->sL('core.core:labels.language'))
|
||||
->setShowLabelText(true);
|
||||
|
||||
try {
|
||||
$site = $this->siteFinder->getSiteByPageId($translations['page']);
|
||||
} catch (SiteNotFoundException) {
|
||||
$site = $request->getAttribute('site');
|
||||
}
|
||||
|
||||
$availableLanguages = $site->getAvailableLanguages($backendUser, false, $translations['page']);
|
||||
|
||||
foreach ($translations['elements'] as $translation) {
|
||||
$siteLanguage = $availableLanguages[$translation['language']] ?? null;
|
||||
if (!$siteLanguage instanceof SiteLanguage) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$languageItem = $this->componentFactory->createDropDownRadio()
|
||||
->setActive($translation['element'] === $element)
|
||||
->setIcon($this->iconFactory->getIcon($siteLanguage->getFlagIdentifier()))
|
||||
->setHref((string)$this->uriBuilder->buildUriFromRoute('record_history', [
|
||||
'element' => $translation['element'],
|
||||
'returnUrl' => $this->returnUrl,
|
||||
]))
|
||||
->setLabel($siteLanguage->getTitle());
|
||||
$languageDropDownButton->addItem($languageItem);
|
||||
|
||||
if ($languageItem->isActive()) {
|
||||
$languageDropDownButton->setLabel($siteLanguage->getTitle());
|
||||
}
|
||||
}
|
||||
|
||||
$this->view->getDocHeaderComponent()->setLanguageSelector($languageDropDownButton);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a diff over multiple fields including rollback links
|
||||
*
|
||||
* @param array $diff Difference array
|
||||
*/
|
||||
protected function displayMultipleDiff(array $diff): void
|
||||
{
|
||||
$languageService = $this->getLanguageService();
|
||||
|
||||
// Get all array keys needed
|
||||
/** @var string[] $arrayKeys */
|
||||
$arrayKeys = array_merge(array_keys($diff['newData']), array_keys($diff['insertsDeletes']), array_keys($diff['oldData']));
|
||||
$arrayKeys = array_unique($arrayKeys);
|
||||
if (!empty($arrayKeys)) {
|
||||
$lines = [];
|
||||
foreach ($arrayKeys as $key) {
|
||||
$singleLine = [];
|
||||
$elParts = explode(':', $key);
|
||||
// Turn around diff because it should be a "rollback preview"
|
||||
if ((int)($diff['insertsDeletes'][$key] ?? 0) === 1) {
|
||||
// insert
|
||||
$singleLine['insertDelete'] = 'delete';
|
||||
} elseif ((int)($diff['insertsDeletes'][$key] ?? 0) === -1) {
|
||||
$singleLine['insertDelete'] = 'insert';
|
||||
}
|
||||
// Build up temporary diff array
|
||||
// turn around diff because it should be a "rollback preview"
|
||||
if ($diff['newData'][$key] ?? false) {
|
||||
$tmpArr = [
|
||||
'newRecord' => $diff['oldData'][$key],
|
||||
'oldRecord' => $diff['newData'][$key],
|
||||
];
|
||||
|
||||
// show changes
|
||||
if (!$this->showDiff) {
|
||||
// Display field names instead of full diff
|
||||
// Re-write field names with labels
|
||||
/** @var string[] $tmpFieldList */
|
||||
$tmpFieldList = array_keys($tmpArr['newRecord']);
|
||||
foreach ($tmpFieldList as $fieldKey => $value) {
|
||||
$itemLabel = '';
|
||||
if ($this->tcaSchemaFactory->has($elParts[0]) && ($schema = $this->tcaSchemaFactory->get($elParts[0]))->hasField($value)) {
|
||||
$itemLabel = $schema->getField($value)->getLabel();
|
||||
}
|
||||
$tmp = str_replace(':', '', $languageService->sL($itemLabel));
|
||||
if ($tmp) {
|
||||
$tmpFieldList[$fieldKey] = $tmp;
|
||||
} else {
|
||||
// remove fields if no label available
|
||||
unset($tmpFieldList[$fieldKey]);
|
||||
}
|
||||
}
|
||||
$singleLine['fieldNames'] = implode(',', $tmpFieldList);
|
||||
} else {
|
||||
// Display diff
|
||||
$singleLine['differences'] = $this->renderDiff($tmpArr, $elParts[0], (int)$elParts[1], true);
|
||||
}
|
||||
}
|
||||
$elParts = explode(':', $key);
|
||||
$singleLine['revertRecordUrl'] = $this->buildUrl(['rollbackFields' => $key]);
|
||||
$singleLine['title'] = $this->generateTitle($elParts[0], $elParts[1]);
|
||||
$singleLine['recordTable'] = $elParts[0];
|
||||
$singleLine['recordUid'] = $elParts[1];
|
||||
$lines[] = $singleLine;
|
||||
}
|
||||
$this->view->assign('revertAllUrl', $this->buildUrl(['rollbackFields' => 'ALL']));
|
||||
$this->view->assign('multipleDiff', $lines);
|
||||
}
|
||||
$this->view->assign('showDifferences', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the full change log
|
||||
*/
|
||||
protected function displayHistory(array $historyEntries): void
|
||||
{
|
||||
if ($historyEntries === []) {
|
||||
return;
|
||||
}
|
||||
$languageService = $this->getLanguageService();
|
||||
$lines = [];
|
||||
$beUserArray = BackendUtility::getUserNames('username,realName,usergroup,uid');
|
||||
|
||||
// Traverse changeLog array:
|
||||
foreach ($historyEntries as $entry) {
|
||||
// Build up single line
|
||||
$singleLine = [];
|
||||
|
||||
// Get user names
|
||||
$singleLine['backendUserUid'] = $entry['userid'];
|
||||
$singleLine['backendUserName'] = $beUserArray[$entry['userid']]['username'] ?? '';
|
||||
$singleLine['backendUserRealName'] = $beUserArray[$entry['userid']]['realName'] ?? '';
|
||||
// Executed by switch user
|
||||
if (!empty($entry['originaluserid'])) {
|
||||
$singleLine['originalBackendUserUid'] = $entry['originaluserid'];
|
||||
$singleLine['originalBackendUserName'] = $beUserArray[$entry['originaluserid']]['username'] ?? '';
|
||||
$singleLine['originalBackendRealName'] = $beUserArray[$entry['originaluserid']]['realName'] ?? '';
|
||||
}
|
||||
|
||||
// Is a change in a workspace?
|
||||
$singleLine['isChangedInWorkspace'] = (int)$entry['workspace'] > 0;
|
||||
|
||||
// Diff link
|
||||
$singleLine['diffUrl'] = $this->buildUrl(['historyEntry' => $entry['uid']]);
|
||||
// Add time
|
||||
$singleLine['day'] = BackendUtility::date($entry['tstamp']);
|
||||
$singleLine['timestamp'] = DateTimeFactory::createFromTimestamp($entry['tstamp']);
|
||||
|
||||
$singleLine['title'] = $this->generateTitle($entry['tablename'], (string)$entry['recuid']);
|
||||
$singleLine['recordTable'] = $entry['tablename'];
|
||||
$singleLine['recordUid'] = $entry['recuid'];
|
||||
|
||||
$singleLine['elementUrl'] = $this->buildUrl(['element' => $entry['tablename'] . ':' . $entry['recuid']]);
|
||||
$singleLine['actiontype'] = $entry['actiontype'];
|
||||
if ((int)$entry['actiontype'] === RecordHistoryStore::ACTION_MODIFY || (int)$entry['actiontype'] === RecordHistoryStore::ACTION_PUBLISH) {
|
||||
// show changes
|
||||
if (!$this->showDiff) {
|
||||
// Display field names instead of full diff
|
||||
// Re-write field names with labels
|
||||
/** @var string[] $tmpFieldList */
|
||||
$tmpFieldList = array_keys($entry['newRecord']);
|
||||
foreach ($tmpFieldList as $key => $value) {
|
||||
$itemLabel = '';
|
||||
if ($this->tcaSchemaFactory->has($entry['tablename']) && ($schema = $this->tcaSchemaFactory->get($entry['tablename']))->hasField($value)) {
|
||||
$itemLabel = $schema->getField($value)->getLabel();
|
||||
}
|
||||
$tmp = str_replace(':', '', $languageService->sL($itemLabel));
|
||||
if ($tmp) {
|
||||
$tmpFieldList[$key] = $tmp;
|
||||
} else {
|
||||
// remove fields if no label available
|
||||
unset($tmpFieldList[$key]);
|
||||
}
|
||||
}
|
||||
$singleLine['fieldNames'] = implode(',', $tmpFieldList);
|
||||
} else {
|
||||
// Display diff
|
||||
$singleLine['differences'] = $this->renderDiff($entry, $entry['tablename'], (int)$entry['recuid']);
|
||||
}
|
||||
}
|
||||
// put line together
|
||||
$lines[] = $singleLine;
|
||||
}
|
||||
$this->view->assign('history', $lines);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders HTML table-rows with the comparison information of a sys_history entry record
|
||||
*
|
||||
* @param array $entry sys_history entry record.
|
||||
* @param string $table The table name
|
||||
* @param int $rollbackUid The UID of the record
|
||||
* @param bool $showRollbackLink Whether a rollback link should be shown for each changed field
|
||||
* @return array array of records
|
||||
*/
|
||||
protected function renderDiff(array $entry, string $table, int $rollbackUid, bool $showRollbackLink = false): array
|
||||
{
|
||||
if (!$this->tcaSchemaFactory->has($table)) {
|
||||
return [];
|
||||
}
|
||||
$lines = [];
|
||||
if (is_array($entry['newRecord'] ?? null)) {
|
||||
$fieldsToDisplay = array_keys($entry['newRecord']);
|
||||
$languageService = $this->getLanguageService();
|
||||
$schema = $this->tcaSchemaFactory->get($table);
|
||||
foreach ($fieldsToDisplay as $fN) {
|
||||
if (!$schema->hasField($fN)) {
|
||||
continue;
|
||||
}
|
||||
$fieldInformation = $schema->getField($fN);
|
||||
if (!$fieldInformation->isType(TableColumnType::PASSTHROUGH)) {
|
||||
if ($fieldInformation->isType(TableColumnType::FLEX)) {
|
||||
$colConfig = $fieldInformation->getConfiguration();
|
||||
$old = $this->flexFormValueFormatter->format($table, $fN, ($entry['oldRecord'][$fN] ?? ''), $rollbackUid, $colConfig);
|
||||
$new = $this->flexFormValueFormatter->format($table, $fN, ($entry['newRecord'][$fN] ?? ''), $rollbackUid, $colConfig);
|
||||
$diffResult = $this->diffUtility->diff(strip_tags($old), strip_tags($new), DiffGranularity::CHARACTER);
|
||||
} else {
|
||||
$old = (string)BackendUtility::getProcessedValue($table, $fN, ($entry['oldRecord'][$fN] ?? ''), 0, true, false, $rollbackUid);
|
||||
$new = (string)BackendUtility::getProcessedValue($table, $fN, ($entry['newRecord'][$fN] ?? ''), 0, true, false, $rollbackUid);
|
||||
$diffResult = $this->diffUtility->diff(strip_tags($old), strip_tags($new));
|
||||
}
|
||||
$rollbackUrl = '';
|
||||
if ($rollbackUid && $showRollbackLink) {
|
||||
$rollbackUrl = $this->buildUrl(['rollbackFields' => $table . ':' . $rollbackUid . ':' . $fN]);
|
||||
}
|
||||
$lines[] = [
|
||||
'title' => $languageService->sL($fieldInformation->getLabel()),
|
||||
'rollbackUrl' => $rollbackUrl,
|
||||
'result' => str_replace('\n', PHP_EOL, str_replace('\r\n', '\n', $diffResult)),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
return $lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the URL for a link to the current page
|
||||
*/
|
||||
protected function buildUrl(array $overrideParameters = []): string
|
||||
{
|
||||
$params = [];
|
||||
|
||||
// Setting default values based on GET parameters:
|
||||
$elementString = $this->historyObject->getElementString();
|
||||
if ($elementString !== '') {
|
||||
$params['element'] = $elementString;
|
||||
}
|
||||
$params['historyEntry'] = $this->historyObject->getLastHistoryEntryNumber();
|
||||
|
||||
if (!empty($this->returnUrl)) {
|
||||
$params['returnUrl'] = $this->returnUrl;
|
||||
}
|
||||
|
||||
// Merging overriding values:
|
||||
$params = array_merge($params, $overrideParameters);
|
||||
|
||||
// Make the link:
|
||||
return (string)$this->uriBuilder->buildUriFromRoute('record_history', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the title and puts the record title behind
|
||||
*/
|
||||
protected function generateTitle(string $table, string $uid): string
|
||||
{
|
||||
if ($this->tcaSchemaFactory->get($table)->hasCapability(TcaSchemaCapability::Label)) {
|
||||
$record = $this->getRecord($table, (int)$uid) ?? [];
|
||||
return BackendUtility::getRecordTitle($table, $record);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a database record (cached).
|
||||
*/
|
||||
protected function getRecord(string $table, int $uid): ?array
|
||||
{
|
||||
if (!isset($this->recordCache[$table][$uid])) {
|
||||
$this->recordCache[$table][$uid] = BackendUtility::getRecord($table, $uid, '*', '', false);
|
||||
}
|
||||
return $this->recordCache[$table][$uid];
|
||||
}
|
||||
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
|
||||
protected function getBackendUser(): BackendUserAuthentication
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the editlock value from page of a history element
|
||||
*/
|
||||
protected function getEditLockFromElement(string $tableName, int $elementUid): bool
|
||||
{
|
||||
// If the user is admin, then he may always edit the page.
|
||||
if ($this->getBackendUser()->isAdmin()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$schema = $this->tcaSchemaFactory->get($tableName);
|
||||
|
||||
// Early return if $elementUid is zero
|
||||
if ($elementUid === 0) {
|
||||
return !$schema->getCapability(TcaSchemaCapability::RestrictionRootLevel)->shallIgnoreRootLevelRestriction();
|
||||
}
|
||||
|
||||
$record = BackendUtility::getRecord($tableName, $elementUid, '*', '', false);
|
||||
// we need the parent page record for the editlock info if element isn't a page
|
||||
if ($tableName !== 'pages') {
|
||||
$pageId = $record['pid'];
|
||||
$record = BackendUtility::getRecord('pages', $pageId, '*', '', false);
|
||||
}
|
||||
|
||||
return $schema->hasCapability(TcaSchemaCapability::EditLock)
|
||||
&& ($record[$schema->getCapability(TcaSchemaCapability::EditLock)->getFieldName()] ?? false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,797 @@
|
||||
<?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\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Attribute\AsController;
|
||||
use TYPO3\CMS\Backend\History\RecordHistory;
|
||||
use TYPO3\CMS\Backend\Routing\Exception\RouteNotFoundException;
|
||||
use TYPO3\CMS\Backend\Routing\PreviewUriBuilder;
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Database\Connection;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Imaging\IconSize;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Resource\File;
|
||||
use TYPO3\CMS\Core\Resource\FileType;
|
||||
use TYPO3\CMS\Core\Resource\Folder;
|
||||
use TYPO3\CMS\Core\Resource\Index\MetaDataRepository;
|
||||
use TYPO3\CMS\Core\Resource\Rendering\RendererRegistry;
|
||||
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
||||
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
|
||||
use TYPO3\CMS\Core\Schema\SearchableSchemaFieldsCollector;
|
||||
use TYPO3\CMS\Core\Schema\TcaSchema;
|
||||
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
|
||||
use TYPO3\CMS\Core\Schema\VisibleSchemaFieldsCollector;
|
||||
use TYPO3\CMS\Core\Type\Bitmask\Permission;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Modal rendering detail about a record. Reached by "Display information" on click menu and records module.
|
||||
*
|
||||
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
#[AsController]
|
||||
class ElementInformationController
|
||||
{
|
||||
/**
|
||||
* Type of element: "db", "file" or "folder"
|
||||
*/
|
||||
protected string $type = 'db';
|
||||
|
||||
protected array $row = [];
|
||||
protected ?string $table = null;
|
||||
protected ?File $fileObject = null;
|
||||
protected ?Folder $folderObject = null;
|
||||
|
||||
public function __construct(
|
||||
protected readonly IconFactory $iconFactory,
|
||||
protected readonly UriBuilder $uriBuilder,
|
||||
protected readonly ModuleTemplateFactory $moduleTemplateFactory,
|
||||
protected readonly ResourceFactory $resourceFactory,
|
||||
protected readonly TcaSchemaFactory $tcaSchemaFactory,
|
||||
protected readonly VisibleSchemaFieldsCollector $visibleSchemaFieldsCollector,
|
||||
private readonly SearchableSchemaFieldsCollector $searchableSchemaFieldsCollector,
|
||||
private readonly MetaDataRepository $metaDataRepository,
|
||||
private readonly ConnectionPool $connectionPool,
|
||||
private readonly RendererRegistry $rendererRegistry,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Injects the request object for the current request or subrequest
|
||||
* As this controller goes only through the main() method, it is rather simple for now
|
||||
*/
|
||||
public function mainAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$backendUser = $this->getBackendUser();
|
||||
$view = $this->moduleTemplateFactory->create($request);
|
||||
$view->getDocHeaderComponent()->disable();
|
||||
$queryParams = $request->getQueryParams();
|
||||
$this->table = $queryParams['table'] ?? null;
|
||||
$uid = $queryParams['uid'] ?? '';
|
||||
$permsClause = $backendUser->getPagePermsClause(Permission::PAGE_SHOW);
|
||||
// Determines if table/uid point to database record or file and if user has access to view information
|
||||
$accessAllowed = false;
|
||||
if ($this->tcaSchemaFactory->has($this->table)) {
|
||||
$uid = (int)$uid;
|
||||
// Check permissions and uid value:
|
||||
if ($uid && $backendUser->check('tables_select', $this->table)) {
|
||||
if ((string)$this->table === 'pages') {
|
||||
$this->row = BackendUtility::readPageAccess($uid, $permsClause) ?: [];
|
||||
$accessAllowed = $this->row !== [];
|
||||
} else {
|
||||
$this->row = BackendUtility::getRecordWSOL($this->table, $uid);
|
||||
if ($this->row) {
|
||||
if (isset($this->row['_ORIG_uid'])) {
|
||||
// Make $uid the uid of the versioned record, while $this->row['uid'] is live record uid
|
||||
$uid = (int)$this->row['_ORIG_uid'];
|
||||
}
|
||||
$pageInfo = BackendUtility::readPageAccess((int)$this->row['pid'], $permsClause) ?: [];
|
||||
$accessAllowed = $pageInfo !== []
|
||||
|| ((int)$this->row['pid'] === 0 && $this->tcaSchemaFactory->get($this->table)->getCapability(TcaSchemaCapability::RestrictionRootLevel)->shallIgnoreRootLevelRestriction());
|
||||
}
|
||||
}
|
||||
}
|
||||
} elseif ($this->table === '_FILE' || $this->table === '_FOLDER' || $this->table === 'sys_file') {
|
||||
$fileOrFolderObject = $this->resourceFactory->retrieveFileOrFolderObject($uid);
|
||||
if ($fileOrFolderObject instanceof Folder) {
|
||||
$this->folderObject = $fileOrFolderObject;
|
||||
$accessAllowed = $this->folderObject->checkActionPermission('read');
|
||||
$this->type = 'folder';
|
||||
} elseif ($fileOrFolderObject instanceof File) {
|
||||
$this->fileObject = $fileOrFolderObject;
|
||||
$accessAllowed = $this->fileObject->checkActionPermission('read');
|
||||
$this->type = 'file';
|
||||
$this->table = 'sys_file';
|
||||
$this->row = BackendUtility::getRecordWSOL($this->table, $fileOrFolderObject->getUid());
|
||||
}
|
||||
}
|
||||
|
||||
// Rendering of the output via fluid
|
||||
$view->assign('accessAllowed', $accessAllowed);
|
||||
$view->assign('hookContent', '');
|
||||
if (!$accessAllowed) {
|
||||
return $view->renderResponse('ContentElement/ElementInformation');
|
||||
}
|
||||
|
||||
// render type by user func
|
||||
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/show_item.php']['typeRendering'] ?? [] as $className) {
|
||||
$typeRenderObj = GeneralUtility::makeInstance($className);
|
||||
if (method_exists($typeRenderObj, 'isValid') && method_exists($typeRenderObj, 'render')) {
|
||||
if ($typeRenderObj->isValid($this->type, $this)) {
|
||||
$view->assign('hookContent', $typeRenderObj->render($this->type, $this, $view));
|
||||
return $view->renderResponse('ContentElement/ElementInformation');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$pageTitle = $this->getPageTitle();
|
||||
$view->setTitle($pageTitle['table'] . ': ' . $pageTitle['title']);
|
||||
$view->assignMultiple($pageTitle);
|
||||
$view->assignMultiple($this->getPreview($request));
|
||||
$view->assignMultiple($this->getPropertiesForTable());
|
||||
$view->assignMultiple($this->getReferences($request, $uid));
|
||||
$view->assign('returnUrl', GeneralUtility::sanitizeLocalUrl($request->getQueryParams()['returnUrl'] ?? '', $request));
|
||||
$view->assign('maxTitleLength', $this->getBackendUser()->uc['titleLen'] ?? 20);
|
||||
|
||||
return $view->renderResponse('ContentElement/ElementInformation');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get page title with icon, table title and record title
|
||||
*/
|
||||
public function getPageTitle(): array
|
||||
{
|
||||
$pageTitle = [
|
||||
'title' => BackendUtility::getRecordTitle($this->table, $this->row),
|
||||
];
|
||||
if ($this->type === 'folder') {
|
||||
$pageTitle['title'] = htmlspecialchars($this->folderObject->getName());
|
||||
$pageTitle['table'] = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:folder');
|
||||
$pageTitle['icon'] = $this->iconFactory->getIconForResource($this->folderObject, IconSize::SMALL)->render();
|
||||
} elseif ($this->type === 'file') {
|
||||
$schema = $this->tcaSchemaFactory->get($this->table);
|
||||
$pageTitle['table'] = $schema->getTitle($this->getLanguageService()->sL(...));
|
||||
$pageTitle['icon'] = $this->iconFactory->getIconForResource($this->fileObject, IconSize::SMALL)->render();
|
||||
} else {
|
||||
$schema = $this->tcaSchemaFactory->get($this->table);
|
||||
$pageTitle['table'] = $schema->getTitle($this->getLanguageService()->sL(...));
|
||||
$pageTitle['icon'] = $this->iconFactory->getIconForRecord($this->table, $this->row, IconSize::SMALL);
|
||||
}
|
||||
return $pageTitle;
|
||||
}
|
||||
|
||||
public function getTable(): ?string
|
||||
{
|
||||
return $this->table;
|
||||
}
|
||||
|
||||
public function getRow(): array
|
||||
{
|
||||
return $this->row;
|
||||
}
|
||||
|
||||
public function getFileObject(): ?File
|
||||
{
|
||||
return $this->fileObject;
|
||||
}
|
||||
|
||||
public function getFolderObject(): ?Folder
|
||||
{
|
||||
return $this->folderObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get preview for current record
|
||||
*/
|
||||
protected function getPreview(ServerRequestInterface $request): array
|
||||
{
|
||||
$preview = [];
|
||||
// Perhaps @todo in future: Also display preview for records - without fileObject
|
||||
if (!$this->fileObject) {
|
||||
return $preview;
|
||||
}
|
||||
|
||||
// check if file is marked as missing
|
||||
if ($this->fileObject->isMissing()) {
|
||||
$preview['missingFile'] = $this->fileObject->getName();
|
||||
} else {
|
||||
$fileRenderer = $this->rendererRegistry->getRenderer($this->fileObject);
|
||||
$preview['url'] = $this->fileObject->getPublicUrl() ?? '';
|
||||
|
||||
// Add "edit metadata" button
|
||||
$preview['editMetadataUrl'] = '';
|
||||
if (($metaDataUid = $this->fileObject->getProperties()['metadata_uid'] ?? false)
|
||||
&& $this->fileObject->isIndexed()
|
||||
&& $this->fileObject->checkActionPermission('editMeta')
|
||||
&& $this->getBackendUser()->check('tables_modify', 'sys_file_metadata')
|
||||
) {
|
||||
$urlParameters = [
|
||||
'edit' => [
|
||||
'sys_file_metadata' => [
|
||||
$metaDataUid => 'edit',
|
||||
],
|
||||
],
|
||||
'returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri(),
|
||||
];
|
||||
$preview['editMetadataUrl'] = (string)$this->uriBuilder->buildUriFromRoute('record_edit', $urlParameters);
|
||||
}
|
||||
|
||||
$width = min(590, $this->fileObject->getMetaData()['width'] ?? 590) . 'm';
|
||||
$height = min(400, $this->fileObject->getMetaData()['height'] ?? 400) . 'm';
|
||||
|
||||
// Check if there is a FileRenderer
|
||||
if ($fileRenderer !== null) {
|
||||
$preview['fileRenderer'] = $fileRenderer->render($this->fileObject, $width, $height);
|
||||
// else check if we can create an Image preview
|
||||
} elseif ($this->fileObject->isImage()) {
|
||||
$preview['fileObject'] = $this->fileObject;
|
||||
$preview['width'] = $width;
|
||||
$preview['height'] = $height;
|
||||
}
|
||||
}
|
||||
return $preview;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get property array for html table
|
||||
*/
|
||||
protected function getPropertiesForTable(): array
|
||||
{
|
||||
$lang = $this->getLanguageService();
|
||||
$propertiesForTable = [];
|
||||
$propertiesForTable['extraFields'] = $this->getExtraFields();
|
||||
|
||||
// Traverse the list of fields to display for the record:
|
||||
$fieldList = $this->getFieldList($this->table, $this->row);
|
||||
$schema = $this->tcaSchemaFactory->has($this->table) ? $this->tcaSchemaFactory->get($this->table) : null;
|
||||
|
||||
foreach ($fieldList as $name) {
|
||||
$name = trim($name);
|
||||
$uid = $this->row['uid'] ?? 0;
|
||||
|
||||
if (!$schema?->hasField($name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// @todo Add meaningful information for mfa field. For the time being we don't display anything at all.
|
||||
if ($this->type === 'db' && $name === 'mfa' && in_array($this->table, ['be_users', 'fe_users'], true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// not a real field -> skip
|
||||
if ($this->type === 'file' && $name === 'fileinfo') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// handled explicitly below with proper byte formatting -> skip
|
||||
if ($this->type === 'file' && $name === 'size') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Field does not exist (e.g. having type=none) -> skip
|
||||
if (!array_key_exists($name, $this->row)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$label = $lang->sL($schema->getField($name)->getLabel());
|
||||
$label = $label ?: $name;
|
||||
|
||||
$propertiesForTable['fields'][] = [
|
||||
'fieldValue' => BackendUtility::getProcessedValue($this->table, $name, $this->row[$name], 0, false, false, $uid, true, 0, $this->row),
|
||||
'fieldLabel' => htmlspecialchars($label),
|
||||
];
|
||||
}
|
||||
|
||||
// additional information for folders and files
|
||||
if ($this->folderObject instanceof Folder || $this->fileObject instanceof File) {
|
||||
// storage
|
||||
if ($this->folderObject instanceof Folder) {
|
||||
$propertiesForTable['fields']['storage'] = [
|
||||
'fieldValue' => $this->folderObject->getStorage()->getName(),
|
||||
'fieldLabel' => htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:sys_file.storage')),
|
||||
];
|
||||
}
|
||||
|
||||
// folder
|
||||
$resourceObject = $this->fileObject ?: $this->folderObject;
|
||||
$parentFolder = $resourceObject->getParentFolder();
|
||||
$propertiesForTable['fields']['folder'] = [
|
||||
'fieldValue' => $parentFolder->getReadablePath(),
|
||||
'fieldLabel' => htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:folder')),
|
||||
];
|
||||
|
||||
if ($this->fileObject instanceof File) {
|
||||
// show file dimensions for images
|
||||
if ($this->fileObject->isType(FileType::IMAGE)) {
|
||||
$propertiesForTable['fields']['width'] = [
|
||||
'fieldValue' => $this->fileObject->getProperty('width') . 'px',
|
||||
'fieldLabel' => htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.width')),
|
||||
];
|
||||
$propertiesForTable['fields']['height'] = [
|
||||
'fieldValue' => $this->fileObject->getProperty('height') . 'px',
|
||||
'fieldLabel' => htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.height')),
|
||||
];
|
||||
}
|
||||
|
||||
// file size
|
||||
$fileSizeInBytes = (int)$this->fileObject->getProperty('size');
|
||||
$propertiesForTable['fields']['size'] = [
|
||||
'fieldValue' => sprintf(
|
||||
'%s (%s)',
|
||||
GeneralUtility::formatSize($fileSizeInBytes, htmlspecialchars($this->getLanguageService()->sL('core.common:byteSizeUnits'))),
|
||||
htmlspecialchars($lang->translate('size_in_bytes', 'core.core', ['numberOfBytes' => GeneralUtility::formatSize($fileSizeInBytes, ' ')])),
|
||||
),
|
||||
'fieldLabel' => $lang->sL($schema?->hasField('size') ? $schema->getField('size')->getLabel() : ''),
|
||||
];
|
||||
|
||||
// show the metadata of a file as well
|
||||
$metaData = $this->metaDataRepository->findByFileUid((int)($this->row['uid'] ?? 0));
|
||||
|
||||
// If there is no metadata record, skip it
|
||||
if ($metaData !== []) {
|
||||
$fileMetadataSchema = $this->tcaSchemaFactory->get('sys_file_metadata');
|
||||
$allowedFields = $this->getFieldList('sys_file_metadata', $metaData);
|
||||
|
||||
foreach ($metaData as $name => $value) {
|
||||
if (!in_array($name, $allowedFields, true)) {
|
||||
continue;
|
||||
}
|
||||
if ($name === 'crdate') {
|
||||
// Is of type=passthrough and already part of
|
||||
// meta information displayed on top of the table
|
||||
continue;
|
||||
}
|
||||
if (!$fileMetadataSchema->hasField($name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$label = $lang->sL($fileMetadataSchema->getField($name)->getLabel());
|
||||
$label = $label ?: $name;
|
||||
|
||||
$propertiesForTable['fields'][] = [
|
||||
'fieldValue' => BackendUtility::getProcessedValue('sys_file_metadata', $name, $value, 0, false, false, (int)$metaData['uid'], true, 0, $metaData),
|
||||
'fieldLabel' => htmlspecialchars($label),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $propertiesForTable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of fields that should be shown for the given table
|
||||
*/
|
||||
protected function getFieldList(string $table, array $row): array
|
||||
{
|
||||
$fieldNamesToExclude = [];
|
||||
if ($this->tcaSchemaFactory->has($table)) {
|
||||
$schema = $this->tcaSchemaFactory->get($table);
|
||||
if ($schema->hasCapability(TcaSchemaCapability::AncestorReferenceField)) {
|
||||
$fieldNamesToExclude[] = $schema->getCapability(TcaSchemaCapability::AncestorReferenceField)->getFieldName();
|
||||
}
|
||||
if ($schema->isLanguageAware()) {
|
||||
$languageCapability = $schema->getCapability(TcaSchemaCapability::Language);
|
||||
$fieldNamesToExclude[] = $languageCapability->getTranslationOriginPointerField()->getName();
|
||||
if ($languageCapability->hasDiffSourceField()) {
|
||||
$fieldNamesToExclude[] = $languageCapability->getDiffSourceField()?->getName();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->searchableSchemaFieldsCollector->getUniqueFieldList(
|
||||
$table,
|
||||
$this->visibleSchemaFieldsCollector->getFieldNames($table, $row, $fieldNamesToExclude),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the extra fields (uid, timestamps, creator) for the table
|
||||
*/
|
||||
protected function getExtraFields(): array
|
||||
{
|
||||
$lang = $this->getLanguageService();
|
||||
$keyLabelPair = [];
|
||||
if (in_array($this->type, ['folder', 'file'], true)) {
|
||||
if ($this->type === 'file') {
|
||||
$keyLabelPair['uid'] = [
|
||||
'value' => (int)$this->row['uid'],
|
||||
];
|
||||
$keyLabelPair['creation_date'] = [
|
||||
'value' => BackendUtility::datetime($this->row['creation_date']),
|
||||
'fieldLabel' => htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.creationDate')),
|
||||
'isDatetime' => true,
|
||||
];
|
||||
$keyLabelPair['modification_date'] = [
|
||||
'value' => BackendUtility::datetime($this->row['modification_date']),
|
||||
'fieldLabel' => htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.timestamp')),
|
||||
'isDatetime' => true,
|
||||
];
|
||||
} else {
|
||||
$keyLabelPair['uid'] = [
|
||||
'value' => $this->folderObject->getCombinedIdentifier(),
|
||||
];
|
||||
}
|
||||
} else {
|
||||
$keyLabelPair['uid'] = [
|
||||
'value' => BackendUtility::getProcessedValueExtra($this->table, 'uid', $this->row['uid']),
|
||||
'fieldLabel' => rtrim(htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:show_item.php.uid')), ':'),
|
||||
];
|
||||
$schema = $this->tcaSchemaFactory->get($this->table);
|
||||
if ($schema->hasCapability(TcaSchemaCapability::CreatedAt)) {
|
||||
$field = $schema->getCapability(TcaSchemaCapability::CreatedAt)->getFieldName();
|
||||
$keyLabelPair[$field] = [
|
||||
'value' => BackendUtility::datetime($this->row[$field]),
|
||||
'fieldLabel' => rtrim(htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.creationDate')), ':'),
|
||||
'isDatetime' => true,
|
||||
];
|
||||
}
|
||||
if ($schema->hasCapability(TcaSchemaCapability::UpdatedAt)) {
|
||||
$field = $schema->getCapability(TcaSchemaCapability::UpdatedAt)->getFieldName();
|
||||
$keyLabelPair[$field] = [
|
||||
'value' => BackendUtility::datetime($this->row[$field]),
|
||||
'fieldLabel' => rtrim(htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.timestamp')), ':'),
|
||||
'isDatetime' => true,
|
||||
];
|
||||
}
|
||||
// Show the user who created the record
|
||||
$recordHistory = GeneralUtility::makeInstance(RecordHistory::class);
|
||||
$ownerInformation = $recordHistory->getCreationInformationForRecord($this->table, $this->row);
|
||||
$ownerUid = (int)(is_array($ownerInformation) && $ownerInformation['usertype'] === 'BE' ? $ownerInformation['userid'] : 0);
|
||||
if ($ownerUid) {
|
||||
$creatorRecord = BackendUtility::getRecord('be_users', $ownerUid);
|
||||
if ($creatorRecord) {
|
||||
$keyLabelPair['creatorRecord'] = [
|
||||
'value' => $creatorRecord,
|
||||
'fieldLabel' => rtrim(htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.creationUserId')), ':'),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
return $keyLabelPair;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get references section (references from and references to current record)
|
||||
*/
|
||||
protected function getReferences(ServerRequestInterface $request, int|string $uid): array
|
||||
{
|
||||
$references = [];
|
||||
switch ($this->type) {
|
||||
case 'db': {
|
||||
$references['refLines'] = $this->makeRef($this->table, $uid, $request);
|
||||
$references['refFromLines'] = $this->makeRefFrom($this->table, $uid, $request);
|
||||
break;
|
||||
}
|
||||
case 'file': {
|
||||
if ($this->fileObject && $this->fileObject->isIndexed()) {
|
||||
$references['refLines'] = $this->makeRef('_FILE', $this->fileObject, $request);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $references;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get field name for specified table/column name
|
||||
*
|
||||
* @param string $fieldName Column name
|
||||
*/
|
||||
protected function getLabelForTableColumn(TcaSchema $schema, string $fieldName): string
|
||||
{
|
||||
if ($schema->hasField($fieldName)) {
|
||||
$field = $schema->getField($fieldName);
|
||||
$field = $field->getLabel() ? $this->getLanguageService()->sL($field->getLabel()) : $fieldName;
|
||||
if (trim($field) === '') {
|
||||
$field = $fieldName;
|
||||
}
|
||||
} else {
|
||||
$field = $fieldName;
|
||||
}
|
||||
return $field;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the record actions
|
||||
*
|
||||
* @param int $uid
|
||||
* @throws RouteNotFoundException
|
||||
*/
|
||||
protected function getRecordActions(TcaSchema $schema, $uid, ServerRequestInterface $request): array
|
||||
{
|
||||
if ($uid < 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$actions = [];
|
||||
// Edit button
|
||||
$urlParameters = [
|
||||
'edit' => [
|
||||
$schema->getName() => [
|
||||
$uid => 'edit',
|
||||
],
|
||||
],
|
||||
'returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri(),
|
||||
];
|
||||
$actions['recordEditUrl'] = (string)$this->uriBuilder->buildUriFromRoute('record_edit', $urlParameters);
|
||||
|
||||
// History button
|
||||
$urlParameters = [
|
||||
'element' => $schema->getName() . ':' . $uid,
|
||||
'returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri(),
|
||||
];
|
||||
$actions['recordHistoryUrl'] = (string)$this->uriBuilder->buildUriFromRoute('record_history', $urlParameters);
|
||||
|
||||
if ($schema->getName() === 'pages') {
|
||||
// Recordlist button
|
||||
$actions['recordsModuleUrl'] = (string)$this->uriBuilder->buildUriFromRoute('records', ['id' => $uid, 'returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri()]);
|
||||
|
||||
// retrieve record to get page language
|
||||
$record = BackendUtility::getRecord($schema->getName(), $uid);
|
||||
$previewUriBuilder = PreviewUriBuilder::create($record)
|
||||
->withRootLine(BackendUtility::BEgetRootLine($uid));
|
||||
|
||||
// View page button
|
||||
$actions['previewUrlAttributes'] = $previewUriBuilder->serializeDispatcherAttributes();
|
||||
}
|
||||
|
||||
return $actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make reference display
|
||||
*
|
||||
* @param string $table Table name
|
||||
* @param int|File $ref Filename or uid
|
||||
* @throws RouteNotFoundException
|
||||
*/
|
||||
protected function makeRef(string $table, $ref, ServerRequestInterface $request): array
|
||||
{
|
||||
$refLines = [];
|
||||
$lang = $this->getLanguageService();
|
||||
// Files reside in sys_file table
|
||||
if ($table === '_FILE') {
|
||||
$selectTable = 'sys_file';
|
||||
$selectUid = $ref->getUid();
|
||||
} else {
|
||||
$selectTable = $table;
|
||||
$selectUid = $ref;
|
||||
}
|
||||
$queryBuilder = $this->connectionPool
|
||||
->getQueryBuilderForTable('sys_refindex');
|
||||
|
||||
$predicates = [
|
||||
$queryBuilder->expr()->eq(
|
||||
'ref_table',
|
||||
$queryBuilder->createNamedParameter($selectTable)
|
||||
),
|
||||
$queryBuilder->expr()->eq(
|
||||
'ref_uid',
|
||||
$queryBuilder->createNamedParameter($selectUid, Connection::PARAM_INT)
|
||||
),
|
||||
];
|
||||
|
||||
$backendUser = $this->getBackendUser();
|
||||
if (!$backendUser->isAdmin()) {
|
||||
$allowedSelectTables = GeneralUtility::trimExplode(',', $backendUser->groupData['tables_select']);
|
||||
$predicates[] = $queryBuilder->expr()->in(
|
||||
'tablename',
|
||||
$queryBuilder->createNamedParameter($allowedSelectTables, Connection::PARAM_STR_ARRAY)
|
||||
);
|
||||
}
|
||||
|
||||
$rows = $queryBuilder
|
||||
->select('*')
|
||||
->from('sys_refindex')
|
||||
->where(...$predicates)
|
||||
->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
|
||||
// Compile information for title tag:
|
||||
foreach ($rows as $row) {
|
||||
if ($row['tablename'] === 'sys_file_reference') {
|
||||
$row = $this->transformFileReferenceToRecordReference($row);
|
||||
if ($row === null) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (!$this->tcaSchemaFactory->has($row['tablename'])) {
|
||||
continue;
|
||||
}
|
||||
$schema = $this->tcaSchemaFactory->get($row['tablename']);
|
||||
$line = [];
|
||||
|
||||
$record = BackendUtility::getRecordWSOL($row['tablename'], $row['recuid']);
|
||||
if ($record) {
|
||||
if (!$this->canAccessPage($schema, $record)) {
|
||||
continue;
|
||||
}
|
||||
$parentRecord = BackendUtility::getRecord('pages', $record['pid']);
|
||||
$parentRecordTitle = is_array($parentRecord)
|
||||
? BackendUtility::getRecordTitle('pages', $parentRecord)
|
||||
: '';
|
||||
$urlParameters = [
|
||||
'edit' => [
|
||||
$row['tablename'] => [
|
||||
$row['recuid'] => 'edit',
|
||||
],
|
||||
],
|
||||
'returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri(),
|
||||
];
|
||||
$url = (string)$this->uriBuilder->buildUriFromRoute('record_edit', $urlParameters);
|
||||
$line['url'] = $url;
|
||||
$line['icon'] = $this->iconFactory->getIconForRecord($row['tablename'], $record, IconSize::SMALL)->render();
|
||||
$line['row'] = $row;
|
||||
$line['record'] = $record;
|
||||
$line['recordTitle'] = BackendUtility::getRecordTitle($row['tablename'], $record);
|
||||
$line['parentRecord'] = $parentRecord;
|
||||
$line['parentRecordTitle'] = $parentRecordTitle;
|
||||
$line['title'] = $schema->getTitle($lang->sL(...)) ?: $row['tablename'];
|
||||
$line['labelForTableColumn'] = $this->getLabelForTableColumn($schema, $row['field']);
|
||||
$line['path'] = BackendUtility::getRecordPath($record['pid'], '', 0, 0);
|
||||
$line['actions'] = $this->getRecordActions($schema, $row['recuid'], $request);
|
||||
} else {
|
||||
$line['row'] = $row;
|
||||
$line['title'] = $schema->getTitle($lang->sL(...)) ?: $row['tablename'];
|
||||
$line['labelForTableColumn'] = $this->getLabelForTableColumn($schema, $row['field']);
|
||||
}
|
||||
$refLines[] = $line;
|
||||
}
|
||||
return $refLines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make reference display (what this elements points to)
|
||||
*
|
||||
* @param string $table Table name
|
||||
* @param int $ref Filename or uid
|
||||
*/
|
||||
protected function makeRefFrom($table, $ref, ServerRequestInterface $request): array
|
||||
{
|
||||
$refFromLines = [];
|
||||
$lang = $this->getLanguageService();
|
||||
|
||||
$queryBuilder = $this->connectionPool
|
||||
->getQueryBuilderForTable('sys_refindex');
|
||||
|
||||
$predicates = [
|
||||
$queryBuilder->expr()->eq(
|
||||
'tablename',
|
||||
$queryBuilder->createNamedParameter($table)
|
||||
),
|
||||
$queryBuilder->expr()->eq(
|
||||
'recuid',
|
||||
$queryBuilder->createNamedParameter($ref, Connection::PARAM_INT)
|
||||
),
|
||||
];
|
||||
|
||||
$backendUser = $this->getBackendUser();
|
||||
if (!$backendUser->isAdmin()) {
|
||||
$allowedSelectTables = GeneralUtility::trimExplode(',', $backendUser->groupData['tables_select']);
|
||||
$predicates[] = $queryBuilder->expr()->in(
|
||||
'ref_table',
|
||||
$queryBuilder->createNamedParameter($allowedSelectTables, Connection::PARAM_STR_ARRAY)
|
||||
);
|
||||
}
|
||||
|
||||
$rows = $queryBuilder
|
||||
->select('*')
|
||||
->from('sys_refindex')
|
||||
->where(...$predicates)
|
||||
->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
|
||||
// Compile information for title tag:
|
||||
foreach ($rows as $row) {
|
||||
$line = [];
|
||||
$record = BackendUtility::getRecordWSOL($row['ref_table'], $row['ref_uid']);
|
||||
if (!$this->tcaSchemaFactory->has($row['ref_table'])) {
|
||||
continue;
|
||||
}
|
||||
$schema = $this->tcaSchemaFactory->get($row['ref_table']);
|
||||
if ($record) {
|
||||
if (!$this->canAccessPage($schema, $record)) {
|
||||
continue;
|
||||
}
|
||||
$urlParameters = [
|
||||
'edit' => [
|
||||
$row['ref_table'] => [
|
||||
$row['ref_uid'] => 'edit',
|
||||
],
|
||||
],
|
||||
'returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri(),
|
||||
];
|
||||
$url = (string)$this->uriBuilder->buildUriFromRoute('record_edit', $urlParameters);
|
||||
$line['url'] = $url;
|
||||
$line['icon'] = $this->iconFactory->getIconForRecord($row['ref_table'], $record, IconSize::SMALL)->render();
|
||||
$line['row'] = $row;
|
||||
$line['record'] = $record;
|
||||
$line['recordTitle'] = BackendUtility::getRecordTitle($row['ref_table'], $record);
|
||||
$line['title'] = $schema->getTitle($lang->sL(...));
|
||||
$line['labelForTableColumn'] = $this->getLabelForTableColumn($schema, $row['field']);
|
||||
$line['path'] = BackendUtility::getRecordPath($record['pid'], '', 0);
|
||||
$line['actions'] = $this->getRecordActions($schema, $row['ref_uid'], $request);
|
||||
} else {
|
||||
$line['row'] = $row;
|
||||
$line['title'] = $schema->getTitle($lang->sL(...));
|
||||
$line['labelForTableColumn'] = $this->getLabelForTableColumn($schema, $row['field']);
|
||||
}
|
||||
$refFromLines[] = $line;
|
||||
}
|
||||
return $refFromLines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert FAL file reference (sys_file_reference) to reference index (sys_refindex) table format
|
||||
*/
|
||||
protected function transformFileReferenceToRecordReference(array $referenceRecord): ?array
|
||||
{
|
||||
$queryBuilder = $this->connectionPool
|
||||
->getQueryBuilderForTable('sys_file_reference');
|
||||
$queryBuilder->getRestrictions()->removeAll();
|
||||
$fileReference = $queryBuilder
|
||||
->select('*')
|
||||
->from('sys_file_reference')
|
||||
->where(
|
||||
$queryBuilder->expr()->eq(
|
||||
'uid',
|
||||
$queryBuilder->createNamedParameter($referenceRecord['recuid'], Connection::PARAM_INT)
|
||||
)
|
||||
)
|
||||
->executeQuery()
|
||||
->fetchAssociative();
|
||||
|
||||
return $fileReference ? [
|
||||
'recuid' => $fileReference['uid_foreign'],
|
||||
'tablename' => $fileReference['tablenames'],
|
||||
'field' => $fileReference['fieldname'],
|
||||
'flexpointer' => '',
|
||||
'softref_key' => '',
|
||||
'sorting' => $fileReference['sorting_foreign'],
|
||||
] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $record Record to be checked (ensure pid is resolved for workspaces)
|
||||
*/
|
||||
protected function canAccessPage(TcaSchema $schema, array $record): bool
|
||||
{
|
||||
$recordPid = (int)($schema->getName() === 'pages' ? $record['uid'] : $record['pid']);
|
||||
$isInWebMount = (bool)$this->getBackendUser()->isInWebMount($schema->getName() === 'pages' ? $record : $record['pid']);
|
||||
return $isInWebMount || ($recordPid === 0 && $schema->getCapability(TcaSchemaCapability::RestrictionRootLevel)->shallIgnoreRootLevelRestriction());
|
||||
}
|
||||
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
|
||||
protected function getBackendUser(): BackendUserAuthentication
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
<?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\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Attribute\AsController;
|
||||
use TYPO3\CMS\Backend\Template\PageRendererBackendSetupTrait;
|
||||
use TYPO3\CMS\Backend\Tree\View\ContentMovingPagePositionMap;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Backend\View\BackendViewFactory;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
|
||||
use TYPO3\CMS\Core\Http\HtmlResponse;
|
||||
use TYPO3\CMS\Core\Localization\LanguageServiceFactory;
|
||||
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
|
||||
use TYPO3\CMS\Core\Page\PageRenderer;
|
||||
use TYPO3\CMS\Core\Type\Bitmask\Permission;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* The "move tt_content element" wizard. Reachable via records module "Re-position content element" on tt_content records.
|
||||
*
|
||||
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
#[AsController]
|
||||
final readonly class MoveElementController
|
||||
{
|
||||
use PageRendererBackendSetupTrait;
|
||||
|
||||
public function __construct(
|
||||
private PageRenderer $pageRenderer,
|
||||
private BackendViewFactory $backendViewFactory,
|
||||
private LanguageServiceFactory $languageServiceFactory,
|
||||
private ExtensionConfiguration $extensionConfiguration
|
||||
) {}
|
||||
|
||||
public function mainAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$this->setUpBasicPageRendererForBackend(
|
||||
$this->pageRenderer,
|
||||
$this->extensionConfiguration,
|
||||
$request,
|
||||
$this->languageServiceFactory->createFromUserPreferences($this->getBackendUser())
|
||||
);
|
||||
$view = $this->backendViewFactory->create($request);
|
||||
$queryParams = $request->getQueryParams();
|
||||
$contentOnly = $queryParams['contentOnly'] ?? false;
|
||||
$this->pageRenderer->loadJavaScriptModule('@typo3/backend/global-event-handler.js');
|
||||
$this->pageRenderer->loadJavaScriptModule('@typo3/backend/tree/page-browser.js');
|
||||
$this->pageRenderer->getJavaScriptRenderer()->addJavaScriptModuleInstruction(
|
||||
JavaScriptModuleInstruction::create('@typo3/backend/wizard/move-content-element.js', 'MoveContentElement')->instance()
|
||||
);
|
||||
$this->pageRenderer->addInlineLanguageLabelFile('EXT:core/Resources/Private/Language/locallang_misc.xlf');
|
||||
$this->pageRenderer->addInlineLanguageLabelFile('EXT:backend/Resources/Private/Language/Wizards/move_content_elements.xlf');
|
||||
|
||||
$view->assignMultiple(array_merge($this->getContentVariables($request), [
|
||||
'contentOnly' => $contentOnly,
|
||||
]));
|
||||
|
||||
$content = $view->render('ContentElement/MoveElement');
|
||||
if ($contentOnly) {
|
||||
return new HtmlResponse($content);
|
||||
}
|
||||
$this->pageRenderer->setBodyContent('<body>' . $content);
|
||||
return new HtmlResponse($this->pageRenderer->render($request));
|
||||
}
|
||||
|
||||
private function getContentVariables(ServerRequestInterface $request): array
|
||||
{
|
||||
$queryParams = $request->getQueryParams();
|
||||
$parsedBody = $request->getParsedBody();
|
||||
|
||||
$contentElementUid = (int)($parsedBody['uid'] ?? $queryParams['uid'] ?? 0);
|
||||
$pageId = (int)($parsedBody['expandPage'] ?? $queryParams['expandPage'] ?? 0);
|
||||
$sysLanguage = (int)($parsedBody['sys_language'] ?? $queryParams['sys_language'] ?? 0);
|
||||
$makeCopy = (bool)($parsedBody['makeCopy'] ?? $queryParams['makeCopy'] ?? 0);
|
||||
$permsClause = $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW);
|
||||
|
||||
if (!$contentElementUid) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$contentElement = BackendUtility::getRecordWSOL('tt_content', $contentElementUid);
|
||||
$pageInfo = BackendUtility::readPageAccess($pageId, $permsClause);
|
||||
$contentElementTitle = BackendUtility::getRecordTitle('tt_content', $contentElement);
|
||||
$assigns = [
|
||||
'record' => $contentElement,
|
||||
'makeCopyChecked' => $makeCopy,
|
||||
'pageInfo' => $pageInfo,
|
||||
'recordTitle' => BackendUtility::cropToTitleLength($contentElementTitle),
|
||||
];
|
||||
if (is_array($pageInfo) && $this->getBackendUser()->isInWebMount($pageInfo['uid'], $permsClause)) {
|
||||
// Initialize the content position map:
|
||||
$contentPositionMap = GeneralUtility::makeInstance(ContentMovingPagePositionMap::class);
|
||||
$contentPositionMap->copyMode = $makeCopy ? 'copy' : 'move';
|
||||
$contentPositionMap->moveUid = $contentElementUid;
|
||||
$contentPositionMap->cur_sys_language = $sysLanguage;
|
||||
|
||||
$pageTitle = BackendUtility::getRecordTitle('pages', $pageInfo);
|
||||
$assigns['pageRecord']['recordTooltip'] = BackendUtility::getRecordIconAltText($pageInfo, 'pages', false);
|
||||
$assigns['pageRecord']['recordTitle'] = BackendUtility::cropToTitleLength($pageTitle);
|
||||
$assigns['contentElementColumns'] = $contentPositionMap->printContentElementColumns($pageId, $pageInfo, $request);
|
||||
}
|
||||
return $assigns;
|
||||
}
|
||||
|
||||
private function getBackendUser(): BackendUserAuthentication
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,710 @@
|
||||
<?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'];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user