Files
cms-backend/Classes/Preview/StandardContentPreviewRenderer.php

334 lines
15 KiB
PHP

<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Backend\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'];
}
}