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,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'];
}
}