‪TYPO3CMS  11.5
ContentObjectRenderer.php
Go to the documentation of this file.
1 <?php
2 
3 /*
4  * This file is part of the TYPO3 CMS project.
5  *
6  * It is free software; you can redistribute it and/or modify it under
7  * the terms of the GNU General Public License, either version 2
8  * of the License, or any later version.
9  *
10  * For the full copyright and license information, please read the
11  * LICENSE.txt file that was distributed with this source code.
12  *
13  * The TYPO3 project - inspiring people to share!
14  */
15 
17 
18 use Doctrine\DBAL\Exception as DBALException;
19 use Doctrine\DBAL\Result;
20 use Psr\Container\ContainerInterface;
21 use Psr\Http\Message\ServerRequestInterface;
22 use Psr\Log\LoggerAwareInterface;
23 use Psr\Log\LoggerAwareTrait;
24 use Psr\Log\LogLevel;
34 use TYPO3\CMS\Core\Database\Query\QueryBuilder;
83 use TYPO3\CMS\Frontend\Service\TypoLinkCodecService;
88 use TYPO3\HtmlSanitizer\Builder\BuilderInterface;
89 
99 class ContentObjectRenderer implements LoggerAwareInterface
100 {
101  use LoggerAwareTrait;
102  use DefaultJavaScriptAssetTrait;
103 
107  protected $container;
108 
113  public $align = [
114  'center',
115  'right',
116  'left',
117  ];
118 
125  public $stdWrapOrder = [
126  'stdWrapPreProcess' => 'hook',
127  // this is a placeholder for the first Hook
128  'cacheRead' => 'hook',
129  // this is a placeholder for checking if the content is available in cache
130  'setContentToCurrent' => 'boolean',
131  'setContentToCurrent.' => 'array',
132  'addPageCacheTags' => 'string',
133  'addPageCacheTags.' => 'array',
134  'setCurrent' => 'string',
135  'setCurrent.' => 'array',
136  'lang.' => 'array',
137  'data' => 'getText',
138  'data.' => 'array',
139  'field' => 'fieldName',
140  'field.' => 'array',
141  'current' => 'boolean',
142  'current.' => 'array',
143  'cObject' => 'cObject',
144  'cObject.' => 'array',
145  'numRows.' => 'array',
146  'preUserFunc' => 'functionName',
147  'stdWrapOverride' => 'hook',
148  // this is a placeholder for the second Hook
149  'override' => 'string',
150  'override.' => 'array',
151  'preIfEmptyListNum' => 'listNum',
152  'preIfEmptyListNum.' => 'array',
153  'ifNull' => 'string',
154  'ifNull.' => 'array',
155  'ifEmpty' => 'string',
156  'ifEmpty.' => 'array',
157  'ifBlank' => 'string',
158  'ifBlank.' => 'array',
159  'listNum' => 'listNum',
160  'listNum.' => 'array',
161  'trim' => 'boolean',
162  'trim.' => 'array',
163  'strPad.' => 'array',
164  'stdWrap' => 'stdWrap',
165  'stdWrap.' => 'array',
166  'stdWrapProcess' => 'hook',
167  // this is a placeholder for the third Hook
168  'required' => 'boolean',
169  'required.' => 'array',
170  'if.' => 'array',
171  'fieldRequired' => 'fieldName',
172  'fieldRequired.' => 'array',
173  'csConv' => 'string',
174  'csConv.' => 'array',
175  'parseFunc' => 'objectpath',
176  'parseFunc.' => 'array',
177  'HTMLparser' => 'boolean',
178  'HTMLparser.' => 'array',
179  'split.' => 'array',
180  'replacement.' => 'array',
181  'prioriCalc' => 'boolean',
182  'prioriCalc.' => 'array',
183  'char' => 'integer',
184  'char.' => 'array',
185  'intval' => 'boolean',
186  'intval.' => 'array',
187  'hash' => 'string',
188  'hash.' => 'array',
189  'round' => 'boolean',
190  'round.' => 'array',
191  'numberFormat.' => 'array',
192  'expandList' => 'boolean',
193  'expandList.' => 'array',
194  'date' => 'dateconf',
195  'date.' => 'array',
196  'strtotime' => 'strtotimeconf',
197  'strtotime.' => 'array',
198  'strftime' => 'strftimeconf',
199  'strftime.' => 'array',
200  'age' => 'boolean',
201  'age.' => 'array',
202  'case' => 'case',
203  'case.' => 'array',
204  'bytes' => 'boolean',
205  'bytes.' => 'array',
206  'substring' => 'parameters',
207  'substring.' => 'array',
208  'cropHTML' => 'crop',
209  'cropHTML.' => 'array',
210  'stripHtml' => 'boolean',
211  'stripHtml.' => 'array',
212  'crop' => 'crop',
213  'crop.' => 'array',
214  'rawUrlEncode' => 'boolean',
215  'rawUrlEncode.' => 'array',
216  'htmlSpecialChars' => 'boolean',
217  'htmlSpecialChars.' => 'array',
218  'encodeForJavaScriptValue' => 'boolean',
219  'encodeForJavaScriptValue.' => 'array',
220  'doubleBrTag' => 'string',
221  'doubleBrTag.' => 'array',
222  'br' => 'boolean',
223  'br.' => 'array',
224  'brTag' => 'string',
225  'brTag.' => 'array',
226  'encapsLines.' => 'array',
227  'keywords' => 'boolean',
228  'keywords.' => 'array',
229  'innerWrap' => 'wrap',
230  'innerWrap.' => 'array',
231  'innerWrap2' => 'wrap',
232  'innerWrap2.' => 'array',
233  'preCObject' => 'cObject',
234  'preCObject.' => 'array',
235  'postCObject' => 'cObject',
236  'postCObject.' => 'array',
237  'wrapAlign' => 'align',
238  'wrapAlign.' => 'array',
239  'typolink.' => 'array',
240  'wrap' => 'wrap',
241  'wrap.' => 'array',
242  'noTrimWrap' => 'wrap',
243  'noTrimWrap.' => 'array',
244  'wrap2' => 'wrap',
245  'wrap2.' => 'array',
246  'dataWrap' => 'dataWrap',
247  'dataWrap.' => 'array',
248  'prepend' => 'cObject',
249  'prepend.' => 'array',
250  'append' => 'cObject',
251  'append.' => 'array',
252  'wrap3' => 'wrap',
253  'wrap3.' => 'array',
254  'orderedStdWrap' => 'stdWrap',
255  'orderedStdWrap.' => 'array',
256  'outerWrap' => 'wrap',
257  'outerWrap.' => 'array',
258  'insertData' => 'boolean',
259  'insertData.' => 'array',
260  'postUserFunc' => 'functionName',
261  'postUserFuncInt' => 'functionName',
262  'prefixComment' => 'string',
263  'prefixComment.' => 'array',
264  'editIcons' => 'string', // @deprecated since v11, will be removed with v12. Drop together with other editIcon removals.
265  'editIcons.' => 'array', // @deprecated since v11, will be removed with v12. Drop together with other editIcon removals.
266  'editPanel' => 'boolean', // @deprecated since v11, will be removed with v12. Drop together with other editPanel removals.
267  'editPanel.' => 'array', // @deprecated since v11, will be removed with v12. Drop together with other editPanel removals.
268  'htmlSanitize' => 'boolean',
269  'htmlSanitize.' => 'array',
270  'cacheStore' => 'hook',
271  // this is a placeholder for storing the content in cache
272  'stdWrapPostProcess' => 'hook',
273  // this is a placeholder for the last Hook
274  'debug' => 'boolean',
275  'debug.' => 'array',
276  'debugFunc' => 'boolean',
277  'debugFunc.' => 'array',
278  'debugData' => 'boolean',
279  'debugData.' => 'array',
280  ];
281 
287  protected $contentObjectClassMap = [];
288 
298  public $data = [];
299 
303  protected $table = '';
304 
311  public $oldData = [];
312 
319  public $alternativeData = '';
320 
326  public $parameters = [];
327 
331  public $currentValKey = 'currentValue_kidjls9dksoje';
332 
339  public $currentRecord = '';
340 
347  public $currentRecordTotal = 0;
348 
354  public $currentRecordNumber = 0;
355 
361  public $parentRecordNumber = 0;
362 
368  public $parentRecord = [];
369 
373  public $checkPid_badDoktypeList = ‪PageRepository::DOKTYPE_RECYCLER;
374 
380  public $lastTypoLinkUrl = '';
381 
387  public $lastTypoLinkTarget = '';
388 
392  public $lastTypoLinkLD = [];
393 
394  public ?LinkResultInterface $lastTypoLinkResult = null;
395 
402  public $recordRegister = [];
403 
409  protected $stdWrapHookObjects = [];
410 
416  protected $getImgResourceHookObjects;
417 
421  protected $currentFile;
422 
427  public $doConvertToUserIntObject = false;
428 
435  protected $userObjectType = false;
436 
440  protected $stopRendering = [];
441 
445  protected $stdWrapRecursionLevel = 0;
446 
450  protected $typoScriptFrontendController;
451 
457  private ?ServerRequestInterface $request = null;
458 
464  public const OBJECTTYPE_USER_INT = 1;
470  public const OBJECTTYPE_USER = 2;
471 
476  public function __construct(TypoScriptFrontendController $typoScriptFrontendController = null, ContainerInterface $container = null)
477  {
478  $this->typoScriptFrontendController = $typoScriptFrontendController;
479  $this->contentObjectClassMap = ‪$GLOBALS['TYPO3_CONF_VARS']['FE']['ContentObjects'] ?? [];
480  $this->container = $container;
481  }
482 
483  public function setRequest(ServerRequestInterface $request): void
484  {
485  $this->request = $request;
486  }
487 
495  public function __sleep()
496  {
497  $vars = get_object_vars($this);
498  unset($vars['typoScriptFrontendController'], $vars['logger'], $vars['container'], $vars['request']);
499  if ($this->currentFile instanceof FileReference) {
500  $this->currentFile = 'FileReference:' . $this->currentFile->getUid();
501  } elseif ($this->currentFile instanceof File) {
502  $this->currentFile = 'File:' . $this->currentFile->getIdentifier();
503  } else {
504  unset($vars['currentFile']);
505  }
506  return array_keys($vars);
507  }
508 
514  public function __wakeup()
515  {
516  if (isset(‪$GLOBALS['TSFE'])) {
517  $this->typoScriptFrontendController = ‪$GLOBALS['TSFE'];
518  }
519  if ($this->currentFile !== null && is_string($this->currentFile)) {
520  [$objectType, $identifier] = explode(':', $this->currentFile, 2);
521  try {
522  if ($objectType === 'File') {
523  $this->currentFile = GeneralUtility::makeInstance(ResourceFactory::class)->retrieveFileOrFolderObject($identifier);
524  } elseif ($objectType === 'FileReference') {
525  $this->currentFile = GeneralUtility::makeInstance(ResourceFactory::class)->getFileReferenceObject($identifier);
526  }
527  } catch (ResourceDoesNotExistException $e) {
528  $this->currentFile = null;
529  }
530  }
531  $this->logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__);
532  $this->container = GeneralUtility::getContainer();
533 
534  // We do not derive $this->request from globals here. The request is expected to be injected
535  // using setRequest() after deserialization or with start().
536  // (A fallback to $GLOBALS['TYPO3_REQUEST'] is available in getRequest() for BC)
537  }
538 
548  public function setContentObjectClassMap(array $contentObjectClassMap)
549  {
550  $this->contentObjectClassMap = $contentObjectClassMap;
551  }
552 
563  public function registerContentObjectClass($className, $contentObjectName)
564  {
565  $this->contentObjectClassMap[$contentObjectName] = $className;
566  }
567 
577  public function start($data, $table = '', ?ServerRequestInterface $request = null)
578  {
579  $this->request = $request ?? $this->request;
580  $this->data = $data;
581  $this->table = $table;
582  $this->currentRecord = $table !== ''
583  ? $table . ':' . ($this->data['uid'] ?? '')
584  : '';
585  $this->parameters = [];
586  $this->stdWrapHookObjects = [];
587  foreach (‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['stdWrap'] ?? [] as $className) {
588  $hookObject = GeneralUtility::makeInstance($className);
589  if (!$hookObject instanceof ContentObjectStdWrapHookInterface) {
590  throw new \UnexpectedValueException($className . ' must implement interface ' . ContentObjectStdWrapHookInterface::class, 1195043965);
591  }
592  $this->stdWrapHookObjects[] = $hookObject;
593  }
594  foreach (‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['postInit'] ?? [] as $className) {
595  $postInitializationProcessor = GeneralUtility::makeInstance($className);
596  if (!$postInitializationProcessor instanceof ContentObjectPostInitHookInterface) {
597  throw new \UnexpectedValueException($className . ' must implement interface ' . ContentObjectPostInitHookInterface::class, 1274563549);
598  }
599  $postInitializationProcessor->postProcessContentObjectInitialization($this);
600  }
601  }
602 
608  public function getCurrentTable()
609  {
610  return $this->table;
611  }
612 
619  protected function getGetImgResourceHookObjects()
620  {
621  if (!isset($this->getImgResourceHookObjects)) {
622  $this->getImgResourceHookObjects = [];
623  foreach (‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['getImgResource'] ?? [] as $className) {
624  $hookObject = GeneralUtility::makeInstance($className);
625  if (!$hookObject instanceof ContentObjectGetImageResourceHookInterface) {
626  throw new \UnexpectedValueException('$hookObject must implement interface ' . ContentObjectGetImageResourceHookInterface::class, 1218636383);
627  }
628  $this->getImgResourceHookObjects[] = $hookObject;
629  }
630  }
631  return $this->getImgResourceHookObjects;
632  }
633 
642  public function setParent($data, $currentRecord)
643  {
644  $this->parentRecord = [
645  'data' => $data,
646  'currentRecord' => $currentRecord,
647  ];
648  }
649 
650  /***********************************************
651  *
652  * CONTENT_OBJ:
653  *
654  ***********************************************/
663  public function getCurrentVal()
664  {
665  return $this->data[$this->currentValKey] ?? null;
666  }
667 
674  public function setCurrentVal($value)
675  {
676  $this->data[$this->currentValKey] = $value;
677  }
678 
688  public function cObjGet($setup, $addKey = '')
689  {
690  if (!is_array($setup)) {
691  return '';
692  }
693  return implode('', $this->cObjGetSeparated($setup, $addKey));
694  }
695 
702  public function cObjGetSeparated(?array $setup, string $addKey = ''): array
703  {
704  if ($setup === null || $setup === []) {
705  return [];
706  }
707  $sKeyArray = ‪ArrayUtility::filterAndSortByNumericKeys($setup);
708  $contentObjects = [];
709  foreach ($sKeyArray as $theKey) {
710  $theValue = $setup[$theKey];
711  if ((int)$theKey && !str_contains($theKey, '.')) {
712  $conf = $setup[$theKey . '.'] ?? [];
713  $contentObjects[] = $this->cObjGetSingle($theValue, $conf, $addKey . $theKey);
714  }
715  }
716  return $contentObjects;
717  }
718 
728  public function cObjGetSingle($name, $conf, $TSkey = '__')
729  {
730  $content = '';
731  $timeTracker = $this->getTimeTracker();
732  $name = trim($name);
733  if ($timeTracker->LR) {
734  $timeTracker->push($TSkey, $name);
735  }
736  // Checking if the COBJ is a reference to another object. (eg. name of 'some.object =< styles.something')
737  if (isset($name[0]) && $name[0] === '<') {
738  $key = trim(substr($name, 1));
739  $cF = GeneralUtility::makeInstance(TypoScriptParser::class);
740  // $name and $conf is loaded with the referenced values.
741  $confOverride = is_array($conf) ? $conf : [];
742  [$name, $conf] = $cF->getVal($key, $this->getTypoScriptFrontendController()->tmpl->setup);
743  $conf = array_replace_recursive($conf, $confOverride);
744  // Getting the cObject
745  $timeTracker->incStackPointer();
746  $content .= $this->cObjGetSingle($name, $conf, $key);
747  $timeTracker->decStackPointer();
748  } else {
749  $contentObject = $this->getContentObject($name);
750  if ($contentObject) {
751  $content .= $this->render($contentObject, $conf);
752  }
753  }
754  if ($timeTracker->LR) {
755  $timeTracker->pull($content);
756  }
757  return $content;
758  }
759 
769  public function getContentObject($name)
770  {
771  if (!isset($this->contentObjectClassMap[$name])) {
772  return null;
773  }
774  $fullyQualifiedClassName = $this->contentObjectClassMap[$name];
775  $contentObject = GeneralUtility::makeInstance($fullyQualifiedClassName, $this);
776  if (!($contentObject instanceof AbstractContentObject)) {
777  throw new ContentRenderingException(sprintf('Registered content object class name "%s" must be an instance of AbstractContentObject, but is not!', $fullyQualifiedClassName), 1422564295);
778  }
779  $contentObject->setRequest($this->getRequest());
780  return $contentObject;
781  }
782 
783  /********************************************
784  *
785  * Functions rendering content objects (cObjects)
786  *
787  ********************************************/
799  public function render(AbstractContentObject $contentObject, $configuration = [])
800  {
801  $content = '';
802 
803  // Evaluate possible cache and return
804  $cacheConfiguration = $configuration['cache.'] ?? null;
805  if ($cacheConfiguration !== null) {
806  unset($configuration['cache.']);
807  $cache = $this->getFromCache($cacheConfiguration);
808  if ($cache !== false) {
809  return $cache;
810  }
811  }
812 
813  // Render content
814  try {
815  $content .= $contentObject->render($configuration);
816  } catch (ContentRenderingException $exception) {
817  // Content rendering Exceptions indicate a critical problem which should not be
818  // caught e.g. when something went wrong with Exception handling itself
819  throw $exception;
820  } catch (\Exception $exception) {
821  $exceptionHandler = $this->createExceptionHandler($configuration);
822  if ($exceptionHandler === null) {
823  throw $exception;
824  }
825  $content = $exceptionHandler->handle($exception, $contentObject, $configuration);
826  }
827 
828  // Store cache
829  if ($cacheConfiguration !== null && !$this->getTypoScriptFrontendController()->no_cache) {
830  $key = $this->calculateCacheKey($cacheConfiguration);
831  if (!empty($key)) {
832  $cacheFrontend = GeneralUtility::makeInstance(CacheManager::class)->getCache('hash');
833  $tags = $this->calculateCacheTags($cacheConfiguration);
834  $lifetime = $this->calculateCacheLifetime($cacheConfiguration);
835  $cacheFrontend->set($key, $content, $tags, $lifetime);
836  }
837  }
838 
839  return $content;
840  }
841 
850  protected function createExceptionHandler($configuration = [])
851  {
852  $exceptionHandler = null;
853  $exceptionHandlerClassName = $this->determineExceptionHandlerClassName($configuration);
854  if (!empty($exceptionHandlerClassName)) {
855  if (method_exists($exceptionHandlerClassName, 'setConfiguration')) {
856  $exceptionHandler = GeneralUtility::makeInstance($exceptionHandlerClassName);
857  $exceptionHandler->setConfiguration($this->mergeExceptionHandlerConfiguration($configuration));
858  } else {
859  trigger_error(
860  'Passing the TypoScript configuration as constructor argument to ' . $exceptionHandlerClassName . ' is deprecated and will stop working in TYPO3 v12.0. Exception handler classes therefore have to implement the setConfiguration() method.',
861  E_USER_DEPRECATED
862  );
863  $exceptionHandler = GeneralUtility::makeInstance($exceptionHandlerClassName, $this->mergeExceptionHandlerConfiguration($configuration));
864  }
865  if (!$exceptionHandler instanceof ExceptionHandlerInterface) {
866  throw new ContentRenderingException('An exception handler was configured but the class does not exist or does not implement the ExceptionHandlerInterface', 1403653369);
867  }
868  }
869 
870  return $exceptionHandler;
871  }
872 
879  protected function determineExceptionHandlerClassName($configuration)
880  {
881  $exceptionHandlerClassName = null;
882  $tsfe = $this->getTypoScriptFrontendController();
883  if (!isset($tsfe->config['config']['contentObjectExceptionHandler'])) {
884  if (‪Environment::getContext()->isProduction()) {
885  $exceptionHandlerClassName = '1';
886  }
887  } else {
888  $exceptionHandlerClassName = $tsfe->config['config']['contentObjectExceptionHandler'];
889  }
890 
891  if (isset($configuration['exceptionHandler'])) {
892  $exceptionHandlerClassName = $configuration['exceptionHandler'];
893  }
894 
895  if ($exceptionHandlerClassName === '1') {
896  $exceptionHandlerClassName = ProductionExceptionHandler::class;
897  }
898 
899  return $exceptionHandlerClassName;
900  }
901 
909  protected function mergeExceptionHandlerConfiguration($configuration)
910  {
911  $exceptionHandlerConfiguration = [];
912  $tsfe = $this->getTypoScriptFrontendController();
913  if (!empty($tsfe->config['config']['contentObjectExceptionHandler.'])) {
914  $exceptionHandlerConfiguration = $tsfe->config['config']['contentObjectExceptionHandler.'];
915  }
916  if (!empty($configuration['exceptionHandler.'])) {
917  $exceptionHandlerConfiguration = array_replace_recursive($exceptionHandlerConfiguration, $configuration['exceptionHandler.']);
918  }
919 
920  return $exceptionHandlerConfiguration;
921  }
922 
931  public function getUserObjectType()
932  {
933  return $this->userObjectType;
934  }
935 
941  public function setUserObjectType($userObjectType)
942  {
943  $this->userObjectType = $userObjectType;
944  }
945 
949  public function convertToUserIntObject()
950  {
951  if ($this->userObjectType !== self::OBJECTTYPE_USER) {
952  $this->getTimeTracker()->setTSlogMessage(self::class . '::convertToUserIntObject() is called in the wrong context or for the wrong object type', LogLevel::WARNING);
953  } else {
954  $this->doConvertToUserIntObject = true;
955  }
956  }
957 
958  /************************************
959  *
960  * Various helper functions for content objects:
961  *
962  ************************************/
970  public function readFlexformIntoConf($flexData, &$conf, $recursive = false)
971  {
972  if ($recursive === false && is_string($flexData)) {
973  $flexData = ‪GeneralUtility::xml2array($flexData, 'T3');
974  }
975  if (is_array($flexData) && isset($flexData['data']['sDEF']['lDEF'])) {
976  $flexData = $flexData['data']['sDEF']['lDEF'];
977  }
978  if (!is_array($flexData)) {
979  return;
980  }
981  foreach ($flexData as $key => $value) {
982  if (!is_array($value)) {
983  continue;
984  }
985  if (isset($value['el'])) {
986  if (is_array($value['el']) && !empty($value['el'])) {
987  foreach ($value['el'] as $ekey => $element) {
988  if (isset($element['vDEF'])) {
989  $conf[$ekey] = $element['vDEF'];
990  } else {
991  if (is_array($element)) {
992  $this->readFlexformIntoConf($element, $conf[$key][key($element)][$ekey], true);
993  } else {
994  $this->readFlexformIntoConf($element, $conf[$key][$ekey], true);
995  }
996  }
997  }
998  } else {
999  $this->readFlexformIntoConf($value['el'], $conf[$key], true);
1000  }
1001  }
1002  if (isset($value['vDEF'])) {
1003  $conf[$key] = $value['vDEF'];
1004  }
1005  }
1006  }
1007 
1016  public function getSlidePids($pidList, $pidConf)
1017  {
1018  // todo: phpstan states that $pidConf always exists and is not nullable. At the moment, this is a false positive
1019  // as null can be passed into this method via $pidConf. As soon as more strict types are used, this isset
1020  // check must be replaced with a more appropriate check like empty or count.
1021  $pidList = isset($pidConf) ? trim((string)$this->stdWrap($pidList, $pidConf)) : trim($pidList);
1022  if ($pidList === '') {
1023  $pidList = 'this';
1024  }
1025  $tsfe = $this->getTypoScriptFrontendController();
1026  $listArr = null;
1027  if (trim($pidList)) {
1028  $listArr = ‪GeneralUtility::intExplode(',', str_replace('this', (string)$tsfe->contentPid, $pidList));
1029  $listArr = $this->checkPidArray($listArr);
1030  }
1031  $pidList = [];
1032  if (is_array($listArr) && !empty($listArr)) {
1033  foreach ($listArr as $uid) {
1034  $page = $tsfe->sys_page->getPage($uid);
1035  if (!$page['is_siteroot']) {
1036  $pidList[] = $page['pid'];
1037  }
1038  }
1039  }
1040  return implode(',', $pidList);
1041  }
1042 
1052  public function imageLinkWrap($string, $imageFile, $conf)
1053  {
1054  $string = (string)$string;
1055  $enable = $this->stdWrapValue('enable', $conf ?? []);
1056  if (!$enable) {
1057  return $string;
1058  }
1059  $content = (string)$this->typoLink($string, $conf['typolink.'] ?? []);
1060  if (isset($conf['file.']) && is_scalar($imageFile)) {
1061  $imageFile = $this->stdWrap((string)$imageFile, $conf['file.']);
1062  }
1063 
1064  if ($imageFile instanceof File) {
1065  $file = $imageFile;
1066  } elseif ($imageFile instanceof FileReference) {
1067  $file = $imageFile->getOriginalFile();
1068  } else {
1070  $file = GeneralUtility::makeInstance(ResourceFactory::class)->getFileObject((int)$imageFile);
1071  } else {
1072  $file = GeneralUtility::makeInstance(ResourceFactory::class)->getFileObjectFromCombinedIdentifier($imageFile);
1073  }
1074  }
1075 
1076  // Create imageFileLink if not created with typolink
1077  if ($content === $string && $file !== null) {
1078  $parameterNames = ['width', 'height', 'effects', 'bodyTag', 'title', 'wrap', 'crop'];
1079  $parameters = [];
1080  $sample = $this->stdWrapValue('sample', $conf ?? []);
1081  if ($sample) {
1082  $parameters['sample'] = 1;
1083  }
1084  foreach ($parameterNames as $parameterName) {
1085  if (isset($conf[$parameterName . '.'])) {
1086  $conf[$parameterName] = $this->stdWrap($conf[$parameterName] ?? '', $conf[$parameterName . '.'] ?? []);
1087  }
1088  if (isset($conf[$parameterName]) && $conf[$parameterName]) {
1089  $parameters[$parameterName] = $conf[$parameterName];
1090  }
1091  }
1092  $parametersEncoded = base64_encode((string)json_encode($parameters));
1093  $hmac = GeneralUtility::hmac(implode('|', [$file->getUid(), $parametersEncoded]));
1094  $params = '&md5=' . $hmac;
1095  foreach (str_split($parametersEncoded, 64) as $index => $chunk) {
1096  $params .= '&parameters' . rawurlencode('[') . $index . rawurlencode(']') . '=' . rawurlencode($chunk);
1097  }
1098  $url = $this->getTypoScriptFrontendController()->absRefPrefix . 'index.php?eID=tx_cms_showpic&file=' . $file->getUid() . $params;
1099  $directImageLink = $this->stdWrapValue('directImageLink', $conf ?? []);
1100  if ($directImageLink) {
1101  $imgResourceConf = [
1102  'file' => $imageFile,
1103  'file.' => $conf,
1104  ];
1105  $url = $this->cObjGetSingle('IMG_RESOURCE', $imgResourceConf);
1106  if (!$url) {
1107  // If no imagemagick / gm is available
1108  $url = $imageFile;
1109  }
1110  }
1111  // Create TARGET-attribute only if the right doctype is used
1112  $target = '';
1113  $xhtmlDocType = $this->getTypoScriptFrontendController()->xhtmlDoctype;
1114  if ($xhtmlDocType !== 'xhtml_strict' && $xhtmlDocType !== 'xhtml_11') {
1115  $target = (string)$this->stdWrapValue('target', $conf ?? []);
1116  if ($target === '') {
1117  $target = 'thePicture';
1118  }
1119  }
1120  $a1 = '';
1121  $a2 = '';
1122  $conf['JSwindow'] = $this->stdWrapValue('JSwindow', $conf ?? []);
1123  if ($conf['JSwindow']) {
1124  $altUrl = $this->stdWrapValue('altUrl', $conf['JSwindow.'] ?? []);
1125  if ($altUrl) {
1126  $url = $altUrl . (($conf['JSwindow.']['altUrl_noDefaultParams'] ?? false) ? '' : '?file=' . rawurlencode((string)$imageFile) . $params);
1127  }
1128 
1129  $processedFile = $file->process(‪ProcessedFile::CONTEXT_IMAGECROPSCALEMASK, $conf);
1130  $JSwindowExpand = $this->stdWrapValue('expand', $conf['JSwindow.'] ?? []);
1131  $offset = ‪GeneralUtility::intExplode(',', $JSwindowExpand . ',');
1132  $newWindow = $this->stdWrapValue('newWindow', $conf['JSwindow.'] ?? []);
1133  $params = [
1134  'width' => ($processedFile->getProperty('width') + $offset[0]),
1135  'height' => ($processedFile->getProperty('height') + $offset[1]),
1136  'status' => '0',
1137  'menubar' => '0',
1138  ];
1139  // params override existing parameters from above, or add more
1140  $windowParams = (string)$this->stdWrapValue('params', $conf['JSwindow.'] ?? []);
1141  $windowParams = explode(',', $windowParams);
1142  foreach ($windowParams as $windowParam) {
1143  $windowParamParts = explode('=', $windowParam);
1144  $paramKey = $windowParamParts[0];
1145  $paramValue = $windowParamParts[1] ?? null;
1146 
1147  if ($paramKey === '') {
1148  continue;
1149  }
1150 
1151  if ($paramValue !== '') {
1152  $params[$paramKey] = $paramValue;
1153  } else {
1154  unset($params[$paramKey]);
1155  }
1156  }
1157  $paramString = '';
1158  foreach ($params as $paramKey => $paramValue) {
1159  $paramString .= htmlspecialchars((string)$paramKey) . '=' . htmlspecialchars((string)$paramValue) . ',';
1160  }
1161 
1162  $attrs = [
1163  'href' => (string)$url,
1164  'data-window-url' => $this->getTypoScriptFrontendController()->baseUrlWrap($url),
1165  'data-window-target' => $newWindow ? md5((string)$url) : 'thePicture',
1166  'data-window-features' => rtrim($paramString, ','),
1167  ];
1168  if ($target !== '') {
1169  $attrs['target'] = $target;
1170  }
1171 
1172  $a1 = sprintf(
1173  '<a %s%s>',
1174  GeneralUtility::implodeAttributes($attrs, true),
1175  trim($this->getTypoScriptFrontendController()->config['config']['ATagParams'] ?? '') ? ' ' . trim($this->getTypoScriptFrontendController()->config['config']['ATagParams']) : ''
1176  );
1177  $a2 = '</a>';
1178  $this->addDefaultFrontendJavaScript();
1179  } else {
1180  $conf['linkParams.']['directImageLink'] = (bool)($conf['directImageLink'] ?? false);
1181  $conf['linkParams.']['parameter'] = $url;
1182  $string = $this->typoLink($string, $conf['linkParams.']);
1183  }
1184  if (isset($conf['stdWrap.'])) {
1185  $string = $this->stdWrap($string, $conf['stdWrap.']);
1186  }
1187  $content = $a1 . $string . $a2;
1188  }
1189  return $content;
1190  }
1191 
1200  public function lastChanged($tstamp)
1201  {
1202  $tstamp = (int)$tstamp;
1203  $tsfe = $this->getTypoScriptFrontendController();
1204  if ($tstamp > (int)($tsfe->register['SYS_LASTCHANGED'] ?? 0)) {
1205  $tsfe->register['SYS_LASTCHANGED'] = $tstamp;
1206  }
1207  }
1208 
1218  public function getATagParams($conf, $addGlobal = null)
1219  {
1220  $aTagParams = ' ' . $this->stdWrapValue('ATagParams', $conf ?? []);
1221  if ($addGlobal !== null) {
1222  trigger_error('Setting the second argument $addGlobal of $cObj->getATagParams will have no effect in TYPO3 v12.0 anymore.', E_USER_DEPRECATED);
1223  }
1224  // Add the global config.ATagParams if $addGlobal is NULL (default) or set to TRUE.
1225  // @deprecated The if clause can be removed in v12
1226  if ($addGlobal === null || $addGlobal) {
1227  $globalParams = trim($this->getTypoScriptFrontendController()->config['config']['ATagParams'] ?? '');
1228  $aTagParams = ' ' . trim($globalParams . $aTagParams);
1229  }
1230  // Extend params
1231  $_params = [
1232  'conf' => &$conf,
1233  'aTagParams' => &$aTagParams,
1234  ];
1235  foreach (‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['getATagParamsPostProc'] ?? [] as $className) {
1236  $processor = GeneralUtility::makeInstance($className);
1237  $aTagParams = $processor->process($_params, $this);
1238  }
1239 
1240  $aTagParams = trim($aTagParams);
1241  if (!empty($aTagParams)) {
1242  $aTagParams = ' ' . $aTagParams;
1243  }
1244 
1245  return $aTagParams;
1246  }
1247 
1248  /***********************************************
1249  *
1250  * HTML template processing functions
1251  *
1252  ***********************************************/
1253 
1259  public function setCurrentFile($fileObject)
1260  {
1261  $this->currentFile = $fileObject;
1262  }
1263 
1269  public function getCurrentFile()
1270  {
1271  return $this->currentFile;
1272  }
1273 
1274  /***********************************************
1275  *
1276  * "stdWrap" + sub functions
1277  *
1278  ***********************************************/
1288  public function stdWrap($content = '', $conf = [])
1289  {
1290  $content = (string)$content;
1291  // If there is any hook object, activate all of the process and override functions.
1292  // The hook interface ContentObjectStdWrapHookInterface takes care that all 4 methods exist.
1293  if ($this->stdWrapHookObjects) {
1294  $conf['stdWrapPreProcess'] = 1;
1295  $conf['stdWrapOverride'] = 1;
1296  $conf['stdWrapProcess'] = 1;
1297  $conf['stdWrapPostProcess'] = 1;
1298  }
1299 
1300  if (!is_array($conf) || !$conf) {
1301  return $content;
1302  }
1303 
1304  // Cache handling
1305  if (isset($conf['cache.']) && is_array($conf['cache.'])) {
1306  $conf['cache.']['key'] = $this->stdWrapValue('key', $conf['cache.'] ?? []);
1307  $conf['cache.']['tags'] = $this->stdWrapValue('tags', $conf['cache.'] ?? []);
1308  $conf['cache.']['lifetime'] = $this->stdWrapValue('lifetime', $conf['cache.'] ?? []);
1309  $conf['cacheRead'] = 1;
1310  $conf['cacheStore'] = 1;
1311  }
1312  // The configuration is sorted and filtered by intersection with the defined stdWrapOrder.
1313  $sortedConf = array_keys(array_intersect_key($this->stdWrapOrder, $conf));
1314  // Functions types that should not make use of nested stdWrap function calls to avoid conflicts with internal TypoScript used by these functions
1315  $stdWrapDisabledFunctionTypes = 'cObject,functionName,stdWrap';
1316  // Additional Array to check whether a function has already been executed
1317  $isExecuted = [];
1318  // Additional switch to make sure 'required', 'if' and 'fieldRequired'
1319  // will still stop rendering immediately in case they return FALSE
1320  $this->stdWrapRecursionLevel++;
1321  $this->stopRendering[$this->stdWrapRecursionLevel] = false;
1322  // execute each function in the predefined order
1323  foreach ($sortedConf as $stdWrapName) {
1324  // eliminate the second key of a pair 'key'|'key.' to make sure functions get called only once and check if rendering has been stopped
1325  if ((!isset($isExecuted[$stdWrapName]) || !$isExecuted[$stdWrapName]) && !$this->stopRendering[$this->stdWrapRecursionLevel]) {
1326  $functionName = rtrim($stdWrapName, '.');
1327  $functionProperties = $functionName . '.';
1328  $functionType = $this->stdWrapOrder[$functionName] ?? '';
1329  // If there is any code on the next level, check if it contains "official" stdWrap functions
1330  // if yes, execute them first - will make each function stdWrap aware
1331  // so additional stdWrap calls within the functions can be removed, since the result will be the same
1332  if (!empty($conf[$functionProperties]) && !GeneralUtility::inList($stdWrapDisabledFunctionTypes, $functionType)) {
1333  if (array_intersect_key($this->stdWrapOrder, $conf[$functionProperties])) {
1334  // Check if there's already content available before processing
1335  // any ifEmpty or ifBlank stdWrap properties
1336  if (($functionName === 'ifBlank' && $content !== '') ||
1337  ($functionName === 'ifEmpty' && !empty(trim((string)$content)))) {
1338  continue;
1339  }
1340 
1341  $conf[$functionName] = $this->stdWrap($conf[$functionName] ?? '', $conf[$functionProperties] ?? []);
1342  }
1343  }
1344  // Check if key is still containing something, since it might have been changed by next level stdWrap before
1345  if ((isset($conf[$functionName]) || ($conf[$functionProperties] ?? null))
1346  && ($functionType !== 'boolean' || ($conf[$functionName] ?? null))
1347  ) {
1348  // Get just that part of $conf that is needed for the particular function
1349  $singleConf = [
1350  $functionName => $conf[$functionName] ?? null,
1351  $functionProperties => $conf[$functionProperties] ?? null,
1352  ];
1353  // Hand over the whole $conf array to the stdWrapHookObjects
1354  if ($functionType === 'hook') {
1355  $singleConf = $conf;
1356  }
1357  // Add both keys - with and without the dot - to the set of executed functions
1358  $isExecuted[$functionName] = true;
1359  $isExecuted[$functionProperties] = true;
1360  // Call the function with the prefix stdWrap_ to make sure nobody can execute functions just by adding their name to the TS Array
1361  $functionName = 'stdWrap_' . $functionName;
1362  $content = $this->{$functionName}($content, $singleConf);
1363  } elseif ($functionType === 'boolean' && !($conf[$functionName] ?? null)) {
1364  $isExecuted[$functionName] = true;
1365  $isExecuted[$functionProperties] = true;
1366  }
1367  }
1368  }
1369  unset($this->stopRendering[$this->stdWrapRecursionLevel]);
1370  $this->stdWrapRecursionLevel--;
1371 
1372  return $content;
1373  }
1374 
1383  public function stdWrapValue($key, array $config, $defaultValue = '')
1384  {
1385  if (isset($config[$key])) {
1386  if (!isset($config[$key . '.'])) {
1387  return $config[$key];
1388  }
1389  } elseif (isset($config[$key . '.'])) {
1390  $config[$key] = '';
1391  } else {
1392  return $defaultValue;
1393  }
1394  $stdWrapped = $this->stdWrap($config[$key], $config[$key . '.']);
1395  // The string "0" should be returned.
1396  return $stdWrapped !== '' ? $stdWrapped : $defaultValue;
1397  }
1398 
1408  public function stdWrap_stdWrapPreProcess($content = '', $conf = [])
1409  {
1410  foreach ($this->stdWrapHookObjects as $hookObject) {
1412  $content = $hookObject->stdWrapPreProcess($content, $conf, $this);
1413  }
1414  return $content;
1415  }
1416 
1424  public function stdWrap_cacheRead($content = '', $conf = [])
1425  {
1426  if (!isset($conf['cache.'])) {
1427  return $content;
1428  }
1429  $result = $this->getFromCache($conf['cache.']);
1430  return $result === false ? $content : $result;
1431  }
1432 
1440  public function stdWrap_addPageCacheTags($content = '', $conf = [])
1441  {
1442  $tags = (string)$this->stdWrapValue('addPageCacheTags', $conf ?? []);
1443  if (!empty($tags)) {
1444  $cacheTags = ‪GeneralUtility::trimExplode(',', $tags, true);
1445  $this->getTypoScriptFrontendController()->addCacheTags($cacheTags);
1446  }
1447  return $content;
1448  }
1449 
1457  public function stdWrap_setContentToCurrent($content = '')
1458  {
1459  $this->data[$this->currentValKey] = $content;
1460  return $content;
1461  }
1462 
1471  public function stdWrap_setCurrent($content = '', $conf = [])
1472  {
1473  $this->data[$this->currentValKey] = $conf['setCurrent'] ?? null;
1474  return $content;
1475  }
1476 
1485  public function stdWrap_lang($content = '', $conf = [])
1486  {
1487  $currentLanguageCode = $this->getTypoScriptFrontendController()->getLanguage()->getTypo3Language();
1488  if (!$currentLanguageCode) {
1489  return $content;
1490  }
1491  if (isset($conf['lang.'][$currentLanguageCode])) {
1492  $content = $conf['lang.'][$currentLanguageCode];
1493  } else {
1494  // Check language dependencies
1495  $locales = GeneralUtility::makeInstance(Locales::class);
1496  foreach ($locales->getLocaleDependencies($currentLanguageCode) as $languageCode) {
1497  if (isset($conf['lang.'][$languageCode])) {
1498  $content = $conf['lang.'][$languageCode];
1499  break;
1500  }
1501  }
1502  }
1503  return $content;
1504  }
1505 
1513  public function stdWrap_data($content = '', $conf = [])
1514  {
1515  // @deprecated since v11, will be removed in v12. Drop together with property $this->alternativeData.
1516  // @todo v12 version: "return $this->getData($conf['data'], $this->data);"
1517  $content = $this->getData($conf['data'], is_array($this->alternativeData) ? $this->alternativeData : $this->data);
1518  $this->alternativeData = '';
1519  return $content;
1520  }
1521 
1530  public function stdWrap_field($content = '', $conf = [])
1531  {
1532  return $this->getFieldVal($conf['field']);
1533  }
1534 
1544  public function stdWrap_current($content = '', $conf = [])
1545  {
1546  return $this->getCurrentVal();
1547  }
1548 
1558  public function stdWrap_cObject($content = '', $conf = [])
1559  {
1560  return $this->cObjGetSingle($conf['cObject'] ?? '', $conf['cObject.'] ?? [], '/stdWrap/.cObject');
1561  }
1562 
1572  public function stdWrap_numRows($content = '', $conf = [])
1573  {
1574  return $this->numRows($conf['numRows.']);
1575  }
1576 
1585  public function stdWrap_preUserFunc($content = '', $conf = [])
1586  {
1587  return $this->callUserFunction($conf['preUserFunc'], $conf['preUserFunc.'] ?? [], $content);
1588  }
1589 
1599  public function stdWrap_stdWrapOverride($content = '', $conf = [])
1600  {
1601  foreach ($this->stdWrapHookObjects as $hookObject) {
1603  $content = $hookObject->stdWrapOverride($content, $conf, $this);
1604  }
1605  return $content;
1606  }
1607 
1616  public function stdWrap_override($content = '', $conf = [])
1617  {
1618  if (trim($conf['override'] ?? false)) {
1619  $content = $conf['override'];
1620  }
1621  return $content;
1622  }
1623 
1633  public function stdWrap_preIfEmptyListNum($content = '', $conf = [])
1634  {
1635  return $this->listNum($content, $conf['preIfEmptyListNum'] ?? null, $conf['preIfEmptyListNum.']['splitChar'] ?? null);
1636  }
1637 
1646  public function stdWrap_ifNull($content = '', $conf = [])
1647  {
1648  return $content ?? $conf['ifNull'];
1649  }
1650 
1660  public function stdWrap_ifEmpty($content = '', $conf = [])
1661  {
1662  if (empty(trim((string)$content))) {
1663  $content = $conf['ifEmpty'];
1664  }
1665  return $content;
1666  }
1667 
1677  public function stdWrap_ifBlank($content = '', $conf = [])
1678  {
1679  if (trim((string)$content) === '') {
1680  $content = $conf['ifBlank'];
1681  }
1682  return $content;
1683  }
1684 
1695  public function stdWrap_listNum($content = '', $conf = [])
1696  {
1697  return $this->listNum($content, $conf['listNum'] ?? null, $conf['listNum.']['splitChar'] ?? null);
1698  }
1699 
1707  public function stdWrap_trim($content = '')
1708  {
1709  return trim((string)$content);
1710  }
1711 
1720  public function stdWrap_strPad($content = '', $conf = [])
1721  {
1722  // Must specify a length in conf for this to make sense
1723  $length = (int)$this->stdWrapValue('length', $conf['strPad.'] ?? [], 0);
1724  // Padding with space is PHP-default
1725  $padWith = (string)$this->stdWrapValue('padWith', $conf['strPad.'] ?? [], ' ');
1726  // Padding on the right side is PHP-default
1727  $padType = STR_PAD_RIGHT;
1728 
1729  if (!empty($conf['strPad.']['type'])) {
1730  $type = (string)$this->stdWrapValue('type', $conf['strPad.'] ?? []);
1731  if (strtolower($type) === 'left') {
1732  $padType = STR_PAD_LEFT;
1733  } elseif (strtolower($type) === 'both') {
1734  $padType = STR_PAD_BOTH;
1735  }
1736  }
1737  return ‪StringUtility::multibyteStringPad($content, $length, $padWith, $padType);
1738  }
1739 
1751  public function stdWrap_stdWrap($content = '', $conf = [])
1752  {
1753  return $this->stdWrap($content, $conf['stdWrap.']);
1754  }
1755 
1765  public function stdWrap_stdWrapProcess($content = '', $conf = [])
1766  {
1767  foreach ($this->stdWrapHookObjects as $hookObject) {
1769  $content = $hookObject->stdWrapProcess($content, $conf, $this);
1770  }
1771  return $content;
1772  }
1773 
1782  public function stdWrap_required($content = '')
1783  {
1784  if ((string)$content === '') {
1785  $content = '';
1786  $this->stopRendering[$this->stdWrapRecursionLevel] = true;
1787  }
1788  return $content;
1789  }
1790 
1800  public function stdWrap_if($content = '', $conf = [])
1801  {
1802  if (empty($conf['if.']) || $this->checkIf($conf['if.'])) {
1803  return $content;
1804  }
1805  $this->stopRendering[$this->stdWrapRecursionLevel] = true;
1806  return '';
1807  }
1808 
1818  public function stdWrap_fieldRequired($content = '', $conf = [])
1819  {
1820  if (!trim($this->data[$conf['fieldRequired'] ?? null] ?? '')) {
1821  $content = '';
1822  $this->stopRendering[$this->stdWrapRecursionLevel] = true;
1823  }
1824  return $content;
1825  }
1826 
1837  public function stdWrap_csConv($content = '', $conf = [])
1838  {
1839  if (!empty($conf['csConv'])) {
1840  ‪$output = mb_convert_encoding($content, 'utf-8', trim(strtolower($conf['csConv'])));
1841  return ‪$output !== false && ‪$output !== '' ? ‪$output : $content;
1842  }
1843  return $content;
1844  }
1845 
1855  public function stdWrap_parseFunc($content = '', $conf = [])
1856  {
1857  return $this->parseFunc($content, $conf['parseFunc.'], $conf['parseFunc']);
1858  }
1859 
1869  public function stdWrap_HTMLparser($content = '', $conf = [])
1870  {
1871  if (isset($conf['HTMLparser.']) && is_array($conf['HTMLparser.'])) {
1872  $content = $this->HTMLparser_TSbridge($content, $conf['HTMLparser.']);
1873  }
1874  return $content;
1875  }
1876 
1886  public function stdWrap_split($content = '', $conf = [])
1887  {
1888  return $this->splitObj($content, $conf['split.']);
1889  }
1890 
1899  public function stdWrap_replacement($content = '', $conf = [])
1900  {
1901  return $this->replacement($content, $conf['replacement.']);
1902  }
1903 
1913  public function stdWrap_prioriCalc($content = '', $conf = [])
1914  {
1915  $content = ‪MathUtility::calculateWithParentheses($content);
1916  if (!empty($conf['prioriCalc']) && $conf['prioriCalc'] === 'intval') {
1917  $content = (int)$content;
1918  }
1919  return $content;
1920  }
1921 
1933  public function stdWrap_char($content = '', $conf = [])
1934  {
1935  return chr((int)$conf['char']);
1936  }
1937 
1945  public function stdWrap_intval($content = '')
1946  {
1947  return (int)$content;
1948  }
1949 
1958  public function stdWrap_hash($content = '', array $conf = [])
1959  {
1960  $algorithm = (string)$this->stdWrapValue('hash', $conf ?? []);
1961  if (function_exists('hash') && in_array($algorithm, hash_algos())) {
1962  return hash($algorithm, $content);
1963  }
1964  // Non-existing hashing algorithm
1965  return '';
1966  }
1967 
1976  public function stdWrap_round($content = '', $conf = [])
1977  {
1978  return $this->round($content, $conf['round.']);
1979  }
1980 
1989  public function stdWrap_numberFormat($content = '', $conf = [])
1990  {
1991  return $this->numberFormat((float)$content, $conf['numberFormat.'] ?? []);
1992  }
1993 
2001  public function stdWrap_expandList($content = '')
2002  {
2003  return GeneralUtility::expandList($content);
2004  }
2005 
2015  public function stdWrap_date($content = '', $conf = [])
2016  {
2017  // Check for zero length string to mimic default case of date/gmdate.
2018  $content = (string)$content === '' ? ‪$GLOBALS['EXEC_TIME'] : (int)$content;
2019  $content = !empty($conf['date.']['GMT']) ? gmdate($conf['date'] ?? null, $content) : date($conf['date'] ?? null, $content);
2020  return $content;
2021  }
2022 
2033  public function stdWrap_strftime($content = '', $conf = [])
2034  {
2035  // Check for zero length string to mimic default case of strtime/gmstrftime
2036  $content = (string)$content === '' ? ‪$GLOBALS['EXEC_TIME'] : (int)$content;
2037  $content = (isset($conf['strftime.']['GMT']) && $conf['strftime.']['GMT'])
2038  ? @gmstrftime($conf['strftime'] ?? null, $content)
2039  : @strftime($conf['strftime'] ?? null, $content);
2040  if (!empty($conf['strftime.']['charset'])) {
2041  ‪$output = mb_convert_encoding((string)$content, 'utf-8', trim(strtolower($conf['strftime.']['charset'])));
2042  return ‪$output ?: $content;
2043  }
2044  return $content;
2045  }
2046 
2055  public function stdWrap_strtotime($content = '', $conf = [])
2056  {
2057  if ($conf['strtotime'] !== '1') {
2058  $content .= ' ' . $conf['strtotime'];
2059  }
2060  return strtotime($content, ‪$GLOBALS['EXEC_TIME']);
2061  }
2062 
2071  public function stdWrap_age($content = '', $conf = [])
2072  {
2073  return $this->calcAge((int)(‪$GLOBALS['EXEC_TIME'] ?? 0) - (int)$content, $conf['age'] ?? null);
2074  }
2075 
2085  public function stdWrap_case($content = '', $conf = [])
2086  {
2087  return $this->HTMLcaseshift($content, $conf['case']);
2088  }
2089 
2098  public function stdWrap_bytes($content = '', $conf = [])
2099  {
2100  return GeneralUtility::formatSize((int)$content, $conf['bytes.']['labels'] ?? '', $conf['bytes.']['base'] ?? 0);
2101  }
2102 
2111  public function stdWrap_substring($content = '', $conf = [])
2112  {
2113  return $this->substring($content, $conf['substring']);
2114  }
2115 
2124  public function stdWrap_cropHTML($content = '', $conf = [])
2125  {
2126  return $this->cropHTML($content, $conf['cropHTML'] ?? '');
2127  }
2128 
2136  public function stdWrap_stripHtml($content = '')
2137  {
2138  return strip_tags($content);
2139  }
2140 
2149  public function stdWrap_crop($content = '', $conf = [])
2150  {
2151  return $this->crop($content, $conf['crop']);
2152  }
2153 
2161  public function stdWrap_rawUrlEncode($content = '')
2162  {
2163  return rawurlencode($content);
2164  }
2165 
2175  public function stdWrap_htmlSpecialChars($content = '', $conf = [])
2176  {
2177  if (!empty($conf['htmlSpecialChars.']['preserveEntities'])) {
2178  $content = htmlspecialchars($content, ENT_COMPAT, 'UTF-8', false);
2179  } else {
2180  $content = htmlspecialchars($content);
2181  }
2182  return $content;
2183  }
2184 
2192  public function stdWrap_encodeForJavaScriptValue($content = '')
2193  {
2194  return GeneralUtility::quoteJSvalue($content);
2195  }
2196 
2205  public function stdWrap_doubleBrTag($content = '', $conf = [])
2206  {
2207  return preg_replace('/\R{1,2}[\t\x20]*\R{1,2}/', $conf['doubleBrTag'] ?? '', $content);
2208  }
2209 
2218  public function stdWrap_br($content = '')
2219  {
2220  return nl2br($content, !empty($this->getTypoScriptFrontendController()->xhtmlDoctype));
2221  }
2222 
2231  public function stdWrap_brTag($content = '', $conf = [])
2232  {
2233  return str_replace(LF, (string)($conf['brTag'] ?? ''), $content);
2234  }
2235 
2245  public function stdWrap_encapsLines($content = '', $conf = [])
2246  {
2247  return $this->encaps_lineSplit($content, $conf['encapsLines.']);
2248  }
2249 
2257  public function stdWrap_keywords($content = '')
2258  {
2259  return $this->keywords($content);
2260  }
2261 
2271  public function stdWrap_innerWrap($content = '', $conf = [])
2272  {
2273  return $this->wrap($content, $conf['innerWrap'] ?? null);
2274  }
2275 
2285  public function stdWrap_innerWrap2($content = '', $conf = [])
2286  {
2287  return $this->wrap($content, $conf['innerWrap2'] ?? null);
2288  }
2289 
2298  public function stdWrap_preCObject($content = '', $conf = [])
2299  {
2300  return $this->cObjGetSingle($conf['preCObject'], $conf['preCObject.'], '/stdWrap/.preCObject') . $content;
2301  }
2302 
2311  public function stdWrap_postCObject($content = '', $conf = [])
2312  {
2313  return $content . $this->cObjGetSingle($conf['postCObject'], $conf['postCObject.'], '/stdWrap/.postCObject');
2314  }
2315 
2325  public function stdWrap_wrapAlign($content = '', $conf = [])
2326  {
2327  $wrapAlign = trim($conf['wrapAlign'] ?? '');
2328  if ($wrapAlign) {
2329  $content = $this->wrap($content, '<div style="text-align:' . htmlspecialchars($wrapAlign) . ';">|</div>');
2330  }
2331  return $content;
2332  }
2333 
2344  public function stdWrap_typolink($content = '', $conf = [])
2345  {
2346  return $this->typoLink($content, $conf['typolink.']);
2347  }
2348 
2361  public function stdWrap_wrap($content = '', $conf = [])
2362  {
2363  return $this->wrap(
2364  $content,
2365  $conf['wrap'] ?? null,
2366  $conf['wrap.']['splitChar'] ?? '|'
2367  );
2368  }
2369 
2379  public function stdWrap_noTrimWrap($content = '', $conf = [])
2380  {
2381  $splitChar = isset($conf['noTrimWrap.']['splitChar.'])
2382  ? $this->stdWrap($conf['noTrimWrap.']['splitChar'] ?? '', $conf['noTrimWrap.']['splitChar.'])
2383  : $conf['noTrimWrap.']['splitChar'] ?? '';
2384  if ($splitChar === null || $splitChar === '') {
2385  $splitChar = '|';
2386  }
2387  $content = $this->noTrimWrap(
2388  $content,
2389  $conf['noTrimWrap'],
2390  $splitChar
2391  );
2392  return $content;
2393  }
2394 
2404  public function stdWrap_wrap2($content = '', $conf = [])
2405  {
2406  return $this->wrap(
2407  $content,
2408  $conf['wrap2'] ?? null,
2409  $conf['wrap2.']['splitChar'] ?? '|'
2410  );
2411  }
2412 
2422  public function stdWrap_dataWrap($content = '', $conf = [])
2423  {
2424  return $this->dataWrap($content, $conf['dataWrap']);
2425  }
2426 
2435  public function stdWrap_prepend($content = '', $conf = [])
2436  {
2437  return $this->cObjGetSingle($conf['prepend'], $conf['prepend.'], '/stdWrap/.prepend') . $content;
2438  }
2439 
2448  public function stdWrap_append($content = '', $conf = [])
2449  {
2450  return $content . $this->cObjGetSingle($conf['append'], $conf['append.'], '/stdWrap/.append');
2451  }
2452 
2462  public function stdWrap_wrap3($content = '', $conf = [])
2463  {
2464  return $this->wrap(
2465  $content,
2466  $conf['wrap3'] ?? null,
2467  $conf['wrap3.']['splitChar'] ?? '|'
2468  );
2469  }
2470 
2479  public function stdWrap_orderedStdWrap($content = '', $conf = [])
2480  {
2481  $sortedKeysArray = ‪ArrayUtility::filterAndSortByNumericKeys($conf['orderedStdWrap.'], true);
2482  foreach ($sortedKeysArray as $key) {
2483  $content = (string)$this->stdWrap($content, $conf['orderedStdWrap.'][$key . '.'] ?? null);
2484  }
2485  return $content;
2486  }
2487 
2496  public function stdWrap_outerWrap($content = '', $conf = [])
2497  {
2498  return $this->wrap($content, $conf['outerWrap'] ?? null);
2499  }
2500 
2508  public function stdWrap_insertData($content = '')
2509  {
2510  return $this->insertData($content);
2511  }
2512 
2521  public function stdWrap_postUserFunc($content = '', $conf = [])
2522  {
2523  return $this->callUserFunction($conf['postUserFunc'], $conf['postUserFunc.'] ?? [], $content);
2524  }
2525 
2535  public function stdWrap_postUserFuncInt($content = '', $conf = [])
2536  {
2537  $substKey = 'INT_SCRIPT.' . $this->getTypoScriptFrontendController()->uniqueHash();
2538  $this->getTypoScriptFrontendController()->config['INTincScript'][$substKey] = [
2539  'content' => $content,
2540  'postUserFunc' => $conf['postUserFuncInt'],
2541  'conf' => $conf['postUserFuncInt.'],
2542  'type' => 'POSTUSERFUNC',
2543  'cObj' => serialize($this),
2544  ];
2545  $content = '<!--' . $substKey . '-->';
2546  return $content;
2547  }
2548 
2557  public function stdWrap_prefixComment($content = '', $conf = [])
2558  {
2559  if (
2560  (!isset($this->getTypoScriptFrontendController()->config['config']['disablePrefixComment']) || !$this->getTypoScriptFrontendController()->config['config']['disablePrefixComment'])
2561  && !empty($conf['prefixComment'])
2562  ) {
2563  $content = $this->prefixComment($conf['prefixComment'], [], $content);
2564  }
2565  return $content;
2566  }
2567 
2577  public function stdWrap_editIcons($content = '', $conf = [])
2578  {
2579  if ($this->getTypoScriptFrontendController()->isBackendUserLoggedIn() && $conf['editIcons']) {
2580  if (!isset($conf['editIcons.']) || !is_array($conf['editIcons.'])) {
2581  $conf['editIcons.'] = [];
2582  }
2583  $content = $this->editIcons($content, $conf['editIcons'], $conf['editIcons.']);
2584  }
2585  return $content;
2586  }
2587 
2597  public function stdWrap_editPanel($content = '', $conf = [])
2598  {
2599  if ($this->getTypoScriptFrontendController()->isBackendUserLoggedIn()) {
2600  $content = $this->editPanel($content, $conf['editPanel.']);
2601  }
2602  return $content;
2603  }
2604 
2605  public function stdWrap_htmlSanitize(string $content = '', array $conf = []): string
2606  {
2607  $build = $conf['build'] ?? 'default';
2608  if (class_exists($build) && is_a($build, BuilderInterface::class, true)) {
2609  $builder = GeneralUtility::makeInstance($build);
2610  } else {
2611  $factory = GeneralUtility::makeInstance(SanitizerBuilderFactory::class);
2612  $builder = $factory->build($build);
2613  }
2614  $sanitizer = $builder->build();
2615  $initiator = $this->shallDebug()
2616  ? GeneralUtility::makeInstance(SanitizerInitiator::class, ‪DebugUtility::debugTrail())
2617  : null;
2618  return $sanitizer->sanitize($content, $initiator);
2619  }
2620 
2628  public function stdWrap_cacheStore($content = '', $conf = [])
2629  {
2630  if (!isset($conf['cache.'])) {
2631  return $content;
2632  }
2633  $key = $this->calculateCacheKey($conf['cache.']);
2634  if (empty($key)) {
2635  return $content;
2636  }
2637  $cacheFrontend = GeneralUtility::makeInstance(CacheManager::class)->getCache('hash');
2638  $tags = $this->calculateCacheTags($conf['cache.']);
2639  $lifetime = $this->calculateCacheLifetime($conf['cache.']);
2640  foreach (‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['stdWrap_cacheStore'] ?? [] as $_funcRef) {
2641  $params = [
2642  'key' => $key,
2643  'content' => $content,
2644  'lifetime' => $lifetime,
2645  'tags' => $tags,
2646  ];
2647  $ref = $this; // introduced for phpstan to not lose type information when passing $this into callUserFunction
2648  GeneralUtility::callUserFunction($_funcRef, $params, $ref);
2649  }
2650  $cacheFrontend->set($key, $content, $tags, $lifetime);
2651  return $content;
2652  }
2653 
2663  public function stdWrap_stdWrapPostProcess($content = '', $conf = [])
2664  {
2665  foreach ($this->stdWrapHookObjects as $hookObject) {
2667  $content = $hookObject->stdWrapPostProcess($content, $conf, $this);
2668  }
2669  return $content;
2670  }
2671 
2679  public function stdWrap_debug($content = '')
2680  {
2681  return '<pre>' . htmlspecialchars($content) . '</pre>';
2682  }
2683 
2692  public function stdWrap_debugFunc($content = '', $conf = [])
2693  {
2694  ‪debug((int)$conf['debugFunc'] === 2 ? [$content] : $content);
2695  return $content;
2696  }
2697 
2705  public function stdWrap_debugData($content = '')
2706  {
2707  ‪debug($this->data, '$cObj->data:');
2708  if (is_array($this->alternativeData)) {
2709  // @deprecated since v11, will be removed in v12. Drop together with property $this->alternativeData
2710  ‪debug($this->alternativeData, '$this->alternativeData');
2711  }
2712  return $content;
2713  }
2714 
2724  public function numRows($conf)
2725  {
2726  $conf['select.']['selectFields'] = 'count(*)';
2727  $statement = $this->exec_getQuery($conf['table'], $conf['select.']);
2728 
2729  return (int)$statement->fetchOne();
2730  }
2731 
2740  public function listNum($content, $listNum, $char)
2741  {
2742  $char = $char ?: ',';
2744  $char = chr((int)$char);
2745  }
2746  $temp = explode($char, $content);
2747  if (empty($temp)) {
2748  return '';
2749  }
2750  $last = '' . (count($temp) - 1);
2751  // Take a random item if requested
2752  if ($listNum === 'rand') {
2753  $listNum = (string)random_int(0, count($temp) - 1);
2754  }
2755  $index = $this->calc(str_ireplace('last', $last, $listNum));
2756  return $temp[$index] ?? '';
2757  }
2758 
2768  public function checkIf($conf)
2769  {
2770  if (!is_array($conf)) {
2771  return true;
2772  }
2773  if (isset($conf['directReturn'])) {
2774  return (bool)$conf['directReturn'];
2775  }
2776  $flag = true;
2777  if (isset($conf['isNull.'])) {
2778  $isNull = $this->stdWrap('', $conf['isNull.']);
2779  if ($isNull !== null) {
2780  $flag = false;
2781  }
2782  }
2783  if (isset($conf['isTrue']) || isset($conf['isTrue.'])) {
2784  $isTrue = trim((string)$this->stdWrapValue('isTrue', $conf ?? []));
2785  if (!$isTrue) {
2786  $flag = false;
2787  }
2788  }
2789  if (isset($conf['isFalse']) || isset($conf['isFalse.'])) {
2790  $isFalse = trim((string)$this->stdWrapValue('isFalse', $conf ?? []));
2791  if ($isFalse) {
2792  $flag = false;
2793  }
2794  }
2795  if (isset($conf['isPositive']) || isset($conf['isPositive.'])) {
2796  $number = $this->calc((string)$this->stdWrapValue('isPositive', $conf ?? []));
2797  if ($number < 1) {
2798  $flag = false;
2799  }
2800  }
2801  if ($flag) {
2802  $value = trim((string)$this->stdWrapValue('value', $conf ?? []));
2803  if (isset($conf['isGreaterThan']) || isset($conf['isGreaterThan.'])) {
2804  $number = trim((string)$this->stdWrapValue('isGreaterThan', $conf ?? []));
2805  if ($number <= $value) {
2806  $flag = false;
2807  }
2808  }
2809  if (isset($conf['isLessThan']) || isset($conf['isLessThan.'])) {
2810  $number = trim((string)$this->stdWrapValue('isLessThan', $conf ?? []));
2811  if ($number >= $value) {
2812  $flag = false;
2813  }
2814  }
2815  if (isset($conf['equals']) || isset($conf['equals.'])) {
2816  $number = trim((string)$this->stdWrapValue('equals', $conf ?? []));
2817  if ($number != $value) {
2818  $flag = false;
2819  }
2820  }
2821  if (isset($conf['isInList']) || isset($conf['isInList.'])) {
2822  $number = trim((string)$this->stdWrapValue('isInList', $conf ?? []));
2823  if (!GeneralUtility::inList($value, $number)) {
2824  $flag = false;
2825  }
2826  }
2827  if (isset($conf['bitAnd']) || isset($conf['bitAnd.'])) {
2828  $number = (int)trim((string)$this->stdWrapValue('bitAnd', $conf ?? []));
2829  if ((new BitSet($number))->get($value) === false) {
2830  $flag = false;
2831  }
2832  }
2833  }
2834  if ($conf['negate'] ?? false) {
2835  $flag = !$flag;
2836  }
2837  return $flag;
2838  }
2839 
2852  public function HTMLparser_TSbridge($theValue, $conf)
2853  {
2854  $htmlParser = GeneralUtility::makeInstance(HtmlParser::class);
2855  $htmlParserCfg = $htmlParser->HTMLparserConfig($conf);
2856  return $htmlParser->HTMLcleaner($theValue, $htmlParserCfg[0], $htmlParserCfg[1], $htmlParserCfg[2], $htmlParserCfg[3]);
2857  }
2858 
2868  public function dataWrap($content, $wrap)
2869  {
2870  return $this->wrap($content, $this->insertData($wrap));
2871  }
2872 
2888  public function insertData($str)
2889  {
2890  $inside = 0;
2891  $newVal = '';
2892  $pointer = 0;
2893  $totalLen = strlen($str);
2894  do {
2895  if (!$inside) {
2896  $len = strcspn(substr($str, $pointer), '{');
2897  $newVal .= substr($str, $pointer, $len);
2898  $inside = true;
2899  if (substr($str, $pointer + $len + 1, 1) === '#') {
2900  $len2 = strcspn(substr($str, $pointer + $len), '}');
2901  $newVal .= substr($str, $pointer + $len, $len2);
2902  $len += $len2;
2903  $inside = false;
2904  }
2905  } else {
2906  $len = strcspn(substr($str, $pointer), '}') + 1;
2907  $newVal .= $this->getData(substr($str, $pointer + 1, $len - 2), $this->data);
2908  $inside = false;
2909  }
2910  $pointer += $len;
2911  } while ($pointer < $totalLen);
2912  return $newVal;
2913  }
2914 
2925  public function prefixComment($str, $conf, $content)
2926  {
2927  if (empty($str)) {
2928  return $content;
2929  }
2930  $parts = explode('|', $str);
2931  $indent = (int)$parts[0];
2932  $comment = htmlspecialchars($this->insertData($parts[1]));
2933  ‪$output = LF
2934  . str_pad('', $indent, "\t") . '<!-- ' . $comment . ' [begin] -->' . LF
2935  . str_pad('', $indent + 1, "\t") . $content . LF
2936  . str_pad('', $indent, "\t") . '<!-- ' . $comment . ' [end] -->' . LF
2937  . str_pad('', $indent + 1, "\t");
2938  return ‪$output;
2939  }
2940 
2950  public function substring($content, $options)
2951  {
2952  $options = ‪GeneralUtility::intExplode(',', $options . ',');
2953  if ($options[1]) {
2954  return mb_substr($content, $options[0], $options[1], 'utf-8');
2955  }
2956  return mb_substr($content, $options[0], null, 'utf-8');
2957  }
2958 
2968  public function crop($content, $options)
2969  {
2970  $options = explode('|', $options);
2971  $chars = (int)$options[0];
2972  $afterstring = trim($options[1] ?? '');
2973  $crop2space = trim($options[2] ?? '');
2974  if ($chars) {
2975  if (mb_strlen($content, 'utf-8') > abs($chars)) {
2976  $truncatePosition = false;
2977  if ($chars < 0) {
2978  $content = mb_substr($content, $chars, null, 'utf-8');
2979  if ($crop2space) {
2980  $truncatePosition = strpos($content, ' ');
2981  }
2982  $content = $truncatePosition ? $afterstring . substr($content, $truncatePosition) : $afterstring . $content;
2983  } else {
2984  $content = mb_substr($content, 0, $chars, 'utf-8');
2985  if ($crop2space) {
2986  $truncatePosition = strrpos($content, ' ');
2987  }
2988  $content = $truncatePosition ? substr($content, 0, $truncatePosition) . $afterstring : $content . $afterstring;
2989  }
2990  }
2991  }
2992  return $content;
2993  }
2994 
3010  public function cropHTML($content, $options)
3011  {
3012  $content = (string)$content;
3013  $options = (string)$options;
3014  $options = explode('|', $options);
3015  $chars = (int)$options[0];
3016  $absChars = abs($chars);
3017  $replacementForEllipsis = trim($options[1] ?? '');
3018  $crop2space = trim($options[2] ?? '') === '1';
3019  // Split $content into an array(even items in the array are outside the tags, odd numbers are tag-blocks).
3020  $tags = 'a|abbr|address|area|article|aside|audio|b|bdi|bdo|blockquote|body|br|button|caption|cite|code|col|colgroup|data|datalist|dd|del|dfn|div|dl|dt|em|embed|fieldset|figcaption|figure|font|footer|form|h1|h2|h3|h4|h5|h6|header|hr|i|iframe|img|input|ins|kbd|keygen|label|legend|li|link|main|map|mark|meter|nav|object|ol|optgroup|option|output|p|param|pre|progress|q|rb|rp|rt|rtc|ruby|s|samp|section|select|small|source|span|strong|sub|sup|table|tbody|td|textarea|tfoot|th|thead|time|tr|track|u|ul|ut|var|video|wbr';
3021  $tagsRegEx = '
3022  (
3023  (?:
3024  <!--.*?--> # a comment
3025  |
3026  <canvas[^>]*>.*?</canvas> # a canvas tag
3027  |
3028  <script[^>]*>.*?</script> # a script tag
3029  |
3030  <noscript[^>]*>.*?</noscript> # a noscript tag
3031  |
3032  <template[^>]*>.*?</template> # a template tag
3033  )
3034  |
3035  </?(?:' . $tags . ')+ # opening tag (\'<tag\') or closing tag (\'</tag\')
3036  (?:
3037  (?:
3038  (?:
3039  \\s+\\w[\\w-]* # EITHER spaces, followed by attribute names
3040  (?:
3041  \\s*=?\\s* # equals
3042  (?>
3043  ".*?" # attribute values in double-quotes
3044  |
3045  \'.*?\' # attribute values in single-quotes
3046  |
3047  [^\'">\\s]+ # plain attribute values
3048  )
3049  )?
3050  )
3051  | # OR a single dash (for TYPO3 link tag)
3052  (?:
3053  \\s+-
3054  )
3055  )+\\s*
3056  | # OR only spaces
3057  \\s*
3058  )
3059  /?> # closing the tag with \'>\' or \'/>\'
3060  )';
3061  $splittedContent = preg_split('%' . $tagsRegEx . '%xs', $content, -1, PREG_SPLIT_DELIM_CAPTURE);
3062  if ($splittedContent === false) {
3063  $this->logger->debug('Unable to split "{content}" into tags.', ['content' => $content]);
3064  $splittedContent = [];
3065  }
3066  // Reverse array if we are cropping from right.
3067  if ($chars < 0) {
3068  $splittedContent = array_reverse($splittedContent);
3069  }
3070  // Crop the text (chars of tag-blocks are not counted).
3071  $strLen = 0;
3072  // This is the offset of the content item which was cropped.
3073  $croppedOffset = null;
3074  $countSplittedContent = count($splittedContent);
3075  // @todo $maxCroppingLength of 962 was determined by hand as the highest
3076  // value to not lead to internal error (Compilation failed: regular
3077  // expression is too large ). Still questionable if we really can
3078  // rely on a fixed value here, or better to say need to be understood
3079  // why the value has to be this value to avoid regular expression
3080  // compilation error.
3081  $maxCroppingLength = 962;
3082  for ($offset = 0; $offset < $countSplittedContent; $offset++) {
3083  if ($offset % 2 === 0) {
3084  $fullTempContent = $splittedContent[$offset];
3085  $thisStrLen = mb_strlen(html_entity_decode($fullTempContent, ENT_COMPAT, 'UTF-8'), 'utf-8');
3086  if ($strLen + $thisStrLen > $absChars) {
3087  $tempProcessedContent = '';
3088  $croppedOffset = $offset;
3089  $cropPosition = $absChars - $strLen;
3090  $cropEnd = ($cropPosition > $maxCroppingLength) ? $maxCroppingLength : $cropPosition;
3091  $processed = 0;
3092  // we need to crop in multiple steps to avoid regexp length compilation errors
3093  do {
3094  // remove already processed string part
3095  $tempContent = mb_substr($fullTempContent, mb_strlen($tempProcessedContent));
3096  $patternMatchEntityAsSingleChar = '(&[^&\\s;]{2,8};|.)';
3097  $cropRegEx = $chars < 0 ? '#' . $patternMatchEntityAsSingleChar . '{0,' . ($cropEnd + 1) . '}$#uis' : '#^' . $patternMatchEntityAsSingleChar . '{0,' . ($cropEnd + 1) . '}#uis';
3098  if (preg_match($cropRegEx, $tempContent, $croppedMatch)) {
3099  $tempContentPlusOneCharacter = $croppedMatch[0];
3100  } else {
3101  $tempContentPlusOneCharacter = false;
3102  }
3103  $cropRegEx = $chars < 0 ? '#' . $patternMatchEntityAsSingleChar . '{0,' . $cropEnd . '}$#uis' : '#^' . $patternMatchEntityAsSingleChar . '{0,' . $cropEnd . '}#uis';
3104  if (preg_match($cropRegEx, $tempContent, $croppedMatch)) {
3105  $tempContent = $croppedMatch[0];
3106  if ($crop2space && $tempContentPlusOneCharacter !== false) {
3107  $cropRegEx = $chars < 0 ? '#(?<=\\s)' . $patternMatchEntityAsSingleChar . '{0,' . $cropEnd . '}$#uis' : '#^' . $patternMatchEntityAsSingleChar . '{0,' . $cropEnd . '}(?=\\s)#uis';
3108  if (preg_match($cropRegEx, $tempContentPlusOneCharacter, $croppedMatch)) {
3109  $tempContent = $croppedMatch[0];
3110  }
3111  }
3112  }
3113  $tempProcessedContent .= $tempContent;
3114  $processed += $cropEnd;
3115  $cropEnd = ($processed + $maxCroppingLength > $cropPosition ? ($cropPosition - $processed) : $maxCroppingLength);
3116  } while ($cropEnd > 0 && $cropEnd < $cropPosition);
3117  $splittedContent[$offset] = $tempProcessedContent;
3118  break;
3119  }
3120  $strLen += $thisStrLen;
3121  }
3122  }
3123  // Close cropped tags.
3124  $closingTags = [];
3125  if ($croppedOffset !== null) {
3126  $openingTagRegEx = '#^<(\\w+)(?:\\s|>)#';
3127  $closingTagRegEx = '#^</(\\w+)(?:\\s|>)#';
3128  for ($offset = $croppedOffset - 1; $offset >= 0; $offset = $offset - 2) {
3129  if (substr($splittedContent[$offset], -2) === '/>') {
3130  // Ignore empty element tags (e.g. <br />).
3131  continue;
3132  }
3133  preg_match($chars < 0 ? $closingTagRegEx : $openingTagRegEx, $splittedContent[$offset], $matches);
3134  $tagName = $matches[1] ?? null;
3135  if ($tagName !== null) {
3136  // Seek for the closing (or opening) tag.
3137  $countSplittedContent = count($splittedContent);
3138  for ($seekingOffset = $offset + 2; $seekingOffset < $countSplittedContent; $seekingOffset = $seekingOffset + 2) {
3139  preg_match($chars < 0 ? $openingTagRegEx : $closingTagRegEx, $splittedContent[$seekingOffset], $matches);
3140  $seekingTagName = $matches[1] ?? null;
3141  if ($tagName === $seekingTagName) {
3142  // We found a matching tag.
3143  // Add closing tag only if it occurs after the cropped content item.
3144  if ($seekingOffset > $croppedOffset) {
3145  $closingTags[] = $splittedContent[$seekingOffset];
3146  }
3147  break;
3148  }
3149  }
3150  }
3151  }
3152  // Drop the cropped items of the content array. The $closingTags will be added later on again.
3153  array_splice($splittedContent, $croppedOffset + 1);
3154  }
3155  $splittedContent = array_merge($splittedContent, [
3156  $croppedOffset !== null ? $replacementForEllipsis : '',
3157  ], $closingTags);
3158  // Reverse array once again if we are cropping from the end.
3159  if ($chars < 0) {
3160  $splittedContent = array_reverse($splittedContent);
3161  }
3162  return implode('', $splittedContent);
3163  }
3164 
3172  public function calc($val)
3173  {
3174  $parts = GeneralUtility::splitCalc($val, '+-*/');
3175  $value = 0;
3176  foreach ($parts as $part) {
3177  $theVal = $part[1];
3178  $sign = $part[0];
3179  if ((string)(int)$theVal === (string)$theVal) {
3180  $theVal = (int)$theVal;
3181  } else {
3182  $theVal = 0;
3183  }
3184  if ($sign === '-') {
3185  $value -= $theVal;
3186  }
3187  if ($sign === '+') {
3188  $value += $theVal;
3189  }
3190  if ($sign === '/') {
3191  if ((int)$theVal) {
3192  $value /= (int)$theVal;
3193  }
3194  }
3195  if ($sign === '*') {
3196  $value *= $theVal;
3197  }
3198  }
3199  return $value;
3200  }
3201 
3214  public function splitObj($value, $conf)
3215  {
3216  $conf['token'] = isset($conf['token.']) ? $this->stdWrap($conf['token'] ?? '', $conf['token.']) : $conf['token'] ?? '';
3217  if ($conf['token'] === '') {
3218  return $value;
3219  }
3220  $valArr = explode($conf['token'], $value);
3221 
3222  // return value directly by returnKey. No further processing
3223  if (!empty($valArr) && (‪MathUtility::canBeInterpretedAsInteger($conf['returnKey'] ?? null) || ($conf['returnKey.'] ?? false))) {
3224  $key = (int)$this->stdWrapValue('returnKey', $conf ?? []);
3225  return $valArr[$key] ?? '';
3226  }
3227 
3228  // return the amount of elements. No further processing
3229  if (!empty($valArr) && (($conf['returnCount'] ?? false) || ($conf['returnCount.'] ?? false))) {
3230  $returnCount = (bool)$this->stdWrapValue('returnCount', $conf ?? []);
3231  return $returnCount ? count($valArr) : 0;
3232  }
3233 
3234  // calculate splitCount
3235  $splitCount = count($valArr);
3236  $max = (int)$this->stdWrapValue('max', $conf ?? []);
3237  if ($max && $splitCount > $max) {
3238  $splitCount = $max;
3239  }
3240  $min = (int)$this->stdWrapValue('min', $conf ?? []);
3241  if ($min && $splitCount < $min) {
3242  $splitCount = $min;
3243  }
3244  $wrap = (string)$this->stdWrapValue('wrap', $conf ?? []);
3245  $cObjNumSplitConf = isset($conf['cObjNum.']) ? $this->stdWrap($conf['cObjNum'] ?? '', $conf['cObjNum.'] ?? []) : (string)($conf['cObjNum'] ?? '');
3246  $splitArr = [];
3247  if ($wrap !== '' || $cObjNumSplitConf !== '') {
3248  $splitArr['wrap'] = $wrap;
3249  $splitArr['cObjNum'] = $cObjNumSplitConf;
3250  $splitArr = GeneralUtility::makeInstance(TypoScriptService::class)
3251  ->explodeConfigurationForOptionSplit($splitArr, $splitCount);
3252  }
3253  $content = '';
3254  for ($a = 0; $a < $splitCount; $a++) {
3255  $this->getTypoScriptFrontendController()->register['SPLIT_COUNT'] = $a;
3256  $value = '' . $valArr[$a];
3257  $this->data[$this->currentValKey] = $value;
3258  if ($splitArr[$a]['cObjNum'] ?? false) {
3259  $objName = (int)$splitArr[$a]['cObjNum'];
3260  $value = (string)(isset($conf[$objName . '.'])
3261  ? $this->stdWrap($this->cObjGet($conf[$objName . '.'], $objName . '.'), $conf[$objName . '.'])
3262  : '');
3263  }
3264  $wrap = (string)$this->stdWrapValue('wrap', $splitArr[$a] ?? []);
3265  if ($wrap) {
3266  $value = $this->wrap($value, $wrap);
3267  }
3268  $content .= $value;
3269  }
3270  return $content;
3271  }
3272 
3280  protected function replacement($content, array $configuration)
3281  {
3282  // Sorts actions in configuration by numeric index
3283  ksort($configuration, SORT_NUMERIC);
3284  foreach ($configuration as $index => $action) {
3285  // Checks whether we have a valid action and a numeric key ending with a dot ("10.")
3286  if (is_array($action) && substr($index, -1) === '.' && ‪MathUtility::canBeInterpretedAsInteger(substr($index, 0, -1))) {
3287  $content = $this->replacementSingle($content, $action);
3288  }
3289  }
3290  return $content;
3291  }
3292 
3300  protected function replacementSingle($content, array $configuration)
3301  {
3302  if ((isset($configuration['search']) || isset($configuration['search.'])) && (isset($configuration['replace']) || isset($configuration['replace.']))) {
3303  // Gets the strings
3304  $search = (string)$this->stdWrapValue('search', $configuration ?? []);
3305  $replace = (string)$this->stdWrapValue('replace', $configuration, null);
3306 
3307  // Determines whether regular expression shall be used
3308  $useRegularExpression = (bool)$this->stdWrapValue('useRegExp', $configuration, false);
3309 
3310  // Determines whether replace-pattern uses option-split
3311  $useOptionSplitReplace = (bool)$this->stdWrapValue('useOptionSplitReplace', $configuration, false);
3312 
3313  // Performs a replacement by preg_replace()
3314  if ($useRegularExpression) {
3315  // Get separator-character which precedes the string and separates search-string from the modifiers
3316  $separator = $search[0];
3317  $startModifiers = strrpos($search, $separator);
3318  if ($separator !== false && $startModifiers > 0) {
3319  $modifiers = substr($search, $startModifiers + 1);
3320  // remove "e" (eval-modifier), which would otherwise allow to run arbitrary PHP-code
3321  $modifiers = str_replace('e', '', $modifiers);
3322  $search = substr($search, 0, $startModifiers + 1) . $modifiers;
3323  }
3324  if ($useOptionSplitReplace) {
3325  // init for replacement
3326  $splitCount = preg_match_all($search, $content, $matches);
3327  $typoScriptService = GeneralUtility::makeInstance(TypoScriptService::class);
3328  $replaceArray = $typoScriptService->explodeConfigurationForOptionSplit([$replace], $splitCount);
3329  $replaceCount = 0;
3330 
3331  $replaceCallback = static function ($match) use ($replaceArray, $search, &$replaceCount) {
3332  $replaceCount++;
3333  return preg_replace($search, $replaceArray[$replaceCount - 1][0], $match[0]);
3334  };
3335  $content = preg_replace_callback($search, $replaceCallback, $content);
3336  } else {
3337  $content = preg_replace($search, $replace, $content);
3338  }
3339  } elseif ($useOptionSplitReplace) {
3340  // turn search-string into a preg-pattern
3341  $searchPreg = '#' . preg_quote($search, '#') . '#';
3342 
3343  // init for replacement
3344  $splitCount = preg_match_all($searchPreg, $content, $matches);
3345  $typoScriptService = GeneralUtility::makeInstance(TypoScriptService::class);
3346  $replaceArray = $typoScriptService->explodeConfigurationForOptionSplit([$replace], $splitCount);
3347  $replaceCount = 0;
3348 
3349  $replaceCallback = static function () use ($replaceArray, &$replaceCount) {
3350  $replaceCount++;
3351  return $replaceArray[$replaceCount - 1][0];
3352  };
3353  $content = preg_replace_callback($searchPreg, $replaceCallback, $content);
3354  } else {
3355  $content = str_replace($search, $replace, $content);
3356  }
3357  }
3358  return $content;
3359  }
3360 
3369  protected function round($content, array $conf = [])
3370  {
3371  $decimals = (int)$this->stdWrapValue('decimals', $conf, 0);
3372  $type = $this->stdWrapValue('roundType', $conf ?? []);
3373  $floatVal = (float)$content;
3374  switch ($type) {
3375  case 'ceil':
3376  $content = ceil($floatVal);
3377  break;
3378  case 'floor':
3379  $content = floor($floatVal);
3380  break;
3381  case 'round':
3382 
3383  default:
3384  $content = round($floatVal, $decimals);
3385  }
3386  return $content;
3387  }
3388 
3397  public function numberFormat($content, $conf)
3398  {
3399  $decimals = (int)$this->stdWrapValue('decimals', $conf, 0);
3400  $dec_point = (string)$this->stdWrapValue('dec_point', $conf, '.');
3401  $thousands_sep = (string)$this->stdWrapValue('thousands_sep', $conf, ',');
3402  return number_format((float)$content, $decimals, $dec_point, $thousands_sep);
3403  }
3404 
3424  public function parseFunc($theValue, $conf, $ref = '')
3425  {
3426  // Fetch / merge reference, if any
3427  if ($ref) {
3428  $temp_conf = [
3429  'parseFunc' => $ref,
3430  'parseFunc.' => $conf,
3431  ];
3432  $temp_conf = $this->mergeTSRef($temp_conf, 'parseFunc');
3433  $conf = $temp_conf['parseFunc.'];
3434  }
3435  // early return, no processing in case no configuration is given
3436  if (empty($conf)) {
3437  // @deprecated Invoking ContentObjectRenderer::parseFunc without any configuration will trigger an exception in TYPO3 v12.0
3438  trigger_error('Invoking ContentObjectRenderer::parseFunc without any configuration will trigger an exception in TYPO3 v12.0', E_USER_DEPRECATED);
3439  return $theValue;
3440  }
3441  // Handle HTML sanitizer invocation
3442  if (!isset($conf['htmlSanitize'])) {
3443  // @deprecated Property htmlSanitize was not defined, but will be mandatory in TYPO3 v12.0
3444  trigger_error('Property htmlSanitize was not defined, but will be mandatory in TYPO3 v12.0', E_USER_DEPRECATED);
3445  $features = GeneralUtility::makeInstance(Features::class);
3446  $conf['htmlSanitize'] = $features->isFeatureEnabled('security.frontend.htmlSanitizeParseFuncDefault');
3447  }
3448  $conf['htmlSanitize'] = (bool)$conf['htmlSanitize'];
3449 
3450  // Process:
3451  if ((string)($conf['externalBlocks'] ?? '') === '') {
3452  $result = $this->_parseFunc($theValue, $conf);
3453  if ($conf['htmlSanitize']) {
3454  $result = $this->stdWrap_htmlSanitize($result, $conf['htmlSanitize.'] ?? []);
3455  }
3456  return $result;
3457  }
3458  $tags = strtolower(implode(',', ‪GeneralUtility::trimExplode(',', $conf['externalBlocks'])));
3459  $htmlParser = GeneralUtility::makeInstance(HtmlParser::class);
3460  $parts = $htmlParser->splitIntoBlock($tags, $theValue);
3461  foreach ($parts as $k => $v) {
3462  if ($k % 2) {
3463  // font:
3464  $tagName = strtolower($htmlParser->getFirstTagName($v));
3465  $cfg = $conf['externalBlocks.'][$tagName . '.'] ?? [];
3466  if ($cfg === []) {
3467  continue;
3468  }
3469  if (($cfg['stripNLprev'] ?? false) || ($cfg['stripNL'] ?? false)) {
3470  $parts[$k - 1] = preg_replace('/' . CR . '?' . LF . '[ ]*$/', '', $parts[$k - 1]);
3471  }
3472  if (($cfg['stripNLnext'] ?? false) || ($cfg['stripNL'] ?? false)) {
3473  if (!isset($parts[$k + 1])) {
3474  $parts[$k + 1] = '';
3475  }
3476  $parts[$k + 1] = preg_replace('/^[ ]*' . CR . '?' . LF . '/', '', $parts[$k + 1]);
3477  }
3478  }
3479  }
3480  foreach ($parts as $k => $v) {
3481  if ($k % 2) {
3482  $tag = $htmlParser->getFirstTag($v);
3483  $tagName = strtolower($htmlParser->getFirstTagName($v));
3484  $cfg = $conf['externalBlocks.'][$tagName . '.'] ?? [];
3485  if ($cfg === []) {
3486  continue;
3487  }
3488  if ($cfg['callRecursive'] ?? false) {
3489  $parts[$k] = $this->parseFunc($htmlParser->removeFirstAndLastTag($v), $conf);
3490  if (!($cfg['callRecursive.']['dontWrapSelf'] ?? false)) {
3491  if ($cfg['callRecursive.']['alternativeWrap'] ?? false) {
3492  $parts[$k] = $this->wrap($parts[$k], $cfg['callRecursive.']['alternativeWrap']);
3493  } else {
3494  if (is_array($cfg['callRecursive.']['tagStdWrap.'] ?? false)) {
3495  $tag = $this->stdWrap($tag, $cfg['callRecursive.']['tagStdWrap.']);
3496  }
3497  $parts[$k] = $tag . $parts[$k] . '</' . $tagName . '>';
3498  }
3499  }
3500  } elseif ($cfg['HTMLtableCells'] ?? false) {
3501  $rowParts = $htmlParser->splitIntoBlock('tr', $parts[$k]);
3502  foreach ($rowParts as $kk => $vv) {
3503  if ($kk % 2) {
3504  $colParts = $htmlParser->splitIntoBlock('td,th', $vv);
3505  $cc = 0;
3506  foreach ($colParts as $kkk => $vvv) {
3507  if ($kkk % 2) {
3508  $cc++;
3509  $tag = $htmlParser->getFirstTag($vvv);
3510  $tagName = strtolower($htmlParser->getFirstTagName($vvv));
3511  $colParts[$kkk] = $htmlParser->removeFirstAndLastTag($vvv);
3512  if (($cfg['HTMLtableCells.'][$cc . '.']['callRecursive'] ?? false)
3513  || (!isset($cfg['HTMLtableCells.'][$cc . '.']['callRecursive']) && ($cfg['HTMLtableCells.']['default.']['callRecursive'] ?? false))) {
3514  if ($cfg['HTMLtableCells.']['addChr10BetweenParagraphs'] ?? false) {
3515  $colParts[$kkk] = str_replace(
3516  '</p><p>',
3517  '</p>' . LF . '<p>',
3518  $colParts[$kkk]
3519  );
3520  }
3521  $colParts[$kkk] = $this->parseFunc($colParts[$kkk], $conf);
3522  }
3523  $tagStdWrap = is_array($cfg['HTMLtableCells.'][$cc . '.']['tagStdWrap.'] ?? false)
3524  ? $cfg['HTMLtableCells.'][$cc . '.']['tagStdWrap.']
3525  : ($cfg['HTMLtableCells.']['default.']['tagStdWrap.'] ?? null);
3526  if (is_array($tagStdWrap)) {
3527  $tag = $this->stdWrap($tag, $tagStdWrap);
3528  }
3529  $stdWrap = is_array($cfg['HTMLtableCells.'][$cc . '.']['stdWrap.'] ?? false)
3530  ? $cfg['HTMLtableCells.'][$cc . '.']['stdWrap.']
3531  : ($cfg['HTMLtableCells.']['default.']['stdWrap.'] ?? null);
3532  if (is_array($stdWrap)) {
3533  $colParts[$kkk] = $this->stdWrap($colParts[$kkk], $stdWrap);
3534  }
3535  $colParts[$kkk] = $tag . $colParts[$kkk] . '</' . $tagName . '>';
3536  }
3537  }
3538  $rowParts[$kk] = implode('', $colParts);
3539  }
3540  }
3541  $parts[$k] = implode('', $rowParts);
3542  }
3543  if (is_array($cfg['stdWrap.'] ?? false)) {
3544  $parts[$k] = $this->stdWrap($parts[$k], $cfg['stdWrap.']);
3545  }
3546  } else {
3547  $parts[$k] = $this->_parseFunc($parts[$k], $conf);
3548  }
3549  }
3550  $result = implode('', $parts);
3551  if ($conf['htmlSanitize']) {
3552  $result = $this->stdWrap_htmlSanitize($result, $conf['htmlSanitize.'] ?? []);
3553  }
3554  return $result;
3555  }
3556 
3566  public function _parseFunc($theValue, $conf)
3567  {
3568  if (!empty($conf['if.']) && !$this->checkIf($conf['if.'])) {
3569  return $theValue;
3570  }
3571  // Indicates that the data is from within a tag.
3572  $inside = false;
3573  // Pointer to the total string position
3574  $pointer = 0;
3575  // Loaded with the current typo-tag if any.
3576  $currentTag = null;
3577  $stripNL = 0;
3578  $contentAccum = [];
3579  $contentAccumP = 0;
3580  $allowTags = strtolower(str_replace(' ', '', $conf['allowTags'] ?? ''));
3581  $denyTags = strtolower(str_replace(' ', '', $conf['denyTags'] ?? ''));
3582  $totalLen = strlen($theValue);
3583  do {
3584  if (!$inside) {
3585  if ($currentTag === null) {
3586  // These operations should only be performed on code outside the typotags...
3587  // data: this checks that we enter tags ONLY if the first char in the tag is alphanumeric OR '/'
3588  $len_p = 0;
3589  $c = 100;
3590  do {
3591  $len = strcspn(substr($theValue, $pointer + $len_p), '<');
3592  $len_p += $len + 1;
3593  $endChar = ord(strtolower(substr($theValue, $pointer + $len_p, 1)));
3594  $c--;
3595  } while ($c > 0 && $endChar && ($endChar < 97 || $endChar > 122) && $endChar != 47);
3596  $len = $len_p - 1;
3597  } else {
3598  $len = $this->getContentLengthOfCurrentTag($theValue, $pointer, (string)$currentTag[0]);
3599  }
3600  // $data is the content until the next <tag-start or end is detected.
3601  // In case of a currentTag set, this would mean all data between the start- and end-tags
3602  $data = substr($theValue, $pointer, $len);
3603  if ($data !== false) {
3604  if ($stripNL) {
3605  // If the previous tag was set to strip NewLines in the beginning of the next data-chunk.
3606  $data = preg_replace('/^[ ]*' . CR . '?' . LF . '/', '', $data);
3607  if ($data === null) {
3608  $this->logger->debug('Stripping new lines failed for "{data}"', ['data' => $data]);
3609  $data = '';
3610  }
3611  }
3612  // These operations should only be performed on code outside the tags...
3613  if (!is_array($currentTag)) {
3614  // Constants
3615  $tsfe = $this->getTypoScriptFrontendController();
3616  $tmpConstants = $tsfe->tmpl->setup['constants.'] ?? null;
3617  if (!empty($conf['constants']) && is_array($tmpConstants)) {
3618  foreach ($tmpConstants as $key => $val) {
3619  if (is_string($val)) {
3620  $data = str_replace('###' . $key . '###', $val, $data);
3621  }
3622  }
3623  }
3624  // Short
3625  if (isset($conf['short.']) && is_array($conf['short.'])) {
3626  $shortWords = $conf['short.'];
3627  krsort($shortWords);
3628  foreach ($shortWords as $key => $val) {
3629  if (is_string($val)) {
3630  $data = str_replace($key, $val, $data);
3631  }
3632  }
3633  }
3634  // stdWrap
3635  if (isset($conf['plainTextStdWrap.']) && is_array($conf['plainTextStdWrap.'])) {
3636  $data = $this->stdWrap($data, $conf['plainTextStdWrap.']);
3637  }
3638  // userFunc
3639  if ($conf['userFunc'] ?? false) {
3640  $data = $this->callUserFunction($conf['userFunc'], $conf['userFunc.'] ?? [], $data);
3641  }
3642  // Search Words:
3643  // @deprecated since TYPO3 v11, will be removed in TYPO3 v12.0.
3644  if (($tsfe->no_cache ?? false) && ($conf['sword'] ?? false) && is_array($tsfe->sWordList) && $tsfe->sWordRegEx) {
3645  if ($conf['sword'] !== '<span class="ce-sword">|</span>') {
3646  trigger_error('Enabling lib.parseFunc.sword will stop working in TYPO3 v12.0. Consider creating your own parser logic in a custom extension (which ideally also works with active caching.', E_USER_DEPRECATED);
3647  }
3648  $newstring = '';
3649  do {
3650  $pregSplitMode = 'i';
3651  // @deprecated
3652  // @todo: ensure these options are removed from the TypoScript reference in TYPO3 v12.0.
3653  if (isset($tsfe->config['config']['sword_noMixedCase']) && !empty($tsfe->config['config']['sword_noMixedCase'])) {
3654  $pregSplitMode = '';
3655  }
3656  $pieces = preg_split('/' . $tsfe->sWordRegEx . '/' . $pregSplitMode, $data, 2);
3657  $newstring .= $pieces[0];
3658  $match_len = strlen($data) - (strlen($pieces[0]) + strlen($pieces[1]));
3659  $inTag = false;
3660  if (str_contains($pieces[0], '<') || str_contains($pieces[0], '>')) {
3661  // Returns TRUE, if a '<' is closer to the string-end than '>'.
3662  // This is the case if we're INSIDE a tag (that could have been
3663  // made by makelinks...) and we must secure, that the inside of a tag is
3664  // not marked up.
3665  $inTag = strrpos($pieces[0], '<') > strrpos($pieces[0], '>');
3666  }
3667  // The searchword:
3668  $match = substr($data, strlen($pieces[0]), $match_len);
3669  if (trim($match) && strlen($match) > 1 && !$inTag) {
3670  $match = $this->wrap($match, $conf['sword'] ?? '');
3671  }
3672  // Concatenate the Search Word again.
3673  $newstring .= $match;
3674  $data = $pieces[1];
3675  } while ($pieces[1]);
3676  $data = $newstring;
3677  }
3678  }
3679  // Search for tags to process in current data and
3680  // call this method recursively if found
3681  if (str_contains($data, '<') && isset($conf['tags.']) && is_array($conf['tags.'])) {
3682  foreach ($conf['tags.'] as $tag => $tagConfig) {
3683  // only match tag `a` in `<a href"...">` but not in `<abbr>`
3684  if (preg_match('#<' . $tag . '[\s/>]#', $data)) {
3685  $data = $this->_parseFunc($data, $conf);
3686  break;
3687  }
3688  }
3689  }
3690  if (!is_array($currentTag) && ($conf['makelinks'] ?? false)) {
3691  $data = $this->http_makelinks($data, $conf['makelinks.']['http.'] ?? []);
3692  $data = $this->mailto_makelinks($data, $conf['makelinks.']['mailto.'] ?? []);
3693  }
3694  $contentAccum[$contentAccumP] = isset($contentAccum[$contentAccumP])
3695  ? $contentAccum[$contentAccumP] . $data
3696  : $data;
3697  }
3698  $inside = true;
3699  } else {
3700  // tags
3701  $len = strcspn(substr($theValue, $pointer), '>') + 1;
3702  $data = substr($theValue, $pointer, $len);
3703  if (str_ends_with($data, '/>') && strpos($data, '<link ') !== 0) {
3704  $tagContent = substr($data, 1, -2);
3705  } else {
3706  $tagContent = substr($data, 1, -1);
3707  }
3708  $tag = explode(' ', trim($tagContent), 2);
3709  $tag[0] = strtolower($tag[0]);
3710  // end tag like </li>
3711  if (str_starts_with($tag[0], '/')) {
3712  $tag[0] = substr($tag[0], 1);
3713  $tag['out'] = 1;
3714  }
3715  if ($conf['tags.'][$tag[0]] ?? false) {
3716  $treated = false;
3717  $stripNL = false;
3718  // in-tag
3719  if (!$currentTag && (!isset($tag['out']) || !$tag['out'])) {
3720  // $currentTag (array!) is the tag we are currently processing
3721  $currentTag = $tag;
3722  $contentAccumP++;
3723  $treated = true;
3724  // in-out-tag: img and other empty tags
3725  if (preg_match('/^(area|base|br|col|hr|img|input|meta|param)$/i', (string)$tag[0])) {
3726  $tag['out'] = 1;
3727  }
3728  }
3729  // out-tag
3730  if (isset($currentTag[0], $tag['out']) && $currentTag[0] === $tag[0] && $tag['out']) {
3731  $theName = $conf['tags.'][$tag[0]];
3732  $theConf = $conf['tags.'][$tag[0] . '.'];
3733  // This flag indicates, that NL- (13-10-chars) should be stripped first and last.
3734  $stripNL = (bool)($theConf['stripNL'] ?? false);
3735  // This flag indicates, that this TypoTag section should NOT be included in the nonTypoTag content.
3736  $breakOut = (bool)($theConf['breakoutTypoTagContent'] ?? false);
3737  $this->parameters = [];
3738  if (isset($currentTag[1])) {
3739  // decode HTML entities in attributes, since they're processed
3740  $params = GeneralUtility::get_tag_attributes((string)$currentTag[1], true);
3741  if (is_array($params)) {
3742  foreach ($params as $option => $val) {
3743  // contains non-encoded values
3744  $this->parameters[strtolower($option)] = $val;
3745  }
3746  }
3747  $this->parameters['allParams'] = trim((string)$currentTag[1]);
3748  }
3749  // Removes NL in the beginning and end of the tag-content AND at the end of the currentTagBuffer.
3750  // $stripNL depends on the configuration of the current tag
3751  if ($stripNL) {
3752  $contentAccum[$contentAccumP - 1] = preg_replace('/' . CR . '?' . LF . '[ ]*$/', '', $contentAccum[$contentAccumP - 1] ?? '');
3753  $contentAccum[$contentAccumP] = preg_replace('/^[ ]*' . CR . '?' . LF . '/', '', $contentAccum[$contentAccumP] ?? '');
3754  $contentAccum[$contentAccumP] = preg_replace('/' . CR . '?' . LF . '[ ]*$/', '', $contentAccum[$contentAccumP] ?? '');
3755  }
3756  $this->data[$this->currentValKey] = $contentAccum[$contentAccumP] ?? null;
3757  $newInput = $this->cObjGetSingle($theName, $theConf, '/parseFunc/.tags.' . $tag[0]);
3758  // fetch the content object
3759  $contentAccum[$contentAccumP] = $newInput;
3760  $contentAccumP++;
3761  // If the TypoTag section
3762  if (!$breakOut) {
3763  if (!isset($contentAccum[$contentAccumP - 2])) {
3764  $contentAccum[$contentAccumP - 2] = '';
3765  }
3766  $contentAccum[$contentAccumP - 2] .= ($contentAccum[$contentAccumP - 1] ?? '') . ($contentAccum[$contentAccumP] ?? '');
3767  unset($contentAccum[$contentAccumP]);
3768  unset($contentAccum[$contentAccumP - 1]);
3769  $contentAccumP -= 2;
3770  }
3771  $currentTag = null;
3772  $treated = true;
3773  }
3774  // other tags
3775  if (!$treated) {
3776  $contentAccum[$contentAccumP] .= $data;
3777  }
3778  } else {
3779  // If a tag was not a typo tag, then it is just added to the content
3780  $stripNL = false;
3781  if (GeneralUtility::inList($allowTags, (string)$tag[0]) ||
3782  ($denyTags !== '*' && !GeneralUtility::inList($denyTags, (string)$tag[0]))) {
3783  $contentAccum[$contentAccumP] = isset($contentAccum[$contentAccumP])
3784  ? $contentAccum[$contentAccumP] . $data
3785  : $data;
3786  } else {
3787  $contentAccum[$contentAccumP] = isset($contentAccum[$contentAccumP])
3788  ? $contentAccum[$contentAccumP] . htmlspecialchars($data)
3789  : htmlspecialchars($data);
3790  }
3791  }
3792  $inside = false;
3793  }
3794  $pointer += $len;
3795  } while ($pointer < $totalLen);
3796  // Parsing nonTypoTag content (all even keys):
3797  reset($contentAccum);
3798  $contentAccumCount = count($contentAccum);
3799  for ($a = 0; $a < $contentAccumCount; $a++) {
3800  if ($a % 2 != 1) {
3801  // stdWrap
3802  if (isset($conf['nonTypoTagStdWrap.']) && is_array($conf['nonTypoTagStdWrap.'])) {
3803  $contentAccum[$a] = $this->stdWrap((string)($contentAccum[$a] ?? ''), $conf['nonTypoTagStdWrap.']);
3804  }
3805  // userFunc
3806  if (!empty($conf['nonTypoTagUserFunc'])) {
3807  $contentAccum[$a] = $this->callUserFunction($conf['nonTypoTagUserFunc'], $conf['nonTypoTagUserFunc.'] ?? [], (string)($contentAccum[$a] ?? ''));
3808  }
3809  }
3810  }
3811  return implode('', $contentAccum);
3812  }
3813 
3822  public function encaps_lineSplit($theValue, $conf)
3823  {
3824  if ((string)$theValue === '') {
3825  return '';
3826  }
3827  $lParts = explode(LF, $theValue);
3828 
3829  // When the last element is an empty linebreak we need to remove it, otherwise we will have a duplicate empty line.
3830  $lastPartIndex = count($lParts) - 1;
3831  if ($lParts[$lastPartIndex] === '' && trim($lParts[$lastPartIndex - 1], CR) === '') {
3832  array_pop($lParts);
3833  }
3834 
3835  $encapTags = ‪GeneralUtility::trimExplode(',', strtolower($conf['encapsTagList'] ?? ''), true);
3836  $defaultAlign = trim((string)$this->stdWrapValue('defaultAlign', $conf ?? []));
3837 
3838  $str_content = '';
3839  foreach ($lParts as $k => $l) {
3840  $sameBeginEnd = false;
3841  $emptyTag = false;
3842  $l = trim($l);
3843  $attrib = [];
3844  $nonWrapped = false;
3845  $tagName = '';
3846  if (isset($l[0]) && $l[0] === '<' && substr($l, -1) === '>') {
3847  $fwParts = explode('>', substr($l, 1), 2);
3848  [$tagName] = explode(' ', $fwParts[0], 2);
3849  if (!$fwParts[1]) {
3850  if (substr($tagName, -1) === '/') {
3851  $tagName = substr($tagName, 0, -1);
3852  }
3853  if (substr($fwParts[0], -1) === '/') {
3854  $sameBeginEnd = true;
3855  $emptyTag = true;
3856  // decode HTML entities, they're encoded later again
3857  $attrib = GeneralUtility::get_tag_attributes('<' . substr($fwParts[0], 0, -1) . '>', true);
3858  }
3859  } else {
3860  $backParts = ‪GeneralUtility::revExplode('<', substr($fwParts[1], 0, -1), 2);
3861  // decode HTML entities, they're encoded later again
3862  $attrib = GeneralUtility::get_tag_attributes('<' . $fwParts[0] . '>', true);
3863  $str_content = $backParts[0];
3864  // Ensure that $backParts could be exploded into 2 items
3865  if (isset($backParts[1])) {
3866  $sameBeginEnd = strtolower(substr($backParts[1], 1, strlen($tagName))) === strtolower($tagName);
3867  }
3868  }
3869  }
3870  if ($sameBeginEnd && in_array(strtolower($tagName), $encapTags)) {
3871  $uTagName = strtoupper($tagName);
3872  $uTagName = strtoupper($conf['remapTag.'][$uTagName] ?? $uTagName);
3873  } else {
3874  $uTagName = strtoupper($conf['nonWrappedTag'] ?? '');
3875  // The line will be wrapped: $uTagName should not be an empty tag
3876  $emptyTag = false;
3877  $str_content = $lParts[$k];
3878  $nonWrapped = true;
3879  $attrib = [];
3880  }
3881  // Wrapping all inner-content:
3882  if (is_array($conf['innerStdWrap_all.'] ?? null)) {
3883  $str_content = (string)$this->stdWrap($str_content, $conf['innerStdWrap_all.']);
3884  }
3885  if ($uTagName) {
3886  // Setting common attributes
3887  if (isset($conf['addAttributes.'][$uTagName . '.']) && is_array($conf['addAttributes.'][$uTagName . '.'])) {
3888  foreach ($conf['addAttributes.'][$uTagName . '.'] as $kk => $vv) {
3889  if (!is_array($vv)) {
3890  if ((string)($conf['addAttributes.'][$uTagName . '.'][$kk . '.']['setOnly'] ?? '') === 'blank') {
3891  if ((string)($attrib[$kk] ?? '') === '') {
3892  $attrib[$kk] = $vv;
3893  }
3894  } elseif ((string)($conf['addAttributes.'][$uTagName . '.'][$kk . '.']['setOnly'] ?? '') === 'exists') {
3895  if (!isset($attrib[$kk])) {
3896  $attrib[$kk] = $vv;
3897  }
3898  } else {
3899  $attrib[$kk] = $vv;
3900  }
3901  }
3902  }
3903  }
3904  // Wrapping all inner-content:
3905  if (isset($conf['encapsLinesStdWrap.'][$uTagName . '.']) && is_array($conf['encapsLinesStdWrap.'][$uTagName . '.'])) {
3906  $str_content = (string)$this->stdWrap($str_content, $conf['encapsLinesStdWrap.'][$uTagName . '.']);
3907  }
3908  // Default align
3909  if ((!isset($attrib['align']) || !$attrib['align']) && $defaultAlign) {
3910  $attrib['align'] = $defaultAlign;
3911  }
3912  // implode (insecure) attributes, that's why `htmlspecialchars` is used here
3913  $params = GeneralUtility::implodeAttributes($attrib, true);
3914  if (!isset($conf['removeWrapping']) || !$conf['removeWrapping'] || ($emptyTag && $conf['removeWrapping.']['keepSingleTag'])) {
3915  $selfClosingTagList = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
3916  if ($emptyTag && in_array(strtolower($uTagName), $selfClosingTagList, true)) {
3917  $str_content = '<' . strtolower($uTagName) . (trim($params) ? ' ' . trim($params) : '') . ' />';
3918  } else {
3919  $str_content = '<' . strtolower($uTagName) . (trim($params) ? ' ' . trim($params) : '') . '>' . $str_content . '</' . strtolower($uTagName) . '>';
3920  }
3921  }
3922  }
3923  if ($nonWrapped && isset($conf['wrapNonWrappedLines']) && $conf['wrapNonWrappedLines']) {
3924  $str_content = $this->wrap($str_content, $conf['wrapNonWrappedLines']);
3925  }
3926  $lParts[$k] = $str_content;
3927  }
3928  return implode(LF, $lParts);
3929  }
3930 
3941  public function http_makelinks($data, $conf)
3942  {
3943  $parts = [];
3944  foreach (['http://', 'https://'] as $scheme) {
3945  $textpieces = explode($scheme, $data);
3946  $pieces = count($textpieces);
3947  $textstr = $textpieces[0];
3948  for ($i = 1; $i < $pieces; $i++) {
3949  $len = strcspn($textpieces[$i], chr(32) . "\t" . CRLF);
3950  if (!(trim(substr($textstr, -1)) === '' && $len)) {
3951  $textstr .= $scheme . $textpieces[$i];
3952  continue;
3953  }
3954  $lastChar = substr($textpieces[$i], $len - 1, 1);
3955  if (!preg_match('/[A-Za-z0-9\\/#_-]/', $lastChar)) {
3956  $len--;
3957  }
3958  // Included '\/' 3/12
3959  $parts[0] = substr($textpieces[$i], 0, $len);
3960  $parts[1] = substr($textpieces[$i], $len);
3961  $keep = $conf['keep'] ?? '';
3962  $linkParts = parse_url($scheme . $parts[0]);
3963  // Check if link couldn't be parsed properly
3964  if (!is_array($linkParts)) {
3965  $textstr .= $scheme . $textpieces[$i];
3966  continue;
3967  }
3968  $linktxt = '';
3969  if (str_contains($keep, 'scheme')) {
3970  $linktxt = $scheme;
3971  }
3972  $linktxt .= $linkParts['host'] ?? '';
3973  if (str_contains($keep, 'path')) {
3974  $linktxt .= ($linkParts['path'] ?? '');
3975  // Added $linkParts['query'] 3/12
3976  if (str_contains($keep, 'query') && $linkParts['query']) {
3977  $linktxt .= '?' . $linkParts['query'];
3978  } elseif (($linkParts['path'] ?? '') === '/') {
3979  $linktxt = substr($linktxt, 0, -1);
3980  }
3981  }
3982  $typolinkConfiguration = $conf;
3983  $typolinkConfiguration['parameter'] = $scheme . $parts[0];
3984  $textstr .= $this->typoLink($linktxt, $typolinkConfiguration) . $parts[1];
3985  }
3986  $data = $textstr;
3987  }
3988  return $textstr;
3989  }
3990 
4000  public function mailto_makelinks($data, $conf)
4001  {
4002  $conf = (array)$conf;
4003  $parts = [];
4004  // split by mailto logic
4005  $textpieces = explode('mailto:', $data);
4006  $pieces = count($textpieces);
4007  $textstr = $textpieces[0];
4008  for ($i = 1; $i < $pieces; $i++) {
4009  $len = strcspn($textpieces[$i], chr(32) . "\t" . CRLF);
4010  if (trim(substr($textstr, -1)) === '' && $len) {
4011  $lastChar = substr($textpieces[$i], $len - 1, 1);
4012  if (!preg_match('/[A-Za-z0-9]/', $lastChar)) {
4013  $len--;
4014  }
4015  $parts[0] = substr($textpieces[$i], 0, $len);
4016  $parts[1] = substr($textpieces[$i], $len);
4017  $linktxt = (string)preg_replace('/\\?.*/', '', $parts[0]);
4018  $typolinkConfiguration = $conf;
4019  $typolinkConfiguration['parameter'] = 'mailto:' . $parts[0];
4020  $textstr .= $this->typoLink($linktxt, $typolinkConfiguration) . $parts[1];
4021  } else {
4022  $textstr .= 'mailto:' . $textpieces[$i];
4023  }
4024  }
4025  return $textstr;
4026  }
4027 
4053  public function getImgResource($file, $fileArray)
4054  {
4055  $importedFile = null;
4056  $fileReference = null;
4057  if (empty($file) && empty($fileArray)) {
4058  return null;
4059  }
4060  if (!is_array($fileArray)) {
4061  $fileArray = (array)$fileArray;
4062  }
4063  $imageResource = null;
4064  if ($file === 'GIFBUILDER') {
4065  $gifCreator = GeneralUtility::makeInstance(GifBuilder::class);
4066  $theImage = '';
4067  if (‪$GLOBALS['TYPO3_CONF_VARS']['GFX']['gdlib']) {
4068  $gifCreator->start($fileArray, $this->data);
4069  $theImage = $gifCreator->gifBuild();
4070  }
4071  $imageResource = $gifCreator->getImageDimensions($theImage);
4072  $imageResource['origFile'] = $theImage;
4073  } else {
4074  if ($file instanceof File) {
4075  $fileObject = $file;
4076  } elseif ($file instanceof FileReference) {
4077  $fileReference = $file;
4078  $fileObject = $file->getOriginalFile();
4079  } else {
4080  try {
4081  if (isset($fileArray['import.']) && $fileArray['import.']) {
4082  $importedFile = trim((string)$this->stdWrap('', $fileArray['import.']));
4083  if (!empty($importedFile)) {
4084  $file = $importedFile;
4085  }
4086  }
4087 
4089  $treatIdAsReference = $this->stdWrapValue('treatIdAsReference', $fileArray ?? []);
4090  if (!empty($treatIdAsReference)) {
4091  $fileReference = $this->getResourceFactory()->getFileReferenceObject($file);
4092  $fileObject = $fileReference->getOriginalFile();
4093  } else {
4094  $fileObject = $this->getResourceFactory()->getFileObject($file);
4095  }
4096  } elseif (preg_match('/^(0|[1-9][0-9]*):/', $file)) { // combined identifier
4097  $fileObject = $this->getResourceFactory()->retrieveFileOrFolderObject($file);
4098  } else {
4099  if ($importedFile && !empty($fileArray['import'])) {
4100  $file = $fileArray['import'] . $file;
4101  }
4102  $fileObject = $this->getResourceFactory()->retrieveFileOrFolderObject($file);
4103  }
4104  } catch (Exception $exception) {
4105  $this->logger->warning('The image "{file}" could not be found and won\'t be included in frontend output', [
4106  'file' => $file,
4107  'exception' => $exception,
4108  ]);
4109  return null;
4110  }
4111  }
4112  if ($fileObject instanceof File) {
4113  $processingConfiguration = [];
4114  $processingConfiguration['width'] = $this->stdWrapValue('width', $fileArray ?? []);
4115  $processingConfiguration['height'] = $this->stdWrapValue('height', $fileArray ?? []);
4116  $processingConfiguration['fileExtension'] = $this->stdWrapValue('ext', $fileArray ?? []);
4117  $processingConfiguration['maxWidth'] = (int)$this->stdWrapValue('maxW', $fileArray ?? []);
4118  $processingConfiguration['maxHeight'] = (int)$this->stdWrapValue('maxH', $fileArray ?? []);
4119  $processingConfiguration['minWidth'] = (int)$this->stdWrapValue('minW', $fileArray ?? []);
4120  $processingConfiguration['minHeight'] = (int)$this->stdWrapValue('minH', $fileArray ?? []);
4121  $processingConfiguration['noScale'] = $this->stdWrapValue('noScale', $fileArray ?? []);
4122  $processingConfiguration['additionalParameters'] = $this->stdWrapValue('params', $fileArray ?? []);
4123  $processingConfiguration['frame'] = (int)$this->stdWrapValue('frame', $fileArray ?? []);
4124  if ($fileReference instanceof FileReference) {
4125  $processingConfiguration['crop'] = $this->getCropAreaFromFileReference($fileReference, $fileArray);
4126  } else {
4127  $processingConfiguration['crop'] = $this->getCropAreaFromFromTypoScriptSettings($fileObject, $fileArray);
4128  }
4129 
4130  // Possibility to cancel/force profile extraction
4131  // see $GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_stripColorProfileParameters']
4132  if (isset($fileArray['stripProfile'])) {
4133  $processingConfiguration['stripProfile'] = $fileArray['stripProfile'];
4134  }
4135  // Check if we can handle this type of file for editing
4136  if ($fileObject->isImage()) {
4137  $maskArray = $fileArray['m.'] ?? false;
4138  // Must render mask images and include in hash-calculating
4139  // - otherwise we cannot be sure the filename is unique for the setup!
4140  if (is_array($maskArray)) {
4141  $processingConfiguration['maskImages']['maskImage'] = $this->getImgResource($maskArray['mask'] ?? '', $maskArray['mask.'] ?? [])['processedFile'] ?? null;
4142  $processingConfiguration['maskImages']['backgroundImage'] = $this->getImgResource($maskArray['bgImg'] ?? '', $maskArray['bgImg.'] ?? [])['processedFile'] ?? null;
4143  $processingConfiguration['maskImages']['maskBottomImage'] = $this->getImgResource($maskArray['bottomImg'] ?? '', $maskArray['bottomImg.'] ?? [])['processedFile'] ?? null;
4144  $processingConfiguration['maskImages']['maskBottomImageMask'] = $this->getImgResource($maskArray['bottomImg_mask'] ?? '', $maskArray['bottomImg_mask.'] ?? [])['processedFile'] ?? null;
4145  }
4146  $processedFileObject = $fileObject->process(‪ProcessedFile::CONTEXT_IMAGECROPSCALEMASK, $processingConfiguration);
4147  if ($processedFileObject->isProcessed()) {
4148  $imageResource = [
4149  0 => (int)$processedFileObject->getProperty('width'),
4150  1 => (int)$processedFileObject->getProperty('height'),
4151  2 => $processedFileObject->getExtension(),
4152  3 => $processedFileObject->getPublicUrl(),
4153  'origFile' => $fileObject->getPublicUrl(),
4154  'origFile_mtime' => $fileObject->getModificationTime(),
4155  // This is needed by \TYPO3\CMS\Frontend\Imaging\GifBuilder,
4156  // in order for the setup-array to create a unique filename hash.
4157  'originalFile' => $fileObject,
4158  'processedFile' => $processedFileObject,
4159  ];
4160  }
4161  }
4162  }
4163  }
4164  // Triggered when the resolved file object isn't considered as image, processing failed and likely other scenarios
4165  // This code path dates back to pre FAL times and should be deprecated and removed eventually
4166  if (!isset($imageResource) && is_string($file)) {
4167  try {
4168  $theImage = GeneralUtility::makeInstance(FilePathSanitizer::class)->sanitize($file);
4169  $info = GeneralUtility::makeInstance(GifBuilder::class)->imageMagickConvert($theImage, 'WEB');
4170  if ($info !== null) {
4171  $info['origFile'] = $theImage;
4172  // This is needed by \TYPO3\CMS\Frontend\Imaging\GifBuilder, ln 100ff in order for the setup-array to create a unique filename hash.
4173  $info['origFile_mtime'] = @filemtime($theImage);
4174  $imageResource = $info;
4175  }
4176  } catch (Exception $e) {
4177  // do nothing in case the file path is invalid
4178  }
4179  }
4180  // Hook 'getImgResource': Post-processing of image resources
4181  if (isset($imageResource)) {
4183  foreach ($this->getGetImgResourceHookObjects() as $hookObject) {
4184  $imageResource = $hookObject->getImgResourcePostProcess($file, (array)$fileArray, $imageResource, $this);
4185  }
4186  }
4187  return $imageResource;
4188  }
4189 
4207  protected function getCropAreaFromFileReference(FileReference $fileReference, array $fileArray)
4208  {
4209  // Use cropping area from file reference if nothing is configured in TypoScript.
4210  if (!isset($fileArray['crop']) && !isset($fileArray['crop.'])) {
4211  // Set crop variant from TypoScript settings. If not set, use default.
4212  $cropVariant = $fileArray['cropVariant'] ?? 'default';
4213  $fileCropArea = $this->createCropAreaFromJsonString((string)$fileReference->getProperty('crop'), $cropVariant);
4214  return $fileCropArea->isEmpty() ? null : $fileCropArea->makeAbsoluteBasedOnFile($fileReference);
4215  }
4216 
4217  return $this->getCropAreaFromFromTypoScriptSettings($fileReference, $fileArray);
4218  }
4219 
4228  protected function getCropAreaFromFromTypoScriptSettings(FileInterface $file, array $fileArray)
4229  {
4231  $cropArea = null;
4232  // Resolve TypoScript configured cropping.
4233  $cropSettings = isset($fileArray['crop.'])
4234  ? $this->stdWrap($fileArray['crop'] ?? '', $fileArray['crop.'])
4235  : ($fileArray['crop'] ?? null);
4236 
4237  if (is_string($cropSettings)) {
4238  // Set crop variant from TypoScript settings. If not set, use default.
4239  $cropVariant = $fileArray['cropVariant'] ?? 'default';
4240  // Get cropArea from CropVariantCollection, if cropSettings is a valid json.
4241  // CropVariantCollection::create does json_decode.
4242  $jsonCropArea = $this->createCropAreaFromJsonString($cropSettings, $cropVariant);
4243  $cropArea = $jsonCropArea->isEmpty() ? null : $jsonCropArea->makeAbsoluteBasedOnFile($file);
4244 
4245  // Cropping is configured in TypoScript in the following way: file.crop = 50,50,100,100
4246  if ($jsonCropArea->isEmpty() && preg_match('/^[0-9]+,[0-9]+,[0-9]+,[0-9]+$/', $cropSettings)) {
4247  $cropSettings = explode(',', $cropSettings);
4248  if (count($cropSettings) === 4) {
4249  $stringCropArea = GeneralUtility::makeInstance(
4250  Area::class,
4251  ...$cropSettings
4252  );
4253  $cropArea = $stringCropArea->isEmpty() ? null : $stringCropArea;
4254  }
4255  }
4256  }
4257 
4258  return $cropArea;
4259  }
4260 
4269  protected function createCropAreaFromJsonString(string $cropSettings, string $cropVariant): Area
4270  {
4271  return ‪CropVariantCollection::create($cropSettings)->‪getCropArea($cropVariant);
4272  }
4273 
4274  /***********************************************
4275  *
4276  * Data retrieval etc.
4277  *
4278  ***********************************************/
4285  public function getFieldVal($field)
4286  {
4287  if (!str_contains($field, '//')) {
4288  return $this->data[trim($field)] ?? null;
4289  }
4290  $sections = ‪GeneralUtility::trimExplode('//', $field, true);
4291  foreach ($sections as $k) {
4292  if ((string)($this->data[$k] ?? '') !== '') {
4293  return $this->data[$k];
4294  }
4295  }
4296 
4297  return '';
4298  }
4299 
4308  public function getData($string, $fieldArray = null)
4309  {
4310  $tsfe = $this->getTypoScriptFrontendController();
4311  if (!is_array($fieldArray)) {
4312  $fieldArray = $tsfe->page;
4313  }
4314  $retVal = '';
4315  // @todo: getData should not be called with non-string as $string. example trigger:
4316  // SecureHtmlRenderingTest htmlViewHelperAvoidsCrossSiteScripting set #07 PHP 8
4317  $sections = is_string($string) ? explode('//', $string) : [];
4318  foreach ($sections as $secKey => $secVal) {
4319  if ($retVal) {
4320  break;
4321  }
4322  $parts = explode(':', $secVal, 2);
4323  $type = strtolower(trim($parts[0]));
4324  $typesWithOutParameters = ['level', 'date', 'current', 'pagelayout'];
4325  $key = trim($parts[1] ?? '');
4326  if (($key != '') || in_array($type, $typesWithOutParameters)) {
4327  switch ($type) {
4328  case 'gp':
4329  // Merge GET and POST and get $key out of the merged array
4330  $getPostArray = GeneralUtility::_GET();
4331  ‪ArrayUtility::mergeRecursiveWithOverrule($getPostArray, GeneralUtility::_POST());
4332  $retVal = $this->getGlobal($key, $getPostArray);
4333  break;
4334  case 'tsfe':
4335  $retVal = $this->getGlobal('TSFE|' . $key);
4336  break;
4337  case 'getenv':
4338  $retVal = getenv($key);
4339  break;
4340  case 'getindpenv':
4341  $retVal = $this->getEnvironmentVariable($key);
4342  break;
4343  case 'field':
4344  $retVal = $this->getGlobal($key, $fieldArray);
4345  break;
4346  case 'file':
4347  $retVal = $this->getFileDataKey($key);
4348  break;
4349  case 'parameters':
4350  $retVal = $this->parameters[$key] ?? null;
4351  break;
4352  case 'register':
4353  $retVal = $tsfe->register[$key] ?? null;
4354  break;
4355  case 'global':
4356  $retVal = $this->getGlobal($key);
4357  break;
4358  case 'level':
4359  $retVal = count($tsfe->tmpl->rootLine) - 1;
4360  break;
4361  case 'leveltitle':
4362  $keyParts = ‪GeneralUtility::trimExplode(',', $key);
4363  $pointer = (int)($keyParts[0] ?? 0);
4364  $slide = (string)($keyParts[1] ?? '');
4365 
4366  $numericKey = $this->getKey($pointer, $tsfe->tmpl->rootLine);
4367  $retVal = $this->rootLineValue($numericKey, 'title', strtolower($slide) === 'slide');
4368  break;
4369  case 'levelmedia':
4370  $keyParts = ‪GeneralUtility::trimExplode(',', $key);
4371  $pointer = (int)($keyParts[0] ?? 0);
4372  $slide = (string)($keyParts[1] ?? '');
4373 
4374  $numericKey = $this->getKey($pointer, $tsfe->tmpl->rootLine);
4375  $retVal = $this->rootLineValue($numericKey, 'media', strtolower($slide) === 'slide');
4376  break;
4377  case 'leveluid':
4378  $numericKey = $this->getKey((int)$key, $tsfe->tmpl->rootLine);
4379  $retVal = $this->rootLineValue($numericKey, 'uid');
4380  break;
4381  case 'levelfield':
4382  $keyParts = ‪GeneralUtility::trimExplode(',', $key);
4383  $pointer = (int)($keyParts[0] ?? 0);
4384  $field = (string)($keyParts[1] ?? '');
4385  $slide = (string)($keyParts[2] ?? '');
4386 
4387  $numericKey = $this->getKey($pointer, $tsfe->tmpl->rootLine);
4388  $retVal = $this->rootLineValue($numericKey, $field, strtolower($slide) === 'slide');
4389  break;
4390  case 'fullrootline':
4391  $keyParts = ‪GeneralUtility::trimExplode(',', $key);
4392  $pointer = (int)($keyParts[0] ?? 0);
4393  $field = (string)($keyParts[1] ?? '');
4394  $slide = (string)($keyParts[2] ?? '');
4395 
4396  $fullKey = (int)($pointer - count($tsfe->tmpl->rootLine) + count($tsfe->rootLine));
4397  if ($fullKey >= 0) {
4398  $retVal = $this->rootLineValue($fullKey, $field, stristr($slide, 'slide') !== false, $tsfe->rootLine);
4399  }
4400  break;
4401  case 'date':
4402  if (!$key) {
4403  $key = 'd/m Y';
4404  }
4405  $retVal = date($key, ‪$GLOBALS['EXEC_TIME']);
4406  break;
4407  case 'page':
4408  $retVal = $tsfe->page[$key] ?? '';
4409  break;
4410  case 'pagelayout':
4411  $retVal = GeneralUtility::makeInstance(PageLayoutResolver::class)
4412  ->getLayoutForPage($tsfe->page, $tsfe->rootLine);
4413  break;
4414  case 'current':
4415  $retVal = $this->data[$this->currentValKey] ?? null;
4416  break;
4417  case 'db':
4418  $selectParts = ‪GeneralUtility::trimExplode(':', $key, true);
4419  if (!isset($selectParts[1])) {
4420  break;
4421  }
4422  $dbRecord = $tsfe->sys_page->getRawRecord($selectParts[0], $selectParts[1]);
4423  if (is_array($dbRecord) && isset($selectParts[2])) {
4424  $retVal = $dbRecord[$selectParts[2]] ?? '';
4425  }
4426  break;
4427  case 'lll':
4428  $retVal = $tsfe->sL('LLL:' . $key);
4429  break;
4430  case 'path':
4431  try {
4432  $retVal = GeneralUtility::makeInstance(FilePathSanitizer::class)->sanitize($key);
4433  } catch (Exception $e) {
4434  // do nothing in case the file path is invalid
4435  $retVal = null;
4436  }
4437  break;
4438  case 'cobj':
4439  switch ($key) {
4440  case 'parentRecordNumber':
4441  $retVal = $this->parentRecordNumber;
4442  break;
4443  }
4444  break;
4445  case 'debug':
4446  switch ($key) {
4447  case 'rootLine':
4448  $retVal = ‪DebugUtility::viewArray($tsfe->tmpl->rootLine);
4449  break;
4450  case 'fullRootLine':
4451  $retVal = ‪DebugUtility::viewArray($tsfe->rootLine);
4452  break;
4453  case 'data':
4454  $retVal = ‪DebugUtility::viewArray($this->data);
4455  break;
4456  case 'register':
4457  $retVal = ‪DebugUtility::viewArray($tsfe->register);
4458  break;
4459  case 'page':
4460  $retVal = ‪DebugUtility::viewArray($tsfe->page);
4461  break;
4462  }
4463  break;
4464  case 'flexform':
4465  $keyParts = ‪GeneralUtility::trimExplode(':', $key, true);
4466  if (count($keyParts) === 2 && isset($this->data[$keyParts[0]])) {
4467  $flexFormContent = $this->data[$keyParts[0]];
4468  if (!empty($flexFormContent)) {
4469  $flexFormService = GeneralUtility::makeInstance(FlexFormService::class);
4470  $flexFormKey = str_replace('.', '|', $keyParts[1]);
4471  $settings = $flexFormService->convertFlexFormContentToArray($flexFormContent);
4472  $retVal = $this->getGlobal($flexFormKey, $settings);
4473  }
4474  }
4475  break;
4476  case 'session':
4477  $keyParts = ‪GeneralUtility::trimExplode('|', $key, true);
4478  $sessionKey = array_shift($keyParts);
4479  $retVal = $this->getTypoScriptFrontendController()->fe_user->getSessionData($sessionKey);
4480  foreach ($keyParts as $keyPart) {
4481  if (is_object($retVal)) {
4482  $retVal = $retVal->{$keyPart};
4483  } elseif (is_array($retVal)) {
4484  $retVal = $retVal[$keyPart];
4485  } else {
4486  $retVal = '';
4487  break;
4488  }
4489  }
4490  if (!is_scalar($retVal)) {
4491  $retVal = '';
4492  }
4493  break;
4494  case 'context':
4495  $context = GeneralUtility::makeInstance(Context::class);
4496  [$aspectName, $propertyName] = ‪GeneralUtility::trimExplode(':', $key, true, 2);
4497  $retVal = $context->getPropertyFromAspect($aspectName, $propertyName, '');
4498  if (is_array($retVal)) {
4499  $retVal = implode(',', $retVal);
4500  }
4501  if (!is_scalar($retVal)) {
4502  $retVal = '';
4503  }
4504  break;
4505  case 'site':
4506  $site = $this->getTypoScriptFrontendController()->getSite();
4507  if ($key === 'identifier') {
4508  $retVal = $site->getIdentifier();
4509  } elseif ($key === 'base') {
4510  $retVal = $site->getBase();
4511  } else {
4512  try {
4513  $retVal = ‪ArrayUtility::getValueByPath($site->getConfiguration(), $key, '.');
4514  } catch (MissingArrayPathException $exception) {
4515  $this->logger->notice('Configuration "{key}" is not defined for site "{site}"', ['key' => $key, 'site' => $site->getIdentifier(), 'exception' => $exception]);
4516  }
4517  }
4518  break;
4519  case 'sitelanguage':
4520  $siteLanguage = $this->getTypoScriptFrontendController()->getLanguage();
4521  $config = $siteLanguage->toArray();
4522  // Harmonizing the namings from the site configuration value with the TypoScript setting
4523  if ($key === 'flag') {
4524  $key = 'flagIdentifier';
4525  }
4526  if (isset($config[$key])) {
4527  $retVal = $config[$key] ?? '';
4528  }
4529  break;
4530  }
4531  }
4532 
4533  foreach (‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['getData'] ?? [] as $className) {
4534  $hookObject = GeneralUtility::makeInstance($className);
4535  if (!$hookObject instanceof ContentObjectGetDataHookInterface) {
4536  throw new \UnexpectedValueException('$hookObject must implement interface ' . ContentObjectGetDataHookInterface::class, 1195044480);
4537  }
4538  $ref = $this; // introduced for phpstan to not lose type information when passing $this into callUserFunction
4539  $retVal = $hookObject->getDataExtension($string, $fieldArray, $secVal, $retVal, $ref);
4540  }
4541  }
4542  return $retVal;
4543  }
4544 
4554  protected function getFileDataKey($key)
4555  {
4556  [$fileUidOrCurrentKeyword, $requestedFileInformationKey] = ‪GeneralUtility::trimExplode(':', $key, false, 3);
4557  try {
4558  if ($fileUidOrCurrentKeyword === 'current') {
4559  $fileObject = $this->getCurrentFile();
4560  } elseif (‪MathUtility::canBeInterpretedAsInteger($fileUidOrCurrentKeyword)) {
4561  $fileFactory = GeneralUtility::makeInstance(ResourceFactory::class);
4562  $fileObject = $fileFactory->getFileObject($fileUidOrCurrentKeyword);
4563  } else {
4564  $fileObject = null;
4565  }
4566  } catch (Exception $exception) {
4567  $this->logger->warning('The file "{uid}" could not be found and won\'t be included in frontend output', ['uid' => $fileUidOrCurrentKeyword, 'exception' => $exception]);
4568  $fileObject = null;
4569  }
4570 
4571  if ($fileObject instanceof FileInterface) {
4572  // All properties of the \TYPO3\CMS\Core\Resource\FileInterface are available here:
4573  switch ($requestedFileInformationKey) {
4574  case 'name':
4575  return $fileObject->getName();
4576  case 'uid':
4577  if (method_exists($fileObject, 'getUid')) {
4578  return $fileObject->getUid();
4579  }
4580  return 0;
4581  case 'originalUid':
4582  if ($fileObject instanceof FileReference) {
4583  return $fileObject->getOriginalFile()->getUid();
4584  }
4585  return null;
4586  case 'size':
4587  return $fileObject->getSize();
4588  case 'sha1':
4589  return $fileObject->getSha1();
4590  case 'extension':
4591  return $fileObject->getExtension();
4592  case 'mimetype':
4593  return $fileObject->getMimeType();
4594  case 'contents':
4595  return $fileObject->getContents();
4596  case 'publicUrl':
4597  return $fileObject->getPublicUrl();
4598  default:
4599  // Generic alternative here
4600  return $fileObject->getProperty($requestedFileInformationKey);
4601  }
4602  } else {
4603  // @todo fail silently as is common in tslib_content
4604  return 'Error: no file object';
4605  }
4606  }
4607 
4619  public function rootLineValue($key, $field, $slideBack = false, $altRootLine = '')
4620  {
4621  $rootLine = is_array($altRootLine) ? $altRootLine : $this->getTypoScriptFrontendController()->tmpl->rootLine;
4622  if (!$slideBack) {
4623  return $rootLine[$key][$field] ?? '';
4624  }
4625  for ($a = $key; $a >= 0; $a--) {
4626  $val = $rootLine[$a][$field] ?? '';
4627  if ($val) {
4628  return $val;
4629  }
4630  }
4631 
4632  return '';
4633  }
4634 
4644  public function getGlobal($keyString, $source = null)
4645  {
4646  $keys = explode('|', $keyString);
4647  $numberOfLevels = count($keys);
4648  $rootKey = trim($keys[0]);
4649  $value = isset($source) ? ($source[$rootKey] ?? '') : (‪$GLOBALS[$rootKey] ?? '');
4650  for ($i = 1; $i < $numberOfLevels && isset($value); $i++) {
4651  $currentKey = trim($keys[$i]);
4652  if (is_object($value)) {
4653  $value = $value->{$currentKey};
4654  } elseif (is_array($value)) {
4655  $value = $value[$currentKey] ?? '';
4656  } else {
4657  $value = '';
4658  break;
4659  }
4660  }
4661  if (!is_scalar($value)) {
4662  $value = '';
4663  }
4664  return $value;
4665  }
4666 
4677  public function getKey($key, $arr)
4678  {
4679  $key = (int)$key;
4680  if (is_array($arr)) {
4681  if ($key < 0) {
4682  $key = count($arr) + $key;
4683  }
4684  if ($key < 0) {
4685  $key = 0;
4686  }
4687  }
4688  return $key;
4689  }
4690 
4691  /***********************************************
4692  *
4693  * Link functions (typolink)
4694  *
4695  ***********************************************/
4710  protected function resolveMixedLinkParameter($linkText, $mixedLinkParameter, &$configuration = [])
4711  {
4712  // Link parameter value = first part
4713  $linkParameterParts = GeneralUtility::makeInstance(TypoLinkCodecService::class)->decode($mixedLinkParameter);
4714 
4715  // Check for link-handler keyword
4716  $linkHandlerExploded = explode(':', $linkParameterParts['url'], 2);
4717  $linkHandlerKeyword = (string)($linkHandlerExploded[0] ?? '');
4718 
4719  if (in_array(strtolower((string)preg_replace('#\s|[[:cntrl:]]#', '', $linkHandlerKeyword)), ['javascript', 'data'], true)) {
4720  // Disallow insecure scheme's like javascript: or data:
4721  return $linkText;
4722  }
4723 
4724  // additional parameters that need to be set
4725  if ($linkParameterParts['additionalParams'] !== '') {
4726  $forceParams = $linkParameterParts['additionalParams'];
4727  // params value
4728  $configuration['additionalParams'] = ($configuration['additionalParams'] ?? '') . $forceParams[0] === '&' ? $forceParams : '&' . $forceParams;
4729  }
4730 
4731  return [
4732  'href' => $linkParameterParts['url'],
4733  'target' => $linkParameterParts['target'],
4734  'class' => $linkParameterParts['class'],
4735  'title' => $linkParameterParts['title'],
4736  ];
4737  }
4738 
4754  public function typoLink($linkText, $conf)
4755  {
4756  $linkText = (string)$linkText;
4757  $tsfe = $this->getTypoScriptFrontendController();
4758 
4759  if (isset($conf['parameter.'])) {
4760  // Evaluate "parameter." stdWrap but keep additional information (like target, class and title)
4761  $typoLinkCodecService = GeneralUtility::makeInstance(TypoLinkCodecService::class);
4762  $linkParameterParts = $typoLinkCodecService->decode($conf['parameter'] ?? '');
4763  $modifiedLinkParameterString = $this->stdWrap($linkParameterParts['url'], $conf['parameter.'] ?? []);
4764  // As the stdWrap result might contain target etc. as well again (".field = header_link")
4765  // the result is then taken from the stdWrap and overridden if the value is not empty.
4766  $modifiedLinkParameterParts = $typoLinkCodecService->decode($modifiedLinkParameterString ?? '');
4767  $linkParameterParts = array_replace($linkParameterParts, array_filter($modifiedLinkParameterParts, 'trim'));
4768  $linkParameter = $typoLinkCodecService->encode($linkParameterParts);
4769  } else {
4770  $linkParameter = trim(($conf['parameter'] ?? ''));
4771  }
4772  $this->lastTypoLinkUrl = '';
4773  $this->lastTypoLinkTarget = '';
4774 
4775  $resolvedLinkParameters = $this->resolveMixedLinkParameter($linkText, $linkParameter, $conf);
4776 
4777  // check if the link handler hook has resolved the link completely already
4778  if (!is_array($resolvedLinkParameters)) {
4779  return $resolvedLinkParameters;
4780  }
4781  $linkParameter = $resolvedLinkParameters['href'];
4782  $target = $resolvedLinkParameters['target'];
4783  $title = $resolvedLinkParameters['title'];
4784 
4785  $linkDetails = [];
4786  if (!$linkParameter) {
4787  // Support anchors without href value if id or name attribute is present.
4788  $aTagParams = (string)$this->stdWrapValue('ATagParams', $conf ?? []);
4789  $aTagParams = GeneralUtility::get_tag_attributes($aTagParams);
4790  // If it looks like an anchor tag, render it anyway
4791  if (isset($aTagParams['id']) || isset($aTagParams['name'])) {
4792  $linkDetails = [
4793  'type' => 'inpage',
4794  'url' => '',
4795  ];
4796  }
4797  } else {
4798  // Detecting kind of link and resolve all necessary parameters
4799  $linkService = GeneralUtility::makeInstance(LinkService::class);
4800  try {
4801  $linkDetails = $linkService->resolve($linkParameter);
4802  } catch (UnknownLinkHandlerException | InvalidPathException $exception) {
4803  $this->logger->warning('The link could not be generated', ['exception' => $exception]);
4804  return $linkText;
4805  }
4806  }
4807 
4808  $linkDetails['typoLinkParameter'] = $linkParameter;
4809  if (isset($linkDetails['type']) && isset(‪$GLOBALS['TYPO3_CONF_VARS']['FE']['typolinkBuilder'][$linkDetails['type']])) {
4811  $linkBuilder = GeneralUtility::makeInstance(
4812  ‪$GLOBALS['TYPO3_CONF_VARS']['FE']['typolinkBuilder'][$linkDetails['type']],
4813  $this,
4814  // AbstractTypolinkBuilder type hints an optional dependency to TypoScriptFrontendController.
4815  // Some core parts however "fake" $GLOBALS['TSFE'] to stdCLass() due to its long list of
4816  // dependencies. f:html view helper is such a scenario. This of course crashes if given to typolink builder
4817  // classes. For now, we check the instance and hand over 'null', giving the link builders the option
4818  // to take care of tsfe themselfs. This scenario is for instance triggered when in BE login when sys_news
4819  // records set links.
4820  $tsfe instanceof TypoScriptFrontendController ? $tsfe : null
4821  );
4822  try {
4823  $linkedResult = $linkBuilder->build($linkDetails, $linkText, $target, $conf);
4824  // Legacy layer, can be removed in TYPO3 v12.0.
4825  if (!($linkedResult instanceof LinkResultInterface)) {
4826  if (is_array($linkedResult)) {
4827  [$url, $linkText, $target] = $linkedResult;
4828  } else {
4829  $url = '';
4830  }
4831  $linkedResult = new LinkResult($linkDetails['type'], $url);
4832  $linkedResult = $linkedResult
4833  ->withTarget($target)
4834  ->withLinkConfiguration($conf)
4835  ->withLinkText($linkText);
4836  }
4837  } catch (UnableToLinkException $e) {
4838  $this->logger->debug('Unable to link "{text}"', [
4839  'text' => $e->getLinkText(),
4840  'exception' => $e,
4841  ]);
4842 
4843  // Only return the link text directly
4844  return $e->getLinkText();
4845  }
4846  } elseif (isset($linkDetails['url'])) {
4847  $linkedResult = new LinkResult($linkDetails['type'], $linkDetails['url']);
4848  $linkedResult = $linkedResult
4849  ->withTarget($target)
4850  ->withLinkConfiguration($conf)
4851  ->withLinkText($linkText);
4852  } else {
4853  return $linkText;
4854  }
4855 
4856  $this->lastTypoLinkResult = $linkedResult;
4857  $this->lastTypoLinkTarget = $linkedResult->getTarget();
4858  $this->lastTypoLinkUrl = $linkedResult->getUrl();
4859  $this->lastTypoLinkLD['target'] = htmlspecialchars($linkedResult->getTarget());
4860  $this->lastTypoLinkLD['totalUrl'] = $linkedResult->getUrl();
4861  $this->lastTypoLinkLD['type'] = $linkedResult->getType();
4862 
4863  // We need to backup the URL because ATagParams might call typolink again and change the last URL.
4864  $url = $this->lastTypoLinkUrl;
4865  $linkResultAttrs = array_filter(
4866  $linkedResult->getAttributes(),
4867  static ‪function (string $name): bool {
4868  return !in_array($name, ['href', 'target']);
4869  },
4870  ARRAY_FILTER_USE_KEY
4871  );
4872  $finalTagParts = [
4873  'aTagParams' => rtrim($this->getATagParams($conf) . ' ' . GeneralUtility::implodeAttributes($linkResultAttrs, true)),
4874  'url' => $url,
4875  'TYPE' => $linkedResult->getType(),
4876  ];
4877 
4878  // Ensure "href" is not in the list of aTagParams to avoid double tags, usually happens within buggy parseFunc settings
4879  if (!empty($finalTagParts['aTagParams'])) {
4880  $aTagParams = GeneralUtility::get_tag_attributes($finalTagParts['aTagParams'], true);
4881  if (isset($aTagParams['href'])) {
4882  unset($aTagParams['href']);
4883  $finalTagParts['aTagParams'] = GeneralUtility::implodeAttributes($aTagParams, true);
4884  }
4885  }
4886 
4887  // Building the final <a href=".."> tag
4888  $tagAttributes = [];
4889 
4890  // Title attribute
4891  if (empty($title)) {
4892  $title = $conf['title'] ?? '';
4893  if (isset($conf['title.']) && is_array($conf['title.'])) {
4894  $title = $this->stdWrap($title, $conf['title.']);
4895  }
4896  }
4897 
4898  // Check, if the target is coded as a JS open window link:
4899  $JSwindowParts = [];
4900  $JSwindowParams = '';
4901  if ($this->lastTypoLinkResult->getTarget() && preg_match('/^([0-9]+)x([0-9]+)(:(.*)|.*)$/', $this->lastTypoLinkResult->getTarget(), $JSwindowParts)) {
4902  // Take all pre-configured and inserted parameters and compile parameter list, including width+height:
4903  $JSwindow_tempParamsArr = GeneralUtility::trimExplode(',', strtolower(($conf['JSwindow_params'] ?? '') . ',' . ($JSwindowParts[4] ?? '')), true);
4904  $JSwindow_paramsArr = [];
4905  $target = $conf['target'] ?? 'FEopenLink';
4906  foreach ($JSwindow_tempParamsArr as $JSv) {
4907  [$JSp, $JSv] = explode('=', $JSv, 2);
4908  // If the target is set as JS param, this is extracted
4909  if ($JSp === 'target') {
4910  $target = $JSv;
4911  } else {
4912  $JSwindow_paramsArr[$JSp] = $JSp . '=' . $JSv;
4913  }
4914  }
4915  $this->lastTypoLinkResult = $this->lastTypoLinkResult->withAttribute('target', $target);
4916  // Add width/height:
4917  $JSwindow_paramsArr['width'] = 'width=' . $JSwindowParts[1];
4918  $JSwindow_paramsArr['height'] = 'height=' . $JSwindowParts[2];
4919  // Imploding into string:
4920  $JSwindowParams = implode(',', $JSwindow_paramsArr);
4921  }
4922 
4923  if (!$JSwindowParams && $linkedResult->getType() === LinkService::TYPE_EMAIL && $tsfe instanceof TypoScriptFrontendController && $tsfe->spamProtectEmailAddresses === 'ascii') {
4924  $tagAttributes['href'] = $finalTagParts['url'];
4925  } else {
4926  $tagAttributes['href'] = htmlspecialchars($finalTagParts['url']);
4927  }
4928  if (!empty($title)) {
4929  $tagAttributes['title'] = htmlspecialchars($title);
4930  $this->lastTypoLinkResult = $this->lastTypoLinkResult->withAttribute('title', $title);
4931  }
4932 
4933  // Target attribute
4934  if (!empty($this->lastTypoLinkResult->getTarget())) {
4935  $tagAttributes['target'] = htmlspecialchars($this->lastTypoLinkResult->getTarget());
4936  }
4937  if ($JSwindowParams && $tsfe instanceof TypoScriptFrontendController && in_array($tsfe->xhtmlDoctype, ['xhtml_strict', 'xhtml_11'], true)) {
4938  // Create TARGET-attribute only if the right doctype is used
4939  unset($tagAttributes['target']);
4940  }
4941 
4942  if ($JSwindowParams) {
4943  $JSwindowAttrs = [
4944  'data-window-url' => $tsfe instanceof TypoScriptFrontendController ? $tsfe->baseUrlWrap($finalTagParts['url']) : $finalTagParts['url'],
4945  'data-window-target' => $this->lastTypoLinkResult->getTarget(),
4946  'data-window-features' => $JSwindowParams,
4947  ];
4948  $tagAttributes = array_merge($tagAttributes, array_map('htmlspecialchars', $JSwindowAttrs));
4949  $this->lastTypoLinkResult = $this->lastTypoLinkResult->withAttributes($JSwindowAttrs);
4950  $this->addDefaultFrontendJavaScript();
4951  }
4952 
4953  if (!empty($resolvedLinkParameters['class'])) {
4954  $tagAttributes['class'] = htmlspecialchars($resolvedLinkParameters['class']);
4955  $this->lastTypoLinkResult = $this->lastTypoLinkResult->withAttribute('class', $tagAttributes['class']);
4956  }
4957 
4958  // Prevent trouble with double and missing spaces between attributes and merge params before implode
4959  // (skip decoding HTML entities, since `$tagAttributes` are expected to be encoded already)
4960  $finalTagAttributes = array_merge($tagAttributes, GeneralUtility::get_tag_attributes($finalTagParts['aTagParams']));
4961  $finalTagAttributes = $this->addSecurityRelValues($finalTagAttributes, $this->lastTypoLinkResult->getTarget(), $tagAttributes['href']);
4962  $this->lastTypoLinkResult = $this->lastTypoLinkResult->withAttributes($finalTagAttributes);
4963  $finalAnchorTag = '<a ' . GeneralUtility::implodeAttributes($finalTagAttributes) . '>';
4964 
4965  $this->lastTypoLinkTarget = $this->lastTypoLinkResult->getTarget();
4966  // kept for backwards-compatibility in hooks
4967  $finalTagParts['targetParams'] = $this->lastTypoLinkResult->getTarget() ? 'target="' . htmlspecialchars($this->lastTypoLinkResult->getTarget()) . '"' : '';
4968 
4969  // Call user function:
4970  if ($conf['userFunc'] ?? false) {
4971  $finalTagParts['TAG'] = $finalAnchorTag;
4972  $finalAnchorTag = $this->callUserFunction($conf['userFunc'], $conf['userFunc.'] ?? [], $finalTagParts);
4973  // Ensure to keep the result object up-to-date even after the user func was called
4974  $finalAnchorTagParts = GeneralUtility::get_tag_attributes($finalAnchorTag, true);
4975  $this->lastTypoLinkResult = $this->lastTypoLinkResult->withAttributes($finalAnchorTagParts, true);
4976  }
4977 
4978  // Hook: Call post processing function for link rendering:
4979  if (!empty(‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['typoLink_PostProc'])) {
4980  $lastTypoLinkText = $this->lastTypoLinkResult->getLinkText();
4981  $_params = [
4982  'conf' => &$conf,
4983  'originalLinktxt' => &$linkText,
4984  'linktxt' => &$lastTypoLinkText,
4985  'finalTag' => &$finalAnchorTag,
4986  'finalTagParts' => &$finalTagParts,
4987  'linkDetails' => &$linkDetails,
4988  'tagAttributes' => &$finalTagAttributes,
4989  ];
4990  foreach (‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['typoLink_PostProc'] ?? [] as $_funcRef) {
4991  $ref = $this; // introduced for phpstan to not lose type information when passing $this into callUserFunction
4992  GeneralUtility::callUserFunction($_funcRef, $_params, $ref);
4993  }
4994  // Ensure to keep the result object up-to-date even after the user func was called
4995  $finalAnchorTagParts = GeneralUtility::get_tag_attributes($finalAnchorTag, true);
4996  $this->lastTypoLinkResult = $this->lastTypoLinkResult
4997  ->withAttributes($finalAnchorTagParts)
4998  ->withLinkText((string)$_params['linktxt']);
4999  }
5000 
5001  // If flag "returnLastTypoLinkUrl" set, then just return the latest URL made:
5002  if ($conf['returnLast'] ?? false) {
5003  switch ($conf['returnLast']) {
5004  case 'url':
5005  return $this->lastTypoLinkUrl;
5006  case 'target':
5007  return $this->lastTypoLinkTarget;
5008  case 'result':
5009  return $this->lastTypoLinkResult;
5010  }
5011  }
5012 
5013  $wrap = (string)$this->stdWrapValue('wrap', $conf ?? []);
5014 
5015  if ($conf['ATagBeforeWrap'] ?? false) {
5016  return $finalAnchorTag . $this->wrap((string)$this->lastTypoLinkResult->getLinkText(), $wrap) . '</a>';
5017  }
5018  return $this->wrap($finalAnchorTag . $this->lastTypoLinkResult->getLinkText() . '</a>', $wrap);
5019  }
5020 
5021  protected function addSecurityRelValues(array $tagAttributes, ?string $target, string $url): array
5022  {
5023  $relAttribute = 'noreferrer';
5024  if (in_array($target, ['', null, '_self', '_parent', '_top'], true) || $this->isInternalUrl($url)) {
5025  return $tagAttributes;
5026  }
5027 
5028  if (!isset($tagAttributes['rel'])) {
5029  $tagAttributes['rel'] = $relAttribute;
5030  return $tagAttributes;
5031  }
5032 
5033  $tagAttributes['rel'] = implode(' ', array_unique(array_merge(
5034  GeneralUtility::trimExplode(' ', $relAttribute),
5035  GeneralUtility::trimExplode(' ', $tagAttributes['rel'])
5036  )));
5037 
5038  return $tagAttributes;
5039  }
5040 
5051  protected function isInternalUrl(string $url): bool
5052  {
5053  $cache = GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
5054  $parsedUrl = parse_url($url);
5055  $foundDomains = 0;
5056  if (!isset($parsedUrl['host'])) {
5057  return true;
5058  }
5059 
5060  $cacheIdentifier = sha1('isInternalDomain' . $parsedUrl['host']);
5061 
5062  if ($cache->has($cacheIdentifier) === false) {
5063  foreach (GeneralUtility::makeInstance(SiteFinder::class)->getAllSites() as $site) {
5064  if ($site->getBase()->getHost() === $parsedUrl['host']) {
5065  ++$foundDomains;
5066  break;
5067  }
5068 
5069  if ($site->getBase()->getHost() === '' && GeneralUtility::isOnCurrentHost($url)) {
5070  ++$foundDomains;
5071  break;
5072  }
5073  }
5074 
5075  $cache->set($cacheIdentifier, $foundDomains > 0);
5076  }
5077 
5078  return (bool)$cache->get($cacheIdentifier);
5079  }
5080 
5088  public function typoLink_URL($conf)
5089  {
5090  $this->typoLink('|', $conf);
5091  return $this->lastTypoLinkUrl;
5092  }
5093 
5107  public function getTypoLink($label, $params, $urlParameters = [], $target = '')
5108  {
5109  $conf = [];
5110  $conf['parameter'] = $params;
5111  if ($target) {
5112  $conf['target'] = $target;
5113  $conf['extTarget'] = $target;
5114  $conf['fileTarget'] = $target;
5115  }
5116  if (is_array($urlParameters)) {
5117  if (!empty($urlParameters)) {
5118  $conf['additionalParams'] = ($conf['additionalParams'] ?? '') . HttpUtility::buildQueryString($urlParameters, '&');
5119  }
5120  } else {
5121  $conf['additionalParams'] = ($conf['additionalParams'] ?? '') . $urlParameters;
5122  }
5123  $out = $this->typoLink($label, $conf);
5124  return $out;
5125  }
5126 
5134  public function getUrlToCurrentLocation($addQueryString = true)
5135  {
5136  $conf = [];
5137  $conf['parameter'] = $this->getTypoScriptFrontendController()->id . ',' . $this->getTypoScriptFrontendController()->type;
5138  if ($addQueryString) {
5139  $conf['addQueryString'] = '1';
5140  $linkVars = implode(',', array_keys(GeneralUtility::explodeUrl2Array($this->getTypoScriptFrontendController()->linkVars)));
5141  $conf['addQueryString.'] = [
5142  'exclude' => 'id,type,cHash' . ($linkVars ? ',' . $linkVars : ''),
5143  ];
5144  }
5145 
5146  return $this->typoLink_URL($conf);
5147  }
5148 
5158  public function getTypoLink_URL($params, $urlParameters = [], $target = '')
5159  {
5160  $this->getTypoLink('', $params, $urlParameters, $target);
5161  return $this->lastTypoLinkUrl;
5162  }
5163 
5173  protected function processUrl($context, $url, $typolinkConfiguration = [])
5174  {
5175  $urlProcessors = ‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['urlProcessing']['urlProcessors'] ?? [];
5176  if (empty($urlProcessors)) {
5177  return $url;
5178  }
5179 
5180  foreach ($urlProcessors as $identifier => $configuration) {
5181  if (empty($configuration) || !is_array($configuration)) {
5182  throw new \RuntimeException('Missing configuration for URI processor "' . $identifier . '".', 1442050529);
5183  }
5184  if (!is_string($configuration['processor']) || empty($configuration['processor']) || !class_exists($configuration['processor']) || !is_subclass_of($configuration['processor'], UrlProcessorInterface::class)) {
5185  throw new \RuntimeException('The URI processor "' . $identifier . '" defines an invalid provider. Ensure the class exists and implements the "' . UrlProcessorInterface::class . '".', 1442050579);
5186  }
5187  }
5188 
5189  $orderedProcessors = GeneralUtility::makeInstance(DependencyOrderingService::class)->orderByDependencies($urlProcessors);
5190  $keepProcessing = true;
5191 
5192  foreach ($orderedProcessors as $configuration) {
5194  $urlProcessor = GeneralUtility::makeInstance($configuration['processor']);
5195  $url = $urlProcessor->process($context, $url, $typolinkConfiguration, $this, $keepProcessing);
5196  if (!$keepProcessing) {
5197  break;
5198  }
5199  }
5200 
5201  return $url;
5202  }
5203 
5218  public function getMailTo($mailAddress, $linktxt)
5219  {
5220  $mailAddress = (string)$mailAddress;
5221  if ((string)$linktxt === '') {
5222  $linktxt = htmlspecialchars($mailAddress);
5223  }
5224 
5225  $originalMailToUrl = 'mailto:' . $mailAddress;
5226  $mailToUrl = $this->processUrl(UrlProcessorInterface::CONTEXT_MAIL, $originalMailToUrl);
5227  $attributes = [];
5228 
5229  // no processing happened, therefore, the default processing kicks in
5230  if ($mailToUrl === $originalMailToUrl) {
5231  $tsfe = $this->getTypoScriptFrontendController();
5232  if ($tsfe instanceof TypoScriptFrontendController && $tsfe->spamProtectEmailAddresses) {
5233  $mailToUrl = $this->encryptEmail($mailToUrl, $tsfe->spamProtectEmailAddresses);
5234  if ($tsfe->spamProtectEmailAddresses !== 'ascii') {
5235  $attributes = [
5236  'data-mailto-token' => $mailToUrl,
5237  'data-mailto-vector' => (int)$tsfe->spamProtectEmailAddresses,
5238  ];
5239  $mailToUrl = '#';
5240  }
5241  $atLabel = '(at)';
5242  if (($atLabelFromConfig = trim($tsfe->config['config']['spamProtectEmailAddresses_atSubst'] ?? '')) !== '') {
5243  $atLabel = $atLabelFromConfig;
5244  }
5245  $spamProtectedMailAddress = str_replace('@', $atLabel, htmlspecialchars($mailAddress));
5246  if ($tsfe->config['config']['spamProtectEmailAddresses_lastDotSubst'] ?? false) {
5247  $lastDotLabel = trim($tsfe->config['config']['spamProtectEmailAddresses_lastDotSubst']);
5248  $lastDotLabel = $lastDotLabel ?: '(dot)';
5249  $spamProtectedMailAddress = preg_replace('/\\.([^\\.]+)$/', $lastDotLabel . '$1', $spamProtectedMailAddress);
5250  if ($spamProtectedMailAddress === null) {
5251  $this->logger->debug('Error replacing the last dot in email address "{email}"', ['email' => $spamProtectedMailAddress]);
5252  $spamProtectedMailAddress = '';
5253  }
5254  }
5255  $linktxt = str_ireplace($mailAddress, $spamProtectedMailAddress, $linktxt);
5256  $this->addDefaultFrontendJavaScript();
5257  }
5258  }
5259 
5260  return [$mailToUrl, $linktxt, $attributes];
5261  }
5262 
5270  protected function encryptEmail(string $string, $type): string
5271  {
5272  $out = '';
5273  // obfuscates using the decimal HTML entity references for each character
5274  if ($type === 'ascii') {
5275  foreach (preg_split('//u', $string, -1, PREG_SPLIT_NO_EMPTY) as $char) {
5276  $out .= '&#' . mb_ord($char) . ';';
5277  }
5278  } else {
5279  // like str_rot13() but with a variable offset and a wider character range
5280  $len = strlen($string);
5281  $offset = (int)$type;
5282  for ($i = 0; $i < $len; $i++) {
5283  $charValue = ord($string[$i]);
5284  // 0-9 . , - + / :
5285  if ($charValue >= 43 && $charValue <= 58) {
5286  $out .= $this->encryptCharcode($charValue, 43, 58, $offset);
5287  } elseif ($charValue >= 64 && $charValue <= 90) {
5288  // A-Z @
5289  $out .= $this->encryptCharcode($charValue, 64, 90, $offset);
5290  } elseif ($charValue >= 97 && $charValue <= 122) {
5291  // a-z
5292  $out .= $this->encryptCharcode($charValue, 97, 122, $offset);
5293  } else {
5294  $out .= $string[$i];
5295  }
5296  }
5297  }
5298  return $out;
5299  }
5300 
5311  protected function encryptCharcode($n, $start, $end, $offset)
5312  {
5313  $n = $n + $offset;
5314  if ($offset > 0 && $n > $end) {
5315  $n = $start + ($n - $end - 1);
5316  } elseif ($offset < 0 && $n < $start) {
5317  $n = $end - ($start - $n - 1);
5318  }
5319  return chr($n);
5320  }
5321 
5329  public function getQueryArguments($conf)
5330  {
5331  $currentQueryArray = GeneralUtility::_GET();
5332  if ($conf['exclude'] ?? false) {
5333  $excludeString = str_replace(',', '&', $conf['exclude']);
5334  $excludedQueryParts = [];
5335  parse_str($excludeString, $excludedQueryParts);
5336  $newQueryArray = ArrayUtility::arrayDiffKeyRecursive($currentQueryArray, $excludedQueryParts);
5337  } else {
5338  $newQueryArray = $currentQueryArray;
5339  }
5340  return HttpUtility::buildQueryString($newQueryArray, '&');
5341  }
5342 
5343  /***********************************************
5344  *
5345  * Miscellaneous functions, stand alone
5346  *
5347  ***********************************************/
5359  public function wrap($content, $wrap, $char = '|')
5360  {
5361  if ($wrap) {
5362  $wrapArr = explode($char, $wrap);
5363  $content = trim($wrapArr[0] ?? '') . $content . trim($wrapArr[1] ?? '');
5364  }
5365  return $content;
5366  }
5367 
5378  public function noTrimWrap($content, $wrap, $char = '|')
5379  {
5380  if ($wrap) {
5381  // expects to be wrapped with (at least) 3 characters (before, middle, after)
5382  // anything else is not taken into account
5383  $wrapArr = explode($char, $wrap, 4);
5384  $content = ($wrapArr[1] ?? '') . $content . ($wrapArr[2] ?? '');
5385  }
5386  return $content;
5387  }
5388 
5401  public function callUserFunction($funcName, $conf, $content)
5402  {
5403  // Split parts
5404  $parts = explode('->', $funcName);
5405  if (count($parts) === 2) {
5406  // Check whether PHP class is available
5407  if (class_exists($parts[0])) {
5408  if ($this->container && $this->container->has($parts[0])) {
5409  $classObj = $this->container->get($parts[0]);
5410  } else {
5411  $classObj = GeneralUtility::makeInstance($parts[0]);
5412  }
5413  $methodName = (string)$parts[1];
5414  $callable = [$classObj, $methodName];
5415 
5416  if (is_object($classObj) && method_exists($classObj, $parts[1]) && is_callable($callable)) {
5417  if (method_exists($classObj, 'setContentObjectRenderer') && is_callable([$classObj, 'setContentObjectRenderer'])) {
5418  $classObj->setContentObjectRenderer($this);
5419  } elseif (property_exists($classObj, 'cObj')) {
5420  trigger_error(
5421  'Setting public property "cObj" on "' . $parts[0] . '" is deprecated since v11 and will be removed in v12. Use explicit setter'
5422  . ' "public function setContentObjectRenderer(ContentObjectRenderer $cObj)" if your plugin needs an instance of ContentObjectRenderer instead.',
5423  E_USER_DEPRECATED
5424  );
5425  // Note this will still fatal if that property is protected. There is no way to
5426  // detect property visibility in PHP without reflection, so we'll deal with this in v11.
5427  // Extensions should either drop the property altogether if they don't need current instance
5428  // of ContentObjectRenderer, or set the property to protected and use the setter above.
5429  $classObj->cObj = $this;
5430  }
5431  $content = $callable($content, $conf, $this->getRequest());
5432  } else {
5433  $this->getTimeTracker()->setTSlogMessage('Method "' . $parts[1] . '" did not exist in class "' . $parts[0] . '"', LogLevel::ERROR);
5434  }
5435  } else {
5436  $this->getTimeTracker()->setTSlogMessage('Class "' . $parts[0] . '" did not exist', LogLevel::ERROR);
5437  }
5438  } elseif (function_exists($funcName)) {
5439  $content = $funcName($content, $conf);
5440  } else {
5441  $this->getTimeTracker()->setTSlogMessage('Function "' . $funcName . '" did not exist', LogLevel::ERROR);
5442  }
5443  return $content;
5444  }
5445 
5452  public function keywords($content)
5453  {
5454  $listArr = preg_split('/[,;' . LF . ']/', $content);
5455  if ($listArr === false) {
5456  return '';
5457  }
5458  foreach ($listArr as $k => $v) {
5459  $listArr[$k] = trim($v);
5460  }
5461  return implode(',', $listArr);
5462  }
5463 
5472  public function caseshift($theValue, $case)
5473  {
5474  switch (strtolower($case)) {
5475  case 'upper':
5476  $theValue = mb_strtoupper($theValue, 'utf-8');
5477  break;
5478  case 'lower':
5479  $theValue = mb_strtolower($theValue, 'utf-8');
5480  break;
5481  case 'capitalize':
5482  $theValue = mb_convert_case($theValue, MB_CASE_TITLE, 'utf-8');
5483  break;
5484  case 'ucfirst':
5485  $firstChar = mb_substr($theValue, 0, 1, 'utf-8');
5486  $firstChar = mb_strtoupper($firstChar, 'utf-8');
5487  $remainder = mb_substr($theValue, 1, null, 'utf-8');
5488  $theValue = $firstChar . $remainder;
5489  break;
5490  case 'lcfirst':
5491  $firstChar = mb_substr($theValue, 0, 1, 'utf-8');
5492  $firstChar = mb_strtolower($firstChar, 'utf-8');
5493  $remainder = mb_substr($theValue, 1, null, 'utf-8');
5494  $theValue = $firstChar . $remainder;
5495  break;
5496  case 'uppercamelcase':
5497  $theValue = GeneralUtility::underscoredToUpperCamelCase($theValue);
5498  break;
5499  case 'lowercamelcase':
5500  $theValue = GeneralUtility::underscoredToLowerCamelCase($theValue);
5501  break;
5502  }
5503  return $theValue;
5504  }
5505 
5514  public function HTMLcaseshift($theValue, $case)
5515  {
5516  $inside = 0;
5517  $newVal = '';
5518  $pointer = 0;
5519  $totalLen = strlen($theValue);
5520  do {
5521  if (!$inside) {
5522  $len = strcspn(substr($theValue, $pointer), '<');
5523  $newVal .= $this->caseshift(substr($theValue, $pointer, $len), $case);
5524  $inside = 1;
5525  } else {
5526  $len = strcspn(substr($theValue, $pointer), '>') + 1;
5527  $newVal .= substr($theValue, $pointer, $len);
5528  $inside = 0;
5529  }
5530  $pointer += $len;
5531  } while ($pointer < $totalLen);
5532  return $newVal;
5533  }
5534 
5542  public function calcAge($seconds, $labels)
5543  {
5544  if (MathUtility::canBeInterpretedAsInteger($labels)) {
5545  $labels = ' min| hrs| days| yrs| min| hour| day| year';
5546  } else {
5547  $labels = str_replace('"', '', $labels);
5548  }
5549  $labelArr = explode('|', $labels);
5550  if (count($labelArr) === 4) {
5551  $labelArr = array_merge($labelArr, $labelArr);
5552  }
5553  $absSeconds = abs($seconds);
5554  $sign = $seconds > 0 ? 1 : -1;
5555  if ($absSeconds < 3600) {
5556  $val = round($absSeconds / 60);
5557  $seconds = $sign * $val . ($val == 1 ? $labelArr[4] : $labelArr[0]);
5558  } elseif ($absSeconds < 24 * 3600) {
5559  $val = round($absSeconds / 3600);
5560  $seconds = $sign * $val . ($val == 1 ? $labelArr[5] : $labelArr[1]);
5561  } elseif ($absSeconds < 365 * 24 * 3600) {
5562  $val = round($absSeconds / (24 * 3600));
5563  $seconds = $sign * $val . ($val == 1 ? $labelArr[6] : $labelArr[2]);
5564  } else {
5565  $val = round($absSeconds / (365 * 24 * 3600));
5566  $seconds = $sign * $val . ($val == 1 ? ($labelArr[7] ?? null) : ($labelArr[3] ?? null));
5567  }
5568  return $seconds;
5569  }
5570 
5579  public function mergeTSRef($confArr, $prop)
5580  {
5581  if ($confArr[$prop][0] === '<') {
5582  $key = trim(substr($confArr[$prop], 1));
5583  $cF = GeneralUtility::makeInstance(TypoScriptParser::class);
5584  // $name and $conf is loaded with the referenced values.
5585  $old_conf = $confArr[$prop . '.'] ?? null;
5586  $setupArray = [];
5587  $tsfe = $this->getTypoScriptFrontendController();
5588  if ($tsfe instanceof TypoScriptFrontendController
5589  && $tsfe->tmpl instanceof TemplateService
5590  && is_array($tsfe->tmpl->setup)
5591  ) {
5592  $setupArray = $tsfe->tmpl->setup;
5593  }
5594  $conf = $cF->getVal($key, $setupArray)[1];
5595  if (is_array($old_conf) && !empty($old_conf)) {
5596  $conf = array_replace_recursive($conf, $old_conf);
5597  }
5598  $confArr[$prop . '.'] = $conf;
5599  }
5600  return $confArr;
5601  }
5602 
5603  /***********************************************
5604  *
5605  * Database functions, making of queries
5606  *
5607  ***********************************************/
5635  public function getTreeList($id, $depth, $begin = 0, $dontCheckEnableFields = false, $addSelectFields = '', $moreWhereClauses = '', array $prevId_array = [], $recursionLevel = 0)
5636  {
5637  $id = (int)$id;
5638  if (!$id) {
5639  return '';
5640  }
5641 
5642  // Init vars:
5643  $allFields = 'uid,hidden,starttime,endtime,fe_group,extendToSubpages,doktype,php_tree_stop,mount_pid,mount_pid_ol,t3ver_state,l10n_parent' . $addSelectFields;
5644  $depth = (int)$depth;
5645  $begin = (int)$begin;
5646  $theList = [];
5647  $addId = 0;
5648  $requestHash = '';
5649 
5650  // First level, check id (second level, this is done BEFORE the recursive call)
5651  $tsfe = $this->getTypoScriptFrontendController();
5652  if (!$recursionLevel) {
5653  // Check tree list cache
5654  // First, create the hash for this request - not sure yet whether we need all these parameters though
5655  $parameters = [
5656  $id,
5657  $depth,
5658  $begin,
5659  $dontCheckEnableFields,
5660  $addSelectFields,
5661  $moreWhereClauses,
5662  $prevId_array,
5663  GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('frontend.user', 'groupIds', [0, -1]),
5664  ];
5665  $requestHash = md5(serialize($parameters));
5666  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
5667  ->getQueryBuilderForTable('cache_treelist');
5668  $cacheEntry = $queryBuilder->select('treelist')
5669  ->from('cache_treelist')
5670  ->where(
5671  $queryBuilder->expr()->eq(
5672  'md5hash',
5673  $queryBuilder->createNamedParameter($requestHash, Connection::PARAM_STR)
5674  ),
5675  $queryBuilder->expr()->orX(
5676  $queryBuilder->expr()->gt(
5677  'expires',
5678  $queryBuilder->createNamedParameter(‪$GLOBALS['EXEC_TIME'], Connection::PARAM_INT)
5679  ),
5680  $queryBuilder->expr()->eq('expires', $queryBuilder->createNamedParameter(0, Connection::PARAM_INT))
5681  )
5682  )
5683  ->setMaxResults(1)
5684  ->executeQuery()
5685  ->fetchAssociative();
5686 
5687  if (is_array($cacheEntry)) {
5688  // Cache hit
5689  return $cacheEntry['treelist'];
5690  }
5691  // If Id less than zero it means we should add the real id to list:
5692  if ($id < 0) {
5693  $addId = $id = abs($id);
5694  }
5695  // Check start page:
5696  if ($tsfe->sys_page->getRawRecord('pages', $id, 'uid')) {
5697  // Find mount point if any:
5698  $mount_info = $tsfe->sys_page->getMountPointInfo($id);
5699  if (is_array($mount_info)) {
5700  $id = $mount_info['mount_pid'];
5701  // In Overlay mode, use the mounted page uid as added ID!:
5702  if ($addId && $mount_info['overlay']) {
5703  $addId = $id;
5704  }
5705  }
5706  } else {
5707  // Return blank if the start page was NOT found at all!
5708  return '';
5709  }
5710  }
5711  // Add this ID to the array of IDs
5712  if ($begin <= 0) {
5713  $prevId_array[] = $id;
5714  }
5715  // Select sublevel:
5716  if ($depth > 0) {
5717  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
5718  $queryBuilder->getRestrictions()
5719  ->removeAll()
5720  ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
5721  $queryBuilder->select(...GeneralUtility::trimExplode(',', $allFields, true))
5722  ->from('pages')
5723  ->where(
5724  $queryBuilder->expr()->eq(
5725  'pid',
5726  $queryBuilder->createNamedParameter($id, Connection::PARAM_INT)
5727  ),
5728  // tree is only built by language=0 pages
5729  $queryBuilder->expr()->eq('sys_language_uid', 0)
5730  )
5731  ->orderBy('sorting');
5732 
5733  if (!empty($moreWhereClauses)) {
5734  $queryBuilder->andWhere(QueryHelper::stripLogicalOperatorPrefix($moreWhereClauses));
5735  }
5736 
5737  $result = $queryBuilder->executeQuery();
5738  while ($row = $result->fetchAssociative()) {
5739  $versionState = VersionState::cast($row['t3ver_state']);
5740  $tsfe->sys_page->versionOL('pages', $row);
5741  if ($row === false
5742  || (int)$row['doktype'] === PageRepository::DOKTYPE_RECYCLER
5743  || (int)$row['doktype'] === PageRepository::DOKTYPE_BE_USER_SECTION
5744  || $versionState->indicatesPlaceholder()
5745  ) {
5746  // falsy row means Overlay prevents access to this page.
5747  // Doing this after the overlay to make sure changes
5748  // in the overlay are respected.
5749  // However, we do not process pages below of and
5750  // including of type recycler and BE user section
5751  continue;
5752  }
5753  // Find mount point if any:
5754  $next_id = $row['uid'];
5755  $mount_info = $tsfe->sys_page->getMountPointInfo($next_id, $row);
5756  // Overlay mode:
5757  if (is_array($mount_info) && $mount_info['overlay']) {
5758  $next_id = $mount_info['mount_pid'];
5759  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
5760  ->getQueryBuilderForTable('pages');
5761  $queryBuilder->getRestrictions()
5762  ->removeAll()
5763  ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
5764  $queryBuilder->select(...GeneralUtility::trimExplode(',', $allFields, true))
5765  ->from('pages')
5766  ->where(
5767  $queryBuilder->expr()->eq(
5768  'uid',
5769  $queryBuilder->createNamedParameter($next_id, Connection::PARAM_INT)
5770  )
5771  )
5772  ->orderBy('sorting')
5773  ->setMaxResults(1);
5774 
5775  if (!empty($moreWhereClauses)) {
5776  $queryBuilder->andWhere(QueryHelper::stripLogicalOperatorPrefix($moreWhereClauses));
5777  }
5778 
5779  $row = $queryBuilder->executeQuery()->fetchAssociative();
5780  $tsfe->sys_page->versionOL('pages', $row);
5781  if ((int)$row['doktype'] === PageRepository::DOKTYPE_RECYCLER
5782  || (int)$row['doktype'] === PageRepository::DOKTYPE_BE_USER_SECTION
5783  || $versionState->indicatesPlaceholder()
5784  ) {
5785  // Doing this after the overlay to make sure
5786  // changes in the overlay are respected.
5787  // see above
5788  continue;
5789  }
5790  }
5791  // Add record:
5792  if ($dontCheckEnableFields || $tsfe->checkPagerecordForIncludeSection($row)) {
5793  // Add ID to list:
5794  if ($begin <= 0) {
5795  if ($dontCheckEnableFields || $tsfe->checkEnableFields($row)) {
5796  $theList[] = $next_id;
5797  }
5798  }
5799  // Next level:
5800  if ($depth > 1 && !$row['php_tree_stop']) {
5801  // Normal mode:
5802  if (is_array($mount_info) && !$mount_info['overlay']) {
5803  $next_id = $mount_info['mount_pid'];
5804  }
5805  // Call recursively, if the id is not in prevID_array:
5806  if (!in_array($next_id, $prevId_array)) {
5807  $theList = array_merge(
5808  $theList,
5809  GeneralUtility::intExplode(
5810  ',',
5811  $this->getTreeList(
5812  $next_id,
5813  $depth - 1,
5814  $begin - 1,
5815  $dontCheckEnableFields,
5816  $addSelectFields,
5817  $moreWhereClauses,
5818  $prevId_array,
5819  $recursionLevel + 1
5820  ),
5821  true
5822  )
5823  );
5824  }
5825  }
5826  }
5827  }
5828  }
5829  // If first run, check if the ID should be returned:
5830  if (!$recursionLevel) {
5831  if ($addId) {
5832  if ($begin > 0) {
5833  $theList[] = 0;
5834  } else {
5835  $theList[] = $addId;
5836  }
5837  }
5838 
5839  $cacheEntry = [
5840  'md5hash' => $requestHash,
5841  'pid' => $id,
5842  'treelist' => implode(',', $theList),
5843  'tstamp' => ‪$GLOBALS['EXEC_TIME'],
5844  ];
5845 
5846  // Only add to cache if not logged into TYPO3 Backend
5847  if (!$this->getFrontendBackendUser() instanceof AbstractUserAuthentication) {
5848  $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('cache_treelist');
5849  try {
5850  $connection->transactional(static function ($connection) use ($cacheEntry) {
5851  $connection->insert('cache_treelist', $cacheEntry);
5852  });
5853  } catch (\Throwable $e) {
5854  }
5855  }
5856  }
5857 
5858  return implode(',', $theList);
5859  }
5860 
5870  public function searchWhere($searchWords, $searchFieldList, $searchTable)
5871  {
5872  if (!$searchWords) {
5873  return '';
5874  }
5875 
5876  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
5877  ->getQueryBuilderForTable($searchTable);
5878 
5879  $prefixTableName = $searchTable ? $searchTable . '.' : '';
5880 
5881  $where = $queryBuilder->expr()->andX();
5882  $searchFields = explode(',', $searchFieldList);
5883  $searchWords = preg_split('/[ ,]/', $searchWords);
5884  foreach ($searchWords as $searchWord) {
5885  $searchWord = trim($searchWord);
5886  if (strlen($searchWord) < 3) {
5887  continue;
5888  }
5889  $searchWordConstraint = $queryBuilder->expr()->orX();
5890  $searchWord = $queryBuilder->escapeLikeWildcards($searchWord);
5891  foreach ($searchFields as $field) {
5892  $searchWordConstraint->add(
5893  $queryBuilder->expr()->like($prefixTableName . $field, $queryBuilder->quote('%' . $searchWord . '%'))
5894  );
5895  }
5896 
5897  if ($searchWordConstraint->count()) {
5898  $where->add($searchWordConstraint);
5899  }
5900  }
5901 
5902  if ((string)$where === '') {
5903  return '';
5904  }
5905 
5906  return ' AND (' . (string)$where . ')';
5907  }
5908 
5918  public function exec_getQuery($table, $conf)
5919  {
5920  $statement = $this->getQuery($table, $conf);
5921  $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
5922 
5923  return $connection->executeQuery($statement);
5924  }
5925 
5935  public function getRecords($tableName, array $queryConfiguration)
5936  {
5937  $records = [];
5938 
5939  $statement = $this->exec_getQuery($tableName, $queryConfiguration);
5940 
5941  $tsfe = $this->getTypoScriptFrontendController();
5942  while ($row = $statement->fetchAssociative()) {
5943  // Versioning preview:
5944  $tsfe->sys_page->versionOL($tableName, $row, true);
5945 
5946  // Language overlay:
5947  if (is_array($row)) {
5948  $row = $tsfe->sys_page->getLanguageOverlay($tableName, $row);
5949  }
5950 
5951  // Might be unset in the language overlay
5952  if (is_array($row)) {
5953  $records[] = $row;
5954  }
5955  }
5956 
5957  return $records;
5958  }
5959 
5973  public function getQuery($table, $conf, $returnQueryArray = false)
5974  {
5975  // Resolve stdWrap in these properties first
5976  $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
5977  $properties = [
5978  'pidInList',
5979  'uidInList',
5980  'languageField',
5981  'selectFields',
5982  'max',
5983  'begin',
5984  'groupBy',
5985  'orderBy',
5986  'join',
5987  'leftjoin',
5988  'rightjoin',
5989  'recursive',
5990  'where',
5991  ];
5992  foreach ($properties as $property) {
5993  $conf[$property] = trim(
5994  isset($conf[$property . '.'])
5995  ? (string)$this->stdWrap($conf[$property] ?? '', $conf[$property . '.'] ?? [])
5996  : (string)($conf[$property] ?? '')
5997  );
5998  if ($conf[$property] === '') {
5999  unset($conf[$property]);
6000  } elseif (in_array($property, ['languageField', 'selectFields', 'join', 'leftjoin', 'rightjoin', 'where'], true)) {
6001  $conf[$property] = QueryHelper::quoteDatabaseIdentifiers($connection, $conf[$property]);
6002  }
6003  if (isset($conf[$property . '.'])) {
6004  // stdWrapping already done, so remove the sub-array
6005  unset($conf[$property . '.']);
6006  }
6007  }
6008  // Handle PDO-style named parameter markers first
6009  $queryMarkers = $this->getQueryMarkers($table, $conf);
6010  // Replace the markers in the non-stdWrap properties
6011  foreach ($queryMarkers as $marker => $markerValue) {
6012  $properties = [
6013  'uidInList',
6014  'selectFields',
6015  'where',
6016  'max',
6017  'begin',
6018  'groupBy',
6019  'orderBy',
6020  'join',
6021  'leftjoin',
6022  'rightjoin',
6023  ];
6024  foreach ($properties as $property) {
6025  if ($conf[$property] ?? false) {
6026  $conf[$property] = str_replace('###' . $marker . '###', $markerValue, $conf[$property]);
6027  }
6028  }
6029  }
6030 
6031  // Construct WHERE clause:
6032  // Handle recursive function for the pidInList
6033  if (isset($conf['recursive'])) {
6034  $conf['recursive'] = (int)$conf['recursive'];
6035  if ($conf['recursive'] > 0) {
6036  $pidList = GeneralUtility::trimExplode(',', $conf['pidInList'], true);
6037  array_walk($pidList, function (&$storagePid) {
6038  if ($storagePid === 'this') {
6039  $storagePid = $this->getTypoScriptFrontendController()->id;
6040  }
6041  if (MathUtility::canBeInterpretedAsInteger($storagePid) && $storagePid > 0) {
6042  $storagePid = -$storagePid;
6043  }
6044  });
6045  $expandedPidList = [];
6046  foreach ($pidList as $value) {
6047  // Implementation of getTreeList allows to pass the id negative to include
6048  // it into the result otherwise only childpages are returned
6049  $expandedPidList = array_merge(
6050  GeneralUtility::intExplode(',', $this->getTreeList((int)$value, (int)($conf['recursive'] ?? 0))),
6051  $expandedPidList
6052  );
6053  }
6054  $conf['pidInList'] = implode(',', $expandedPidList);
6055  }
6056  }
6057  if ((string)($conf['pidInList'] ?? '') === '') {
6058  $conf['pidInList'] = 'this';
6059  }
6060 
6061  $queryParts = $this->getQueryConstraints($table, $conf);
6062 
6063  $queryBuilder = $connection->createQueryBuilder();
6064  // @todo Check against getQueryConstraints, can probably use FrontendRestrictions
6065  // @todo here and remove enableFields there.
6066  $queryBuilder->getRestrictions()->removeAll();
6067  $queryBuilder->select('*')->from($table);
6068 
6069  if ($queryParts['where'] ?? false) {
6070  $queryBuilder->where($queryParts['where']);
6071  }
6072 
6073  if ($queryParts['groupBy'] ?? false) {
6074  $queryBuilder->groupBy(...$queryParts['groupBy']);
6075  }
6076 
6077  if (is_array($queryParts['orderBy'] ?? false)) {
6078  foreach ($queryParts['orderBy'] as $orderBy) {
6079  $queryBuilder->addOrderBy(...$orderBy);
6080  }
6081  }
6082 
6083  // Fields:
6084  if ($conf['selectFields'] ?? false) {
6085  $queryBuilder->selectLiteral($this->sanitizeSelectPart($conf['selectFields'], $table));
6086  }
6087 
6088  // Setting LIMIT:
6089  $error = false;
6090  if (($conf['max'] ?? false) || ($conf['begin'] ?? false)) {
6091  // Finding the total number of records, if used:
6092  if (str_contains(strtolower(($conf['begin'] ?? '') . ($conf['max'] ?? '')), 'total')) {
6093  $countQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
6094  $countQueryBuilder->getRestrictions()->removeAll();
6095  $countQueryBuilder->count('*')
6096  ->from($table)
6097  ->where($queryParts['where']);
6098 
6099  if ($queryParts['groupBy']) {
6100  $countQueryBuilder->groupBy(...$queryParts['groupBy']);
6101  }
6102 
6103  try {
6104  $count = $countQueryBuilder->executeQuery()->fetchOne();
6105  if (isset($conf['max'])) {
6106  $conf['max'] = str_ireplace('total', $count, (string)$conf['max']);
6107  }
6108  if (isset($conf['begin'])) {
6109  $conf['begin'] = str_ireplace('total', $count, (string)$conf['begin']);
6110  }
6111  } catch (DBALException $e) {
6112  $this->getTimeTracker()->setTSlogMessage($e->getPrevious()->getMessage());
6113  $error = true;
6114  }
6115  }
6116 
6117  if (!$error) {
6118  if (isset($conf['begin']) && $conf['begin'] > 0) {
6119  $conf['begin'] = MathUtility::forceIntegerInRange((int)ceil($this->calc($conf['begin'])), 0);
6120  $queryBuilder->setFirstResult($conf['begin']);
6121  }
6122  if (isset($conf['max'])) {
6123  $conf['max'] = MathUtility::forceIntegerInRange((int)ceil($this->calc($conf['max'])), 0);
6124  $queryBuilder->setMaxResults($conf['max'] ?: 100000);
6125  }
6126  }
6127  }
6128 
6129  if (!$error) {
6130  // Setting up tablejoins:
6131  if ($conf['join'] ?? false) {
6132  $joinParts = QueryHelper::parseJoin($conf['join']);
6133  $queryBuilder->join(
6134  $table,
6135  $joinParts['tableName'],
6136  $joinParts['tableAlias'],
6137  $joinParts['joinCondition']
6138  );
6139  } elseif ($conf['leftjoin'] ?? false) {
6140  $joinParts = QueryHelper::parseJoin($conf['leftjoin']);
6141  $queryBuilder->leftJoin(
6142  $table,
6143  $joinParts['tableName'],
6144  $joinParts['tableAlias'],
6145  $joinParts['joinCondition']
6146  );
6147  } elseif ($conf['rightjoin'] ?? false) {
6148  $joinParts = QueryHelper::parseJoin($conf['rightjoin']);
6149  $queryBuilder->rightJoin(
6150  $table,
6151  $joinParts['tableName'],
6152  $joinParts['tableAlias'],
6153  $joinParts['joinCondition']
6154  );
6155  }
6156 
6157  // Convert the QueryBuilder object into a SQL statement.
6158  $query = $queryBuilder->getSQL();
6159 
6160  // Replace the markers in the queryParts to handle stdWrap enabled properties
6161  foreach ($queryMarkers as $marker => $markerValue) {
6162  // @todo Ugly hack that needs to be cleaned up, with the current architecture
6163  // @todo for exec_Query / getQuery it's the best we can do.
6164  $query = str_replace('###' . $marker . '###', $markerValue, $query);
6165  }
6166 
6167  return $returnQueryArray ? $this->getQueryArray($queryBuilder) : $query;
6168  }
6169 
6170  return '';
6171  }
6172 
6181  protected function getQueryArray(QueryBuilder $queryBuilder)
6182  {
6183  $fromClauses = [];
6184  $knownAliases = [];
6185  $queryParts = [];
6186 
6187  // Loop through all FROM clauses
6188  foreach ($queryBuilder->getQueryPart('from') as $from) {
6189  if ($from['alias'] === null) {
6190  $tableSql = $from['table'];
6191  $tableReference = $from['table'];
6192  } else {
6193  $tableSql = $from['table'] . ' ' . $from['alias'];
6194  $tableReference = $from['alias'];
6195  }
6196 
6197  $knownAliases[$tableReference] = true;
6198 
6199  $fromClauses[$tableReference] = $tableSql . $this->getQueryArrayJoinHelper(
6200  $tableReference,
6201  $queryBuilder->getQueryPart('join'),
6202  $knownAliases
6203  );
6204  }
6205 
6206  $queryParts['SELECT'] = implode(', ', $queryBuilder->getQueryPart('select'));
6207  $queryParts['FROM'] = implode(', ', $fromClauses);
6208  $queryParts['WHERE'] = (string)$queryBuilder->getQueryPart('where') ?: '';
6209  $queryParts['GROUPBY'] = implode(', ', $queryBuilder->getQueryPart('groupBy'));
6210  $queryParts['ORDERBY'] = implode(', ', $queryBuilder->getQueryPart('orderBy'));
6211  if ($queryBuilder->getFirstResult() > 0) {
6212  $queryParts['LIMIT'] = $queryBuilder->getFirstResult() . ',' . $queryBuilder->getMaxResults();
6213  } elseif ($queryBuilder->getMaxResults() > 0) {
6214  $queryParts['LIMIT'] = $queryBuilder->getMaxResults();
6215  }
6216 
6217  return $queryParts;
6218  }
6219 
6229  protected function getQueryArrayJoinHelper(string $fromAlias, array $joinParts, array &$knownAliases): string
6230  {
6231  $sql = '';
6232 
6233  if (isset($joinParts['join'][$fromAlias])) {
6234  foreach ($joinParts['join'][$fromAlias] as $join) {
6235  if (array_key_exists($join['joinAlias'], $knownAliases)) {
6236  throw new \RuntimeException(
6237  'Non unique join alias: "' . $join['joinAlias'] . '" found.',
6238  1472748872
6239  );
6240  }
6241  $sql .= ' ' . strtoupper($join['joinType'])
6242  . ' JOIN ' . $join['joinTable'] . ' ' . $join['joinAlias']
6243  . ' ON ' . ((string)$join['joinCondition']);
6244  $knownAliases[$join['joinAlias']] = true;
6245  }
6246 
6247  foreach ($joinParts['join'][$fromAlias] as $join) {
6248  $sql .= $this->getQueryArrayJoinHelper($join['joinAlias'], $joinParts, $knownAliases);
6249  }
6250  }
6251 
6252  return $sql;
6253  }
6263  protected function getQueryConstraints(string $table, array $conf): array
6264  {
6265  // Init:
6266  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
6267  $expressionBuilder = $queryBuilder->expr();
6268  $tsfe = $this->getTypoScriptFrontendController();
6269  $constraints = [];
6270  $pid_uid_flag = 0;
6271  $enableFieldsIgnore = [];
6272  $queryParts = [
6273  'where' => null,
6274  'groupBy' => null,
6275  'orderBy' => null,
6276  ];
6277 
6278  $isInWorkspace = GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('workspace', 'isOffline');
6279  $considerMovePointers = (
6280  $isInWorkspace && $table !== 'pages'
6281  && !empty(‪$GLOBALS['TCA'][$table]['ctrl']['versioningWS'])
6282  );
6283 
6284  if (trim($conf['uidInList'] ?? '')) {
6285  $listArr = GeneralUtility::intExplode(',', str_replace('this', (string)$tsfe->contentPid, $conf['uidInList']));
6286 
6287  // If moved records shall be considered, select via t3ver_oid
6288  if ($considerMovePointers) {
6289  $constraints[] = (string)$expressionBuilder->orX(
6290  $expressionBuilder->in($table . '.uid', $listArr),
6291  $expressionBuilder->andX(
6292  $expressionBuilder->eq(
6293  $table . '.t3ver_state',
6294  (int)(string)VersionState::cast(VersionState::MOVE_POINTER)
6295  ),
6296  $expressionBuilder->in($table . '.t3ver_oid', $listArr)
6297  )
6298  );
6299  } else {
6300  $constraints[] = (string)$expressionBuilder->in($table . '.uid', $listArr);
6301  }
6302  $pid_uid_flag++;
6303  }
6304 
6305  // Static_* tables are allowed to be fetched from root page
6306  if (strpos($table, 'static_') === 0) {
6307  $pid_uid_flag++;
6308  }
6309 
6310  if (trim($conf['pidInList'])) {
6311  $listArr = GeneralUtility::intExplode(',', str_replace('this', (string)$tsfe->contentPid, $conf['pidInList']));
6312  // Removes all pages which are not visible for the user!
6313  $listArr = $this->checkPidArray($listArr);
6314  if (GeneralUtility::inList($conf['pidInList'], 'root')) {
6315  $listArr[] = 0;
6316  }
6317  if (GeneralUtility::inList($conf['pidInList'], '-1')) {
6318  $listArr[] = -1;
6319  $enableFieldsIgnore['pid'] = true;
6320  }
6321  if (!empty($listArr)) {
6322  $constraints[] = $expressionBuilder->in($table . '.pid', array_map('intval', $listArr));
6323  $pid_uid_flag++;
6324  } else {
6325  // If not uid and not pid then uid is set to 0 - which results in nothing!!
6326  $pid_uid_flag = 0;
6327  }
6328  }
6329 
6330  // If not uid and not pid then uid is set to 0 - which results in nothing!!
6331  if (!$pid_uid_flag) {
6332  $constraints[] = $expressionBuilder->eq($table . '.uid', 0);
6333  }
6334 
6335  $where = trim((string)$this->stdWrapValue('where', $conf ?? []));
6336  if ($where) {
6337  $constraints[] = QueryHelper::stripLogicalOperatorPrefix($where);
6338  }
6339 
6340  // Check if the default language should be fetched (= doing overlays), or if only the records of a language should be fetched
6341  // but only do this for TCA tables that have languages enabled
6342  $languageConstraint = $this->getLanguageRestriction($expressionBuilder, $table, $conf, GeneralUtility::makeInstance(Context::class));
6343  if ($languageConstraint !== null) {
6344  $constraints[] = $languageConstraint;
6345  }
6346 
6347  // Enablefields
6348  if ($table === 'pages') {
6349  $constraints[] = QueryHelper::stripLogicalOperatorPrefix($tsfe->sys_page->where_hid_del);
6350  $constraints[] = QueryHelper::stripLogicalOperatorPrefix($tsfe->sys_page->where_groupAccess);
6351  } else {
6352  $constraints[] = QueryHelper::stripLogicalOperatorPrefix($tsfe->sys_page->enableFields($table, -1, $enableFieldsIgnore));
6353  }
6354 
6355  // MAKE WHERE:
6356  if (count($constraints) !== 0) {
6357  $queryParts['where'] = $expressionBuilder->andX(...$constraints);
6358  }
6359  // GROUP BY
6360  $groupBy = trim((string)$this->stdWrapValue('groupBy', $conf ?? []));
6361  if ($groupBy) {
6362  $queryParts['groupBy'] = QueryHelper::parseGroupBy($groupBy);
6363  }
6364 
6365  // ORDER BY
6366  $orderByString = trim((string)$this->stdWrapValue('orderBy', $conf ?? []));
6367  if ($orderByString) {
6368  $queryParts['orderBy'] = QueryHelper::parseOrderBy($orderByString);
6369  }
6370 
6371  // Return result:
6372  return $queryParts;
6373  }
6374 
6401  protected function getLanguageRestriction(ExpressionBuilder $expressionBuilder, string $table, array $conf, Context $context)
6402  {
6403  $languageField = '';
6404  $localizationParentField = ‪$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] ?? null;
6405  // Check if the table is translatable, and set the language field by default from the TCA information
6406  if (!empty($conf['languageField']) || !isset($conf['languageField'])) {
6407  if (isset($conf['languageField']) && !empty(‪$GLOBALS['TCA'][$table]['columns'][$conf['languageField']])) {
6408  $languageField = $conf['languageField'];
6409  } elseif (!empty(‪$GLOBALS['TCA'][$table]['ctrl']['languageField']) && !empty($localizationParentField)) {
6410  $languageField = $table . '.' . ‪$GLOBALS['TCA'][$table]['ctrl']['languageField'];
6411  }
6412  }
6413 
6414  // No language restriction enabled explicitly or available via TCA
6415  if (empty($languageField)) {
6416  return null;
6417  }
6418 
6420  $languageAspect = $context->getAspect('language');
6421  if ($languageAspect->doOverlays() && !empty($localizationParentField)) {
6422  // Sys language content is set to zero/-1 - and it is expected that whatever routine processes the output will
6423  // OVERLAY the records with localized versions!
6424  $languageQuery = $expressionBuilder->in($languageField, [0, -1]);
6425  // Use this option to include records that don't have a default language counterpart ("free mode")
6426  // (originalpointerfield is 0 and the language field contains the requested language)
6427  if (isset($conf['includeRecordsWithoutDefaultTranslation']) || !empty($conf['includeRecordsWithoutDefaultTranslation.'])) {
6428  $includeRecordsWithoutDefaultTranslation = isset($conf['includeRecordsWithoutDefaultTranslation.'])
6429  ? $this->stdWrap($conf['includeRecordsWithoutDefaultTranslation'], $conf['includeRecordsWithoutDefaultTranslation.'])
6430  : $conf['includeRecordsWithoutDefaultTranslation'];
6431  $includeRecordsWithoutDefaultTranslation = trim($includeRecordsWithoutDefaultTranslation) !== '';
6432  } else {
6433  // Option was not explicitly set, check what's in for the language overlay type.
6434  $includeRecordsWithoutDefaultTranslation = $languageAspect->getOverlayType() === $languageAspect::OVERLAYS_ON_WITH_FLOATING;
6435  }
6436  if ($includeRecordsWithoutDefaultTranslation) {
6437  $languageQuery = $expressionBuilder->orX(
6438  $languageQuery,
6439  $expressionBuilder->andX(
6440  $expressionBuilder->eq($table . '.' . $localizationParentField, 0),
6441  $expressionBuilder->eq($languageField, $languageAspect->getContentId())
6442  )
6443  );
6444  }
6445  return $languageQuery;
6446  }
6447  // No overlays = only fetch records given for the requested language and "all languages"
6448  return $expressionBuilder->in($languageField, [$languageAspect->getContentId(), -1]);
6449  }
6450 
6463  protected function sanitizeSelectPart($selectPart, $table)
6464  {
6465  $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
6466 
6467  // Pattern matching parts
6468  $matchStart = '/(^\\s*|,\\s*|' . $table . '\\.)';
6469  $matchEnd = '(\\s*,|\\s*$)/';
6470  $necessaryFields = ['uid', 'pid'];
6471  $wsFields = ['t3ver_state'];
6472  $languageField = ‪$GLOBALS['TCA'][$table]['ctrl']['languageField'] ?? false;
6473  if (isset(‪$GLOBALS['TCA'][$table]) && !preg_match($matchStart . '\\*' . $matchEnd, $selectPart) && !preg_match('/(count|max|min|avg|sum)\\([^\\)]+\\)|distinct/i', $selectPart)) {
6474  foreach ($necessaryFields as $field) {
6475  $match = $matchStart . $field . $matchEnd;
6476  if (!preg_match($match, $selectPart)) {
6477  $selectPart .= ', ' . $connection->quoteIdentifier($table . '.' . $field) . ' AS ' . $connection->quoteIdentifier($field);
6478  }
6479  }
6480  if (is_string($languageField)) {
6481  $match = $matchStart . $languageField . $matchEnd;
6482  if (!preg_match($match, $selectPart)) {
6483  $selectPart .= ', ' . $connection->quoteIdentifier($table . '.' . $languageField) . ' AS ' . $connection->quoteIdentifier($languageField);
6484  }
6485  }
6486  if (‪$GLOBALS['TCA'][$table]['ctrl']['versioningWS'] ?? false) {
6487  foreach ($wsFields as $field) {
6488  $match = $matchStart . $field . $matchEnd;
6489  if (!preg_match($match, $selectPart)) {
6490  $selectPart .= ', ' . $connection->quoteIdentifier($table . '.' . $field) . ' AS ' . $connection->quoteIdentifier($field);
6491  }
6492  }
6493  }
6494  }
6495  return $selectPart;
6496  }
6497 
6505  public function checkPidArray($pageIds)
6506  {
6507  if (!is_array($pageIds) || empty($pageIds)) {
6508  return [];
6509  }
6510  $restrictionContainer = GeneralUtility::makeInstance(FrontendRestrictionContainer::class);
6511  $restrictionContainer->add(GeneralUtility::makeInstance(
6512  DocumentTypeExclusionRestriction::class,
6513  GeneralUtility::intExplode(',', (string)$this->checkPid_badDoktypeList, true)
6514  ));
6515  return $this->getTypoScriptFrontendController()->sys_page->filterAccessiblePageIds($pageIds, $restrictionContainer);
6516  }
6517 
6528  public function getQueryMarkers($table, $conf)
6529  {
6530  if (!isset($conf['markers.']) || !is_array($conf['markers.'])) {
6531  return [];
6532  }
6533  // Parse markers and prepare their values
6534  $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
6535  $markerValues = [];
6536  foreach ($conf['markers.'] as $dottedMarker => $dummy) {
6537  $marker = rtrim($dottedMarker, '.');
6538  if ($dottedMarker != $marker . '.') {
6539  continue;
6540  }
6541  // Parse definition
6542  // todo else value is always null
6543  $tempValue = isset($conf['markers.'][$dottedMarker])
6544  ? $this->stdWrap($conf['markers.'][$dottedMarker]['value'] ?? '', $conf['markers.'][$dottedMarker])
6545  : $conf['markers.'][$dottedMarker]['value'];
6546  // Quote/escape if needed
6547  if (is_numeric($tempValue)) {
6548  if ((int)$tempValue == $tempValue) {
6549  // Handle integer
6550  $markerValues[$marker] = (int)$tempValue;
6551  } else {
6552  // Handle float
6553  $markerValues[$marker] = (float)$tempValue;
6554  }
6555  } elseif ($tempValue === null) {
6556  // It represents NULL
6557  $markerValues[$marker] = 'NULL';
6558  } elseif (!empty($conf['markers.'][$dottedMarker]['commaSeparatedList'])) {
6559  // See if it is really a comma separated list of values
6560  $explodeValues = GeneralUtility::trimExplode(',', $tempValue);
6561  if (count($explodeValues) > 1) {
6562  // Handle each element of list separately
6563  $tempArray = [];
6564  foreach ($explodeValues as $listValue) {
6565  if (is_numeric($listValue)) {
6566  if ((int)$listValue == $listValue) {
6567  $tempArray[] = (int)$listValue;
6568  } else {
6569  $tempArray[] = (float)$listValue;
6570  }
6571  } else {
6572  // If quoted, remove quotes before
6573  // escaping.
6574  if (preg_match('/^\'([^\']*)\'$/', $listValue, $matches)) {
6575  $listValue = $matches[1];
6576  } elseif (preg_match('/^\\"([^\\"]*)\\"$/', $listValue, $matches)) {
6577  $listValue = $matches[1];
6578  }
6579  $tempArray[] = $connection->quote($listValue);
6580  }
6581  }
6582  $markerValues[$marker] = implode(',', $tempArray);
6583  } else {
6584  // Handle remaining values as string
6585  $markerValues[$marker] = $connection->quote($tempValue);
6586  }
6587  } else {
6588  // Handle remaining values as string
6589  $markerValues[$marker] = $connection->quote($tempValue);
6590  }
6591  }
6592  return $markerValues;
6593  }
6594 
6595  /***********************************************
6596  *
6597  * Frontend editing functions
6598  *
6599  ***********************************************/
6612  public function editPanel($content, $conf, $currentRecord = '', $dataArray = [])
6613  {
6614  if (!$this->getTypoScriptFrontendController()->isBackendUserLoggedIn()) {
6615  return $content;
6616  }
6617  if (!$this->getTypoScriptFrontendController()->displayEditIcons) {
6618  return $content;
6619  }
6620 
6621  if (!$currentRecord) {
6622  $currentRecord = $this->currentRecord;
6623  }
6624  if (empty($dataArray)) {
6625  $dataArray = $this->data;
6626  }
6627 
6628  if ($conf['newRecordFromTable']) {
6629  $currentRecord = $conf['newRecordFromTable'] . ':NEW';
6630  $conf['allow'] = 'new';
6631  $checkEditAccessInternals = false;
6632  } else {
6633  $checkEditAccessInternals = true;
6634  }
6635  [$table, $uid] = explode(':', $currentRecord);
6636  // Page ID for new records, 0 if not specified
6637  $newRecordPid = (int)$conf['newRecordInPid'];
6638  $newUid = null;
6639  if (!$conf['onlyCurrentPid'] || $dataArray['pid'] == $this->getTypoScriptFrontendController()->id) {
6640  if ($table === 'pages') {
6641  $newUid = $uid;
6642  } else {
6643  if ($conf['newRecordFromTable']) {
6644  $newUid = $this->getTypoScriptFrontendController()->id;
6645  if ($newRecordPid) {
6646  $newUid = $newRecordPid;
6647  }
6648  } else {
6649  $newUid = -1 * $uid;
6650  }
6651  }
6652  }
6653  if ($table && $this->getFrontendBackendUser()->allowedToEdit($table, $dataArray, $conf, $checkEditAccessInternals) && $this->getFrontendBackendUser()->allowedToEditLanguage($table, $dataArray)) {
6654  $editClass = ‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/classes/class.frontendedit.php']['edit'];
6655  if ($editClass) {
6656  trigger_error('Hook "typo3/classes/class.frontendedit.php" is deprecated together with stdWrap.editPanel and will be removed in TYPO3 12.0.', E_USER_DEPRECATED);
6657  $edit = GeneralUtility::makeInstance($editClass);
6658  $allowedActions = $this->getFrontendBackendUser()->getAllowedEditActions($table, $conf, $dataArray['pid']);
6659  $content = $edit->editPanel($content, $conf, $currentRecord, $dataArray, $table, $allowedActions, $newUid, []);
6660  }
6661  }
6662  return $content;
6663  }
6664 
6678  public function editIcons($content, $params, array $conf = [], $currentRecord = '', $dataArray = [], $addUrlParamStr = '')
6679  {
6680  if (!$this->getTypoScriptFrontendController()->isBackendUserLoggedIn()) {
6681  return $content;
6682  }
6683  if (!$this->getTypoScriptFrontendController()->displayFieldEditIcons) {
6684  return $content;
6685  }
6686  if (!$currentRecord) {
6687  $currentRecord = $this->currentRecord;
6688  }
6689  if (empty($dataArray)) {
6690  $dataArray = $this->data;
6691  }
6692  // Check incoming params:
6693  [$currentRecordTable, $currentRecordUID] = explode(':', $currentRecord);
6694  [$fieldList, $table] = array_reverse(GeneralUtility::trimExplode(':', $params, true));
6695  // Reverse the array because table is optional
6696  if (!$table) {
6697  $table = $currentRecordTable;
6698  } elseif ($table != $currentRecordTable) {
6699  // If the table is set as the first parameter, and does not match the table of the current record, then just return.
6700  return $content;
6701  }
6702 
6703  $editUid = $dataArray['_LOCALIZED_UID'] ?? $currentRecordUID;
6704  // Edit icons imply that the editing action is generally allowed, assuming page and content element permissions permit it.
6705  if (!array_key_exists('allow', $conf)) {
6706  $conf['allow'] = 'edit';
6707  }
6708  if ($table && $this->getFrontendBackendUser()->allowedToEdit($table, $dataArray, $conf, true) && $fieldList && $this->getFrontendBackendUser()->allowedToEditLanguage($table, $dataArray)) {
6709  $editClass = ‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/classes/class.frontendedit.php']['edit'];
6710  if ($editClass) {
6711  trigger_error('Hook "typo3/classes/class.frontendedit.php" is deprecated together with stdWrap.editIcons and will be removed in TYPO3 12.0.', E_USER_DEPRECATED);
6712  $edit = GeneralUtility::makeInstance($editClass);
6713  $content = $edit->editIcons($content, $params, $conf, $currentRecord, $dataArray, $addUrlParamStr, $table, $editUid, $fieldList);
6714  }
6715  }
6716  return $content;
6717  }
6718 
6728  public function isDisabled($table, $row)
6729  {
6730  trigger_error('Method ' . __METHOD__ . ' is deprecated and will be removed in TYPO3 12.0.', E_USER_DEPRECATED);
6731  $tsfe = $this->getTypoScriptFrontendController();
6732  $enablecolumns = ‪$GLOBALS['TCA'][$table]['ctrl']['enablecolumns'];
6733  return $enablecolumns['disabled'] && $row[$enablecolumns['disabled']]
6734  || $enablecolumns['fe_group'] && $tsfe->simUserGroup && (int)$row[$enablecolumns['fe_group']] === (int)$tsfe->simUserGroup
6735  || $enablecolumns['starttime'] && $row[$enablecolumns['starttime']] > ‪$GLOBALS['EXEC_TIME']
6736  || $enablecolumns['endtime'] && $row[$enablecolumns['endtime']] && $row[$enablecolumns['endtime']] < ‪$GLOBALS['EXEC_TIME'];
6737  }
6738 
6744  protected function getResourceFactory()
6745  {
6746  return GeneralUtility::makeInstance(ResourceFactory::class);
6747  }
6748 
6756  protected function getEnvironmentVariable($key)
6757  {
6758  if ($key === 'REQUEST_URI') {
6759  return $this->getRequest()->getAttribute('normalizedParams')->getRequestUri();
6760  }
6761  return GeneralUtility::getIndpEnv($key);
6762  }
6763 
6771  protected function getFromCache(array $configuration)
6772  {
6773  $content = false;
6774 
6775  if ($this->getTypoScriptFrontendController()->no_cache) {
6776  return $content;
6777  }
6778  $cacheKey = $this->calculateCacheKey($configuration);
6779  if (!empty($cacheKey)) {
6780  $cacheFrontend = GeneralUtility::makeInstance(CacheManager::class)->getCache('hash');
6781  $content = $cacheFrontend->get($cacheKey);
6782  }
6783  return $content;
6784  }
6785 
6792  protected function calculateCacheLifetime(array $configuration)
6793  {
6794  $configuration['lifetime'] = $configuration['lifetime'] ?? '';
6795  $lifetimeConfiguration = (string)$this->stdWrapValue('lifetime', $configuration);
6796 
6797  $lifetime = null; // default lifetime
6798  if (strtolower($lifetimeConfiguration) === 'unlimited') {
6799  $lifetime = 0; // unlimited
6800  } elseif ($lifetimeConfiguration > 0) {
6801  $lifetime = (int)$lifetimeConfiguration; // lifetime in seconds
6802  }
6803  return $lifetime;
6804  }
6805 
6812  protected function calculateCacheTags(array $configuration)
6813  {
6814  $configuration['tags'] = $configuration['tags'] ?? '';
6815  $tags = (string)$this->stdWrapValue('tags', $configuration);
6816  return empty($tags) ? [] : GeneralUtility::trimExplode(',', $tags);
6817  }
6818 
6825  protected function calculateCacheKey(array $configuration)
6826  {
6827  $configuration['key'] = $configuration['key'] ?? '';
6828  return $this->stdWrapValue('key', $configuration);
6829  }
6830 
6836  protected function getFrontendBackendUser()
6837  {
6838  return ‪$GLOBALS['BE_USER'];
6839  }
6840 
6844  protected function getTimeTracker()
6845  {
6846  return GeneralUtility::makeInstance(TimeTracker::class);
6847  }
6848 
6853  public function getTypoScriptFrontendController()
6854  {
6855  return $this->typoScriptFrontendController ?: ‪$GLOBALS['TSFE'] ?? null;
6856  }
6857 
6866  protected function getContentLengthOfCurrentTag(string $theValue, int $pointer, string $currentTag): int
6867  {
6868  $tempContent = strtolower(substr($theValue, $pointer));
6869  $startTag = '<' . $currentTag;
6870  $endTag = '</' . $currentTag . '>';
6871  $offsetCount = 0;
6872 
6873  // Take care for nested tags
6874  do {
6875  $nextMatchingEndTagPosition = strpos($tempContent, $endTag);
6876  // only match tag `a` in `<a href"...">` but not in `<abbr>`
6877  $nextSameTypeTagPosition = preg_match(
6878  '#' . $startTag . '[\s/>]#',
6879  $tempContent,
6880  $nextSameStartTagMatches,
6881  PREG_OFFSET_CAPTURE
6882  ) ? $nextSameStartTagMatches[0][1] : false;
6883 
6884  // filter out nested tag contents to help getting the correct closing tag
6885  if ($nextMatchingEndTagPosition !== false && $nextSameTypeTagPosition !== false && $nextSameTypeTagPosition < $nextMatchingEndTagPosition) {
6886  $lastOpeningTagStartPosition = (int)strrpos(substr($tempContent, 0, $nextMatchingEndTagPosition), $startTag);
6887  $closingTagEndPosition = $nextMatchingEndTagPosition + strlen($endTag);
6888  $offsetCount += $closingTagEndPosition - $lastOpeningTagStartPosition;
6889 
6890  // replace content from latest tag start to latest tag end
6891  $tempContent = substr($tempContent, 0, $lastOpeningTagStartPosition) . substr($tempContent, $closingTagEndPosition);
6892  }
6893  } while (
6894  ($nextMatchingEndTagPosition !== false && $nextSameTypeTagPosition !== false) &&
6895  $nextSameTypeTagPosition < $nextMatchingEndTagPosition
6896  );
6897 
6898  // if no closing tag is found we use length of the whole content
6899  $endingOffset = strlen($tempContent);
6900  if ($nextMatchingEndTagPosition !== false) {
6901  $endingOffset = $nextMatchingEndTagPosition + $offsetCount;
6902  }
6903 
6904  return $endingOffset;
6905  }
6906 
6907  protected function shallDebug(): bool
6908  {
6909  $tsfe = $this->getTypoScriptFrontendController();
6910  if ($tsfe !== null && isset($tsfe->config['config']['debug'])) {
6911  return (bool)($tsfe->config['config']['debug']);
6912  }
6913  return !empty(‪$GLOBALS['TYPO3_CONF_VARS']['FE']['debug']);
6914  }
6915 
6916  public function getRequest(): ServerRequestInterface
6917  {
6918  if ($this->request instanceof ServerRequestInterface) {
6919  return $this->request;
6920  }
6921 
6922  if (isset(‪$GLOBALS['TYPO3_REQUEST']) && ‪$GLOBALS['TYPO3_REQUEST'] instanceof ServerRequestInterface) {
6923  return ‪$GLOBALS['TYPO3_REQUEST'];
6924  }
6925 
6926  throw new ContentRenderingException('PSR-7 request is missing in ContentObjectRenderer. Inject with start(), setRequest() or provide via $GLOBALS[\'TYPO3_REQUEST\'].', 1607172972);
6927  }
6928 }
‪TYPO3\CMS\Core\Utility\GeneralUtility\trimExplode
‪static list< string > trimExplode($delim, $string, $removeEmptyValues=false, $limit=0)
Definition: GeneralUtility.php:999
‪TYPO3\CMS\Core\Utility\GeneralUtility\xml2array
‪static mixed xml2array($string, $NSprefix='', $reportDocTag=false)
Definition: GeneralUtility.php:1482
‪TYPO3\CMS\Core\Database\Query\Restriction\DocumentTypeExclusionRestriction
Definition: DocumentTypeExclusionRestriction.php:27
‪TYPO3\CMS\Core\Utility\GeneralUtility\revExplode
‪static list< string > revExplode($delimiter, $string, $count=0)
Definition: GeneralUtility.php:964
‪TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder
Definition: ExpressionBuilder.php:36
‪TYPO3\CMS\Core\Utility\MathUtility\canBeInterpretedAsInteger
‪static bool canBeInterpretedAsInteger($var)
Definition: MathUtility.php:74
‪TYPO3\CMS\Core\Resource\FileInterface
Definition: FileInterface.php:22
‪TYPO3\CMS\Core\Html\HtmlParser
Definition: HtmlParser.php:27
‪TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser
Definition: TypoScriptParser.php:36
‪TYPO3\CMS\Core\Imaging\ImageManipulation\Area
Definition: Area.php:23
‪TYPO3\CMS\Core\Resource\ProcessedFile\CONTEXT_IMAGECROPSCALEMASK
‪const CONTEXT_IMAGECROPSCALEMASK
Definition: ProcessedFile.php:59
‪function
‪static return function(ContainerConfigurator $container, ContainerBuilder $containerBuilder)
Definition: Services.php:9
‪TYPO3\CMS\Core\Utility\Exception\MissingArrayPathException
Definition: MissingArrayPathException.php:27
‪TYPO3\CMS\Frontend\ContentObject
Definition: AbstractContentObject.php:16
‪TYPO3\CMS\Frontend\Http\UrlProcessorInterface
Definition: UrlProcessorInterface.php:27
‪TYPO3\CMS\Frontend\Resource\FilePathSanitizer
Definition: FilePathSanitizer.php:39
‪TYPO3\CMS\Core\Resource\FileReference
Definition: FileReference.php:33
‪TYPO3\CMS\Core\Site\SiteFinder
Definition: SiteFinder.php:31
‪TYPO3\CMS\Frontend\ContentObject\Exception\ExceptionHandlerInterface
Definition: ExceptionHandlerInterface.php:24
‪TYPO3\CMS\Core\Localization\Locales
Definition: Locales.php:30
‪TYPO3\CMS\Core\Utility\ArrayUtility\mergeRecursiveWithOverrule
‪static mergeRecursiveWithOverrule(array &$original, array $overrule, $addKeys=true, $includeEmptyValues=true, $enableUnsetFeature=true)
Definition: ArrayUtility.php:654
‪TYPO3\CMS\Frontend\ContentObject\Exception\ContentRenderingException
Definition: ContentRenderingException.php:24
‪TYPO3\CMS\Core\Context\Context
Definition: Context.php:53
‪TYPO3\CMS\Core\Core\Environment\getContext
‪static ApplicationContext getContext()
Definition: Environment.php:141
‪TYPO3\CMS\Core\Service\FlexFormService
Definition: FlexFormService.php:25
‪TYPO3\CMS\Core\Type\BitSet
Definition: BitSet.php:62
‪TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression
Definition: CompositeExpression.php:25
‪TYPO3\CMS\Core\Utility\ArrayUtility\getValueByPath
‪static mixed getValueByPath(array $array, $path, $delimiter='/')
Definition: ArrayUtility.php:180
‪TYPO3\CMS\Core\Utility\DebugUtility\debugTrail
‪static string debugTrail($prependFileNames=false)
Definition: DebugUtility.php:145
‪TYPO3\CMS\Core\Database\Query\QueryHelper
Definition: QueryHelper.php:32
‪TYPO3\CMS\Core\Resource\Folder
Definition: Folder.php:37
‪TYPO3\CMS\Core\Resource\ResourceFactory
Definition: ResourceFactory.php:41
‪TYPO3\CMS\Frontend\Page\PageLayoutResolver
Definition: PageLayoutResolver.php:32
‪TYPO3\CMS\Core\Utility\DebugUtility\viewArray
‪static string viewArray($array_in)
Definition: DebugUtility.php:205
‪TYPO3\CMS\Core\Resource\Exception\ResourceDoesNotExistException
Definition: ResourceDoesNotExistException.php:23
‪TYPO3\CMS\Core\Resource\File
Definition: File.php:24
‪TYPO3\CMS\Core\Configuration\Features
Definition: Features.php:56
‪TYPO3\CMS\Core\Html\SanitizerInitiator
Definition: SanitizerInitiator.php:25
‪TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection\getCropArea
‪Area getCropArea(string $id='default')
Definition: CropVariantCollection.php:129
‪TYPO3\CMS\Core\Cache\CacheManager
Definition: CacheManager.php:36
‪TYPO3\CMS\Core\Service\DependencyOrderingService
Definition: DependencyOrderingService.php:32
‪TYPO3\CMS\Core\Page\DefaultJavaScriptAssetTrait
Definition: DefaultJavaScriptAssetTrait.php:30
‪TYPO3\CMS\Core\Context\LanguageAspect
Definition: LanguageAspect.php:57
‪TYPO3\CMS\Core\Utility\DebugUtility
Definition: DebugUtility.php:27
‪TYPO3\CMS\Frontend\Imaging\GifBuilder
Definition: GifBuilder.php:57
‪TYPO3\CMS\Core\Versioning\VersionState
Definition: VersionState.php:24
‪TYPO3\CMS\Core\Resource\ProcessedFile
Definition: ProcessedFile.php:45
‪TYPO3\CMS\Core\TypoScript\TypoScriptService
Definition: TypoScriptService.php:25
‪TYPO3\CMS\Core\Utility\MathUtility\calculateWithParentheses
‪static int calculateWithParentheses($string)
Definition: MathUtility.php:172
‪$output
‪$output
Definition: annotationChecker.php:121
‪debug
‪debug($variable='', $title=null, $group=null)
Definition: GlobalDebugFunctions.php:19
‪TYPO3\CMS\Core\Database\Connection
Definition: Connection.php:38
‪TYPO3\CMS\Core\TypoScript\TemplateService
Definition: TemplateService.php:46
‪TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController
Definition: TypoScriptFrontendController.php:104
‪TYPO3\CMS\Core\Utility\ArrayUtility
Definition: ArrayUtility.php:24
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction
Definition: DeletedRestriction.php:28
‪TYPO3\CMS\Core\Log\LogManager
Definition: LogManager.php:33
‪TYPO3\CMS\Core\Core\Environment
Definition: Environment.php:43
‪TYPO3\CMS\Core\Utility\GeneralUtility\intExplode
‪static int[] intExplode($delimiter, $string, $removeEmptyValues=false, $limit=0)
Definition: GeneralUtility.php:927
‪TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection
Definition: CropVariantCollection.php:23
‪TYPO3\CMS\Core\Utility\MathUtility
Definition: MathUtility.php:22
‪TYPO3\CMS\Core\Utility\HttpUtility
Definition: HttpUtility.php:22
‪TYPO3\CMS\Core\Domain\Repository\PageRepository
Definition: PageRepository.php:53
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:46
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:50
‪TYPO3\CMS\Core\Utility\StringUtility
Definition: StringUtility.php:22
‪TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection\create
‪static CropVariantCollection create(string $jsonString, array $tcaConfig=[])
Definition: CropVariantCollection.php:42
‪TYPO3\CMS\Core\Database\Query\Restriction\FrontendRestrictionContainer
Definition: FrontendRestrictionContainer.php:31
‪TYPO3\CMS\Core\TimeTracker\TimeTracker
Definition: TimeTracker.php:31
‪TYPO3\CMS\Core\Resource\Exception\InvalidPathException
Definition: InvalidPathException.php:23
‪TYPO3\CMS\Core\Utility\ArrayUtility\filterAndSortByNumericKeys
‪static array filterAndSortByNumericKeys($setupArr, $acceptAnyKeys=false)
Definition: ArrayUtility.php:852
‪TYPO3\CMS\Core\Resource\Exception
Definition: AbstractFileOperationException.php:16
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\DOKTYPE_RECYCLER
‪const DOKTYPE_RECYCLER
Definition: PageRepository.php:117
‪TYPO3\CMS\Core\Html\SanitizerBuilderFactory
Definition: SanitizerBuilderFactory.php:35
‪TYPO3\CMS\Frontend\ContentObject\Exception\ProductionExceptionHandler
Definition: ProductionExceptionHandler.php:32
‪TYPO3\CMS\Core\Utility\StringUtility\multibyteStringPad
‪static string multibyteStringPad(string $string, int $length, string $pad_string=' ', int $pad_type=STR_PAD_RIGHT, string $encoding='UTF-8')
Definition: StringUtility.php:213
‪TYPO3\CMS\Core\Authentication\AbstractUserAuthentication
Definition: AbstractUserAuthentication.php:56