'event', 'cacheRead' => 'hook', // this is a placeholder for checking if the content is available in cache 'setContentToCurrent' => 'boolean', 'setContentToCurrent.' => 'array', 'addPageCacheTags' => 'string', 'addPageCacheTags.' => 'array', 'setCurrent' => 'string', 'setCurrent.' => 'array', 'lang.' => 'array', 'data' => 'getText', 'data.' => 'array', 'field' => 'fieldName', 'field.' => 'array', 'current' => 'boolean', 'current.' => 'array', 'cObject' => 'cObject', 'cObject.' => 'array', 'numRows.' => 'array', 'preUserFunc' => 'functionName', AfterStdWrapFunctionsInitializedEvent::class => 'event', 'override' => 'string', 'override.' => 'array', 'preIfEmptyListNum' => 'listNum', 'preIfEmptyListNum.' => 'array', 'ifNull' => 'string', 'ifNull.' => 'array', 'ifEmpty' => 'string', 'ifEmpty.' => 'array', 'ifBlank' => 'string', 'ifBlank.' => 'array', 'listNum' => 'listNum', 'listNum.' => 'array', 'trim' => 'boolean', 'trim.' => 'array', 'strPad.' => 'array', 'stdWrap' => 'stdWrap', 'stdWrap.' => 'array', BeforeStdWrapFunctionsExecutedEvent::class => 'event', 'required' => 'boolean', 'required.' => 'array', 'if.' => 'array', 'fieldRequired' => 'fieldName', 'fieldRequired.' => 'array', 'csConv' => 'string', 'csConv.' => 'array', 'parseFunc' => 'objectpath', 'parseFunc.' => 'array', 'HTMLparser' => 'boolean', 'HTMLparser.' => 'array', 'split.' => 'array', 'replacement.' => 'array', 'prioriCalc' => 'boolean', 'prioriCalc.' => 'array', 'char' => 'integer', 'char.' => 'array', 'intval' => 'boolean', 'intval.' => 'array', 'hash' => 'string', 'hash.' => 'array', 'round' => 'boolean', 'round.' => 'array', 'numberFormat.' => 'array', 'expandList' => 'boolean', 'expandList.' => 'array', 'date' => 'dateconf', 'date.' => 'array', 'strtotime' => 'strtotimeconf', 'strtotime.' => 'array', 'strftime' => 'strftimeconf', 'strftime.' => 'array', 'formattedDate' => 'formattedDateconf', 'formattedDate.' => 'array', 'age' => 'boolean', 'age.' => 'array', 'case' => 'case', 'case.' => 'array', 'bytes' => 'boolean', 'bytes.' => 'array', 'substring' => 'parameters', 'substring.' => 'array', 'cropHTML' => 'crop', 'cropHTML.' => 'array', 'stripHtml' => 'boolean', 'stripHtml.' => 'array', 'crop' => 'crop', 'crop.' => 'array', 'rawUrlEncode' => 'boolean', 'rawUrlEncode.' => 'array', 'htmlSpecialChars' => 'boolean', 'htmlSpecialChars.' => 'array', 'encodeForJavaScriptValue' => 'boolean', 'encodeForJavaScriptValue.' => 'array', 'doubleBrTag' => 'string', 'doubleBrTag.' => 'array', 'br' => 'boolean', 'br.' => 'array', 'brTag' => 'string', 'brTag.' => 'array', 'encapsLines.' => 'array', 'keywords' => 'boolean', 'keywords.' => 'array', 'innerWrap' => 'wrap', 'innerWrap.' => 'array', 'innerWrap2' => 'wrap', 'innerWrap2.' => 'array', 'preCObject' => 'cObject', 'preCObject.' => 'array', 'postCObject' => 'cObject', 'postCObject.' => 'array', 'wrapAlign' => 'align', 'wrapAlign.' => 'array', 'typolink.' => 'array', 'wrap' => 'wrap', 'wrap.' => 'array', 'noTrimWrap' => 'wrap', 'noTrimWrap.' => 'array', 'wrap2' => 'wrap', 'wrap2.' => 'array', 'dataWrap' => 'dataWrap', 'dataWrap.' => 'array', 'prepend' => 'cObject', 'prepend.' => 'array', 'append' => 'cObject', 'append.' => 'array', 'wrap3' => 'wrap', 'wrap3.' => 'array', 'orderedStdWrap' => 'stdWrap', 'orderedStdWrap.' => 'array', 'outerWrap' => 'wrap', 'outerWrap.' => 'array', 'insertData' => 'boolean', 'insertData.' => 'array', 'postUserFunc' => 'functionName', 'postUserFuncInt' => 'functionName', 'prefixComment' => 'string', 'prefixComment.' => 'array', 'htmlSanitize' => 'boolean', 'htmlSanitize.' => 'array', 'cacheStore' => 'hook', // this is a placeholder for storing the content in cache AfterStdWrapFunctionsExecutedEvent::class => 'event', 'debug' => 'boolean', 'debug.' => 'array', 'debugFunc' => 'boolean', 'debugFunc.' => 'array', 'debugData' => 'boolean', 'debugData.' => 'array', ]; /** * Loaded with the current data-record. * * If the instance of this class is used to render records from the database those records are found in this array. * The function stdWrap has TypoScript properties that fetch field-data from this array. */ public array $data = []; protected string $table = ''; /** * Used by the parseFunc function and is loaded with tag-parameters when parsing tags. */ public array $parameters = []; public string $currentValKey = 'currentValue_kidjls9dksoje'; /** * This is set to the [table]:[uid] of the record delivered in the $data-array, if the cObjects CONTENT or RECORD is in operation. */ public string $currentRecord = ''; /** * @internal */ protected array $parentRecord = []; /** * Current file object during iterations over files. */ protected File|FileReference|Folder|FileInterface|FolderInterface|null $currentFile = null; /** * Set to true by doConvertToUserIntObject() if USER object wants to become USER_INT. */ public bool $doConvertToUserIntObject = false; /** * Indicates current object type. Can hold one of OBJECTTYPE_ constants or false. * The value is set and reset inside USER() function. Any time outside of * USER() it is false. */ protected int|false $userObjectType = false; /** * Per-nesting-level stop flags for stdWrap processing. Keyed by $stdWrapNestingLevel. * Set to true to abort remaining stdWrap properties at that level (e.g. when "if", "required" * or "ifEmpty" conditions are not met). Isolates stop conditions so an inner stdWrap call * cannot accidentally abort an outer one. */ protected array $stopRendering = []; /** * Current stdWrap nesting depth, used as the key into $stopRendering. */ protected int $stdWrapNestingLevel = 0; private ?ServerRequestInterface $request = null; public function __construct( private readonly ContainerInterface $container, private readonly Context $context, private readonly LoggerInterface $logger, private readonly EventDispatcherInterface $eventDispatcher, private readonly ConnectionPool $connectionPool, private readonly ResourceFactory $resourceFactory, #[Autowire(expression: 'service("features").isFeatureEnabled("frontend.cache.autoTagging")')] private readonly bool $autoTagging, private readonly CacheLifetimeCalculator $cacheLifetimeCalculator, #[Autowire(service: 'cache.hash')] private readonly FrontendInterface $cacheHash, private readonly HashService $hashService, private readonly FrontendUrlPrefix $frontendUrlPrefix, private readonly Locales $locales, private readonly TypoScriptService $typoScriptService, private readonly SanitizerBuilderFactory $sanitizerBuilderFactory, private readonly TextCropper $textCropper, private readonly HtmlCropper $htmlCropper, private readonly LinkVarsCalculator $linkVarsCalculator, private readonly SystemResourceFactory $systemResourceFactory, private readonly SystemResourcePublisherInterface $systemResourcePublisher, private readonly PageLayoutResolver $pageLayoutResolver, private readonly LanguageServiceFactory $languageServiceFactory, private readonly FlexFormTools $flexFormTools, private readonly LinkFactory $linkFactory, // TimeTracker is a stateful singleton, we would usually not inject this. This // instance however is set up early in middlewares and carried around with its accumulating // state throughout entire FE rendering. As such, it is ok to get it injected here, // similar to PageRenderer and Context, which are designed in a similar way. private readonly TimeTracker $timeTracker, private readonly TcaSchemaFactory $tcaSchemaFactory, ) {} public function setRequest(ServerRequestInterface $request): void { $this->request = $request; } /** * Used strictly internally to preserve state when dealing with non-cached elements. * * @internal These are not the methods you are looking for. */ public function getState(): array { // Request is of course NOT returned! $state = [ 'data' => $this->data, 'table' => $this->table, 'parameters' => $this->parameters, 'currentValKey' => $this->currentValKey, 'currentRecord' => $this->currentRecord, 'parentRecord' => $this->parentRecord, 'doConvertToUserIntObject' => $this->doConvertToUserIntObject, 'userObjectType' => $this->userObjectType, 'stopRendering' => $this->stopRendering, 'stdWrapNestingLevel' => $this->stdWrapNestingLevel, 'currentFile' => null, ]; if ($this->currentFile instanceof FileReference) { $state['currentFile'] = 'FileReference:' . $this->currentFile->getUid(); } elseif ($this->currentFile instanceof File) { $state['currentFile'] = 'File:' . $this->currentFile->getIdentifier(); } return $state; } /** * Counterpart of getState() * * @internal */ public function updateState(array $state): void { $this->data = $state['data']; $this->table = $state['table']; $this->parameters = $state['parameters']; $this->currentValKey = $state['currentValKey']; $this->currentRecord = $state['currentRecord']; $this->parentRecord = $state['parentRecord']; $this->doConvertToUserIntObject = $state['doConvertToUserIntObject']; $this->userObjectType = $state['userObjectType']; $this->stopRendering = $state['stopRendering']; $this->stdWrapNestingLevel = $state['stdWrapNestingLevel']; $this->currentFile = null; if (is_string($state['currentFile'])) { [$objectType, $identifier] = explode(':', $state['currentFile'], 2); try { if ($objectType === 'File') { $this->currentFile = $this->resourceFactory->retrieveFileOrFolderObject($identifier); } elseif ($objectType === 'FileReference') { $this->currentFile = $this->resourceFactory->getFileReferenceObject((int)$identifier); } } catch (ResourceDoesNotExistException $e) { // Keep $this->currentFile null } } } /** * After making an instance of the class, call this function and pass to it a * database record and the tablename from where the record is from. That will * then become the "current" record loaded into memory and accessed by the .fields * property found in eg. stdWrap. * * @param string $table The table that the data record is from. */ public function start(array $data, string $table = ''): void { $this->data = $data; $this->table = $table; $this->currentRecord = $table !== '' ? $table . ':' . ($this->data['uid'] ?? '') : ''; $this->parameters = []; $this->eventDispatcher->dispatch(new AfterContentObjectRendererInitializedEvent($this)); if ($this->currentRecord !== '' && $this->autoTagging && $this->table !== 'pages') { // Page lifetime for the requested page is calculated in RequestHandler, taking // cache_period and TypoScript into account. // When start() is called here, it can be the requested page record, which is handled // already, OR it is a page record *used* on this page, for instance from a menu rendering. // In the latter case, we do *not* want to reduce the lifetime of the rendered page down // to the cache_period of that content related page record. We can thus skip 'pages' records // here altogether. $lifetime = $this->cacheLifetimeCalculator->calculateLifetimeForRow($this->table, $this->data); $cacheTags = [ sprintf('%s_%s', $this->table, ($this->data['uid'] ?? 0)), ]; if ((int)($this->data['_LOCALIZED_UID'] ?? 0) > 0) { $cacheTags[] = sprintf('%s_%s', $this->table, (int)$this->data['_LOCALIZED_UID']); } $this->getRequest()->getAttribute('frontend.cache.collector')?->addCacheTags( ...array_map( static fn(string $cacheTag) => new CacheTag($cacheTag, $lifetime), $cacheTags, ), ); } } /** * Returns the current table * * @return string */ public function getCurrentTable() { return $this->table; } /** * @param array $data The record array * @param string $currentRecord Format: "table:uid" * @internal */ public function setParent($data, string $currentRecord): void { $this->parentRecord = [ 'data' => $data, 'currentRecord' => $currentRecord, ]; } /** * Returns the "current" value. * The "current" value is just an internal variable that can be used by functions to pass a single value on to another * function later in the TypoScript processing. It's like "load accumulator" in the good old C64 days... basically a "register" * you can use as you like. The TSref will tell if functions are setting this value before calling some other object so that * you know if it holds any special information. * * @return mixed The "current" value */ public function getCurrentVal() { return $this->data[$this->currentValKey] ?? null; } /** * Sets the "current" value. * * @param mixed $value The variable that you want to set as "current */ public function setCurrentVal($value): void { $this->data[$this->currentValKey] = $value; } /** * Rendering of a "numerical array" of cObjects from TypoScript * Will call ->cObjGetSingle() for each cObject found and accumulate the output. * * @param mixed $setup array with cObjects as values. * @param string $addKey A prefix for the debugging information * @return string Rendered output from the cObjects in the array. * @see cObjGetSingle() */ public function cObjGet($setup, $addKey = ''): string { if (!is_array($setup)) { return ''; } return implode('', $this->cObjGetSeparated($setup, $addKey)); } /** * Rendering of a "numerical array" of cObjects from TypoScript * Will call ->cObjGetSingle() for each cObject found. * * @return list */ public function cObjGetSeparated(?array $setup, string $addKey = ''): array { if ($setup === null || $setup === []) { return []; } $sKeyArray = ArrayUtility::filterAndSortByNumericKeys($setup); $contentObjects = []; foreach ($sKeyArray as $theKey) { $theValue = $setup[$theKey]; if ((int)$theKey && !str_contains($theKey, '.')) { $conf = $setup[$theKey . '.'] ?? []; $contentObjects[] = $this->cObjGetSingle($theValue, $conf, $addKey . $theKey); } } return $contentObjects; } /** * Renders a content object * * @param string $name The content object name, eg. "TEXT" or "USER" or "IMAGE" * @param mixed $conf The array with TypoScript properties for the content object * @param string $TSkey A string label used for the internal debugging tracking. * @return string cObject output * @throws \UnexpectedValueException */ public function cObjGetSingle(string $name, $conf, $TSkey = '__') { $name = trim($name); if ($this->timeTracker->LR) { $this->timeTracker->push($TSkey, $name); } $fullConfigArray = [ 'tempKey' => $name, 'tempKey.' => is_array($conf) ? $conf : [], ]; // Resolve '=<' operator if needed $fullConfigArray = $this->mergeTSRef($fullConfigArray, 'tempKey'); $contentObject = $this->getContentObject($fullConfigArray['tempKey']); $content = ''; if ($contentObject) { $content = $this->render($contentObject, $fullConfigArray['tempKey.']); } if ($this->timeTracker->LR) { $this->timeTracker->pull($content); } return $content; } /** * Returns a new content object of type $name. * * @throws ContentRenderingException */ public function getContentObject(string $name): ?AbstractContentObject { $contentObjectFactory = $this->container->get(ContentObjectFactory::class); return $contentObjectFactory->getContentObject($name, $this->getRequest(), $this); } /** * Renders a content object by taking exception and cache handling * into consideration * * @param AbstractContentObject $contentObject Content object instance * @param array $configuration Array of TypoScript properties * * @throws ContentRenderingException * @throws \Exception */ public function render(AbstractContentObject $contentObject, $configuration = []): string { $content = ''; // Evaluate possible cache and return $cacheConfiguration = $configuration['cache.'] ?? null; if ($cacheConfiguration !== null) { unset($configuration['cache.']); $cache = $this->getFromCache($cacheConfiguration); if ($cache !== false) { return $cache; } } // Render content try { $content .= $contentObject->render($configuration); } catch (ContentRenderingException $exception) { // Content rendering Exceptions indicate a critical problem which should not be // caught e.g. when something went wrong with Exception handling itself throw $exception; } catch (\Throwable $exception) { $exceptionHandler = $this->createExceptionHandler($configuration); if ($exceptionHandler === null) { throw $exception; } // Ensure that the exception handler receives an \Exception instance, // which is required by the \ExceptionHandlerInterface. if (!$exception instanceof \Exception) { $exception = new \Exception($exception->getMessage(), 1698347363, $exception); } $content = $exceptionHandler->handle($exception, $contentObject, $configuration); } // Store cache if ($cacheConfiguration !== null && $this->getRequest()->getAttribute('frontend.cache.instruction')->isCachingAllowed()) { $key = $this->calculateCacheKey($cacheConfiguration); if (!empty($key)) { $tags = $this->calculateCacheTags($cacheConfiguration); $cacheLifetime = $this->calculateCacheLifetime($cacheConfiguration); $cachedData = [ 'content' => $content, 'cacheTags' => $tags, ]; $this->cacheHash->set($key, $cachedData, $tags, $cacheLifetime); // If no tags are given, we restrict the maximum lifetime of the cache to the lifetime of the cache entry. if ($tags === []) { $this->getRequest()->getAttribute('frontend.cache.collector')->restrictMaximumLifetime($cacheLifetime); } $this->getRequest()->getAttribute('frontend.cache.collector')->addCacheTags( ...array_map(fn(string $tag) => new CacheTag($tag, $cacheLifetime), $tags) ); } } return $content; } /** * Creates the content object exception handler from local content object configuration * or, from global configuration if not explicitly disabled in local configuration * * @throws ContentRenderingException */ protected function createExceptionHandler(array $configuration): ?ExceptionHandlerInterface { $exceptionHandler = null; $exceptionHandlerClassName = $this->determineExceptionHandlerClassName($configuration); if (!empty($exceptionHandlerClassName)) { $exceptionHandler = GeneralUtility::makeInstance($exceptionHandlerClassName); if (!$exceptionHandler instanceof ExceptionHandlerInterface) { throw new ContentRenderingException('An exception handler was configured but the class does not exist or does not implement the ExceptionHandlerInterface', 1403653369); } $exceptionHandler->setConfiguration($this->mergeExceptionHandlerConfiguration($configuration)); } return $exceptionHandler; } /** * Determine exception handler class name from global and content object configuration */ protected function determineExceptionHandlerClassName(array $configuration): ?string { $typoScriptConfigArray = $this->getRequest()->getAttribute('frontend.typoscript')->getConfigArray(); $exceptionHandlerClassName = null; if (!isset($typoScriptConfigArray['contentObjectExceptionHandler'])) { if (Environment::getContext()->isProduction()) { $exceptionHandlerClassName = '1'; } } else { $exceptionHandlerClassName = $typoScriptConfigArray['contentObjectExceptionHandler']; } if (isset($configuration['exceptionHandler'])) { $exceptionHandlerClassName = $configuration['exceptionHandler']; } if ($exceptionHandlerClassName === '1') { $exceptionHandlerClassName = ProductionExceptionHandler::class; } return $exceptionHandlerClassName; } /** * Merges global exception handler configuration with the one from the content object * and returns the merged exception handler configuration */ protected function mergeExceptionHandlerConfiguration(array $configuration): array { $exceptionHandlerConfiguration = []; $typoScriptConfigArray = $this->getRequest()->getAttribute('frontend.typoscript')->getConfigArray(); if (!empty($typoScriptConfigArray['contentObjectExceptionHandler.'])) { $exceptionHandlerConfiguration = $typoScriptConfigArray['contentObjectExceptionHandler.']; } if (!empty($configuration['exceptionHandler.'])) { $exceptionHandlerConfiguration = array_replace_recursive($exceptionHandlerConfiguration, $configuration['exceptionHandler.']); } return $exceptionHandlerConfiguration; } /** * Retrieves a type of object called as USER or USER_INT. Object can detect their * type by using this call. It returns OBJECTTYPE_USER_INT or OBJECTTYPE_USER depending on the * current object execution. In all other cases it will return FALSE to indicate * a call out of context. * * @return int|false One of OBJECTTYPE_ class constants or false */ public function getUserObjectType(): int|false { return $this->userObjectType; } /** * Sets the user object type */ public function setUserObjectType(int|false $userObjectType): void { $this->userObjectType = $userObjectType; } /** * Requests the current USER object to be converted to USER_INT. */ public function convertToUserIntObject(): void { if ($this->userObjectType !== self::OBJECTTYPE_USER) { $this->timeTracker->setTSlogMessage(self::class . '::convertToUserIntObject() is called in the wrong context or for the wrong object type', LogLevel::WARNING); } else { $this->doConvertToUserIntObject = true; } } /** * Returns all parents of the given PID (Page UID) list * * @param string $pidList A list of page Content-Element PIDs (Page UIDs) / stdWrap * @param array $pidConf stdWrap array for the list * @return string A list of PIDs * @internal */ public function getSlidePids($pidList, array $pidConf = []): string { $pidList = $pidConf !== [] ? trim((string)$this->stdWrap($pidList, $pidConf)) : trim($pidList); if ($pidList === '') { $pidList = 'this'; } $pageRepository = GeneralUtility::makeInstance(PageRepository::class); if (trim($pidList)) { $contentPid = $this->getRequest()->getAttribute('frontend.page.information')->getContentFromPid(); $listArr = GeneralUtility::intExplode(',', str_replace('this', (string)$contentPid, $pidList)); $listArr = $this->checkPidArray($listArr); $pidList = []; foreach ($listArr as $uid) { $page = $pageRepository->getPage((int)$uid); if (!$page['is_siteroot']) { $pidList[] = $page['pid']; } } return implode(',', $pidList); } return ''; } /** * Wraps the input string in link-tags that opens the image in a new window. * * @param string $string String to wrap, probably an tag * @param string|File|FileReference $imageFile The original image file * @param array $conf TypoScript properties for the "imageLinkWrap" function * @return string The input string, $string, wrapped as configured. * @internal This method should be used within TYPO3 Core only */ public function imageLinkWrap($string, $imageFile, $conf) { $string = (string)$string; $enable = $this->stdWrapValue('enable', $conf); if (!$enable) { return $string; } $content = (string)$this->typoLink($string, $conf['typolink.'] ?? []); if (isset($conf['file.']) && is_scalar($imageFile)) { $imageFile = $this->stdWrap((string)$imageFile, $conf['file.']); } if ($imageFile instanceof File) { $file = $imageFile; } elseif ($imageFile instanceof FileReference) { $file = $imageFile->getOriginalFile(); } elseif (MathUtility::canBeInterpretedAsInteger($imageFile)) { $file = $this->resourceFactory->getFileObject((int)$imageFile); } else { $file = $this->resourceFactory->getFileObjectFromCombinedIdentifier($imageFile); } // Create imageFileLink if not created with typolink if ($content === $string && $file !== null) { $parameterNames = ['width', 'height', 'effects', 'bodyTag', 'title', 'wrap', 'crop']; $parameters = []; $sample = $this->stdWrapValue('sample', $conf); if ($sample) { $parameters['sample'] = 1; } foreach ($parameterNames as $parameterName) { if (isset($conf[$parameterName . '.'])) { $conf[$parameterName] = $this->stdWrap($conf[$parameterName] ?? '', $conf[$parameterName . '.'] ?? []); } if (isset($conf[$parameterName]) && $conf[$parameterName]) { $parameters[$parameterName] = $conf[$parameterName]; } } $parametersEncoded = base64_encode((string)json_encode($parameters)); $hmac = $this->hashService->hmac(implode('|', [$file->getUid(), $parametersEncoded]), 'tx_cms_showpic', HashAlgo::SHA3_256); $params = '&md5=' . $hmac; foreach (str_split($parametersEncoded, 64) as $index => $chunk) { $params .= '¶meters' . rawurlencode('[') . $index . rawurlencode(']') . '=' . rawurlencode($chunk); } $absRefPrefix = $this->frontendUrlPrefix->getUrlPrefix($this->getRequest()); $url = $absRefPrefix . 'index.php?eID=tx_cms_showpic&file=' . $file->getUid() . $params; $directImageLink = $this->stdWrapValue('directImageLink', $conf); if ($directImageLink) { $imgResourceConf = [ 'file' => $imageFile, 'file.' => $conf, ]; $url = $this->cObjGetSingle('IMG_RESOURCE', $imgResourceConf); if (!$url) { // Either imagemagick/gm is not available or image URL could not be resolved due to invalid image file if ($imageFile instanceof File || $imageFile instanceof FileReference) { $url = $imageFile->getPublicUrl(); } else { $url = $imageFile; } } } $target = (string)$this->stdWrapValue('target', $conf); if ($target === '') { $target = 'thePicture'; } $a1 = ''; $a2 = ''; $conf['JSwindow'] = $this->stdWrapValue('JSwindow', $conf); if ($conf['JSwindow']) { $altUrl = $this->stdWrapValue('altUrl', $conf['JSwindow.'] ?? []); if ($altUrl) { $url = $altUrl . (($conf['JSwindow.']['altUrl_noDefaultParams'] ?? false) ? '' : '?file=' . rawurlencode((string)$imageFile) . $params); } if ($file instanceof ProcessedFile) { // TypoScript record delivered like 'file = fileadmin/something.jpg' which can result // in an already processed file. Process the original file with the proper config now. $file = $file->getOriginalFile(); } $processedFile = $file->process(ProcessedFile::CONTEXT_IMAGECROPSCALEMASK, $conf); $JSwindowExpand = $this->stdWrapValue('expand', $conf['JSwindow.'] ?? []); $offset = GeneralUtility::intExplode(',', $JSwindowExpand . ','); $newWindow = $this->stdWrapValue('newWindow', $conf['JSwindow.'] ?? []); $params = [ 'width' => ($processedFile->getProperty('width') + $offset[0]), 'height' => ($processedFile->getProperty('height') + $offset[1]), 'status' => '0', 'menubar' => '0', ]; // params override existing parameters from above, or add more $windowParams = (string)$this->stdWrapValue('params', $conf['JSwindow.'] ?? []); $windowParams = explode(',', $windowParams); foreach ($windowParams as $windowParam) { $windowParamParts = explode('=', $windowParam); $paramKey = $windowParamParts[0]; $paramValue = $windowParamParts[1] ?? null; if ($paramKey === '') { continue; } if ($paramValue !== '') { $params[$paramKey] = $paramValue; } else { unset($params[$paramKey]); } } $paramString = ''; foreach ($params as $paramKey => $paramValue) { $paramString .= htmlspecialchars($paramKey) . '=' . htmlspecialchars((string)$paramValue) . ','; } $attrs = [ 'href' => (string)$url, 'data-window-url' => $url, 'data-window-target' => $newWindow ? md5((string)$url) : 'thePicture', 'data-window-features' => rtrim($paramString, ','), 'target' => $target, ]; $typoScriptConfigArray = $this->getRequest()->getAttribute('frontend.typoscript')->getConfigArray(); $a1 = sprintf( '', GeneralUtility::implodeAttributes($attrs, true), trim($typoScriptConfigArray['ATagParams'] ?? '') ? ' ' . trim($typoScriptConfigArray['ATagParams']) : '' ); $a2 = ''; $this->addDefaultFrontendJavaScript($this->getRequest()); } else { $conf['linkParams.']['directImageLink'] = (bool)($conf['directImageLink'] ?? false); $conf['linkParams.']['parameter'] = $url; $string = (string)$this->typoLink($string, $conf['linkParams.']); } if (isset($conf['stdWrap.'])) { $string = (string)$this->stdWrap($string, $conf['stdWrap.']); } $content = $a1 . $string . $a2; } return $content; } /** * Sets the SYS_LASTCHANGED timestamp if input timestamp is larger than current value. * The SYS_LASTCHANGED timestamp can be used by various caching/indexing applications to determine if the page has new content. * Therefore, you should call this function with the last-changed timestamp of any element you display. * * @param RecordInterface|int|string|float|null $item a record objet or a Unix timestamp (number of seconds since 1970) */ public function lastChanged(RecordInterface|int|string|float|null $item): void { if (MathUtility::canBeInterpretedAsInteger($item)) { $item = (int)$item; } elseif ($item instanceof Record) { $item = $item->getSystemProperties()->getLastUpdatedAt()->getTimestamp(); } else { return; } $pageParts = $this->getRequest()->getAttribute('frontend.page.parts'); if ($item > $pageParts->getLastChanged()) { $pageParts->setLastChanged($item); } } /** * Sets the current file object during iterations over files. */ public function setCurrentFile(File|FileReference|Folder|FileInterface|FolderInterface|null $fileObject): void { $this->currentFile = $fileObject; } /** * Gets the current file object during iterations over files. */ public function getCurrentFile(): File|FileReference|Folder|FileInterface|FolderInterface|null { return $this->currentFile; } /** * The "stdWrap" function. This is the implementation of what is known as "stdWrap properties" in TypoScript. * Basically "stdWrap" performs some processing of a value based on properties in the input $conf array, holding * the TypoScript "stdWrap properties". * * @param string $content Input value undergoing processing in this function. * @param mixed $conf TypoScript "stdWrap properties". - should be enforced to be an array at some point * @return string|null The processed input value */ public function stdWrap($content = '', $conf = []) { $content = (string)$content; if (!is_array($conf) || $conf === []) { return $content; } // Activate the stdWrap PSR-14 Events - They will be executed // as stdWrap functions, based on the STD_WRAP_ORDER constant. $conf[BeforeStdWrapFunctionsInitializedEvent::class] = 1; $conf[AfterStdWrapFunctionsInitializedEvent::class] = 1; $conf[BeforeStdWrapFunctionsExecutedEvent::class] = 1; $conf[AfterStdWrapFunctionsExecutedEvent::class] = 1; // Cache handling if (is_array($conf['cache.'] ?? null)) { $conf['cache.']['key'] = $this->stdWrapValue('key', $conf['cache.']); $conf['cache.']['tags'] = $this->stdWrapValue('tags', $conf['cache.']); $conf['cache.']['lifetime'] = $this->stdWrapValue('lifetime', $conf['cache.']); $conf['cacheRead'] = 1; $conf['cacheStore'] = 1; } // The configuration is sorted and filtered by intersection with the defined STD_WRAP_ORDER. $sortedConf = array_keys(array_intersect_key(self::STD_WRAP_ORDER, $conf)); // Functions types that should not make use of nested stdWrap function calls to avoid conflicts with internal TypoScript used by these functions $stdWrapDisabledFunctionTypes = 'cObject,functionName,stdWrap'; // Additional Array to check whether a function has already been executed $isExecuted = []; // Additional switch to make sure 'required', 'if' and 'fieldRequired' // will still stop rendering immediately in case they return FALSE $this->stdWrapNestingLevel++; $this->stopRendering[$this->stdWrapNestingLevel] = false; // execute each function in the predefined order foreach ($sortedConf as $stdWrapName) { // eliminate the second key of a pair 'key'|'key.' to make sure functions get called only once and check if rendering has been stopped if (!isset($isExecuted[$stdWrapName]) && !$this->stopRendering[$this->stdWrapNestingLevel]) { $functionName = rtrim($stdWrapName, '.'); $functionProperties = $functionName . '.'; $functionType = self::STD_WRAP_ORDER[$functionName] ?? ''; // If there is any code on the next level, check if it contains "official" stdWrap functions // if yes, execute them first - will make each function stdWrap aware // so additional stdWrap calls within the functions can be removed, since the result will be the same if (!empty($conf[$functionProperties]) && !GeneralUtility::inList($stdWrapDisabledFunctionTypes, $functionType)) { if (array_intersect_key(self::STD_WRAP_ORDER, $conf[$functionProperties])) { // Check if there's already content available before processing // any ifEmpty or ifBlank stdWrap properties if (($functionName === 'ifBlank' && $content !== '') || ($functionName === 'ifEmpty' && !empty(trim((string)$content)))) { continue; } $conf[$functionName] = $this->stdWrap($conf[$functionName] ?? '', $conf[$functionProperties]); } } // Check if key is still containing something, since it might have been changed by next level stdWrap before if ((isset($conf[$functionName]) || ($conf[$functionProperties] ?? null)) && ($functionType !== 'boolean' || ($conf[$functionName] ?? null)) ) { // Get just that part of $conf that is needed for the particular function $singleConf = [ $functionName => $conf[$functionName] ?? null, $functionProperties => $conf[$functionProperties] ?? null, ]; // Hand over the whole $conf array to the hooks if ($functionType === 'hook') { $singleConf = $conf; } // Add both keys - with and without the dot - to the set of executed functions $isExecuted[$functionName] = true; $isExecuted[$functionProperties] = true; if ($functionType === 'event') { // @phpstan-ignore-next-line phpstan does not understand $functionName is only called on 'event' types $content = $this->eventDispatcher->dispatch(new $functionName($content, $conf, $this))->getContent(); } else { // Call the function with the prefix stdWrap_ to make sure nobody can execute functions just by adding their name to the TS Array $functionName = 'stdWrap_' . $functionName; // @phpstan-ignore-next-line phpstan complains about some methods not having a second argument, which is not required/useful - doing reflection here would be too intense. $content = $this->{$functionName}($content, $singleConf); } } elseif ($functionType === 'boolean' && !($conf[$functionName] ?? null)) { $isExecuted[$functionName] = true; $isExecuted[$functionProperties] = true; } } } unset($this->stopRendering[$this->stdWrapNestingLevel]); $this->stdWrapNestingLevel--; return $content; } /** * Gets a configuration value by passing them through stdWrap first and taking a default value if stdWrap doesn't yield a result. * * @param string $key The config variable key (from TS array). * @param array $config The TypoScript array. * @param string|int|bool|null $defaultValue Optional default value. * @return string|int|bool|null Value of the config variable */ public function stdWrapValue($key, array $config, $defaultValue = '') { if (isset($config[$key])) { if (!isset($config[$key . '.'])) { return $config[$key]; } } elseif (isset($config[$key . '.'])) { $config[$key] = ''; } else { return $defaultValue; } $stdWrapped = $this->stdWrap($config[$key], $config[$key . '.']); // The string "0" should be returned. return $stdWrapped !== '' ? $stdWrapped : $defaultValue; } /** * Check if content was cached before (depending on the given cache key) * * @param string $content Input value undergoing processing in these functions. * @param array $conf All stdWrap properties, not just the ones for a particular function. * @return string The processed input value */ public function stdWrap_cacheRead($content = '', $conf = []) { if (!isset($conf['cache.'])) { return $content; } $result = $this->getFromCache($conf['cache.']); return $result === false ? $content : $result; } /** * Add tags to page cache (comma-separated list) * * @param string $content Input value undergoing processing in these functions. * @param array $conf All stdWrap properties, not just the ones for a particular function. * @return string The processed input value */ public function stdWrap_addPageCacheTags($content = '', $conf = []) { $tags = (string)$this->stdWrapValue('addPageCacheTags', $conf); if (!empty($tags)) { $cacheTags = GeneralUtility::trimExplode(',', $tags, true); $this->getRequest()->getAttribute('frontend.cache.collector')->addCacheTags( ...array_map(fn(string $tag) => new CacheTag($tag), $cacheTags) ); } return $content; } /** * Actually it just does the contrary: Set the value of 'current' based on current content. Wait. What? * * @param string $content Input value undergoing processing in this function. * @return string The processed input value */ public function stdWrap_setContentToCurrent($content = '') { $this->data[$this->currentValKey] = $content; return $content; } /** * Sets the value of 'current' based on the outcome of stdWrap operations * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for setCurrent. * @return string The processed input value */ public function stdWrap_setCurrent($content = '', $conf = []) { $this->data[$this->currentValKey] = $conf['setCurrent'] ?? null; return $content; } /** * Translates content based on the language currently used by the FE * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for lang. * @return string The processed input value */ public function stdWrap_lang($content = '', $conf = []) { // @todo: Check when/if there are scenarios where attribute 'language' is not yet set in $request. $siteLanguage = $this->getRequest()->getAttribute('language') ?? $this->getRequest()->getAttribute('site')->getDefaultLanguage(); $currentLanguageCode = $siteLanguage->getTypo3Language(); if (!$currentLanguageCode) { return $content; } if (isset($conf['lang.'][$currentLanguageCode])) { $content = $conf['lang.'][$currentLanguageCode]; } else { // @todo: use the Locale object and its dependencies in TYPO3 v13 // Check language dependencies foreach ($this->locales->getLocaleDependencies($currentLanguageCode) as $languageCode) { if (isset($conf['lang.'][$languageCode])) { $content = $conf['lang.'][$languageCode]; break; } } } return $content; } /** * Gets content from different sources based on getText functions. * * @param string $_ Unused * @param array $conf stdWrap properties for data. * @return string The processed input value */ public function stdWrap_data($_ = '', $conf = []) { return $this->getData($conf['data'], $this->data); } /** * Gets content from a DB field * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for field. * @return string|null The processed input value */ public function stdWrap_field($content = '', $conf = []) { return $this->getFieldVal($conf['field']); } /** * Gets content that has been previously set as 'current' * Can be set via setContentToCurrent or setCurrent or will be set automatically i.e. inside the split function * * @return string The processed input value */ public function stdWrap_current(mixed $_ = null, mixed $__ = null) { return $this->getCurrentVal(); } /** * Will replace the content with the value of an official TypoScript cObject * like TEXT, COA, HMENU * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for cObject. * @return string The processed input value */ public function stdWrap_cObject($content = '', $conf = []) { return $this->cObjGetSingle($conf['cObject'] ?? '', $conf['cObject.'] ?? [], '/stdWrap/.cObject'); } /** * Counts the number of returned records of a DB operation, using select. * * @param string $_ Input value undergoing processing in this function. * @param array $conf stdWrap properties for numRows. */ public function stdWrap_numRows($_ = '', $conf = []): int { return $this->numRows($conf['numRows.']); } /** * Will execute a user public function before the content will be modified by any other stdWrap function * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for preUserFunc. * @return string The processed input value */ public function stdWrap_preUserFunc($content = '', $conf = []) { return $this->callUserFunction($conf['preUserFunc'], $conf['preUserFunc.'] ?? [], $content); } /** * Will override the current value of content with its own value' * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for override. * @return string The processed input value */ public function stdWrap_override($content = '', $conf = []) { if (trim($conf['override'] ?? false)) { $content = $conf['override']; } return $content; } /** * Gets a value off a CSV list before the following ifEmpty check * Makes sure that the result of ifEmpty will be TRUE in case the CSV does not contain a value at the position given by preIfEmptyListNum * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for preIfEmptyListNum. * @return string The processed input value */ public function stdWrap_preIfEmptyListNum($content = '', $conf = []) { return $this->listNum($content, $conf['preIfEmptyListNum'] ?? '0', $conf['preIfEmptyListNum.']['splitChar'] ?? ','); } /** * Will set content to a replacement value in case the value of content is NULL * * @param string|null $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for ifNull. * @return string The processed input value */ public function stdWrap_ifNull($content = '', $conf = []) { return $content ?? $conf['ifNull']; } /** * Will set content to a replacement value in case the trimmed value of content returns FALSE * 0 (zero) will be replaced as well * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for ifEmpty. * @return string The processed input value */ public function stdWrap_ifEmpty($content = '', $conf = []) { if (empty(trim((string)$content))) { $content = $conf['ifEmpty']; } return $content; } /** * Will set content to a replacement value in case the trimmed value of content has no length * 0 (zero) will not be replaced * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for ifBlank. * @return string The processed input value */ public function stdWrap_ifBlank($content = '', $conf = []) { if (trim((string)$content) === '') { $content = $conf['ifBlank']; } return $content; } /** * Gets a value off a CSV list after ifEmpty check * Might return an empty value in case the CSV does not contain a value at the position given by listNum * Use preIfEmptyListNum to avoid that behaviour * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for listNum. * @return string The processed input value */ public function stdWrap_listNum($content = '', $conf = []) { return $this->listNum($content, $conf['listNum'] ?? '0', $conf['listNum.']['splitChar'] ?? ','); } /** * Cut off any whitespace at the beginning and the end of the content * * @param string $content Input value undergoing processing in this function. */ public function stdWrap_trim($content = ''): string { return trim((string)$content); } /** * Return a string padded left/right/on both sides, based on configuration given as stdWrap properties * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for strPad. */ public function stdWrap_strPad($content = '', $conf = []): string { // Must specify a length in conf for this to make sense $length = (int)$this->stdWrapValue('length', $conf['strPad.'] ?? [], 0); // Padding with space is PHP-default $padWith = (string)$this->stdWrapValue('padWith', $conf['strPad.'] ?? [], ' '); // Padding on the right side is PHP-default $padType = STR_PAD_RIGHT; if (!empty($conf['strPad.']['type'])) { $type = (string)$this->stdWrapValue('type', $conf['strPad.']); if (strtolower($type) === 'left') { $padType = STR_PAD_LEFT; } elseif (strtolower($type) === 'both') { $padType = STR_PAD_BOTH; } } // mb_str_pad() throws a ValueError on an empty pad string, so return the content unchanged in that case. if ($padWith === '') { return $content; } return mb_str_pad($content, $length, $padWith, $padType); } /** * A recursive call of the stdWrap function set * This enables the user to execute stdWrap functions in another than the predefined order * It modifies the content, not the property * while the new feature of chained stdWrap functions modifies the property and not the content * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for stdWrap. * @return string The processed input value */ public function stdWrap_stdWrap($content = '', $conf = []) { return $this->stdWrap($content, $conf['stdWrap.']); } /** * Will immediately stop rendering and return an empty value * when there is no content at this point * * @param string $content Input value undergoing processing in this function. * @return string The processed input value */ public function stdWrap_required($content = '') { if ((string)$content === '') { $content = ''; $this->stopRendering[$this->stdWrapNestingLevel] = true; } return $content; } /** * Will immediately stop rendering and return an empty value * when the result of the checks returns FALSE * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for if. * @return string The processed input value */ public function stdWrap_if($content = '', $conf = []) { if (empty($conf['if.']) || $this->checkIf($conf['if.'])) { return $content; } $this->stopRendering[$this->stdWrapNestingLevel] = true; return ''; } /** * Will immediately stop rendering and return an empty value * when there is no content in the field given by fieldRequired * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for fieldRequired. * @return string The processed input value */ public function stdWrap_fieldRequired($content = '', $conf = []) { $fieldName = (string)($conf['fieldRequired'] ?? ''); if ($fieldName !== '' && !trim($this->data[$fieldName] ?? '')) { $content = ''; $this->stopRendering[$this->stdWrapNestingLevel] = true; } return $content; } /** * stdWrap csConv: Converts the input to UTF-8 * * The character set of the input must be specified. Returns the input if * matters go wrong, for example if an invalid character set is given. * * @param string $content The string to convert. * @param array $conf stdWrap properties for csConv. * @return string The processed input. */ public function stdWrap_csConv($content = '', $conf = []) { if (!empty($conf['csConv'])) { $output = mb_convert_encoding($content, 'utf-8', trim(strtolower($conf['csConv']))); return $output !== false && $output !== '' ? $output : $content; } return $content; } /** * Will parse the content based on functions given as stdWrap properties * Heavily used together with RTE based content * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for parseFunc. * @return string The processed input value */ public function stdWrap_parseFunc($content = '', $conf = []) { return $this->parseFunc($content, $conf['parseFunc.'], $conf['parseFunc']); } /** * Will parse HTML content based on functions given as stdWrap properties * Heavily used together with RTE based content * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for HTMLparser. * @return string The processed input value */ public function stdWrap_HTMLparser($content = '', $conf = []) { if (isset($conf['HTMLparser.']) && is_array($conf['HTMLparser.'])) { $content = $this->HTMLparser_TSbridge($content, $conf['HTMLparser.']); } return $content; } /** * Split the content by a given token and treat the results separately * Automatically fills 'current' with a single result * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for split. * @return string|int The processed input value */ public function stdWrap_split($content = '', $conf = []): string|int { return $this->splitObj($content, $conf['split.']); } /** * Will execute replacements on the content (optionally with preg-regex) * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for replacement. * @return string The processed input value */ public function stdWrap_replacement($content = '', $conf = []) { $configuration = $conf['replacement.'] ?? []; // Sort actions in configuration by numeric index ksort($configuration, SORT_NUMERIC); foreach ($configuration as $index => $action) { // Check whether we have a valid action and a numeric key ending with a dot ("10.") if (is_array($action) && str_ends_with($index, '.') && MathUtility::canBeInterpretedAsInteger(substr($index, 0, -1)) && (isset($action['search']) || isset($action['search.'])) && (isset($action['replace']) || isset($action['replace.'])) ) { $search = (string)$this->stdWrapValue('search', $action); $replace = (string)$this->stdWrapValue('replace', $action, null); // Determines whether regular expression shall be used $useRegularExpression = (bool)$this->stdWrapValue('useRegExp', $action, false); // Determines whether replace-pattern uses option-split $useOptionSplitReplace = (bool)$this->stdWrapValue('useOptionSplitReplace', $action, false); // Performs a replacement by preg_replace() if ($useRegularExpression) { // Get separator-character which precedes the string and separates search-string from the modifiers $separator = $search[0]; $startModifiers = strrpos($search, $separator); if ($startModifiers > 0) { $modifiers = substr($search, $startModifiers + 1); // remove "e" (eval-modifier), which would otherwise allow to run arbitrary PHP-code $modifiers = str_replace('e', '', $modifiers); $search = substr($search, 0, $startModifiers + 1) . $modifiers; } if ($useOptionSplitReplace) { // init for replacement $splitCount = preg_match_all($search, $content); $replaceArray = $this->typoScriptService->explodeConfigurationForOptionSplit([$replace], $splitCount); $replaceCount = 0; $replaceCallback = static function ($match) use ($replaceArray, $search, &$replaceCount) { $replaceCount++; return preg_replace($search, $replaceArray[$replaceCount - 1][0], $match[0]); }; $content = preg_replace_callback($search, $replaceCallback, $content); } else { $content = preg_replace($search, $replace, $content); } } elseif ($useOptionSplitReplace) { // turn search-string into a preg-pattern $searchPreg = '#' . preg_quote($search, '#') . '#'; // init for replacement $splitCount = preg_match_all($searchPreg, $content); $replaceArray = $this->typoScriptService->explodeConfigurationForOptionSplit([$replace], $splitCount); $replaceCount = 0; $replaceCallback = static function () use ($replaceArray, &$replaceCount) { $replaceCount++; return $replaceArray[$replaceCount - 1][0]; }; $content = preg_replace_callback($searchPreg, $replaceCallback, $content); } else { $content = str_replace($search, $replace, $content); } } } return $content; } /** * Will use the content as a mathematical term and calculate the result * Can be set to 1 to just get a calculated value or 'intval' to get the integer of the result * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for prioriCalc. */ public function stdWrap_prioriCalc($content = '', $conf = []): string|int { $content = MathUtility::calculateWithParentheses($content); if (!empty($conf['prioriCalc']) && $conf['prioriCalc'] === 'intval') { $content = (int)$content; } return $content; } /** * Returns a one-character string containing the character specified by ascii code. * Reliable results only for character codes in the integer range 0 - 127. * * @see https://php.net/manual/en/function.chr.php * @param array $conf stdWrap properties for char. */ public function stdWrap_char($_ = '', $conf = []): string { return chr((int)$conf['char']); } /** * Will return an integer value of the current content * * @param string $content Input value undergoing processing in this function. */ public function stdWrap_intval($content = ''): int { return (int)$content; } /** * Will return a hashed value of the current content * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for hash. * @link https://php.net/manual/de/function.hash-algos.php for a list of supported hash algorithms */ public function stdWrap_hash($content = '', array $conf = []): string { $algorithm = (string)$this->stdWrapValue('hash', $conf); if (in_array($algorithm, hash_algos())) { return hash($algorithm, $content); } // Non-existing hashing algorithm return ''; } /** * stdWrap_round will return a rounded number with ceil(), floor() or round(), defaults to round() * Only the english number format is supported . (dot) as decimal point * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for round. */ public function stdWrap_round($content = '', $conf = []): float { $decimals = (int)$this->stdWrapValue('decimals', $conf['round.'] ?? [], 0); $type = $this->stdWrapValue('roundType', $conf['round.'] ?? []); $floatVal = (float)$content; return match ($type) { 'ceil' => ceil($floatVal), 'floor' => floor($floatVal), default => round($floatVal, $decimals), }; } /** * Will return a formatted number based on configuration given as stdWrap properties * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for numberFormat. */ public function stdWrap_numberFormat($content = '', $conf = []): string { return $this->numberFormat((float)$content, $conf['numberFormat.'] ?? []); } /** * Will return a formatted number based on configuration given as stdWrap properties * * @param string $content Input value undergoing processing in this function. * @return string The processed input value */ public function stdWrap_expandList($content = ''): string { return GeneralUtility::expandList($content); } /** * Will return a formatted date based on configuration given according to PHP date/gmdate properties * Will return gmdate when the property GMT returns TRUE * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for date. */ public function stdWrap_date($content = '', $conf = []): string { // Check for zero length string to mimic default case of date/gmdate. $content = (string)$content === '' ? $GLOBALS['EXEC_TIME'] : (int)$content; return !empty($conf['date.']['GMT']) ? gmdate($conf['date'] ?? null, $content) : date($conf['date'] ?? null, $content); } /** * Will return a formatted date based on configuration given according to PHP strftime/gmstrftime properties * Will return gmstrftime when the property GMT returns TRUE * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for strftime. * @return string The processed input value */ public function stdWrap_strftime($content = '', $conf = []) { // Check for zero length string to mimic default case of strtime/gmstrftime $content = (string)$content === '' ? $GLOBALS['EXEC_TIME'] : (int)$content; $content = (isset($conf['strftime.']['GMT']) && $conf['strftime.']['GMT']) ? (new DateFormatter())->strftime($conf['strftime'] ?? '', $content, null, true) : (new DateFormatter())->strftime($conf['strftime'] ?? '', $content); if (!empty($conf['strftime.']['charset'])) { $output = mb_convert_encoding((string)$content, 'utf-8', trim(strtolower($conf['strftime.']['charset']))); return $output ?: $content; } return $content; } /** * Will return a timestamp based on configuration given according to PHP strtotime * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for strtotime. */ public function stdWrap_strtotime($content = '', $conf = []): int|false { if ($conf['strtotime'] !== '1') { $content .= ' ' . $conf['strtotime']; } return strtotime($content, $GLOBALS['EXEC_TIME']); } /** * php-intl date format * Will return a timestamp based on configuration given according to PHP-intl DateFormatter->format() * see https://unicode-org.github.io/icu/userguide/format_parse/datetime/#datetime-format-syntax * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for formattedDate. */ public function stdWrap_formattedDate(string $content, array $conf): string { $pattern = $conf['formattedDate'] ?? 'LONG'; // @todo: Check when/if there are scenarios where attribute 'language' is not yet set in $request. $language = $this->getRequest()->getAttribute('language') ?? $this->getRequest()->getAttribute('site')->getDefaultLanguage(); $locale = $conf['formattedDate.']['locale'] ?? $language->getLocale(); if ($content === '' || $content === '0') { $content = $this->context->getAspect('date')->getDateTime(); } else { // format this to a timestamp now $content = strtotime((MathUtility::canBeInterpretedAsInteger($content) ? '@' : '') . $content); if ($content === false) { $content = $this->context->getAspect('date')->getDateTime(); } } return (new DateFormatter())->format($content, $pattern, $locale); } /** * Return the age of a given timestamp based on configuration given by stdWrap properties * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for age. * @return string The processed input value */ public function stdWrap_age($content = '', $conf = []): string { return $this->calcAge((int)($GLOBALS['EXEC_TIME'] ?? 0) - (int)$content, $conf['age'] ?? null); } /** * Transform the content to be upper or lower case only * Leaves HTML tags untouched * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for case. */ public function stdWrap_case($content = '', $conf = []): string { return $this->HTMLcaseshift($content, $conf['case']); } /** * Will return the size of a given number in Bytes * * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for bytes. * @return string The processed input value */ public function stdWrap_bytes($content = '', $conf = []) { $decimals = $conf['bytes.']['decimals'] ?? null; if ($decimals !== null) { $decimals = (int)$decimals; } return GeneralUtility::formatSize((int)$content, $conf['bytes.']['labels'] ?? '', $conf['bytes.']['base'] ?? 0, $decimals); } /** * Will return a substring based on position information given by stdWrap properties * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for substring. */ public function stdWrap_substring($content = '', $conf = []): string { $options = GeneralUtility::intExplode(',', ($conf['substring'] ?? '') . ','); if ($options[1]) { return mb_substr($content, $options[0], $options[1], 'utf-8'); } return mb_substr($content, $options[0], null, 'utf-8'); } /** * Crops content to a given size while leaving HTML tags untouched * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for cropHTML. */ public function stdWrap_cropHTML($content = '', $conf = []): string { return $this->cropHTML($content, $conf['cropHTML'] ?? ''); } /** * Completely removes HTML tags from content * * @param string $content Input value undergoing processing in this function. */ public function stdWrap_stripHtml($content = ''): string { return strip_tags((string)$content); } /** * Crops content to a given size without caring about HTML tags * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for crop. */ public function stdWrap_crop($content = '', $conf = []): string { return $this->crop($content, $conf['crop']); } /** * Encode content to be used within URLs */ public function stdWrap_rawUrlEncode($content = ''): string { return rawurlencode($content); } /** * Transforms HTML tags to readable text by replacing special characters with their HTML entity * When preserveEntities returns TRUE, existing entities will be left untouched * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for htmlSpecialChars. */ public function stdWrap_htmlSpecialChars($content = '', $conf = []) { if (!empty($conf['htmlSpecialChars.']['preserveEntities'])) { $content = htmlspecialchars((string)$content, ENT_COMPAT, 'UTF-8', false); } else { $content = htmlspecialchars((string)$content); } return $content; } /** * Escapes content to be used inside JavaScript strings. Single quotes are added around the value. * * @param string $content Input value undergoing processing in this function */ public function stdWrap_encodeForJavaScriptValue($content = ''): string { return GeneralUtility::quoteJSvalue($content); } /** * Searches for double line breaks and replaces them with the given value * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for doubleBrTag. * @return string The processed input value */ public function stdWrap_doubleBrTag($content = '', $conf = []) { return preg_replace('/\R{1,2}[\t\x20]*\R{1,2}/', $conf['doubleBrTag'] ?? '', $content); } /** * Searches for single line breaks and replaces them with a
/
tag * according to the doctype * * @param string $content Input value undergoing processing in this function. */ public function stdWrap_br($content = ''): string { return nl2br($content, DocType::createFromRequest($this->getRequest())->isXmlCompliant()); } /** * Searches for single line feeds and replaces them with the given value * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for brTag. */ public function stdWrap_brTag($content = '', $conf = []): string { return str_replace(LF, (string)($conf['brTag'] ?? ''), $content); } /** * Modifies text blocks by searching for lines which are not surrounded by HTML tags yet * and wrapping them with values given by stdWrap properties * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for erncapsLines. * @return string The processed input value */ public function stdWrap_encapsLines($content = '', $conf = []) { return $this->encaps_lineSplit($content, $conf['encapsLines.'] ?? []); } /** * Transforms content into a CSV list to be used i.e. as keywords within a meta tag * * @param string $content Input value undergoing processing in this function. */ public function stdWrap_keywords($content = ''): string { return $this->keywords($content); } /** * First of a set of different wraps which will be applied in a certain order before or after other functions that modify the content * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for innerWrap. * @return string The processed input value */ public function stdWrap_innerWrap($content = '', $conf = []) { return $this->wrap($content, $conf['innerWrap'] ?? null); } /** * Second of a set of different wraps which will be applied in a certain order before or after other functions that modify the content * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for innerWrap2. * @return string The processed input value */ public function stdWrap_innerWrap2($content = '', $conf = []) { return $this->wrap($content, $conf['innerWrap2'] ?? null); } /** * A content object that is prepended to the current content but between the innerWraps and the rest of the wraps * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for preCObject. * @return string The processed input value */ public function stdWrap_preCObject($content = '', $conf = []) { return $this->cObjGetSingle($conf['preCObject'], $conf['preCObject.'], '/stdWrap/.preCObject') . $content; } /** * A content object that is appended to the current content but between the innerWraps and the rest of the wraps * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for postCObject. * @return string The processed input value */ public function stdWrap_postCObject($content = '', $conf = []) { return $content . $this->cObjGetSingle($conf['postCObject'], $conf['postCObject.'], '/stdWrap/.postCObject'); } /** * Wraps content with a div container having the style attribute text-align set to the given value * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for wrapAlign. * @return string The processed input value */ public function stdWrap_wrapAlign($content = '', $conf = []) { $wrapAlign = trim($conf['wrapAlign'] ?? ''); if ($wrapAlign) { $content = $this->wrap($content, '
|
'); } return $content; } /** * Wraps the content with a link tag * URLs and other attributes are created automatically by the values given in the stdWrap properties * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for typolink. * @return string The processed input value */ public function stdWrap_typolink($content = '', $conf = []) { return $this->typoLink((string)$content, $conf['typolink.'] ?? []); } /** * This is the "mother" of all wraps * Third of a set of different wraps which will be applied in a certain order before or after other functions that modify the content * Basically it will put additional content before and after the current content using a split character as a placeholder for the current content * The default split character is | but it can be replaced with other characters by the property splitChar * Any other wrap that does not have own splitChar settings will be using the default split char though * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for wrap. * @return string The processed input value */ public function stdWrap_wrap($content = '', $conf = []) { return $this->wrap($content, $conf['wrap'] ?? null, $conf['wrap.']['splitChar'] ?? '|'); } /** * Fourth of a set of different wraps which will be applied in a certain order before or after other functions that modify the content * The major difference to any other wrap is, that this one can make use of whitespace without trimming * * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for noTrimWrap. * @return string The processed input value */ public function stdWrap_noTrimWrap($content = '', $conf = []) { $splitChar = isset($conf['noTrimWrap.']['splitChar.']) ? $this->stdWrap($conf['noTrimWrap.']['splitChar'] ?? '', $conf['noTrimWrap.']['splitChar.']) : $conf['noTrimWrap.']['splitChar'] ?? ''; if ($splitChar === null || $splitChar === '') { $splitChar = '|'; } return $this->noTrimWrap($content, $conf['noTrimWrap'], $splitChar); } /** * Fifth of a set of different wraps which will be applied in a certain order before or after other functions that modify the content * The default split character is | but it can be replaced with other characters by the property splitChar * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for wrap2. * @return string The processed input value */ public function stdWrap_wrap2($content = '', $conf = []) { return $this->wrap($content, $conf['wrap2'] ?? null, $conf['wrap2.']['splitChar'] ?? '|'); } /** * Sixth of a set of different wraps which will be applied in a certain order before or after other functions that modify the content * Can fetch additional content the same way data does (i.e. {field:whatever}) and apply it to the wrap before that is applied to the content * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for dataWrap. * @return string The processed input value */ public function stdWrap_dataWrap($content = '', $conf = []) { return $this->dataWrap($content, $conf['dataWrap']); } /** * A content object that will be prepended to the current content after most of the wraps have already been applied * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for prepend. * @return string The processed input value */ public function stdWrap_prepend($content = '', $conf = []) { return $this->cObjGetSingle($conf['prepend'], $conf['prepend.'], '/stdWrap/.prepend') . $content; } /** * A content object that will be appended to the current content after most of the wraps have already been applied * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for append. * @return string The processed input value */ public function stdWrap_append($content = '', $conf = []) { return $content . $this->cObjGetSingle($conf['append'], $conf['append.'], '/stdWrap/.append'); } /** * Seventh of a set of different wraps which will be applied in a certain order before or after other functions that modify the content * The default split character is | but it can be replaced with other characters by the property splitChar * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for wrap3. * @return string The processed input value */ public function stdWrap_wrap3($content = '', $conf = []) { return $this->wrap($content, $conf['wrap3'] ?? null, $conf['wrap3.']['splitChar'] ?? '|'); } /** * Calls stdWrap for each entry in the provided array * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for orderedStdWrap. * @return string The processed input value */ public function stdWrap_orderedStdWrap($content = '', $conf = []) { $sortedKeysArray = ArrayUtility::filterAndSortByNumericKeys($conf['orderedStdWrap.'], true); foreach ($sortedKeysArray as $key) { $content = (string)$this->stdWrap($content, $conf['orderedStdWrap.'][$key . '.'] ?? null); } return $content; } /** * Eighth of a set of different wraps which will be applied in a certain order before or after other functions that modify the content * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for outerWrap. * @return string The processed input value */ public function stdWrap_outerWrap($content = '', $conf = []) { return $this->wrap($content, $conf['outerWrap'] ?? null); } /** * Can fetch additional content the same way data does and replaces any occurrence of {field:whatever} with this content * * @param string $content Input value undergoing processing in this function. */ public function stdWrap_insertData($content = ''): string { return $this->insertData($content); } /** * Will execute a user function after the content has been modified by any other stdWrap function * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for postUserFunc. * @return string The processed input value */ public function stdWrap_postUserFunc($content = '', $conf = []) { return $this->callUserFunction($conf['postUserFunc'], $conf['postUserFunc.'] ?? [], $content); } /** * Will execute a user function after the content has been created and each time it is fetched from Cache * The result of this function itself will not be cached * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for postUserFuncInt. */ public function stdWrap_postUserFuncInt($content = '', $conf = []): string { $substKey = 'INT_SCRIPT.' . md5(StringUtility::getUniqueId()); $pageParts = $this->getRequest()->getAttribute('frontend.page.parts'); $pageParts->addNotCachedContentElement([ 'substKey' => $substKey, 'content' => $content, 'postUserFunc' => $conf['postUserFuncInt'], 'conf' => $conf['postUserFuncInt.'], 'type' => 'POSTUSERFUNC', 'cObjData' => serialize($this->getState()), ]); return ''; } /** * Add HTML comments to the content to make it easier to identify certain content elements within the HTML output later on * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for prefixComment. * @return string The processed input value */ public function stdWrap_prefixComment($content = '', $conf = []) { $typoScriptConfigArray = $this->getRequest()->getAttribute('frontend.typoscript')->getConfigArray(); if ((!isset($typoScriptConfigArray['disablePrefixComment']) || !$typoScriptConfigArray['disablePrefixComment']) && !empty($conf['prefixComment']) ) { $content = $this->prefixComment($conf['prefixComment'], [], $content); } return $content; } public function stdWrap_htmlSanitize(string $content = '', array $conf = []): string { $build = $conf['build'] ?? 'default'; // @todo: Simplify: There is a factory to build a builder, and this is wrapped here // to build a different builder. This is not one, but two levels too complex. if (class_exists($build) && is_a($build, BuilderInterface::class, true)) { $builder = GeneralUtility::makeInstance($build); } else { $builder = $this->sanitizerBuilderFactory->build($build); } $sanitizer = $builder->build(); $initiator = $this->shallDebug() ? GeneralUtility::makeInstance(SanitizerInitiator::class, DebugUtility::debugTrail()) : null; return $sanitizer->sanitize($content, $initiator); } /** * Store content into cache * * @param string|null $content Input value undergoing processing in these functions. * @param array $conf All stdWrap properties, not just the ones for a particular function. * @return string|null The processed input value */ public function stdWrap_cacheStore($content = '', $conf = []): ?string { if (!isset($conf['cache.'])) { return $content; } $key = $this->calculateCacheKey($conf['cache.']); if (empty($key)) { return $content; } $event = $this->eventDispatcher->dispatch( new BeforeStdWrapContentStoredInCacheEvent( content: $content, tags: $this->calculateCacheTags($conf['cache.']), key: (string)$key, lifetime: $this->calculateCacheLifetime($conf['cache.']), configuration: $conf, contentObjectRenderer: $this ) ); $this->cacheHash->set( $event->getKey(), ['content' => $event->getContent(), 'cacheTags' => $event->getTags()], $event->getTags(), $event->getLifetime() ); // If no tags are given, we restrict the maximum lifetime of the cache to the lifetime of the cache entry. $cacheCollector = $this->getRequest()->getAttribute('frontend.cache.collector'); if ($event->getTags() === []) { $cacheCollector->restrictMaximumLifetime($event->getLifetime()); } $cacheCollector->addCacheTags(...array_map(fn(string $tag) => new CacheTag($tag, $event->getLifetime()), $event->getTags())); return $event->getContent(); } /** * Will output the content as readable HTML code * * @param string $content Input value undergoing processing in this function. */ public function stdWrap_debug($content = ''): string { return '
' . htmlspecialchars($content) . '
'; } /** * Will output the content in a debug table * * @param string $content Input value undergoing processing in this function. * @param array $conf stdWrap properties for debugFunc. * @return string The processed input value */ public function stdWrap_debugFunc($content = '', $conf = []) { debug((int)$conf['debugFunc'] === 2 ? [$content] : $content); return $content; } /** * Will output the data used by the current record in a debug table * * @param string $content Input value undergoing processing in this function. * @return string The processed input value */ public function stdWrap_debugData($content = '') { debug($this->data, '$cObj->data:'); return $content; } /** * Returns number of rows selected by the query made by the properties set. * Implements the stdWrap "numRows" property * * @param array $conf TypoScript properties for the property (see link to "numRows") * @internal */ public function numRows($conf): int { $conf['select.']['selectFields'] = 'count(*)'; $statement = $this->exec_getQuery($conf['table'], $conf['select.']); return (int)$statement->fetchOne(); } /** * Explode a string by the $delimiter value and return the value of index $listNum * * @param string $content String to explode * @param string $listNum Index-number | 'last' | 'rand' | arithmetic expression. You can place the word "last" in it and it will be * substituted with the pointer to the last value. You can use math operators like "+-/*" (passed to calc()) * @param string $delimiter Either a string used to explode the content string or an integer value (as string) which will then be * changed into a character, eg. "10" for a linebreak char. * @return string */ public function listNum($content, $listNum, $delimiter = ',') { $delimiter = $delimiter ?: ','; if (MathUtility::canBeInterpretedAsInteger($delimiter)) { $delimiter = chr((int)$delimiter); } $temp = explode($delimiter, $content); if ($temp === ['']) { return ''; } $last = '' . (count($temp) - 1); // Take a random item if requested if ($listNum === 'rand') { $listNum = (string)random_int(0, count($temp) - 1); } $index = $this->calc(str_ireplace('last', $last, $listNum)); return $temp[$index] ?? ''; } /** * Compares values together based on the settings in the input TypoScript array and returns the comparison result. * Implements the "if" function in TYPO3 TypoScript * * @param mixed $conf TypoScript properties defining what to compare, ideally an array */ public function checkIf($conf): bool { if (!is_array($conf)) { return true; } if (isset($conf['directReturn'])) { return (bool)$conf['directReturn']; } $flag = true; if (isset($conf['isNull.'])) { $isNull = $this->stdWrap('', $conf['isNull.']); if ($isNull !== null) { $flag = false; } } if (isset($conf['isTrue']) || isset($conf['isTrue.'])) { $isTrue = trim((string)$this->stdWrapValue('isTrue', $conf)); if (!$isTrue) { $flag = false; } } if (isset($conf['isFalse']) || isset($conf['isFalse.'])) { $isFalse = trim((string)$this->stdWrapValue('isFalse', $conf)); if ($isFalse) { $flag = false; } } if (isset($conf['isPositive']) || isset($conf['isPositive.'])) { $number = $this->calc((string)$this->stdWrapValue('isPositive', $conf)); if ($number < 1) { $flag = false; } } if ($flag) { $comparisonValue = trim((string)$this->stdWrapValue('value', $conf)); if (isset($conf['isGreaterThan']) || isset($conf['isGreaterThan.'])) { $number = trim((string)$this->stdWrapValue('isGreaterThan', $conf)); if ($number <= $comparisonValue) { $flag = false; } } if (isset($conf['isLessThan']) || isset($conf['isLessThan.'])) { $number = trim((string)$this->stdWrapValue('isLessThan', $conf)); if ($number >= $comparisonValue) { $flag = false; } } if (isset($conf['equals']) || isset($conf['equals.'])) { $number = trim((string)$this->stdWrapValue('equals', $conf)); if ($number != $comparisonValue) { $flag = false; } } if (isset($conf['contains']) || isset($conf['contains.'])) { $needle = trim((string)$this->stdWrapValue('contains', $conf)); if (!str_contains($comparisonValue, $needle)) { $flag = false; } } if (isset($conf['startsWith']) || isset($conf['startsWith.'])) { $needle = trim((string)$this->stdWrapValue('startsWith', $conf)); if (!str_starts_with($comparisonValue, $needle)) { $flag = false; } } if (isset($conf['endsWith']) || isset($conf['endsWith.'])) { $needle = trim((string)$this->stdWrapValue('endsWith', $conf)); if (!str_ends_with($comparisonValue, $needle)) { $flag = false; } } if (isset($conf['isInList']) || isset($conf['isInList.'])) { $singleValueWhichNeedsToBeInList = trim((string)$this->stdWrapValue('isInList', $conf)); if (!GeneralUtility::inList($comparisonValue, $singleValueWhichNeedsToBeInList)) { $flag = false; } } if (isset($conf['bitAnd']) || isset($conf['bitAnd.'])) { $number = (int)trim((string)$this->stdWrapValue('bitAnd', $conf)); if ((new BitSet($number))->get((int)$comparisonValue) === false) { $flag = false; } } } if ($conf['negate'] ?? false) { $flag = !$flag; } return $flag; } /** * Passes the input value, $theValue, to an instance of "\TYPO3\CMS\Core\Html\HtmlParser" * together with the TypoScript options which are first converted from a TS style array * to a set of arrays with options for the \TYPO3\CMS\Core\Html\HtmlParser class. * * @param string $theValue The value to parse by the class \TYPO3\CMS\Core\Html\HtmlParser * @param array $conf TypoScript properties for the parser. See link. * @see stdWrap() * @internal */ public function HTMLparser_TSbridge($theValue, $conf): string { $htmlParser = GeneralUtility::makeInstance(HtmlParser::class); $htmlParserCfg = $htmlParser->HTMLparserConfig($conf); return $htmlParser->HTMLcleaner($theValue, $htmlParserCfg[0], $htmlParserCfg[1], $htmlParserCfg[2], $htmlParserCfg[3]); } /** * Wrapping input value in a regular "wrap" but parses the wrapping value first for "insertData" codes. * * @param string $content Input string being wrapped * @param string $wrap The wrap string, eg. "" or more likely here ' | ' * which will wrap the input string in a tag linking to the current page. * @return string Output string wrapped in the wrapping value. * @see insertData() * @see stdWrap() */ public function dataWrap($content, $wrap) { return $this->wrap($content, $this->insertData($wrap)); } /** * Implements the "insertData" property of stdWrap meaning that if strings matching {...} is found in the input string they * will be substituted with the return value from getData (datatype) which is passed the content of the curly braces. * If the content inside the curly braces starts with a hash sign {#...} it is a field name that must be quoted by Doctrine * DBAL and is skipped here for later processing. * * Example: If input string is "This is the page title: {page:title}" then the part, '{page:title}', will be substituted with * the current pages title field value. * * @param string $str Input value * @see getData() * @see stdWrap() * @see dataWrap() * @internal */ public function insertData($str): string { $inside = 0; $newVal = ''; $pointer = 0; $totalLen = strlen($str); do { if (!$inside) { $len = strcspn(substr($str, $pointer), '{'); $newVal .= substr($str, $pointer, $len); $inside = true; if (substr($str, $pointer + $len + 1, 1) === '#') { $len2 = strcspn(substr($str, $pointer + $len), '}'); $newVal .= substr($str, $pointer + $len, $len2); $len += $len2; $inside = false; } } else { $len = strcspn(substr($str, $pointer), '}') + 1; $newVal .= $this->getData(substr($str, $pointer + 1, $len - 2), $this->data); $inside = false; } $pointer += $len; } while ($pointer < $totalLen); return $newVal; } /** * Returns an HTML comment with the second part of input string (divided by "|") where first part is * an integer telling how many trailing tabs to put before the comment on a new line. * This function (used by stdWrap) can be disabled by a "config.disablePrefixComment" setting in TypoScript. * * @param string $str Input value * @param string $content The content to wrap the comment around. * @internal */ public function prefixComment($str, $_, $content): string { if (empty($str)) { return $content; } $parts = explode('|', $str); $indent = (int)$parts[0]; $comment = htmlspecialchars($this->insertData($parts[1])); return LF . str_pad('', $indent, "\t") . '' . LF . str_pad('', $indent + 1, "\t") . $content . LF . str_pad('', $indent, "\t") . '' . LF . str_pad('', $indent + 1, "\t"); } /** * Implements the stdWrap property "crop" which is a modified "substr" function allowing to limit * a string length to a certain number of chars (from either start or end of string) and having a * pre/postfix applied if the string really was cropped. * * @param string $content The string to perform the operation on * @param string $options The parameters splitted by "|": First parameter is the max number of chars of the string. * Negative value means cropping from end of string. Second parameter is the pre/postfix * string to apply if cropping occurs. Third parameter is a boolean value. If set then crop * will be applied at nearest space. * @return string The processed input value. * @see stdWrap() * @internal */ public function crop($content, $options): string { $options = explode('|', $options); $numberOfChars = (int)$options[0]; $replacementForEllipsis = trim($options[1] ?? ''); $cropToSpace = trim($options[2] ?? '') === '1'; return $this->textCropper->crop($content, $numberOfChars, $replacementForEllipsis, $cropToSpace); } /** * Implements the stdWrap property "cropHTML" which is a modified "substr" function allowing to limit a string length * to a certain number of chars (from either start or end of string) and having a pre/postfix applied if the string * really was cropped. * * Compared to stdWrap.crop it respects HTML tags and entities. * * @param string $content The string to perform the operation on * @param string $options The parameters splitted by "|": First parameter is the max number of chars of the string. * Negative value means cropping from end of string. Second parameter is the pre/postfix * string to apply if cropping occurs. Third parameter is a boolean value. If set then crop * will be applied at nearest space. * @see stdWrap() * @return string The processed input value. * @internal */ public function cropHTML(string $content, string $options): string { $options = explode('|', $options); $numberOfChars = (int)$options[0]; $replacementForEllipsis = trim($options[1] ?? ''); $cropToSpace = trim($options[2] ?? '') === '1'; return $this->htmlCropper->crop($content, $numberOfChars, $replacementForEllipsis, $cropToSpace); } /** * Performs basic mathematical evaluation of the input string. Does NOT take parenthesis and operator precedence * into account! (for that, see \TYPO3\CMS\Core\Utility\MathUtility::calculateWithPriorityToAdditionAndSubtraction()) * * @param string $val The string to evaluate. Example: "3+4*10/5" will generate "35". Only integer numbers can be used. * @return int The result (might be a float if you did a division of the numbers). * @see \TYPO3\CMS\Core\Utility\MathUtility::calculateWithPriorityToAdditionAndSubtraction() */ public function calc($val): int { $parts = GeneralUtility::splitCalc($val, '+-*/'); $value = 0; foreach ($parts as $part) { $theVal = $part[1]; $sign = $part[0]; if ((string)(int)$theVal === (string)$theVal) { $theVal = (int)$theVal; } else { $theVal = 0; } if ($sign === '-') { $value -= $theVal; } if ($sign === '+') { $value += $theVal; } if ($sign === '/') { if ((int)$theVal) { $value /= (int)$theVal; } } if ($sign === '*') { $value *= $theVal; } } return $value; } /** * Implements the "split" property of stdWrap; Splits a string based on a token (given in TypoScript properties), * sets the "current" value to each part and then renders a content object pointer to by a number. * In classic TypoScript (like 'content (default)'/'styles.content (default)') this is used to render tables, * splitting rows and cells by tokens and putting them together again wrapped in tags etc. * Implements the "optionSplit" processing of the TypoScript options for each splitted value to parse. * * @param string $value The string value to explode by $conf[token] and process each part * @param array $conf TypoScript properties for "split * @internal * @see stdWrap() * @see \TYPO3\CMS\Frontend\ContentObject\Menu\AbstractMenuContentObject::processItemStates() */ public function splitObj($value, $conf): string|int { $conf['token'] = isset($conf['token.']) ? $this->stdWrap($conf['token'] ?? '', $conf['token.']) : $conf['token'] ?? ''; if ($conf['token'] === '') { return $value; } $valArr = explode($conf['token'], $value); // return value directly by returnKey. No further processing if ($valArr !== [''] && (MathUtility::canBeInterpretedAsInteger($conf['returnKey'] ?? null) || ($conf['returnKey.'] ?? false))) { $key = (int)$this->stdWrapValue('returnKey', $conf); return $valArr[$key] ?? ''; } // return the amount of elements. No further processing if ($valArr !== [''] && (($conf['returnCount'] ?? false) || ($conf['returnCount.'] ?? false))) { $returnCount = (bool)$this->stdWrapValue('returnCount', $conf); return $returnCount ? count($valArr) : 0; } // calculate splitCount $splitCount = count($valArr); $max = (int)$this->stdWrapValue('max', $conf); if ($max && $splitCount > $max) { $splitCount = $max; } $min = (int)$this->stdWrapValue('min', $conf); if ($min && $splitCount < $min) { $splitCount = $min; } $wrap = (string)$this->stdWrapValue('wrap', $conf); $cObjNumSplitConf = isset($conf['cObjNum.']) ? $this->stdWrap($conf['cObjNum'] ?? '', $conf['cObjNum.']) : (string)($conf['cObjNum'] ?? ''); $splitArr = []; if ($wrap !== '' || $cObjNumSplitConf !== '') { $splitArr['wrap'] = $wrap; $splitArr['cObjNum'] = $cObjNumSplitConf; $splitArr = $this->typoScriptService->explodeConfigurationForOptionSplit($splitArr, $splitCount); } $content = ''; for ($a = 0; $a < $splitCount; $a++) { $this->getRequest()->getAttribute('frontend.register.stack')->current()->set('SPLIT_COUNT', $a); $value = $valArr[$a]; $this->data[$this->currentValKey] = $value; if ($splitArr[$a]['cObjNum'] ?? false) { $objName = (int)$splitArr[$a]['cObjNum']; $value = (string)(isset($conf[$objName . '.']) ? $this->stdWrap($this->cObjGet($conf[$objName . '.'], $objName . '.'), $conf[$objName . '.']) : ''); } $wrap = (string)$this->stdWrapValue('wrap', $splitArr[$a] ?? []); if ($wrap) { $value = $this->wrap($value, $wrap); } $content .= $value; } return $content; } /** * Implements the stdWrap property "numberFormat" * This is a Wrapper function for php's number_format() * * @param float $content Value to process * @param array $conf TypoScript Configuration for numberFormat * @internal */ public function numberFormat($content, $conf): string { $decimals = (int)$this->stdWrapValue('decimals', $conf, 0); $dec_point = (string)$this->stdWrapValue('dec_point', $conf, '.'); $thousands_sep = (string)$this->stdWrapValue('thousands_sep', $conf, ','); return number_format((float)$content, $decimals, $dec_point, $thousands_sep); } /** * Implements the stdWrap property, "parseFunc". * This is a function with a lot of interesting uses. In classic TypoScript this is used to process text * from the bodytext field; This included highlighting of search words, changing http:// and mailto: prefixed strings into etc. * It is still a very important function for processing of bodytext which is normally stored in the database * in a format which is not fully ready to be outputted. * This situation has not become better by having a RTE around... * * This function is actually just splitting the input content according to the configuration of "external blocks". * This means that before the input string is actually "parsed" it will be split into the parts configured to BE parsed * (while other parts/blocks should NOT be parsed). * Therefore, the actual processing of the parseFunc properties goes on in ->parseFuncInternal() * * @param string $theValue The value to process. * @param non-empty-array|null $conf TypoScript configuration for parseFunc * @param non-empty-string|null $ref Reference to get configuration from. Eg. "< lib.parseFunc" which means that the configuration * of the object path "lib.parseFunc" will be retrieved and MERGED with what is in $conf! * @return string The processed value */ public function parseFunc($theValue, ?array $conf, ?string $ref = null) { // Fetch / merge reference, if any if (!empty($ref)) { $temp_conf = [ 'parseFunc' => $ref, 'parseFunc.' => $conf ?? [], ]; $temp_conf = $this->mergeTSRef($temp_conf, 'parseFunc'); $conf = $temp_conf['parseFunc.']; } if (empty($conf)) { // `parseFunc` relies on configuration, either given in `$conf` or resolved from `$ref` throw new \LogicException('Invoked ContentObjectRenderer::parseFunc without any configuration', 1641989097); } // Handle HTML sanitizer invocation $conf['htmlSanitize'] = (bool)($conf['htmlSanitize'] ?? true); // Process: if ((string)($conf['externalBlocks'] ?? '') === '') { $result = $this->parseFuncInternal($theValue, $conf); if ($conf['htmlSanitize']) { $result = $this->stdWrap_htmlSanitize($result, $conf['htmlSanitize.'] ?? []); } return $result; } $tags = strtolower(implode(',', GeneralUtility::trimExplode(',', $conf['externalBlocks']))); $htmlParser = GeneralUtility::makeInstance(HtmlParser::class); $parts = $htmlParser->splitIntoBlock($tags, $theValue); foreach ($parts as $k => $v) { if ($k % 2) { // font: $tagName = strtolower($htmlParser->getFirstTagName($v)); $cfg = $conf['externalBlocks.'][$tagName . '.'] ?? []; if ($cfg === []) { continue; } if (($cfg['stripNLprev'] ?? false) || ($cfg['stripNL'] ?? false)) { $parts[$k - 1] = preg_replace('/' . CR . '?' . LF . '[ ]*$/', '', $parts[$k - 1]); } if (($cfg['stripNLnext'] ?? false) || ($cfg['stripNL'] ?? false)) { if (!isset($parts[$k + 1])) { $parts[$k + 1] = ''; } $parts[$k + 1] = preg_replace('/^[ ]*' . CR . '?' . LF . '/', '', $parts[$k + 1]); } } } foreach ($parts as $k => $v) { if ($k % 2) { $tag = $htmlParser->getFirstTag($v); $tagName = strtolower($htmlParser->getFirstTagName($v)); $cfg = $conf['externalBlocks.'][$tagName . '.'] ?? []; if ($cfg === []) { continue; } if ($cfg['callRecursive'] ?? false) { $parts[$k] = $this->parseFunc($htmlParser->removeFirstAndLastTag($v), $conf); if (!($cfg['callRecursive.']['dontWrapSelf'] ?? false)) { if ($cfg['callRecursive.']['alternativeWrap'] ?? false) { $parts[$k] = $this->wrap($parts[$k], $cfg['callRecursive.']['alternativeWrap']); } else { if (is_array($cfg['callRecursive.']['tagStdWrap.'] ?? false)) { $tag = $this->stdWrap($tag, $cfg['callRecursive.']['tagStdWrap.']); // Update $tagName in case $tag has been modified (eg by remap), so closing tag matches the opening $tagName = strtolower($htmlParser->getFirstTagName($tag)); } $parts[$k] = $tag . $parts[$k] . ''; } } } elseif ($cfg['HTMLtableCells'] ?? false) { $rowParts = $htmlParser->splitIntoBlock('tr', $parts[$k]); foreach ($rowParts as $kk => $vv) { if ($kk % 2) { $colParts = $htmlParser->splitIntoBlock('td,th', $vv); $cc = 0; foreach ($colParts as $kkk => $vvv) { if ($kkk % 2) { $cc++; $tag = $htmlParser->getFirstTag($vvv); $tagName = strtolower($htmlParser->getFirstTagName($vvv)); $colParts[$kkk] = $htmlParser->removeFirstAndLastTag($vvv); if (($cfg['HTMLtableCells.'][$cc . '.']['callRecursive'] ?? false) || (!isset($cfg['HTMLtableCells.'][$cc . '.']['callRecursive']) && ($cfg['HTMLtableCells.']['default.']['callRecursive'] ?? false))) { if ($cfg['HTMLtableCells.']['addChr10BetweenParagraphs'] ?? false) { $colParts[$kkk] = str_replace( '

', '

' . LF . '

', $colParts[$kkk] ); } $colParts[$kkk] = $this->parseFunc($colParts[$kkk], $conf); } $tagStdWrap = is_array($cfg['HTMLtableCells.'][$cc . '.']['tagStdWrap.'] ?? false) ? $cfg['HTMLtableCells.'][$cc . '.']['tagStdWrap.'] : ($cfg['HTMLtableCells.']['default.']['tagStdWrap.'] ?? null); if (is_array($tagStdWrap)) { $tag = $this->stdWrap($tag, $tagStdWrap); } $stdWrap = is_array($cfg['HTMLtableCells.'][$cc . '.']['stdWrap.'] ?? false) ? $cfg['HTMLtableCells.'][$cc . '.']['stdWrap.'] : ($cfg['HTMLtableCells.']['default.']['stdWrap.'] ?? null); if (is_array($stdWrap)) { $colParts[$kkk] = $this->stdWrap($colParts[$kkk], $stdWrap); } $colParts[$kkk] = $tag . $colParts[$kkk] . ''; } } $rowParts[$kk] = implode('', $colParts); } } $parts[$k] = implode('', $rowParts); } if (is_array($cfg['stdWrap.'] ?? false)) { $parts[$k] = $this->stdWrap($parts[$k], $cfg['stdWrap.']); } } else { $parts[$k] = $this->parseFuncInternal($parts[$k], $conf); } } $result = implode('', $parts); if ($conf['htmlSanitize']) { $result = $this->stdWrap_htmlSanitize($result, $conf['htmlSanitize.'] ?? []); } return $result; } /** * Helper function for parseFunc() * * @param string $theValue The value to process. * @param array $conf TypoScript configuration for parseFunc * @return string The processed value */ protected function parseFuncInternal($theValue, $conf) { if (!empty($conf['if.']) && !$this->checkIf($conf['if.'])) { return $theValue; } // Indicates that the data is from within a tag. $inside = false; // Pointer to the total string position $pointer = 0; // Loaded with the current typo-tag if any. $currentTag = null; $stripNL = 0; $contentAccum = []; $contentAccumP = 0; $allowTags = GeneralUtility::trimExplode(',', strtolower($conf['allowTags'] ?? ''), true); if (in_array('*', $allowTags, true)) { $allowTags = ['*']; } $denyTags = GeneralUtility::trimExplode(',', strtolower($conf['denyTags'] ?? ''), true); if (in_array('*', $denyTags, true)) { $denyTags = ['*']; } $totalLen = strlen($theValue); do { if (!$inside) { if ($currentTag === null) { // These operations should only be performed on code outside the typotags... // data: this checks that we enter tags ONLY if the first char in the tag is alphanumeric OR '/' $len_p = 0; $c = 100; do { $len = strcspn(substr($theValue, $pointer + $len_p), '<'); $len_p += $len + 1; $ordValue = strtolower(substr($theValue, $pointer + $len_p, 1)); $endChar = empty($ordValue) ? 0 : ord($ordValue); unset($ordValue); $c--; } while ($c > 0 && $endChar && ($endChar < 97 || $endChar > 122) && $endChar != 47); $len = $len_p - 1; } else { $len = $this->getContentLengthOfCurrentTag($theValue, $pointer, (string)$currentTag[0]); } // $data is the content until the next logger->debug('Stripping new lines failed for "{data}"', ['data' => $data]); $data = ''; } } // These operations should only be performed on code outside the tags... if (!is_array($currentTag)) { // Short if (isset($conf['short.']) && is_array($conf['short.'])) { $shortWords = $conf['short.']; krsort($shortWords); foreach ($shortWords as $key => $val) { if (is_string($val)) { $data = str_replace($key, $val, $data); } } } // stdWrap if (isset($conf['plainTextStdWrap.']) && is_array($conf['plainTextStdWrap.'])) { $data = $this->stdWrap($data, $conf['plainTextStdWrap.']); } // userFunc if ($conf['userFunc'] ?? false) { $data = $this->callUserFunction($conf['userFunc'], $conf['userFunc.'] ?? [], $data); } } // Search for tags to process in current data and // call this method recursively if found if (str_contains($data, '<') && isset($conf['tags.']) && is_array($conf['tags.'])) { // @todo probably use a DOM tree traversal for the whole stuff // This iterations basically re-processes the markup string, as // long as there are `<$tag ` or `<$tag>` "tags" found... foreach (array_keys($conf['tags.']) as $tag) { // only match tag `a` in `` but not in `` if (preg_match('#<' . $tag . '[\s/>]#', $data)) { $data = $this->parseFuncInternal($data, $conf); break; } } } if (!is_array($currentTag) && ($conf['makelinks'] ?? false)) { $data = $this->http_makelinks($data, $conf['makelinks.']['http.'] ?? []); $data = $this->mailto_makelinks($data, $conf['makelinks.']['mailto.'] ?? []); } $contentAccum[$contentAccumP] = ($contentAccum[$contentAccumP] ?? '') . $data; $inside = true; } else { // tags $len = strcspn(substr($theValue, $pointer), '>') + 1; $data = substr($theValue, $pointer, $len); if (str_ends_with($data, '/>') && !str_starts_with($data, ' if (str_starts_with($tag[0], '/')) { $tag[0] = substr($tag[0], 1); $tag['out'] = 1; } if ($conf['tags.'][$tag[0]] ?? false) { $treated = false; $stripNL = false; // in-tag if (!$currentTag && (!isset($tag['out']) || !$tag['out'])) { // $currentTag (array!) is the tag we are currently processing $currentTag = $tag; $contentAccumP++; $treated = true; // in-out-tag: img and other empty tags if (preg_match('/^(area|base|br|col|hr|img|input|meta|param)$/i', (string)$tag[0])) { $tag['out'] = 1; } } // out-tag if (isset($currentTag[0], $tag['out']) && $currentTag[0] === $tag[0] && $tag['out']) { $theName = $conf['tags.'][$tag[0]]; $theConf = $conf['tags.'][$tag[0] . '.']; // This flag indicates, that NL- (13-10-chars) should be stripped first and last. $stripNL = (bool)($theConf['stripNL'] ?? false); // This flag indicates, that this TypoTag section should NOT be included in the nonTypoTag content. $breakOut = (bool)($theConf['breakoutTypoTagContent'] ?? false); $this->parameters = []; if (isset($currentTag[1])) { // decode HTML entities in attributes, since they're processed $params = GeneralUtility::get_tag_attributes((string)$currentTag[1], true); foreach ($params as $option => $val) { // contains non-encoded values $this->parameters[strtolower($option)] = $val; } $this->parameters['allParams'] = trim((string)$currentTag[1]); } // Removes NL in the beginning and end of the tag-content AND at the end of the currentTagBuffer. // $stripNL depends on the configuration of the current tag if ($stripNL) { $contentAccum[$contentAccumP - 1] = preg_replace('/' . CR . '?' . LF . '[ ]*$/', '', $contentAccum[$contentAccumP - 1] ?? ''); $contentAccum[$contentAccumP] = preg_replace('/^[ ]*' . CR . '?' . LF . '/', '', $contentAccum[$contentAccumP] ?? ''); $contentAccum[$contentAccumP] = preg_replace('/' . CR . '?' . LF . '[ ]*$/', '', $contentAccum[$contentAccumP] ?? ''); } $this->data[$this->currentValKey] = $contentAccum[$contentAccumP] ?? null; $newInput = $this->cObjGetSingle($theName, $theConf, '/parseFunc/.tags.' . $tag[0]); // fetch the content object $contentAccum[$contentAccumP] = $newInput; $contentAccumP++; // If the TypoTag section if (!$breakOut) { if (!isset($contentAccum[$contentAccumP - 2])) { $contentAccum[$contentAccumP - 2] = ''; } $contentAccum[$contentAccumP - 2] .= ($contentAccum[$contentAccumP - 1] ?? '') . ($contentAccum[$contentAccumP] ?? ''); unset($contentAccum[$contentAccumP]); unset($contentAccum[$contentAccumP - 1]); $contentAccumP -= 2; } $currentTag = null; $treated = true; } // other tags if (!$treated) { $contentAccum[$contentAccumP] .= $data; } } else { $contentAccum[$contentAccumP] = $contentAccum[$contentAccumP] ?? ''; // If a tag was not a typo tag, then it is just added to the content $stripNL = false; if ( // Neither allowTags or denyTags set, thus everything is allowed ($denyTags === [] && $allowTags === []) // Explicitly allowed || ($allowTags !== [] && in_array((string)$tag[0], $allowTags, true)) // Explicitly denied or everything "denied" (except for the explicitly allowed) || ($denyTags !== [] && $denyTags !== ['*'] && !in_array((string)$tag[0], $denyTags)) // All tags are allowed, but not in the denied list above, so this is OK || ($allowTags === ['*'] && !in_array((string)$tag[0], $denyTags)) ) { $contentAccum[$contentAccumP] .= $data; } else { $contentAccum[$contentAccumP] .= htmlspecialchars($data); } } $inside = false; } $pointer += $len; } while ($pointer < $totalLen); // Parsing nonTypoTag content (all even keys): reset($contentAccum); $contentAccumCount = count($contentAccum); for ($a = 0; $a < $contentAccumCount; $a++) { if ($a % 2 != 1) { // stdWrap if (isset($conf['nonTypoTagStdWrap.']) && is_array($conf['nonTypoTagStdWrap.'])) { $contentAccum[$a] = $this->stdWrap((string)($contentAccum[$a] ?? ''), $conf['nonTypoTagStdWrap.']); } // userFunc if (!empty($conf['nonTypoTagUserFunc'])) { $contentAccum[$a] = $this->callUserFunction($conf['nonTypoTagUserFunc'], $conf['nonTypoTagUserFunc.'] ?? [], (string)($contentAccum[$a] ?? '')); } } } return implode('', $contentAccum); } /** * Lets you split the content by LF and process each line independently. Used to format content made with the RTE. * * @param string $theValue The input value * @param array $conf TypoScript options * @return string The processed input value being returned; Split lines imploded by LF again. * @internal */ public function encaps_lineSplit($theValue, array $conf): string { if ((string)$theValue === '') { return ''; } $lParts = explode(LF, $theValue); // When the last element is an empty linebreak we need to remove it, otherwise we will have a duplicate empty line. $lastPartIndex = count($lParts) - 1; if ($lParts[$lastPartIndex] === '' && trim($lParts[$lastPartIndex - 1], CR) === '') { array_pop($lParts); } $encapTags = GeneralUtility::trimExplode(',', strtolower($conf['encapsTagList'] ?? ''), true); $defaultAlign = trim((string)$this->stdWrapValue('defaultAlign', $conf)); $str_content = ''; foreach ($lParts as $k => $l) { $sameBeginEnd = false; $emptyTag = false; $l = trim($l); $attrib = []; $nonWrapped = false; $tagName = ''; if (isset($l[0]) && $l[0] === '<' && str_ends_with($l, '>')) { $fwParts = explode('>', substr($l, 1), 2); [$tagName] = explode(' ', $fwParts[0], 2); if (!$fwParts[1]) { if (str_ends_with($tagName, '/')) { $tagName = substr($tagName, 0, -1); } if (str_ends_with($fwParts[0], '/')) { $sameBeginEnd = true; $emptyTag = true; // decode HTML entities, they're encoded later again $attrib = GeneralUtility::get_tag_attributes('<' . substr($fwParts[0], 0, -1) . '>', true); } } else { $backParts = GeneralUtility::revExplode('<', substr($fwParts[1], 0, -1), 2); // decode HTML entities, they're encoded later again $attrib = GeneralUtility::get_tag_attributes('<' . $fwParts[0] . '>', true); $str_content = $backParts[0]; // Ensure that $backParts could be exploded into 2 items if (isset($backParts[1])) { $sameBeginEnd = strtolower(substr($backParts[1], 1, strlen($tagName))) === strtolower($tagName); } } } if ($sameBeginEnd && in_array(strtolower($tagName), $encapTags)) { $uTagName = strtoupper($tagName); $uTagName = strtoupper($conf['remapTag.'][$uTagName] ?? $uTagName); } else { $uTagName = strtoupper($conf['nonWrappedTag'] ?? ''); // The line will be wrapped: $uTagName should not be an empty tag $emptyTag = false; $str_content = $lParts[$k]; $nonWrapped = true; $attrib = []; } // Wrapping all inner-content: if (is_array($conf['innerStdWrap_all.'] ?? null)) { $str_content = (string)$this->stdWrap($str_content, $conf['innerStdWrap_all.']); } if ($uTagName) { // Setting common attributes if (isset($conf['addAttributes.'][$uTagName . '.']) && is_array($conf['addAttributes.'][$uTagName . '.'])) { foreach ($conf['addAttributes.'][$uTagName . '.'] as $kk => $vv) { if (!is_array($vv)) { if ((string)($conf['addAttributes.'][$uTagName . '.'][$kk . '.']['setOnly'] ?? '') === 'blank') { if ((string)($attrib[$kk] ?? '') === '') { $attrib[$kk] = $vv; } } elseif ((string)($conf['addAttributes.'][$uTagName . '.'][$kk . '.']['setOnly'] ?? '') === 'exists') { if (!isset($attrib[$kk])) { $attrib[$kk] = $vv; } } else { $attrib[$kk] = $vv; } } } } // Wrapping all inner-content: if (isset($conf['encapsLinesStdWrap.'][$uTagName . '.']) && is_array($conf['encapsLinesStdWrap.'][$uTagName . '.'])) { $str_content = (string)$this->stdWrap($str_content, $conf['encapsLinesStdWrap.'][$uTagName . '.']); } // Default align if ((!isset($attrib['align']) || !$attrib['align']) && $defaultAlign) { $attrib['align'] = $defaultAlign; } // implode (insecure) attributes, that's why `htmlspecialchars` is used here $params = GeneralUtility::implodeAttributes($attrib, true); if (!isset($conf['removeWrapping']) || !$conf['removeWrapping'] || ($emptyTag && $conf['removeWrapping.']['keepSingleTag'])) { $selfClosingTagList = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr']; if ($emptyTag && in_array(strtolower($uTagName), $selfClosingTagList, true)) { $str_content = '<' . strtolower($uTagName) . (trim($params) ? ' ' . trim($params) : '') . ' />'; } else { $str_content = '<' . strtolower($uTagName) . (trim($params) ? ' ' . trim($params) : '') . '>' . $str_content . ''; } } } if ($nonWrapped && isset($conf['wrapNonWrappedLines']) && $conf['wrapNonWrappedLines']) { $str_content = $this->wrap($str_content, $conf['wrapNonWrappedLines']); } $lParts[$k] = $str_content; } return implode(LF, $lParts); } /** * Finds URLs in text and makes it to a real link. * Will find all strings prefixed with "http://" and "https://" in the $data string and make them into a link, * linking to the URL we should have found. * * Helper method of parseFuncInternal(). * * @param string $data The string in which to search for "http:// * @param array $conf Configuration for makeLinks, see link * @return string The processed input string, being returned. */ protected function http_makelinks(string $data, array $conf): string { $parts = []; foreach (['http://', 'https://'] as $scheme) { $textpieces = explode($scheme, $data); $pieces = count($textpieces); $textstr = $textpieces[0]; for ($i = 1; $i < $pieces; $i++) { $len = strcspn($textpieces[$i], chr(32) . "\t" . CRLF); if (!(trim(substr($textstr, -1)) === '' && $len)) { $textstr .= $scheme . $textpieces[$i]; continue; } $lastChar = substr($textpieces[$i], $len - 1, 1); if (!preg_match('/[A-Za-z0-9\\/#_-]/', $lastChar)) { $len--; } // Included '\/' 3/12 $parts[0] = substr($textpieces[$i], 0, $len); $parts[1] = substr($textpieces[$i], $len); $keep = $conf['keep'] ?? ''; $linkParts = parse_url($scheme . $parts[0]); // Check if link couldn't be parsed properly if (!is_array($linkParts)) { $textstr .= $scheme . $textpieces[$i]; continue; } $linktxt = ''; if (str_contains($keep, 'scheme')) { $linktxt = $scheme; } $linktxt .= $linkParts['host'] ?? ''; if (str_contains($keep, 'path')) { $linktxt .= ($linkParts['path'] ?? ''); // Added $linkParts['query'] 3/12 if (str_contains($keep, 'query') && $linkParts['query']) { $linktxt .= '?' . $linkParts['query']; } elseif (($linkParts['path'] ?? '') === '/') { $linktxt = substr($linktxt, 0, -1); } } $typolinkConfiguration = $conf; $typolinkConfiguration['parameter'] = $scheme . $parts[0]; $textstr .= $this->typoLink($linktxt, $typolinkConfiguration) . $parts[1]; } $data = $textstr; } return $textstr; } /** * Will find all strings prefixed with "mailto:" in the $data string and make them into a link, * linking to the email address they point to. * * Helper method of parseFuncInternal(). * * @param string $data The string in which to search for "mailto: * @param array $conf Configuration for makeLinks, see link * @return string The processed input string, being returned. */ protected function mailto_makelinks(string $data, array $conf): string { $conf = (array)$conf; $parts = []; // split by mailto logic $textpieces = explode('mailto:', $data); $pieces = count($textpieces); $textstr = $textpieces[0]; for ($i = 1; $i < $pieces; $i++) { $len = strcspn($textpieces[$i], chr(32) . "\t" . CRLF); if (trim(substr($textstr, -1)) === '' && $len) { $lastChar = substr($textpieces[$i], $len - 1, 1); if (!preg_match('/[A-Za-z0-9]/', $lastChar)) { $len--; } $parts[0] = substr($textpieces[$i], 0, $len); $parts[1] = substr($textpieces[$i], $len); $linktxt = (string)preg_replace('/\\?.*/', '', $parts[0]); $typolinkConfiguration = $conf; $typolinkConfiguration['parameter'] = 'mailto:' . $parts[0]; $textstr .= (string)$this->typoLink($linktxt, $typolinkConfiguration) . $parts[1]; } else { $textstr .= 'mailto:' . $textpieces[$i]; } } return $textstr; } /** * Creates and returns a TypoScript "imgResource". * The value ($file) can either be a file reference (TypoScript resource) or the string "GIFBUILDER". * In the first case a current image is returned, possibly scaled down or otherwise processed. * In the latter case a GIFBUILDER image is returned; This means an image is made by TYPO3 from layers of elements as GIFBUILDER defines. * In the function IMG_RESOURCE() this function is called like $this->getImgResource($conf['file'], $conf['file.']); * * Structure of the returned info array: * 0 => width * 1 => height * 2 => file extension * 3 => file name * origFile => original file name * origFile_mtime => original file mtime * -- only available if processed via FAL: -- * originalFile => original file object * processedFile => processed file object * fileCacheHash => checksum of processed file * * @param string|File|FileReference $file A "imgResource" TypoScript data type. Either a TypoScript file resource, a file * or a file reference object or the string GIFBUILDER. See description above. * @param array $fileArray TypoScript properties for the imgResource type * @see cImage() */ public function getImgResource($file, array $fileArray): ?ImageResource { $importedFile = null; $fileReference = null; if (empty($file) && empty($fileArray)) { return null; } $imageResource = null; if ($file === 'GIFBUILDER') { $gifBuilder = GeneralUtility::makeInstance(GifBuilder::class); $gifBuilder->start($fileArray, $this->data); $imageResource = $gifBuilder->gifBuild(); } else { if ($file instanceof File) { $fileObject = $file; } elseif ($file instanceof FileReference) { $fileReference = $file; $fileObject = $file->getOriginalFile(); } else { try { if (isset($fileArray['import.']) && $fileArray['import.']) { $importedFile = trim((string)$this->stdWrap('', $fileArray['import.'])); if (!empty($importedFile)) { $file = $importedFile; } } if (MathUtility::canBeInterpretedAsInteger($file)) { $treatIdAsReference = $this->stdWrapValue('treatIdAsReference', $fileArray); if (!empty($treatIdAsReference)) { $fileReference = $this->resourceFactory->getFileReferenceObject((int)$file); $fileObject = $fileReference->getOriginalFile(); } else { $fileObject = $this->resourceFactory->getFileObject((int)$file); } } elseif (preg_match('/^(0|[1-9][0-9]*):/', $file)) { // combined identifier $fileObject = $this->resourceFactory->retrieveFileOrFolderObject($file); } else { if ($importedFile && !empty($fileArray['import'])) { $file = $fileArray['import'] . $file; } $fileObject = $this->resourceFactory->retrieveFileOrFolderObject($file); } } catch (Exception $exception) { $this->logger->warning('The image "{file}" could not be found and won\'t be included in frontend output', [ 'file' => $file, 'exception' => $exception, ]); return null; } } if ($fileObject instanceof File) { $processingConfiguration['width'] = $this->stdWrapValue('width', $fileArray); $processingConfiguration['height'] = $this->stdWrapValue('height', $fileArray); $processingConfiguration['fileExtension'] = $this->stdWrapValue('ext', $fileArray); $processingConfiguration['maxWidth'] = (int)$this->stdWrapValue('maxW', $fileArray); $processingConfiguration['maxHeight'] = (int)$this->stdWrapValue('maxH', $fileArray); $processingConfiguration['minWidth'] = (int)$this->stdWrapValue('minW', $fileArray); $processingConfiguration['minHeight'] = (int)$this->stdWrapValue('minH', $fileArray); $processingConfiguration['noScale'] = $this->stdWrapValue('noScale', $fileArray); $processingConfiguration['sample'] = (bool)$this->stdWrapValue('sample', $fileArray); $processingConfiguration['additionalParameters'] = $this->stdWrapValue('params', $fileArray); $processingConfiguration['frame'] = (int)$this->stdWrapValue('frame', $fileArray); if ($fileReference === null) { $processingConfiguration['crop'] = $this->getCropAreaFromFromTypoScriptSettings($fileObject, $fileArray); } else { $processingConfiguration['crop'] = $this->getCropAreaFromFileReference($fileReference, $fileArray); } // Possibility to cancel/force profile extraction // see $GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_stripColorProfileParameters'] if (isset($fileArray['stripProfile'])) { $processingConfiguration['stripProfile'] = $fileArray['stripProfile']; } // Check if we can handle this type of file for editing if ($fileObject->isImage()) { $maskArray = $fileArray['m.'] ?? false; // Must render mask images and include in hash-calculating // - otherwise we cannot be sure the filename is unique for the setup! if (is_array($maskArray)) { $processingConfiguration['maskImages']['maskImage'] = $this->getImgResource($maskArray['mask'] ?? '', $maskArray['mask.'] ?? [])?->getProcessedFile(); $processingConfiguration['maskImages']['backgroundImage'] = $this->getImgResource($maskArray['bgImg'] ?? '', $maskArray['bgImg.'] ?? [])?->getProcessedFile(); $processingConfiguration['maskImages']['maskBottomImage'] = $this->getImgResource($maskArray['bottomImg'] ?? '', $maskArray['bottomImg.'] ?? [])?->getProcessedFile(); $processingConfiguration['maskImages']['maskBottomImageMask'] = $this->getImgResource($maskArray['bottomImg_mask'] ?? '', $maskArray['bottomImg_mask.'] ?? [])?->getProcessedFile(); } $processedFileObject = $fileObject->process(ProcessedFile::CONTEXT_IMAGECROPSCALEMASK, $processingConfiguration); if ($processedFileObject->isProcessed()) { $imageResource = ImageResource::createFromProcessedFile($processedFileObject); } } } elseif ($fileObject instanceof ProcessedFile) { $imageResource = ImageResource::createFromProcessedFile($fileObject); } } return $this->eventDispatcher->dispatch( new AfterImageResourceResolvedEvent($file, $fileArray, $imageResource) )->getImageResource(); } /** * Returns an ImageManipulation\Area object for the given cropVariant (or 'default') * or null if the crop settings or crop area is empty. * * The cropArea from file reference is used, if not set in TypoScript. * * Example TypoScript settings: * file.crop = * OR * file.crop = 50,50,100,100 * OR * file.crop.data = file:current:crop * * @param array $fileArray TypoScript properties for the imgResource type */ protected function getCropAreaFromFileReference(FileReference $fileReference, array $fileArray): ?Area { // Use cropping area from file reference if nothing is configured in TypoScript. if (!isset($fileArray['crop']) && !isset($fileArray['crop.'])) { // Set crop variant from TypoScript settings. If not set, use default. $cropVariant = $fileArray['cropVariant'] ?? 'default'; $fileCropArea = $this->createCropAreaFromJsonString((string)$fileReference->getProperty('crop'), $cropVariant); return $fileCropArea->isEmpty() ? null : $fileCropArea->makeAbsoluteBasedOnFile($fileReference); } return $this->getCropAreaFromFromTypoScriptSettings($fileReference, $fileArray); } /** * Returns an ImageManipulation\Area object for the given cropVariant (or 'default') * or null if the crop settings or crop area is empty. */ protected function getCropAreaFromFromTypoScriptSettings(FileInterface $file, array $fileArray): ?Area { $cropArea = null; // Resolve TypoScript configured cropping. $cropSettings = isset($fileArray['crop.']) ? $this->stdWrap($fileArray['crop'] ?? '', $fileArray['crop.']) : ($fileArray['crop'] ?? null); if (is_string($cropSettings)) { // Set crop variant from TypoScript settings. If not set, use default. $cropVariant = $fileArray['cropVariant'] ?? 'default'; // Get cropArea from CropVariantCollection, if cropSettings is a valid json. // CropVariantCollection::create does json_decode. $jsonCropArea = $this->createCropAreaFromJsonString($cropSettings, $cropVariant); $cropArea = $jsonCropArea->isEmpty() ? null : $jsonCropArea->makeAbsoluteBasedOnFile($file); // Cropping is configured in TypoScript in the following way: file.crop = 50,50,100,100 if ($jsonCropArea->isEmpty() && preg_match('/^[0-9]+,[0-9]+,[0-9]+,[0-9]+$/', $cropSettings)) { $cropSettings = explode(',', $cropSettings); if (count($cropSettings) === 4) { $cropSettings = array_map(floatval(...), $cropSettings); $stringCropArea = GeneralUtility::makeInstance(Area::class, ...$cropSettings); $cropArea = $stringCropArea->isEmpty() ? null : $stringCropArea; } } } return $cropArea; } /** * Takes a JSON string and creates CropVariantCollection and fetches the corresponding CropArea for that. */ protected function createCropAreaFromJsonString(string $cropSettings, string $cropVariant): Area { return CropVariantCollection::create($cropSettings)->getCropArea($cropVariant); } /** * Returns the value for the field from $this->data. If "//" is found in the $field value that token will split * the field values apart and the first field having a non-blank value will be returned. * * @param string $field The fieldname, e.g. "title" or "navtitle // title" (in the latter case the value of * $this->data[navtitle] is returned if not blank, otherwise $this->data[title] will be) * @return string|null * @internal */ public function getFieldVal($field) { if (!str_contains($field, '//')) { return $this->data[trim($field)] ?? null; } $sections = GeneralUtility::trimExplode('//', $field, true); foreach ($sections as $k) { if ((string)($this->data[$k] ?? '') !== '') { return $this->data[$k]; } } return ''; } /** * Implements the TypoScript data type "getText". This takes a string with parameters * and based on those a value from somewhere in the system is returned. * * @param mixed $string The parameter string, eg. "field : title" or "field : navtitle // field : title" * In the latter case and example of how the value is FIRST split by "//" is shown. Should be a string obviously * @param array|null $fieldArray Alternative field array; If you set this to an array this variable will be used to * look up values for the "field" key. Otherwise, the current page record is used. * @return mixed The value fetched */ public function getData($string, $fieldArray = null) { if (!is_array($fieldArray)) { $fieldArray = $this->getRequest()->getAttribute('frontend.page.information')->getPageRecord(); } $retVal = ''; // @todo: getData should not be called with non-string as $string. example trigger: // SecureHtmlRenderingTest htmlViewHelperAvoidsCrossSiteScripting set #07 PHP 8 $sections = is_string($string) ? explode('//', $string) : []; foreach ($sections as $secVal) { if ($retVal) { break; } $parts = explode(':', $secVal, 2); $type = strtolower(trim($parts[0])); $typesWithOutParameters = ['level', 'date', 'current', 'pagelayout', 'applicationcontext']; $key = trim($parts[1] ?? ''); if (($key != '') || in_array($type, $typesWithOutParameters)) { switch ($type) { case 'gp': // Merge GET and POST and get $key out of the merged array $requestParameters = $this->getRequest()->getQueryParams(); $requestParameters = array_replace_recursive($requestParameters, (array)$this->getRequest()->getParsedBody()); $retVal = $this->getGlobal($key, $requestParameters); break; case 'request': $retVal = $this->getValueFromRecursiveData(GeneralUtility::trimExplode('|', $key), $this->getRequest()); break; case 'tsfe': $valueParts = GeneralUtility::trimExplode('|', $key); if (($valueParts[0] ?? '') === 'fe_user') { $frontendUser = $this->getRequest()->getAttribute('frontend.user'); array_shift($valueParts); $retVal = $this->getValueFromRecursiveData($valueParts, $frontendUser); } elseif (($valueParts[0] ?? '') === 'linkVars') { $typoScriptConfigArray = $this->getRequest()->getAttribute('frontend.typoscript')->getConfigArray(); $typoScriptConfigLinkVars = (string)($typoScriptConfigArray['linkVars'] ?? ''); $retVal = $this->linkVarsCalculator->getAllowedLinkVarsFromRequest($typoScriptConfigLinkVars, $this->getRequest()->getQueryParams(), $this->context); } elseif (($valueParts[0] ?? '') === 'id') { $retVal = $this->getRequest()->getAttribute('frontend.page.information')->getId(); } elseif (($valueParts[0] ?? '') === 'contentPid') { $retVal = $this->getRequest()->getAttribute('frontend.page.information')->getContentFromPid(); } elseif (($valueParts[0] ?? '') === 'rootLine') { array_shift($valueParts); $retVal = $this->getValueFromRecursiveData($valueParts, $this->getRequest()->getAttribute('frontend.page.information')->getRootLine()); } elseif (($valueParts[0] ?? '') === 'page') { array_shift($valueParts); $retVal = $this->getValueFromRecursiveData($valueParts, $this->getRequest()->getAttribute('frontend.page.information')->getPageRecord()); } elseif (($valueParts[0] ?? '') === 'config' && ($valueParts[1] ?? '') === 'rootLine') { array_shift($valueParts); array_shift($valueParts); $retVal = $this->getValueFromRecursiveData($valueParts, $this->getRequest()->getAttribute('frontend.page.information')->getLocalRootLine()); } break; case 'getenv': $retVal = getenv($key); break; case 'getindpenv': $normalizedParams = $this->getRequest()->getAttribute('normalizedParams'); $retVal = match ($key) { 'HTTP_HOST' => $normalizedParams->getHttpHost(), 'TYPO3_HOST_ONLY' => $normalizedParams->getRequestHostOnly(), 'TYPO3_PORT' => $normalizedParams->getRequestPort(), 'PATH_INFO' => $normalizedParams->getPathInfo(), 'QUERY_STRING' => $normalizedParams->getQueryString(), 'REQUEST_URI' => $normalizedParams->getRequestUri(), 'HTTP_REFERER' => $normalizedParams->getHttpReferer(), 'TYPO3_REQUEST_HOST' => $normalizedParams->getRequestHost(), 'TYPO3_REQUEST_URL' => $normalizedParams->getRequestUrl(), 'TYPO3_REQUEST_SCRIPT' => $normalizedParams->getRequestScript(), 'TYPO3_REQUEST_DIR' => $normalizedParams->getRequestDir(), 'TYPO3_SITE_URL' => $normalizedParams->getSiteUrl(), 'TYPO3_SITE_SCRIPT' => $normalizedParams->getSiteScript(), 'TYPO3_SSL' => $normalizedParams->isHttps(), 'TYPO3_REV_PROXY' => $normalizedParams->isBehindReverseProxy(), 'SCRIPT_NAME' => $normalizedParams->getScriptName(), 'TYPO3_DOCUMENT_ROOT' => $normalizedParams->getDocumentRoot(), 'SCRIPT_FILENAME' => $normalizedParams->getScriptFilename(), 'REMOTE_ADDR' => $normalizedParams->getRemoteAddress(), 'REMOTE_HOST' => $normalizedParams->getRemoteHost(), 'HTTP_USER_AGENT' => $normalizedParams->getHttpUserAgent(), 'HTTP_ACCEPT_LANGUAGE' => $normalizedParams->getHttpAcceptLanguage(), default => null, }; break; case 'field': $retVal = $this->getGlobal($key, $fieldArray); break; case 'file': $retVal = $this->getFileDataKey($key); break; case 'asset': case 'path': $options = null; if ($type === 'path') { $options = new UriGenerationOptions(cacheBusting: false); } try { $resource = $this->systemResourceFactory->createPublicResource($key); $retVal = (string)$this->systemResourcePublisher->generateUri($resource, $this->getRequest(), $options); } catch (Exception) { $retVal = null; } break; case 'parameters': $retVal = $this->parameters[$key] ?? null; break; case 'register': if ($key === 'SYS_LASTCHANGED') { // b/w compat layer: SYS_LASTCHANGED has been a register entry until TYPO3 v14. It is now part // of a request attribute. The register access via TS should continue to work, though. $retVal = $this->getRequest()->getAttribute('frontend.page.parts')->getLastChanged(); } else { $retVal = $this->getRequest()->getAttribute('frontend.register.stack')->current()->get($key); } break; case 'global': $retVal = $this->getGlobal($key); break; case 'level': $localRootLine = $this->getRequest()->getAttribute('frontend.page.information')->getLocalRootLine(); $retVal = count($localRootLine) - 1; break; case 'leveltitle': $keyParts = GeneralUtility::trimExplode(',', $key); $pointer = (int)($keyParts[0] ?? 0); $slide = $keyParts[1] ?? ''; $localRootLine = $this->getRequest()->getAttribute('frontend.page.information')->getLocalRootLine(); $numericKey = $this->getKey($pointer, $localRootLine); $retVal = $this->rootLineValue($numericKey, 'title', strtolower($slide) === 'slide'); break; case 'levelmedia': $keyParts = GeneralUtility::trimExplode(',', $key); $pointer = (int)($keyParts[0] ?? 0); $slide = $keyParts[1] ?? ''; $localRootLine = $this->getRequest()->getAttribute('frontend.page.information')->getLocalRootLine(); $numericKey = $this->getKey($pointer, $localRootLine); $retVal = $this->rootLineValue($numericKey, 'media', strtolower($slide) === 'slide'); break; case 'leveluid': $localRootLine = $this->getRequest()->getAttribute('frontend.page.information')->getLocalRootLine(); $numericKey = $this->getKey((int)$key, $localRootLine); $retVal = $this->rootLineValue($numericKey, 'uid'); break; case 'levelfield': $keyParts = GeneralUtility::trimExplode(',', $key); $pointer = (int)($keyParts[0] ?? 0); $field = $keyParts[1] ?? ''; $slide = $keyParts[2] ?? ''; $localRootLine = $this->getRequest()->getAttribute('frontend.page.information')->getLocalRootLine(); $numericKey = $this->getKey($pointer, $localRootLine); $retVal = $this->rootLineValue($numericKey, $field, strtolower($slide) === 'slide'); break; case 'fullrootline': $keyParts = GeneralUtility::trimExplode(',', $key); $pointer = (int)($keyParts[0] ?? 0); $field = $keyParts[1] ?? ''; $slide = $keyParts[2] ?? ''; $rootLine = $this->getRequest()->getAttribute('frontend.page.information')->getRootLine(); $localRootLine = $this->getRequest()->getAttribute('frontend.page.information')->getLocalRootLine(); $fullKey = $pointer - count($localRootLine) + count($rootLine); if ($fullKey >= 0) { $retVal = $this->rootLineValue($fullKey, $field, stristr($slide, 'slide') !== false, $rootLine); } break; case 'date': if (!$key) { $key = 'd/m Y'; } $retVal = date($key, $GLOBALS['EXEC_TIME']); break; case 'page': $pageRecord = $this->getRequest()->getAttribute('frontend.page.information')->getPageRecord(); $retVal = $pageRecord[$key] ?? ''; break; case 'pagelayout': $pageInformation = $this->getRequest()->getAttribute('frontend.page.information'); $retVal = $this->pageLayoutResolver->getLayoutIdentifierForPage($pageInformation->getPageRecord(), $pageInformation->getRootLine()); break; case 'current': $retVal = $this->data[$this->currentValKey] ?? null; break; case 'db': $selectParts = GeneralUtility::trimExplode(':', $key, true); if (!isset($selectParts[1])) { break; } $pageRepository = GeneralUtility::makeInstance(PageRepository::class); $dbRecord = $pageRepository->getRawRecord($selectParts[0], (int)$selectParts[1]); if (is_array($dbRecord) && isset($selectParts[2])) { $retVal = $dbRecord[$selectParts[2]] ?? ''; } break; case 'lll': // @todo: Check when/if there are scenarios where attribute 'language' is not yet set in $request. $language = $this->getRequest()->getAttribute('language') ?? $this->getRequest()->getAttribute('site')->getDefaultLanguage(); $languageService = $this->languageServiceFactory->createFromSiteLanguage($language); $retVal = $languageService->sL('LLL:' . $key); break; case 'debug': switch ($key) { case 'rootLine': $retVal = DebugUtility::viewArray($this->getRequest()->getAttribute('frontend.page.information')->getLocalRootLine()); break; case 'fullRootLine': $retVal = DebugUtility::viewArray($this->getRequest()->getAttribute('frontend.page.information')->getRootLine()); break; case 'data': $retVal = DebugUtility::viewArray($this->data); break; case 'register': $retVal = DebugUtility::viewArray($this->getRequest()->getAttribute('frontend.register.stack')->current()); break; case 'page': $retVal = DebugUtility::viewArray($this->getRequest()->getAttribute('frontend.page.information')->getPageRecord()); break; } break; case 'flexform': $keyParts = GeneralUtility::trimExplode(':', $key, true); if (count($keyParts) === 2 && isset($this->data[$keyParts[0]])) { $flexFormContent = $this->data[$keyParts[0]]; if (!empty($flexFormContent)) { $flexFormKey = str_replace('.', '|', $keyParts[1]); $settings = $this->flexFormTools->convertFlexFormContentToArray($flexFormContent); $retVal = $this->getGlobal($flexFormKey, $settings); } } break; case 'session': $keyParts = GeneralUtility::trimExplode('|', $key, true); $sessionKey = array_shift($keyParts); $retVal = $this->getRequest()->getAttribute('frontend.user')->getSessionData($sessionKey); foreach ($keyParts as $keyPart) { if (is_object($retVal)) { $retVal = $retVal->{$keyPart}; } elseif (is_array($retVal)) { $retVal = $retVal[$keyPart]; } else { $retVal = ''; break; } } if (!is_scalar($retVal)) { $retVal = ''; } break; case 'context': [$aspectName, $propertyName] = GeneralUtility::trimExplode(':', $key, true, 2); $retVal = $this->context->getPropertyFromAspect($aspectName, $propertyName, ''); if (is_array($retVal)) { $retVal = implode(',', $retVal); } if (!is_scalar($retVal)) { $retVal = ''; } break; case 'site': $site = $this->getRequest()->getAttribute('site'); if ($key === 'identifier') { $retVal = $site->getIdentifier(); } elseif ($key === 'base') { $retVal = $site->getBase(); } else { try { $retVal = ArrayUtility::getValueByPath($site->getConfiguration(), $key, '.'); } catch (MissingArrayPathException $exception) { $this->logger->notice('Configuration "{key}" is not defined for site "{site}"', ['key' => $key, 'site' => $site->getIdentifier(), 'exception' => $exception]); } } break; case 'sitelanguage': // @todo: Check when/if there are scenarios where attribute 'language' is not yet set in $request. $siteLanguage = $this->getRequest()->getAttribute('language') ?? $this->getRequest()->getAttribute('site')->getDefaultLanguage(); if ($key === 'twoLetterIsoCode') { $key = 'locale:languageCode'; } // Harmonizing the namings from the site configuration value with the TypoScript setting if ($key === 'flag') { $key = 'flagIdentifier'; } // Special handling for the locale object if (str_starts_with($key, 'locale')) { $localeObject = $siteLanguage->getLocale(); if ($key === 'locale') { // backwards-compatibility $retVal = $localeObject->posixFormatted(); } else { $keyParts = explode(':', $key, 2); switch ($keyParts[1] ?? '') { case 'languageCode': $retVal = $localeObject->getLanguageCode(); break; case 'countryCode': $retVal = $localeObject->getCountryCode(); break; case 'full': default: $retVal = $localeObject->getName(); } } } else { $config = $siteLanguage->toArray(); if (isset($config[$key])) { $retVal = $config[$key]; } } break; case 'sitesettings': $siteSettings = $this->getRequest()->getAttribute('site')->getSettings(); $retVal = $siteSettings->get($key, ''); break; case 'applicationcontext': $retVal = Environment::getContext()->__toString(); break; } } } return $this->eventDispatcher->dispatch( new AfterGetDataResolvedEvent($string, $fieldArray, $retVal, $this) )->getResult(); } /** * Gets file information. This is a helper function for the getData() method above, which resolves e.g. * page.10.data = file:current:title * or * page.10.data = file:17:title * * @param string $key A colon-separated key, e.g. 17:name or current:sha1, with the first part being a sys_file uid * or the keyword "current" and the second part being the key of information to get from * file (e.g. "title", "size", "description", etc.) * @return string|int|null The value as retrieved from the file object. */ protected function getFileDataKey($key) { [$fileUidOrCurrentKeyword, $requestedFileInformationKey] = GeneralUtility::trimExplode(':', $key, false, 3); try { if ($fileUidOrCurrentKeyword === 'current') { $fileObject = $this->getCurrentFile(); } elseif (MathUtility::canBeInterpretedAsInteger($fileUidOrCurrentKeyword)) { $fileObject = $this->resourceFactory->getFileObject((int)$fileUidOrCurrentKeyword); } else { $fileObject = null; } } catch (Exception $exception) { $this->logger->warning('The file "{uid}" could not be found and won\'t be included in frontend output', ['uid' => $fileUidOrCurrentKeyword, 'exception' => $exception]); $fileObject = null; } if ($fileObject instanceof FileInterface) { // All properties of the \TYPO3\CMS\Core\Resource\FileInterface are available here: switch ($requestedFileInformationKey) { case 'name': return $fileObject->getName(); case 'uid': if (method_exists($fileObject, 'getUid')) { return $fileObject->getUid(); } return 0; case 'originalUid': if ($fileObject instanceof FileReference) { return $fileObject->getOriginalFile()->getUid(); } return null; case 'size': return $fileObject->getSize(); case 'sha1': return $fileObject->getSha1(); case 'extension': return $fileObject->getExtension(); case 'mimetype': return $fileObject->getMimeType(); case 'contents': return $fileObject->getContents(); case 'publicUrl': return $fileObject->getPublicUrl(); default: // Generic alternative here return $fileObject->getProperty($requestedFileInformationKey); } } else { // @todo fail silently as is common in tslib_content return 'Error: no file object'; } } /** * Returns a value from the current rootline. * * @param int $key Which level in the root line * @param string $field The field in the rootline record to return (a field from the pages table) * @param bool $slideBack If set, then we will traverse through the rootline from outer level towards the root level until the value found is TRUE * @param mixed $altRootLine If you supply an array for this it will be used as an alternative root line array * @return string The value from the field of the rootline. */ protected function rootLineValue($key, $field, $slideBack = false, $altRootLine = ''): string { if (is_array($altRootLine)) { $rootLine = $altRootLine; } else { $rootLine = $this->getRequest()->getAttribute('frontend.page.information')->getLocalRootLine(); } if (!$slideBack) { return $rootLine[$key][$field] ?? ''; } for ($a = $key; $a >= 0; $a--) { $val = $rootLine[$a][$field] ?? ''; if ($val) { return $val; } } return ''; } /** * Return global variable where the input string $var defines array keys separated by "|" * Example: $var = "HTTP_SERVER_VARS | something" will return the value $GLOBALS['HTTP_SERVER_VARS']['something'] value * * @param string $keyString Global var key, eg. "HTTP_GET_VAR" or "HTTP_GET_VARS|id" to get the GET parameter "id" back. * @param array $source Alternative array than $GLOBAL to get variables from. * @return float|int|string Whatever value. If none, then blank string. */ public function getGlobal($keyString, $source = null) { $keys = GeneralUtility::trimExplode('|', $keyString); // remove the first key, as this is only used for finding the original value $rootKey = array_shift($keys); $value = isset($source) ? ($source[$rootKey] ?? '') : ($GLOBALS[$rootKey] ?? ''); return $this->getValueFromRecursiveData($keys, $value); } /** * This method recursively checks for values in methods, arrays, objects, but * does not fall back to $GLOBALS object instead of getGlobal(). */ protected function getValueFromRecursiveData(array $keys, mixed $startValue): int|float|string { $value = $startValue; $numberOfLevels = count($keys); for ($i = 0; $i < $numberOfLevels && isset($value); $i++) { $currentKey = $keys[$i]; if (is_object($value)) { // getter method if (method_exists($value, 'get' . ucfirst($currentKey))) { $getterMethod = 'get' . ucfirst($currentKey); $value = $value->$getterMethod(...)(); // server request attribute, such as "routing" } elseif ($value instanceof ServerRequestInterface) { $value = $value->getAttribute($currentKey); } else { // Public property $value = $value->{$currentKey}; } } elseif (is_array($value)) { $value = $value[$currentKey] ?? ''; } else { $value = ''; break; } } if (!is_scalar($value)) { $value = ''; } return $value; } /** * Processing of key values pointing to entries in $arr; Here negative values are converted to positive keys pointer * to an entry in the array but from behind (based on the negative value). * Example: entrylevel = -1 means that entryLevel ends up pointing at the outermost-level, -2 means the level before the outermost * * @param int $key The integer to transform * @param array $arr array in which the key should be found. * @return int The processed integer key value. * @internal */ public function getKey($key, array $arr): int { $key = (int)$key; if ($key < 0) { $key = count($arr) + $key; } if ($key < 0) { $key = 0; } return $key; } /** * Implements the "typolink" property of stdWrap (and others) * Basically the input string, $linkText, is (typically) wrapped in a -tag linking to some page, email address, * file or URL based on a parameter defined by the configuration array $conf. * This function is best used from internal functions as is. There are some API functions defined after this * function which is more suited for general usage in external applications. * * Generally the concept "typolink" should be used in your own applications as an API for making links to pages with * parameters and more. The reason for this is that you will then automatically make links compatible with all the * centralized functions for URL simulation and manipulation of parameters into hashes and more. * * For many more details on the parameters and how they are interpreted, please see the link to TSref below. * * @param string $linkText The string (text) to link * @param array $conf TypoScript configuration (see link below) * @return string|LinkResult A link-wrapped string. * @see stdWrap() */ public function typoLink(string $linkText, array $conf) { try { $linkResult = $this->createLink($linkText, $conf); } catch (UnableToLinkException $e) { return $e->getLinkText(); } // If flag "returnLast" set, then just return the latest URL / url / target that was built. // This returns the information without being wrapped in a "LinkResult" object. switch ($conf['returnLast'] ?? null) { case 'url': return $linkResult->getUrl(); case 'target': return $linkResult->getTarget(); case 'result': // kept for backwards-compatibility, as this was added in TYPO3 v11 return LinkResult::adapt($linkResult, LinkResult::STRING_CAST_JSON); } $wrap = (string)$this->stdWrapValue('wrap', $conf); if ($conf['ATagBeforeWrap'] ?? false) { $linkResult = $linkResult->withLinkText($this->wrap((string)$linkResult->getLinkText(), $wrap)); return LinkResult::adapt($linkResult)->getHtml(); } $result = LinkResult::adapt($linkResult)->getHtml(); return $this->wrap($result, $wrap); } /** * Similar to ->typoLink(), however it does not evaluate the .wrap and .ATagBeforeWrap * functionality. * * For this reason, it also does not consider the LinkResult functionality, * and "returnLast" logic, as the whole LinkResult object is available. * * It is recommended to use this method when working with PHP and wanting to create * a typolink, but be aware that you need to escape the Link yourself as PHP developer depending * on the needs. * * @param string $linkText the text to be wrapped in a link * @param array $conf the typolink configuration * @throws UnableToLinkException * @see typoLink() * @see createUrl() */ public function createLink(string $linkText, array $conf): LinkResultInterface { return $this->linkFactory->create($linkText, $conf, $this); } /** * This method creates a typoLink() and just returns the information of the "href" attribute * of the link (most of the time, this is the URL). * * @param array $conf the typolink configuration. * @return string The URL * @see typoLink() * @see createLink() */ public function createUrl(array $conf): string { try { return $this->createLink('', $conf)->getUrl(); } catch (UnableToLinkException) { // @todo: Inconsistent. createLink() throws it, but createUrl() eats it? // URL could not be generated return ''; } } /** * Based on the input "TypoLink" TypoScript configuration this will return the generated URL * * @param array $conf TypoScript properties for "typolink" * @return string The URL of the link-tag that typoLink() would by itself return */ public function typoLink_URL($conf): string { return $this->createUrl($conf); } /** * Wrapping a string. * Implements the TypoScript "wrap" property. * Example: $content = "HELLO WORLD" and $wrap = " | ", result: "HELLO WORLD" * * @param string $content The content to wrap * @param string $wrap The wrap value, eg. " | * @param string $char The char used to split the wrapping value, default is "| * @return string Wrapped input string * @see noTrimWrap() */ public function wrap($content, $wrap, $char = '|') { if ($wrap) { $wrapArr = explode($char, $wrap); $content = trim($wrapArr[0]) . $content . trim($wrapArr[1] ?? ''); } return $content; } /** * Wrapping a string, preserving whitespace in wrap value. * Notice that the wrap value uses part 1/2 to wrap (and not 0/1 which wrap() does) * * @param string $content The content to wrap, eg. "HELLO WORLD * @param string $wrap The wrap value, eg. " | | * @param string $char The char used to split the wrapping value, default is "|" * @return string Wrapped input string, eg. " HELLO WORD */ public function noTrimWrap($content, $wrap, $char = '|') { if ($wrap) { // expects to be wrapped with (at least) 3 characters (before, middle, after) // anything else is not taken into account $wrapArr = explode($char, $wrap, 4); $content = ($wrapArr[1] ?? '') . $content . ($wrapArr[2] ?? ''); } return $content; } /** * Call a user function/class-method * * @param string|RawValue $funcName The functionname, eg "user_myfunction" or "user_myclass->main". Notice that there * are rules for the names of functions/classes you can instantiate. If a function cannot * be called for some reason it will be seen in the TypoScript log in the AdminPanel. * @param array $conf The TypoScript configuration to pass the function * @param mixed $content The content payload to pass the function * @return mixed The return content from the function call. Should probably be a string. */ public function callUserFunction(string|RawValue $funcName, $conf, $content) { if ($funcName instanceof RawValue) { $isTrusted = $funcName->trusted; $funcName = $funcName->value; } else { $isTrusted = false; } $invokableAssertion = GeneralUtility::makeInstance(AllowedCallableAssertion::class); // Split parts $parts = explode('->', $funcName); if (count($parts) === 2) { // Check whether PHP class is available if (class_exists($parts[0])) { if (!$isTrusted) { $invokableAssertion->assertCallable($parts); } if ($this->container->has($parts[0])) { $classObj = $this->container->get($parts[0]); } else { $classObj = GeneralUtility::makeInstance($parts[0]); } $methodName = $parts[1]; $callable = [$classObj, $methodName]; if (is_object($classObj) && method_exists($classObj, $parts[1]) && is_callable($callable)) { if (is_callable([$classObj, 'setContentObjectRenderer'])) { $classObj->setContentObjectRenderer($this); } $content = $callable($content, $conf, $this->getRequest()->withAttribute('currentContentObject', $this)); } else { $this->timeTracker->setTSlogMessage('Method "' . $parts[1] . '" did not exist in class "' . $parts[0] . '"', LogLevel::ERROR); } } else { $this->timeTracker->setTSlogMessage('Class "' . $parts[0] . '" did not exist', LogLevel::ERROR); } } elseif (function_exists($funcName)) { if (!$isTrusted) { $invokableAssertion->assertCallable($funcName); } $content = $funcName($content, $conf, $this->getRequest()->withAttribute('currentContentObject', $this)); } else { $this->timeTracker->setTSlogMessage('Function "' . $funcName . '" did not exist', LogLevel::ERROR); } return $content; } /** * Cleans up a string of keywords. Keywords are split by "," (comma) ";" (semicolon) and linebreak * * @param string $content String of keywords * @return string Cleaned up string, keywords will be separated by a comma only. */ public function keywords($content): string { $listArr = preg_split('/[,;' . LF . ']/', $content); if ($listArr === false) { return ''; } foreach ($listArr as $k => $v) { $listArr[$k] = trim($v); } return implode(',', $listArr); } /** * Changing character case of a string, converting typically used western charset characters as well. * * @param string $theValue The string to change case for. * @param string $case The direction; either "upper" or "lower * @return string * @internal */ public function caseshift($theValue, $case) { switch (strtolower($case)) { case 'upper': $theValue = mb_strtoupper($theValue, 'utf-8'); break; case 'lower': $theValue = mb_strtolower($theValue, 'utf-8'); break; case 'capitalize': $theValue = mb_convert_case($theValue, MB_CASE_TITLE, 'utf-8'); break; case 'ucfirst': $firstChar = mb_substr($theValue, 0, 1, 'utf-8'); $firstChar = mb_strtoupper($firstChar, 'utf-8'); $remainder = mb_substr($theValue, 1, null, 'utf-8'); $theValue = $firstChar . $remainder; break; case 'lcfirst': $firstChar = mb_substr($theValue, 0, 1, 'utf-8'); $firstChar = mb_strtolower($firstChar, 'utf-8'); $remainder = mb_substr($theValue, 1, null, 'utf-8'); $theValue = $firstChar . $remainder; break; case 'uppercamelcase': $theValue = GeneralUtility::underscoredToUpperCamelCase($theValue); break; case 'lowercamelcase': $theValue = GeneralUtility::underscoredToLowerCamelCase($theValue); break; } return $theValue; } /** * Shifts the case of characters outside of HTML tags in the input string * * @param string $theValue The string to change case for. * @param string $case The direction; either "upper" or "lower" * @internal */ public function HTMLcaseshift($theValue, $case): string { $inside = 0; $newVal = ''; $pointer = 0; $totalLen = strlen($theValue); do { if (!$inside) { $len = strcspn(substr($theValue, $pointer), '<'); $newVal .= $this->caseshift(substr($theValue, $pointer, $len), $case); $inside = 1; } else { $len = strcspn(substr($theValue, $pointer), '>') + 1; $newVal .= substr($theValue, $pointer, $len); $inside = 0; } $pointer += $len; } while ($pointer < $totalLen); return $newVal; } /** * Returns the 'age' of the tstamp $seconds * * @param int $seconds Seconds to return age for. Example: "70" => "1 min", "3601" => "1 hrs * @param string|int|null $labels The labels of the individual units. Defaults to : ' min| hrs| days| yrs' */ public function calcAge($seconds, $labels = null): string { $now = DateTimeFactory::createFromTimestamp($GLOBALS['EXEC_TIME']); $then = DateTimeFactory::createFromTimestamp($GLOBALS['EXEC_TIME'] - $seconds); // Show past dates without a leading sign, but future dates with. // This does not make sense, but is kept for legacy reasons. $sign = $then > $now ? '-' : ''; // Take an absolute diff, since we don't want formatDateInterval to output the (correct) sign $diff = $now->diff($then, true); $labels = ($labels === null || MathUtility::canBeInterpretedAsInteger($labels)) ? 'min|hrs|days|yrs|min|hour|day|year' : str_replace('"', '', $labels); return $sign . (new DateFormatter())->formatDateInterval($diff, $labels); } /** * Resolve a TypoScript reference value to the full set of properties BUT overridden with any local properties set. * So the reference is resolved but overlaid with local TypoScript properties of the reference value. * * In short: This parses the "=<" operator for a couple of special properties like "parseFunc" and "tt_content.*". * * Note the "=<" operator is not a general TypoScript language construct, but applied here for a couple * of special objects only. * * @param array $typoScriptArray The TypoScript array: ['someProperty' => 'somePropertyValue', 'someProperty.' => [ 'someSubProperty' => 'someSubValue', ... ] * @param string $propertyName The property name: If this value in $typoScriptArray[$prop] is a reference (eg. "< lib.contentElement"), then * the reference will be retrieved and inserted at that position and overlaid with given local properties if any. * @return array The modified TypoScript array with resolved "=<" reference operator * @internal * @todo: It would be better if this method would get the setup object tree to resolve a * ReferenceChildNode only once per node. This would however mean the object tree * is moved around in the entire rendering chain, which is quite hard to achieve. */ public function mergeTSRef(array $typoScriptArray, string $propertyName): array { if (!isset($typoScriptArray[$propertyName]) || !str_starts_with($typoScriptArray[$propertyName], '<')) { return $typoScriptArray; } $frontendTypoScript = $this->getRequest()->getAttribute('frontend.typoscript'); if (!$frontendTypoScript || !$frontendTypoScript->hasSetup()) { return $typoScriptArray; } $fullTypoScriptArray = $frontendTypoScript->getSetupArray(); $dottedSourceIdentifier = trim(substr($typoScriptArray[$propertyName], 1)); $dottedSourceIdentifierArray = StringUtility::explodeEscaped('.', $dottedSourceIdentifier); $overrideConfig = $typoScriptArray[$propertyName . '.'] ?? []; $resolvedValue = $dottedSourceIdentifier; $resolvedConfig = $fullTypoScriptArray; foreach ($dottedSourceIdentifierArray as $identifierPart) { $resolvedValue = $resolvedConfig[$identifierPart] ?? $resolvedValue; $resolvedConfig = $resolvedConfig[$identifierPart . '.'] ?? []; } $resolvedConfig = array_replace_recursive($resolvedConfig, $overrideConfig); $typoScriptArray[$propertyName] = $resolvedValue; $typoScriptArray[$propertyName . '.'] = $resolvedConfig; if (!isset($typoScriptArray[$propertyName]) || !str_starts_with($typoScriptArray[$propertyName], '<')) { return $typoScriptArray; } // Call recursive to resolve a nested =< operator return $this->mergeTSRef($typoScriptArray, $propertyName); } /** * Generates a search where clause based on the input search words (AND operation - all search words must be found in record.) * Example: The $sw is "content management, system" (from an input form) and the $searchFieldList is "bodytext,header" then * the output will be ' AND (bodytext LIKE "%content%" OR header LIKE "%content%") AND (bodytext LIKE "%management%" OR header * LIKE "%management%") AND (bodytext LIKE "%system%" OR header LIKE "%system%")' * * @param string $searchWords The search words. These will be separated by space and comma. * @param string $searchFieldList The fields to search in * @param string $searchTable The table name you search in (recommended for DBAL compliance. Will be prepended field names as well) * @return string The WHERE clause. */ public function searchWhere($searchWords, $searchFieldList, $searchTable): string { if (!$searchWords) { return ''; } $prefixTableName = $searchTable ? $searchTable . '.' : ''; $queryBuilder = $this->connectionPool->getQueryBuilderForTable($searchTable); $where = $queryBuilder->expr()->and(); $searchFields = explode(',', $searchFieldList); $searchWords = preg_split('/[ ,]/', $searchWords); foreach ($searchWords as $searchWord) { $searchWord = trim($searchWord); if (strlen($searchWord) < 3) { continue; } $searchWordConstraint = $queryBuilder->expr()->or(); $searchWord = $queryBuilder->escapeLikeWildcards($searchWord); foreach ($searchFields as $field) { $searchWordConstraint = $searchWordConstraint->with( $queryBuilder->expr()->like($prefixTableName . $field, $queryBuilder->quote('%' . $searchWord . '%')) ); } if ($searchWordConstraint->count()) { $where = $where->with($searchWordConstraint); } } if ((string)$where === '') { return ''; } return ' AND (' . $where . ')'; } /** * Executes a SELECT query for records from $table and with conditions based on the configuration in the $conf array * This function is preferred over ->getQuery() if you just need to create and then execute a query. * * @param string $table The table name * @param array $conf The TypoScript configuration properties */ public function exec_getQuery($table, $conf): Result { $connection = $this->connectionPool->getConnectionForTable($table); $statement = $this->getQuery($connection, $table, $conf); return $connection->executeQuery($statement); } /** * Executes a SELECT query for records from $table and with conditions based on the configuration in the $conf array * and overlays with translation and version if available * * @param string $tableName the name of the TCA database table * @param array $queryConfiguration The TypoScript configuration properties, see .select in TypoScript reference * @throws \UnexpectedValueException */ public function getRecords($tableName, array $queryConfiguration): array { $records = []; $statement = $this->exec_getQuery($tableName, $queryConfiguration); $pageRepository = GeneralUtility::makeInstance(PageRepository::class); while ($row = $statement->fetchAssociative()) { // Versioning preview: $pageRepository->versionOL($tableName, $row, true); // Language overlay: if (is_array($row)) { $row = $pageRepository->getLanguageOverlay($tableName, $row); } // Might be unset in the language overlay if (is_array($row)) { $records[] = $row; } } if ($this->autoTagging) { $cacheTags = array_map(fn(array $record) => new CacheTag( name: sprintf('%s_%s', $tableName, ($record['uid'] ?? 0)), lifetime: $this->cacheLifetimeCalculator->calculateLifetimeForRow($tableName, $record) ), $records); $this->getRequest()->getAttribute('frontend.cache.collector')?->addCacheTags(...$cacheTags); } return $records; } /** * Creates and returns a SELECT query for records from $table and with conditions based on the * configuration in the $conf array. Implements the "select" function in TypoScript. * * @param string $table See ->exec_getQuery() * @param array $conf See ->exec_getQuery() * @throws \RuntimeException * @throws \InvalidArgumentException * @internal */ public function getQuery(Connection $connection, string $table, array $conf): string { // Resolve stdWrap in these properties first $properties = [ 'pidInList', 'uidInList', 'languageField', 'selectFields', 'max', 'begin', 'groupBy', 'orderBy', 'join', 'leftjoin', 'rightjoin', 'recursive', 'where', ]; foreach ($properties as $property) { $conf[$property] = trim( isset($conf[$property . '.']) ? (string)$this->stdWrap($conf[$property] ?? '', $conf[$property . '.'] ?? []) : (string)($conf[$property] ?? '') ); if ($conf[$property] === '') { unset($conf[$property]); } elseif (in_array($property, ['languageField', 'selectFields', 'join', 'leftjoin', 'rightjoin', 'where'], true)) { $conf[$property] = QueryHelper::quoteDatabaseIdentifiers($connection, $conf[$property]); } if (isset($conf[$property . '.'])) { // stdWrapping already done, so remove the sub-array unset($conf[$property . '.']); } } // Handle PDO-style named parameter markers first $queryMarkers = $this->getQueryMarkers($connection, $conf); // Replace the markers in the non-stdWrap properties foreach ($queryMarkers as $marker => $markerValue) { $properties = [ 'uidInList', 'selectFields', 'where', 'max', 'begin', 'groupBy', 'orderBy', 'join', 'leftjoin', 'rightjoin', ]; foreach ($properties as $property) { if ($conf[$property] ?? false) { $conf[$property] = str_replace('###' . $marker . '###', $markerValue, $conf[$property]); } } } // Construct WHERE clause: // Handle recursive function for the pidInList if (isset($conf['recursive'])) { $conf['recursive'] = (int)$conf['recursive']; if ($conf['recursive'] > 0) { $pidList = GeneralUtility::trimExplode(',', $conf['pidInList'], true); array_walk($pidList, function (&$storagePid) { if ($storagePid === 'this') { $storagePid = $this->getRequest()->getAttribute('frontend.page.information')->getId(); } }); $pageRepository = GeneralUtility::makeInstance(PageRepository::class); $expandedPidList = $pageRepository->getPageIdsRecursive($pidList, $conf['recursive']); $conf['pidInList'] = implode(',', $expandedPidList); } } if ((string)($conf['pidInList'] ?? '') === '') { $conf['pidInList'] = 'this'; } $queryParts = $this->getQueryConstraints($connection, $table, $conf); $queryBuilder = $connection->createQueryBuilder(); // @todo Check against getQueryConstraints, can probably use FrontendRestrictions // @todo here and remove enableFields there. $queryBuilder->getRestrictions()->removeAll(); $queryBuilder->select('*')->from($table); if ($queryParts['where'] ?? false) { $queryBuilder->where($queryParts['where']); } if ($queryParts['groupBy'] ?? false) { $queryBuilder->groupBy(...$queryParts['groupBy']); } if (is_array($queryParts['orderBy'] ?? false)) { foreach ($queryParts['orderBy'] as $orderBy) { $queryBuilder->addOrderBy(...$orderBy); } } // Fields: if ($conf['selectFields'] ?? false) { $queryBuilder->selectLiteral($this->sanitizeSelectPart($connection, $conf['selectFields'], $table)); } // Setting LIMIT: if (($conf['max'] ?? false) || ($conf['begin'] ?? false)) { // Finding the total number of records, if used: if (str_contains(strtolower(($conf['begin'] ?? '') . ($conf['max'] ?? '')), 'total')) { $countQueryBuilder = $connection->createQueryBuilder(); $countQueryBuilder->getRestrictions()->removeAll(); $countQueryBuilder->count('*') ->from($table) ->where($queryParts['where']); if (is_array($queryParts['groupBy'])) { $countQueryBuilder->groupBy(...$queryParts['groupBy']); } try { $count = $countQueryBuilder->executeQuery()->fetchOne(); if (isset($conf['max'])) { $conf['max'] = str_ireplace('total', $count, (string)$conf['max']); } if (isset($conf['begin'])) { $conf['begin'] = str_ireplace('total', $count, (string)$conf['begin']); } } catch (DBALException $e) { $this->timeTracker->setTSlogMessage($e->getMessage()); return ''; } } if (isset($conf['begin']) && $conf['begin'] > 0) { $conf['begin'] = MathUtility::forceIntegerInRange((int)ceil($this->calc($conf['begin'])), 0); $queryBuilder->setFirstResult($conf['begin']); } if (isset($conf['max'])) { $conf['max'] = MathUtility::forceIntegerInRange((int)ceil($this->calc($conf['max'])), 0); $queryBuilder->setMaxResults($conf['max'] ?: 100000); } } // Setting up tablejoins: if ($conf['join'] ?? false) { $joinParts = QueryHelper::parseJoin($conf['join']); $queryBuilder->join( $table, $joinParts['tableName'], $joinParts['tableAlias'], $joinParts['joinCondition'] ); } elseif ($conf['leftjoin'] ?? false) { $joinParts = QueryHelper::parseJoin($conf['leftjoin']); $queryBuilder->leftJoin( $table, $joinParts['tableName'], $joinParts['tableAlias'], $joinParts['joinCondition'] ); } elseif ($conf['rightjoin'] ?? false) { $joinParts = QueryHelper::parseJoin($conf['rightjoin']); $queryBuilder->rightJoin( $table, $joinParts['tableName'], $joinParts['tableAlias'], $joinParts['joinCondition'] ); } // Convert the QueryBuilder object into a SQL statement. $query = $queryBuilder->getSQL(); // Replace the markers in the queryParts to handle stdWrap enabled properties foreach ($queryMarkers as $marker => $markerValue) { // @todo Ugly hack that needs to be cleaned up, with the current architecture // @todo for exec_Query / getQuery it's the best we can do. $query = str_replace('###' . $marker . '###', $markerValue, $query); } return $query; } /** * Helper function for getQuery(), creating the WHERE clause of the SELECT query * * @param string $table The table name * @param array $conf The TypoScript configuration properties * @return array Associative array containing the prepared data for WHERE, ORDER BY and GROUP BY fragments * @see getQuery() */ protected function getQueryConstraints(Connection $connection, string $table, array $conf): array { $queryBuilder = $connection->createQueryBuilder(); $expressionBuilder = $queryBuilder->expr(); $request = $this->getRequest(); $contentPid = $request->getAttribute('frontend.page.information')->getContentFromPid(); $constraints = []; $pid_uid_flag = 0; $enableFieldsIgnore = []; $queryParts = [ 'where' => null, 'groupBy' => null, 'orderBy' => null, ]; $isInWorkspace = $this->context->getPropertyFromAspect('workspace', 'isOffline'); if (trim($conf['uidInList'] ?? '')) { $listArr = GeneralUtility::intExplode(',', str_replace('this', (string)$contentPid, $conf['uidInList'])); // If moved records shall be considered, select via t3ver_oid $considerMovePointers = $isInWorkspace && $table !== 'pages' && $this->getTcaSchema($table)?->isWorkspaceAware(); if ($considerMovePointers) { $constraints[] = (string)$expressionBuilder->or( $expressionBuilder->in($table . '.uid', $listArr), $expressionBuilder->and( $expressionBuilder->eq( $table . '.t3ver_state', VersionState::MOVE_POINTER->value ), $expressionBuilder->in($table . '.t3ver_oid', $listArr) ) ); } else { $constraints[] = (string)$expressionBuilder->in($table . '.uid', $listArr); } $pid_uid_flag++; } // Static_* tables are allowed to be fetched from root page if (str_starts_with($table, 'static_')) { $pid_uid_flag++; } if (trim($conf['pidInList'])) { $listArr = GeneralUtility::intExplode(',', str_replace('this', (string)$contentPid, $conf['pidInList'])); // Removes all pages which are not visible for the user! $listArr = $this->checkPidArray($listArr); if (GeneralUtility::inList($conf['pidInList'], 'root')) { $listArr[] = 0; } if (GeneralUtility::inList($conf['pidInList'], '-1')) { $listArr[] = -1; $enableFieldsIgnore['pid'] = true; } if (!empty($listArr)) { $constraints[] = $expressionBuilder->in($table . '.pid', array_map('intval', $listArr)); $pid_uid_flag++; } else { // If not uid and not pid then uid is set to 0 - which results in nothing!! $pid_uid_flag = 0; } } // If not uid and not pid then uid is set to 0 - which results in nothing!! if (!$pid_uid_flag) { $constraints[] = $expressionBuilder->eq($table . '.uid', 0); } $where = trim((string)$this->stdWrapValue('where', $conf)); if ($where) { $constraints[] = QueryHelper::stripLogicalOperatorPrefix($where); } // Check if the default language should be fetched (= doing overlays), or if only the records of a language should be fetched // but only do this for TCA tables that have languages enabled $languageConstraint = $this->getLanguageRestriction($expressionBuilder, $table, $conf); if ($languageConstraint !== null) { $constraints[] = $languageConstraint; } // default constraints from TCA $pageRepository = GeneralUtility::makeInstance(PageRepository::class); $constraints = array_merge($constraints, array_values($pageRepository->getDefaultConstraints($table, $enableFieldsIgnore))); // MAKE WHERE: if ($constraints !== []) { $queryParts['where'] = $expressionBuilder->and(...$constraints); } // GROUP BY $groupBy = trim((string)$this->stdWrapValue('groupBy', $conf)); if ($groupBy) { $queryParts['groupBy'] = QueryHelper::parseGroupBy($groupBy); } // ORDER BY $orderByString = trim((string)$this->stdWrapValue('orderBy', $conf)); if ($orderByString) { $queryParts['orderBy'] = QueryHelper::parseOrderBy($orderByString); } // Return result: return $queryParts; } /** * Adds parts to the WHERE clause that are related to language. * This only works on TCA tables which have the [ctrl][languageField] field set or if they * have select.languageField = my_language_field set explicitly. * * It is also possible to disable the language restriction for a query by using select.languageField = 0, * if select.languageField is not explicitly set, the TCA default values are taken. * * If the table is "localizeable" (= any of the criteria above is met), then the DB query is restricted: * * If the current language aspect has overlays enabled, then the only records with language "0" or "-1" are * fetched (the overlays are taken care of later-on). * if the current language has overlays but also records without localization-parent (free mode) available, * then these are fetched as well. This can explicitly set via select.includeRecordsWithoutDefaultTranslation = 1 * which overrules the overlayType within the language aspect. * * If the language aspect has NO overlays enabled, it behaves as in "free mode" (= only fetch the records * for the current language. */ protected function getLanguageRestriction(ExpressionBuilder $expressionBuilder, string $table, array $conf): string|CompositeExpression|null { $languageField = ''; $localizationParentField = ''; $languageCapability = null; $schema = $this->getTcaSchema($table); if ($schema?->isLanguageAware()) { $languageCapability = $schema->getCapability(TcaSchemaCapability::Language); $localizationParentField = $languageCapability->getTranslationOriginPointerField()->getName(); } // Check if the table is translatable, and set the language field by default from the TCA information if (!empty($conf['languageField']) || !isset($conf['languageField'])) { if (isset($conf['languageField']) && $schema?->hasField($conf['languageField'])) { $languageField = $conf['languageField']; } elseif ($languageCapability) { $languageField = $table . '.' . $languageCapability->getLanguageField()->getName(); } } // No language restriction enabled explicitly or available via TCA if (empty($languageField)) { return null; } /** @var LanguageAspect $languageAspect */ $languageAspect = $this->context->getAspect('language'); if ($languageAspect->doOverlays() && !empty($localizationParentField)) { // Sys language content is set to zero/-1 - and it is expected that whatever routine processes the output will // OVERLAY the records with localized versions! $languageQuery = $expressionBuilder->in($languageField, [0, -1]); // Use this option to include records that don't have a default language counterpart ("free mode") // (originalpointerfield is 0 and the language field contains the requested language) if (isset($conf['includeRecordsWithoutDefaultTranslation']) || !empty($conf['includeRecordsWithoutDefaultTranslation.'])) { $includeRecordsWithoutDefaultTranslation = isset($conf['includeRecordsWithoutDefaultTranslation.']) ? $this->stdWrap($conf['includeRecordsWithoutDefaultTranslation'], $conf['includeRecordsWithoutDefaultTranslation.']) : $conf['includeRecordsWithoutDefaultTranslation']; $includeRecordsWithoutDefaultTranslation = trim((string)$includeRecordsWithoutDefaultTranslation); $includeRecordsWithoutDefaultTranslation = $includeRecordsWithoutDefaultTranslation !== '' && $includeRecordsWithoutDefaultTranslation !== '0'; } else { // Option was not explicitly set, check what's in for the language overlay type. // OVERLAYS_ON means that we do not include the "floating" records (records without default translation) $includeRecordsWithoutDefaultTranslation = $languageAspect->getOverlayType() !== $languageAspect::OVERLAYS_ON; } if ($includeRecordsWithoutDefaultTranslation) { $languageQuery = $expressionBuilder->or( $languageQuery, $expressionBuilder->and( $expressionBuilder->eq($table . '.' . $localizationParentField, 0), $expressionBuilder->eq($languageField, $languageAspect->getContentId()) ) ); } return $languageQuery; } // No overlays = only fetch records given for the requested language and "all languages" return $expressionBuilder->in($languageField, [$languageAspect->getContentId(), -1]); } /** * Helper function for getQuery, sanitizing the select part * * This functions checks if the necessary fields are part of the select * and adds them if necessary. * * @see getQuery */ protected function sanitizeSelectPart(Connection $connection, string $selectPart, string $table): string { // Pattern matching parts $matchStart = '/(^\\s*|,\\s*|' . $table . '\\.)'; $matchEnd = '(\\s*,|\\s*$)/'; $necessaryFields = ['uid', 'pid']; $wsFields = ['t3ver_state']; $schema = $this->getTcaSchema($table); if ($schema === null) { return $selectPart; } if (!preg_match($matchStart . '\\*' . $matchEnd, $selectPart) && !preg_match('/(count|max|min|avg|sum)\\([^\\)]+\\)|distinct/i', $selectPart)) { foreach ($necessaryFields as $field) { $match = $matchStart . $field . $matchEnd; if (!preg_match($match, $selectPart)) { $selectPart .= ', ' . $connection->quoteIdentifier($table . '.' . $field) . ' AS ' . $connection->quoteIdentifier($field); } } if ($schema->isLanguageAware()) { $languageCapability = $schema->getCapability(TcaSchemaCapability::Language); $languageField = $languageCapability->getLanguageField()->getName(); $match = $matchStart . $languageField . $matchEnd; if (!preg_match($match, $selectPart)) { $selectPart .= ', ' . $connection->quoteIdentifier($table . '.' . $languageField) . ' AS ' . $connection->quoteIdentifier($languageField); } } if ($schema->isWorkspaceAware()) { foreach ($wsFields as $field) { $match = $matchStart . $field . $matchEnd; if (!preg_match($match, $selectPart)) { $selectPart .= ', ' . $connection->quoteIdentifier($table . '.' . $field) . ' AS ' . $connection->quoteIdentifier($field); } } } } return $selectPart; } /** * Removes Page UID numbers from the input array which are not available due to enableFields(). * * @param int[] $pageIds Array of Page UID numbers for select and for which pages with enablefields should be removed. * @return array Returns the array of remaining page UID numbers * @internal */ public function checkPidArray(array $pageIds): array { if ($pageIds === []) { return []; } if ($pageIds === [$this->getRequest()->getAttribute('frontend.page.information')->getId()]) { // Middlewares already checked access to the current page and made sure the current doktype // is a doktype whose content should be rendered, so there is no need to check that again. return $pageIds; } $restrictionContainer = GeneralUtility::makeInstance(FrontendRestrictionContainer::class); $pageRepository = GeneralUtility::makeInstance(PageRepository::class); return $pageRepository->filterAccessiblePageIds($pageIds, $restrictionContainer); } /** * Builds list of marker values for handling PDO-like parameter markers in select parts. * Marker values support stdWrap functionality thus allowing a way to use stdWrap functionality in various * properties of 'select' AND prevents SQL-injection problems by quoting and escaping of numeric values, * strings, NULL values and comma separated lists. * * @param array $conf Select part of CONTENT definition * @return array List of values to replace markers with * @internal * @see getQuery() */ public function getQueryMarkers(Connection $connection, array $conf): array { if (!isset($conf['markers.']) || !is_array($conf['markers.'])) { return []; } $markerValues = []; foreach ($conf['markers.'] as $dottedMarker => $dummy) { $marker = rtrim($dottedMarker, '.'); if ($dottedMarker != $marker . '.') { continue; } // Parse definition // todo else value is always null $tempValue = isset($conf['markers.'][$dottedMarker]) ? $this->stdWrap($conf['markers.'][$dottedMarker]['value'] ?? '', $conf['markers.'][$dottedMarker]) : $conf['markers.'][$dottedMarker]['value']; // Quote/escape if needed if (is_numeric($tempValue)) { if ((int)$tempValue == $tempValue) { // Handle integer $markerValues[$marker] = (int)$tempValue; } else { // Handle float $markerValues[$marker] = (float)$tempValue; } } elseif ($tempValue === null) { // It represents NULL $markerValues[$marker] = 'NULL'; } elseif (!empty($conf['markers.'][$dottedMarker]['commaSeparatedList'])) { // See if it is really a comma separated list of values $explodeValues = GeneralUtility::trimExplode(',', $tempValue); if (count($explodeValues) > 1) { // Handle each element of list separately $tempArray = []; foreach ($explodeValues as $listValue) { if (is_numeric($listValue)) { if ((int)$listValue == $listValue) { $tempArray[] = (int)$listValue; } else { $tempArray[] = (float)$listValue; } } else { // If quoted, remove quotes before // escaping. if (preg_match('/^\'([^\']*)\'$/', $listValue, $matches)) { $listValue = $matches[1]; } elseif (preg_match('/^\\"([^\\"]*)\\"$/', $listValue, $matches)) { $listValue = $matches[1]; } $tempArray[] = $connection->quote($listValue); } } $markerValues[$marker] = implode(',', $tempArray); } else { // Handle remaining values as string $markerValues[$marker] = $connection->quote($tempValue); } } else { // Handle remaining values as string $markerValues[$marker] = $connection->quote($tempValue); } } return $markerValues; } /** * Fetch content from cache * * @return string|false FALSE on cache miss */ protected function getFromCache(array $configuration): string|false { if (!$this->getRequest()->getAttribute('frontend.cache.instruction')->isCachingAllowed()) { return false; } $cacheKey = $this->calculateCacheKey($configuration); if (empty($cacheKey)) { return false; } $cachedData = $this->cacheHash->get($cacheKey); if ($cachedData === false) { return false; } $this->getRequest()->getAttribute('frontend.cache.collector')->addCacheTags( ...array_map(fn(string $tag) => new CacheTag($tag), $cachedData['cacheTags']) ); return $cachedData['content'] ?? false; } /** * Calculates the lifetime of a cache entry based on the given configuration */ protected function calculateCacheLifetime(array $configuration): int { $configuration['lifetime'] = $configuration['lifetime'] ?? ''; $lifetimeConfiguration = (string)$this->stdWrapValue('lifetime', $configuration); if (strtolower($lifetimeConfiguration) === 'unlimited') { $lifetime = 31536000; // unlimited lifetime - 1 year. } elseif (strtolower($lifetimeConfiguration) === 'default') { $lifetime = $this->getDefaultCachePeriod(); // default lifetime of config.cache_period or 86400 seconds } elseif ($lifetimeConfiguration > 0) { $lifetime = (int)$lifetimeConfiguration; } else { // If no lifetime is specified, we use the default cache period. $lifetime = $this->getDefaultCachePeriod(); } return $lifetime; } /** * Returns the default cache period in seconds */ protected function getDefaultCachePeriod(): int { $frontendTyposcript = $this->getRequest()->getAttribute('frontend.typoscript'); return (int)($frontendTyposcript->getConfigArray()['cache_period'] ?? 86400); } /** * Calculates the tags for a cache entry bases on the given configuration * * @return array */ protected function calculateCacheTags(array $configuration) { $configuration['tags'] = $configuration['tags'] ?? ''; $tags = (string)$this->stdWrapValue('tags', $configuration); return empty($tags) ? [] : GeneralUtility::trimExplode(',', $tags); } /** * Applies stdWrap to the cache key * * @return string */ protected function calculateCacheKey(array $configuration) { $configuration['key'] = $configuration['key'] ?? ''; return $this->stdWrapValue('key', $configuration); } protected function getTcaSchema(string $table): ?TcaSchema { return $this->tcaSchemaFactory->has($table) ? $this->tcaSchemaFactory->get($table) : null; } /** * Get content length of the current tag that could also contain nested tag contents * Helper method of parseFuncInternal(). */ protected function getContentLengthOfCurrentTag(string $theValue, int $pointer, string $currentTag): int { $tempContent = strtolower(substr($theValue, $pointer)); $startTag = '<' . $currentTag; $endTag = ''; $offsetCount = 0; // Take care for nested tags do { $nextMatchingEndTagPosition = strpos($tempContent, $endTag); // only match tag `a` in `` but not in `` $nextSameTypeTagPosition = preg_match( '#' . $startTag . '[\s/>]#', $tempContent, $nextSameStartTagMatches, PREG_OFFSET_CAPTURE ) ? $nextSameStartTagMatches[0][1] : false; // filter out nested tag contents to help getting the correct closing tag if ($nextMatchingEndTagPosition !== false && $nextSameTypeTagPosition !== false && $nextSameTypeTagPosition < $nextMatchingEndTagPosition) { $lastOpeningTagStartPosition = (int)strrpos(substr($tempContent, 0, $nextMatchingEndTagPosition), $startTag); $closingTagEndPosition = $nextMatchingEndTagPosition + strlen($endTag); $offsetCount += $closingTagEndPosition - $lastOpeningTagStartPosition; // replace content from latest tag start to latest tag end $tempContent = substr($tempContent, 0, $lastOpeningTagStartPosition) . substr($tempContent, $closingTagEndPosition); } } while ( ($nextMatchingEndTagPosition !== false && $nextSameTypeTagPosition !== false) && $nextSameTypeTagPosition < $nextMatchingEndTagPosition ); // if no closing tag is found we use length of the whole content $endingOffset = strlen($tempContent); if ($nextMatchingEndTagPosition !== false) { $endingOffset = $nextMatchingEndTagPosition + $offsetCount; } return $endingOffset; } protected function shallDebug(): bool { $typoScriptConfigArray = $this->getRequest()->getAttribute('frontend.typoscript')?->getConfigArray(); if (isset($typoScriptConfigArray['debug'])) { return (bool)($typoScriptConfigArray['debug']); } return !empty($GLOBALS['TYPO3_CONF_VARS']['FE']['debug']); } /** * @todo: This getRequest() is still a bit messy. * Underling code depends on both, a ContentObjectRenderer instance and a request, * but the API currently only passes one or the other. For instance Extbase and Fluid * only pass the Request, DataProcessors only a ContentObjectRenderer. * This is why getRequest() is currently public here. * A potential refactoring could: * * Create interfaces to pass both where needed (or pass a combined context object) * * Deprecate access to getRequest() here afterward * A circular dependency that the instance of ContentObjectRenderer holds a * request with the instance of itself as attribute must be avoided. * This is currently achieved by adding a new request with * $this->request->withAttribute('currentContentObject', $cObj) in code that needs * it, but this new request is NOT passed back into the ContentObjectRenderer instance. * * @internal */ public function getRequest(): ServerRequestInterface { if ($this->request instanceof ServerRequestInterface) { return $this->request; } throw new ContentRenderingException( 'PSR-7 request is missing in ContentObjectRenderer. Call setRequest() after object instantiation.', 1607172972 ); } }