TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:20 +02:00
commit c7a46689ff
115 changed files with 11736 additions and 0 deletions
@@ -0,0 +1,129 @@
<?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\Fluid\ViewHelpers\Render;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Page\ContentArea;
use TYPO3\CMS\Fluid\Event\ModifyRenderedContentAreaEvent;
use TYPO3Fluid\Fluid\Core\Variables\ScopedVariableProvider;
use TYPO3Fluid\Fluid\Core\Variables\StandardVariableProvider;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
use TYPO3Fluid\Fluid\Core\ViewHelper\InvalidArgumentValueException;
/**
* ViewHelper to render a content area as provided by the page-content processor.
* The most common use case is to render all content elements within a column from a
* backend layout.
*
* ```typoscript
* page = PAGE
* page.10 = PAGEVIEW
* page.10.paths.10 = EXT:my_site_package/Resources/Private/Templates/
* ```
*
* ```html
* <f:render.contentArea contentArea="{content.main}" />
* ```
*
* or:
*
* ```html
* {content.main -> f:render.contentArea()}
* ```
*
* or with markup before and after rendered record by using the "recordAs" argument
* in combination with the `<f:render.record> ViewHelper <https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-render-record>`_:
*
* ```html
* <f:render.contentArea contentArea="{content.main}" recordAs="record">
* before {record.fullType}
* <f:render.record record="{record}" />
* after {record.fullType}
* </f:render.contentArea>
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-render-contentarea
*/
final class ContentAreaViewHelper extends AbstractViewHelper
{
/**
* @var bool use content as-is
*/
protected $escapeOutput = false;
public function __construct(
private readonly EventDispatcherInterface $eventDispatcher,
) {}
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerArgument('contentArea', ContentArea::class, 'A content area from the page-content processor');
$this->registerArgument('recordAs', 'string', 'Name of the variable to store the current record in, if you want to use it in the before/after content.');
}
public function render(): string
{
// use argument and fallback to renderChildren for inline records.
$contentArea = $this->arguments['contentArea'] ?? $this->renderChildren();
if (!$contentArea instanceof ContentArea) {
throw new InvalidArgumentValueException('The "contentArea" argument must be an instance of ' . ContentArea::class, 1770212183);
}
$result = '';
if ($this->arguments['recordAs'] !== null) {
$globalVariableProvider = $this->renderingContext->getVariableProvider();
foreach ($contentArea->getRecords() as $record) {
$localVariableProvider = new StandardVariableProvider([$this->arguments['recordAs'] => $record]);
$scopedVariableProvider = new ScopedVariableProvider($globalVariableProvider, $localVariableProvider);
$this->renderingContext->setVariableProvider($scopedVariableProvider);
$result .= $this->renderChildren();
}
$this->renderingContext->setVariableProvider($globalVariableProvider);
} else {
foreach ($contentArea->getRecords() as $record) {
$result .= $this->renderingContext->getViewHelperInvoker()->invoke(
RecordViewHelper::class,
[
'record' => $record,
],
$this->renderingContext,
);
}
}
$event = $this->eventDispatcher->dispatch(
new ModifyRenderedContentAreaEvent(
renderedContentArea: $result,
contentArea: $contentArea,
request: $this->getRequest(),
),
);
return $event->getRenderedContentArea();
}
private function getRequest(): ServerRequestInterface
{
if (!$this->renderingContext->hasAttribute(ServerRequestInterface::class)) {
throw new \RuntimeException('Required request not found in RenderingContext', 1769183896);
}
return $this->renderingContext->getAttribute(ServerRequestInterface::class);
}
}
@@ -0,0 +1,141 @@
<?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\Fluid\ViewHelpers\Render;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Domain\RecordInterface;
use TYPO3\CMS\Core\TimeTracker\TimeTracker;
use TYPO3\CMS\Core\TypoScript\FrontendTypoScript;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Fluid\Event\ModifyRenderedRecordEvent;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
use TYPO3Fluid\Fluid\Core\ViewHelper\InvalidArgumentValueException;
/**
* ViewHelper to render a record object using its TypoScript definition.
* The most common use case is to render a content element, which is
* available as a record object in a Fluid template.
*
* ```html
* <f:render.record record="{record}" />
* ```
*
* or:
*
* ```html
* {record -> f:render.record()}
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-render-record
*/
final class RecordViewHelper extends AbstractViewHelper
{
/**
* @var bool use content as-is
*/
protected $escapeOutput = false;
public function __construct(
private readonly EventDispatcherInterface $eventDispatcher,
private readonly TimeTracker $timeTracker,
) {}
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerArgument('record', RecordInterface::class, 'The record to be rendered');
}
public function getContentArgumentName(): string
{
return 'record';
}
public function render(): string
{
$record = $this->renderChildren();
if (!$record instanceof RecordInterface) {
throw new InvalidArgumentValueException('The "record" argument must be an instance of ' . RecordInterface::class, 1770215699);
}
$request = $this->getRequest();
$result = $this->renderRecord($record, $request);
$event = $this->eventDispatcher->dispatch(
new ModifyRenderedRecordEvent(
renderedRecord: $result,
record: $record,
request: $request,
),
);
return $event->getRenderedRecord();
}
private function renderRecord(RecordInterface $record, ServerRequestInterface $request): string
{
$table = $record->getMainType();
$data = $record->getRawRecord()?->toArray(true) ?? $record->toArray();
$contentObjectRenderer = GeneralUtility::makeInstance(ContentObjectRenderer::class);
$contentObjectRenderer->setRequest($request);
$parent = $request->getAttribute('currentContentObject');
if ($parent instanceof ContentObjectRenderer) {
$contentObjectRenderer->setParent($parent->data, $parent->currentRecord);
}
$contentObjectRenderer->start($data, $table);
$frontendTypoScript = $request->getAttribute('frontend.typoscript');
if (!$frontendTypoScript instanceof FrontendTypoScript || !$frontendTypoScript->hasSetup()) {
throw new \RuntimeException(
'Full TypoScript setup is not available in the current request. The "f:render.record" ViewHelper'
. ' can only be used in Frontend rendering context.',
1781223613
);
}
$setup = $frontendTypoScript->getSetupArray();
if (!isset($setup[$table])) {
throw new InvalidArgumentValueException(
'No Content Object definition found at TypoScript object path "' . $table . '"',
1769184455
);
}
$timeTracker = $this->timeTracker;
if ($timeTracker->LR) {
$timeTracker->push('/f:render.record/', '<' . $table);
}
$timeTracker->incStackPointer();
$content = $contentObjectRenderer->cObjGetSingle($setup[$table], $setup[$table . '.'] ?? [], $table);
$timeTracker->decStackPointer();
if ($timeTracker->LR) {
$timeTracker->pull($content);
}
return $content;
}
private function getRequest(): ServerRequestInterface
{
if (!$this->renderingContext->hasAttribute(ServerRequestInterface::class)) {
throw new \RuntimeException('Required request not found in RenderingContext', 1769508877);
}
return $this->renderingContext->getAttribute(ServerRequestInterface::class);
}
}
@@ -0,0 +1,200 @@
<?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\Fluid\ViewHelpers\Render;
use TYPO3\CMS\Core\Domain\Exception\RecordPropertyNotFoundException;
use TYPO3\CMS\Core\Domain\RecordFactory;
use TYPO3\CMS\Core\Domain\RecordInterface;
use TYPO3\CMS\Core\Schema\Field\InputFieldType;
use TYPO3\CMS\Core\Schema\Field\TextFieldType;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
use TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface;
use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMap;
use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapFactory;
use TYPO3\CMS\Fluid\ViewHelpers\Format\HtmlViewHelper;
use TYPO3\CMS\Frontend\Page\PageInformation;
use TYPO3Fluid\Fluid\Core\Parser\UnsafeHTML;
use TYPO3Fluid\Fluid\Core\Parser\UnsafeHTMLString;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
use TYPO3Fluid\Fluid\Core\ViewHelper\InvalidArgumentValueException;
/**
* ViewHelper to render content based on records and fields from a TCA schema.
* Handles the processing of both simple and rich text fields. By default,
* accessing a missing field raises an error. Set `optional` to `true` to
* return null instead.
*
* Can also handle extbase models, you still need to provide the field name, not the property name.
*
* ```html
* <f:render.text record="{page}" field="bodytext" />
* {record -> f:render.text(field: 'title')}
* <f:render.text field="subheader">{record}</f:render.text>
* {record -> f:render.text(field: 'subheader', optional: true)}
* ```
*
* @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-render-text
*/
final class TextViewHelper extends AbstractViewHelper
{
/**
* We need to disable escaping for the children, otherwise extbase models are given as string to the viewHelper.
* AbstractDomainObject has a __toString method, fluid executes it before giving use the object.
* This is a deeper issue in Fluid that we cannot easily resolve.
* This ViewHelper escapes the output itself, so we can safely disable escaping for the children and output.
*/
protected $escapeChildren = false;
protected $escapeOutput = false;
public function __construct(
private readonly TcaSchemaFactory $tcaSchema,
private readonly RecordFactory $recordFactory,
private readonly DataMapFactory $dataMapFactory,
) {}
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerArgument('record', PageInformation::class . '|' . RecordInterface::class . '|' . DomainObjectInterface::class, 'A Record API Object or extbase model');
$this->registerArgument('field', 'string', 'The database field that should be rendered (even if extbase model is used).', true);
$this->registerArgument('optional', 'boolean', 'If the provided field does not exist in the record, null will be returned.', false, false);
}
public function getContentArgumentName(): string
{
return 'record';
}
public function validateAdditionalArguments(array $arguments): void
{
// This prevents the default Fluid exception from being thrown for this ViewHelper if it's used
// with arguments that aren't defined in initialArguments(). We do this to make it possible for
// extensions to offer additional functionality by overriding this ViewHelper, which sometimes
// requires adding more (most likely optional) arguments to the ViewHelper's definition.
// Note that this is probably not a long-term solution and might change with future TYPO3 major
// versions. Currently, it has minimal impact to template authors and makes things possible
// for extensions that wouldn't be possible otherwise.
}
public function render(): ?UnsafeHTML
{
$input = $this->renderChildren();
$field = $this->arguments['field'];
if ($input instanceof PageInformation) {
$input = $this->recordFactory->createResolvedRecordFromDatabaseRow('pages', $input->getPageRecord());
}
if (!$input instanceof RecordInterface && !$input instanceof DomainObjectInterface) {
throw new InvalidArgumentValueException(
'The record argument must be an instance of ' . PageInformation::class . ' or ' . RecordInterface::class . ' or ' . DomainObjectInterface::class . ' . Given: ' . get_debug_type($input),
1770539910,
);
}
try {
['table' => $table, 'fullType' => $fullType, 'value' => $value] = $this->extractInformation($input, $field);
} catch (RecordPropertyNotFoundException $exception) {
if ($this->arguments['optional']) {
return null;
}
throw new InvalidArgumentValueException($exception->getMessage(), 1775553111, $exception);
}
if (!is_string($value)) {
throw new InvalidArgumentValueException('The value of the field "' . $table . '.' . $field . '" must be a string. Given: ' . get_debug_type($value), 1770321858);
}
$fieldSchema = $this->tcaSchema->get($fullType)->getField($field);
if ($fieldSchema instanceof InputFieldType) {
return new UnsafeHTMLString(htmlspecialchars($value));
}
if ($fieldSchema instanceof TextFieldType) {
if (!$fieldSchema->isRichText()) {
return new UnsafeHTMLString(nl2br(htmlspecialchars($value)));
}
return new UnsafeHTMLString(
$this->renderingContext->getViewHelperInvoker()->invoke(
HtmlViewHelper::class,
[],
$this->renderingContext,
fn() => $value,
),
);
}
throw new InvalidArgumentValueException('The field "' . $table . '.' . $field . '" is not supported. Given: ' . get_debug_type($fieldSchema), 1770618219);
}
/**
* @return array{table: string, fullType: string, value: mixed}
*/
private function extractInformation(RecordInterface|DomainObjectInterface $input, string $field): array
{
if ($input instanceof RecordInterface) {
return [
'table' => $input->getMainType(),
'fullType' => $input->getFullType(),
'value' => $input->get($field) ?? '',
];
}
$dataMap = $this->dataMapFactory->buildDataMap($input::class);
$recordType = $this->getRecordType($input, $dataMap);
return [
'table' => $dataMap->getTableName(),
'fullType' => $dataMap->getTableName() . ($recordType ? '.' . $recordType : ''),
'value' => $this->getResultingValue($input, $dataMap, $field),
];
}
private function getRecordType(DomainObjectInterface $input, DataMap $dataMap): ?string
{
$recordType = $dataMap->getRecordType();
if ($recordType !== null) {
return $recordType;
}
$recordTypeFieldName = $dataMap->getRecordTypeColumnName();
if ($recordTypeFieldName === null) {
return null;
}
foreach ($input->_getProperties() as $propertyName => $value) {
if ($dataMap->getColumnMap($propertyName)?->columnName === $recordTypeFieldName) {
return $value;
}
}
throw new InvalidArgumentValueException('The record type field "' . $recordTypeFieldName . '" does not exist in the given model ' . $input::class . '.', 1771507212);
}
private function getResultingValue(DomainObjectInterface $input, DataMap $dataMap, string $field): mixed
{
foreach ($input->_getProperties() as $propertyName => $value) {
if ($dataMap->getColumnMap($propertyName)?->columnName === $field) {
return $value ?? '';
}
}
throw new RecordPropertyNotFoundException('Could not find the field "' . $field . '" in the given model ' . $input::class . '.', 1771507213);
}
}