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,90 @@
<?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\Preview;
use Psr\Log\LoggerInterface;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Backend\View\Event\PageContentPreviewRenderingEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\View\ViewFactoryData;
use TYPO3\CMS\Core\View\ViewFactoryInterface;
/**
* Check if a Fluid-based preview template was defined for a given CType and render it via Fluid.
*
* Example in page TSconfig:
* mod.web_layout.tt_content.preview.textmedia = EXT:site_mysite/Resources/Private/Templates/Preview/Textmedia.fluid.html
*
* @internal not part of the TYPO3 Core API
*/
final readonly class FluidBasedContentPreviewRenderer
{
public function __construct(
private LoggerInterface $logger,
private ViewFactoryInterface $viewFactory,
) {}
#[AsEventListener('typo3-backend/fluid-preview/content')]
public function __invoke(PageContentPreviewRenderingEvent $event): void
{
$record = $event->getRecord();
$context = $event->getPageLayoutContext();
$fluidTemplateFile = BackendUtility::getPagesTSconfig($record->getPid())['mod.']['web_layout.'][$event->getTable() . '.']['preview.'][$event->getRecordType()] ?? '';
if ($fluidTemplateFile === '') {
return;
}
$fluidTemplateFileAbsolutePath = GeneralUtility::getFileAbsFileName($fluidTemplateFile);
if ($fluidTemplateFileAbsolutePath === '') {
return;
}
try {
$event->setPreviewContent(
$this->viewFactory
->create(new ViewFactoryData(templatePathAndFilename: $fluidTemplateFileAbsolutePath, request: $context->getCurrentRequest()))
->assign('record', $record)
->render()
);
} catch (\Exception $e) {
$this->logger->warning('The backend preview for content element {uid} can not be rendered using the Fluid template file "{file}"', [
'uid' => $record->getUid(),
'file' => $fluidTemplateFileAbsolutePath,
'exception' => $e,
]);
if ($this->getBackendUser()->shallDisplayDebugInformation()) {
$event->setPreviewContent(
$this->viewFactory
->create(new ViewFactoryData(templatePathAndFilename: 'EXT:backend/Resources/Private/Templates/PageLayout/FluidBasedContentPreviewRenderingException.fluid.html'))
->assign('error', [
'message' => str_replace(Environment::getProjectPath(), '', $e->getMessage()),
'title' => 'Error while rendering FluidTemplate preview using ' . str_replace(Environment::getProjectPath(), '', $fluidTemplateFileAbsolutePath),
])
->render()
);
}
}
}
private function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
}
@@ -0,0 +1,70 @@
<?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\Preview;
use TYPO3\CMS\Backend\View\BackendLayout\Grid\GridColumnItem;
/**
* Interface PreviewRendererInterface
*
* Contract for classes capable of rendering previews of a given record
* from a table. Responsible for rendering preview header, preview content
* and wrapping of those two values.
*
* Responsibilities are segmented into three methods, one for each responsibility,
* which is done in order to allow overriding classes to change those parts
* individually without having to replace other parts. Rather than relying on
* implementations to be friendly and divide code into smaller pieces and
* give them (at least) protected visibility, the key methods are instead required
* on the interface directly.
*
* Callers are then responsible for calling each method and combining/wrapping
* the output appropriately.
*/
interface PreviewRendererInterface
{
/**
* Dedicated method for rendering preview header HTML for
* the page module only. Receives the GridColumnItem
* that contains the record for which a preview header
* should be rendered and returned.
*/
public function renderPageModulePreviewHeader(GridColumnItem $item): string;
/**
* Dedicated method for rendering preview body HTML for
* the page module only. Receives the GridColumnItem
* that contains the record for which a preview should be
* rendered and returned.
*/
public function renderPageModulePreviewContent(GridColumnItem $item): string;
/**
* Render a footer for the record to display in page module below
* the body of the item's preview.
*/
public function renderPageModulePreviewFooter(GridColumnItem $item): string;
/**
* Dedicated method for wrapping a preview header and body
* HTML. Receives $item, an instance of GridColumnItem holding
* among other things the record, which can be used to determine
* appropriate wrapping.
*/
public function wrapPageModulePreview(string $previewHeader, string $previewContent, GridColumnItem $item): string;
}
@@ -0,0 +1,263 @@
<?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\Preview;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
use TYPO3\CMS\Core\DataHandling\TableColumnType;
use TYPO3\CMS\Core\Domain\RecordInterface;
use TYPO3\CMS\Core\Html\SanitizerBuilderFactory;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Imaging\IconSize;
use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Resource\FileReference;
use TYPO3\CMS\Core\Resource\ProcessedFile;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
use TYPO3\CMS\Core\Type\Bitmask\Permission;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Use this to render certain fields (label + values) as kind of shortcut or helper
* methods when implementing your own Preview Renderer.
*
* If you need your custom rendering, build your own renderer for your own PreviewRenderer
* class that you can then inject.
*
* The result is always HTML, and it's always HSCed -> ready to be rendered.
*/
#[Autoconfigure(public: true)]
final readonly class RecordFieldPreviewProcessor
{
public function __construct(
private TcaSchemaFactory $schemaFactory,
private UriBuilder $uriBuilder,
private IconFactory $iconFactory,
private SanitizerBuilderFactory $sanitizerBuilderFactory,
#[Autowire(service: 'cache.runtime')]
private FrontendInterface $runtimeCache,
) {}
/**
* Prepare the value of a field but prepend the label before.
*/
public function prepareFieldWithLabel(RecordInterface $record, string $fieldName): ?string
{
if ($record->has($fieldName)) {
$table = $record->getMainType();
$value = $record->get($fieldName);
if ($value !== '' && $value !== null) {
$itemLabels = $this->getItemLabels($record);
$fieldValue = BackendUtility::getProcessedValue($table, $fieldName, $value, 0, false, false, $record->getUid(), true, $record->getPid(), $record->getRawRecord()->toArray()) ?? '';
if ($fieldValue !== '') {
return htmlspecialchars((string)($itemLabels[$fieldName] ?? '')) . ': ' . htmlspecialchars((string)$fieldValue);
}
}
}
return null;
}
/**
* Prepare the value of a field if it exists and it is not empty.
*/
public function prepareField(RecordInterface $record, string $fieldName): ?string
{
if ($record->has($fieldName)) {
$table = $record->getMainType();
$value = $record->get($fieldName);
if ($value !== '' && $value !== null) {
$fieldValue = BackendUtility::getProcessedValue($table, $fieldName, $value, 0, false, false, $record->getUid(), true, $record->getPid(), $record->getRawRecord()->toArray()) ?? '';
if ($fieldValue !== '') {
return htmlspecialchars((string)$fieldValue);
}
}
}
return null;
}
/**
* Processing of larger amounts of text (usually from RTE/bodytext fields) with word wrapping, etc.
*/
public function prepareText(RecordInterface $record, string $fieldName, int $maxLength = 1500): ?string
{
if ($record->has($fieldName)) {
$input = $record->get($fieldName);
$schemaForType = $this->schemaFactory->get($record->getFullType());
if (is_string($input) && $input !== '' && $schemaForType->hasField($fieldName)) {
$isSimpleText = $schemaForType->getField($fieldName)->isType(TableColumnType::INPUT);
if (!$isSimpleText) {
$input = strip_tags($input);
}
$input = GeneralUtility::fixed_lgd_cs($input, $maxLength);
return nl2br(htmlspecialchars(trim($input), ENT_QUOTES, 'UTF-8', false));
}
}
return null;
}
public function preparePlainHtml(RecordInterface $record, string $fieldName, int $maxLines = 100): ?string
{
if ($record->has($fieldName)) {
$html = GeneralUtility::trimExplode(LF, (string)$record->get($fieldName), true);
if ($html !== []) {
$html = array_slice($html, 0, $maxLines);
return str_replace(LF, '<br />', htmlspecialchars(implode(LF, $html)));
}
}
return null;
}
public function preparePreviewableHtml(RecordInterface $record, string $fieldName): ?string
{
if ($record->has($fieldName)) {
$content = $record->get($fieldName);
if (!is_string($content)) {
return null;
}
$builder = $this->sanitizerBuilderFactory->build('preview');
return $builder->build()->sanitize($content);
}
return null;
}
/**
* Render thumbnails for a file collection or files.
*/
public function prepareFiles(iterable|FileReference $fileReferences): ?string
{
$thumbData = [];
$fileReferences = $fileReferences instanceof FileReference ? [$fileReferences] : $fileReferences;
foreach ($fileReferences as $fileReferenceObject) {
// Do not show previews of hidden references
if ($fileReferenceObject->getProperty('hidden')) {
continue;
}
$fileObject = $fileReferenceObject->getOriginalFile();
if ($fileObject->isMissing()) {
$thumbData[] = $this->iconFactory
->getIcon('mimetypes-other-other', IconSize::MEDIUM, 'overlay-missing')
->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:warning.file_missing') . ' ' . $fileObject->getName())
->render();
continue;
}
// Preview web image or media elements
if ($GLOBALS['TYPO3_CONF_VARS']['GFX']['thumbnails']
&& ($fileReferenceObject->getOriginalFile()->isImage() || $fileReferenceObject->getOriginalFile()->isMediaFile())
) {
$cropVariantCollection = CropVariantCollection::create((string)$fileReferenceObject->getProperty('crop'));
$cropArea = $cropVariantCollection->getCropArea();
$processingConfiguration = [
'maxWidth' => 64,
'maxHeight' => 64,
];
if (!$cropArea->isEmpty()) {
$processingConfiguration = [
'maxWidth' => 64,
'maxHeight' => 64,
'crop' => $cropArea->makeAbsoluteBasedOnFile($fileReferenceObject),
];
}
$processedImage = $fileObject->process(ProcessedFile::CONTEXT_IMAGECROPSCALEMASK, $processingConfiguration);
$attributes = [
'src' => $processedImage->getPublicUrl() ?? '',
'width' => $processedImage->getProperty('width'),
'height' => $processedImage->getProperty('height'),
'alt' => $fileReferenceObject->getAlternative() ?: $fileReferenceObject->getName(),
'loading' => 'lazy',
];
$thumbData[] = '<img ' . GeneralUtility::implodeAttributes($attributes, true) . '/>';
} else {
$thumbData[] = $this->iconFactory->getIconForResource($fileObject)->setTitle($fileObject->getName())->render();
}
}
if ($thumbData !== []) {
$result = '';
foreach ($thumbData as $thumbDataItem) {
$result .= '<div class="preview-thumbnails-element"><div class="preview-thumbnails-element-image">' . $thumbDataItem . '</div></div>';
}
return '<div class="preview-thumbnails">' . $result . '</div>';
}
return null;
}
/**
* Will create a link on the input string and possibly a big button after the string which links to editing in the
* RTE. Used for content element content displayed so the user can click the content / "Edit in Rich Text Editor"
* button
*
* @param string $linkText String to link. Must be prepared for HTML output.
* @return string If the whole thing was editable and $linkText is not empty $linkText is returned with the link
* around. Otherwise just $linkText.
*/
public function linkToEditForm(string $linkText, RecordInterface $record, ServerRequestInterface $request): string
{
if ($linkText === '') {
return $linkText;
}
$table = $record->getMainType();
$backendUser = $this->getBackendUser();
if ($backendUser->check('tables_modify', $table)
&& $backendUser->checkRecordEditAccess($table, $record)->isAllowed
&& (new Permission($backendUser->calcPerms(BackendUtility::getRecord('pages', $record->getPid()) ?? [])))->editContentPermissionIsGranted()
) {
$returnUrl = $request->getAttribute('normalizedParams')->getRequestUri() . '#element-' . $table . '-' . $record->getUid();
$editParams = [
'edit' => [$table => [$record->getUid() => 'edit']],
'returnUrl' => $returnUrl,
];
return '<typo3-backend-contextual-record-edit-trigger'
. ' url="' . htmlspecialchars((string)$this->uriBuilder->buildUriFromRoute('record_edit_contextual', $editParams)) . '"'
. ' edit-url="' . htmlspecialchars((string)$this->uriBuilder->buildUriFromRoute('record_edit', $editParams)) . '"'
. ' title="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:edit')) . '"'
. '>' . $linkText . '</typo3-backend-contextual-record-edit-trigger>';
}
return $linkText;
}
private function getItemLabels(RecordInterface $record): array
{
$mainType = $record->getMainType();
$cacheIdentifier = 'recordfieldpreviewprocessor-' . $mainType;
$itemLabels = $this->runtimeCache->get($cacheIdentifier);
if ($itemLabels === false) {
$itemLabels = [];
foreach ($this->schemaFactory->get($mainType)->getFields() as $field) {
$itemLabels[$field->getName()] = $this->getLanguageService()->sL($field->getLabel());
}
$this->runtimeCache->set($cacheIdentifier, $itemLabels);
}
return $itemLabels;
}
private function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
private function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}
@@ -0,0 +1,333 @@
<?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\Preview;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Backend\Domain\Repository\Localization\LocalizationRepository;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Backend\View\BackendLayout\Grid\GridColumnItem;
use TYPO3\CMS\Backend\View\BackendLayoutView;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\DataHandling\TableColumnType;
use TYPO3\CMS\Core\Domain\RawRecord;
use TYPO3\CMS\Core\Domain\Record;
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\Type\Bitmask\Permission;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Legacy preview rendering refactored from PageLayoutView.
* Provided as default preview rendering mechanism via
* StandardPreviewRendererResolver which detects the renderer
* based on TCA configuration.
*
* Can be replaced by custom implementations by changing this TCA configuration.
*
* See also PreviewRendererInterface documentation.
*/
#[Autoconfigure(public: true)]
final readonly class StandardContentPreviewRenderer implements PreviewRendererInterface
{
public function __construct(
private RecordFieldPreviewProcessor $fieldProcessor,
private TcaSchemaFactory $tcaSchemaFactory,
private LocalizationRepository $localizationRepository,
private BackendLayoutView $backendLayoutView,
private IconFactory $iconFactory,
) {}
public function renderPageModulePreviewHeader(GridColumnItem $item): string
{
$record = $item->getRecord()->getRawRecord() ?? $item->getRecord();
$request = $item->getContext()->getCurrentRequest();
if (!$this->tcaSchemaFactory->has($record->getFullType())) {
return '';
}
$schema = $this->tcaSchemaFactory->get($item->getTable());
$outHeader = '';
if ($record->has('header_layout')) {
$headerLayout = (string)$record->get('header_layout');
if ($headerLayout === '100') {
$headerLayoutHiddenLabel = $this->getLanguageService()->sL('LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:header_layout.I.6');
$outHeader .= '<div class="element-preview-header-status">' . htmlspecialchars($headerLayoutHiddenLabel) . '</div>';
}
}
$dateLabel = $this->fieldProcessor->prepareFieldWithLabel($record, 'date');
if ($dateLabel) {
$outHeader .= '<div class="element-preview-header-date">' . htmlspecialchars(strip_tags($dateLabel)) . ' </div>';
}
if ($schema->hasCapability(TcaSchemaCapability::Label)) {
$labelFieldName = $schema->getCapability(TcaSchemaCapability::Label)->getPrimaryFieldName();
$label = $this->fieldProcessor->prepareText($record, $labelFieldName);
if ($label !== null) {
$outHeader .= '<div class="element-preview-header-header">' . $this->fieldProcessor->linkToEditForm($label, $record, $request) . '</div>';
}
}
$subHeader = $this->fieldProcessor->prepareText($record, 'subheader');
if ($subHeader !== null) {
$outHeader .= '<div class="element-preview-header-subheader">' . $this->fieldProcessor->linkToEditForm($subHeader, $record, $request) . '</div>';
}
return $outHeader;
}
public function renderPageModulePreviewContent(GridColumnItem $item): string
{
$recordObj = $item->getRecord();
// This preview should only be used for tt_content records.
if ($recordObj->getMainType() !== 'tt_content') {
return '';
}
$languageService = $this->getLanguageService();
$recordType = $recordObj->getRecordType();
$schema = $this->tcaSchemaFactory->get($recordObj->getMainType());
// If the record type is unknown, render a warning message.
if (!$schema->hasSubSchema($recordType)) {
$message = sprintf(
$languageService->sL('core.core:labels.noMatchingValue'),
$recordType
);
return '<span class="badge badge-warning">' . htmlspecialchars($message) . '</span>';
}
if (!$this->backendLayoutView->isCTypeAllowedInColPosByPage(
$item->getRecordType(),
$item->getColumn()->getColumnNumber() ?? 0,
$item->getRecord()->getPid()
)) {
$message = sprintf(
$languageService->sL('core.core:labels.typeNotAllowedInColumn'),
$recordType
);
return '<span class="badge badge-warning">' . htmlspecialchars($message) . '</span>';
}
$subSchema = $schema->getSubSchema($recordType);
$request = $item->getContext()->getCurrentRequest();
// Draw preview of the item depending on its record type
switch ($recordType) {
case 'header':
break;
case 'shortcut':
if ($recordObj->has('records') && ($records = $recordObj->get('records'))) {
$shortcutContent = '';
$shortcutRecords = $records instanceof \Traversable ? $records : [$records];
foreach ($shortcutRecords as $shortcutRecord) {
$shortcutTableName = $shortcutRecord->getMainType();
$row = $shortcutRecord->getRawRecord()?->toArray() ?? [];
if ($recordObj instanceof Record) {
$shortcutRecord = $this->translateShortcutRecord($recordObj, $shortcutRecord, $shortcutTableName);
}
$icon = $this->iconFactory->getIconForRecord($shortcutTableName, $row, IconSize::SMALL)->render();
$icon = BackendUtility::wrapClickMenuOnIcon(
$icon,
$shortcutTableName,
$shortcutRecord->getUid(),
'1'
);
$pathToContainingPage = BackendUtility::getRecordPath($row['pid'], $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW), 0);
$title = BackendUtility::getRecordTitle($shortcutTableName, $row);
$itemContent = htmlspecialchars($title) . ' <span class="text-variant">[' . $recordObj->getUid() . '] ' . htmlspecialchars($pathToContainingPage) . '</span>';
$shortcutContent .= '<li class="list-group-item">'
. $icon
. ' '
. $this->fieldProcessor->linkToEditForm($itemContent, $shortcutRecord, $request)
. '</li>';
}
return $shortcutContent !== '' ? '<ul class="list-group">' . $shortcutContent . '</ul>' : '';
}
break;
case 'menu_abstract':
case 'menu_categorized_content':
case 'menu_categorized_pages':
case 'menu_pages':
case 'menu_recently_updated':
case 'menu_related_pages':
case 'menu_section':
case 'menu_section_pages':
case 'menu_sitemap':
case 'menu_sitemap_pages':
case 'menu_subpages':
$row = $recordObj->getRawRecord()?->toArray() ?? [];
if ($recordType !== 'menu_sitemap' && (($row['pages'] ?? false) || ($row['selected_categories'] ?? false))) {
// Show pages/categories if the menu type is not "Sitemap"
$content = $this->generateListForMenuContentTypes($row, $recordType);
return $this->fieldProcessor->linkToEditForm($content, $recordObj, $request);
}
break;
case 'bullets':
$list = GeneralUtility::trimExplode(LF, $recordObj->get('bodytext') ?? '', true);
if ($list !== []) {
switch ($recordObj->get('bullets_type')) {
case 0:
$list = array_map(
static fn(string $item) => '<li>' . htmlspecialchars($item) . '</li>',
$list
);
return '<ul>' . implode(LF, $list) . '</ul>';
case 1:
$list = array_map(
static fn(string $item) => '<li>' . htmlspecialchars($item) . '</li>',
$list
);
return '<ol>' . implode(LF, $list) . '</ol>';
case 2:
$list = array_map(
static fn(string $item): string
=> (static function () use ($item) {
$split = GeneralUtility::trimExplode('|', $item, true, 2);
return '<dt>' . htmlspecialchars($split[0]) . '</dt>'
. '<dd>' . htmlspecialchars($split[1] ?? '') . '</dd>';
})(),
$list
);
return '<dl>' . implode(LF, $list) . '</dl>';
}
}
break;
case 'html':
$html = (string)$this->fieldProcessor->preparePlainHtml($recordObj, 'bodytext');
return $this->fieldProcessor->linkToEditForm($html, $recordObj, $request);
default:
$content = (string)$this->fieldProcessor->preparePreviewableHtml($recordObj, 'bodytext');
foreach ($subSchema->getFieldsOfType(TableColumnType::FILE) as $field) {
$fieldName = $field->getName();
if ($recordObj->has($fieldName) && ($image = $recordObj->get($fieldName))) {
$content .= $this->fieldProcessor->prepareFiles($image);
}
}
return $this->fieldProcessor->linkToEditForm($content, $recordObj, $request);
}
return '';
}
/**
* Render a footer for the record
*/
public function renderPageModulePreviewFooter(GridColumnItem $item): string
{
$info = [];
$record = $item->getRecord()->getRawRecord() ?? $item->getRecord();
$schema = $this->tcaSchemaFactory->get($item->getTable());
if ($schema->hasCapability(TcaSchemaCapability::RestrictionStartTime)) {
$info[] = $this->fieldProcessor->prepareFieldWithLabel($record, $schema->getCapability(TcaSchemaCapability::RestrictionStartTime)->getFieldName());
}
if ($schema->hasCapability(TcaSchemaCapability::RestrictionEndTime)) {
$info[] = $this->fieldProcessor->prepareFieldWithLabel($record, $schema->getCapability(TcaSchemaCapability::RestrictionEndTime)->getFieldName());
}
if ($schema->hasCapability(TcaSchemaCapability::RestrictionUserGroup)) {
$info[] = $this->fieldProcessor->prepareFieldWithLabel($record, $schema->getCapability(TcaSchemaCapability::RestrictionUserGroup)->getFieldName());
}
if ($record->getMainType() === 'tt_content') {
foreach (['space_before_class', 'space_after_class'] as $additionalFieldName) {
$itm = $this->fieldProcessor->prepareFieldWithLabel($record, $additionalFieldName);
if ($itm !== null) {
$info[] = $itm;
}
}
}
if ($schema->hasCapability(TcaSchemaCapability::InternalDescription)) {
$item = $this->fieldProcessor->prepareField($record, $schema->getCapability(TcaSchemaCapability::InternalDescription)->getFieldName());
if ($item !== null) {
$info[] = $item;
}
}
$info = array_filter($info);
if ($info === []) {
return '';
}
return implode('<br>', $info);
}
public function wrapPageModulePreview(string $previewHeader, string $previewContent, GridColumnItem $item): string
{
$previewHeader = $previewHeader ? '<div class="element-preview-header">' . $previewHeader . '</div>' : '';
$previewContent = $previewContent ? '<div class="element-preview-content">' . $previewContent . '</div>' : '';
return $previewHeader || $previewContent ? '<div class="element-preview">' . $previewHeader . $previewContent . '</div>' : '';
}
private function translateShortcutRecord(Record $targetRecord, Record $shortcutRecord, string $tableName): RawRecord
{
$targetLanguage = ($targetRecord->getLanguageId() ?? 0);
if ($targetLanguage === 0
|| !$this->tcaSchemaFactory->get($tableName)->isLanguageAware()
|| $targetLanguage === ($shortcutRecord->getLanguageId() ?? 0)
) {
return $shortcutRecord->getRawRecord();
}
// record is localized - fetch the shortcut record translation, if available
$shortcutRecordLocalization = $this->localizationRepository->getRecordTranslation($tableName, $shortcutRecord, $targetLanguage);
return $shortcutRecordLocalization ?? $shortcutRecord->getRawRecord();
}
/**
* Generates a list of selected pages or categories for the menu content types
*
* @param array $record row from pages
*/
private function generateListForMenuContentTypes(array $record, string $contentType): string
{
$table = 'pages';
$field = 'pages';
// get categories instead of pages
if (str_contains($contentType, 'menu_categorized')) {
$table = 'sys_category';
$field = 'selected_categories';
}
if (trim($record[$field] ?? '') === '') {
return '';
}
$content = '';
$uidList = GeneralUtility::intExplode(',', $record[$field], true);
foreach ($uidList as $uid) {
$pageRecord = BackendUtility::getRecord($table, $uid);
if ($pageRecord) {
$title = BackendUtility::getRecordTitle($table, $pageRecord);
$pathToContainingPage = BackendUtility::getRecordPath($pageRecord['pid'], $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW), 0);
$content .= '<li class="list-group-item">' . htmlspecialchars($title) . ' <span class="text-variant">[' . $uid . '] ' . htmlspecialchars($pathToContainingPage) . '</span></li>';
}
}
return $content ? '<ul class="list-group">' . $content . '</ul>' : '';
}
private function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
private function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}
@@ -0,0 +1,99 @@
<?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\Preview;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Domain\RecordInterface;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Scans TCA configuration to detect:
*
* - TCA.$table.types.$typeFromTypeField.previewRenderer
* - TCA.$table.ctrl.previewRenderer
*
* Depending on which one is defined and checking the first, type-specific
* variant first.
*/
#[Autoconfigure(public: true)]
readonly class StandardPreviewRendererResolver
{
public function __construct(
protected TcaSchemaFactory $tcaSchemaFactory
) {}
/**
* @param RecordInterface $record A record from $table which will be previewed - allows returning a different PreviewRenderer based on record attributes
* @throws \UnexpectedValueException
* @throws \RuntimeException
*/
public function resolveRendererFor(RecordInterface $record): PreviewRendererInterface
{
$table = $record->getMainType();
$row = $record->getRawRecord()?->toArray() ?? [];
$schema = $this->tcaSchemaFactory->get($table);
$previewRendererClassName = null;
if ($schema->supportsSubSchema()) {
$tcaTypeOfRow = '';
$subSchemaTypeInformation = $schema->getSubSchemaTypeInformation();
if ($subSchemaTypeInformation->isPointerToForeignFieldInForeignSchema()) {
if ($this->tcaSchemaFactory->has($subSchemaTypeInformation->getForeignSchemaName())) {
// Note: We override the schema here to work on the foreign schema from now on.
$schema = $this->tcaSchemaFactory->get($subSchemaTypeInformation->getForeignSchemaName());
if (isset($row[$subSchemaTypeInformation->getFieldName()]) && $schema->hasField($subSchemaTypeInformation->getForeignFieldName())) {
$foreignRecord = BackendUtility::getRecord($subSchemaTypeInformation->getForeignSchemaName(), $row[$subSchemaTypeInformation->getFieldName()], $subSchemaTypeInformation->getForeignFieldName());
$tcaTypeOfRow = (string)($foreignRecord[$subSchemaTypeInformation->getForeignFieldName()] ?? '');
}
}
} else {
$tcaTypeOfRow = (string)($row[$subSchemaTypeInformation->getFieldName()] ?? '');
}
if ($schema->hasSubSchema($tcaTypeOfRow)) {
// Outdated subschemas may still be present in the database fields, this must not block backend rendering and utilize fallback.
$subSchema = $schema->getSubSchema($record->getRecordType());
if (is_string($subSchema->getRawConfiguration()['previewRenderer'] ?? false) && $subSchema->getRawConfiguration()['previewRenderer'] !== '') {
// A type-specific preview renderer was configured for the TCA type
$previewRendererClassName = $subSchema->getRawConfiguration()['previewRenderer'];
}
}
}
if (!$previewRendererClassName) {
// Table either has no type field or no custom preview renderer was defined for the type.
// Use table's standard renderer if any is defined.
$previewRendererClassName = $schema->getRawConfiguration()['previewRenderer'] ?? null;
}
if (is_string($previewRendererClassName) && $previewRendererClassName !== '') {
if (!is_a($previewRendererClassName, PreviewRendererInterface::class, true)) {
throw new \UnexpectedValueException(
sprintf(
'Class %s must implement %s',
$previewRendererClassName,
PreviewRendererInterface::class
),
1477512798
);
}
return GeneralUtility::makeInstance($previewRendererClassName);
}
throw new \RuntimeException(sprintf('No Preview renderer registered for table %s', $table), 1477520356);
}
}