TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:00 +02:00
commit f9941541b7
1178 changed files with 135377 additions and 0 deletions
@@ -0,0 +1,475 @@
<?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\View\BackendLayout\Grid;
use Psr\EventDispatcher\EventDispatcherInterface;
use TYPO3\CMS\Backend\Domain\Repository\Localization\LocalizationRepository;
use TYPO3\CMS\Backend\Preview\StandardPreviewRendererResolver;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Backend\View\BackendLayoutView;
use TYPO3\CMS\Backend\View\Event\AfterPageContentPreviewRenderedEvent;
use TYPO3\CMS\Backend\View\Event\PageContentPreviewRenderingEvent;
use TYPO3\CMS\Backend\View\PageLayoutContext;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Database\ReferenceIndex;
use TYPO3\CMS\Core\Domain\RecordInterface;
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\SchemaLabelResolver;
use TYPO3\CMS\Core\Schema\TcaSchema;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
use TYPO3\CMS\Core\Type\Bitmask\Permission;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Grid Column Item
*
* Model/proxy around a single record which appears in a grid column
* in the page layout. Returns titles, urls etc. and performs basic
* assertions on the contained content element record such as
* is-versioned, is-editable, is-delible and so on.
*
* Accessed from Fluid templates.
*
* @internal
*/
class GridColumnItem
{
/**
* @var GridColumnItem[]
*/
protected array $translations = [];
protected TcaSchema $schema;
protected BackendLayoutView $backendLayoutView;
protected readonly IconFactory $iconFactory;
public function __construct(
protected readonly PageLayoutContext $context,
protected readonly GridColumn $column,
protected RecordInterface $record,
protected readonly string $table = 'tt_content'
) {
$this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
$this->schema = GeneralUtility::makeInstance(TcaSchemaFactory::class)->get($this->table);
$this->backendLayoutView = GeneralUtility::makeInstance(BackendLayoutView::class);
}
public function getContext(): PageLayoutContext
{
return $this->context;
}
public function isVersioned(): bool
{
return $this->record->getComputedProperties()->getVersionedUid() > 0 || (int)($this->getRow()['t3ver_state'] ?? 0) !== 0;
}
public function getPreview(): string
{
$eventDispatcher = GeneralUtility::makeInstance(EventDispatcherInterface::class);
$previewRenderer = GeneralUtility::makeInstance(StandardPreviewRendererResolver::class)->resolveRendererFor($this->record);
$previewHeader = $previewRenderer->renderPageModulePreviewHeader($this);
// Dispatch event to allow listeners adding an alternative content type
// specific preview or to manipulate the content elements' record data.
$event = $eventDispatcher->dispatch(
new PageContentPreviewRenderingEvent($this->table, $this->getRecordType(), $this->record, $this->context)
);
// Update the modified record data
$this->record = $event->getRecord();
// Get specific preview from listeners. In case non was added,
// fall back to the standard preview rendering workflow.
$previewContent = $event->getPreviewContent();
if ($previewContent === null) {
$previewContent = $previewRenderer->renderPageModulePreviewContent($this);
}
if (!$this->backendLayoutView->isCTypeAllowedInColPosByPage(
$this->getRecordType(),
$this->getColumn()->getColumnNumber() ?? 0,
$this->getRecord()->getPid()
)) {
return '<span class="badge badge-warning">' . sprintf($this->getLanguageService()->sL('core.core:labels.typeNotAllowedInColumn'), $this->getContentTypeLabel()) . '</span>';
}
$previewContent = $previewRenderer->wrapPageModulePreview($previewHeader, $previewContent, $this);
$event = $eventDispatcher->dispatch(
new AfterPageContentPreviewRenderedEvent($this->table, $this->getRecordType(), $this->record, $this->context, $previewContent)
);
return $event->getPreviewContent();
}
public function getWrapperClassName(): string
{
$wrapperClassNames = [];
if ($this->isDisabled()) {
$wrapperClassNames[] = 't3-page-ce-hidden t3js-hidden-record';
}
if ($this->isInconsistentLanguage()
|| !$this->backendLayoutView->isCTypeAllowedInColPosByPage(
$this->getRecordType(),
$this->getColumn()->getColumnNumber() ?? 0,
$this->getRecord()->getPid()
)
) {
$wrapperClassNames[] = 't3-page-ce-warning';
}
return implode(' ', $wrapperClassNames);
}
public function isDelible(): bool
{
$backendUser = $this->getBackendUser();
if (!$backendUser->doesUserHaveAccess($this->context->getPageRecord(), Permission::CONTENT_EDIT)) {
return false;
}
return !($backendUser->getTSConfig()['options.']['disableDelete.'][$this->table] ?? $backendUser->getTSConfig()['options.']['disableDelete'] ?? false);
}
public function getDeleteUrl(): string
{
return (string)GeneralUtility::makeInstance(UriBuilder::class)->buildUriFromRoute(
'tce_db',
[
'cmd' => [
$this->table => [
$this->record->getUid() => [
'delete' => 1,
],
],
],
'redirect' => $this->context->getReturnUrl(),
]
);
}
public function getDeleteMessage(): string
{
$recordInfo = BackendUtility::cropToTitleLength(BackendUtility::getRecordTitle($this->table, $this->getRow()));
if ($this->getBackendUser()->shallDisplayDebugInformation()) {
$recordInfo .= ' [' . $this->table . ':' . $this->record->getUid() . ']';
}
$refCountMsg = BackendUtility::referenceCount(
$this->table,
$this->record->getUid(),
LF . $this->getLanguageService()->sL('core.core:labels.referencesToRecord'),
(string)$this->getReferenceCount($this->record->getUid())
);
$translationCount = count(GeneralUtility::makeInstance(LocalizationRepository::class)->getRecordTranslations($this->table, $this->record->getUid()));
if ($translationCount > 0) {
$refCountMsg .= LF . sprintf(
$this->getLanguageService()->sL('core.core:labels.translationsOfRecord'),
$translationCount
);
}
return sprintf($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:deleteWarning'), trim($recordInfo)) . $refCountMsg;
}
public function getFooterInfo(): string
{
$previewRenderer = GeneralUtility::makeInstance(StandardPreviewRendererResolver::class)->resolveRendererFor($this->record);
return $previewRenderer->renderPageModulePreviewFooter($this);
}
public function getContentTypeLabel(): string
{
if (($recordType = $this->getRecordType()) === '') {
return '';
}
$contentTypeLabels = $this->context->getContentTypeLabels();
$contentTypeLabel = $contentTypeLabels[$recordType] ?? '';
if ($contentTypeLabel === '') {
$contentTypeLabel = $this->getLabelFromItemListMerged();
$contentTypeLabel = $this->getLanguageService()->sL($contentTypeLabel);
}
return $contentTypeLabel;
}
public function getIcons(): string
{
$row = $this->record->getRawRecord()?->toArray() ?? [];
$icons = [];
$icon = $this->iconFactory
->getIconForRecord($this->table, $row, IconSize::SMALL)
->setTitle(BackendUtility::getRecordIconAltText($row, $this->table, false))
->render();
if ($this->getBackendUser()->checkRecordEditAccess($this->table, $this->getRow())->isAllowed) {
$icon = BackendUtility::wrapClickMenuOnIcon($icon, $this->table, $this->record->getUid());
}
$icons[] = $icon;
if ($lockInfo = BackendUtility::isRecordLocked($this->table, $this->record->getUid())) {
$icons[] = '<a href="#" title="' . htmlspecialchars($lockInfo['msg']) . '">'
. $this->iconFactory->getIcon('status-user-backend', IconSize::SMALL, 'overlay-edit')->render() . '</a>';
}
return implode(' ', $icons);
}
public function getSiteLanguage(): SiteLanguage
{
return $this->context->getSiteLanguage((int)($this->getRow()[$this->schema->getCapability(TcaSchemaCapability::Language)->getLanguageField()->getName()] ?? 0));
}
public function getRecord(): RecordInterface
{
return $this->record;
}
public function getRow(): array
{
return $this->record->getRawRecord()?->toArray(true) ?? [];
}
public function setRecord(RecordInterface $record): void
{
$this->record = $record;
}
public function getColumn(): GridColumn
{
return $this->column;
}
public function getTranslations(): array
{
return $this->translations;
}
public function addTranslation(int $languageId, GridColumnItem $translation): GridColumnItem
{
$this->translations[$languageId] = $translation;
return $this;
}
public function isDisabled(): bool
{
$row = $this->getRow();
return (
$this->schema->hasCapability(TcaSchemaCapability::RestrictionDisabledField)
&& ($row[(string)$this->schema->getCapability(TcaSchemaCapability::RestrictionDisabledField)] ?? false)
)
|| (
$this->schema->hasCapability(TcaSchemaCapability::RestrictionStartTime)
&& ($row[(string)$this->schema->getCapability(TcaSchemaCapability::RestrictionStartTime)] ?? 0) > $GLOBALS['EXEC_TIME']
)
|| (
$this->schema->hasCapability(TcaSchemaCapability::RestrictionEndTime)
&& (($endTime = ($row[(string)$this->schema->getCapability(TcaSchemaCapability::RestrictionEndTime)] ?? 0)) !== 0 && $endTime < $GLOBALS['EXEC_TIME'])
);
}
public function isEditable(): bool
{
$backendUser = $this->getBackendUser();
if ($backendUser->isAdmin()) {
return true;
}
$pageRecord = $this->context->getPageRecord();
return $backendUser->doesUserHaveAccess($pageRecord, Permission::CONTENT_EDIT)
&& $backendUser->checkRecordEditAccess($this->table, $this->record)->isAllowed
&& (
!($pagesSchema = GeneralUtility::makeInstance(TcaSchemaFactory::class)->get('pages'))->hasCapability(TcaSchemaCapability::EditLock)
|| !($pageRecord[$pagesSchema->getCapability(TcaSchemaCapability::EditLock)->getFieldName()] ?? false)
);
}
public function isDragAndDropAllowed(): bool
{
$pageRecord = $this->context->getPageRecord();
$typeColumn = $this->getTypeColumn();
return (int)($this->getRow()[$this->schema->getCapability(TcaSchemaCapability::Language)->getTranslationOriginPointerField()->getName()] ?? 0) === 0
&& (
$this->getBackendUser()->isAdmin()
|| (
(
!($this->getRow()[$this->schema->getCapability(TcaSchemaCapability::EditLock)->getFieldName()] ?? false)
&& (
!($pagesSchema = GeneralUtility::makeInstance(TcaSchemaFactory::class)->get('pages'))->hasCapability(TcaSchemaCapability::EditLock)
|| !($pageRecord[$pagesSchema->getCapability(TcaSchemaCapability::EditLock)->getFieldName()] ?? false)
)
)
&& $this->getBackendUser()->doesUserHaveAccess($pageRecord, Permission::CONTENT_EDIT)
&& $this->getBackendUser()->checkAuthMode($this->table, $typeColumn, $this->getRecordType())
)
)
;
}
public function isInconsistentLanguage(): bool
{
$allowInconsistentLanguageHandling = $this->context->getDrawingConfiguration()->getAllowInconsistentLanguageHandling();
return !$allowInconsistentLanguageHandling
&& $this->getSiteLanguage()->getLanguageId() !== 0
&& $this->context->getLanguageModeIdentifier() === 'mixed'
&& (int)($this->getRow()[$this->schema->getCapability(TcaSchemaCapability::Language)->getTranslationOriginPointerField()->getName()] ?? 0) === 0;
}
public function getNewContentAfterUrl(): string
{
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
return (string)$uriBuilder->buildUriFromRoute('new_content_element_wizard', [
'id' => $this->context->getPageId(),
'sys_language_uid' => $this->context->getSiteLanguage()->getLanguageId(),
'colPos' => $this->column->getColumnNumber(),
'uid_pid' => -$this->record->getUid(),
'returnUrl' => $this->context->getReturnUrl(),
]);
}
public function getVisibilityToggleUrl(): string
{
$disabledFieldName = $this->getDisabledFieldName();
if ($this->getRow()[$disabledFieldName] ?? false) {
$value = 0;
} else {
$value = 1;
}
return GeneralUtility::makeInstance(UriBuilder::class)->buildUriFromRoute(
'tce_db',
[
'data' => [
$this->table => [
$this->record->getComputedProperties()->getVersionedUid() ?: $this->record->getUid() => [
$disabledFieldName => $value,
],
],
],
'redirect' => $this->context->getReturnUrl(),
]
) . '#element-' . $this->table . '-' . $this->record->getUid();
}
public function getVisibilityToggleTitle(): string
{
if ($this->getRow()[$this->getDisabledFieldName()] ?? false) {
return $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:unHide');
}
return $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:hide');
}
public function getVisibilityToggleIconName(): string
{
return ($this->getRow()[$this->getDisabledFieldName()] ?? false) ? 'unhide' : 'hide';
}
public function isVisibilityToggling(): bool
{
$disabledFieldName = $this->getDisabledFieldName();
return $disabledFieldName
&& $this->schema->hasField($disabledFieldName)
&& (
!$this->schema->getField($disabledFieldName)->supportsAccessControl()
|| $this->getBackendUser()->check('non_exclude_fields', $this->table . ':' . $disabledFieldName)
)
;
}
public function getEditUrl(): string
{
$urlParameters = [
'edit' => [
$this->table => [
$this->record->getUid() => 'edit',
],
],
'module' => 'web_layout',
'returnUrl' => $this->context->getReturnUrl() . '#element-' . $this->table . '-' . $this->record->getUid(),
];
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
return $uriBuilder->buildUriFromRoute('record_edit', $urlParameters) . '#element-' . $this->table . '-' . $this->record->getUid();
}
public function getContextualEditUrl(): string
{
$urlParameters = [
'edit' => [
$this->table => [
$this->record->getUid() => 'edit',
],
],
'module' => 'web_layout',
'returnUrl' => $this->context->getReturnUrl() . '#element-' . $this->table . '-' . $this->record->getUid(),
];
return (string)GeneralUtility::makeInstance(UriBuilder::class)->buildUriFromRoute('record_edit_contextual', $urlParameters);
}
public function getTypeColumn(): string
{
// @todo This only supports local record types due to usages in this class
return $this->schema->supportsSubSchema() && !$this->schema->getSubSchemaTypeInformation()->isPointerToForeignFieldInForeignSchema()
? $this->schema->getSubSchemaTypeInformation()->getFieldName()
: '';
}
public function getRecordType(): string
{
return $this->record->getRecordType() ?? '';
}
public function getTable(): string
{
return $this->table;
}
/**
* Gets the number of records referencing the record with the UID $uid in
* the current table.
*
* @return int The number of references to record $uid in table
*/
protected function getReferenceCount(int $uid): int
{
return GeneralUtility::makeInstance(ReferenceIndex::class)->getNumberOfReferencedRecords($this->table, $uid);
}
protected function getLabelFromItemListMerged(): string
{
$table = $this->table;
$typeColumn = $this->getTypeColumn();
$recordType = $this->getRecordType();
$columnTsConfig = BackendUtility::getPagesTSconfig($this->record->getPid())['TCEFORM.'][$table . '.'][$typeColumn . '.'] ?? [];
return GeneralUtility::makeInstance(SchemaLabelResolver::class)
->getLabelForFieldValue($table, $typeColumn, $recordType, $this->getRow(), is_array($columnTsConfig) ? $columnTsConfig : []);
}
protected function getDisabledFieldName(): ?string
{
return $this->schema->hasCapability(TcaSchemaCapability::RestrictionDisabledField) ? (string)$this->schema->getCapability(TcaSchemaCapability::RestrictionDisabledField) : null;
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
protected function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
}