TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:27 +02:00
commit 44c503b2d1
233 changed files with 31556 additions and 0 deletions
@@ -0,0 +1,71 @@
<?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\Frontend\ContentObject;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\Exception\ContentRenderingException;
/**
* Contains an abstract class for all tslib content class implementations.
*/
abstract class AbstractContentObject
{
/**
* Always set via setRequest() by ContentObjectFactory after instantiation
*/
protected ServerRequestInterface $request;
protected ?ContentObjectRenderer $cObj = null;
/**
* Renders the content object.
*
* @param mixed $conf Array of TypoScript properties (marked as "mixed" currently because we don't know what we're receiving)
* @return string
* @throws ContentRenderingException
* @throws \Exception
*/
abstract public function render($conf = []);
public function getContentObjectRenderer(): ContentObjectRenderer
{
return $this->cObj;
}
public function setRequest(ServerRequestInterface $request): void
{
$this->request = $request;
}
public function setContentObjectRenderer(ContentObjectRenderer $cObj): void
{
$this->cObj = $cObj;
// Provide the ContentObjectRenderer to the request as well, for code
// that only passes the request to more underlying layers, like Extbase does.
// Also makes sure the request in a Fluid RenderingContext also has the current
// content object available.
$this->request = $this->request->withAttribute('currentContentObject', $cObj);
}
protected function getPageRepository(): PageRepository
{
return GeneralUtility::makeInstance(PageRepository::class);
}
}
@@ -0,0 +1,52 @@
<?php
/*
* 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\Frontend\ContentObject;
/**
* Contains CASE class object.
*/
class CaseContentObject extends AbstractContentObject
{
/**
* Rendering the cObject, CASE
*
* @param mixed $conf Array of TypoScript properties (marked as "mixed" currently because we don't know what we're receiving)
* @return string Output
*/
public function render($conf = []): string
{
if (!empty($conf['if.']) && !$this->cObj->checkIf($conf['if.'])) {
return '';
}
$setCurrent = $this->cObj->stdWrapValue('setCurrent', $conf);
if ($setCurrent) {
$this->cObj->data[$this->cObj->currentValKey] = $setCurrent;
}
$key = $this->cObj->stdWrapValue('key', $conf, null);
$key = isset($conf[$key]) && (string)$conf[$key] !== '' ? $key : 'default';
// If no "default" property is available, then an empty string is returned
if ($key === 'default' && !isset($conf['default'])) {
$theValue = '';
} else {
$theValue = $this->cObj->cObjGetSingle($conf[$key], $conf[$key . '.'] ?? [], $key);
}
if (isset($conf['stdWrap.'])) {
$theValue = $this->cObj->stdWrap($theValue, $conf['stdWrap.']);
}
return $theValue;
}
}
@@ -0,0 +1,135 @@
<?php
/*
* 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\Frontend\ContentObject;
use Psr\EventDispatcher\EventDispatcherInterface;
use TYPO3\CMS\Core\TimeTracker\TimeTracker;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\Event\ModifyRecordsAfterFetchingContentEvent;
/**
* Contains CONTENT class object.
*/
class ContentContentObject extends AbstractContentObject
{
public function __construct(
private readonly TimeTracker $timeTracker,
private readonly EventDispatcherInterface $eventDispatcher,
) {}
/**
* Rendering the cObject, CONTENT
*
* @param array $conf Array of TypoScript properties
* @return string Output
*/
public function render($conf = [])
{
if (!empty($conf['if.']) && !$this->cObj->checkIf($conf['if.'])) {
return '';
}
$theValue = '';
$conf['table'] = trim((string)$this->cObj->stdWrapValue('table', $conf));
$conf['select.'] = !empty($conf['select.']) ? $conf['select.'] : [];
$renderObjName = ($conf['renderObj'] ?? false) ? $conf['renderObj'] : '<' . $conf['table'];
$renderObjKey = ($conf['renderObj'] ?? false) ? 'renderObj' : '';
$renderObjConf = $conf['renderObj.'] ?? [];
$slide = (int)$this->cObj->stdWrapValue('slide', $conf);
if (!$slide) {
$slide = 0;
}
$slideCollect = (int)$this->cObj->stdWrapValue('collect', $conf['slide.'] ?? []);
if (!$slideCollect) {
$slideCollect = 0;
}
$slideCollectReverse = (bool)$this->cObj->stdWrapValue('collectReverse', $conf['slide.'] ?? []);
$slideCollectFuzzy = (bool)$this->cObj->stdWrapValue('collectFuzzy', $conf['slide.'] ?? []);
if (!$slideCollect) {
$slideCollectFuzzy = true;
}
$again = false;
$tmpValue = '';
do {
$cobjValue = '';
$modifyRecordsEvent = $this->eventDispatcher->dispatch(
new ModifyRecordsAfterFetchingContentEvent(
$this->cObj->getRecords($conf['table'], $conf['select.']),
$theValue,
$slide,
$slideCollect,
$slideCollectReverse,
$slideCollectFuzzy,
$conf
)
);
$records = $modifyRecordsEvent->getRecords();
$theValue = $modifyRecordsEvent->getFinalContent();
$slide = $modifyRecordsEvent->getSlide();
$slideCollect = $modifyRecordsEvent->getSlideCollect();
$slideCollectReverse = $modifyRecordsEvent->getSlideCollectReverse();
$slideCollectFuzzy = $modifyRecordsEvent->getSlideCollectFuzzy();
$conf = $modifyRecordsEvent->getConfiguration();
if ($records !== []) {
$this->timeTracker->setTSlogMessage('NUMROWS: ' . count($records));
$cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class);
$cObj->setParent($this->cObj->data, $this->cObj->currentRecord);
foreach ($records as $row) {
$this->cObj->lastChanged($row['tstamp'] ?? 0);
$cObj->setRequest($this->request);
$cObj->start($row, $conf['table']);
$tmpValue = $cObj->cObjGetSingle($renderObjName, $renderObjConf, $renderObjKey);
$cobjValue .= $tmpValue;
}
}
if ($slideCollectReverse) {
$theValue = $cobjValue . $theValue;
} else {
$theValue .= $cobjValue;
}
if ($slideCollect > 0) {
$slideCollect--;
}
if ($slide) {
if ($slide > 0) {
$slide--;
}
$conf['select.']['pidInList'] = $this->cObj->getSlidePids(
$conf['select.']['pidInList'] ?? '',
$conf['select.']['pidInList.'] ?? [],
);
if (isset($conf['select.']['pidInList.'])) {
unset($conf['select.']['pidInList.']);
}
$again = (string)$conf['select.']['pidInList'] !== '';
}
} while ($again && $slide && ((string)$tmpValue === '' && $slideCollectFuzzy || $slideCollect));
$wrap = $this->cObj->stdWrapValue('wrap', $conf);
if ($wrap) {
$theValue = $this->cObj->wrap($theValue, $wrap);
}
if (isset($conf['stdWrap.'])) {
$theValue = $this->cObj->stdWrap($theValue, $conf['stdWrap.']);
}
return $theValue;
}
}
@@ -0,0 +1,101 @@
<?php
/*
* 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\Frontend\ContentObject;
use Psr\Container\ContainerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\DataProcessing\DataProcessorRegistry;
/**
* A class that contains methods that can be used to use the dataProcessing functionality
*/
#[Autoconfigure(public: true)]
readonly class ContentDataProcessor
{
public function __construct(
private ContainerInterface $container,
private DataProcessorRegistry $dataProcessorRegistry,
) {}
/**
* Check for the availability of processors, defined in TypoScript, and use them for data processing
*
* @param array $configuration Configuration array
* @param array $variables the variables to be processed
* @return array the processed data and variables as key/value store
* @throws \UnexpectedValueException If a processor class does not exist
*/
public function process(ContentObjectRenderer $cObject, array $configuration, array $variables)
{
if (
!empty($configuration['dataProcessing.'])
&& is_array($configuration['dataProcessing.'])
) {
$processors = $configuration['dataProcessing.'];
$processorKeys = ArrayUtility::filterAndSortByNumericKeys($processors);
foreach ($processorKeys as $key) {
$dataProcessor = $this->dataProcessorRegistry->getDataProcessor($processors[$key])
?? $this->getDataProcessor($processors[$key]);
$processorConfiguration = $processors[$key . '.'] ?? [];
$variables = $dataProcessor->process(
$cObject,
$configuration,
$processorConfiguration,
$variables
);
}
}
return $variables;
}
private function getDataProcessor(string $serviceName): DataProcessorInterface
{
if (!$this->container->has($serviceName)) {
// assume serviceName is the class name if it is not available in the container
return $this->instantiateDataProcessor($serviceName);
}
$dataProcessor = $this->container->get($serviceName);
if (!$dataProcessor instanceof DataProcessorInterface) {
throw new \UnexpectedValueException(
'Processor with service name "' . $serviceName . '" '
. 'must implement interface "' . DataProcessorInterface::class . '"',
1635927108
);
}
return $dataProcessor;
}
private function instantiateDataProcessor(string $className): DataProcessorInterface
{
if (!class_exists($className)) {
throw new \UnexpectedValueException('Processor class or service name "' . $className . '" does not exist!', 1427455378);
}
if (!in_array(DataProcessorInterface::class, class_implements($className) ?: [], true)) {
throw new \UnexpectedValueException(
'Processor with class name "' . $className . '" '
. 'must implement interface "' . DataProcessorInterface::class . '"',
1427455377
);
}
return GeneralUtility::makeInstance($className);
}
}
@@ -0,0 +1,61 @@
<?php
/*
* 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\Frontend\ContentObject;
use Psr\Log\LogLevel;
use TYPO3\CMS\Core\TimeTracker\TimeTracker;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Contains COA class object.
*/
class ContentObjectArrayContentObject extends AbstractContentObject
{
/**
* Rendering the cObject, COBJ_ARRAY / COA
*
* @param array $conf Array of TypoScript properties
* @return string Output
*/
public function render($conf = [])
{
if (empty($conf)) {
$this->getTimeTracker()->setTSlogMessage('No elements in this content object array (COBJ_ARRAY, COA).', LogLevel::WARNING);
return '';
}
if (!empty($conf['if.']) && !$this->cObj->checkIf($conf['if.'])) {
return '';
}
$content = $this->cObj->cObjGet($conf);
$wrap = $this->cObj->stdWrapValue('wrap', $conf);
if ($wrap) {
$content = $this->cObj->wrap($content, $wrap);
}
if (isset($conf['stdWrap.'])) {
$content = $this->cObj->stdWrap($content, $conf['stdWrap.']);
}
return $content;
}
/**
* @return TimeTracker
*/
protected function getTimeTracker()
{
return GeneralUtility::makeInstance(TimeTracker::class);
}
}
@@ -0,0 +1,55 @@
<?php
/*
* 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\Frontend\ContentObject;
use Psr\Log\LogLevel;
use TYPO3\CMS\Core\TimeTracker\TimeTracker;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* Contains COA_INT class object.
*/
class ContentObjectArrayInternalContentObject extends AbstractContentObject
{
/**
* Rendering the cObject, COA_INT
*
* @param array $conf Array of TypoScript properties
* @return string Output
*/
public function render($conf = [])
{
if (empty($conf)) {
$this->getTimeTracker()->setTSlogMessage('No elements in this content object array (COA_INT).', LogLevel::WARNING);
return '';
}
$substKey = 'INT_SCRIPT.' . md5(StringUtility::getUniqueId());
$pageParts = $this->request->getAttribute('frontend.page.parts');
$pageParts->addNotCachedContentElement([
'substKey' => $substKey,
'conf' => $conf,
'cObjData' => serialize($this->cObj->getState()),
'type' => 'COA',
]);
return '<!--' . $substKey . '-->';
}
protected function getTimeTracker(): TimeTracker
{
return GeneralUtility::makeInstance(TimeTracker::class);
}
}
@@ -0,0 +1,48 @@
<?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\Frontend\ContentObject;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Frontend\ContentObject\Exception\ContentRenderingException;
/**
* Registry to create cObjects (e.g. TEXT)
* @internal
*/
class ContentObjectFactory
{
public function __construct(private ContainerInterface $contentObjectLocator) {}
public function getContentObject(string $name, ServerRequestInterface $request, ContentObjectRenderer $contentObjectRenderer): ?AbstractContentObject
{
if (!$this->contentObjectLocator->has($name)) {
return null;
}
$contentObject = $this->contentObjectLocator->get($name);
if (!($contentObject instanceof AbstractContentObject)) {
throw new ContentRenderingException(sprintf('Registered content object class name "%s" must be an instance of AbstractContentObject, but is not!', get_class($contentObject)), 1422564295);
}
$contentObject->setRequest($request);
$contentObject->setContentObjectRenderer($contentObjectRenderer);
return $contentObject;
}
}
@@ -0,0 +1,34 @@
<?php
/*
* 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\Frontend\ContentObject;
use TYPO3\CMS\Core\Resource\File;
/**
* Interface for hooks to fetch the public URL of files
*/
interface ContentObjectGetPublicUrlForFileHookInterface
{
/**
* Post-processes a public URL.
*
* @param \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer $parent The current content object (context)
* @param array $configuration TypoScript configuration
* @param File $file The file object to be used
* @param string $pubicUrl Reference to the public URL
*/
public function postProcess(ContentObjectRenderer $parent, array $configuration, File $file, &$pubicUrl);
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,39 @@
<?php
/*
* 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\Frontend\ContentObject;
/**
* Interface for data processor classes processing data from
* ContentObjectRenderer, used e.g. with the FLUIDTEMPLATE content object
*/
interface DataProcessorInterface
{
/**
* Process content object data
*
* @param ContentObjectRenderer $cObj The data of the content element or page
* @param array $contentObjectConfiguration The configuration of Content Object
* @param array $processorConfiguration The configuration of this processor
* @param array $processedData Key/value store of processed data (e.g. to be passed to a Fluid View)
* @return array the processed data as key/value store
*/
public function process(
ContentObjectRenderer $cObj,
array $contentObjectConfiguration,
array $processorConfiguration,
array $processedData
);
}
@@ -0,0 +1,35 @@
<?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\Frontend\ContentObject\Event;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* Listeners are able to modify the initialized ContentObjectRenderer instance
*/
final readonly class AfterContentObjectRendererInitializedEvent
{
public function __construct(
private ContentObjectRenderer $contentObjectRenderer
) {}
public function getContentObjectRenderer(): ContentObjectRenderer
{
return $this->contentObjectRenderer;
}
}
@@ -0,0 +1,58 @@
<?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\Frontend\ContentObject\Event;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* Listeners are able to modify the resolved ContentObjectRenderer->getData() result
*/
final class AfterGetDataResolvedEvent
{
public function __construct(
private readonly string $parameterString,
private readonly array $alternativeFieldArray,
private mixed $result,
private readonly ContentObjectRenderer $contentObjectRenderer
) {}
public function getResult(): mixed
{
return $this->result;
}
public function setResult(mixed $result): void
{
$this->result = $result;
}
public function getParameterString(): string
{
return $this->parameterString;
}
public function getAlternativeFieldArray(): array
{
return $this->alternativeFieldArray;
}
public function getContentObjectRenderer(): ContentObjectRenderer
{
return $this->contentObjectRenderer;
}
}
@@ -0,0 +1,54 @@
<?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\Frontend\ContentObject\Event;
use TYPO3\CMS\Core\Imaging\ImageResource;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\FileReference;
/**
* Listeners are able to modify the resolved ContentObjectRenderer->getImgResource() result
*/
final class AfterImageResourceResolvedEvent
{
public function __construct(
private readonly string|File|FileReference $file,
private readonly array $fileArray,
private ?ImageResource $imageResource
) {}
public function getFile(): string|File|FileReference
{
return $this->file;
}
public function getFileArray(): array
{
return $this->fileArray;
}
public function getImageResource(): ?ImageResource
{
return $this->imageResource;
}
public function setImageResource(?ImageResource $imageResource): void
{
$this->imageResource = $imageResource;
}
}
@@ -0,0 +1,23 @@
<?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\Frontend\ContentObject\Event;
/**
* Event is called after the content has been modified by the rest of the stdWrap functions
*/
final class AfterStdWrapFunctionsExecutedEvent extends EnhanceStdWrapEvent {}
@@ -0,0 +1,24 @@
<?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\Frontend\ContentObject\Event;
/**
* Event is dispatched after stdWrap functions have been initialized,
* but before any content gets modified or replaced.
*/
final class AfterStdWrapFunctionsInitializedEvent extends EnhanceStdWrapEvent {}
@@ -0,0 +1,89 @@
<?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\Frontend\ContentObject\Event;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* Listeners to this Event are able to modify the final stdWrap content
* and corresponding cache tags, before being stored in cache.
*
* Additionally, listeners are also able to change the cache key to be used
* as well as the lifetime. Therefore, the whole configuration is available.
*/
final class BeforeStdWrapContentStoredInCacheEvent
{
public function __construct(
private ?string $content,
private array $tags,
private string $key,
private ?int $lifetime,
private readonly array $configuration,
private readonly ContentObjectRenderer $contentObjectRenderer
) {}
public function getContent(): ?string
{
return $this->content;
}
public function setContent(string $content): void
{
$this->content = $content;
}
public function getTags(): array
{
return $this->tags;
}
public function setTags(array $tags): void
{
$this->tags = $tags;
}
public function getKey(): string
{
return $this->key;
}
public function setKey(string $key): void
{
$this->key = $key;
}
public function getLifetime(): ?int
{
return $this->lifetime;
}
public function setLifetime(?int $lifetime): void
{
$this->lifetime = $lifetime;
}
public function getConfiguration(): array
{
return $this->configuration;
}
public function getContentObjectRenderer(): ContentObjectRenderer
{
return $this->contentObjectRenderer;
}
}
@@ -0,0 +1,23 @@
<?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\Frontend\ContentObject\Event;
/**
* Event is called directly after the recursive stdWrap function call but still before the content gets modified
*/
final class BeforeStdWrapFunctionsExecutedEvent extends EnhanceStdWrapEvent {}
@@ -0,0 +1,23 @@
<?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\Frontend\ContentObject\Event;
/**
* Event is dispatched before any stdWrap function is initialized / called
*/
final class BeforeStdWrapFunctionsInitializedEvent extends EnhanceStdWrapEvent {}
@@ -0,0 +1,62 @@
<?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\Frontend\ContentObject\Event;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* Listeners to this Event are able to modify the stdWrap processing, enhancing the functionality and
* manipulating the final result / content. This is the parent Event, which allows the corresponding
* listeners to be called on each step, see child Events:
*
* @see BeforeStdWrapFunctionsInitializedEvent
* @see AfterStdWrapFunctionsInitializedEvent
* @see BeforeStdWrapFunctionsExecutedEvent
* @see AfterStdWrapFunctionsExecutedEvent
*
* Note: The class is declared abstract to prevent it from being dispatched directly.
* Only child classes are to be dispatched to prevent duplicate executions.
*/
abstract class EnhanceStdWrapEvent
{
public function __construct(
private ?string $content,
private readonly array $configuration,
private readonly ContentObjectRenderer $contentObjectRenderer
) {}
public function getContent(): ?string
{
return $this->content;
}
public function setContent(string $content): void
{
$this->content = $content;
}
public function getConfiguration(): array
{
return $this->configuration;
}
public function getContentObjectRenderer(): ContentObjectRenderer
{
return $this->contentObjectRenderer;
}
}
@@ -0,0 +1,64 @@
<?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\Frontend\ContentObject\Event;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* Listeners are able to enrich the final source collection result
*/
final class ModifyImageSourceCollectionEvent
{
public function __construct(
private string $sourceCollection,
private readonly string $fullSourceCollection,
private readonly array $sourceConfiguration,
private readonly array $sourceRenderConfiguration,
private readonly ContentObjectRenderer $contentObjectRenderer
) {}
public function setSourceCollection(string $sourceCollection): void
{
$this->sourceCollection = $sourceCollection;
}
public function getSourceCollection(): string
{
return $this->sourceCollection;
}
public function getFullSourceCollection(): string
{
return $this->fullSourceCollection;
}
public function getSourceConfiguration(): array
{
return $this->sourceConfiguration;
}
public function getSourceRenderConfiguration(): array
{
return $this->sourceRenderConfiguration;
}
public function getContentObjectRenderer(): ContentObjectRenderer
{
return $this->contentObjectRenderer;
}
}
@@ -0,0 +1,110 @@
<?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\Frontend\ContentObject\Event;
/**
* Event which is fired after ContentContentObject has pulled records from database.
*
* Therefore, allows listeners to completely manipulate the fetched
* records, prior to being further processed by the content object.
*
* Additionally, the event also allows to manipulate the configuration
* and options, such as the "value" or "slide".
*/
final class ModifyRecordsAfterFetchingContentEvent
{
public function __construct(
private array $records,
private string $finalContent,
private int $slide,
private int $slideCollect,
private bool $slideCollectReverse,
private bool $slideCollectFuzzy,
private array $configuration,
) {}
public function getRecords(): array
{
return $this->records;
}
public function setRecords(array $records): void
{
$this->records = $records;
}
public function getFinalContent(): string
{
return $this->finalContent;
}
public function setFinalContent(string $finalContent): void
{
$this->finalContent = $finalContent;
}
public function getSlide(): int
{
return $this->slide;
}
public function setSlide(int $slide): void
{
$this->slide = $slide;
}
public function getSlideCollect(): int
{
return $this->slideCollect;
}
public function setSlideCollect(int $slideCollect): void
{
$this->slideCollect = $slideCollect;
}
public function getSlideCollectReverse(): bool
{
return $this->slideCollectReverse;
}
public function setSlideCollectReverse(bool $slideCollectReverse): void
{
$this->slideCollectReverse = $slideCollectReverse;
}
public function getSlideCollectFuzzy(): bool
{
return $this->slideCollectFuzzy;
}
public function setSlideCollectFuzzy(bool $slideCollectFuzzy): void
{
$this->slideCollectFuzzy = $slideCollectFuzzy;
}
public function getConfiguration(): array
{
return $this->configuration;
}
public function setConfiguration(array $configuration): void
{
$this->configuration = $configuration;
}
}
@@ -0,0 +1,23 @@
<?php
/*
* 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\Frontend\ContentObject\Exception;
use TYPO3\CMS\Core\Error\Exception;
/**
* @internal this is a concrete TYPO3 implementation and solely used for EXT:frontend and not part of TYPO3's Core API.
*/
class ContentRenderingException extends Exception {}
@@ -0,0 +1,39 @@
<?php
/*
* 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\Frontend\ContentObject\Exception;
use TYPO3\CMS\Frontend\ContentObject\AbstractContentObject;
/**
* Interface ExceptionHandlerInterface
*/
interface ExceptionHandlerInterface
{
/**
* Handles exceptions thrown during rendering of content objects
* The handler can decide whether to re-throw the exception or
* return a nice error message for production context.
*
* @param array $contentObjectConfiguration
* @return string
*/
public function handle(\Exception $exception, ?AbstractContentObject $contentObject = null, $contentObjectConfiguration = []);
/**
* Used to pass the TypoScript configuration to the exception handler
*/
public function setConfiguration(array $configuration): void;
}
@@ -0,0 +1,86 @@
<?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\Frontend\ContentObject\Exception;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Core\RequestId;
use TYPO3\CMS\Core\Crypto\Random;
use TYPO3\CMS\Core\Error\AbstractExceptionHandler;
use TYPO3\CMS\Core\Http\ImmediateResponseException;
use TYPO3\CMS\Frontend\ContentObject\AbstractContentObject;
/**
* Exception handler class for content object rendering
* @internal this is a concrete TYPO3 implementation and solely used for EXT:frontend and not part of TYPO3's Core API.
*/
#[Autoconfigure(public: true, shared: false)]
class ProductionExceptionHandler implements ExceptionHandlerInterface
{
protected array $configuration = [];
public function __construct(
protected Context $context,
protected Random $random,
protected LoggerInterface $logger,
protected RequestId $requestId
) {}
public function setConfiguration(array $configuration): void
{
$this->configuration = $configuration;
}
/**
* Handles exceptions thrown during rendering of content objects
* The handler can decide whether to re-throw the exception or
* return a nice error message for production context.
*
* @param array $contentObjectConfiguration
* @throws \Exception
*/
public function handle(\Exception $exception, ?AbstractContentObject $contentObject = null, $contentObjectConfiguration = []): string
{
// ImmediateResponseException (and the derived PropagateResponseException) should work similar to
// exit / die and must therefore not be handled by this ExceptionHandler.
if ($exception instanceof ImmediateResponseException) {
throw $exception;
}
if (!empty($this->configuration['ignoreCodes.'])
&& in_array($exception->getCode(), array_map('intval', $this->configuration['ignoreCodes.']), true)
) {
throw $exception;
}
$errorMessage = $this->configuration['errorMessage'] ?? 'Oops, an error occurred! Request: {requestId}';
// $code and it's placeholder %s for b/w compatibility
$code = $this->context->getAspect('date')->getDateTime()->format('YmdHis') . $this->random->generateRandomHexString(8);
$errorMessage = str_replace('%s', '{code}', $errorMessage);
// Log exception except HMAC validation exceptions caused by potentially forged requests
if (!in_array($exception->getCode(), AbstractExceptionHandler::IGNORED_HMAC_EXCEPTION_CODES, true)) {
$this->logger->alert($errorMessage, ['exception' => $exception, 'code' => $code, 'requestId' => $this->requestId]);
}
// Return interpolated error message
return str_replace(['{code}', '{requestId}'], [$code, (string)$this->requestId], $errorMessage);
}
}
@@ -0,0 +1,31 @@
<?php
/*
* 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\Frontend\ContentObject;
use TYPO3\CMS\Core\Resource\File;
/**
* Interface for classes which hook into \TYPO3\CMS\Frontend\ContentObject and do additional getImgResource processing
*/
interface FileLinkHookInterface
{
/**
* Finds alternative previewImage for given File.
*
* @return File
*/
public function getPreviewImage(File $file);
}
@@ -0,0 +1,185 @@
<?php
/*
* 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\Frontend\ContentObject;
use TYPO3\CMS\Core\TypoScript\TypoScriptService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Frontend\Resource\FileCollector;
/**
* Contains FILES content object
*/
class FilesContentObject extends AbstractContentObject
{
/**
* Rendering the cObject FILES
*
* @param array $conf Array of TypoScript properties
* @return string Output
*/
public function render($conf = [])
{
if (!empty($conf['if.']) && !$this->cObj->checkIf($conf['if.'])) {
return '';
}
$register = $this->request->getAttribute('frontend.register.stack')->current();
// Store the original "currentFile" within a variable so it can be re-applied later-on
$originalFileInContentObject = $this->cObj->getCurrentFile();
$fileCollector = $this->findAndSortFiles($conf);
$fileObjects = $fileCollector->getFiles();
$availableFileObjectCount = count($fileObjects);
// optionSplit applied to conf to allow different settings per file
$splitConf = GeneralUtility::makeInstance(TypoScriptService::class)
->explodeConfigurationForOptionSplit($conf, $availableFileObjectCount);
$start = (int)$this->cObj->stdWrapValue('begin', $conf, 0);
$start = MathUtility::forceIntegerInRange($start, 0, $availableFileObjectCount);
$limit = (int)$this->cObj->stdWrapValue('maxItems', $conf, $availableFileObjectCount);
$end = MathUtility::forceIntegerInRange($start + $limit, $start, $availableFileObjectCount);
$register->set('FILES_COUNT', min($limit, $availableFileObjectCount));
$fileObjectCounter = 0;
$keys = array_keys($fileObjects);
$content = '';
for ($i = $start; $i < $end; $i++) {
$key = $keys[$i];
$fileObject = $fileObjects[$key];
$register->set('FILE_NUM_CURRENT', $fileObjectCounter);
$this->cObj->setCurrentFile($fileObject);
$content .= $this->cObj->cObjGetSingle($splitConf[$key]['renderObj'], $splitConf[$key]['renderObj.'], 'renderObj');
$fileObjectCounter++;
}
// Reset current file within cObj to the original file after rendering output of FILES
// so e.g. stdWrap is not working on the last current file applied, thus avoiding side-effects
$this->cObj->setCurrentFile($originalFileInContentObject);
return $this->cObj->stdWrap($content, $conf['stdWrap.'] ?? []);
}
/**
* Function to check for references, collections, folders and
* accumulates into one etc.
*/
protected function findAndSortFiles(array $conf): FileCollector
{
$fileCollector = $this->getFileCollector();
// Getting the files
if ((isset($conf['references']) && $conf['references']) || (isset($conf['references.']) && $conf['references.'])) {
/*
The TypoScript could look like this:
# all items related to the page.media field:
references {
table = pages
uid.data = page:uid
fieldName = media
}
# or: sys_file_references with uid 27:
references = 27
*/
$referencesUidList = (string)$this->cObj->stdWrapValue('references', $conf);
$referencesUids = GeneralUtility::intExplode(',', $referencesUidList, true);
$fileCollector->addFileReferences($referencesUids);
if (!empty($conf['references.'])) {
$this->addFileReferences($conf, (array)$this->cObj->data, $fileCollector);
}
}
if ((isset($conf['files']) && $conf['files']) || (isset($conf['files.']) && $conf['files.'])) {
/*
The TypoScript could look like this:
# with sys_file UIDs:
files = 12,14,15# using stdWrap:
files.field = some_field
*/
$fileUids = GeneralUtility::intExplode(',', (string)$this->cObj->stdWrapValue('files', $conf), true);
$fileCollector->addFiles($fileUids);
}
if ((isset($conf['collections']) && $conf['collections']) || (isset($conf['collections.']) && $conf['collections.'])) {
$collectionUids = GeneralUtility::intExplode(',', (string)$this->cObj->stdWrapValue('collections', $conf), true);
$fileCollector->addFilesFromFileCollections($collectionUids);
}
if ((isset($conf['folders']) && $conf['folders']) || (isset($conf['folders.']) && $conf['folders.'])) {
$folderIdentifiers = GeneralUtility::trimExplode(',', (string)$this->cObj->stdWrapValue('folders', $conf));
$fileCollector->addFilesFromFolders($folderIdentifiers, !empty($conf['folders.']['recursive']));
}
// Enable sorting for multiple fileObjects
$sortingProperty = (string)$this->cObj->stdWrapValue('sorting', $conf);
if ($sortingProperty !== '') {
$sortingDirection = $this->cObj->stdWrapValue('direction', $conf['sorting.'] ?? []);
$fileCollector->sort($sortingProperty, $sortingDirection);
}
return $fileCollector;
}
/**
* Handles and resolves file references.
*
* @param array $configuration TypoScript configuration
* @param array $element The parent element referencing to files
*/
protected function addFileReferences(array $configuration, array $element, FileCollector $fileCollector): void
{
// It's important that this always stays "fieldName" and not be renamed to "field" as it would otherwise collide with the stdWrap key of that name
$referencesFieldName = $this->cObj->stdWrapValue('fieldName', $configuration['references.'] ?? []);
// If no reference fieldName is set, there's nothing to do
if (empty($referencesFieldName)) {
return;
}
$currentId = !empty($element['uid']) ? $element['uid'] : 0;
$tableName = $this->cObj->getCurrentTable();
// Fetch the references of the default element
$referencesForeignTable = (string)$this->cObj->stdWrapValue('table', $configuration['references.'], $tableName);
$referencesForeignUid = (int)$this->cObj->stdWrapValue('uid', $configuration['references.'], $currentId);
$pageRepository = $this->getPageRepository();
// Fetch element if definition has been modified via TypoScript
if (
($referencesForeignTable !== '' && $referencesForeignTable !== $tableName)
|| ($referencesForeignUid !== 0 && $referencesForeignUid !== $currentId)
) {
$element = $pageRepository->getRawRecord($referencesForeignTable, $referencesForeignUid);
// Do versionOL() again and unset move pointers
$pageRepository->versionOL($referencesForeignTable, $element, true);
if (is_array($element)) {
$element = $pageRepository->getLanguageOverlay($referencesForeignTable, $element);
}
}
if (is_array($element)) {
$fileCollector->addFilesFromRelation($referencesForeignTable ?: $tableName, $referencesFieldName, $element);
}
}
protected function getFileCollector(): FileCollector
{
return GeneralUtility::makeInstance(FileCollector::class);
}
}
@@ -0,0 +1,273 @@
<?php
/*
* 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\Frontend\ContentObject;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Information\Typo3Information;
use TYPO3\CMS\Core\TypoScript\TypoScriptService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\View\ViewFactoryData;
use TYPO3\CMS\Core\View\ViewFactoryInterface;
use TYPO3\CMS\Extbase\Configuration\ConfigurationManager;
use TYPO3\CMS\Extbase\Mvc\Web\RequestBuilder;
use TYPO3\CMS\Fluid\View\FluidViewAdapter;
use TYPO3\CMS\Frontend\ContentObject\Exception\ContentRenderingException;
use TYPO3Fluid\Fluid\View\Exception\InvalidLayoutException;
use TYPO3Fluid\Fluid\View\Exception\InvalidPartialException;
use TYPO3Fluid\Fluid\View\Exception\InvalidTemplateResourceException;
/**
* Contains FLUIDTEMPLATE class object
*/
class FluidTemplateContentObject extends AbstractContentObject
{
public function __construct(
private readonly ContentDataProcessor $contentDataProcessor,
private readonly TypoScriptService $typoScriptService,
private readonly ViewFactoryInterface $viewFactory,
) {}
/**
* Rendering the cObject, FLUIDTEMPLATE
*
* Configuration properties:
* - file string+stdWrap The FLUID template file
* - layoutRootPaths array of filepath+stdWrap Root paths to layouts (fallback)
* - partialRootPaths array of filepath+stdWrap Root paths to partials (fallback)
* - variable array of cObjects, the keys are the variable names in fluid
* - dataProcessing array of data processors which are classes to manipulate $data
* - extbase.pluginName
* - extbase.controllerExtensionName
* - extbase.controllerName
* - extbase.controllerActionName
*
* Example:
* 10 = FLUIDTEMPLATE
* 10.templateName = MyTemplate
* 10.templateRootPaths.10 = EXT:site_configuration/Resources/Private/Templates/
* 10.partialRootPaths.10 = EXT:site_configuration/Resources/Private/Partials/
* 10.layoutRootPaths.10 = EXT:site_configuration/Resources/Private/Layouts/
* 10.variables {
* mylabel = TEXT
* mylabel.value = Label from TypoScript coming
* }
*
* @param mixed $conf Array of TypoScript properties (marked as "mixed" currently because we don't know what we're receiving)
*/
public function render($conf = []): string
{
if (!is_array($conf)) {
$conf = [];
}
$request = $this->buildExtbaseRequestIfNeeded($this->request, $conf);
$templateFilename = '';
$templateSource = null;
if ((!empty($conf['templateName']) || !empty($conf['templateName.']))
&& !empty($conf['templateRootPaths.']) && is_array($conf['templateRootPaths.'])
) {
// This is the most preferred way to render fluid: set up paths, then call render('My/Template')
$viewFactoryData = new ViewFactoryData(
templateRootPaths: $this->applyStandardWrapToFluidPaths($conf['templateRootPaths.']),
partialRootPaths: $this->getPartialRootPaths($conf),
layoutRootPaths: $this->getLayoutRootPaths($conf),
request: $request,
format: $this->cObj->stdWrapValue('format', $conf, null),
);
$templateFilename = $this->cObj->stdWrapValue('templateName', $conf);
} elseif (!empty($conf['template']) && !empty($conf['template.'])) {
// Fetch the Fluid template by template cObject "template = TEXT, template.value = <f:foo ..."
$templateSource = $this->cObj->cObjGetSingle($conf['template'], $conf['template.'], 'template');
if ($templateSource === '') {
throw new ContentRenderingException(
'Could not find template source for ' . $conf['template'],
1437420865
);
}
$viewFactoryData = new ViewFactoryData(
partialRootPaths: $this->getPartialRootPaths($conf),
layoutRootPaths: $this->getLayoutRootPaths($conf),
request: $request,
format: $this->cObj->stdWrapValue('format', $conf, null),
);
} else {
// Fetch the Fluid template by file stdWrap "file = EXT:myExt/.../Foo.html"
$file = (string)$this->cObj->stdWrapValue('file', $conf);
// Get the absolute file name
$templatePathAndFilename = GeneralUtility::getFileAbsFileName($file);
$viewFactoryData = new ViewFactoryData(
partialRootPaths: $this->getPartialRootPaths($conf),
layoutRootPaths: $this->getLayoutRootPaths($conf),
templatePathAndFilename: $templatePathAndFilename,
request: $request,
format: $this->cObj->stdWrapValue('format', $conf, null),
);
}
$view = $this->viewFactory->create($viewFactoryData);
if (!$view instanceof FluidViewAdapter) {
throw new ContentRenderingException(
'The FLUIDTEMPLATE content object only works with FluidViewAdapter view. Use a different'
. ' content object to render some other view',
1724680477
);
}
if ($templateSource) {
$view->getRenderingContext()->getTemplatePaths()->setTemplateSource($templateSource);
}
if (isset($conf['settings.'])) {
$settings = $this->typoScriptService->convertTypoScriptArrayToPlainArray($conf['settings.']);
$view->assign('settings', $settings);
}
$variables = $this->getContentObjectVariables($conf);
$variables = $this->contentDataProcessor->process($this->cObj, $conf, $variables);
$view->assignMultiple($variables);
try {
// View needs to be rendered before the following asset rendering because it
// sets the template (paths) internally.
$content = $view->render($templateFilename);
} catch (InvalidTemplateResourceException $e) {
// Only add a FLUIDTEMPLATE specific message in case the exception has been thrown for the given template
if ($e instanceof InvalidPartialException || $e instanceof InvalidLayoutException || $templateFilename === '' || $e->templateName !== 'Default/' . $templateFilename) {
throw $e;
}
throw new InvalidTemplateResourceException(
sprintf(
'FLUIDTEMPLATE TypoScript object: Failed to resolve a template file for templateName "%s". See also: %s. The following paths were checked: "%s"',
$templateFilename,
Typo3Information::getDocsLink('t3tsref:cobj-template'),
implode('", "', $e->evaluatedTemplatePaths),
),
1772572794,
$e,
$e->templateName,
$e->evaluatedTemplatePaths,
);
}
if (isset($conf['stdWrap.'])) {
return $this->cObj->stdWrap($content, $conf['stdWrap.']);
}
return $content;
}
protected function getLayoutRootPaths(array $conf): ?array
{
$layoutPaths = [];
$layoutRootPath = (string)$this->cObj->stdWrapValue('layoutRootPath', $conf);
if ($layoutRootPath !== '') {
$layoutPaths[] = GeneralUtility::getFileAbsFileName($layoutRootPath);
}
if (isset($conf['layoutRootPaths.'])) {
$layoutPaths = array_replace($layoutPaths, $this->applyStandardWrapToFluidPaths($conf['layoutRootPaths.']));
}
return !empty($layoutPaths) ? $layoutPaths : null;
}
protected function getPartialRootPaths(array $conf): ?array
{
$partialPaths = [];
$partialRootPath = (string)$this->cObj->stdWrapValue('partialRootPath', $conf);
if ($partialRootPath !== '') {
$partialPaths[] = GeneralUtility::getFileAbsFileName($partialRootPath);
}
if (isset($conf['partialRootPaths.'])) {
$partialPaths = array_replace($partialPaths, $this->applyStandardWrapToFluidPaths($conf['partialRootPaths.']));
}
return !empty($partialPaths) ? $partialPaths : null;
}
/**
* @todo: This magic has to fall one way or the other. It has been introduced for ext:form to
* mimic extbase, see https://forge.typo3.org/issues/78842. This is actively used when
* rendering forms using the formvh:render strategy, see the documentation.
*/
protected function buildExtbaseRequestIfNeeded(ServerRequestInterface $request, array $conf): ServerRequestInterface
{
$requestPluginName = (string)$this->cObj->stdWrapValue('pluginName', $conf['extbase.'] ?? []);
$requestControllerExtensionName = (string)$this->cObj->stdWrapValue('controllerExtensionName', $conf['extbase.'] ?? []);
$requestControllerName = (string)$this->cObj->stdWrapValue('controllerName', $conf['extbase.'] ?? []);
$requestControllerActionName = (string)$this->cObj->stdWrapValue('controllerActionName', $conf['extbase.'] ?? []);
if ($requestPluginName && $requestControllerExtensionName && $requestControllerName && $requestControllerActionName) {
$configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class);
$configurationManager->setConfiguration([
'extensionName' => $requestControllerExtensionName,
'pluginName' => $requestPluginName,
]);
if (!isset($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['extbase']['extensions'][$requestControllerExtensionName]['plugins'][$requestPluginName]['controllers'])) {
$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['extbase']['extensions'][$requestControllerExtensionName]['plugins'][$requestPluginName]['controllers'] = [
$requestControllerName => [
'actions' => [
$requestControllerActionName,
],
],
];
}
$requestBuilder = GeneralUtility::makeInstance(RequestBuilder::class);
$request = $requestBuilder->build($request);
}
return $request;
}
/**
* Compile rendered content objects in variables array ready to assign to the view.
*/
protected function getContentObjectVariables(array $conf): array
{
$variables = [];
$reservedVariables = ['data', 'current'];
// Accumulate the variables to be process and loop them through cObjGetSingle
$variablesToProcess = (array)($conf['variables.'] ?? []);
foreach ($variablesToProcess as $variableName => $cObjType) {
if (is_array($cObjType)) {
continue;
}
if (!in_array($variableName, $reservedVariables)) {
$cObjConf = $variablesToProcess[$variableName . '.'] ?? [];
$variables[$variableName] = $this->cObj->cObjGetSingle($cObjType, $cObjConf, 'variables.' . $variableName);
} else {
throw new \InvalidArgumentException(
'Cannot use reserved name "' . $variableName . '" as variable name in FLUIDTEMPLATE.',
1288095720
);
}
}
$variables['data'] = $this->cObj->data;
$variables['current'] = $this->cObj->data[$this->cObj->currentValKey] ?? null;
return $variables;
}
protected function applyStandardWrapToFluidPaths(array $paths): array
{
$finalPaths = [];
foreach ($paths as $key => $path) {
if (str_ends_with((string)$key, '.')) {
if (isset($paths[substr($key, 0, -1)])) {
continue;
}
$path = $this->cObj->stdWrap('', $path);
} elseif (isset($paths[$key . '.'])) {
$path = $this->cObj->stdWrap($path, $paths[$key . '.']);
}
$finalPaths[$key] = GeneralUtility::getFileAbsFileName($path);
}
return $finalPaths;
}
}
@@ -0,0 +1,65 @@
<?php
/*
* 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\Frontend\ContentObject;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\Menu\Exception\NoSuchMenuTypeException;
use TYPO3\CMS\Frontend\ContentObject\Menu\MenuContentObjectFactory;
/**
* Contains HMENU class object.
*/
class HierarchicalMenuContentObject extends AbstractContentObject
{
/**
* Rendering the cObject, HMENU
*
* @param array $conf Array of TypoScript properties
* @return string Output
*/
public function render($conf = [])
{
if (!empty($conf['if.']) && !$this->cObj->checkIf($conf['if.'])) {
return '';
}
$theValue = '';
$menuType = $conf[1] ?? '';
try {
$register = $this->request->getAttribute('frontend.register.stack')->current();
$menuObjectFactory = GeneralUtility::makeInstance(MenuContentObjectFactory::class);
$menu = $menuObjectFactory->getMenuObjectByType($menuType);
$countHMENU = (int)$register->get('count_HMENU', 0);
$countHMENU++;
$register->set('count_HMENU', $countHMENU);
$register->set('count_HMENU_MENUOBJ', 0);
$register->set('count_MENUOBJ', 0);
$menu->parent_cObj = $this->getContentObjectRenderer();
$menu->start(null, $this->getPageRepository(), '', $conf, 1, '', $this->request);
$menu->makeMenu();
$theValue .= $menu->writeMenu();
} catch (NoSuchMenuTypeException) {
}
$wrap = $this->cObj->stdWrapValue('wrap', $conf);
if ($wrap) {
$theValue = $this->cObj->wrap($theValue, $wrap);
}
if (isset($conf['stdWrap.'])) {
$theValue = $this->cObj->stdWrap($theValue, $conf['stdWrap.']);
}
return $theValue;
}
}
@@ -0,0 +1,303 @@
<?php
/*
* 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\Frontend\ContentObject;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
use TYPO3\CMS\Core\Page\AssetCollector;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\FileReference;
use TYPO3\CMS\Core\Service\MarkerBasedTemplateService;
use TYPO3\CMS\Core\TimeTracker\TimeTracker;
use TYPO3\CMS\Core\Type\DocType;
use TYPO3\CMS\Core\TypoScript\TypoScriptService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\Event\ModifyImageSourceCollectionEvent;
use TYPO3\CMS\Frontend\Page\FrontendUrlPrefix;
/**
* Contains IMAGE class object.
*/
class ImageContentObject extends AbstractContentObject
{
public function __construct(
protected readonly MarkerBasedTemplateService $markerTemplateService,
protected readonly TimeTracker $timeTracker,
protected readonly LoggerInterface $logger,
) {}
/**
* Rendering the cObject, IMAGE
*
* @param mixed $conf Array of TypoScript properties (marked as "mixed" currently because we don't know what we're receiving)
* @return string Output
*/
public function render($conf = []): string
{
if (!empty($conf['if.']) && !$this->cObj->checkIf($conf['if.'])) {
return '';
}
$theValue = $this->cImage($conf['file'] ?? '', is_array($conf) ? $conf : []);
if (isset($conf['stdWrap.'])) {
$theValue = $this->cObj->stdWrap($theValue, $conf['stdWrap.']);
}
return $theValue;
}
/**
* Returns a <img> tag with the image file defined by $file and processed according to the properties in the TypoScript array.
* Mostly this function is a sub-function to the IMAGE function which renders the IMAGE cObject in TypoScript.
*
* @param string|File|FileReference|null $file File TypoScript resource
* @param array $conf TypoScript configuration properties
* @return string HTML <img> tag, (possibly wrapped in links and other HTML) if any image found.
*/
protected function cImage($file, array $conf): string
{
$imageResource = $this->cObj->getImgResource($file, $conf['file.'] ?? []);
if ($imageResource === null) {
return '';
}
// $info['originalFile'] will be set, when the file is processed by FAL.
// In that case the URL is final and we must not add a prefix
if ($imageResource->getOriginalFile() === null && is_file($imageResource->getFullPath())) {
$absRefPrefix = GeneralUtility::makeInstance(FrontendUrlPrefix::class)->getUrlPrefix($this->request);
$source = $absRefPrefix . str_replace('%2F', '/', rawurlencode($imageResource->getPublicUrl()));
} else {
$source = $imageResource->getPublicUrl();
}
// A file whose physical resource is gone (sys_file.missing=1) resolves to
// an image resource without public URL. Render nothing in this case, just
// like for an image resource that could not be resolved at all.
if ($source === null) {
$identifier = $imageResource->getOriginalFile()?->getIdentifier() ?: $imageResource->getFullPath();
$this->logger->warning('The image "{file}" has no public URL, the file is probably missing, and won\'t be included in frontend output', [
'file' => $identifier,
]);
$this->timeTracker->setTSlogMessage(
'The image "' . $identifier . '" has no public URL, the file is probably missing. It is not rendered.',
LogLevel::WARNING
);
return '';
}
GeneralUtility::makeInstance(AssetCollector::class)->addMedia(
$source,
$imageResource->getLegacyImageResourceInformation()
);
$layoutKey = (string)$this->cObj->stdWrapValue('layoutKey', $conf);
$imageTagTemplate = $this->getImageTagTemplate($layoutKey, $conf);
$sourceCollection = $this->getImageSourceCollection($layoutKey, $conf, $file);
$altParam = $this->getAltParam($conf);
$params = $this->cObj->stdWrapValue('params', $conf);
if ($params !== '' && $params[0] !== ' ') {
$params = ' ' . $params;
}
$imageTagValues = [
'width' => $imageResource->getWidth(),
'height' => $imageResource->getHeight(),
'src' => htmlspecialchars($source),
'params' => $params,
'altParams' => $altParam,
'sourceCollection' => $sourceCollection,
'selfClosingTagSlash' => DocType::createFromRequest($this->request)->isXmlCompliant() ? ' /' : '',
];
$theValue = $this->markerTemplateService->substituteMarkerArray($imageTagTemplate, $imageTagValues, '###|###', true, true);
$linkWrap = (string)$this->cObj->stdWrapValue('linkWrap', $conf);
if ($linkWrap !== '') {
$theValue = $this->linkWrap($theValue, $linkWrap);
} elseif ($conf['imageLinkWrap'] ?? false) {
$originalFile = urldecode($imageResource->getFullPath());
$theValue = $this->cObj->imageLinkWrap($theValue, $originalFile, $conf['imageLinkWrap.']);
}
$wrap = $this->cObj->stdWrapValue('wrap', $conf);
if ((string)$wrap !== '') {
$theValue = $this->cObj->wrap($theValue, $conf['wrap']);
}
return $theValue;
}
/**
* Returns the html-template for rendering the image-Tag if no template is defined via typoscript the
* default <img> tag template is returned
*
* @param string $layoutKey rendering key
* @param array $conf TypoScript configuration properties
*/
protected function getImageTagTemplate($layoutKey, $conf): string
{
if ($layoutKey && isset($conf['layout.']) && isset($conf['layout.'][$layoutKey . '.'])) {
return $this->cObj->stdWrapValue('element', $conf['layout.'][$layoutKey . '.']);
}
return '<img src="###SRC###" width="###WIDTH###" height="###HEIGHT###" ###PARAMS### ###ALTPARAMS### ###SELFCLOSINGTAGSLASH###>';
}
/**
* Render alternate sources for the image tag. If no source collection is given an empty string is returned.
*
* @param string $layoutKey rendering key
* @param array $conf TypoScript configuration properties
* @param string|File|FileReference|null $file
* @return string
*/
protected function getImageSourceCollection(string $layoutKey, array $conf, $file)
{
$sourceCollection = '';
if ($layoutKey
&& isset($conf['sourceCollection.']) && $conf['sourceCollection.']
&& (
isset($conf['layout.'][$layoutKey . '.']['source']) && $conf['layout.'][$layoutKey . '.']['source']
|| isset($conf['layout.'][$layoutKey . '.']['source.']) && $conf['layout.'][$layoutKey . '.']['source.']
)
) {
// find active sourceCollection
$activeSourceCollections = [];
foreach ($conf['sourceCollection.'] as $sourceCollectionKey => $sourceCollectionConfiguration) {
if (str_ends_with($sourceCollectionKey, '.')) {
if (empty($sourceCollectionConfiguration['if.']) || $this->cObj->checkIf($sourceCollectionConfiguration['if.'])) {
$activeSourceCollections[] = $sourceCollectionConfiguration;
}
}
}
// apply option split to configurations
$typoScriptService = GeneralUtility::makeInstance(TypoScriptService::class);
$srcLayoutOptionSplitted = $typoScriptService->explodeConfigurationForOptionSplit((array)$conf['layout.'][$layoutKey . '.'], count($activeSourceCollections));
$eventDispatcher = GeneralUtility::makeInstance(EventDispatcherInterface::class);
$isXmlCompliant = DocType::createFromRequest($this->request)->isXmlCompliant();
// render sources
foreach ($activeSourceCollections as $key => $sourceConfiguration) {
$sourceLayout = $this->cObj->stdWrapValue('source', $srcLayoutOptionSplitted[$key] ?? []);
$sourceRenderConfiguration = [
'file' => $file,
'file.' => $conf['file.'] ?? null,
];
$imageQuality = $this->cObj->stdWrapValue('quality', $sourceConfiguration ?? []);
if ($imageQuality) {
$sourceRenderConfiguration['file.']['params'] = '-quality ' . (int)$imageQuality;
}
$pixelDensity = (int)$this->cObj->stdWrapValue('pixelDensity', $sourceConfiguration, 1);
$dimensionKeys = ['width', 'height', 'maxW', 'minW', 'maxH', 'minH', 'maxWidth', 'maxHeight', 'XY'];
foreach ($dimensionKeys as $dimensionKey) {
$dimension = (string)$this->cObj->stdWrapValue($dimensionKey, $sourceConfiguration);
if ($dimension === '') {
$dimension = (string)$this->cObj->stdWrapValue($dimensionKey, $conf['file.'] ?? []);
}
if ($dimension !== '') {
if (str_contains($dimension, 'c') && ($dimensionKey === 'width' || $dimensionKey === 'height')) {
$dimensionParts = explode('c', $dimension, 2);
$dimension = ((int)$dimensionParts[0] * $pixelDensity) . 'c';
if ($dimensionParts[1]) {
$dimension .= $dimensionParts[1];
}
} elseif ($dimensionKey === 'XY') {
$dimensionParts = GeneralUtility::intExplode(',', $dimension);
$dimension = $dimensionParts[0] * $pixelDensity;
if ($dimensionParts[1]) {
$dimension .= ',' . $dimensionParts[1] * $pixelDensity;
}
} else {
$dimension = (int)$dimension * $pixelDensity;
}
$sourceRenderConfiguration['file.'][$dimensionKey] = $dimension;
// Remove the stdWrap properties for dimension as they have been processed already above.
unset($sourceRenderConfiguration['file.'][$dimensionKey . '.']);
}
}
$imageResource = $this->cObj->getImgResource($sourceRenderConfiguration['file'], $sourceRenderConfiguration['file.']);
if ($imageResource !== null) {
$sourceConfiguration['width'] = $imageResource->getWidth();
$sourceConfiguration['height'] = $imageResource->getHeight();
$urlPrefix = '';
// Prepend 'absRefPrefix' to file path only if file was not processed by FAL, e.g. GIFBUILDER
if ($imageResource->getOriginalFile() === null && is_file($imageResource->getFullPath())) {
$urlPrefix = GeneralUtility::makeInstance(FrontendUrlPrefix::class)->getUrlPrefix($this->request);
}
$sourceConfiguration['src'] = htmlspecialchars($urlPrefix . $imageResource->getPublicUrl());
$sourceConfiguration['selfClosingTagSlash'] = $isXmlCompliant ? ' /' : '';
$oneSourceCollection = $this->markerTemplateService->substituteMarkerArray($sourceLayout, $sourceConfiguration, '###|###', true, true);
$sourceCollection .= $eventDispatcher->dispatch(
new ModifyImageSourceCollectionEvent($oneSourceCollection, $sourceCollection, (array)$sourceConfiguration, $sourceRenderConfiguration, $this->cObj)
)->getSourceCollection();
}
}
}
return $sourceCollection;
}
/**
* Wraps the input string by the $wrap value and implements the "linkWrap" data type as well.
*
* The "linkWrap" data type means that this function will find any integer encapsulated
* in {} (curly braces) in the first wrap part and substitute it with the corresponding page
* uid from the rootline where the found integer is pointing to the key in the rootline.
*
* @param string $content Input string
* @param string $wrap A string where the first two parts separated by "|" (vertical line) will be wrapped around the input string
*/
protected function linkWrap(string $content, string $wrap): string
{
$wrapArr = explode('|', $wrap);
if (preg_match('/\\{([0-9]*)\\}/', $wrapArr[0], $reg)) {
$localRootLine = $this->request->getAttribute('frontend.page.information')->getLocalRootLine();
$uid = $localRootLine[$reg[1]]['uid'] ?? null;
if ($uid) {
$wrapArr[0] = str_replace($reg[0], $uid, $wrapArr[0]);
}
}
return trim($wrapArr[0]) . $content . trim($wrapArr[1] ?? '');
}
/**
* An abstraction method which creates an alt or title parameter for an HTML img, applet, area or input element and the FILE content element.
* From the $conf array it implements the properties "altText" and "titleText"
*
* @param array $conf TypoScript configuration properties
* @return string Parameter string containing alt and title parameters (if any)
*/
protected function getAltParam(array $conf): string
{
$altText = trim((string)$this->cObj->stdWrapValue('altText', $conf));
$titleText = trim((string)$this->cObj->stdWrapValue('titleText', $conf));
// "alt":
$altParam = ' alt="' . htmlspecialchars($altText) . '"';
// "title":
$emptyTitleHandling = $this->cObj->stdWrapValue('emptyTitleHandling', $conf);
// Choices: 'keepEmpty' | 'useAlt' | 'removeAttr'
if ($titleText || $emptyTitleHandling === 'keepEmpty') {
$altParam .= ' title="' . htmlspecialchars($titleText) . '"';
} elseif ($emptyTitleHandling === 'useAlt') {
$altParam .= ' title="' . htmlspecialchars($altText) . '"';
}
return $altParam;
}
}
@@ -0,0 +1,39 @@
<?php
/*
* 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\Frontend\ContentObject;
/**
* Contains IMG_RESOURCE class object.
*/
class ImageResourceContentObject extends AbstractContentObject
{
/**
* Rendering the cObject, IMG_RESOURCE
*
* @param array $conf Array of TypoScript properties
* @return string Output
*/
public function render($conf = [])
{
$imageResource = $this->cObj->getImgResource($conf['file'] ?? '', $conf['file.'] ?? []);
if ($imageResource === null) {
return '';
}
return isset($conf['stdWrap.'])
? $this->cObj->stdWrap($imageResource->getPublicUrl(), $conf['stdWrap.'])
: $imageResource->getPublicUrl();
}
}
@@ -0,0 +1,57 @@
<?php
/*
* 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\Frontend\ContentObject;
/**
* Implement cObj "LOAD_REGISTER":
* Get latest Register, clone it, set a key/value, push as new latest RegisterStack entry.
*
* Note the naming "LOAD_REGISTER" is kinda misleading since it rather "loads into" or
* sets a new value and pushes as key/value to the stack. "RESTORE_REGISTER" is the
* counterpart cObj to get rid of that state again.
*/
class LoadRegisterContentObject extends AbstractContentObject
{
/**
* Does not return any content, it just sets internal data based on the TypoScript properties.
*
* @param mixed $conf Array of TypoScript properties (marked as "mixed" currently because we don't know what we're receiving)
* @return string Empty string
*/
public function render($conf = []): string
{
$registerStack = $this->request->getAttribute('frontend.register.stack');
$clonedRegister = clone $registerStack->current();
if (is_array($conf)) {
$isExecuted = [];
foreach ($conf as $key => $value) {
$key = rtrim($key, '.');
if (!isset($isExecuted[$key])) {
$registerProperties = $key . '.';
if (isset($conf[$key]) && isset($conf[$registerProperties])) {
$value = $this->cObj->stdWrap($conf[$key], $conf[$registerProperties]);
} elseif (isset($conf[$registerProperties])) {
$value = $this->cObj->stdWrap('', $conf[$registerProperties]);
}
$clonedRegister->set($key, $value);
$isExecuted[$key] = true;
}
}
}
$registerStack->push($clonedRegister);
return '';
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,117 @@
<?php
/*
* 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\Frontend\ContentObject\Menu;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\Category\Collection\CategoryCollection;
/**
* Utility class for menus based on category collections of pages.
*
* Returns all the relevant pages for rendering with a menu content object.
* @internal this is only used for internal purposes and solely used for EXT:frontend and not part of TYPO3's Core API.
*/
class CategoryMenuUtility
{
/**
* @var string Name of the field used for sorting the pages
*/
protected static $sortingField;
/**
* Collects all pages for the selected categories, sorted according to configuration.
*
* @param string $selectedCategories Comma-separated list of system categories primary keys
* @param array|null $configuration TypoScript configuration for the "special." keyword
* @param AbstractMenuContentObject $parentObject Back-reference to the calling object
* @return array List of selected pages
*/
public function collectPages($selectedCategories, $configuration, $parentObject)
{
$selectedPages = [];
$categoriesPerPage = [];
// Determine the name of the relation field
$relationField = (string)$parentObject->getParentContentObject()->stdWrapValue('relation', $configuration ?? []);
// Get the pages for each selected category
$selectedCategories = GeneralUtility::intExplode(',', $selectedCategories, true);
foreach ($selectedCategories as $aCategory) {
$collection = CategoryCollection::load(
$aCategory,
true,
'pages',
$relationField
);
$categoryUid = $collection->getUid();
// Loop on the results, overlay each page record found
foreach ($collection as $pageItem) {
$parentObject->getSysPage()->versionOL('pages', $pageItem, true);
if (is_array($pageItem)) {
$selectedPages[$pageItem['uid']] = $parentObject->getSysPage()->getLanguageOverlay('pages', $pageItem);
// Keep a list of the categories each page belongs to
if (!isset($categoriesPerPage[$pageItem['uid']])) {
$categoriesPerPage[$pageItem['uid']] = [];
}
$categoriesPerPage[$pageItem['uid']][] = $categoryUid;
}
}
}
// Loop on the selected pages to add the categories they belong to, as comma-separated list of category uid's)
// (this makes them available for rendering, if needed)
foreach ($selectedPages as $uid => $pageRecord) {
$selectedPages[$uid]['_categories'] = implode(',', $categoriesPerPage[$uid]);
}
// Sort the pages according to the sorting property
self::$sortingField = (string)$parentObject->getParentContentObject()->stdWrapValue('sorting', $configuration ?? []);
$order = (string)$parentObject->getParentContentObject()->stdWrapValue('order', $configuration ?? []);
$selectedPages = $this->sortPages($selectedPages, $order);
return $selectedPages;
}
/**
* Sorts the selected pages
*
* If the sorting field is not defined or does not corresponding to an existing field
* of the "pages" tables, the list of pages will remain unchanged.
*
* @param array $pages List of selected pages
* @param string $order Order for sorting (should "asc" or "desc")
* @return array Sorted list of pages
*/
protected function sortPages($pages, $order)
{
// Perform the sorting only if a criterion was actually defined
if (!empty(self::$sortingField)) {
// Check that the sorting field exists (checking the first record is enough)
$firstPage = current($pages);
if (isset($firstPage[self::$sortingField])) {
// Make sure the order property is either "asc" or "desc" (default is "asc")
if (!empty($order)) {
$order = strtolower($order);
if ($order !== 'desc') {
$order = 'asc';
}
}
$sortMultiplier = $order === 'asc' ? 1 : -1;
uasort($pages, static function (array $pageA, array $pageB) use ($sortMultiplier): int {
return strnatcasecmp($pageA[self::$sortingField], $pageB[self::$sortingField]) * $sortMultiplier;
});
}
}
return $pages;
}
}
@@ -0,0 +1,23 @@
<?php
/*
* 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\Frontend\ContentObject\Menu\Exception;
use TYPO3\CMS\Frontend\Exception;
/**
* No such menu type exception
*/
class NoSuchMenuTypeException extends Exception {}
@@ -0,0 +1,68 @@
<?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\Frontend\ContentObject\Menu;
use TYPO3\CMS\Core\SingletonInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\Menu\Exception\NoSuchMenuTypeException;
/**
* Factory for menu content objects. Allows overriding the default
* types like 'TMENU' with an own implementation (only one possible)
* and new types can be registered.
* @internal this is only used for internal purposes and solely used for EXT:frontend and not part of TYPO3's Core API.
*/
class MenuContentObjectFactory implements SingletonInterface
{
/**
* Register of TypoScript keys to according render class
*/
protected array $menuTypeToClassMapping = [
'TMENU' => TextMenuContentObject::class,
];
/**
* Gets a typo script string like 'TMENU' and returns an object of this type
*
* @throws Exception\NoSuchMenuTypeException
*/
public function getMenuObjectByType(string $type = ''): AbstractMenuContentObject
{
$upperCasedClassName = strtoupper($type);
if (array_key_exists($upperCasedClassName, $this->menuTypeToClassMapping)) {
/** @var AbstractMenuContentObject $object */
$object = GeneralUtility::makeInstance($this->menuTypeToClassMapping[$upperCasedClassName]);
return $object;
}
throw new NoSuchMenuTypeException(
'Menu type ' . (string)$type . ' has no implementing class.',
1363278130
);
}
/**
* Register new menu type or override existing type
*
* @param string $type Menu type to be used in TypoScript
* @param string $className Class rendering the menu
*/
public function registerMenuType(string $type, string $className)
{
$this->menuTypeToClassMapping[strtoupper($type)] = $className;
}
}
@@ -0,0 +1,166 @@
<?php
/*
* 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\Frontend\ContentObject\Menu;
use TYPO3\CMS\Core\TypoScript\TypoScriptService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* Extension class creating text based menus
*/
class TextMenuContentObject extends AbstractMenuContentObject
{
/**
* Traverses the ->result array of menu items configuration (made by ->generate()) and renders each item.
* An instance of ContentObjectRenderer is also made and for each menu item rendered it is loaded with
* the record for that page so that any stdWrap properties that applies will have the current menu items record available.
*
* @return string The HTML for the menu including submenus
*/
public function writeMenu()
{
if (empty($this->result)) {
return '';
}
$register = $this->request->getAttribute('frontend.register.stack')->current();
$cObjectForCurrentMenu = GeneralUtility::makeInstance(ContentObjectRenderer::class);
$menuContent = [];
$typoScriptService = GeneralUtility::makeInstance(TypoScriptService::class);
$subMenuObjSuffixes = $typoScriptService->explodeConfigurationForOptionSplit(['sOSuffix' => $this->mconf['submenuObjSuffixes'] ?? null], count($this->result));
$explicitSpacerRenderingEnabled = ($this->mconf['SPC'] ?? false);
foreach ($this->result as $key => $val) {
$register->set('count_HMENU_MENUOBJ', (int)$register->get('count_HMENU_MENUOBJ', 0) + 1);
$register->set('count_MENUOBJ', (int)$register->get('count_MENUOBJ', 0) + 1);
// Initialize the cObj with the page record of the menu item
$cObjectForCurrentMenu->setRequest($this->request);
$cObjectForCurrentMenu->start($this->menuArr[$key], 'pages');
$this->I = [];
$this->I['key'] = $key;
$this->I['val'] = $val;
$this->I['title'] = $this->getPageTitle($this->menuArr[$key]['title'] ?? '', $this->menuArr[$key]['nav_title'] ?? '');
$this->I['title.'] = $this->I['val']['stdWrap.'] ?? [];
$this->I['title'] = $cObjectForCurrentMenu->stdWrapValue('title', $this->I);
$this->I['uid'] = $this->menuArr[$key]['uid'] ?? 0;
$this->I['mount_pid'] = $this->menuArr[$key]['mount_pid'] ?? 0;
$this->I['pid'] = $this->menuArr[$key]['pid'] ?? 0;
$this->I['spacer'] = $this->menuArr[$key]['isSpacer'] ?? false;
// Make link tag
$this->I['val']['additionalParams'] = $cObjectForCurrentMenu->stdWrapValue('additionalParams', $this->I['val']);
$linkResult = $this->link((int)$key, (string)($this->I['val']['altTarget'] ?? ''), ($this->mconf['forceTypeValue'] ?? ''));
if ($linkResult === null) {
$this->I['val']['doNotLinkIt'] = 1;
}
// Title attribute of links
$titleAttrValue = $cObjectForCurrentMenu->stdWrapValue('ATagTitle', $this->I['val']);
if ($linkResult && $titleAttrValue !== '') {
$linkResult = $linkResult->withAttribute('title', $titleAttrValue);
}
$this->I['linkHREF'] = $linkResult;
$this->I['val']['doNotLinkIt'] = (bool)$cObjectForCurrentMenu->stdWrapValue('doNotLinkIt', $this->I['val']);
// Compile link tag
if (!$this->I['spacer'] && !$this->I['val']['doNotLinkIt']) {
$this->setATagParts($linkResult);
} else {
$this->I['A1'] = '';
$this->I['A2'] = '';
}
// ATagBeforeWrap processing:
if ($this->I['val']['ATagBeforeWrap'] ?? false) {
$wrapPartsBefore = explode('|', $this->I['val']['linkWrap'] ?? '');
$wrapPartsAfter = ['', ''];
} else {
$wrapPartsBefore = ['', ''];
$wrapPartsAfter = explode('|', $this->I['val']['linkWrap'] ?? '');
}
if (($this->I['val']['stdWrap2'] ?? false) || isset($this->I['val']['stdWrap2.'])) {
$stdWrap2 = (string)(isset($this->I['val']['stdWrap2.']) ? $cObjectForCurrentMenu->stdWrap('|', $this->I['val']['stdWrap2.']) : '|');
$stdWrap2Value = (string)($this->I['val']['stdWrap2'] ?? '|');
$stdWrap2Value = $stdWrap2Value !== '' ? $stdWrap2Value : '|';
$wrapPartsStdWrap = explode($stdWrap2Value, $stdWrap2);
} else {
$wrapPartsStdWrap = ['', ''];
}
// Make before, middle and after parts
$this->I['parts'] = [];
$this->I['parts']['before'] = $this->getBeforeAfter('before', $cObjectForCurrentMenu);
$this->I['parts']['stdWrap2_begin'] = $wrapPartsStdWrap[0];
// stdWrap for doNotShowLink
$this->I['val']['doNotShowLink'] = $cObjectForCurrentMenu->stdWrapValue('doNotShowLink', $this->I['val']);
if (!$this->I['val']['doNotShowLink']) {
$this->I['parts']['notATagBeforeWrap_begin'] = $wrapPartsAfter[0];
$this->I['parts']['ATag_begin'] = $this->I['A1'];
$this->I['parts']['ATagBeforeWrap_begin'] = $wrapPartsBefore[0];
$this->I['parts']['title'] = $this->I['title'];
$this->I['parts']['ATagBeforeWrap_end'] = $wrapPartsBefore[1] ?? '';
$this->I['parts']['ATag_end'] = $this->I['A2'];
$this->I['parts']['notATagBeforeWrap_end'] = $wrapPartsAfter[1] ?? '';
}
$this->I['parts']['stdWrap2_end'] = $wrapPartsStdWrap[1] ?? '';
$this->I['parts']['after'] = $this->getBeforeAfter('after', $cObjectForCurrentMenu);
// Passing I to a user function
if ($this->mconf['IProcFunc'] ?? false) {
$this->I = $this->userProcess('IProcFunc', $this->I);
}
// Merge parts + beforeAllWrap
$this->I['theItem'] = implode('', $this->I['parts']);
$allWrap = $cObjectForCurrentMenu->stdWrapValue('allWrap', $this->I['val']);
$this->I['theItem'] = $cObjectForCurrentMenu->wrap($this->I['theItem'], $allWrap);
if ($this->I['val']['subst_elementUid'] ?? false) {
$this->I['theItem'] = str_replace('{elementUid}', (string)$this->I['uid'], $this->I['theItem']);
}
if (is_array($this->I['val']['allStdWrap.'] ?? null)) {
$this->I['theItem'] = $cObjectForCurrentMenu->stdWrap($this->I['theItem'], $this->I['val']['allStdWrap.']);
}
$isSpacerPage = $this->I['spacer'] ?? false;
// If rendering of SPACERs is enabled, also allow rendering submenus with Spacers
if (!$isSpacerPage || $explicitSpacerRenderingEnabled) {
// Add part to the accumulated result + fetch submenus
$this->I['theItem'] .= $this->subMenu($this->I['uid'], $subMenuObjSuffixes[$key]['sOSuffix'] ?? '', $key);
}
$part = $cObjectForCurrentMenu->stdWrapValue('wrapItemAndSub', $this->I['val']);
$menuContent[] = $part ? $cObjectForCurrentMenu->wrap($this->I['theItem'], $part) : $this->I['theItem'];
}
$menuContent = implode('', $menuContent);
if (is_array($this->mconf['stdWrap.'] ?? null)) {
$menuContent = (string)$cObjectForCurrentMenu->stdWrap($menuContent, $this->mconf['stdWrap.']);
}
return $cObjectForCurrentMenu->wrap($menuContent, $this->mconf['wrap'] ?? '');
}
/**
* Generates the before* and after* stdWrap for TMENUs
* Evaluates:
* - before.stdWrap*
* - beforeWrap
* - after.stdWrap*
* - afterWrap
*
* @param string $pref Can be "before" or "after" and determines which kind of stdWrap to process (basically this is the prefix of the TypoScript properties that are read from the ->I['val'] array
* @return string The resulting HTML
*/
protected function getBeforeAfter(string $pref, ContentObjectRenderer $cObjectForCurrentMenu): string
{
$processedPref = $cObjectForCurrentMenu->stdWrapValue($pref, $this->I['val']);
if (isset($this->I['val'][$pref . 'Wrap'])) {
return $cObjectForCurrentMenu->wrap($processedPref, $this->I['val'][$pref . 'Wrap']);
}
return $processedPref;
}
}
@@ -0,0 +1,175 @@
<?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\Frontend\ContentObject;
use TYPO3\CMS\Core\Information\Typo3Information;
use TYPO3\CMS\Core\Page\PageLayoutResolver;
use TYPO3\CMS\Core\TypoScript\TypoScriptService;
use TYPO3\CMS\Core\Utility\PathUtility;
use TYPO3\CMS\Core\View\ViewFactoryData;
use TYPO3\CMS\Core\View\ViewFactoryInterface;
use TYPO3\CMS\Frontend\ContentObject\Exception\ContentRenderingException;
use TYPO3Fluid\Fluid\View\Exception\InvalidLayoutException;
use TYPO3Fluid\Fluid\View\Exception\InvalidPartialException;
use TYPO3Fluid\Fluid\View\Exception\InvalidTemplateResourceException;
/**
* PAGEVIEW Content Object.
*
* Built to render a full page with Fluid, and does the following
* - uses the template from the given Page Layout / Backend Layout of the current page in a folder "Pages/Mylayout.html"
* - paths are resolved from "paths." configuration
* - automatically adds templateRootPaths to the layoutRootPaths and partialRootPaths
* - injects pageInformation, site and siteLanguage (= language) as variables by default
* - adds all page settings (= TypoScript constants) into the settings variable of the View
*
* In contrast to FLUIDTEMPLATE, by design this cObject
* - does not handle custom layoutRootPaths and partialRootPaths
* - does not handle Extbase specialities
* - does not handle "templateName.", "template." and "file." resolving from cObject
*/
final class PageViewContentObject extends AbstractContentObject
{
private const array reservedVariables = ['site', 'language', 'page'];
public function __construct(
private readonly ContentDataProcessor $contentDataProcessor,
private readonly TypoScriptService $typoScriptService,
private readonly PageLayoutResolver $pageLayoutResolver,
private readonly ViewFactoryInterface $viewFactory,
) {}
/**
* Rendering the cObject, PAGEVIEW
*
* Configuration properties:
* - paths array to template files
* - variables array of cObjects, the keys are the variable names in fluid
* - dataProcessing array of data processors which are classes to manipulate $data
*
* Example:
* page.10 = PAGEVIEW
* page.10.paths.10 = EXT:site_configuration/Resources/Private/Templates/
* page.10.variables {
* mylabel = TEXT
* mylabel.value = Label from TypoScript
* }
*
* @param mixed $conf Array of TypoScript properties (marked as "mixed" currently because we don't know what we're receiving)
* @return string The HTML output
* @throws ContentRenderingException
*/
public function render($conf = []): string
{
if (!is_array($conf)) {
$conf = [];
}
if (!is_array($conf['paths.'] ?? false) || $conf['paths.'] === []) {
throw new ContentRenderingException(
'PAGEVIEW content object needs a "paths." TypoScript array',
1724601907
);
}
$paths = array_map(PathUtility::sanitizeTrailingSeparator(...), $conf['paths.']);
$viewFactoryData = new ViewFactoryData(
// @todo: Do discuss: Rename 'paths.' to 'templateRootPaths.' again?
templateRootPaths: array_map(static fn(string $path): string => $path . 'Pages/', $paths),
// @todo: We should *still* allow setting both partialRootPaths and layoutRootPaths, and only fall back to
// [templateRootPaths]/Partials and [templateRootPaths]/Layouts if not set. And the fallback should be
// advertised as best practice.
partialRootPaths: array_map(static fn(string $path): string => $path . 'Partials/', $paths),
layoutRootPaths: array_map(static fn(string $path): string => $path . 'Layouts/', $paths),
request: $this->request,
);
$view = $this->viewFactory->create($viewFactoryData);
$pageSettings = $this->request->getAttribute('frontend.typoscript')->getSettingsTree()->toArray();
$view->assign('settings', $this->typoScriptService->convertTypoScriptArrayToPlainArray($pageSettings));
$variables = $this->getContentObjectVariables($conf);
$variables = $this->contentDataProcessor->process($this->cObj, $conf, $variables);
$view->assignMultiple($variables);
// Fetch the Fluid template by the name of the Page Layout and underneath "Pages"
$pageInformationObject = $this->request->getAttribute('frontend.page.information');
$pageLayoutName = $this->pageLayoutResolver->getLayoutIdentifierForPageWithoutPrefix(
$pageInformationObject->getPageRecord(),
$pageInformationObject->getRootLine()
);
try {
return $view->render($pageLayoutName);
} catch (InvalidTemplateResourceException $e) {
// Only add a PAGEVIEW specific message in case the exception has been thrown for the given template.
if ($e instanceof InvalidPartialException || $e instanceof InvalidLayoutException || $e->templateName !== 'Default/' . $pageLayoutName) {
throw $e;
}
throw new InvalidTemplateResourceException(
sprintf(
'PAGEVIEW TypoScript object: Failed to resolve a template file for page layout "%s". See also: %s. The following paths were checked: "%s"',
$pageLayoutName,
Typo3Information::getDocsLink('t3tsref:cobj-pageview'),
implode('", "', $e->evaluatedTemplatePaths),
),
1742058289,
$e,
$e->templateName,
$e->evaluatedTemplatePaths,
);
}
}
/**
* Compile rendered content objects in variables array ready to assign to the view
*
* @param array $conf Configuration array
* @return array the variables to be assigned
*/
private function getContentObjectVariables(array $conf): array
{
$pageInformation = $this->request->getAttribute('frontend.page.information');
$variables = [
'site' => $this->request->getAttribute('site'),
'language' => $this->request->getAttribute('language'),
'page' => $pageInformation,
];
// Accumulate the variables to be process and loop them through cObjGetSingle
if (is_array($conf['variables.'] ?? false) && $conf['variables.'] !== []) {
foreach ($conf['variables.'] as $variableName => $cObjType) {
if (!is_string($cObjType)) {
continue;
}
if (in_array($variableName, self::reservedVariables, true)) {
throw new \InvalidArgumentException(
'Cannot use reserved name "' . $variableName . '" as variable name in PAGEVIEW.',
1711748615
);
}
$cObjConf = $conf['variables.'][$variableName . '.'] ?? [];
$variables[$variableName] = $this->cObj->cObjGetSingle($cObjType, $cObjConf, 'variables.' . $variableName);
}
}
if (!($conf['contentAs'] ?? false) && isset($variables['content'])) {
throw new \InvalidArgumentException(
'No variable name ("contentAs" option) for the content areas has been defined in PAGEVIEW, and the fallback name "content" is not available because it has been manually set.',
1726475574
);
}
$variables[$conf['contentAs'] ?? 'content'] = $pageInformation->getPageLayout()?->getContentAreas()->withRequest($this->request);
return $variables;
}
}
@@ -0,0 +1,224 @@
<?php
/*
* 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\Frontend\ContentObject;
use Psr\Log\LogLevel;
use TYPO3\CMS\Core\Database\RelationHandler;
use TYPO3\CMS\Core\TimeTracker\TimeTracker;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\Category\Collection\CategoryCollection;
/**
* Contains RECORDS class object.
*/
class RecordsContentObject extends AbstractContentObject
{
/**
* List of all items with table and uid information
*
* @var array
*/
protected $itemArray = [];
/**
* List of all selected records with full data, arranged per table
*
* @var array
*/
protected $data = [];
public function __construct(protected readonly TimeTracker $timeTracker) {}
/**
* Rendering the cObject, RECORDS
*
* @param mixed $conf Array of TypoScript properties (marked as "mixed" currently because we don't know what we're receiving)
* @return string Output
*/
public function render($conf = [])
{
// Reset items and data
$this->itemArray = [];
$this->data = [];
$theValue = '';
$tables = (string)$this->cObj->stdWrapValue('tables', $conf ?? []);
if ($tables !== '') {
$tablesArray = array_unique(GeneralUtility::trimExplode(',', $tables, true));
// Add tables which have a configuration (note that this may create duplicate entries)
if (is_array($conf['conf.'] ?? false)) {
foreach ($conf['conf.'] as $key => $value) {
if (!str_ends_with($key, '.') && !in_array($key, $tablesArray)) {
$tablesArray[] = $key;
}
}
}
// Get the data, depending on collection method.
// Property "source" is considered more precise and thus takes precedence over "categories"
$source = (string)$this->cObj->stdWrapValue('source', $conf ?? []);
$categories = (string)$this->cObj->stdWrapValue('categories', $conf ?? []);
if ($source !== '') {
$this->collectRecordsFromSource($source, $tablesArray);
} elseif ($categories !== '') {
$relationField = (string)$this->cObj->stdWrapValue('relation', $conf['categories.'] ?? []);
$this->collectRecordsFromCategories($categories, $tablesArray, $relationField);
}
if (!empty($this->itemArray)) {
$cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class);
$cObj->setParent($this->cObj->data, $this->cObj->currentRecord);
$pageRepository = $this->getPageRepository();
foreach ($this->itemArray as $val) {
$row = $this->data[$val['table']][$val['id']] ?? null;
if (!is_array($row)) {
continue;
}
// Perform overlays if necessary (records coming from category collections are already overlaid)
if ($source !== '') {
// Versioning preview
$pageRepository->versionOL($val['table'], $row);
// Language overlay
if (is_array($row)) {
$row = $pageRepository->getLanguageOverlay($val['table'], $row);
}
}
// Might be unset during the overlay process
if (!is_array($row)) {
continue;
}
if ($this->isRecordsPageAccessible($val['table'], $row, $conf)) {
$renderObjName = ($conf['conf.'][$val['table']] ?? false) ? $conf['conf.'][$val['table']] : '<' . $val['table'];
$renderObjKey = ($conf['conf.'][$val['table']] ?? false) ? 'conf.' . $val['table'] : '';
$renderObjConf = ($conf['conf.'][$val['table'] . '.'] ?? false) ? $conf['conf.'][$val['table'] . '.'] : [];
$this->cObj->lastChanged($row['tstamp'] ?? 0);
$cObj->setRequest($this->request);
$cObj->start($row, $val['table']);
$tmpValue = $cObj->cObjGetSingle($renderObjName, $renderObjConf, $renderObjKey);
$theValue .= $tmpValue;
}
}
}
}
$wrap = $this->cObj->stdWrapValue('wrap', $conf);
if ($wrap) {
$theValue = $this->cObj->wrap($theValue, $wrap);
}
if (isset($conf['stdWrap.'])) {
$theValue = $this->cObj->stdWrap($theValue, $conf['stdWrap.']);
}
return $theValue;
}
/**
* Checks if the records page is accessible
*/
protected function isRecordsPageAccessible(string $table, array $row, array $conf): bool
{
$pageId = (int)($table === 'pages' ? $row['uid'] : $row['pid']);
if ($pageId === $this->request->getAttribute('frontend.page.information')->getId()) {
// Access to current page has already been checked before rendering this content object.
return true;
}
if ($this->cObj->stdWrapValue('dontCheckPid', $conf)) {
return true;
}
$validPageId = $this->getPageRepository()->filterAccessiblePageIds([$pageId]);
return $validPageId !== [];
}
/**
* Collects records according to the configured source
*
* @param string $source Source of records
* @param array $tables List of tables
*/
protected function collectRecordsFromSource($source, array $tables)
{
$loadDB = GeneralUtility::makeInstance(RelationHandler::class);
$loadDB->start($source, implode(',', $tables));
foreach ($loadDB->tableArray as $table => $v) {
$constraints = $this->getPageRepository()->getDefaultConstraints($table);
if ($constraints !== []) {
$loadDB->additionalWhere[$table] = implode(' AND ', $constraints);
}
}
$this->data = $loadDB->getFromDB();
reset($loadDB->itemArray);
$this->itemArray = $loadDB->itemArray;
}
/**
* Collects records for all selected tables and categories.
*
* @param string $selectedCategories Comma-separated list of categories
* @param array $tables List of tables
* @param string $relationField Name of the field containing the categories relation
*/
protected function collectRecordsFromCategories($selectedCategories, array $tables, $relationField)
{
$selectedCategories = array_unique(GeneralUtility::intExplode(',', $selectedCategories, true));
// Loop on all selected tables
foreach ($tables as $table) {
// Get the records for each selected category
$tableRecords = [];
$categoriesPerRecord = [];
foreach ($selectedCategories as $aCategory) {
try {
$collection = CategoryCollection::load(
$aCategory,
true,
$table,
$relationField
);
if ($collection->count() > 0) {
// Add items to the collection of records for the current table
foreach ($collection as $item) {
$tableRecords[$item['uid']] = $item;
// Keep track of all categories a given item belongs to
if (!isset($categoriesPerRecord[$item['uid']])) {
$categoriesPerRecord[$item['uid']] = [];
}
$categoriesPerRecord[$item['uid']][] = $aCategory;
}
}
} catch (\Exception $e) {
$message = sprintf(
'Could not get records for category id %d. Error: %s (%d)',
$aCategory,
$e->getMessage(),
$e->getCode()
);
$this->timeTracker->setTSlogMessage($message, LogLevel::WARNING);
}
}
// Store the resulting records into the itemArray and data results array
if (!empty($tableRecords)) {
$this->data[$table] = [];
foreach ($tableRecords as $record) {
$this->itemArray[] = [
'id' => $record['uid'],
'table' => $table,
];
// Add to the record the categories it belongs to
$record['_categories'] = implode(',', $categoriesPerRecord[$record['uid']]);
$this->data[$table][$record['uid']] = $record;
}
}
}
}
}
+43
View File
@@ -0,0 +1,43 @@
<?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\Frontend\ContentObject;
/**
* A simple key/value store. See class RegisterStack for more information.
*
* @internal This class is not part of the TYPO3 Core API
*/
final class Register
{
private array $keyValues = [];
/**
* Unable to deal with objects, accepts simple types only.
* This is done by intention to not run into issues if this
* state needs to be serialized (cached).
*/
public function set(string $key, string|int|bool|float $value): void
{
$this->keyValues[$key] = $value;
}
public function get(string $key, string|int|bool|float|null $default = null): string|int|bool|float|null
{
return $this->keyValues[$key] ?? $default;
}
}
+86
View File
@@ -0,0 +1,86 @@
<?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\Frontend\ContentObject;
/**
* Contains TypoScript "Register" state together with class Register.
*
* An instance of this class is created during frontend rendering and registered
* as Request attribute "frontend.register.stack".
*
* The TypoScript "register" is a key-value store (implemented by class Register)
* combined with a stack. This has probably been invented for menu rendering back
* then: content objects in general and TypoScript based menu rendering specifically
* can be nested when multiple menu depths/layers are rendered. Each of those layers
* may need own values within their scope.
*
* The two main usages are TypoScript data/getText to access register entries along
* with content objects LOAD_REGISTER and RESTORE_REGISTER which push and pop register
* objects from the stack and store key/value entries.
*
* There is one quirk in comparison to a "classic" stack: New Register instances are
* typically clones of the underlying Register instance plus new key/values, see class
* LoadRegisterContentObject for an implementation: Values can be set on a lower
* level register and is still "seen/part of" a register on top. key/values bubble up,
* but not down.
*
* @internal This class is not part of the TYPO3 Core API
*/
final class RegisterStack
{
/**
* @var Register[]
*/
private array $registerStack = [];
public function __construct()
{
$this->push(new Register());
}
/**
* Peek current Register. Does not change stack.
*/
public function current(): Register
{
return array_last($this->registerStack);
}
/**
* Add a new Register instance to top of stack
*/
public function push(Register $register): void
{
$this->registerStack[] = $register;
}
/**
* Remove top of stack Register and return it.
* Re-inits with an empty register if empty to avoid exception or nullable handling
* in consumers if there is for example a "RESTORE_REGISTER" cObj too much. As
* drawback, consumers never know if there is a leftover pop().
*/
public function pop(): Register
{
$register = array_pop($this->registerStack);
if (empty($this->registerStack)) {
$this->push(new Register());
}
return $register;
}
}
@@ -0,0 +1,36 @@
<?php
/*
* 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\Frontend\ContentObject;
/**
* Implement cObj "RESTORE_REGISTER":
* As the counterpart of "LOAD_REGISTER", "RESTORE_REGISTER" removes any state
* added by latest "LOAD_REGISTER" again.
*/
class RestoreRegisterContentObject extends AbstractContentObject
{
/**
* Does not return any content, it just sets internal data based on the TypoScript properties.
*
* @param array $conf Array of TypoScript properties
* @return string Empty string
*/
public function render($conf = []): string
{
$this->request->getAttribute('frontend.register.stack')->pop();
return '';
}
}
@@ -0,0 +1,150 @@
<?php
/*
* 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\Frontend\ContentObject;
use TYPO3\CMS\Core\Imaging\Exception\InvalidSvgException;
use TYPO3\CMS\Core\Imaging\Svg\SvgDocumentFactory;
use TYPO3\CMS\Core\Imaging\Svg\SvgDocumentService;
use TYPO3\CMS\Core\SystemResource\Exception\SystemResourceDoesNotExistException;
use TYPO3\CMS\Core\SystemResource\Exception\SystemResourceException;
use TYPO3\CMS\Core\SystemResource\Publishing\SystemResourcePublisherInterface;
use TYPO3\CMS\Core\SystemResource\SystemResourceFactory;
use TYPO3\CMS\Core\SystemResource\Type\PublicResourceInterface;
use TYPO3\CMS\Core\SystemResource\Type\SystemResourceInterface;
/**
* Contains SVG content object.
*/
class ScalableVectorGraphicsContentObject extends AbstractContentObject
{
public function __construct(
protected readonly SystemResourceFactory $resourceFactory,
protected readonly SystemResourcePublisherInterface $resourcePublisher,
protected readonly SvgDocumentFactory $svgDocumentFactory,
protected readonly SvgDocumentService $svgDocumentService,
) {}
/**
* Rendering the cObject, SVG
*
* @param array $conf Array of TypoScript properties
*/
public function render($conf = []): string
{
$renderMode = $this->cObj->stdWrapValue('renderMode', $conf);
if ($renderMode === 'inline') {
return $this->renderInline($conf);
}
return $this->renderObject($conf);
}
protected function renderInline(array $conf): string
{
$resource = $this->resolveResource($conf);
[$width, $height, $isDefaultWidth, $isDefaultHeight] = $this->getDimensions($conf);
$content = $svgContent = '';
if ($resource instanceof SystemResourceInterface) {
try {
$svgContent = $resource->getContents();
} catch (SystemResourceDoesNotExistException) {
}
}
if ($svgContent !== '') {
try {
$document = $this->svgDocumentFactory->fromStringAndSanitize($svgContent);
if (!$isDefaultWidth) {
$document->documentElement->setAttribute('width', (string)$width);
}
if (!$isDefaultHeight) {
$document->documentElement->setAttribute('height', (string)$height);
}
$content = $this->svgDocumentService->toInlineMarkup($document);
} catch (InvalidSvgException) {
$content = '';
}
} else {
$value = $this->cObj->stdWrapValue('value', $conf);
if (!empty($value)) {
$content = [];
$content[] = '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="' . (int)$width . '" height="' . (int)$height . '">';
$content[] = $value;
$content[] = '</svg>';
$content = implode(LF, $content);
}
}
if (isset($conf['stdWrap.'])) {
$content = $this->cObj->stdWrap($content, $conf['stdWrap.']);
}
return $content;
}
/**
* Render the SVG as <object> tag
*/
protected function renderObject(array $conf): string
{
$resource = $this->resolveResource($conf);
[$width, $height] = $this->getDimensions($conf);
$content = [];
if ($resource !== null) {
$uri = $this->resourcePublisher->generateUri($resource, $this->request);
$content[] = '<!--[if IE]>';
$content[] = ' <object src="' . htmlspecialchars($uri) . '" classid="image/svg+xml" width="' . (int)$width . '" height="' . (int)$height . '">';
$content[] = '<![endif]-->';
$content[] = '<!--[if !IE]>-->';
$content[] = ' <object data="' . htmlspecialchars($uri) . '" type="image/svg+xml" width="' . (int)$width . '" height="' . (int)$height . '">';
$content[] = '<!--<![endif]-->';
$content[] = '</object>';
}
$content = implode(LF, $content);
if (isset($conf['stdWrap.'])) {
$content = $this->cObj->stdWrap($content, $conf['stdWrap.']);
}
return $content;
}
protected function resolveResource(array $conf): ?PublicResourceInterface
{
try {
$resourceIdentifier = (string)$this->cObj->stdWrapValue('src', $conf);
return $this->resourceFactory->createPublicResource($resourceIdentifier);
} catch (SystemResourceException) {
return null;
}
}
protected function getDimensions(array $conf): array
{
$isDefaultWidth = false;
$isDefaultHeight = false;
$width = $this->cObj->stdWrapValue('width', $conf);
$height = $this->cObj->stdWrapValue('height', $conf);
if (empty($width)) {
$isDefaultWidth = true;
$width = 600;
}
if (empty($height)) {
$isDefaultHeight = true;
$height = 400;
}
return [$width, $height, $isDefaultWidth, $isDefaultHeight];
}
}
@@ -0,0 +1,48 @@
<?php
/*
* 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\Frontend\ContentObject;
/**
* Contains TEXT class object.
*/
class TextContentObject extends AbstractContentObject
{
/**
* Rendering the cObject, TEXT
*
* @param mixed $conf Array of TypoScript properties (marked as "mixed" currently because we don't know what we're receiving)
* @return string Output
*/
public function render($conf = [])
{
if (!is_array($conf)) {
return '';
}
$content = '';
if (isset($conf['value'])) {
$content = $conf['value'];
unset($conf['value']);
}
if (isset($conf['value.'])) {
$content = $this->cObj->stdWrap($content, $conf['value.']);
unset($conf['value.']);
}
if (!empty($conf)) {
$content = $this->cObj->stdWrap($content, $conf);
}
return $content;
}
}
@@ -0,0 +1,66 @@
<?php
/*
* 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\Frontend\ContentObject;
use Psr\Log\LogLevel;
use TYPO3\CMS\Core\TimeTracker\TimeTracker;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Contains USER class object.
*/
class UserContentObject extends AbstractContentObject
{
/**
* Rendering the cObject, USER
*
* @param array $conf Array of TypoScript properties
* @return string Output
*/
public function render($conf = [])
{
if (empty($conf)) {
$this->getTimeTracker()->setTSlogMessage('USER without configuration.', LogLevel::WARNING);
return '';
}
$content = '';
if ($this->cObj->getUserObjectType() === false) {
// Render this if we are a delayed non cached object
$this->cObj->setUserObjectType(ContentObjectRenderer::OBJECTTYPE_USER);
}
$tempContent = $this->cObj->callUserFunction($conf['userFunc'] ?? '', $conf, '');
if ($this->cObj->doConvertToUserIntObject) {
$this->cObj->doConvertToUserIntObject = false;
$content = $this->cObj->cObjGetSingle('USER_INT', $conf);
} else {
$content .= $tempContent;
// Only executed when the element is not converted to USER_INT
if (isset($conf['stdWrap.'])) {
$content = $this->cObj->stdWrap($content, $conf['stdWrap.']);
}
}
$this->cObj->setUserObjectType(false);
return $content;
}
/**
* @return TimeTracker
*/
protected function getTimeTracker()
{
return GeneralUtility::makeInstance(TimeTracker::class);
}
}
@@ -0,0 +1,45 @@
<?php
/*
* 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\Frontend\ContentObject;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* Contains USER_INT class object.
*/
class UserInternalContentObject extends AbstractContentObject
{
/**
* Rendering the cObject, USER_INT
*
* @param array $conf Array of TypoScript properties
* @return string Output
*/
public function render($conf = [])
{
$this->cObj->setUserObjectType(ContentObjectRenderer::OBJECTTYPE_USER_INT);
$substKey = 'INT_SCRIPT.' . md5(StringUtility::getUniqueId());
$pageParts = $this->request->getAttribute('frontend.page.parts');
$pageParts->addNotCachedContentElement([
'substKey' => $substKey,
'conf' => $conf,
'cObjData' => serialize($this->cObj->getState()),
'type' => 'FUNC',
]);
$this->cObj->setUserObjectType(false);
return '<!--' . $substKey . '-->';
}
}