1<?php 2namespace TYPO3\CMS\Frontend\ContentObject; 3 4/* 5 * This file is part of the TYPO3 CMS project. 6 * 7 * It is free software; you can redistribute it and/or modify it under 8 * the terms of the GNU General Public License, either version 2 9 * of the License, or any later version. 10 * 11 * For the full copyright and license information, please read the 12 * LICENSE.txt file that was distributed with this source code. 13 * 14 * The TYPO3 project - inspiring people to share! 15 */ 16 17use Doctrine\DBAL\DBALException; 18use Doctrine\DBAL\Driver\Statement; 19use Psr\Log\LoggerAwareInterface; 20use Psr\Log\LoggerAwareTrait; 21use TYPO3\CMS\Core\Authentication\AbstractUserAuthentication; 22use TYPO3\CMS\Core\Cache\CacheManager; 23use TYPO3\CMS\Core\Configuration\Features; 24use TYPO3\CMS\Core\Context\Context; 25use TYPO3\CMS\Core\Context\LanguageAspect; 26use TYPO3\CMS\Core\Core\Environment; 27use TYPO3\CMS\Core\Database\Connection; 28use TYPO3\CMS\Core\Database\ConnectionPool; 29use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder; 30use TYPO3\CMS\Core\Database\Query\QueryBuilder; 31use TYPO3\CMS\Core\Database\Query\QueryHelper; 32use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction; 33use TYPO3\CMS\Core\Database\Query\Restriction\FrontendRestrictionContainer; 34use TYPO3\CMS\Core\Html\HtmlParser; 35use TYPO3\CMS\Core\Html\SanitizerBuilderFactory; 36use TYPO3\CMS\Core\Html\SanitizerInitiator; 37use TYPO3\CMS\Core\Imaging\ImageManipulation\Area; 38use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection; 39use TYPO3\CMS\Core\LinkHandling\LinkService; 40use TYPO3\CMS\Core\Log\LogManager; 41use TYPO3\CMS\Core\Mail\MailMessage; 42use TYPO3\CMS\Core\Resource\Exception; 43use TYPO3\CMS\Core\Resource\Exception\ResourceDoesNotExistException; 44use TYPO3\CMS\Core\Resource\File; 45use TYPO3\CMS\Core\Resource\FileInterface; 46use TYPO3\CMS\Core\Resource\FileReference; 47use TYPO3\CMS\Core\Resource\ProcessedFile; 48use TYPO3\CMS\Core\Resource\ResourceFactory; 49use TYPO3\CMS\Core\Resource\StorageRepository; 50use TYPO3\CMS\Core\Service\DependencyOrderingService; 51use TYPO3\CMS\Core\Service\FlexFormService; 52use TYPO3\CMS\Core\Service\MarkerBasedTemplateService; 53use TYPO3\CMS\Core\Site\Entity\Site; 54use TYPO3\CMS\Core\Site\Entity\SiteLanguage; 55use TYPO3\CMS\Core\TimeTracker\TimeTracker; 56use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser; 57use TYPO3\CMS\Core\TypoScript\TypoScriptService; 58use TYPO3\CMS\Core\Utility\ArrayUtility; 59use TYPO3\CMS\Core\Utility\DebugUtility; 60use TYPO3\CMS\Core\Utility\Exception\MissingArrayPathException; 61use TYPO3\CMS\Core\Utility\GeneralUtility; 62use TYPO3\CMS\Core\Utility\HttpUtility; 63use TYPO3\CMS\Core\Utility\MailUtility; 64use TYPO3\CMS\Core\Utility\MathUtility; 65use TYPO3\CMS\Core\Utility\PathUtility; 66use TYPO3\CMS\Core\Utility\StringUtility; 67use TYPO3\CMS\Core\Versioning\VersionState; 68use TYPO3\CMS\Frontend\ContentObject\Exception\ContentRenderingException; 69use TYPO3\CMS\Frontend\ContentObject\Exception\ExceptionHandlerInterface; 70use TYPO3\CMS\Frontend\ContentObject\Exception\ProductionExceptionHandler; 71use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; 72use TYPO3\CMS\Frontend\Http\UrlProcessorInterface; 73use TYPO3\CMS\Frontend\Imaging\GifBuilder; 74use TYPO3\CMS\Frontend\Page\PageRepository; 75use TYPO3\CMS\Frontend\Resource\FilePathSanitizer; 76use TYPO3\CMS\Frontend\Service\TypoLinkCodecService; 77use TYPO3\CMS\Frontend\Typolink\AbstractTypolinkBuilder; 78use TYPO3\CMS\Frontend\Typolink\UnableToLinkException; 79use TYPO3\HtmlSanitizer\Builder\BuilderInterface; 80 81/** 82 * This class contains all main TypoScript features. 83 * This includes the rendering of TypoScript content objects (cObjects). 84 * Is the backbone of TypoScript Template rendering. 85 * 86 * There are lots of functions you can use from your include-scripts. 87 * The class is normally instantiated and referred to as "cObj". 88 * When you call your own PHP-code typically through a USER or USER_INT cObject then it is this class that instantiates the object and calls the main method. Before it does so it will set (if you are using classes) a reference to itself in the internal variable "cObj" of the object. Thus you can access all functions and data from this class by $this->cObj->... from within you classes written to be USER or USER_INT content objects. 89 */ 90class ContentObjectRenderer implements LoggerAwareInterface 91{ 92 use LoggerAwareTrait; 93 94 /** 95 * @var array 96 */ 97 public $align = [ 98 'center', 99 'right', 100 'left' 101 ]; 102 103 /** 104 * stdWrap functions in their correct order 105 * 106 * @see stdWrap() 107 */ 108 public $stdWrapOrder = [ 109 'stdWrapPreProcess' => 'hook', 110 // this is a placeholder for the first Hook 111 'cacheRead' => 'hook', 112 // this is a placeholder for checking if the content is available in cache 113 'setContentToCurrent' => 'boolean', 114 'setContentToCurrent.' => 'array', 115 'addPageCacheTags' => 'string', 116 'addPageCacheTags.' => 'array', 117 'setCurrent' => 'string', 118 'setCurrent.' => 'array', 119 'lang.' => 'array', 120 'data' => 'getText', 121 'data.' => 'array', 122 'field' => 'fieldName', 123 'field.' => 'array', 124 'current' => 'boolean', 125 'current.' => 'array', 126 'cObject' => 'cObject', 127 'cObject.' => 'array', 128 'numRows.' => 'array', 129 // @deprecated - will be removed in TYPO3 v10.0. 130 'filelist' => 'dir', 131 // @deprecated - will be removed in TYPO3 v10.0. 132 'filelist.' => 'array', 133 'preUserFunc' => 'functionName', 134 'stdWrapOverride' => 'hook', 135 // this is a placeholder for the second Hook 136 'override' => 'string', 137 'override.' => 'array', 138 'preIfEmptyListNum' => 'listNum', 139 'preIfEmptyListNum.' => 'array', 140 'ifNull' => 'string', 141 'ifNull.' => 'array', 142 'ifEmpty' => 'string', 143 'ifEmpty.' => 'array', 144 'ifBlank' => 'string', 145 'ifBlank.' => 'array', 146 'listNum' => 'listNum', 147 'listNum.' => 'array', 148 'trim' => 'boolean', 149 'trim.' => 'array', 150 'strPad.' => 'array', 151 'stdWrap' => 'stdWrap', 152 'stdWrap.' => 'array', 153 'stdWrapProcess' => 'hook', 154 // this is a placeholder for the third Hook 155 'required' => 'boolean', 156 'required.' => 'array', 157 'if.' => 'array', 158 'fieldRequired' => 'fieldName', 159 'fieldRequired.' => 'array', 160 'csConv' => 'string', 161 'csConv.' => 'array', 162 'parseFunc' => 'objectpath', 163 'parseFunc.' => 'array', 164 'HTMLparser' => 'boolean', 165 'HTMLparser.' => 'array', 166 'split.' => 'array', 167 'replacement.' => 'array', 168 'prioriCalc' => 'boolean', 169 'prioriCalc.' => 'array', 170 'char' => 'integer', 171 'char.' => 'array', 172 'intval' => 'boolean', 173 'intval.' => 'array', 174 'hash' => 'string', 175 'hash.' => 'array', 176 'round' => 'boolean', 177 'round.' => 'array', 178 'numberFormat.' => 'array', 179 'expandList' => 'boolean', 180 'expandList.' => 'array', 181 'date' => 'dateconf', 182 'date.' => 'array', 183 'strtotime' => 'strtotimeconf', 184 'strtotime.' => 'array', 185 'strftime' => 'strftimeconf', 186 'strftime.' => 'array', 187 'age' => 'boolean', 188 'age.' => 'array', 189 'case' => 'case', 190 'case.' => 'array', 191 'bytes' => 'boolean', 192 'bytes.' => 'array', 193 'substring' => 'parameters', 194 'substring.' => 'array', 195 'cropHTML' => 'crop', 196 'cropHTML.' => 'array', 197 'stripHtml' => 'boolean', 198 'stripHtml.' => 'array', 199 'crop' => 'crop', 200 'crop.' => 'array', 201 'rawUrlEncode' => 'boolean', 202 'rawUrlEncode.' => 'array', 203 'htmlSpecialChars' => 'boolean', 204 'htmlSpecialChars.' => 'array', 205 'encodeForJavaScriptValue' => 'boolean', 206 'encodeForJavaScriptValue.' => 'array', 207 'doubleBrTag' => 'string', 208 'doubleBrTag.' => 'array', 209 'br' => 'boolean', 210 'br.' => 'array', 211 'brTag' => 'string', 212 'brTag.' => 'array', 213 'encapsLines.' => 'array', 214 'keywords' => 'boolean', 215 'keywords.' => 'array', 216 'innerWrap' => 'wrap', 217 'innerWrap.' => 'array', 218 'innerWrap2' => 'wrap', 219 'innerWrap2.' => 'array', 220 // @deprecated - will be removed in TYPO3 v10.0. 221 'addParams.' => 'array', 222 // @deprecated - will be removed in TYPO3 v10.0. 223 'filelink.' => 'array', 224 'preCObject' => 'cObject', 225 'preCObject.' => 'array', 226 'postCObject' => 'cObject', 227 'postCObject.' => 'array', 228 'wrapAlign' => 'align', 229 'wrapAlign.' => 'array', 230 'typolink.' => 'array', 231 'wrap' => 'wrap', 232 'wrap.' => 'array', 233 'noTrimWrap' => 'wrap', 234 'noTrimWrap.' => 'array', 235 'wrap2' => 'wrap', 236 'wrap2.' => 'array', 237 'dataWrap' => 'dataWrap', 238 'dataWrap.' => 'array', 239 'prepend' => 'cObject', 240 'prepend.' => 'array', 241 'append' => 'cObject', 242 'append.' => 'array', 243 'wrap3' => 'wrap', 244 'wrap3.' => 'array', 245 'orderedStdWrap' => 'stdWrap', 246 'orderedStdWrap.' => 'array', 247 'outerWrap' => 'wrap', 248 'outerWrap.' => 'array', 249 'insertData' => 'boolean', 250 'insertData.' => 'array', 251 'postUserFunc' => 'functionName', 252 'postUserFuncInt' => 'functionName', 253 'prefixComment' => 'string', 254 'prefixComment.' => 'array', 255 'editIcons' => 'string', 256 'editIcons.' => 'array', 257 'editPanel' => 'boolean', 258 'editPanel.' => 'array', 259 'htmlSanitize' => 'boolean', 260 'htmlSanitize.' => 'array', 261 'cacheStore' => 'hook', 262 // this is a placeholder for storing the content in cache 263 'stdWrapPostProcess' => 'hook', 264 // this is a placeholder for the last Hook 265 'debug' => 'boolean', 266 'debug.' => 'array', 267 'debugFunc' => 'boolean', 268 'debugFunc.' => 'array', 269 'debugData' => 'boolean', 270 'debugData.' => 'array' 271 ]; 272 273 /** 274 * Class names for accordant content object names 275 * 276 * @var array 277 */ 278 protected $contentObjectClassMap = []; 279 280 /** 281 * Loaded with the current data-record. 282 * 283 * If the instance of this class is used to render records from the database those records are found in this array. 284 * The function stdWrap has TypoScript properties that fetch field-data from this array. 285 * 286 * @var array 287 * @see start() 288 */ 289 public $data = []; 290 291 /** 292 * @var string 293 */ 294 protected $table = ''; 295 296 /** 297 * Used for backup 298 * 299 * @var array 300 */ 301 public $oldData = []; 302 303 /** 304 * If this is set with an array before stdWrap, it's used instead of $this->data in the data-property in stdWrap 305 * 306 * @var string 307 */ 308 public $alternativeData = ''; 309 310 /** 311 * Used by the parseFunc function and is loaded with tag-parameters when parsing tags. 312 * 313 * @var array 314 */ 315 public $parameters = []; 316 317 /** 318 * @var string 319 */ 320 public $currentValKey = 'currentValue_kidjls9dksoje'; 321 322 /** 323 * This is set to the [table]:[uid] of the record delivered in the $data-array, if the cObjects CONTENT or RECORD is in operation. 324 * Note that $GLOBALS['TSFE']->currentRecord is set to an equal value but always indicating the latest record rendered. 325 * 326 * @var string 327 */ 328 public $currentRecord = ''; 329 330 /** 331 * Set in RecordsContentObject and ContentContentObject to the current number of records selected in a query. 332 * 333 * @var int 334 */ 335 public $currentRecordTotal = 0; 336 337 /** 338 * Incremented in RecordsContentObject and ContentContentObject before each record rendering. 339 * 340 * @var int 341 */ 342 public $currentRecordNumber = 0; 343 344 /** 345 * Incremented in RecordsContentObject and ContentContentObject before each record rendering. 346 * 347 * @var int 348 */ 349 public $parentRecordNumber = 0; 350 351 /** 352 * If the ContentObjectRender was started from ContentContentObject, RecordsContentObject or SearchResultContentObject this array has two keys, 'data' and 'currentRecord' which indicates the record and data for the parent cObj. 353 * 354 * @var array 355 */ 356 public $parentRecord = []; 357 358 /** 359 * This is used by checkPid, that checks if pages are accessible. The $checkPid_cache['page_uid'] is set TRUE or FALSE upon this check featuring a caching function for the next request. 360 * 361 * @var array 362 */ 363 public $checkPid_cache = []; 364 365 /** 366 * @var string 367 */ 368 public $checkPid_badDoktypeList = '255'; 369 370 /** 371 * This will be set by typoLink() to the url of the most recent link created. 372 * 373 * @var string 374 */ 375 public $lastTypoLinkUrl = ''; 376 377 /** 378 * DO. link target. 379 * 380 * @var string 381 */ 382 public $lastTypoLinkTarget = ''; 383 384 /** 385 * @var array 386 */ 387 public $lastTypoLinkLD = []; 388 389 /** 390 * array that registers rendered content elements (or any table) to make sure they are not rendered recursively! 391 * 392 * @var array 393 */ 394 public $recordRegister = []; 395 396 /** 397 * Additionally registered content object types and class names 398 * 399 * @var array 400 */ 401 protected $cObjHookObjectsRegistry = []; 402 403 /** 404 * @var array 405 */ 406 public $cObjHookObjectsArr = []; 407 408 /** 409 * Containing hook objects for stdWrap 410 * 411 * @var array 412 */ 413 protected $stdWrapHookObjects = []; 414 415 /** 416 * Containing hook objects for getImgResource 417 * 418 * @var array 419 */ 420 protected $getImgResourceHookObjects; 421 422 /** 423 * @var File Current file objects (during iterations over files) 424 */ 425 protected $currentFile; 426 427 /** 428 * Set to TRUE by doConvertToUserIntObject() if USER object wants to become USER_INT 429 */ 430 public $doConvertToUserIntObject = false; 431 432 /** 433 * Indicates current object type. Can hold one of OBJECTTYPE_ constants or FALSE. 434 * The value is set and reset inside USER() function. Any time outside of 435 * USER() it is FALSE. 436 */ 437 protected $userObjectType = false; 438 439 /** 440 * @var array 441 */ 442 protected $stopRendering = []; 443 444 /** 445 * @var int 446 */ 447 protected $stdWrapRecursionLevel = 0; 448 449 /** 450 * @var TypoScriptFrontendController 451 */ 452 protected $typoScriptFrontendController; 453 454 /** 455 * Indicates that object type is USER. 456 * 457 * @see ContentObjectRender::$userObjectType 458 */ 459 const OBJECTTYPE_USER_INT = 1; 460 /** 461 * Indicates that object type is USER. 462 * 463 * @see ContentObjectRender::$userObjectType 464 */ 465 const OBJECTTYPE_USER = 2; 466 467 /** 468 * @param TypoScriptFrontendController $typoScriptFrontendController 469 */ 470 public function __construct(TypoScriptFrontendController $typoScriptFrontendController = null) 471 { 472 $this->typoScriptFrontendController = $typoScriptFrontendController; 473 $this->contentObjectClassMap = $GLOBALS['TYPO3_CONF_VARS']['FE']['ContentObjects']; 474 } 475 476 /** 477 * Prevent several objects from being serialized. 478 * If currentFile is set, it is either a File or a FileReference object. As the object itself can't be serialized, 479 * we have store a hash and restore the object in __wakeup() 480 * 481 * @return array 482 */ 483 public function __sleep() 484 { 485 $vars = get_object_vars($this); 486 unset($vars['typoScriptFrontendController'], $vars['logger']); 487 if ($this->currentFile instanceof FileReference) { 488 $this->currentFile = 'FileReference:' . $this->currentFile->getUid(); 489 } elseif ($this->currentFile instanceof File) { 490 $this->currentFile = 'File:' . $this->currentFile->getIdentifier(); 491 } else { 492 unset($vars['currentFile']); 493 } 494 return array_keys($vars); 495 } 496 497 /** 498 * Restore currentFile from hash. 499 * If currentFile references a File, the identifier equals file identifier. 500 * If it references a FileReference the identifier equals the uid of the reference. 501 */ 502 public function __wakeup() 503 { 504 if (isset($GLOBALS['TSFE'])) { 505 $this->typoScriptFrontendController = $GLOBALS['TSFE']; 506 } 507 if ($this->currentFile !== null && is_string($this->currentFile)) { 508 list($objectType, $identifier) = explode(':', $this->currentFile, 2); 509 try { 510 if ($objectType === 'File') { 511 $this->currentFile = ResourceFactory::getInstance()->retrieveFileOrFolderObject($identifier); 512 } elseif ($objectType === 'FileReference') { 513 $this->currentFile = ResourceFactory::getInstance()->getFileReferenceObject($identifier); 514 } 515 } catch (ResourceDoesNotExistException $e) { 516 $this->currentFile = null; 517 } 518 } 519 $this->logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__); 520 } 521 522 /** 523 * Allow injecting content object class map. 524 * 525 * This method is private API, please use configuration 526 * $GLOBALS['TYPO3_CONF_VARS']['FE']['ContentObjects'] to add new content objects 527 * 528 * @internal 529 * @param array $contentObjectClassMap 530 */ 531 public function setContentObjectClassMap(array $contentObjectClassMap) 532 { 533 $this->contentObjectClassMap = $contentObjectClassMap; 534 } 535 536 /** 537 * Register a single content object name to class name 538 * 539 * This method is private API, please use configuration 540 * $GLOBALS['TYPO3_CONF_VARS']['FE']['ContentObjects'] to add new content objects 541 * 542 * @param string $className 543 * @param string $contentObjectName 544 * @internal 545 */ 546 public function registerContentObjectClass($className, $contentObjectName) 547 { 548 $this->contentObjectClassMap[$contentObjectName] = $className; 549 } 550 551 /** 552 * Class constructor. 553 * Well, it has to be called manually since it is not a real constructor function. 554 * So after making an instance of the class, call this function and pass to it a database record and the tablename from where the record is from. That will then become the "current" record loaded into memory and accessed by the .fields property found in eg. stdWrap. 555 * 556 * @param array $data The record data that is rendered. 557 * @param string $table The table that the data record is from. 558 */ 559 public function start($data, $table = '') 560 { 561 $this->data = $data; 562 $this->table = $table; 563 $this->currentRecord = $table !== '' 564 ? $table . ':' . ($this->data['uid'] ?? '') 565 : ''; 566 $this->parameters = []; 567 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['cObjTypeAndClass'] ?? [] as $classArr) { 568 $this->cObjHookObjectsRegistry[$classArr[0]] = $classArr[1]; 569 } 570 $this->stdWrapHookObjects = []; 571 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['stdWrap'] ?? [] as $className) { 572 $hookObject = GeneralUtility::makeInstance($className); 573 if (!$hookObject instanceof ContentObjectStdWrapHookInterface) { 574 throw new \UnexpectedValueException($className . ' must implement interface ' . ContentObjectStdWrapHookInterface::class, 1195043965); 575 } 576 $this->stdWrapHookObjects[] = $hookObject; 577 } 578 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['postInit'] ?? [] as $className) { 579 $postInitializationProcessor = GeneralUtility::makeInstance($className); 580 if (!$postInitializationProcessor instanceof ContentObjectPostInitHookInterface) { 581 throw new \UnexpectedValueException($className . ' must implement interface ' . ContentObjectPostInitHookInterface::class, 1274563549); 582 } 583 $postInitializationProcessor->postProcessContentObjectInitialization($this); 584 } 585 } 586 587 /** 588 * Returns the current table 589 * 590 * @return string 591 */ 592 public function getCurrentTable() 593 { 594 return $this->table; 595 } 596 597 /** 598 * Gets the 'getImgResource' hook objects. 599 * The first call initializes the accordant objects. 600 * 601 * @return array The 'getImgResource' hook objects (if any) 602 */ 603 protected function getGetImgResourceHookObjects() 604 { 605 if (!isset($this->getImgResourceHookObjects)) { 606 $this->getImgResourceHookObjects = []; 607 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['getImgResource'] ?? [] as $className) { 608 $hookObject = GeneralUtility::makeInstance($className); 609 if (!$hookObject instanceof ContentObjectGetImageResourceHookInterface) { 610 throw new \UnexpectedValueException('$hookObject must implement interface ' . ContentObjectGetImageResourceHookInterface::class, 1218636383); 611 } 612 $this->getImgResourceHookObjects[] = $hookObject; 613 } 614 } 615 return $this->getImgResourceHookObjects; 616 } 617 618 /** 619 * Sets the internal variable parentRecord with information about current record. 620 * If the ContentObjectRender was started from CONTENT, RECORD or SEARCHRESULT cObject's this array has two keys, 'data' and 'currentRecord' which indicates the record and data for the parent cObj. 621 * 622 * @param array $data The record array 623 * @param string $currentRecord This is set to the [table]:[uid] of the record delivered in the $data-array, if the cObjects CONTENT or RECORD is in operation. Note that $GLOBALS['TSFE']->currentRecord is set to an equal value but always indicating the latest record rendered. 624 * @internal 625 */ 626 public function setParent($data, $currentRecord) 627 { 628 $this->parentRecord = [ 629 'data' => $data, 630 'currentRecord' => $currentRecord 631 ]; 632 } 633 634 /*********************************************** 635 * 636 * CONTENT_OBJ: 637 * 638 ***********************************************/ 639 /** 640 * Returns the "current" value. 641 * The "current" value is just an internal variable that can be used by functions to pass a single value on to another function later in the TypoScript processing. 642 * It's like "load accumulator" in the good old C64 days... basically a "register" you can use as you like. 643 * The TSref will tell if functions are setting this value before calling some other object so that you know if it holds any special information. 644 * 645 * @return mixed The "current" value 646 */ 647 public function getCurrentVal() 648 { 649 return $this->data[$this->currentValKey]; 650 } 651 652 /** 653 * Sets the "current" value. 654 * 655 * @param mixed $value The variable that you want to set as "current 656 * @see getCurrentVal() 657 */ 658 public function setCurrentVal($value) 659 { 660 $this->data[$this->currentValKey] = $value; 661 } 662 663 /** 664 * Rendering of a "numerical array" of cObjects from TypoScript 665 * Will call ->cObjGetSingle() for each cObject found and accumulate the output. 666 * 667 * @param array $setup array with cObjects as values. 668 * @param string $addKey A prefix for the debugging information 669 * @return string Rendered output from the cObjects in the array. 670 * @see cObjGetSingle() 671 */ 672 public function cObjGet($setup, $addKey = '') 673 { 674 if (!is_array($setup)) { 675 return ''; 676 } 677 $sKeyArray = ArrayUtility::filterAndSortByNumericKeys($setup); 678 $content = ''; 679 foreach ($sKeyArray as $theKey) { 680 $theValue = $setup[$theKey]; 681 if ((int)$theKey && strpos($theKey, '.') === false) { 682 $conf = $setup[$theKey . '.']; 683 $content .= $this->cObjGetSingle($theValue, $conf, $addKey . $theKey); 684 } 685 } 686 return $content; 687 } 688 689 /** 690 * Renders a content object 691 * 692 * @param string $name The content object name, eg. "TEXT" or "USER" or "IMAGE 693 * @param array $conf The array with TypoScript properties for the content object 694 * @param string $TSkey A string label used for the internal debugging tracking. 695 * @return string cObject output 696 * @throws \UnexpectedValueException 697 */ 698 public function cObjGetSingle($name, $conf, $TSkey = '__') 699 { 700 $content = ''; 701 // Checking that the function is not called eternally. This is done by interrupting at a depth of 100 702 $this->getTypoScriptFrontendController()->cObjectDepthCounter--; 703 if ($this->getTypoScriptFrontendController()->cObjectDepthCounter > 0) { 704 $timeTracker = $this->getTimeTracker(); 705 $name = trim($name); 706 if ($timeTracker->LR) { 707 $timeTracker->push($TSkey, $name); 708 } 709 // Checking if the COBJ is a reference to another object. (eg. name of 'some.object =< styles.something') 710 if (isset($name[0]) && $name[0] === '<') { 711 $key = trim(substr($name, 1)); 712 $cF = GeneralUtility::makeInstance(TypoScriptParser::class); 713 // $name and $conf is loaded with the referenced values. 714 $confOverride = is_array($conf) ? $conf : []; 715 list($name, $conf) = $cF->getVal($key, $this->getTypoScriptFrontendController()->tmpl->setup); 716 $conf = array_replace_recursive(is_array($conf) ? $conf : [], $confOverride); 717 // Getting the cObject 718 $timeTracker->incStackPointer(); 719 $content .= $this->cObjGetSingle($name, $conf, $key); 720 $timeTracker->decStackPointer(); 721 } else { 722 $hooked = false; 723 // Application defined cObjects 724 if (!empty($this->cObjHookObjectsRegistry[$name])) { 725 if (empty($this->cObjHookObjectsArr[$name])) { 726 $this->cObjHookObjectsArr[$name] = GeneralUtility::makeInstance($this->cObjHookObjectsRegistry[$name]); 727 } 728 $hookObj = $this->cObjHookObjectsArr[$name]; 729 if (method_exists($hookObj, 'cObjGetSingleExt')) { 730 $content .= $hookObj->cObjGetSingleExt($name, $conf, $TSkey, $this); 731 $hooked = true; 732 } 733 } 734 if (!$hooked) { 735 $contentObject = $this->getContentObject($name); 736 if ($contentObject) { 737 $content .= $this->render($contentObject, $conf); 738 } else { 739 // Call hook functions for extra processing 740 if ($name) { 741 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['cObjTypeAndClassDefault'] ?? [] as $className) { 742 $hookObject = GeneralUtility::makeInstance($className); 743 if (!$hookObject instanceof ContentObjectGetSingleHookInterface) { 744 throw new \UnexpectedValueException('$hookObject must implement interface ' . ContentObjectGetSingleHookInterface::class, 1195043731); 745 } 746 /** @var ContentObjectGetSingleHookInterface $hookObject */ 747 $content .= $hookObject->getSingleContentObject($name, (array)$conf, $TSkey, $this); 748 } 749 } else { 750 // Log error in AdminPanel 751 $warning = sprintf('Content Object "%s" does not exist', $name); 752 $timeTracker->setTSlogMessage($warning, 2); 753 } 754 } 755 } 756 } 757 if ($timeTracker->LR) { 758 $timeTracker->pull($content); 759 } 760 } 761 // Increasing on exit... 762 $this->getTypoScriptFrontendController()->cObjectDepthCounter++; 763 return $content; 764 } 765 766 /** 767 * Returns a new content object of type $name. 768 * This content object needs to be registered as content object 769 * in $this->contentObjectClassMap 770 * 771 * @param string $name 772 * @return AbstractContentObject|null 773 * @throws ContentRenderingException 774 */ 775 public function getContentObject($name) 776 { 777 if (!isset($this->contentObjectClassMap[$name])) { 778 return null; 779 } 780 $fullyQualifiedClassName = $this->contentObjectClassMap[$name]; 781 $contentObject = GeneralUtility::makeInstance($fullyQualifiedClassName, $this); 782 if (!($contentObject instanceof AbstractContentObject)) { 783 throw new ContentRenderingException(sprintf('Registered content object class name "%s" must be an instance of AbstractContentObject, but is not!', $fullyQualifiedClassName), 1422564295); 784 } 785 return $contentObject; 786 } 787 788 /******************************************** 789 * 790 * Functions rendering content objects (cObjects) 791 * 792 ********************************************/ 793 794 /** 795 * Renders a content object by taking exception and cache handling 796 * into consideration 797 * 798 * @param AbstractContentObject $contentObject Content object instance 799 * @param array $configuration Array of TypoScript properties 800 * 801 * @throws ContentRenderingException 802 * @throws \Exception 803 * @return string 804 */ 805 public function render(AbstractContentObject $contentObject, $configuration = []) 806 { 807 $content = ''; 808 809 // Evaluate possible cache and return 810 $cacheConfiguration = $configuration['cache.'] ?? null; 811 if ($cacheConfiguration !== null) { 812 unset($configuration['cache.']); 813 $cache = $this->getFromCache($cacheConfiguration); 814 if ($cache !== false) { 815 return $cache; 816 } 817 } 818 819 // Render content 820 try { 821 $content .= $contentObject->render($configuration); 822 } catch (ContentRenderingException $exception) { 823 // Content rendering Exceptions indicate a critical problem which should not be 824 // caught e.g. when something went wrong with Exception handling itself 825 throw $exception; 826 } catch (\Exception $exception) { 827 $exceptionHandler = $this->createExceptionHandler($configuration); 828 if ($exceptionHandler === null) { 829 throw $exception; 830 } 831 $content = $exceptionHandler->handle($exception, $contentObject, $configuration); 832 } 833 834 // Store cache 835 if ($cacheConfiguration !== null && !$this->getTypoScriptFrontendController()->no_cache) { 836 $key = $this->calculateCacheKey($cacheConfiguration); 837 if (!empty($key)) { 838 /** @var \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface $cacheFrontend */ 839 $cacheFrontend = GeneralUtility::makeInstance(CacheManager::class)->getCache('cache_hash'); 840 $tags = $this->calculateCacheTags($cacheConfiguration); 841 $lifetime = $this->calculateCacheLifetime($cacheConfiguration); 842 $cacheFrontend->set($key, $content, $tags, $lifetime); 843 } 844 } 845 846 return $content; 847 } 848 849 /** 850 * Creates the content object exception handler from local content object configuration 851 * or, from global configuration if not explicitly disabled in local configuration 852 * 853 * @param array $configuration 854 * @return ExceptionHandlerInterface|null 855 * @throws ContentRenderingException 856 */ 857 protected function createExceptionHandler($configuration = []) 858 { 859 $exceptionHandler = null; 860 $exceptionHandlerClassName = $this->determineExceptionHandlerClassName($configuration); 861 if (!empty($exceptionHandlerClassName)) { 862 $exceptionHandler = GeneralUtility::makeInstance($exceptionHandlerClassName, $this->mergeExceptionHandlerConfiguration($configuration)); 863 if (!$exceptionHandler instanceof ExceptionHandlerInterface) { 864 throw new ContentRenderingException('An exception handler was configured but the class does not exist or does not implement the ExceptionHandlerInterface', 1403653369); 865 } 866 } 867 868 return $exceptionHandler; 869 } 870 871 /** 872 * Determine exception handler class name from global and content object configuration 873 * 874 * @param array $configuration 875 * @return string|null 876 */ 877 protected function determineExceptionHandlerClassName($configuration) 878 { 879 $exceptionHandlerClassName = null; 880 $tsfe = $this->getTypoScriptFrontendController(); 881 if (!isset($tsfe->config['config']['contentObjectExceptionHandler'])) { 882 if (GeneralUtility::getApplicationContext()->isProduction()) { 883 $exceptionHandlerClassName = '1'; 884 } 885 } else { 886 $exceptionHandlerClassName = $tsfe->config['config']['contentObjectExceptionHandler']; 887 } 888 889 if (isset($configuration['exceptionHandler'])) { 890 $exceptionHandlerClassName = $configuration['exceptionHandler']; 891 } 892 893 if ($exceptionHandlerClassName === '1') { 894 $exceptionHandlerClassName = ProductionExceptionHandler::class; 895 } 896 897 return $exceptionHandlerClassName; 898 } 899 900 /** 901 * Merges global exception handler configuration with the one from the content object 902 * and returns the merged exception handler configuration 903 * 904 * @param array $configuration 905 * @return array 906 */ 907 protected function mergeExceptionHandlerConfiguration($configuration) 908 { 909 $exceptionHandlerConfiguration = []; 910 $tsfe = $this->getTypoScriptFrontendController(); 911 if (!empty($tsfe->config['config']['contentObjectExceptionHandler.'])) { 912 $exceptionHandlerConfiguration = $tsfe->config['config']['contentObjectExceptionHandler.']; 913 } 914 if (!empty($configuration['exceptionHandler.'])) { 915 $exceptionHandlerConfiguration = array_replace_recursive($exceptionHandlerConfiguration, $configuration['exceptionHandler.']); 916 } 917 918 return $exceptionHandlerConfiguration; 919 } 920 921 /** 922 * Retrieves a type of object called as USER or USER_INT. Object can detect their 923 * type by using this call. It returns OBJECTTYPE_USER_INT or OBJECTTYPE_USER depending on the 924 * current object execution. In all other cases it will return FALSE to indicate 925 * a call out of context. 926 * 927 * @return mixed One of OBJECTTYPE_ class constants or FALSE 928 */ 929 public function getUserObjectType() 930 { 931 return $this->userObjectType; 932 } 933 934 /** 935 * Sets the user object type 936 * 937 * @param mixed $userObjectType 938 */ 939 public function setUserObjectType($userObjectType) 940 { 941 $this->userObjectType = $userObjectType; 942 } 943 944 /** 945 * Requests the current USER object to be converted to USER_INT. 946 */ 947 public function convertToUserIntObject() 948 { 949 if ($this->userObjectType !== self::OBJECTTYPE_USER) { 950 $this->getTimeTracker()->setTSlogMessage(self::class . '::convertToUserIntObject() is called in the wrong context or for the wrong object type', 2); 951 } else { 952 $this->doConvertToUserIntObject = true; 953 } 954 } 955 956 /************************************ 957 * 958 * Various helper functions for content objects: 959 * 960 ************************************/ 961 /** 962 * Converts a given config in Flexform to a conf-array 963 * 964 * @param string|array $flexData Flexform data 965 * @param array $conf Array to write the data into, by reference 966 * @param bool $recursive Is set if called recursive. Don't call function with this parameter, it's used inside the function only 967 */ 968 public function readFlexformIntoConf($flexData, &$conf, $recursive = false) 969 { 970 if ($recursive === false && is_string($flexData)) { 971 $flexData = GeneralUtility::xml2array($flexData, 'T3'); 972 } 973 if (is_array($flexData) && isset($flexData['data']['sDEF']['lDEF'])) { 974 $flexData = $flexData['data']['sDEF']['lDEF']; 975 } 976 if (!is_array($flexData)) { 977 return; 978 } 979 foreach ($flexData as $key => $value) { 980 if (!is_array($value)) { 981 continue; 982 } 983 if (isset($value['el'])) { 984 if (is_array($value['el']) && !empty($value['el'])) { 985 foreach ($value['el'] as $ekey => $element) { 986 if (isset($element['vDEF'])) { 987 $conf[$ekey] = $element['vDEF']; 988 } else { 989 if (is_array($element)) { 990 $this->readFlexformIntoConf($element, $conf[$key][key($element)][$ekey], true); 991 } else { 992 $this->readFlexformIntoConf($element, $conf[$key][$ekey], true); 993 } 994 } 995 } 996 } else { 997 $this->readFlexformIntoConf($value['el'], $conf[$key], true); 998 } 999 } 1000 if (isset($value['vDEF'])) { 1001 $conf[$key] = $value['vDEF']; 1002 } 1003 } 1004 } 1005 1006 /** 1007 * Returns all parents of the given PID (Page UID) list 1008 * 1009 * @param string $pidList A list of page Content-Element PIDs (Page UIDs) / stdWrap 1010 * @param array $pidConf stdWrap array for the list 1011 * @return string A list of PIDs 1012 * @internal 1013 */ 1014 public function getSlidePids($pidList, $pidConf) 1015 { 1016 $pidList = isset($pidConf) ? trim($this->stdWrap($pidList, $pidConf)) : trim($pidList); 1017 if ($pidList === '') { 1018 $pidList = 'this'; 1019 } 1020 $tsfe = $this->getTypoScriptFrontendController(); 1021 $listArr = null; 1022 if (trim($pidList)) { 1023 $listArr = GeneralUtility::intExplode(',', str_replace('this', $tsfe->contentPid, $pidList)); 1024 $listArr = $this->checkPidArray($listArr); 1025 } 1026 $pidList = []; 1027 if (is_array($listArr) && !empty($listArr)) { 1028 foreach ($listArr as $uid) { 1029 $page = $tsfe->sys_page->getPage($uid); 1030 if (!$page['is_siteroot']) { 1031 $pidList[] = $page['pid']; 1032 } 1033 } 1034 } 1035 return implode(',', $pidList); 1036 } 1037 1038 /** 1039 * Returns a <img> tag with the image file defined by $file and processed according to the properties in the TypoScript array. 1040 * Mostly this function is a sub-function to the IMAGE function which renders the IMAGE cObject in TypoScript. 1041 * This function is called by "$this->cImage($conf['file'], $conf);" from IMAGE(). 1042 * 1043 * @param string $file File TypoScript resource 1044 * @param array $conf TypoScript configuration properties 1045 * @return string <img> tag, (possibly wrapped in links and other HTML) if any image found. 1046 * @internal 1047 * @see IMAGE() 1048 */ 1049 public function cImage($file, $conf) 1050 { 1051 $tsfe = $this->getTypoScriptFrontendController(); 1052 $info = $this->getImgResource($file, $conf['file.']); 1053 $tsfe->lastImageInfo = $info; 1054 if (!is_array($info)) { 1055 return ''; 1056 } 1057 if (is_file(Environment::getPublicPath() . '/' . $info['3'])) { 1058 $source = $tsfe->absRefPrefix . str_replace('%2F', '/', rawurlencode($info['3'])); 1059 } else { 1060 $source = $info[3]; 1061 } 1062 1063 $layoutKey = $this->stdWrap($conf['layoutKey'], $conf['layoutKey.']); 1064 $imageTagTemplate = $this->getImageTagTemplate($layoutKey, $conf); 1065 $sourceCollection = $this->getImageSourceCollection($layoutKey, $conf, $file); 1066 1067 // This array is used to collect the image-refs on the page... 1068 $tsfe->imagesOnPage[] = $source; 1069 $altParam = $this->getAltParam($conf); 1070 $params = $this->stdWrapValue('params', $conf); 1071 if ($params !== '' && $params[0] !== ' ') { 1072 $params = ' ' . $params; 1073 } 1074 1075 $imageTagValues = [ 1076 'width' => (int)$info[0], 1077 'height' => (int)$info[1], 1078 'src' => htmlspecialchars($source), 1079 'params' => $params, 1080 'altParams' => $altParam, 1081 'border' => $this->getBorderAttr(' border="' . (int)$conf['border'] . '"'), 1082 'sourceCollection' => $sourceCollection, 1083 'selfClosingTagSlash' => !empty($tsfe->xhtmlDoctype) ? ' /' : '', 1084 ]; 1085 1086 $markerTemplateEngine = GeneralUtility::makeInstance(MarkerBasedTemplateService::class); 1087 $theValue = $markerTemplateEngine->substituteMarkerArray($imageTagTemplate, $imageTagValues, '###|###', true, true); 1088 1089 $linkWrap = isset($conf['linkWrap.']) ? $this->stdWrap($conf['linkWrap'], $conf['linkWrap.']) : $conf['linkWrap']; 1090 if ($linkWrap) { 1091 $theValue = $this->linkWrap($theValue, $linkWrap); 1092 } elseif ($conf['imageLinkWrap']) { 1093 $originalFile = !empty($info['originalFile']) ? $info['originalFile'] : $info['origFile']; 1094 $theValue = $this->imageLinkWrap($theValue, $originalFile, $conf['imageLinkWrap.']); 1095 } 1096 $wrap = isset($conf['wrap.']) ? $this->stdWrap($conf['wrap'], $conf['wrap.']) : $conf['wrap']; 1097 if ((string)$wrap !== '') { 1098 $theValue = $this->wrap($theValue, $conf['wrap']); 1099 } 1100 return $theValue; 1101 } 1102 1103 /** 1104 * Returns the 'border' attribute for an <img> tag only if the doctype is not xhtml_strict, xhtml_11 or html5 1105 * or if the config parameter 'disableImgBorderAttr' is not set. 1106 * 1107 * @param string $borderAttr The border attribute 1108 * @return string The border attribute 1109 */ 1110 public function getBorderAttr($borderAttr) 1111 { 1112 $tsfe = $this->getTypoScriptFrontendController(); 1113 $docType = $tsfe->xhtmlDoctype; 1114 if ( 1115 $docType !== 'xhtml_strict' && $docType !== 'xhtml_11' 1116 && $tsfe->config['config']['doctype'] !== 'html5' 1117 && !$tsfe->config['config']['disableImgBorderAttr'] 1118 ) { 1119 return $borderAttr; 1120 } 1121 return ''; 1122 } 1123 1124 /** 1125 * Returns the html-template for rendering the image-Tag if no template is defined via typoscript the 1126 * default <img> tag template is returned 1127 * 1128 * @param string $layoutKey rendering key 1129 * @param array $conf TypoScript configuration properties 1130 * @return string 1131 */ 1132 public function getImageTagTemplate($layoutKey, $conf) 1133 { 1134 if ($layoutKey && isset($conf['layout.']) && isset($conf['layout.'][$layoutKey . '.'])) { 1135 $imageTagLayout = $this->stdWrap( 1136 $conf['layout.'][$layoutKey . '.']['element'] ?? '', 1137 $conf['layout.'][$layoutKey . '.']['element.'] ?? [] 1138 ); 1139 } else { 1140 $imageTagLayout = '<img src="###SRC###" width="###WIDTH###" height="###HEIGHT###" ###PARAMS### ###ALTPARAMS### ###BORDER######SELFCLOSINGTAGSLASH###>'; 1141 } 1142 return $imageTagLayout; 1143 } 1144 1145 /** 1146 * Render alternate sources for the image tag. If no source collection is given an empty string is returned. 1147 * 1148 * @param string $layoutKey rendering key 1149 * @param array $conf TypoScript configuration properties 1150 * @param string $file 1151 * @throws \UnexpectedValueException 1152 * @return string 1153 */ 1154 public function getImageSourceCollection($layoutKey, $conf, $file) 1155 { 1156 $sourceCollection = ''; 1157 if ($layoutKey 1158 && isset($conf['sourceCollection.']) && $conf['sourceCollection.'] 1159 && ( 1160 isset($conf['layout.'][$layoutKey . '.']['source']) && $conf['layout.'][$layoutKey . '.']['source'] 1161 || isset($conf['layout.'][$layoutKey . '.']['source.']) && $conf['layout.'][$layoutKey . '.']['source.'] 1162 ) 1163 ) { 1164 1165 // find active sourceCollection 1166 $activeSourceCollections = []; 1167 foreach ($conf['sourceCollection.'] as $sourceCollectionKey => $sourceCollectionConfiguration) { 1168 if (substr($sourceCollectionKey, -1) === '.') { 1169 if (empty($sourceCollectionConfiguration['if.']) || $this->checkIf($sourceCollectionConfiguration['if.'])) { 1170 $activeSourceCollections[] = $sourceCollectionConfiguration; 1171 } 1172 } 1173 } 1174 1175 // apply option split to configurations 1176 $tsfe = $this->getTypoScriptFrontendController(); 1177 $typoScriptService = GeneralUtility::makeInstance(TypoScriptService::class); 1178 $srcLayoutOptionSplitted = $typoScriptService->explodeConfigurationForOptionSplit((array)$conf['layout.'][$layoutKey . '.'], count($activeSourceCollections)); 1179 1180 // render sources 1181 foreach ($activeSourceCollections as $key => $sourceConfiguration) { 1182 $sourceLayout = $this->stdWrap( 1183 $srcLayoutOptionSplitted[$key]['source'] ?? '', 1184 $srcLayoutOptionSplitted[$key]['source.'] ?? [] 1185 ); 1186 1187 $sourceRenderConfiguration = [ 1188 'file' => $file, 1189 'file.' => $conf['file.'] ?? null 1190 ]; 1191 1192 if (isset($sourceConfiguration['quality']) || isset($sourceConfiguration['quality.'])) { 1193 $imageQuality = $sourceConfiguration['quality'] ?? ''; 1194 if (isset($sourceConfiguration['quality.'])) { 1195 $imageQuality = $this->stdWrap($sourceConfiguration['quality'], $sourceConfiguration['quality.']); 1196 } 1197 if ($imageQuality) { 1198 $sourceRenderConfiguration['file.']['params'] = '-quality ' . (int)$imageQuality; 1199 } 1200 } 1201 1202 if (isset($sourceConfiguration['pixelDensity'])) { 1203 $pixelDensity = (int)$this->stdWrap( 1204 $sourceConfiguration['pixelDensity'] ?? '', 1205 $sourceConfiguration['pixelDensity.'] ?? [] 1206 ); 1207 } else { 1208 $pixelDensity = 1; 1209 } 1210 $dimensionKeys = ['width', 'height', 'maxW', 'minW', 'maxH', 'minH', 'maxWidth', 'maxHeight', 'XY']; 1211 foreach ($dimensionKeys as $dimensionKey) { 1212 $dimension = $this->stdWrap( 1213 $sourceConfiguration[$dimensionKey] ?? '', 1214 $sourceConfiguration[$dimensionKey . '.'] ?? [] 1215 ); 1216 if (!$dimension) { 1217 $dimension = $this->stdWrap( 1218 $conf['file.'][$dimensionKey] ?? '', 1219 $conf['file.'][$dimensionKey . '.'] ?? [] 1220 ); 1221 } 1222 if ($dimension) { 1223 if (strstr($dimension, 'c') !== false && ($dimensionKey === 'width' || $dimensionKey === 'height')) { 1224 $dimensionParts = explode('c', $dimension, 2); 1225 $dimension = ((int)$dimensionParts[0] * $pixelDensity) . 'c'; 1226 if ($dimensionParts[1]) { 1227 $dimension .= $dimensionParts[1]; 1228 } 1229 } elseif ($dimensionKey === 'XY') { 1230 $dimensionParts = GeneralUtility::intExplode(',', $dimension, false, 2); 1231 $dimension = $dimensionParts[0] * $pixelDensity; 1232 if ($dimensionParts[1]) { 1233 $dimension .= ',' . $dimensionParts[1] * $pixelDensity; 1234 } 1235 } else { 1236 $dimension = (int)$dimension * $pixelDensity; 1237 } 1238 $sourceRenderConfiguration['file.'][$dimensionKey] = $dimension; 1239 // Remove the stdWrap properties for dimension as they have been processed already above. 1240 unset($sourceRenderConfiguration['file.'][$dimensionKey . '.']); 1241 } 1242 } 1243 $sourceInfo = $this->getImgResource($sourceRenderConfiguration['file'], $sourceRenderConfiguration['file.']); 1244 if ($sourceInfo) { 1245 $sourceConfiguration['width'] = $sourceInfo[0]; 1246 $sourceConfiguration['height'] = $sourceInfo[1]; 1247 $urlPrefix = ''; 1248 if (parse_url($sourceInfo[3], PHP_URL_HOST) === null) { 1249 $urlPrefix = $tsfe->absRefPrefix; 1250 } 1251 $sourceConfiguration['src'] = htmlspecialchars($urlPrefix . $sourceInfo[3]); 1252 $sourceConfiguration['selfClosingTagSlash'] = !empty($tsfe->xhtmlDoctype) ? ' /' : ''; 1253 1254 $markerTemplateEngine = GeneralUtility::makeInstance(MarkerBasedTemplateService::class); 1255 $oneSourceCollection = $markerTemplateEngine->substituteMarkerArray($sourceLayout, $sourceConfiguration, '###|###', true, true); 1256 1257 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['getImageSourceCollection'] ?? [] as $className) { 1258 $hookObject = GeneralUtility::makeInstance($className); 1259 if (!$hookObject instanceof ContentObjectOneSourceCollectionHookInterface) { 1260 throw new \UnexpectedValueException( 1261 '$hookObject must implement interface ' . ContentObjectOneSourceCollectionHookInterface::class, 1262 1380007853 1263 ); 1264 } 1265 $oneSourceCollection = $hookObject->getOneSourceCollection((array)$sourceRenderConfiguration, (array)$sourceConfiguration, $oneSourceCollection, $this); 1266 } 1267 1268 $sourceCollection .= $oneSourceCollection; 1269 } 1270 } 1271 } 1272 return $sourceCollection; 1273 } 1274 1275 /** 1276 * Wraps the input string in link-tags that opens the image in a new window. 1277 * 1278 * @param string $string String to wrap, probably an <img> tag 1279 * @param string|File|FileReference $imageFile The original image file 1280 * @param array $conf TypoScript properties for the "imageLinkWrap" function 1281 * @return string The input string, $string, wrapped as configured. 1282 * @see cImage() 1283 */ 1284 public function imageLinkWrap($string, $imageFile, $conf) 1285 { 1286 $string = (string)$string; 1287 $enable = isset($conf['enable.']) ? $this->stdWrap($conf['enable'], $conf['enable.']) : $conf['enable']; 1288 if (!$enable) { 1289 return $string; 1290 } 1291 $content = (string)$this->typoLink($string, $conf['typolink.']); 1292 if (isset($conf['file.'])) { 1293 $imageFile = $this->stdWrap($imageFile, $conf['file.']); 1294 } 1295 1296 if ($imageFile instanceof File) { 1297 $file = $imageFile; 1298 } elseif ($imageFile instanceof FileReference) { 1299 $file = $imageFile->getOriginalFile(); 1300 } else { 1301 if (MathUtility::canBeInterpretedAsInteger($imageFile)) { 1302 $file = ResourceFactory::getInstance()->getFileObject((int)$imageFile); 1303 } else { 1304 $file = ResourceFactory::getInstance()->getFileObjectFromCombinedIdentifier($imageFile); 1305 } 1306 } 1307 1308 // Create imageFileLink if not created with typolink 1309 if ($content === $string && $file !== null) { 1310 $parameterNames = ['width', 'height', 'effects', 'bodyTag', 'title', 'wrap', 'crop']; 1311 $parameters = []; 1312 $sample = isset($conf['sample.']) ? $this->stdWrap($conf['sample'], $conf['sample.']) : $conf['sample']; 1313 if ($sample) { 1314 $parameters['sample'] = 1; 1315 } 1316 foreach ($parameterNames as $parameterName) { 1317 if (isset($conf[$parameterName . '.'])) { 1318 $conf[$parameterName] = $this->stdWrap($conf[$parameterName], $conf[$parameterName . '.']); 1319 } 1320 if (isset($conf[$parameterName]) && $conf[$parameterName]) { 1321 $parameters[$parameterName] = $conf[$parameterName]; 1322 } 1323 } 1324 $parametersEncoded = base64_encode(json_encode($parameters)); 1325 $hmac = GeneralUtility::hmac(implode('|', [$file->getUid(), $parametersEncoded])); 1326 $params = '&md5=' . $hmac; 1327 foreach (str_split($parametersEncoded, 64) as $index => $chunk) { 1328 $params .= '¶meters' . rawurlencode('[') . $index . rawurlencode(']') . '=' . rawurlencode($chunk); 1329 } 1330 $url = $this->getTypoScriptFrontendController()->absRefPrefix . 'index.php?eID=tx_cms_showpic&file=' . $file->getUid() . $params; 1331 $directImageLink = isset($conf['directImageLink.']) ? $this->stdWrap($conf['directImageLink'], $conf['directImageLink.']) : $conf['directImageLink']; 1332 if ($directImageLink) { 1333 $imgResourceConf = [ 1334 'file' => $imageFile, 1335 'file.' => $conf 1336 ]; 1337 $url = $this->cObjGetSingle('IMG_RESOURCE', $imgResourceConf); 1338 if (!$url) { 1339 // If no imagemagick / gm is available 1340 $url = $imageFile; 1341 } 1342 } 1343 // Create TARGET-attribute only if the right doctype is used 1344 $target = ''; 1345 $xhtmlDocType = $this->getTypoScriptFrontendController()->xhtmlDoctype; 1346 if ($xhtmlDocType !== 'xhtml_strict' && $xhtmlDocType !== 'xhtml_11') { 1347 $target = isset($conf['target.']) 1348 ? (string)$this->stdWrap($conf['target'], $conf['target.']) 1349 : (string)$conf['target']; 1350 if ($target === '') { 1351 $target = 'thePicture'; 1352 } 1353 } 1354 $a1 = ''; 1355 $a2 = ''; 1356 $conf['JSwindow'] = isset($conf['JSwindow.']) ? $this->stdWrap($conf['JSwindow'], $conf['JSwindow.']) : $conf['JSwindow']; 1357 if ($conf['JSwindow']) { 1358 if ($conf['JSwindow.']['altUrl'] || $conf['JSwindow.']['altUrl.']) { 1359 $altUrl = isset($conf['JSwindow.']['altUrl.']) ? $this->stdWrap($conf['JSwindow.']['altUrl'], $conf['JSwindow.']['altUrl.']) : $conf['JSwindow.']['altUrl']; 1360 if ($altUrl) { 1361 $url = $altUrl . ($conf['JSwindow.']['altUrl_noDefaultParams'] ? '' : '?file=' . rawurlencode($imageFile) . $params); 1362 } 1363 } 1364 1365 $processedFile = $file->process(ProcessedFile::CONTEXT_IMAGECROPSCALEMASK, $conf); 1366 $JSwindowExpand = isset($conf['JSwindow.']['expand.']) ? $this->stdWrap($conf['JSwindow.']['expand'], $conf['JSwindow.']['expand.']) : $conf['JSwindow.']['expand']; 1367 $offset = GeneralUtility::intExplode(',', $JSwindowExpand . ','); 1368 $newWindow = isset($conf['JSwindow.']['newWindow.']) ? $this->stdWrap($conf['JSwindow.']['newWindow'], $conf['JSwindow.']['newWindow.']) : $conf['JSwindow.']['newWindow']; 1369 $onClick = 'openPic(' 1370 . GeneralUtility::quoteJSvalue($this->getTypoScriptFrontendController()->baseUrlWrap($url)) . ',' 1371 . '\'' . ($newWindow ? md5($url) : 'thePicture') . '\',' 1372 . GeneralUtility::quoteJSvalue('width=' . ($processedFile->getProperty('width') + $offset[0]) 1373 . ',height=' . ($processedFile->getProperty('height') + $offset[1]) . ',status=0,menubar=0') 1374 . '); return false;'; 1375 $a1 = '<a href="' . htmlspecialchars($url) . '"' 1376 . ' onclick="' . htmlspecialchars($onClick) . '"' 1377 . ($target !== '' ? ' target="' . htmlspecialchars($target) . '"' : '') 1378 . $this->getTypoScriptFrontendController()->ATagParams . '>'; 1379 $a2 = '</a>'; 1380 $this->getTypoScriptFrontendController()->setJS('openPic'); 1381 } else { 1382 $conf['linkParams.']['directImageLink'] = (bool)$conf['directImageLink']; 1383 $conf['linkParams.']['parameter'] = $url; 1384 $string = $this->typoLink($string, $conf['linkParams.']); 1385 } 1386 if (isset($conf['stdWrap.'])) { 1387 $string = $this->stdWrap($string, $conf['stdWrap.']); 1388 } 1389 $content = $a1 . $string . $a2; 1390 } 1391 return $content; 1392 } 1393 1394 /** 1395 * Sets the SYS_LASTCHANGED timestamp if input timestamp is larger than current value. 1396 * The SYS_LASTCHANGED timestamp can be used by various caching/indexing applications to determine if the page has new content. 1397 * Therefore you should call this function with the last-changed timestamp of any element you display. 1398 * 1399 * @param int $tstamp Unix timestamp (number of seconds since 1970) 1400 * @see TypoScriptFrontendController::setSysLastChanged() 1401 */ 1402 public function lastChanged($tstamp) 1403 { 1404 $tstamp = (int)$tstamp; 1405 $tsfe = $this->getTypoScriptFrontendController(); 1406 if ($tstamp > (int)$tsfe->register['SYS_LASTCHANGED']) { 1407 $tsfe->register['SYS_LASTCHANGED'] = $tstamp; 1408 } 1409 } 1410 1411 /** 1412 * Wraps the input string by the $wrap value and implements the "linkWrap" data type as well. 1413 * The "linkWrap" data type means that this function will find any integer encapsulated in {} (curly braces) in the first wrap part and substitute it with the corresponding page uid from the rootline where the found integer is pointing to the key in the rootline. See link below. 1414 * 1415 * @param string $content Input string 1416 * @param string $wrap A string where the first two parts separated by "|" (vertical line) will be wrapped around the input string 1417 * @return string Wrapped output string 1418 * @see wrap(), cImage(), FILE() 1419 */ 1420 public function linkWrap($content, $wrap) 1421 { 1422 $wrapArr = explode('|', $wrap); 1423 if (preg_match('/\\{([0-9]*)\\}/', $wrapArr[0], $reg)) { 1424 $uid = $this->getTypoScriptFrontendController()->tmpl->rootLine[$reg[1]]['uid'] ?? null; 1425 if ($uid) { 1426 $wrapArr[0] = str_replace($reg[0], $uid, $wrapArr[0]); 1427 } 1428 } 1429 return trim($wrapArr[0] ?? '') . $content . trim($wrapArr[1] ?? ''); 1430 } 1431 1432 /** 1433 * An abstraction method which creates an alt or title parameter for an HTML img, applet, area or input element and the FILE content element. 1434 * From the $conf array it implements the properties "altText", "titleText" and "longdescURL" 1435 * 1436 * @param array $conf TypoScript configuration properties 1437 * @param bool $longDesc If set, the longdesc attribute will be generated - must only be used for img elements! 1438 * @return string Parameter string containing alt and title parameters (if any) 1439 * @see IMGTEXT(), FILE(), FORM(), cImage(), filelink() 1440 */ 1441 public function getAltParam($conf, $longDesc = true) 1442 { 1443 $altText = isset($conf['altText.']) ? trim($this->stdWrap($conf['altText'], $conf['altText.'])) : trim($conf['altText']); 1444 $titleText = isset($conf['titleText.']) ? trim($this->stdWrap($conf['titleText'], $conf['titleText.'])) : trim($conf['titleText']); 1445 if (isset($conf['longdescURL.']) && $this->getTypoScriptFrontendController()->config['config']['doctype'] !== 'html5') { 1446 $longDescUrl = $this->typoLink_URL($conf['longdescURL.']); 1447 } else { 1448 $longDescUrl = trim($conf['longdescURL']); 1449 } 1450 $longDescUrl = strip_tags($longDescUrl); 1451 1452 // "alt": 1453 $altParam = ' alt="' . htmlspecialchars($altText) . '"'; 1454 // "title": 1455 $emptyTitleHandling = isset($conf['emptyTitleHandling.']) ? $this->stdWrap($conf['emptyTitleHandling'], $conf['emptyTitleHandling.']) : $conf['emptyTitleHandling']; 1456 // Choices: 'keepEmpty' | 'useAlt' | 'removeAttr' 1457 if ($titleText || $emptyTitleHandling === 'keepEmpty') { 1458 $altParam .= ' title="' . htmlspecialchars($titleText) . '"'; 1459 } elseif (!$titleText && $emptyTitleHandling === 'useAlt') { 1460 $altParam .= ' title="' . htmlspecialchars($altText) . '"'; 1461 } 1462 // "longDesc" URL 1463 if ($longDesc && !empty($longDescUrl)) { 1464 $altParam .= ' longdesc="' . htmlspecialchars($longDescUrl) . '"'; 1465 } 1466 return $altParam; 1467 } 1468 1469 /** 1470 * An abstraction method to add parameters to an A tag. 1471 * Uses the ATagParams property. 1472 * 1473 * @param array $conf TypoScript configuration properties 1474 * @param bool|int $addGlobal If set, will add the global config.ATagParams to the link 1475 * @return string String containing the parameters to the A tag (if non empty, with a leading space) 1476 * @see IMGTEXT(), filelink(), makelinks(), typolink() 1477 */ 1478 public function getATagParams($conf, $addGlobal = 1) 1479 { 1480 $aTagParams = ''; 1481 if ($conf['ATagParams.'] ?? false) { 1482 $aTagParams = ' ' . $this->stdWrap($conf['ATagParams'], $conf['ATagParams.']); 1483 } elseif ($conf['ATagParams'] ?? false) { 1484 $aTagParams = ' ' . $conf['ATagParams']; 1485 } 1486 if ($addGlobal) { 1487 $aTagParams = ' ' . trim($this->getTypoScriptFrontendController()->ATagParams . $aTagParams); 1488 } 1489 // Extend params 1490 $_params = [ 1491 'conf' => &$conf, 1492 'aTagParams' => &$aTagParams 1493 ]; 1494 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['getATagParamsPostProc'] ?? [] as $className) { 1495 $processor = GeneralUtility::makeInstance($className); 1496 $aTagParams = $processor->process($_params, $this); 1497 } 1498 1499 $aTagParams = trim($aTagParams); 1500 if (!empty($aTagParams)) { 1501 $aTagParams = ' ' . $aTagParams; 1502 } 1503 1504 return $aTagParams; 1505 } 1506 1507 /** 1508 * All extension links should ask this function for additional properties to their tags. 1509 * Designed to add for instance an "onclick" property for site tracking systems. 1510 * 1511 * @param string $URL URL of the website 1512 * @param string $TYPE 1513 * @return string The additional tag properties 1514 */ 1515 public function extLinkATagParams($URL, $TYPE) 1516 { 1517 $out = ''; 1518 if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['extLinkATagParamsHandler'])) { 1519 $extLinkATagParamsHandler = GeneralUtility::makeInstance($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['extLinkATagParamsHandler']); 1520 if (method_exists($extLinkATagParamsHandler, 'main')) { 1521 $out .= trim($extLinkATagParamsHandler->main($URL, $TYPE, $this)); 1522 } 1523 } 1524 return trim($out) ? ' ' . trim($out) : ''; 1525 } 1526 1527 /*********************************************** 1528 * 1529 * HTML template processing functions 1530 * 1531 ***********************************************/ 1532 1533 /** 1534 * Sets the current file object during iterations over files. 1535 * 1536 * @param File $fileObject The file object. 1537 */ 1538 public function setCurrentFile($fileObject) 1539 { 1540 $this->currentFile = $fileObject; 1541 } 1542 1543 /** 1544 * Gets the current file object during iterations over files. 1545 * 1546 * @return File The current file object. 1547 */ 1548 public function getCurrentFile() 1549 { 1550 return $this->currentFile; 1551 } 1552 1553 /*********************************************** 1554 * 1555 * "stdWrap" + sub functions 1556 * 1557 ***********************************************/ 1558 /** 1559 * The "stdWrap" function. This is the implementation of what is known as "stdWrap properties" in TypoScript. 1560 * Basically "stdWrap" performs some processing of a value based on properties in the input $conf array(holding the TypoScript "stdWrap properties") 1561 * See the link below for a complete list of properties and what they do. The order of the table with properties found in TSref (the link) follows the actual order of implementation in this function. 1562 * 1563 * If $this->alternativeData is an array it's used instead of the $this->data array in ->getData 1564 * 1565 * @param string $content Input value undergoing processing in this function. Possibly substituted by other values fetched from another source. 1566 * @param array $conf TypoScript "stdWrap properties". 1567 * @return string The processed input value 1568 */ 1569 public function stdWrap($content = '', $conf = []) 1570 { 1571 $content = (string)$content; 1572 // If there is any hook object, activate all of the process and override functions. 1573 // The hook interface ContentObjectStdWrapHookInterface takes care that all 4 methods exist. 1574 if ($this->stdWrapHookObjects) { 1575 $conf['stdWrapPreProcess'] = 1; 1576 $conf['stdWrapOverride'] = 1; 1577 $conf['stdWrapProcess'] = 1; 1578 $conf['stdWrapPostProcess'] = 1; 1579 } 1580 1581 if (!is_array($conf) || !$conf) { 1582 return $content; 1583 } 1584 1585 // Cache handling 1586 if (isset($conf['cache.']) && is_array($conf['cache.'])) { 1587 $conf['cache.']['key'] = $this->stdWrap($conf['cache.']['key'], $conf['cache.']['key.']); 1588 $conf['cache.']['tags'] = $this->stdWrap($conf['cache.']['tags'], $conf['cache.']['tags.']); 1589 $conf['cache.']['lifetime'] = $this->stdWrap($conf['cache.']['lifetime'], $conf['cache.']['lifetime.']); 1590 $conf['cacheRead'] = 1; 1591 $conf['cacheStore'] = 1; 1592 } 1593 // The configuration is sorted and filtered by intersection with the defined stdWrapOrder. 1594 $sortedConf = array_keys(array_intersect_key($this->stdWrapOrder, $conf)); 1595 // Functions types that should not make use of nested stdWrap function calls to avoid conflicts with internal TypoScript used by these functions 1596 $stdWrapDisabledFunctionTypes = 'cObject,functionName,stdWrap'; 1597 // Additional Array to check whether a function has already been executed 1598 $isExecuted = []; 1599 // Additional switch to make sure 'required', 'if' and 'fieldRequired' 1600 // will still stop rendering immediately in case they return FALSE 1601 $this->stdWrapRecursionLevel++; 1602 $this->stopRendering[$this->stdWrapRecursionLevel] = false; 1603 // execute each function in the predefined order 1604 foreach ($sortedConf as $stdWrapName) { 1605 // eliminate the second key of a pair 'key'|'key.' to make sure functions get called only once and check if rendering has been stopped 1606 if ((!isset($isExecuted[$stdWrapName]) || !$isExecuted[$stdWrapName]) && !$this->stopRendering[$this->stdWrapRecursionLevel]) { 1607 $functionName = rtrim($stdWrapName, '.'); 1608 $functionProperties = $functionName . '.'; 1609 $functionType = $this->stdWrapOrder[$functionName] ?? null; 1610 // If there is any code on the next level, check if it contains "official" stdWrap functions 1611 // if yes, execute them first - will make each function stdWrap aware 1612 // so additional stdWrap calls within the functions can be removed, since the result will be the same 1613 if (!empty($conf[$functionProperties]) && !GeneralUtility::inList($stdWrapDisabledFunctionTypes, $functionType)) { 1614 if (array_intersect_key($this->stdWrapOrder, $conf[$functionProperties])) { 1615 // Check if there's already content available before processing 1616 // any ifEmpty or ifBlank stdWrap properties 1617 if (($functionName === 'ifBlank' && $content !== '') || 1618 ($functionName === 'ifEmpty' && trim($content) !== '')) { 1619 continue; 1620 } 1621 1622 $conf[$functionName] = $this->stdWrap($conf[$functionName] ?? '', $conf[$functionProperties] ?? []); 1623 } 1624 } 1625 // Check if key is still containing something, since it might have been changed by next level stdWrap before 1626 if ((isset($conf[$functionName]) || $conf[$functionProperties]) 1627 && ($functionType !== 'boolean' || $conf[$functionName]) 1628 ) { 1629 // Get just that part of $conf that is needed for the particular function 1630 $singleConf = [ 1631 $functionName => $conf[$functionName] ?? null, 1632 $functionProperties => $conf[$functionProperties] ?? null 1633 ]; 1634 // Hand over the whole $conf array to the stdWrapHookObjects 1635 if ($functionType === 'hook') { 1636 $singleConf = $conf; 1637 } 1638 // Add both keys - with and without the dot - to the set of executed functions 1639 $isExecuted[$functionName] = true; 1640 $isExecuted[$functionProperties] = true; 1641 // Call the function with the prefix stdWrap_ to make sure nobody can execute functions just by adding their name to the TS Array 1642 $functionName = 'stdWrap_' . $functionName; 1643 $content = $this->{$functionName}($content, $singleConf); 1644 } elseif ($functionType === 'boolean' && !$conf[$functionName]) { 1645 $isExecuted[$functionName] = true; 1646 $isExecuted[$functionProperties] = true; 1647 } 1648 } 1649 } 1650 unset($this->stopRendering[$this->stdWrapRecursionLevel]); 1651 $this->stdWrapRecursionLevel--; 1652 1653 return $content; 1654 } 1655 1656 /** 1657 * Gets a configuration value by passing them through stdWrap first and taking a default value if stdWrap doesn't yield a result. 1658 * 1659 * @param string $key The config variable key (from TS array). 1660 * @param array $config The TypoScript array. 1661 * @param string $defaultValue Optional default value. 1662 * @return string Value of the config variable 1663 */ 1664 public function stdWrapValue($key, array $config, $defaultValue = '') 1665 { 1666 if (isset($config[$key])) { 1667 if (!isset($config[$key . '.'])) { 1668 return $config[$key]; 1669 } 1670 } elseif (isset($config[$key . '.'])) { 1671 $config[$key] = ''; 1672 } else { 1673 return $defaultValue; 1674 } 1675 $stdWrapped = $this->stdWrap($config[$key], $config[$key . '.']); 1676 return $stdWrapped ?: $defaultValue; 1677 } 1678 1679 /** 1680 * stdWrap pre process hook 1681 * can be used by extensions authors to modify the behaviour of stdWrap functions to their needs 1682 * this hook will execute functions before any other stdWrap function can modify anything 1683 * 1684 * @param string $content Input value undergoing processing in these functions. 1685 * @param array $conf All stdWrap properties, not just the ones for a particular function. 1686 * @return string The processed input value 1687 */ 1688 public function stdWrap_stdWrapPreProcess($content = '', $conf = []) 1689 { 1690 foreach ($this->stdWrapHookObjects as $hookObject) { 1691 /** @var ContentObjectStdWrapHookInterface $hookObject */ 1692 $content = $hookObject->stdWrapPreProcess($content, $conf, $this); 1693 } 1694 return $content; 1695 } 1696 1697 /** 1698 * Check if content was cached before (depending on the given cache key) 1699 * 1700 * @param string $content Input value undergoing processing in these functions. 1701 * @param array $conf All stdWrap properties, not just the ones for a particular function. 1702 * @return string The processed input value 1703 */ 1704 public function stdWrap_cacheRead($content = '', $conf = []) 1705 { 1706 if (!isset($conf['cache.'])) { 1707 return $content; 1708 } 1709 $result = $this->getFromCache($conf['cache.']); 1710 return $result === false ? $content : $result; 1711 } 1712 1713 /** 1714 * Add tags to page cache (comma-separated list) 1715 * 1716 * @param string $content Input value undergoing processing in these functions. 1717 * @param array $conf All stdWrap properties, not just the ones for a particular function. 1718 * @return string The processed input value 1719 */ 1720 public function stdWrap_addPageCacheTags($content = '', $conf = []) 1721 { 1722 $tags = isset($conf['addPageCacheTags.']) 1723 ? $this->stdWrap($conf['addPageCacheTags'], $conf['addPageCacheTags.']) 1724 : $conf['addPageCacheTags']; 1725 if (!empty($tags)) { 1726 $cacheTags = GeneralUtility::trimExplode(',', $tags, true); 1727 $this->getTypoScriptFrontendController()->addCacheTags($cacheTags); 1728 } 1729 return $content; 1730 } 1731 1732 /** 1733 * setContentToCurrent 1734 * actually it just does the contrary: Sets the value of 'current' based on current content 1735 * 1736 * @param string $content Input value undergoing processing in this function. 1737 * @return string The processed input value 1738 */ 1739 public function stdWrap_setContentToCurrent($content = '') 1740 { 1741 $this->data[$this->currentValKey] = $content; 1742 return $content; 1743 } 1744 1745 /** 1746 * setCurrent 1747 * Sets the value of 'current' based on the outcome of stdWrap operations 1748 * 1749 * @param string $content Input value undergoing processing in this function. 1750 * @param array $conf stdWrap properties for setCurrent. 1751 * @return string The processed input value 1752 */ 1753 public function stdWrap_setCurrent($content = '', $conf = []) 1754 { 1755 $this->data[$this->currentValKey] = $conf['setCurrent'] ?? null; 1756 return $content; 1757 } 1758 1759 /** 1760 * lang 1761 * Translates content based on the language currently used by the FE 1762 * 1763 * @param string $content Input value undergoing processing in this function. 1764 * @param array $conf stdWrap properties for lang. 1765 * @return string The processed input value 1766 */ 1767 public function stdWrap_lang($content = '', $conf = []) 1768 { 1769 $request = $GLOBALS['TYPO3_REQUEST'] ?? null; 1770 $siteLanguage = $request ? $request->getAttribute('language') : null; 1771 if ($siteLanguage instanceof SiteLanguage) { 1772 $currentLanguageCode = $siteLanguage->getTypo3Language(); 1773 } else { 1774 $tsfe = $this->getTypoScriptFrontendController(); 1775 $currentLanguageCode = $tsfe->config['config']['language'] ?? null; 1776 } 1777 if ($currentLanguageCode && isset($conf['lang.'][$currentLanguageCode])) { 1778 $content = $conf['lang.'][$currentLanguageCode]; 1779 } 1780 return $content; 1781 } 1782 1783 /** 1784 * data 1785 * Gets content from different sources based on getText functions, makes use of alternativeData, when set 1786 * 1787 * @param string $content Input value undergoing processing in this function. 1788 * @param array $conf stdWrap properties for data. 1789 * @return string The processed input value 1790 */ 1791 public function stdWrap_data($content = '', $conf = []) 1792 { 1793 $content = $this->getData($conf['data'], is_array($this->alternativeData) ? $this->alternativeData : $this->data); 1794 // This must be unset directly after 1795 $this->alternativeData = ''; 1796 return $content; 1797 } 1798 1799 /** 1800 * field 1801 * Gets content from a DB field 1802 * 1803 * @param string $content Input value undergoing processing in this function. 1804 * @param array $conf stdWrap properties for field. 1805 * @return string The processed input value 1806 */ 1807 public function stdWrap_field($content = '', $conf = []) 1808 { 1809 return $this->getFieldVal($conf['field']); 1810 } 1811 1812 /** 1813 * current 1814 * Gets content that has been perviously set as 'current' 1815 * Can be set via setContentToCurrent or setCurrent or will be set automatically i.e. inside the split function 1816 * 1817 * @param string $content Input value undergoing processing in this function. 1818 * @param array $conf stdWrap properties for current. 1819 * @return string The processed input value 1820 */ 1821 public function stdWrap_current($content = '', $conf = []) 1822 { 1823 return $this->data[$this->currentValKey]; 1824 } 1825 1826 /** 1827 * cObject 1828 * Will replace the content with the value of an official TypoScript cObject 1829 * like TEXT, COA, HMENU 1830 * 1831 * @param string $content Input value undergoing processing in this function. 1832 * @param array $conf stdWrap properties for cObject. 1833 * @return string The processed input value 1834 */ 1835 public function stdWrap_cObject($content = '', $conf = []) 1836 { 1837 return $this->cObjGetSingle($conf['cObject'] ?? '', $conf['cObject.'] ?? [], '/stdWrap/.cObject'); 1838 } 1839 1840 /** 1841 * numRows 1842 * Counts the number of returned records of a DB operation 1843 * makes use of select internally 1844 * 1845 * @param string $content Input value undergoing processing in this function. 1846 * @param array $conf stdWrap properties for numRows. 1847 * @return string The processed input value 1848 */ 1849 public function stdWrap_numRows($content = '', $conf = []) 1850 { 1851 return $this->numRows($conf['numRows.']); 1852 } 1853 1854 /** 1855 * filelist 1856 * Will create a list of files based on some additional parameters 1857 * 1858 * @param string $content Input value undergoing processing in this function. 1859 * @param array $conf stdWrap properties for filelist. 1860 * @return string The processed input value 1861 * @deprecated since TYPO3 v9.5, will be removed in TYPO3 v10.0. Use cObject FILES instead. 1862 */ 1863 public function stdWrap_filelist($content = '', $conf = []) 1864 { 1865 return $this->filelist($conf['filelist'], true); 1866 } 1867 1868 /** 1869 * preUserFunc 1870 * Will execute a user public function before the content will be modified by any other stdWrap function 1871 * 1872 * @param string $content Input value undergoing processing in this function. 1873 * @param array $conf stdWrap properties for preUserFunc. 1874 * @return string The processed input value 1875 */ 1876 public function stdWrap_preUserFunc($content = '', $conf = []) 1877 { 1878 return $this->callUserFunction($conf['preUserFunc'], $conf['preUserFunc.'], $content); 1879 } 1880 1881 /** 1882 * stdWrap override hook 1883 * can be used by extensions authors to modify the behaviour of stdWrap functions to their needs 1884 * this hook will execute functions on existing content but still before the content gets modified or replaced 1885 * 1886 * @param string $content Input value undergoing processing in these functions. 1887 * @param array $conf All stdWrap properties, not just the ones for a particular function. 1888 * @return string The processed input value 1889 */ 1890 public function stdWrap_stdWrapOverride($content = '', $conf = []) 1891 { 1892 foreach ($this->stdWrapHookObjects as $hookObject) { 1893 /** @var ContentObjectStdWrapHookInterface $hookObject */ 1894 $content = $hookObject->stdWrapOverride($content, $conf, $this); 1895 } 1896 return $content; 1897 } 1898 1899 /** 1900 * override 1901 * Will override the current value of content with its own value' 1902 * 1903 * @param string $content Input value undergoing processing in this function. 1904 * @param array $conf stdWrap properties for override. 1905 * @return string The processed input value 1906 */ 1907 public function stdWrap_override($content = '', $conf = []) 1908 { 1909 if (trim($conf['override'] ?? false)) { 1910 $content = $conf['override']; 1911 } 1912 return $content; 1913 } 1914 1915 /** 1916 * preIfEmptyListNum 1917 * Gets a value off a CSV list before the following ifEmpty check 1918 * Makes sure that the result of ifEmpty will be TRUE in case the CSV does not contain a value at the position given by preIfEmptyListNum 1919 * 1920 * @param string $content Input value undergoing processing in this function. 1921 * @param array $conf stdWrap properties for preIfEmptyListNum. 1922 * @return string The processed input value 1923 */ 1924 public function stdWrap_preIfEmptyListNum($content = '', $conf = []) 1925 { 1926 return $this->listNum($content, $conf['preIfEmptyListNum'] ?? null, $conf['preIfEmptyListNum.']['splitChar'] ?? null); 1927 } 1928 1929 /** 1930 * ifNull 1931 * Will set content to a replacement value in case the value of content is NULL 1932 * 1933 * @param string|null $content Input value undergoing processing in this function. 1934 * @param array $conf stdWrap properties for ifNull. 1935 * @return string The processed input value 1936 */ 1937 public function stdWrap_ifNull($content = '', $conf = []) 1938 { 1939 return $content ?? $conf['ifNull']; 1940 } 1941 1942 /** 1943 * ifEmpty 1944 * Will set content to a replacement value in case the trimmed value of content returns FALSE 1945 * 0 (zero) will be replaced as well 1946 * 1947 * @param string $content Input value undergoing processing in this function. 1948 * @param array $conf stdWrap properties for ifEmpty. 1949 * @return string The processed input value 1950 */ 1951 public function stdWrap_ifEmpty($content = '', $conf = []) 1952 { 1953 if (!trim($content)) { 1954 $content = $conf['ifEmpty']; 1955 } 1956 return $content; 1957 } 1958 1959 /** 1960 * ifBlank 1961 * Will set content to a replacement value in case the trimmed value of content has no length 1962 * 0 (zero) will not be replaced 1963 * 1964 * @param string $content Input value undergoing processing in this function. 1965 * @param array $conf stdWrap properties for ifBlank. 1966 * @return string The processed input value 1967 */ 1968 public function stdWrap_ifBlank($content = '', $conf = []) 1969 { 1970 if (trim($content) === '') { 1971 $content = $conf['ifBlank']; 1972 } 1973 return $content; 1974 } 1975 1976 /** 1977 * listNum 1978 * Gets a value off a CSV list after ifEmpty check 1979 * Might return an empty value in case the CSV does not contain a value at the position given by listNum 1980 * Use preIfEmptyListNum to avoid that behaviour 1981 * 1982 * @param string $content Input value undergoing processing in this function. 1983 * @param array $conf stdWrap properties for listNum. 1984 * @return string The processed input value 1985 */ 1986 public function stdWrap_listNum($content = '', $conf = []) 1987 { 1988 return $this->listNum($content, $conf['listNum'] ?? null, $conf['listNum.']['splitChar'] ?? null); 1989 } 1990 1991 /** 1992 * trim 1993 * Cuts off any whitespace at the beginning and the end of the content 1994 * 1995 * @param string $content Input value undergoing processing in this function. 1996 * @return string The processed input value 1997 */ 1998 public function stdWrap_trim($content = '') 1999 { 2000 return trim($content); 2001 } 2002 2003 /** 2004 * strPad 2005 * Will return a string padded left/right/on both sides, based on configuration given as stdWrap properties 2006 * 2007 * @param string $content Input value undergoing processing in this function. 2008 * @param array $conf stdWrap properties for strPad. 2009 * @return string The processed input value 2010 */ 2011 public function stdWrap_strPad($content = '', $conf = []) 2012 { 2013 // Must specify a length in conf for this to make sense 2014 $length = 0; 2015 // Padding with space is PHP-default 2016 $padWith = ' '; 2017 // Padding on the right side is PHP-default 2018 $padType = STR_PAD_RIGHT; 2019 if (!empty($conf['strPad.']['length'])) { 2020 $length = isset($conf['strPad.']['length.']) ? $this->stdWrap($conf['strPad.']['length'], $conf['strPad.']['length.']) : $conf['strPad.']['length']; 2021 $length = (int)$length; 2022 } 2023 if (isset($conf['strPad.']['padWith']) && (string)$conf['strPad.']['padWith'] !== '') { 2024 $padWith = isset($conf['strPad.']['padWith.']) ? $this->stdWrap($conf['strPad.']['padWith'], $conf['strPad.']['padWith.']) : $conf['strPad.']['padWith']; 2025 } 2026 if (!empty($conf['strPad.']['type'])) { 2027 $type = isset($conf['strPad.']['type.']) ? $this->stdWrap($conf['strPad.']['type'], $conf['strPad.']['type.']) : $conf['strPad.']['type']; 2028 if (strtolower($type) === 'left') { 2029 $padType = STR_PAD_LEFT; 2030 } elseif (strtolower($type) === 'both') { 2031 $padType = STR_PAD_BOTH; 2032 } 2033 } 2034 return str_pad($content, $length, $padWith, $padType); 2035 } 2036 2037 /** 2038 * stdWrap 2039 * A recursive call of the stdWrap function set 2040 * This enables the user to execute stdWrap functions in another than the predefined order 2041 * It modifies the content, not the property 2042 * while the new feature of chained stdWrap functions modifies the property and not the content 2043 * 2044 * @param string $content Input value undergoing processing in this function. 2045 * @param array $conf stdWrap properties for stdWrap. 2046 * @return string The processed input value 2047 */ 2048 public function stdWrap_stdWrap($content = '', $conf = []) 2049 { 2050 return $this->stdWrap($content, $conf['stdWrap.']); 2051 } 2052 2053 /** 2054 * stdWrap process hook 2055 * can be used by extensions authors to modify the behaviour of stdWrap functions to their needs 2056 * this hook executes functions directly after the recursive stdWrap function call but still before the content gets modified 2057 * 2058 * @param string $content Input value undergoing processing in these functions. 2059 * @param array $conf All stdWrap properties, not just the ones for a particular function. 2060 * @return string The processed input value 2061 */ 2062 public function stdWrap_stdWrapProcess($content = '', $conf = []) 2063 { 2064 foreach ($this->stdWrapHookObjects as $hookObject) { 2065 /** @var ContentObjectStdWrapHookInterface $hookObject */ 2066 $content = $hookObject->stdWrapProcess($content, $conf, $this); 2067 } 2068 return $content; 2069 } 2070 2071 /** 2072 * required 2073 * Will immediately stop rendering and return an empty value 2074 * when there is no content at this point 2075 * 2076 * @param string $content Input value undergoing processing in this function. 2077 * @return string The processed input value 2078 */ 2079 public function stdWrap_required($content = '') 2080 { 2081 if ((string)$content === '') { 2082 $content = ''; 2083 $this->stopRendering[$this->stdWrapRecursionLevel] = true; 2084 } 2085 return $content; 2086 } 2087 2088 /** 2089 * if 2090 * Will immediately stop rendering and return an empty value 2091 * when the result of the checks returns FALSE 2092 * 2093 * @param string $content Input value undergoing processing in this function. 2094 * @param array $conf stdWrap properties for if. 2095 * @return string The processed input value 2096 */ 2097 public function stdWrap_if($content = '', $conf = []) 2098 { 2099 if (empty($conf['if.']) || $this->checkIf($conf['if.'])) { 2100 return $content; 2101 } 2102 $this->stopRendering[$this->stdWrapRecursionLevel] = true; 2103 return ''; 2104 } 2105 2106 /** 2107 * fieldRequired 2108 * Will immediately stop rendering and return an empty value 2109 * when there is no content in the field given by fieldRequired 2110 * 2111 * @param string $content Input value undergoing processing in this function. 2112 * @param array $conf stdWrap properties for fieldRequired. 2113 * @return string The processed input value 2114 */ 2115 public function stdWrap_fieldRequired($content = '', $conf = []) 2116 { 2117 if (!trim($this->data[$conf['fieldRequired'] ?? null] ?? '')) { 2118 $content = ''; 2119 $this->stopRendering[$this->stdWrapRecursionLevel] = true; 2120 } 2121 return $content; 2122 } 2123 2124 /** 2125 * stdWrap csConv: Converts the input to UTF-8 2126 * 2127 * The character set of the input must be specified. Returns the input if 2128 * matters go wrong, for example if an invalid character set is given. 2129 * 2130 * @param string $content The string to convert. 2131 * @param array $conf stdWrap properties for csConv. 2132 * @return string The processed input. 2133 */ 2134 public function stdWrap_csConv($content = '', $conf = []) 2135 { 2136 if (!empty($conf['csConv'])) { 2137 $output = mb_convert_encoding($content, 'utf-8', trim(strtolower($conf['csConv']))); 2138 return $output !== false && $output !== '' ? $output : $content; 2139 } 2140 return $content; 2141 } 2142 2143 /** 2144 * parseFunc 2145 * Will parse the content based on functions given as stdWrap properties 2146 * Heavily used together with RTE based content 2147 * 2148 * @param string $content Input value undergoing processing in this function. 2149 * @param array $conf stdWrap properties for parseFunc. 2150 * @return string The processed input value 2151 */ 2152 public function stdWrap_parseFunc($content = '', $conf = []) 2153 { 2154 return $this->parseFunc($content, $conf['parseFunc.'], $conf['parseFunc']); 2155 } 2156 2157 /** 2158 * HTMLparser 2159 * Will parse HTML content based on functions given as stdWrap properties 2160 * Heavily used together with RTE based content 2161 * 2162 * @param string $content Input value undergoing processing in this function. 2163 * @param array $conf stdWrap properties for HTMLparser. 2164 * @return string The processed input value 2165 */ 2166 public function stdWrap_HTMLparser($content = '', $conf = []) 2167 { 2168 if (isset($conf['HTMLparser.']) && is_array($conf['HTMLparser.'])) { 2169 $content = $this->HTMLparser_TSbridge($content, $conf['HTMLparser.']); 2170 } 2171 return $content; 2172 } 2173 2174 /** 2175 * split 2176 * Will split the content by a given token and treat the results separately 2177 * Automatically fills 'current' with a single result 2178 * 2179 * @param string $content Input value undergoing processing in this function. 2180 * @param array $conf stdWrap properties for split. 2181 * @return string The processed input value 2182 */ 2183 public function stdWrap_split($content = '', $conf = []) 2184 { 2185 return $this->splitObj($content, $conf['split.']); 2186 } 2187 2188 /** 2189 * replacement 2190 * Will execute replacements on the content (optionally with preg-regex) 2191 * 2192 * @param string $content Input value undergoing processing in this function. 2193 * @param array $conf stdWrap properties for replacement. 2194 * @return string The processed input value 2195 */ 2196 public function stdWrap_replacement($content = '', $conf = []) 2197 { 2198 return $this->replacement($content, $conf['replacement.']); 2199 } 2200 2201 /** 2202 * prioriCalc 2203 * Will use the content as a mathematical term and calculate the result 2204 * Can be set to 1 to just get a calculated value or 'intval' to get the integer of the result 2205 * 2206 * @param string $content Input value undergoing processing in this function. 2207 * @param array $conf stdWrap properties for prioriCalc. 2208 * @return string The processed input value 2209 */ 2210 public function stdWrap_prioriCalc($content = '', $conf = []) 2211 { 2212 $content = MathUtility::calculateWithParentheses($content); 2213 if (!empty($conf['prioriCalc']) && $conf['prioriCalc'] === 'intval') { 2214 $content = (int)$content; 2215 } 2216 return $content; 2217 } 2218 2219 /** 2220 * char 2221 * Returns a one-character string containing the character specified by ascii code. 2222 * 2223 * Reliable results only for character codes in the integer range 0 - 127. 2224 * 2225 * @see http://php.net/manual/en/function.chr.php 2226 * @param string $content Input value undergoing processing in this function. 2227 * @param array $conf stdWrap properties for char. 2228 * @return string The processed input value 2229 */ 2230 public function stdWrap_char($content = '', $conf = []) 2231 { 2232 return chr((int)$conf['char']); 2233 } 2234 2235 /** 2236 * intval 2237 * Will return an integer value of the current content 2238 * 2239 * @param string $content Input value undergoing processing in this function. 2240 * @return string The processed input value 2241 */ 2242 public function stdWrap_intval($content = '') 2243 { 2244 return (int)$content; 2245 } 2246 2247 /** 2248 * Will return a hashed value of the current content 2249 * 2250 * @param string $content Input value undergoing processing in this function. 2251 * @param array $conf stdWrap properties for hash. 2252 * @return string The processed input value 2253 * @link http://php.net/manual/de/function.hash-algos.php for a list of supported hash algorithms 2254 */ 2255 public function stdWrap_hash($content = '', array $conf = []) 2256 { 2257 $algorithm = isset($conf['hash.']) ? $this->stdWrap($conf['hash'], $conf['hash.']) : $conf['hash']; 2258 if (function_exists('hash') && in_array($algorithm, hash_algos())) { 2259 return hash($algorithm, $content); 2260 } 2261 // Non-existing hashing algorithm 2262 return ''; 2263 } 2264 2265 /** 2266 * stdWrap_round will return a rounded number with ceil(), floor() or round(), defaults to round() 2267 * Only the english number format is supported . (dot) as decimal point 2268 * 2269 * @param string $content Input value undergoing processing in this function. 2270 * @param array $conf stdWrap properties for round. 2271 * @return string The processed input value 2272 */ 2273 public function stdWrap_round($content = '', $conf = []) 2274 { 2275 return $this->round($content, $conf['round.']); 2276 } 2277 2278 /** 2279 * numberFormat 2280 * Will return a formatted number based on configuration given as stdWrap properties 2281 * 2282 * @param string $content Input value undergoing processing in this function. 2283 * @param array $conf stdWrap properties for numberFormat. 2284 * @return string The processed input value 2285 */ 2286 public function stdWrap_numberFormat($content = '', $conf = []) 2287 { 2288 return $this->numberFormat($content, $conf['numberFormat.'] ?? []); 2289 } 2290 2291 /** 2292 * expandList 2293 * Will return a formatted number based on configuration given as stdWrap properties 2294 * 2295 * @param string $content Input value undergoing processing in this function. 2296 * @return string The processed input value 2297 */ 2298 public function stdWrap_expandList($content = '') 2299 { 2300 return GeneralUtility::expandList($content); 2301 } 2302 2303 /** 2304 * date 2305 * Will return a formatted date based on configuration given according to PHP date/gmdate properties 2306 * Will return gmdate when the property GMT returns TRUE 2307 * 2308 * @param string $content Input value undergoing processing in this function. 2309 * @param array $conf stdWrap properties for date. 2310 * @return string The processed input value 2311 */ 2312 public function stdWrap_date($content = '', $conf = []) 2313 { 2314 // Check for zero length string to mimic default case of date/gmdate. 2315 $content = (string)$content === '' ? $GLOBALS['EXEC_TIME'] : (int)$content; 2316 $content = !empty($conf['date.']['GMT']) ? gmdate($conf['date'] ?? null, $content) : date($conf['date'] ?? null, $content); 2317 return $content; 2318 } 2319 2320 /** 2321 * strftime 2322 * Will return a formatted date based on configuration given according to PHP strftime/gmstrftime properties 2323 * Will return gmstrftime when the property GMT returns TRUE 2324 * 2325 * @param string $content Input value undergoing processing in this function. 2326 * @param array $conf stdWrap properties for strftime. 2327 * @return string The processed input value 2328 */ 2329 public function stdWrap_strftime($content = '', $conf = []) 2330 { 2331 // Check for zero length string to mimic default case of strtime/gmstrftime 2332 $content = (string)$content === '' ? $GLOBALS['EXEC_TIME'] : (int)$content; 2333 $content = (isset($conf['strftime.']['GMT']) && $conf['strftime.']['GMT']) 2334 ? gmstrftime($conf['strftime'] ?? null, $content) 2335 : strftime($conf['strftime'] ?? null, $content); 2336 if (!empty($conf['strftime.']['charset'])) { 2337 $output = mb_convert_encoding($content, 'utf-8', trim(strtolower($conf['strftime.']['charset']))); 2338 return $output ?: $content; 2339 } 2340 return $content; 2341 } 2342 2343 /** 2344 * strtotime 2345 * Will return a timestamp based on configuration given according to PHP strtotime 2346 * 2347 * @param string $content Input value undergoing processing in this function. 2348 * @param array $conf stdWrap properties for strtotime. 2349 * @return string The processed input value 2350 */ 2351 public function stdWrap_strtotime($content = '', $conf = []) 2352 { 2353 if ($conf['strtotime'] !== '1') { 2354 $content .= ' ' . $conf['strtotime']; 2355 } 2356 return strtotime($content, $GLOBALS['EXEC_TIME']); 2357 } 2358 2359 /** 2360 * age 2361 * Will return the age of a given timestamp based on configuration given by stdWrap properties 2362 * 2363 * @param string $content Input value undergoing processing in this function. 2364 * @param array $conf stdWrap properties for age. 2365 * @return string The processed input value 2366 */ 2367 public function stdWrap_age($content = '', $conf = []) 2368 { 2369 return $this->calcAge((int)($GLOBALS['EXEC_TIME'] ?? 0) - (int)$content, $conf['age'] ?? null); 2370 } 2371 2372 /** 2373 * case 2374 * Will transform the content to be upper or lower case only 2375 * Leaves HTML tags untouched 2376 * 2377 * @param string $content Input value undergoing processing in this function. 2378 * @param array $conf stdWrap properties for case. 2379 * @return string The processed input value 2380 */ 2381 public function stdWrap_case($content = '', $conf = []) 2382 { 2383 return $this->HTMLcaseshift($content, $conf['case']); 2384 } 2385 2386 /** 2387 * bytes 2388 * Will return the size of a given number in Bytes * 2389 * 2390 * @param string $content Input value undergoing processing in this function. 2391 * @param array $conf stdWrap properties for bytes. 2392 * @return string The processed input value 2393 */ 2394 public function stdWrap_bytes($content = '', $conf = []) 2395 { 2396 return GeneralUtility::formatSize($content, $conf['bytes.']['labels'], $conf['bytes.']['base']); 2397 } 2398 2399 /** 2400 * substring 2401 * Will return a substring based on position information given by stdWrap properties 2402 * 2403 * @param string $content Input value undergoing processing in this function. 2404 * @param array $conf stdWrap properties for substring. 2405 * @return string The processed input value 2406 */ 2407 public function stdWrap_substring($content = '', $conf = []) 2408 { 2409 return $this->substring($content, $conf['substring']); 2410 } 2411 2412 /** 2413 * cropHTML 2414 * Crops content to a given size while leaving HTML tags untouched 2415 * 2416 * @param string $content Input value undergoing processing in this function. 2417 * @param array $conf stdWrap properties for cropHTML. 2418 * @return string The processed input value 2419 */ 2420 public function stdWrap_cropHTML($content = '', $conf = []) 2421 { 2422 return $this->cropHTML($content, $conf['cropHTML'] ?? ''); 2423 } 2424 2425 /** 2426 * stripHtml 2427 * Copmletely removes HTML tags from content 2428 * 2429 * @param string $content Input value undergoing processing in this function. 2430 * @return string The processed input value 2431 */ 2432 public function stdWrap_stripHtml($content = '') 2433 { 2434 return strip_tags($content); 2435 } 2436 2437 /** 2438 * crop 2439 * Crops content to a given size without caring about HTML tags 2440 * 2441 * @param string $content Input value undergoing processing in this function. 2442 * @param array $conf stdWrap properties for crop. 2443 * @return string The processed input value 2444 */ 2445 public function stdWrap_crop($content = '', $conf = []) 2446 { 2447 return $this->crop($content, $conf['crop']); 2448 } 2449 2450 /** 2451 * rawUrlEncode 2452 * Encodes content to be used within URLs 2453 * 2454 * @param string $content Input value undergoing processing in this function. 2455 * @return string The processed input value 2456 */ 2457 public function stdWrap_rawUrlEncode($content = '') 2458 { 2459 return rawurlencode($content); 2460 } 2461 2462 /** 2463 * htmlSpecialChars 2464 * Transforms HTML tags to readable text by replacing special characters with their HTML entity 2465 * When preserveEntities returns TRUE, existing entities will be left untouched 2466 * 2467 * @param string $content Input value undergoing processing in this function. 2468 * @param array $conf stdWrap properties for htmlSpecalChars. 2469 * @return string The processed input value 2470 */ 2471 public function stdWrap_htmlSpecialChars($content = '', $conf = []) 2472 { 2473 if (!empty($conf['htmlSpecialChars.']['preserveEntities'])) { 2474 $content = htmlspecialchars($content, ENT_COMPAT, 'UTF-8', false); 2475 } else { 2476 $content = htmlspecialchars($content); 2477 } 2478 return $content; 2479 } 2480 2481 /** 2482 * encodeForJavaScriptValue 2483 * Escapes content to be used inside JavaScript strings. Single quotes are added around the value. 2484 * 2485 * @param string $content Input value undergoing processing in this function 2486 * @return string The processed input value 2487 */ 2488 public function stdWrap_encodeForJavaScriptValue($content = '') 2489 { 2490 return GeneralUtility::quoteJSvalue($content); 2491 } 2492 2493 /** 2494 * doubleBrTag 2495 * Searches for double line breaks and replaces them with the given value 2496 * 2497 * @param string $content Input value undergoing processing in this function. 2498 * @param array $conf stdWrap properties for doubleBrTag. 2499 * @return string The processed input value 2500 */ 2501 public function stdWrap_doubleBrTag($content = '', $conf = []) 2502 { 2503 return preg_replace('/\R{1,2}[\t\x20]*\R{1,2}/', $conf['doubleBrTag'] ?? null, $content); 2504 } 2505 2506 /** 2507 * br 2508 * Searches for single line breaks and replaces them with a <br />/<br> tag 2509 * according to the doctype 2510 * 2511 * @param string $content Input value undergoing processing in this function. 2512 * @return string The processed input value 2513 */ 2514 public function stdWrap_br($content = '') 2515 { 2516 return nl2br($content, !empty($this->getTypoScriptFrontendController()->xhtmlDoctype)); 2517 } 2518 2519 /** 2520 * brTag 2521 * Searches for single line feeds and replaces them with the given value 2522 * 2523 * @param string $content Input value undergoing processing in this function. 2524 * @param array $conf stdWrap properties for brTag. 2525 * @return string The processed input value 2526 */ 2527 public function stdWrap_brTag($content = '', $conf = []) 2528 { 2529 return str_replace(LF, $conf['brTag'] ?? null, $content); 2530 } 2531 2532 /** 2533 * encapsLines 2534 * Modifies text blocks by searching for lines which are not surrounded by HTML tags yet 2535 * and wrapping them with values given by stdWrap properties 2536 * 2537 * @param string $content Input value undergoing processing in this function. 2538 * @param array $conf stdWrap properties for erncapsLines. 2539 * @return string The processed input value 2540 */ 2541 public function stdWrap_encapsLines($content = '', $conf = []) 2542 { 2543 return $this->encaps_lineSplit($content, $conf['encapsLines.']); 2544 } 2545 2546 /** 2547 * keywords 2548 * Transforms content into a CSV list to be used i.e. as keywords within a meta tag 2549 * 2550 * @param string $content Input value undergoing processing in this function. 2551 * @return string The processed input value 2552 */ 2553 public function stdWrap_keywords($content = '') 2554 { 2555 return $this->keywords($content); 2556 } 2557 2558 /** 2559 * innerWrap 2560 * First of a set of different wraps which will be applied in a certain order before or after other functions that modify the content 2561 * See wrap 2562 * 2563 * @param string $content Input value undergoing processing in this function. 2564 * @param array $conf stdWrap properties for innerWrap. 2565 * @return string The processed input value 2566 */ 2567 public function stdWrap_innerWrap($content = '', $conf = []) 2568 { 2569 return $this->wrap($content, $conf['innerWrap'] ?? null); 2570 } 2571 2572 /** 2573 * innerWrap2 2574 * Second of a set of different wraps which will be applied in a certain order before or after other functions that modify the content 2575 * See wrap 2576 * 2577 * @param string $content Input value undergoing processing in this function. 2578 * @param array $conf stdWrap properties for innerWrap2. 2579 * @return string The processed input value 2580 */ 2581 public function stdWrap_innerWrap2($content = '', $conf = []) 2582 { 2583 return $this->wrap($content, $conf['innerWrap2'] ?? null); 2584 } 2585 2586 /** 2587 * addParams 2588 * Adds tag attributes to any content that is a tag 2589 * 2590 * @param string $content Input value undergoing processing in this function. 2591 * @param array $conf stdWrap properties for addParams. 2592 * @return string The processed input value 2593 * @deprecated since TYPO3 v9.5, will be removed in TYPO3 v10.0. 2594 */ 2595 public function stdWrap_addParams($content = '', $conf = []) 2596 { 2597 return $this->addParams($content, $conf['addParams.'] ?? [], true); 2598 } 2599 2600 /** 2601 * filelink 2602 * Used to make lists of links to files 2603 * See wrap 2604 * 2605 * @param string $content Input value undergoing processing in this function. 2606 * @param array $conf stdWrap properties for filelink. 2607 * @return string The processed input value 2608 * @deprecated since TYPO3 v9.5, will be removed in TYPO3 v10.0. Use cObject FILES instead. 2609 */ 2610 public function stdWrap_filelink($content = '', $conf = []) 2611 { 2612 return $this->filelink($content, $conf['filelink.'] ?? [], true); 2613 } 2614 2615 /** 2616 * preCObject 2617 * A content object that is prepended to the current content but between the innerWraps and the rest of the wraps 2618 * 2619 * @param string $content Input value undergoing processing in this function. 2620 * @param array $conf stdWrap properties for preCObject. 2621 * @return string The processed input value 2622 */ 2623 public function stdWrap_preCObject($content = '', $conf = []) 2624 { 2625 return $this->cObjGetSingle($conf['preCObject'], $conf['preCObject.'], '/stdWrap/.preCObject') . $content; 2626 } 2627 2628 /** 2629 * postCObject 2630 * A content object that is appended to the current content but between the innerWraps and the rest of the wraps 2631 * 2632 * @param string $content Input value undergoing processing in this function. 2633 * @param array $conf stdWrap properties for postCObject. 2634 * @return string The processed input value 2635 */ 2636 public function stdWrap_postCObject($content = '', $conf = []) 2637 { 2638 return $content . $this->cObjGetSingle($conf['postCObject'], $conf['postCObject.'], '/stdWrap/.postCObject'); 2639 } 2640 2641 /** 2642 * wrapAlign 2643 * Wraps content with a div container having the style attribute text-align set to the given value 2644 * See wrap 2645 * 2646 * @param string $content Input value undergoing processing in this function. 2647 * @param array $conf stdWrap properties for wrapAlign. 2648 * @return string The processed input value 2649 */ 2650 public function stdWrap_wrapAlign($content = '', $conf = []) 2651 { 2652 $wrapAlign = trim($conf['wrapAlign'] ?? ''); 2653 if ($wrapAlign) { 2654 $content = $this->wrap($content, '<div style="text-align:' . htmlspecialchars($wrapAlign) . ';">|</div>'); 2655 } 2656 return $content; 2657 } 2658 2659 /** 2660 * typolink 2661 * Wraps the content with a link tag 2662 * URLs and other attributes are created automatically by the values given in the stdWrap properties 2663 * See wrap 2664 * 2665 * @param string $content Input value undergoing processing in this function. 2666 * @param array $conf stdWrap properties for typolink. 2667 * @return string The processed input value 2668 */ 2669 public function stdWrap_typolink($content = '', $conf = []) 2670 { 2671 return $this->typoLink($content, $conf['typolink.']); 2672 } 2673 2674 /** 2675 * wrap 2676 * This is the "mother" of all wraps 2677 * Third of a set of different wraps which will be applied in a certain order before or after other functions that modify the content 2678 * Basically it will put additional content before and after the current content using a split character as a placeholder for the current content 2679 * The default split character is | but it can be replaced with other characters by the property splitChar 2680 * Any other wrap that does not have own splitChar settings will be using the default split char though 2681 * 2682 * @param string $content Input value undergoing processing in this function. 2683 * @param array $conf stdWrap properties for wrap. 2684 * @return string The processed input value 2685 */ 2686 public function stdWrap_wrap($content = '', $conf = []) 2687 { 2688 return $this->wrap( 2689 $content, 2690 $conf['wrap'] ?? null, 2691 $conf['wrap.']['splitChar'] ?? '|' 2692 ); 2693 } 2694 2695 /** 2696 * noTrimWrap 2697 * Fourth of a set of different wraps which will be applied in a certain order before or after other functions that modify the content 2698 * The major difference to any other wrap is, that this one can make use of whitespace without trimming * 2699 * 2700 * @param string $content Input value undergoing processing in this function. 2701 * @param array $conf stdWrap properties for noTrimWrap. 2702 * @return string The processed input value 2703 */ 2704 public function stdWrap_noTrimWrap($content = '', $conf = []) 2705 { 2706 $splitChar = isset($conf['noTrimWrap.']['splitChar.']) 2707 ? $this->stdWrap($conf['noTrimWrap.']['splitChar'] ?? '', $conf['noTrimWrap.']['splitChar.']) 2708 : $conf['noTrimWrap.']['splitChar'] ?? ''; 2709 if ($splitChar === null || $splitChar === '') { 2710 $splitChar = '|'; 2711 } 2712 $content = $this->noTrimWrap( 2713 $content, 2714 $conf['noTrimWrap'], 2715 $splitChar 2716 ); 2717 return $content; 2718 } 2719 2720 /** 2721 * wrap2 2722 * Fifth of a set of different wraps which will be applied in a certain order before or after other functions that modify the content 2723 * The default split character is | but it can be replaced with other characters by the property splitChar 2724 * 2725 * @param string $content Input value undergoing processing in this function. 2726 * @param array $conf stdWrap properties for wrap2. 2727 * @return string The processed input value 2728 */ 2729 public function stdWrap_wrap2($content = '', $conf = []) 2730 { 2731 return $this->wrap( 2732 $content, 2733 $conf['wrap2'] ?? null, 2734 $conf['wrap2.']['splitChar'] ?? '|' 2735 ); 2736 } 2737 2738 /** 2739 * dataWrap 2740 * Sixth of a set of different wraps which will be applied in a certain order before or after other functions that modify the content 2741 * Can fetch additional content the same way data does (i.e. {field:whatever}) and apply it to the wrap before that is applied to the content 2742 * 2743 * @param string $content Input value undergoing processing in this function. 2744 * @param array $conf stdWrap properties for dataWrap. 2745 * @return string The processed input value 2746 */ 2747 public function stdWrap_dataWrap($content = '', $conf = []) 2748 { 2749 return $this->dataWrap($content, $conf['dataWrap']); 2750 } 2751 2752 /** 2753 * prepend 2754 * A content object that will be prepended to the current content after most of the wraps have already been applied 2755 * 2756 * @param string $content Input value undergoing processing in this function. 2757 * @param array $conf stdWrap properties for prepend. 2758 * @return string The processed input value 2759 */ 2760 public function stdWrap_prepend($content = '', $conf = []) 2761 { 2762 return $this->cObjGetSingle($conf['prepend'], $conf['prepend.'], '/stdWrap/.prepend') . $content; 2763 } 2764 2765 /** 2766 * append 2767 * A content object that will be appended to the current content after most of the wraps have already been applied 2768 * 2769 * @param string $content Input value undergoing processing in this function. 2770 * @param array $conf stdWrap properties for append. 2771 * @return string The processed input value 2772 */ 2773 public function stdWrap_append($content = '', $conf = []) 2774 { 2775 return $content . $this->cObjGetSingle($conf['append'], $conf['append.'], '/stdWrap/.append'); 2776 } 2777 2778 /** 2779 * wrap3 2780 * Seventh of a set of different wraps which will be applied in a certain order before or after other functions that modify the content 2781 * The default split character is | but it can be replaced with other characters by the property splitChar 2782 * 2783 * @param string $content Input value undergoing processing in this function. 2784 * @param array $conf stdWrap properties for wrap3. 2785 * @return string The processed input value 2786 */ 2787 public function stdWrap_wrap3($content = '', $conf = []) 2788 { 2789 return $this->wrap( 2790 $content, 2791 $conf['wrap3'] ?? null, 2792 $conf['wrap3.']['splitChar'] ?? '|' 2793 ); 2794 } 2795 2796 /** 2797 * orderedStdWrap 2798 * Calls stdWrap for each entry in the provided array 2799 * 2800 * @param string $content Input value undergoing processing in this function. 2801 * @param array $conf stdWrap properties for orderedStdWrap. 2802 * @return string The processed input value 2803 */ 2804 public function stdWrap_orderedStdWrap($content = '', $conf = []) 2805 { 2806 $sortedKeysArray = ArrayUtility::filterAndSortByNumericKeys($conf['orderedStdWrap.'], true); 2807 foreach ($sortedKeysArray as $key) { 2808 $content = $this->stdWrap($content, $conf['orderedStdWrap.'][$key . '.'] ?? null); 2809 } 2810 return $content; 2811 } 2812 2813 /** 2814 * outerWrap 2815 * Eighth of a set of different wraps which will be applied in a certain order before or after other functions that modify the content 2816 * 2817 * @param string $content Input value undergoing processing in this function. 2818 * @param array $conf stdWrap properties for outerWrap. 2819 * @return string The processed input value 2820 */ 2821 public function stdWrap_outerWrap($content = '', $conf = []) 2822 { 2823 return $this->wrap($content, $conf['outerWrap'] ?? null); 2824 } 2825 2826 /** 2827 * insertData 2828 * Can fetch additional content the same way data does and replaces any occurrence of {field:whatever} with this content 2829 * 2830 * @param string $content Input value undergoing processing in this function. 2831 * @return string The processed input value 2832 */ 2833 public function stdWrap_insertData($content = '') 2834 { 2835 return $this->insertData($content); 2836 } 2837 2838 /** 2839 * postUserFunc 2840 * Will execute a user function after the content has been modified by any other stdWrap function 2841 * 2842 * @param string $content Input value undergoing processing in this function. 2843 * @param array $conf stdWrap properties for postUserFunc. 2844 * @return string The processed input value 2845 */ 2846 public function stdWrap_postUserFunc($content = '', $conf = []) 2847 { 2848 return $this->callUserFunction($conf['postUserFunc'], $conf['postUserFunc.'], $content); 2849 } 2850 2851 /** 2852 * postUserFuncInt 2853 * Will execute a user function after the content has been created and each time it is fetched from Cache 2854 * The result of this function itself will not be cached 2855 * 2856 * @param string $content Input value undergoing processing in this function. 2857 * @param array $conf stdWrap properties for postUserFuncInt. 2858 * @return string The processed input value 2859 */ 2860 public function stdWrap_postUserFuncInt($content = '', $conf = []) 2861 { 2862 $substKey = 'INT_SCRIPT.' . $this->getTypoScriptFrontendController()->uniqueHash(); 2863 $this->getTypoScriptFrontendController()->config['INTincScript'][$substKey] = [ 2864 'content' => $content, 2865 'postUserFunc' => $conf['postUserFuncInt'], 2866 'conf' => $conf['postUserFuncInt.'], 2867 'type' => 'POSTUSERFUNC', 2868 'cObj' => serialize($this) 2869 ]; 2870 $content = '<!--' . $substKey . '-->'; 2871 return $content; 2872 } 2873 2874 /** 2875 * prefixComment 2876 * Will add HTML comments to the content to make it easier to identify certain content elements within the HTML output later on 2877 * 2878 * @param string $content Input value undergoing processing in this function. 2879 * @param array $conf stdWrap properties for prefixComment. 2880 * @return string The processed input value 2881 */ 2882 public function stdWrap_prefixComment($content = '', $conf = []) 2883 { 2884 if ( 2885 (!isset($this->getTypoScriptFrontendController()->config['config']['disablePrefixComment']) || !$this->getTypoScriptFrontendController()->config['config']['disablePrefixComment']) 2886 && !empty($conf['prefixComment']) 2887 ) { 2888 $content = $this->prefixComment($conf['prefixComment'], [], $content); 2889 } 2890 return $content; 2891 } 2892 2893 /** 2894 * editIcons 2895 * Will render icons for frontend editing as long as there is a BE user logged in 2896 * 2897 * @param string $content Input value undergoing processing in this function. 2898 * @param array $conf stdWrap properties for editIcons. 2899 * @return string The processed input value 2900 */ 2901 public function stdWrap_editIcons($content = '', $conf = []) 2902 { 2903 if ($this->getTypoScriptFrontendController()->isBackendUserLoggedIn() && $conf['editIcons']) { 2904 if (!isset($conf['editIcons.']) || !is_array($conf['editIcons.'])) { 2905 $conf['editIcons.'] = []; 2906 } 2907 $content = $this->editIcons($content, $conf['editIcons'], $conf['editIcons.']); 2908 } 2909 return $content; 2910 } 2911 2912 /** 2913 * editPanel 2914 * Will render the edit panel for frontend editing as long as there is a BE user logged in 2915 * 2916 * @param string $content Input value undergoing processing in this function. 2917 * @param array $conf stdWrap properties for editPanel. 2918 * @return string The processed input value 2919 */ 2920 public function stdWrap_editPanel($content = '', $conf = []) 2921 { 2922 if ($this->getTypoScriptFrontendController()->isBackendUserLoggedIn()) { 2923 $content = $this->editPanel($content, $conf['editPanel.']); 2924 } 2925 return $content; 2926 } 2927 2928 public function stdWrap_htmlSanitize(string $content = '', array $conf = []): string 2929 { 2930 $build = $conf['build'] ?? 'default'; 2931 if (class_exists($build) && is_a($build, BuilderInterface::class, true)) { 2932 $builder = GeneralUtility::makeInstance($build); 2933 } else { 2934 $factory = GeneralUtility::makeInstance(SanitizerBuilderFactory::class); 2935 $builder = $factory->build($build); 2936 } 2937 $sanitizer = $builder->build(); 2938 $initiator = $this->shallDebug() 2939 ? GeneralUtility::makeInstance(SanitizerInitiator::class, DebugUtility::debugTrail()) 2940 : null; 2941 return $sanitizer->sanitize($content, $initiator); 2942 } 2943 2944 /** 2945 * Store content into cache 2946 * 2947 * @param string $content Input value undergoing processing in these functions. 2948 * @param array $conf All stdWrap properties, not just the ones for a particular function. 2949 * @return string The processed input value 2950 */ 2951 public function stdWrap_cacheStore($content = '', $conf = []) 2952 { 2953 if (!isset($conf['cache.'])) { 2954 return $content; 2955 } 2956 $key = $this->calculateCacheKey($conf['cache.']); 2957 if (empty($key)) { 2958 return $content; 2959 } 2960 /** @var \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface $cacheFrontend */ 2961 $cacheFrontend = GeneralUtility::makeInstance(CacheManager::class)->getCache('cache_hash'); 2962 $tags = $this->calculateCacheTags($conf['cache.']); 2963 $lifetime = $this->calculateCacheLifetime($conf['cache.']); 2964 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['stdWrap_cacheStore'] ?? [] as $_funcRef) { 2965 $params = [ 2966 'key' => $key, 2967 'content' => $content, 2968 'lifetime' => $lifetime, 2969 'tags' => $tags 2970 ]; 2971 GeneralUtility::callUserFunction($_funcRef, $params, $this); 2972 } 2973 $cacheFrontend->set($key, $content, $tags, $lifetime); 2974 return $content; 2975 } 2976 2977 /** 2978 * stdWrap post process hook 2979 * can be used by extensions authors to modify the behaviour of stdWrap functions to their needs 2980 * this hook executes functions at after the content has been modified by the rest of the stdWrap functions but still before debugging 2981 * 2982 * @param string $content Input value undergoing processing in these functions. 2983 * @param array $conf All stdWrap properties, not just the ones for a particular function. 2984 * @return string The processed input value 2985 */ 2986 public function stdWrap_stdWrapPostProcess($content = '', $conf = []) 2987 { 2988 foreach ($this->stdWrapHookObjects as $hookObject) { 2989 /** @var ContentObjectStdWrapHookInterface $hookObject */ 2990 $content = $hookObject->stdWrapPostProcess($content, $conf, $this); 2991 } 2992 return $content; 2993 } 2994 2995 /** 2996 * debug 2997 * Will output the content as readable HTML code 2998 * 2999 * @param string $content Input value undergoing processing in this function. 3000 * @return string The processed input value 3001 */ 3002 public function stdWrap_debug($content = '') 3003 { 3004 return '<pre>' . htmlspecialchars($content) . '</pre>'; 3005 } 3006 3007 /** 3008 * debugFunc 3009 * Will output the content in a debug table 3010 * 3011 * @param string $content Input value undergoing processing in this function. 3012 * @param array $conf stdWrap properties for debugFunc. 3013 * @return string The processed input value 3014 */ 3015 public function stdWrap_debugFunc($content = '', $conf = []) 3016 { 3017 debug((int)$conf['debugFunc'] === 2 ? [$content] : $content); 3018 return $content; 3019 } 3020 3021 /** 3022 * debugData 3023 * Will output the data used by the current record in a debug table 3024 * 3025 * @param string $content Input value undergoing processing in this function. 3026 * @return string The processed input value 3027 */ 3028 public function stdWrap_debugData($content = '') 3029 { 3030 debug($this->data, '$cObj->data:'); 3031 if (is_array($this->alternativeData)) { 3032 debug($this->alternativeData, '$this->alternativeData'); 3033 } 3034 return $content; 3035 } 3036 3037 /** 3038 * Returns number of rows selected by the query made by the properties set. 3039 * Implements the stdWrap "numRows" property 3040 * 3041 * @param array $conf TypoScript properties for the property (see link to "numRows") 3042 * @return int The number of rows found by the select 3043 * @internal 3044 * @see stdWrap() 3045 */ 3046 public function numRows($conf) 3047 { 3048 $conf['select.']['selectFields'] = 'count(*)'; 3049 $statement = $this->exec_getQuery($conf['table'], $conf['select.']); 3050 3051 return (int)$statement->fetchColumn(0); 3052 } 3053 3054 /** 3055 * Exploding a string by the $char value (if integer its an ASCII value) and returning index $listNum 3056 * 3057 * @param string $content String to explode 3058 * @param string $listNum Index-number. You can place the word "last" in it and it will be substituted with the pointer to the last value. You can use math operators like "+-/*" (passed to calc()) 3059 * @param string $char Either a string used to explode the content string or an integer value which will then be changed into a character, eg. "10" for a linebreak char. 3060 * @return string 3061 */ 3062 public function listNum($content, $listNum, $char) 3063 { 3064 $char = $char ?: ','; 3065 if (MathUtility::canBeInterpretedAsInteger($char)) { 3066 $char = chr($char); 3067 } 3068 $temp = explode($char, $content); 3069 $last = '' . (count($temp) - 1); 3070 // Take a random item if requested 3071 if ($listNum === 'rand') { 3072 $listNum = rand(0, count($temp) - 1); 3073 } 3074 $index = $this->calc(str_ireplace('last', $last, $listNum)); 3075 return $temp[$index]; 3076 } 3077 3078 /** 3079 * Compares values together based on the settings in the input TypoScript array and returns the comparison result. 3080 * Implements the "if" function in TYPO3 TypoScript 3081 * 3082 * @param array $conf TypoScript properties defining what to compare 3083 * @return bool 3084 * @see stdWrap(), _parseFunc() 3085 */ 3086 public function checkIf($conf) 3087 { 3088 if (!is_array($conf)) { 3089 return true; 3090 } 3091 if (isset($conf['directReturn'])) { 3092 return (bool)$conf['directReturn']; 3093 } 3094 $flag = true; 3095 if (isset($conf['isNull.'])) { 3096 $isNull = $this->stdWrap('', $conf['isNull.']); 3097 if ($isNull !== null) { 3098 $flag = false; 3099 } 3100 } 3101 if (isset($conf['isTrue']) || isset($conf['isTrue.'])) { 3102 $isTrue = isset($conf['isTrue.']) ? trim($this->stdWrap($conf['isTrue'], $conf['isTrue.'])) : trim($conf['isTrue']); 3103 if (!$isTrue) { 3104 $flag = false; 3105 } 3106 } 3107 if (isset($conf['isFalse']) || isset($conf['isFalse.'])) { 3108 $isFalse = isset($conf['isFalse.']) ? trim($this->stdWrap($conf['isFalse'], $conf['isFalse.'])) : trim($conf['isFalse']); 3109 if ($isFalse) { 3110 $flag = false; 3111 } 3112 } 3113 if (isset($conf['isPositive']) || isset($conf['isPositive.'])) { 3114 $number = isset($conf['isPositive.']) ? $this->calc($this->stdWrap($conf['isPositive'], $conf['isPositive.'])) : $this->calc($conf['isPositive']); 3115 if ($number < 1) { 3116 $flag = false; 3117 } 3118 } 3119 if ($flag) { 3120 $value = isset($conf['value.']) 3121 ? trim($this->stdWrap($conf['value'] ?? '', $conf['value.'])) 3122 : trim($conf['value'] ?? ''); 3123 if (isset($conf['isGreaterThan']) || isset($conf['isGreaterThan.'])) { 3124 $number = isset($conf['isGreaterThan.']) ? trim($this->stdWrap($conf['isGreaterThan'], $conf['isGreaterThan.'])) : trim($conf['isGreaterThan']); 3125 if ($number <= $value) { 3126 $flag = false; 3127 } 3128 } 3129 if (isset($conf['isLessThan']) || isset($conf['isLessThan.'])) { 3130 $number = isset($conf['isLessThan.']) ? trim($this->stdWrap($conf['isLessThan'], $conf['isLessThan.'])) : trim($conf['isLessThan']); 3131 if ($number >= $value) { 3132 $flag = false; 3133 } 3134 } 3135 if (isset($conf['equals']) || isset($conf['equals.'])) { 3136 $number = isset($conf['equals.']) ? trim($this->stdWrap($conf['equals'], $conf['equals.'])) : trim($conf['equals']); 3137 if ($number != $value) { 3138 $flag = false; 3139 } 3140 } 3141 if (isset($conf['isInList']) || isset($conf['isInList.'])) { 3142 $number = isset($conf['isInList.']) ? trim($this->stdWrap($conf['isInList'], $conf['isInList.'])) : trim($conf['isInList']); 3143 if (!GeneralUtility::inList($value, $number)) { 3144 $flag = false; 3145 } 3146 } 3147 } 3148 if ($conf['negate'] ?? false) { 3149 $flag = !$flag; 3150 } 3151 return $flag; 3152 } 3153 3154 /** 3155 * Reads a directory for files and returns the filepaths in a string list separated by comma. 3156 * Implements the stdWrap property "filelist" 3157 * 3158 * @param string $data The command which contains information about what files/directory listing to return. See the "filelist" property of stdWrap for details. 3159 * @param bool $isCoreCall if set, the deprecation message is suppressed 3160 * @return string Comma list of files. 3161 * @internal 3162 * @see stdWrap() 3163 * @deprecated since TYPO3 v9.5, will be removed in TYPO3 v10.0. Use cObject FILES instead. 3164 */ 3165 public function filelist($data, bool $isCoreCall = false) 3166 { 3167 if (!$isCoreCall) { 3168 trigger_error('ContentObjectRenderer->filelist() will be removed in TYPO3 v10.0. Use cObject FILES instead.', E_USER_DEPRECATED); 3169 } 3170 $data = trim($data); 3171 if ($data === '') { 3172 return ''; 3173 } 3174 list($possiblePath, $ext_list, $sorting, $reverse, $useFullPath) = GeneralUtility::trimExplode('|', $data); 3175 // read directory: 3176 // MUST exist! 3177 $path = ''; 3178 // proceeds if no '//', '..' or '\' is in the $theFile 3179 if (GeneralUtility::validPathStr($possiblePath)) { 3180 // Removes all dots, slashes and spaces after a path. 3181 $possiblePath = preg_replace('/[\\/\\. ]*$/', '', $possiblePath); 3182 if (!GeneralUtility::isAbsPath($possiblePath) && @is_dir($possiblePath)) { 3183 // Now check if it matches one of the FAL storages 3184 $storageRepository = GeneralUtility::makeInstance(StorageRepository::class); 3185 $storages = $storageRepository->findAll(); 3186 foreach ($storages as $storage) { 3187 if ($storage->getDriverType() === 'Local' && $storage->isPublic() && $storage->isOnline()) { 3188 $folder = $storage->getPublicUrl($storage->getRootLevelFolder(), true); 3189 if (GeneralUtility::isFirstPartOfStr($possiblePath . '/', $folder)) { 3190 $path = $possiblePath; 3191 break; 3192 } 3193 } 3194 } 3195 } 3196 } 3197 if (!$path) { 3198 return ''; 3199 } 3200 $items = [ 3201 'files' => [], 3202 'sorting' => [] 3203 ]; 3204 $ext_list = strtolower(GeneralUtility::uniqueList($ext_list)); 3205 // Read dir: 3206 $d = @dir($path); 3207 if (is_object($d)) { 3208 $count = 0; 3209 while ($entry = $d->read()) { 3210 if ($entry !== '.' && $entry !== '..') { 3211 // Because of odd PHP-error where <br />-tag is sometimes placed after a filename!! 3212 $wholePath = $path . '/' . $entry; 3213 if (file_exists($wholePath) && filetype($wholePath) === 'file') { 3214 $info = GeneralUtility::split_fileref($wholePath); 3215 if (!$ext_list || GeneralUtility::inList($ext_list, $info['fileext'])) { 3216 $items['files'][] = $info['file']; 3217 switch ($sorting) { 3218 case 'name': 3219 $items['sorting'][] = strtolower($info['file']); 3220 break; 3221 case 'size': 3222 $items['sorting'][] = filesize($wholePath); 3223 break; 3224 case 'ext': 3225 $items['sorting'][] = $info['fileext']; 3226 break; 3227 case 'date': 3228 $items['sorting'][] = filectime($wholePath); 3229 break; 3230 case 'mdate': 3231 $items['sorting'][] = filemtime($wholePath); 3232 break; 3233 default: 3234 $items['sorting'][] = $count; 3235 } 3236 $count++; 3237 } 3238 } 3239 } 3240 } 3241 $d->close(); 3242 } 3243 // Sort if required 3244 if (!empty($items['sorting'])) { 3245 if (strtolower($reverse) !== 'r') { 3246 asort($items['sorting']); 3247 } else { 3248 arsort($items['sorting']); 3249 } 3250 } 3251 if (!empty($items['files'])) { 3252 // Make list 3253 reset($items['sorting']); 3254 $list_arr = []; 3255 foreach ($items['sorting'] as $key => $v) { 3256 $list_arr[] = $useFullPath ? $path . '/' . $items['files'][$key] : $items['files'][$key]; 3257 } 3258 return implode(',', $list_arr); 3259 } 3260 return ''; 3261 } 3262 3263 /** 3264 * Passes the input value, $theValue, to an instance of "\TYPO3\CMS\Core\Html\HtmlParser" 3265 * together with the TypoScript options which are first converted from a TS style array 3266 * to a set of arrays with options for the \TYPO3\CMS\Core\Html\HtmlParser class. 3267 * 3268 * @param string $theValue The value to parse by the class \TYPO3\CMS\Core\Html\HtmlParser 3269 * @param array $conf TypoScript properties for the parser. See link. 3270 * @return string Return value. 3271 * @see stdWrap(), \TYPO3\CMS\Core\Html\HtmlParser::HTMLparserConfig(), \TYPO3\CMS\Core\Html\HtmlParser::HTMLcleaner() 3272 */ 3273 public function HTMLparser_TSbridge($theValue, $conf) 3274 { 3275 $htmlParser = GeneralUtility::makeInstance(HtmlParser::class); 3276 $htmlParserCfg = $htmlParser->HTMLparserConfig($conf); 3277 return $htmlParser->HTMLcleaner($theValue, $htmlParserCfg[0], $htmlParserCfg[1], $htmlParserCfg[2], $htmlParserCfg[3]); 3278 } 3279 3280 /** 3281 * Wrapping input value in a regular "wrap" but parses the wrapping value first for "insertData" codes. 3282 * 3283 * @param string $content Input string being wrapped 3284 * @param string $wrap The wrap string, eg. "<strong></strong>" or more likely here '<a href="index.php?id={TSFE:id}"> | </a>' which will wrap the input string in a <a> tag linking to the current page. 3285 * @return string Output string wrapped in the wrapping value. 3286 * @see insertData(), stdWrap() 3287 */ 3288 public function dataWrap($content, $wrap) 3289 { 3290 return $this->wrap($content, $this->insertData($wrap)); 3291 } 3292 3293 /** 3294 * Implements the "insertData" property of stdWrap meaning that if strings matching {...} is found in the input string they 3295 * will be substituted with the return value from getData (datatype) which is passed the content of the curly braces. 3296 * If the content inside the curly braces starts with a hash sign {#...} it is a field name that must be quoted by Doctrine 3297 * DBAL and is skipped here for later processing. 3298 * 3299 * Example: If input string is "This is the page title: {page:title}" then the part, '{page:title}', will be substituted with 3300 * the current pages title field value. 3301 * 3302 * @param string $str Input value 3303 * @return string Processed input value 3304 * @see getData(), stdWrap(), dataWrap() 3305 */ 3306 public function insertData($str) 3307 { 3308 $inside = 0; 3309 $newVal = ''; 3310 $pointer = 0; 3311 $totalLen = strlen($str); 3312 do { 3313 if (!$inside) { 3314 $len = strcspn(substr($str, $pointer), '{'); 3315 $newVal .= substr($str, $pointer, $len); 3316 $inside = true; 3317 if (substr($str, $pointer + $len + 1, 1) === '#') { 3318 $len2 = strcspn(substr($str, $pointer + $len), '}'); 3319 $newVal .= substr($str, $pointer + $len, $len2); 3320 $len += $len2; 3321 $inside = false; 3322 } 3323 } else { 3324 $len = strcspn(substr($str, $pointer), '}') + 1; 3325 $newVal .= $this->getData(substr($str, $pointer + 1, $len - 2), $this->data); 3326 $inside = false; 3327 } 3328 $pointer += $len; 3329 } while ($pointer < $totalLen); 3330 return $newVal; 3331 } 3332 3333 /** 3334 * Returns a HTML comment with the second part of input string (divided by "|") where first part is an integer telling how many trailing tabs to put before the comment on a new line. 3335 * Notice; this function (used by stdWrap) can be disabled by a "config.disablePrefixComment" setting in TypoScript. 3336 * 3337 * @param string $str Input value 3338 * @param array $conf TypoScript Configuration (not used at this point.) 3339 * @param string $content The content to wrap the comment around. 3340 * @return string Processed input value 3341 * @see stdWrap() 3342 */ 3343 public function prefixComment($str, $conf, $content) 3344 { 3345 if (empty($str)) { 3346 return $content; 3347 } 3348 $parts = explode('|', $str); 3349 $indent = (int)$parts[0]; 3350 $comment = htmlspecialchars($this->insertData($parts[1])); 3351 $output = LF 3352 . str_pad('', $indent, "\t") . '<!-- ' . $comment . ' [begin] -->' . LF 3353 . str_pad('', $indent + 1, "\t") . $content . LF 3354 . str_pad('', $indent, "\t") . '<!-- ' . $comment . ' [end] -->' . LF 3355 . str_pad('', $indent + 1, "\t"); 3356 return $output; 3357 } 3358 3359 /** 3360 * Implements the stdWrap property "substring" which is basically a TypoScript implementation of the PHP function, substr() 3361 * 3362 * @param string $content The string to perform the operation on 3363 * @param string $options The parameters to substring, given as a comma list of integers where the first and second number is passed as arg 1 and 2 to substr(). 3364 * @return string The processed input value. 3365 * @internal 3366 * @see stdWrap() 3367 */ 3368 public function substring($content, $options) 3369 { 3370 $options = GeneralUtility::intExplode(',', $options . ','); 3371 if ($options[1]) { 3372 return mb_substr($content, $options[0], $options[1], 'utf-8'); 3373 } 3374 return mb_substr($content, $options[0], null, 'utf-8'); 3375 } 3376 3377 /** 3378 * Implements the stdWrap property "crop" which is a modified "substr" function allowing to limit a string length to a certain number of chars (from either start or end of string) and having a pre/postfix applied if the string really was cropped. 3379 * 3380 * @param string $content The string to perform the operation on 3381 * @param string $options The parameters splitted by "|": First parameter is the max number of chars of the string. Negative value means cropping from end of string. Second parameter is the pre/postfix string to apply if cropping occurs. Third parameter is a boolean value. If set then crop will be applied at nearest space. 3382 * @return string The processed input value. 3383 * @internal 3384 * @see stdWrap() 3385 */ 3386 public function crop($content, $options) 3387 { 3388 $options = explode('|', $options); 3389 $chars = (int)$options[0]; 3390 $afterstring = trim($options[1] ?? ''); 3391 $crop2space = trim($options[2] ?? ''); 3392 if ($chars) { 3393 if (mb_strlen($content, 'utf-8') > abs($chars)) { 3394 $truncatePosition = false; 3395 if ($chars < 0) { 3396 $content = mb_substr($content, $chars, null, 'utf-8'); 3397 if ($crop2space) { 3398 $truncatePosition = strpos($content, ' '); 3399 } 3400 $content = $truncatePosition ? $afterstring . substr($content, $truncatePosition) : $afterstring . $content; 3401 } else { 3402 $content = mb_substr($content, 0, $chars, 'utf-8'); 3403 if ($crop2space) { 3404 $truncatePosition = strrpos($content, ' '); 3405 } 3406 $content = $truncatePosition ? substr($content, 0, $truncatePosition) . $afterstring : $content . $afterstring; 3407 } 3408 } 3409 } 3410 return $content; 3411 } 3412 3413 /** 3414 * Implements the stdWrap property "cropHTML" which is a modified "substr" function allowing to limit a string length 3415 * to a certain number of chars (from either start or end of string) and having a pre/postfix applied if the string 3416 * really was cropped. 3417 * 3418 * Compared to stdWrap.crop it respects HTML tags and entities. 3419 * 3420 * @param string $content The string to perform the operation on 3421 * @param string $options The parameters splitted by "|": First parameter is the max number of chars of the string. Negative value means cropping from end of string. Second parameter is the pre/postfix string to apply if cropping occurs. Third parameter is a boolean value. If set then crop will be applied at nearest space. 3422 * @return string The processed input value. 3423 * @internal 3424 * @see stdWrap() 3425 */ 3426 public function cropHTML($content, $options) 3427 { 3428 $options = explode('|', $options); 3429 $chars = (int)$options[0]; 3430 $absChars = abs($chars); 3431 $replacementForEllipsis = trim($options[1] ?? ''); 3432 $crop2space = trim($options[2] ?? '') === '1'; 3433 // Split $content into an array(even items in the array are outside the tags, odd numbers are tag-blocks). 3434 $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'; 3435 $tagsRegEx = ' 3436 ( 3437 (?: 3438 <!--.*?--> # a comment 3439 | 3440 <canvas[^>]*>.*?</canvas> # a canvas tag 3441 | 3442 <script[^>]*>.*?</script> # a script tag 3443 | 3444 <noscript[^>]*>.*?</noscript> # a noscript tag 3445 | 3446 <template[^>]*>.*?</template> # a template tag 3447 ) 3448 | 3449 </?(?:' . $tags . ')+ # opening tag (\'<tag\') or closing tag (\'</tag\') 3450 (?: 3451 (?: 3452 (?: 3453 \\s+\\w[\\w-]* # EITHER spaces, followed by attribute names 3454 (?: 3455 \\s*=?\\s* # equals 3456 (?> 3457 ".*?" # attribute values in double-quotes 3458 | 3459 \'.*?\' # attribute values in single-quotes 3460 | 3461 [^\'">\\s]+ # plain attribute values 3462 ) 3463 )? 3464 ) 3465 | # OR a single dash (for TYPO3 link tag) 3466 (?: 3467 \\s+- 3468 ) 3469 )+\\s* 3470 | # OR only spaces 3471 \\s* 3472 ) 3473 /?> # closing the tag with \'>\' or \'/>\' 3474 )'; 3475 $splittedContent = preg_split('%' . $tagsRegEx . '%xs', $content, -1, PREG_SPLIT_DELIM_CAPTURE); 3476 // Reverse array if we are cropping from right. 3477 if ($chars < 0) { 3478 $splittedContent = array_reverse($splittedContent); 3479 } 3480 // Crop the text (chars of tag-blocks are not counted). 3481 $strLen = 0; 3482 // This is the offset of the content item which was cropped. 3483 $croppedOffset = null; 3484 $countSplittedContent = count($splittedContent); 3485 for ($offset = 0; $offset < $countSplittedContent; $offset++) { 3486 if ($offset % 2 === 0) { 3487 $tempContent = $splittedContent[$offset]; 3488 $thisStrLen = mb_strlen(html_entity_decode($tempContent, ENT_COMPAT, 'UTF-8'), 'utf-8'); 3489 if ($strLen + $thisStrLen > $absChars) { 3490 $croppedOffset = $offset; 3491 $cropPosition = $absChars - $strLen; 3492 // The snippet "&[^&\s;]{2,8};" in the RegEx below represents entities. 3493 $patternMatchEntityAsSingleChar = '(&[^&\\s;]{2,8};|.)'; 3494 $cropRegEx = $chars < 0 ? '#' . $patternMatchEntityAsSingleChar . '{0,' . ($cropPosition + 1) . '}$#uis' : '#^' . $patternMatchEntityAsSingleChar . '{0,' . ($cropPosition + 1) . '}#uis'; 3495 if (preg_match($cropRegEx, $tempContent, $croppedMatch)) { 3496 $tempContentPlusOneCharacter = $croppedMatch[0]; 3497 } else { 3498 $tempContentPlusOneCharacter = false; 3499 } 3500 $cropRegEx = $chars < 0 ? '#' . $patternMatchEntityAsSingleChar . '{0,' . $cropPosition . '}$#uis' : '#^' . $patternMatchEntityAsSingleChar . '{0,' . $cropPosition . '}#uis'; 3501 if (preg_match($cropRegEx, $tempContent, $croppedMatch)) { 3502 $tempContent = $croppedMatch[0]; 3503 if ($crop2space && $tempContentPlusOneCharacter !== false) { 3504 $cropRegEx = $chars < 0 ? '#(?<=\\s)' . $patternMatchEntityAsSingleChar . '{0,' . $cropPosition . '}$#uis' : '#^' . $patternMatchEntityAsSingleChar . '{0,' . $cropPosition . '}(?=\\s)#uis'; 3505 if (preg_match($cropRegEx, $tempContentPlusOneCharacter, $croppedMatch)) { 3506 $tempContent = $croppedMatch[0]; 3507 } 3508 } 3509 } 3510 $splittedContent[$offset] = $tempContent; 3511 break; 3512 } 3513 $strLen += $thisStrLen; 3514 } 3515 } 3516 // Close cropped tags. 3517 $closingTags = []; 3518 if ($croppedOffset !== null) { 3519 $openingTagRegEx = '#^<(\\w+)(?:\\s|>)#'; 3520 $closingTagRegEx = '#^</(\\w+)(?:\\s|>)#'; 3521 for ($offset = $croppedOffset - 1; $offset >= 0; $offset = $offset - 2) { 3522 if (substr($splittedContent[$offset], -2) === '/>') { 3523 // Ignore empty element tags (e.g. <br />). 3524 continue; 3525 } 3526 preg_match($chars < 0 ? $closingTagRegEx : $openingTagRegEx, $splittedContent[$offset], $matches); 3527 $tagName = $matches[1] ?? null; 3528 if ($tagName !== null) { 3529 // Seek for the closing (or opening) tag. 3530 $countSplittedContent = count($splittedContent); 3531 for ($seekingOffset = $offset + 2; $seekingOffset < $countSplittedContent; $seekingOffset = $seekingOffset + 2) { 3532 preg_match($chars < 0 ? $openingTagRegEx : $closingTagRegEx, $splittedContent[$seekingOffset], $matches); 3533 $seekingTagName = $matches[1] ?? null; 3534 if ($tagName === $seekingTagName) { 3535 // We found a matching tag. 3536 // Add closing tag only if it occurs after the cropped content item. 3537 if ($seekingOffset > $croppedOffset) { 3538 $closingTags[] = $splittedContent[$seekingOffset]; 3539 } 3540 break; 3541 } 3542 } 3543 } 3544 } 3545 // Drop the cropped items of the content array. The $closingTags will be added later on again. 3546 array_splice($splittedContent, $croppedOffset + 1); 3547 } 3548 $splittedContent = array_merge($splittedContent, [ 3549 $croppedOffset !== null ? $replacementForEllipsis : '' 3550 ], $closingTags); 3551 // Reverse array once again if we are cropping from the end. 3552 if ($chars < 0) { 3553 $splittedContent = array_reverse($splittedContent); 3554 } 3555 return implode('', $splittedContent); 3556 } 3557 3558 /** 3559 * Implements the TypoScript function "addParams" 3560 * 3561 * @param string $content The string with the HTML tag. 3562 * @param array $conf The TypoScript configuration properties 3563 * @param bool $isCoreCall if set, the deprecation message is suppressed 3564 * @return string The modified string 3565 * @todo Make it XHTML compatible. Will not present "/>" endings of tags right now. Further getting the tagname might fail if it is not separated by a normal space from the attributes. 3566 * @deprecated since TYPO3 v9.5, will be removed in TYPO3 v10.0. 3567 */ 3568 public function addParams($content, $conf, $isCoreCall = false) 3569 { 3570 if (!$isCoreCall) { 3571 trigger_error('ContentObjectRenderer->addParams() will be removed in TYPO3 v10.0.', E_USER_DEPRECATED); 3572 } 3573 // For XHTML compliance. 3574 $lowerCaseAttributes = true; 3575 if (!is_array($conf)) { 3576 return $content; 3577 } 3578 $key = 1; 3579 $parts = explode('<', $content); 3580 if (isset($conf['_offset']) && (int)$conf['_offset']) { 3581 $key = (int)$conf['_offset'] < 0 ? count($parts) + (int)$conf['_offset'] : (int)$conf['_offset']; 3582 } 3583 $subparts = explode('>', $parts[$key] ?? ''); 3584 if (trim($subparts[0])) { 3585 // Get attributes and name 3586 $attribs = GeneralUtility::get_tag_attributes('<' . $subparts[0] . '>'); 3587 list($tagName) = explode(' ', $subparts[0], 2); 3588 // adds/overrides attributes 3589 foreach ($conf as $pkey => $val) { 3590 if (substr($pkey, -1) !== '.' && $pkey[0] !== '_') { 3591 $tmpVal = isset($conf[$pkey . '.']) ? $this->stdWrap($conf[$pkey], $conf[$pkey . '.']) : (string)$val; 3592 if ($lowerCaseAttributes) { 3593 $pkey = strtolower($pkey); 3594 } 3595 if ($tmpVal !== '') { 3596 $attribs[$pkey] = $tmpVal; 3597 } 3598 } 3599 } 3600 // Re-assembles the tag and content 3601 $subparts[0] = trim($tagName . ' ' . GeneralUtility::implodeAttributes($attribs)); 3602 $parts[$key] = implode('>', $subparts); 3603 $content = implode('<', $parts); 3604 } 3605 return $content; 3606 } 3607 3608 /** 3609 * Creates a list of links to files. 3610 * Implements the stdWrap property "filelink" 3611 * 3612 * @param string $theValue The filename to link to, possibly prefixed with $conf[path] 3613 * @param array $conf TypoScript parameters for the TypoScript function ->filelink 3614 * @param bool $isCoreCall if set, the deprecation message is suppressed 3615 * @return string The link to the file possibly with icons, thumbnails, size in bytes shown etc. 3616 * @internal 3617 * @see stdWrap() 3618 * @deprecated since TYPO3 v9.5, will be removed in TYPO3 v10.0. Use cObject FILES instead. 3619 */ 3620 public function filelink($theValue, $conf, $isCoreCall = false) 3621 { 3622 if (!$isCoreCall) { 3623 trigger_error('ContentObjectRenderer->filelink() will be removed in TYPO3 v10.0. Use cObject FILES instead.', E_USER_DEPRECATED); 3624 } 3625 $conf['path'] = isset($conf['path.']) 3626 ? $this->stdWrap($conf['path'] ?? '', $conf['path.']) 3627 : ($conf['path'] ?? ''); 3628 $theFile = trim($conf['path']) . $theValue; 3629 if (!@is_file($theFile)) { 3630 return ''; 3631 } 3632 $theFileEnc = str_replace('%2F', '/', rawurlencode($theFile)); 3633 $title = $conf['title'] ?? ''; 3634 if (isset($conf['title.'])) { 3635 $title = $this->stdWrap($title, $conf['title.']); 3636 } 3637 $target = $conf['target'] ?? ''; 3638 if (isset($conf['target.'])) { 3639 $target = $this->stdWrap($target, $conf['target.']); 3640 } 3641 $tsfe = $this->getTypoScriptFrontendController(); 3642 3643 $typoLinkConf = [ 3644 'parameter' => $theFileEnc, 3645 'fileTarget' => $target, 3646 'title' => $title, 3647 'ATagParams' => $this->getATagParams($conf) 3648 ]; 3649 3650 if (isset($conf['typolinkConfiguration.'])) { 3651 $additionalTypoLinkConfiguration = $conf['typolinkConfiguration.']; 3652 // We only allow additional configuration. This is why the generated conf overwrites the additional conf. 3653 ArrayUtility::mergeRecursiveWithOverrule($additionalTypoLinkConfiguration, $typoLinkConf); 3654 $typoLinkConf = $additionalTypoLinkConfiguration; 3655 } 3656 3657 $theLinkWrap = $this->typoLink('|', $typoLinkConf); 3658 $theSize = filesize($theFile); 3659 $fI = GeneralUtility::split_fileref($theFile); 3660 $icon = ''; 3661 if ($conf['icon'] ?? false) { 3662 $conf['icon.']['path'] = isset($conf['icon.']['path.']) 3663 ? $this->stdWrap($conf['icon.']['path'], $conf['icon.']['path.']) 3664 : $conf['icon.']['path']; 3665 $iconPath = !empty($conf['icon.']['path']) 3666 ? $conf['icon.']['path'] 3667 : GeneralUtility::getFileAbsFileName('EXT:frontend/Resources/Public/Icons/FileIcons/'); 3668 $conf['icon.']['ext'] = isset($conf['icon.']['ext.']) 3669 ? $this->stdWrap($conf['icon.']['ext'], $conf['icon.']['ext.']) 3670 : $conf['icon.']['ext']; 3671 $iconExt = !empty($conf['icon.']['ext']) ? '.' . $conf['icon.']['ext'] : '.gif'; 3672 $icon = @is_file($iconPath . $fI['fileext'] . $iconExt) 3673 ? $iconPath . $fI['fileext'] . $iconExt 3674 : $iconPath . 'default' . $iconExt; 3675 $icon = PathUtility::stripPathSitePrefix($icon); 3676 // Checking for images: If image, then return link to thumbnail. 3677 $IEList = isset($conf['icon_image_ext_list.']) ? $this->stdWrap($conf['icon_image_ext_list'], $conf['icon_image_ext_list.']) : $conf['icon_image_ext_list']; 3678 $image_ext_list = str_replace(' ', '', strtolower($IEList)); 3679 if ($fI['fileext'] && GeneralUtility::inList($image_ext_list, $fI['fileext'])) { 3680 if ($conf['iconCObject']) { 3681 $icon = $this->cObjGetSingle($conf['iconCObject'], $conf['iconCObject.'], 'iconCObject'); 3682 } else { 3683 $notFoundThumb = GeneralUtility::getFileAbsFileName('EXT:core/Resources/Public/Images/NotFound.gif'); 3684 $notFoundThumb = PathUtility::stripPathSitePrefix($notFoundThumb); 3685 $sizeParts = [64, 64]; 3686 if ($GLOBALS['TYPO3_CONF_VARS']['GFX']['thumbnails']) { 3687 // using the File Abstraction Layer to generate a preview image 3688 try { 3689 /** @var File $fileObject */ 3690 $fileObject = ResourceFactory::getInstance()->retrieveFileOrFolderObject($theFile); 3691 if ($fileObject->isMissing()) { 3692 $icon = $notFoundThumb; 3693 } else { 3694 $fileExtension = $fileObject->getExtension(); 3695 if ($fileExtension === 'ttf' || GeneralUtility::inList($GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'], $fileExtension)) { 3696 if ($conf['icon_thumbSize'] || $conf['icon_thumbSize.']) { 3697 $thumbSize = isset($conf['icon_thumbSize.']) ? $this->stdWrap($conf['icon_thumbSize'], $conf['icon_thumbSize.']) : $conf['icon_thumbSize']; 3698 $sizeParts = explode('x', $thumbSize); 3699 } 3700 $icon = $fileObject->process(ProcessedFile::CONTEXT_IMAGEPREVIEW, [ 3701 'width' => $sizeParts[0], 3702 'height' => $sizeParts[1] 3703 ])->getPublicUrl(true); 3704 } 3705 } 3706 } catch (ResourceDoesNotExistException $exception) { 3707 $icon = $notFoundThumb; 3708 } 3709 } else { 3710 $icon = $notFoundThumb; 3711 } 3712 $urlPrefix = ''; 3713 if (parse_url($icon, PHP_URL_HOST) === null) { 3714 $urlPrefix = $tsfe->absRefPrefix; 3715 } 3716 $icon = '<img src="' . htmlspecialchars($urlPrefix . $icon) . '"' . 3717 ' width="' . (int)$sizeParts[0] . '" height="' . (int)$sizeParts[1] . '" ' . 3718 $this->getBorderAttr(' border="0"') . '' . $this->getAltParam($conf) . ' />'; 3719 } 3720 } else { 3721 $conf['icon.']['widthAttribute'] = isset($conf['icon.']['widthAttribute.']) 3722 ? $this->stdWrap($conf['icon.']['widthAttribute'], $conf['icon.']['widthAttribute.']) 3723 : $conf['icon.']['widthAttribute']; 3724 $iconWidth = !empty($conf['icon.']['widthAttribute']) ? $conf['icon.']['widthAttribute'] : 18; 3725 $conf['icon.']['heightAttribute'] = isset($conf['icon.']['heightAttribute.']) 3726 ? $this->stdWrap($conf['icon.']['heightAttribute'], $conf['icon.']['heightAttribute.']) 3727 : $conf['icon.']['heightAttribute']; 3728 $iconHeight = !empty($conf['icon.']['heightAttribute']) ? (int)$conf['icon.']['heightAttribute'] : 16; 3729 $icon = '<img src="' . htmlspecialchars($tsfe->absRefPrefix . $icon) . '" width="' . (int)$iconWidth . '" height="' . (int)$iconHeight . '"' 3730 . $this->getBorderAttr(' border="0"') . $this->getAltParam($conf) . ' />'; 3731 } 3732 if ($conf['icon_link'] && !$conf['combinedLink']) { 3733 $icon = $this->wrap($icon, $theLinkWrap); 3734 } 3735 $icon = isset($conf['icon.']) ? $this->stdWrap($icon, $conf['icon.']) : $icon; 3736 } 3737 $size = ''; 3738 if ($conf['size'] ?? false) { 3739 $size = isset($conf['size.']) ? $this->stdWrap($theSize, $conf['size.']) : $theSize; 3740 } 3741 // Wrapping file label 3742 if ($conf['removePrependedNumbers'] ?? false) { 3743 $theValue = preg_replace('/_[0-9][0-9](\\.[[:alnum:]]*)$/', '\\1', $theValue); 3744 } 3745 if (isset($conf['labelStdWrap.'])) { 3746 $theValue = $this->stdWrap($theValue, $conf['labelStdWrap.']); 3747 } 3748 // Wrapping file 3749 $wrap = isset($conf['wrap.']) 3750 ? $this->stdWrap($conf['wrap'] ?? '', $conf['wrap.']) 3751 : ($conf['wrap'] ?? ''); 3752 if ($conf['combinedLink'] ?? false) { 3753 $theValue = $icon . $theValue; 3754 if ($conf['ATagBeforeWrap']) { 3755 $theValue = $this->wrap($this->wrap($theValue, $wrap), $theLinkWrap); 3756 } else { 3757 $theValue = $this->wrap($this->wrap($theValue, $theLinkWrap), $wrap); 3758 } 3759 $file = isset($conf['file.']) ? $this->stdWrap($theValue, $conf['file.']) : $theValue; 3760 // output 3761 $output = $file . $size; 3762 } else { 3763 if ($conf['ATagBeforeWrap'] ?? false) { 3764 $theValue = $this->wrap($this->wrap($theValue, $wrap), $theLinkWrap); 3765 } else { 3766 $theValue = $this->wrap($this->wrap($theValue, $theLinkWrap), $wrap); 3767 } 3768 $file = isset($conf['file.']) ? $this->stdWrap($theValue, $conf['file.']) : $theValue; 3769 // output 3770 $output = $icon . $file . $size; 3771 } 3772 if (isset($conf['stdWrap.'])) { 3773 $output = $this->stdWrap($output, $conf['stdWrap.']); 3774 } 3775 return $output; 3776 } 3777 3778 /** 3779 * Performs basic mathematical evaluation of the input string. Does NOT take parathesis and operator precedence into account! (for that, see \TYPO3\CMS\Core\Utility\MathUtility::calculateWithPriorityToAdditionAndSubtraction()) 3780 * 3781 * @param string $val The string to evaluate. Example: "3+4*10/5" will generate "35". Only integer numbers can be used. 3782 * @return int The result (might be a float if you did a division of the numbers). 3783 * @see \TYPO3\CMS\Core\Utility\MathUtility::calculateWithPriorityToAdditionAndSubtraction() 3784 */ 3785 public function calc($val) 3786 { 3787 $parts = GeneralUtility::splitCalc($val, '+-*/'); 3788 $value = 0; 3789 foreach ($parts as $part) { 3790 $theVal = $part[1]; 3791 $sign = $part[0]; 3792 if ((string)(int)$theVal === (string)$theVal) { 3793 $theVal = (int)$theVal; 3794 } else { 3795 $theVal = 0; 3796 } 3797 if ($sign === '-') { 3798 $value -= $theVal; 3799 } 3800 if ($sign === '+') { 3801 $value += $theVal; 3802 } 3803 if ($sign === '/') { 3804 if ((int)$theVal) { 3805 $value /= (int)$theVal; 3806 } 3807 } 3808 if ($sign === '*') { 3809 $value *= $theVal; 3810 } 3811 } 3812 return $value; 3813 } 3814 3815 /** 3816 * This explodes a comma-list into an array where the values are parsed through ContentObjectRender::calc() and cast to (int)(so you are sure to have integers in the output array) 3817 * Used to split and calculate min and max values for GMENUs. 3818 * 3819 * @param string $delim Delimited to explode by 3820 * @param string $string The string with parts in (where each part is evaluated by ->calc()) 3821 * @return array And array with evaluated values. 3822 * @see calc(), \TYPO3\CMS\Frontend\ContentObject\Menu\GraphicalMenuContentObject::makeGifs() 3823 * @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0. It is solely used in GMENU, which can be handled there directly. 3824 */ 3825 public function calcIntExplode($delim, $string) 3826 { 3827 trigger_error('calcIntExplode will be removed in TYPO3 v10.0.', E_USER_DEPRECATED); 3828 $temp = explode($delim, $string); 3829 foreach ($temp as $key => $val) { 3830 $temp[$key] = (int)$this->calc($val); 3831 } 3832 return $temp; 3833 } 3834 3835 /** 3836 * Implements the "split" property of stdWrap; Splits a string based on a token (given in TypoScript properties), sets the "current" value to each part and then renders a content object pointer to by a number. 3837 * In classic TypoScript (like 'content (default)'/'styles.content (default)') this is used to render tables, splitting rows and cells by tokens and putting them together again wrapped in <td> tags etc. 3838 * Implements the "optionSplit" processing of the TypoScript options for each splitted value to parse. 3839 * 3840 * @param string $value The string value to explode by $conf[token] and process each part 3841 * @param array $conf TypoScript properties for "split 3842 * @return string Compiled result 3843 * @internal 3844 * @see stdWrap(), \TYPO3\CMS\Frontend\ContentObject\Menu\AbstractMenuContentObject::procesItemStates() 3845 */ 3846 public function splitObj($value, $conf) 3847 { 3848 $conf['token'] = isset($conf['token.']) ? $this->stdWrap($conf['token'], $conf['token.']) : $conf['token']; 3849 if ($conf['token'] === '') { 3850 return $value; 3851 } 3852 $valArr = explode($conf['token'], $value); 3853 3854 // return value directly by returnKey. No further processing 3855 if (!empty($valArr) && (MathUtility::canBeInterpretedAsInteger($conf['returnKey'] ?? null) || ($conf['returnKey.'] ?? false)) 3856 ) { 3857 $key = isset($conf['returnKey.']) ? (int)$this->stdWrap($conf['returnKey'], $conf['returnKey.']) : (int)$conf['returnKey']; 3858 return $valArr[$key] ?? ''; 3859 } 3860 3861 // return the amount of elements. No further processing 3862 if (!empty($valArr) && ($conf['returnCount'] || $conf['returnCount.'])) { 3863 $returnCount = isset($conf['returnCount.']) ? (bool)$this->stdWrap($conf['returnCount'], $conf['returnCount.']) : (bool)$conf['returnCount']; 3864 return $returnCount ? count($valArr) : 0; 3865 } 3866 3867 // calculate splitCount 3868 $splitCount = count($valArr); 3869 $max = isset($conf['max.']) ? (int)$this->stdWrap($conf['max'], $conf['max.']) : (int)$conf['max']; 3870 if ($max && $splitCount > $max) { 3871 $splitCount = $max; 3872 } 3873 $min = isset($conf['min.']) ? (int)$this->stdWrap($conf['min'], $conf['min.']) : (int)$conf['min']; 3874 if ($min && $splitCount < $min) { 3875 $splitCount = $min; 3876 } 3877 $wrap = isset($conf['wrap.']) ? (string)$this->stdWrap($conf['wrap'], $conf['wrap.']) : (string)$conf['wrap']; 3878 $cObjNumSplitConf = isset($conf['cObjNum.']) ? (string)$this->stdWrap($conf['cObjNum'], $conf['cObjNum.']) : (string)$conf['cObjNum']; 3879 $splitArr = []; 3880 if ($wrap !== '' || $cObjNumSplitConf !== '') { 3881 $splitArr['wrap'] = $wrap; 3882 $splitArr['cObjNum'] = $cObjNumSplitConf; 3883 $splitArr = GeneralUtility::makeInstance(TypoScriptService::class) 3884 ->explodeConfigurationForOptionSplit($splitArr, $splitCount); 3885 } 3886 $content = ''; 3887 for ($a = 0; $a < $splitCount; $a++) { 3888 $this->getTypoScriptFrontendController()->register['SPLIT_COUNT'] = $a; 3889 $value = '' . $valArr[$a]; 3890 $this->data[$this->currentValKey] = $value; 3891 if ($splitArr[$a]['cObjNum']) { 3892 $objName = (int)$splitArr[$a]['cObjNum']; 3893 $value = isset($conf[$objName . '.']) 3894 ? $this->stdWrap($this->cObjGet($conf[$objName . '.'], $objName . '.'), $conf[$objName . '.']) 3895 : $this->cObjGet($conf[$objName . '.'], $objName . '.'); 3896 } 3897 $wrap = isset($splitArr[$a]['wrap.']) ? $this->stdWrap($splitArr[$a]['wrap'], $splitArr[$a]['wrap.']) : $splitArr[$a]['wrap']; 3898 if ($wrap) { 3899 $value = $this->wrap($value, $wrap); 3900 } 3901 $content .= $value; 3902 } 3903 return $content; 3904 } 3905 3906 /** 3907 * Processes ordered replacements on content data. 3908 * 3909 * @param string $content The content to be processed 3910 * @param array $configuration The TypoScript configuration for stdWrap.replacement 3911 * @return string The processed content data 3912 */ 3913 protected function replacement($content, array $configuration) 3914 { 3915 // Sorts actions in configuration by numeric index 3916 ksort($configuration, SORT_NUMERIC); 3917 foreach ($configuration as $index => $action) { 3918 // Checks whether we have an valid action and a numeric key ending with a dot ("10.") 3919 if (is_array($action) && substr($index, -1) === '.' && MathUtility::canBeInterpretedAsInteger(substr($index, 0, -1))) { 3920 $content = $this->replacementSingle($content, $action); 3921 } 3922 } 3923 return $content; 3924 } 3925 3926 /** 3927 * Processes a single search/replace on content data. 3928 * 3929 * @param string $content The content to be processed 3930 * @param array $configuration The TypoScript of the search/replace action to be processed 3931 * @return string The processed content data 3932 */ 3933 protected function replacementSingle($content, array $configuration) 3934 { 3935 if ((isset($configuration['search']) || isset($configuration['search.'])) && (isset($configuration['replace']) || isset($configuration['replace.']))) { 3936 // Gets the strings 3937 $search = isset($configuration['search.']) ? $this->stdWrap($configuration['search'], $configuration['search.']) : $configuration['search']; 3938 $replace = isset($configuration['replace.']) 3939 ? $this->stdWrap($configuration['replace'] ?? null, $configuration['replace.']) 3940 : $configuration['replace'] ?? null; 3941 $useRegularExpression = false; 3942 // Determines whether regular expression shall be used 3943 if (isset($configuration['useRegExp']) 3944 || (isset($configuration['useRegExp.']) && $configuration['useRegExp.']) 3945 ) { 3946 $useRegularExpression = isset($configuration['useRegExp.']) ? (bool)$this->stdWrap($configuration['useRegExp'], $configuration['useRegExp.']) : (bool)$configuration['useRegExp']; 3947 } 3948 $useOptionSplitReplace = false; 3949 // Determines whether replace-pattern uses option-split 3950 if (isset($configuration['useOptionSplitReplace']) || isset($configuration['useOptionSplitReplace.'])) { 3951 $useOptionSplitReplace = isset($configuration['useOptionSplitReplace.']) ? (bool)$this->stdWrap($configuration['useOptionSplitReplace'], $configuration['useOptionSplitReplace.']) : (bool)$configuration['useOptionSplitReplace']; 3952 } 3953 3954 // Performs a replacement by preg_replace() 3955 if ($useRegularExpression) { 3956 // Get separator-character which precedes the string and separates search-string from the modifiers 3957 $separator = $search[0]; 3958 $startModifiers = strrpos($search, $separator); 3959 if ($separator !== false && $startModifiers > 0) { 3960 $modifiers = substr($search, $startModifiers + 1); 3961 // remove "e" (eval-modifier), which would otherwise allow to run arbitrary PHP-code 3962 $modifiers = str_replace('e', '', $modifiers); 3963 $search = substr($search, 0, $startModifiers + 1) . $modifiers; 3964 } 3965 if ($useOptionSplitReplace) { 3966 // init for replacement 3967 $splitCount = preg_match_all($search, $content, $matches); 3968 $typoScriptService = GeneralUtility::makeInstance(TypoScriptService::class); 3969 $replaceArray = $typoScriptService->explodeConfigurationForOptionSplit([$replace], $splitCount); 3970 $replaceCount = 0; 3971 3972 $replaceCallback = function ($match) use ($replaceArray, $search, &$replaceCount) { 3973 $replaceCount++; 3974 return preg_replace($search, $replaceArray[$replaceCount - 1][0], $match[0]); 3975 }; 3976 $content = preg_replace_callback($search, $replaceCallback, $content); 3977 } else { 3978 $content = preg_replace($search, $replace, $content); 3979 } 3980 } elseif ($useOptionSplitReplace) { 3981 // turn search-string into a preg-pattern 3982 $searchPreg = '#' . preg_quote($search, '#') . '#'; 3983 3984 // init for replacement 3985 $splitCount = preg_match_all($searchPreg, $content, $matches); 3986 $typoScriptService = GeneralUtility::makeInstance(TypoScriptService::class); 3987 $replaceArray = $typoScriptService->explodeConfigurationForOptionSplit([$replace], $splitCount); 3988 $replaceCount = 0; 3989 3990 $replaceCallback = function () use ($replaceArray, $search, &$replaceCount) { 3991 $replaceCount++; 3992 return $replaceArray[$replaceCount - 1][0]; 3993 }; 3994 $content = preg_replace_callback($searchPreg, $replaceCallback, $content); 3995 } else { 3996 $content = str_replace($search, $replace, $content); 3997 } 3998 } 3999 return $content; 4000 } 4001 4002 /** 4003 * Implements the "round" property of stdWrap 4004 * This is a Wrapper function for PHP's rounding functions (round,ceil,floor), defaults to round() 4005 * 4006 * @param string $content Value to process 4007 * @param array $conf TypoScript configuration for round 4008 * @return string The formatted number 4009 */ 4010 protected function round($content, array $conf = []) 4011 { 4012 $decimals = isset($conf['decimals.']) 4013 ? $this->stdWrap($conf['decimals'] ?? '', $conf['decimals.']) 4014 : ($conf['decimals'] ?? null); 4015 $type = isset($conf['roundType.']) 4016 ? $this->stdWrap($conf['roundType'] ?? '', $conf['roundType.']) 4017 : ($conf['roundType'] ?? null); 4018 $floatVal = (float)$content; 4019 switch ($type) { 4020 case 'ceil': 4021 $content = ceil($floatVal); 4022 break; 4023 case 'floor': 4024 $content = floor($floatVal); 4025 break; 4026 case 'round': 4027 4028 default: 4029 $content = round($floatVal, (int)$decimals); 4030 } 4031 return $content; 4032 } 4033 4034 /** 4035 * Implements the stdWrap property "numberFormat" 4036 * This is a Wrapper function for php's number_format() 4037 * 4038 * @param float $content Value to process 4039 * @param array $conf TypoScript Configuration for numberFormat 4040 * @return string The formatted number 4041 */ 4042 public function numberFormat($content, $conf) 4043 { 4044 $decimals = isset($conf['decimals.']) 4045 ? (int)$this->stdWrap($conf['decimals'] ?? '', $conf['decimals.']) 4046 : (int)($conf['decimals'] ?? 0); 4047 $dec_point = isset($conf['dec_point.']) 4048 ? $this->stdWrap($conf['dec_point'] ?? '', $conf['dec_point.']) 4049 : ($conf['dec_point'] ?? null); 4050 $thousands_sep = isset($conf['thousands_sep.']) 4051 ? $this->stdWrap($conf['thousands_sep'] ?? '', $conf['thousands_sep.']) 4052 : ($conf['thousands_sep'] ?? null); 4053 return number_format((float)$content, $decimals, $dec_point, $thousands_sep); 4054 } 4055 4056 /** 4057 * Implements the stdWrap property, "parseFunc". 4058 * This is a function with a lot of interesting uses. In classic TypoScript this is used to process text 4059 * from the bodytext field; This included highlighting of search words, changing http:// and mailto: prefixed strings into etc. 4060 * It is still a very important function for processing of bodytext which is normally stored in the database 4061 * in a format which is not fully ready to be outputted. 4062 * This situation has not become better by having a RTE around... 4063 * 4064 * This function is actually just splitting the input content according to the configuration of "external blocks". 4065 * This means that before the input string is actually "parsed" it will be splitted into the parts configured to BE parsed 4066 * (while other parts/blocks should NOT be parsed). 4067 * Therefore the actual processing of the parseFunc properties goes on in ->_parseFunc() 4068 * 4069 * @param string $theValue The value to process. 4070 * @param array $conf TypoScript configuration for parseFunc 4071 * @param string $ref Reference to get configuration from. Eg. "< lib.parseFunc" which means that the configuration of the object path "lib.parseFunc" will be retrieved and MERGED with what is in $conf! 4072 * @return string The processed value 4073 * @see _parseFunc() 4074 */ 4075 public function parseFunc($theValue, $conf, $ref = '') 4076 { 4077 // Fetch / merge reference, if any 4078 if ($ref) { 4079 $temp_conf = [ 4080 'parseFunc' => $ref, 4081 'parseFunc.' => $conf 4082 ]; 4083 $temp_conf = $this->mergeTSRef($temp_conf, 'parseFunc'); 4084 $conf = $temp_conf['parseFunc.']; 4085 } 4086 // early return, no processing in case no configuration is given 4087 if (empty($conf)) { 4088 // @deprecated Invoking ContentObjectRenderer::parseFunc without any configuration will trigger an exception in TYPO3 v12.0 4089 trigger_error('Invoking ContentObjectRenderer::parseFunc without any configuration will trigger an exception in TYPO3 v12.0', E_USER_DEPRECATED); 4090 return $theValue; 4091 } 4092 // Handle HTML sanitizer invocation 4093 if (!isset($conf['htmlSanitize'])) { 4094 // @deprecated Property htmlSanitize was not defined, but will be mandatory in TYPO3 v12.0 4095 trigger_error('Property htmlSanitize was not defined, but will be mandatory in TYPO3 v12.0', E_USER_DEPRECATED); 4096 $features = GeneralUtility::makeInstance(Features::class); 4097 $conf['htmlSanitize'] = $features->isFeatureEnabled('security.frontend.htmlSanitizeParseFuncDefault'); 4098 } 4099 $conf['htmlSanitize'] = (bool)$conf['htmlSanitize']; 4100 4101 // Process: 4102 if ((string)($conf['externalBlocks'] ?? '') === '') { 4103 $result = $this->_parseFunc($theValue, $conf); 4104 if ($conf['htmlSanitize']) { 4105 $result = $this->stdWrap_htmlSanitize($result, $conf['htmlSanitize.'] ?? []); 4106 } 4107 return $result; 4108 } 4109 $tags = strtolower(implode(',', GeneralUtility::trimExplode(',', $conf['externalBlocks']))); 4110 $htmlParser = GeneralUtility::makeInstance(HtmlParser::class); 4111 $parts = $htmlParser->splitIntoBlock($tags, $theValue); 4112 foreach ($parts as $k => $v) { 4113 if ($k % 2) { 4114 // font: 4115 $tagName = strtolower($htmlParser->getFirstTagName($v)); 4116 $cfg = $conf['externalBlocks.'][$tagName . '.']; 4117 if ($cfg['stripNLprev'] || $cfg['stripNL']) { 4118 $parts[$k - 1] = preg_replace('/' . CR . '?' . LF . '[ ]*$/', '', $parts[$k - 1]); 4119 } 4120 if ($cfg['stripNLnext'] || $cfg['stripNL']) { 4121 $parts[$k + 1] = preg_replace('/^[ ]*' . CR . '?' . LF . '/', '', $parts[$k + 1]); 4122 } 4123 } 4124 } 4125 foreach ($parts as $k => $v) { 4126 if ($k % 2) { 4127 $tag = $htmlParser->getFirstTag($v); 4128 $tagName = strtolower($htmlParser->getFirstTagName($v)); 4129 $cfg = $conf['externalBlocks.'][$tagName . '.']; 4130 if ($cfg['callRecursive']) { 4131 $parts[$k] = $this->parseFunc($htmlParser->removeFirstAndLastTag($v), $conf); 4132 if (!$cfg['callRecursive.']['dontWrapSelf']) { 4133 if ($cfg['callRecursive.']['alternativeWrap']) { 4134 $parts[$k] = $this->wrap($parts[$k], $cfg['callRecursive.']['alternativeWrap']); 4135 } else { 4136 if (is_array($cfg['callRecursive.']['tagStdWrap.'])) { 4137 $tag = $this->stdWrap($tag, $cfg['callRecursive.']['tagStdWrap.']); 4138 } 4139 $parts[$k] = $tag . $parts[$k] . '</' . $tagName . '>'; 4140 } 4141 } 4142 } elseif ($cfg['HTMLtableCells']) { 4143 $rowParts = $htmlParser->splitIntoBlock('tr', $parts[$k]); 4144 foreach ($rowParts as $kk => $vv) { 4145 if ($kk % 2) { 4146 $colParts = $htmlParser->splitIntoBlock('td,th', $vv); 4147 $cc = 0; 4148 foreach ($colParts as $kkk => $vvv) { 4149 if ($kkk % 2) { 4150 $cc++; 4151 $tag = $htmlParser->getFirstTag($vvv); 4152 $tagName = strtolower($htmlParser->getFirstTagName($vvv)); 4153 $colParts[$kkk] = $htmlParser->removeFirstAndLastTag($vvv); 4154 if ($cfg['HTMLtableCells.'][$cc . '.']['callRecursive'] || !isset($cfg['HTMLtableCells.'][$cc . '.']['callRecursive']) && $cfg['HTMLtableCells.']['default.']['callRecursive']) { 4155 if ($cfg['HTMLtableCells.']['addChr10BetweenParagraphs']) { 4156 $colParts[$kkk] = str_replace('</p><p>', '</p>' . LF . '<p>', $colParts[$kkk]); 4157 } 4158 $colParts[$kkk] = $this->parseFunc($colParts[$kkk], $conf); 4159 } 4160 $tagStdWrap = is_array($cfg['HTMLtableCells.'][$cc . '.']['tagStdWrap.']) 4161 ? $cfg['HTMLtableCells.'][$cc . '.']['tagStdWrap.'] 4162 : $cfg['HTMLtableCells.']['default.']['tagStdWrap.']; 4163 if (is_array($tagStdWrap)) { 4164 $tag = $this->stdWrap($tag, $tagStdWrap); 4165 } 4166 $stdWrap = is_array($cfg['HTMLtableCells.'][$cc . '.']['stdWrap.']) 4167 ? $cfg['HTMLtableCells.'][$cc . '.']['stdWrap.'] 4168 : $cfg['HTMLtableCells.']['default.']['stdWrap.']; 4169 if (is_array($stdWrap)) { 4170 $colParts[$kkk] = $this->stdWrap($colParts[$kkk], $stdWrap); 4171 } 4172 $colParts[$kkk] = $tag . $colParts[$kkk] . '</' . $tagName . '>'; 4173 } 4174 } 4175 $rowParts[$kk] = implode('', $colParts); 4176 } 4177 } 4178 $parts[$k] = implode('', $rowParts); 4179 } 4180 if (is_array($cfg['stdWrap.'])) { 4181 $parts[$k] = $this->stdWrap($parts[$k], $cfg['stdWrap.']); 4182 } 4183 } else { 4184 $parts[$k] = $this->_parseFunc($parts[$k], $conf); 4185 } 4186 } 4187 $result = implode('', $parts); 4188 if ($conf['htmlSanitize']) { 4189 $result = $this->stdWrap_htmlSanitize($result, $conf['htmlSanitize.'] ?? []); 4190 } 4191 return $result; 4192 } 4193 4194 /** 4195 * Helper function for parseFunc() 4196 * 4197 * @param string $theValue The value to process. 4198 * @param array $conf TypoScript configuration for parseFunc 4199 * @return string The processed value 4200 * @internal 4201 * @see parseFunc() 4202 */ 4203 public function _parseFunc($theValue, $conf) 4204 { 4205 if (!empty($conf['if.']) && !$this->checkIf($conf['if.'])) { 4206 return $theValue; 4207 } 4208 // Indicates that the data is from within a tag. 4209 $inside = false; 4210 // Pointer to the total string position 4211 $pointer = 0; 4212 // Loaded with the current typo-tag if any. 4213 $currentTag = ''; 4214 $stripNL = 0; 4215 $contentAccum = []; 4216 $contentAccumP = 0; 4217 $allowTags = strtolower(str_replace(' ', '', $conf['allowTags'] ?? '')); 4218 $denyTags = strtolower(str_replace(' ', '', $conf['denyTags'] ?? '')); 4219 $totalLen = strlen($theValue); 4220 do { 4221 if (!$inside) { 4222 if (!is_array($currentTag)) { 4223 // These operations should only be performed on code outside the typotags... 4224 // data: this checks that we enter tags ONLY if the first char in the tag is alphanumeric OR '/' 4225 $len_p = 0; 4226 $c = 100; 4227 do { 4228 $len = strcspn(substr($theValue, $pointer + $len_p), '<'); 4229 $len_p += $len + 1; 4230 $endChar = ord(strtolower(substr($theValue, $pointer + $len_p, 1))); 4231 $c--; 4232 } while ($c > 0 && $endChar && ($endChar < 97 || $endChar > 122) && $endChar != 47); 4233 $len = $len_p - 1; 4234 } else { 4235 // If we're inside a currentTag, just take it to the end of that tag! 4236 $tempContent = strtolower(substr($theValue, $pointer)); 4237 $len = strpos($tempContent, '</' . $currentTag[0]); 4238 if (is_string($len) && !$len) { 4239 $len = strlen($tempContent); 4240 } 4241 } 4242 // $data is the content until the next <tag-start or end is detected. 4243 // In case of a currentTag set, this would mean all data between the start- and end-tags 4244 $data = substr($theValue, $pointer, $len); 4245 if ($data != '') { 4246 if ($stripNL) { 4247 // If the previous tag was set to strip NewLines in the beginning of the next data-chunk. 4248 $data = preg_replace('/^[ ]*' . CR . '?' . LF . '/', '', $data); 4249 } 4250 // These operations should only be performed on code outside the tags... 4251 if (!is_array($currentTag)) { 4252 // Constants 4253 $tsfe = $this->getTypoScriptFrontendController(); 4254 $tmpConstants = $tsfe->tmpl->setup['constants.'] ?? null; 4255 if ($conf['constants'] && is_array($tmpConstants)) { 4256 foreach ($tmpConstants as $key => $val) { 4257 if (is_string($val)) { 4258 $data = str_replace('###' . $key . '###', $val, $data); 4259 } 4260 } 4261 } 4262 // Short 4263 if (isset($conf['short.']) && is_array($conf['short.'])) { 4264 $shortWords = $conf['short.']; 4265 krsort($shortWords); 4266 foreach ($shortWords as $key => $val) { 4267 if (is_string($val)) { 4268 $data = str_replace($key, $val, $data); 4269 } 4270 } 4271 } 4272 // stdWrap 4273 if (isset($conf['plainTextStdWrap.']) && is_array($conf['plainTextStdWrap.'])) { 4274 $data = $this->stdWrap($data, $conf['plainTextStdWrap.']); 4275 } 4276 // userFunc 4277 if ($conf['userFunc'] ?? false) { 4278 $data = $this->callUserFunction($conf['userFunc'], $conf['userFunc.'], $data); 4279 } 4280 // Makelinks: (Before search-words as we need the links to be generated when searchwords go on...!) 4281 if ($conf['makelinks'] ?? false) { 4282 $data = $this->http_makelinks($data, $conf['makelinks.']['http.']); 4283 $data = $this->mailto_makelinks($data, $conf['makelinks.']['mailto.'] ?? []); 4284 } 4285 // Search Words: 4286 if ($tsfe->no_cache && $conf['sword'] && is_array($tsfe->sWordList) && $tsfe->sWordRegEx) { 4287 $newstring = ''; 4288 do { 4289 $pregSplitMode = 'i'; 4290 if (isset($tsfe->config['config']['sword_noMixedCase']) && !empty($tsfe->config['config']['sword_noMixedCase'])) { 4291 $pregSplitMode = ''; 4292 } 4293 $pieces = preg_split('/' . $tsfe->sWordRegEx . '/' . $pregSplitMode, $data, 2); 4294 $newstring .= $pieces[0]; 4295 $match_len = strlen($data) - (strlen($pieces[0]) + strlen($pieces[1])); 4296 $inTag = false; 4297 if (strstr($pieces[0], '<') || strstr($pieces[0], '>')) { 4298 // Returns TRUE, if a '<' is closer to the string-end than '>'. 4299 // This is the case if we're INSIDE a tag (that could have been 4300 // made by makelinks...) and we must secure, that the inside of a tag is 4301 // not marked up. 4302 $inTag = strrpos($pieces[0], '<') > strrpos($pieces[0], '>'); 4303 } 4304 // The searchword: 4305 $match = substr($data, strlen($pieces[0]), $match_len); 4306 if (trim($match) && strlen($match) > 1 && !$inTag) { 4307 $match = $this->wrap($match, $conf['sword']); 4308 } 4309 // Concatenate the Search Word again. 4310 $newstring .= $match; 4311 $data = $pieces[1]; 4312 } while ($pieces[1]); 4313 $data = $newstring; 4314 } 4315 } 4316 $contentAccum[$contentAccumP] = isset($contentAccum[$contentAccumP]) 4317 ? $contentAccum[$contentAccumP] . $data 4318 : $data; 4319 } 4320 $inside = true; 4321 } else { 4322 // tags 4323 $len = strcspn(substr($theValue, $pointer), '>') + 1; 4324 $data = substr($theValue, $pointer, $len); 4325 if (StringUtility::endsWith($data, '/>') && strpos($data, '<link ') !== 0) { 4326 $tagContent = substr($data, 1, -2); 4327 } else { 4328 $tagContent = substr($data, 1, -1); 4329 } 4330 $tag = explode(' ', trim($tagContent), 2); 4331 $tag[0] = strtolower($tag[0]); 4332 if ($tag[0][0] === '/') { 4333 $tag[0] = substr($tag[0], 1); 4334 $tag['out'] = 1; 4335 } 4336 if ($conf['tags.'][$tag[0]] ?? false) { 4337 $treated = false; 4338 $stripNL = false; 4339 // in-tag 4340 if (!$currentTag && (!isset($tag['out']) || !$tag['out'])) { 4341 // $currentTag (array!) is the tag we are currently processing 4342 $currentTag = $tag; 4343 $contentAccumP++; 4344 $treated = true; 4345 // in-out-tag: img and other empty tags 4346 if (preg_match('/^(area|base|br|col|hr|img|input|meta|param)$/i', $tag[0])) { 4347 $tag['out'] = 1; 4348 } 4349 } 4350 // out-tag 4351 if ($currentTag[0] === $tag[0] && isset($tag['out']) && $tag['out']) { 4352 $theName = $conf['tags.'][$tag[0]]; 4353 $theConf = $conf['tags.'][$tag[0] . '.']; 4354 // This flag indicates, that NL- (13-10-chars) should be stripped first and last. 4355 $stripNL = (bool)($theConf['stripNL'] ?? false); 4356 // This flag indicates, that this TypoTag section should NOT be included in the nonTypoTag content. 4357 $breakOut = (bool)($theConf['breakoutTypoTagContent'] ?? false); 4358 $this->parameters = []; 4359 if ($currentTag[1]) { 4360 // decode HTML entities in attributes, since they're processed 4361 $params = GeneralUtility::get_tag_attributes($currentTag[1], true); 4362 if (is_array($params)) { 4363 foreach ($params as $option => $val) { 4364 // contains non-encoded values 4365 $this->parameters[strtolower($option)] = $val; 4366 } 4367 } 4368 } 4369 $this->parameters['allParams'] = trim($currentTag[1]); 4370 // Removes NL in the beginning and end of the tag-content AND at the end of the currentTagBuffer. 4371 // $stripNL depends on the configuration of the current tag 4372 if ($stripNL) { 4373 $contentAccum[$contentAccumP - 1] = preg_replace('/' . CR . '?' . LF . '[ ]*$/', '', $contentAccum[$contentAccumP - 1]); 4374 $contentAccum[$contentAccumP] = preg_replace('/^[ ]*' . CR . '?' . LF . '/', '', $contentAccum[$contentAccumP]); 4375 $contentAccum[$contentAccumP] = preg_replace('/' . CR . '?' . LF . '[ ]*$/', '', $contentAccum[$contentAccumP]); 4376 } 4377 $this->data[$this->currentValKey] = $contentAccum[$contentAccumP]; 4378 $newInput = $this->cObjGetSingle($theName, $theConf, '/parseFunc/.tags.' . $tag[0]); 4379 // fetch the content object 4380 $contentAccum[$contentAccumP] = $newInput; 4381 $contentAccumP++; 4382 // If the TypoTag section 4383 if (!$breakOut) { 4384 if (!isset($contentAccum[$contentAccumP - 2])) { 4385 $contentAccum[$contentAccumP - 2] = ''; 4386 } 4387 $contentAccum[$contentAccumP - 2] .= ($contentAccum[$contentAccumP - 1] ?? '') . ($contentAccum[$contentAccumP] ?? ''); 4388 unset($contentAccum[$contentAccumP]); 4389 unset($contentAccum[$contentAccumP - 1]); 4390 $contentAccumP -= 2; 4391 } 4392 unset($currentTag); 4393 $treated = true; 4394 } 4395 // other tags 4396 if (!$treated) { 4397 $contentAccum[$contentAccumP] .= $data; 4398 } 4399 } else { 4400 // If a tag was not a typo tag, then it is just added to the content 4401 $stripNL = false; 4402 if (GeneralUtility::inList($allowTags, $tag[0]) || $denyTags !== '*' && !GeneralUtility::inList($denyTags, $tag[0])) { 4403 $contentAccum[$contentAccumP] = isset($contentAccum[$contentAccumP]) 4404 ? $contentAccum[$contentAccumP] . $data 4405 : $data; 4406 } else { 4407 $contentAccum[$contentAccumP] = isset($contentAccum[$contentAccumP]) 4408 ? $contentAccum[$contentAccumP] . htmlspecialchars($data) 4409 : htmlspecialchars($data); 4410 } 4411 } 4412 $inside = false; 4413 } 4414 $pointer += $len; 4415 } while ($pointer < $totalLen); 4416 // Parsing nonTypoTag content (all even keys): 4417 reset($contentAccum); 4418 $contentAccumCount = count($contentAccum); 4419 for ($a = 0; $a < $contentAccumCount; $a++) { 4420 if ($a % 2 != 1) { 4421 // stdWrap 4422 if (isset($conf['nonTypoTagStdWrap.']) && is_array($conf['nonTypoTagStdWrap.'])) { 4423 $contentAccum[$a] = $this->stdWrap($contentAccum[$a], $conf['nonTypoTagStdWrap.']); 4424 } 4425 // userFunc 4426 if (!empty($conf['nonTypoTagUserFunc'])) { 4427 $contentAccum[$a] = $this->callUserFunction($conf['nonTypoTagUserFunc'], $conf['nonTypoTagUserFunc.'], $contentAccum[$a]); 4428 } 4429 } 4430 } 4431 return implode('', $contentAccum); 4432 } 4433 4434 /** 4435 * Lets you split the content by LF and process each line independently. Used to format content made with the RTE. 4436 * 4437 * @param string $theValue The input value 4438 * @param array $conf TypoScript options 4439 * @return string The processed input value being returned; Splitted lines imploded by LF again. 4440 * @internal 4441 */ 4442 public function encaps_lineSplit($theValue, $conf) 4443 { 4444 if ((string)$theValue === '') { 4445 return ''; 4446 } 4447 $lParts = explode(LF, $theValue); 4448 4449 // When the last element is an empty linebreak we need to remove it, otherwise we will have a duplicate empty line. 4450 $lastPartIndex = count($lParts) - 1; 4451 if ($lParts[$lastPartIndex] === '' && trim($lParts[$lastPartIndex - 1], CR) === '') { 4452 array_pop($lParts); 4453 } 4454 4455 $encapTags = GeneralUtility::trimExplode(',', strtolower($conf['encapsTagList']), true); 4456 $nonWrappedTag = $conf['nonWrappedTag']; 4457 $defaultAlign = isset($conf['defaultAlign.']) 4458 ? trim($this->stdWrap($conf['defaultAlign'] ?? '', $conf['defaultAlign.'])) 4459 : trim($conf['defaultAlign'] ?? ''); 4460 4461 $str_content = ''; 4462 foreach ($lParts as $k => $l) { 4463 $sameBeginEnd = 0; 4464 $emptyTag = false; 4465 $l = trim($l); 4466 $attrib = []; 4467 $nonWrapped = false; 4468 $tagName = ''; 4469 if (isset($l[0]) && $l[0] === '<' && substr($l, -1) === '>') { 4470 $fwParts = explode('>', substr($l, 1), 2); 4471 list($tagName) = explode(' ', $fwParts[0], 2); 4472 if (!$fwParts[1]) { 4473 if (substr($tagName, -1) === '/') { 4474 $tagName = substr($tagName, 0, -1); 4475 } 4476 if (substr($fwParts[0], -1) === '/') { 4477 $sameBeginEnd = 1; 4478 $emptyTag = true; 4479 // decode HTML entities, they're encoded later again 4480 $attrib = GeneralUtility::get_tag_attributes('<' . substr($fwParts[0], 0, -1) . '>', true); 4481 } 4482 } else { 4483 $backParts = GeneralUtility::revExplode('<', substr($fwParts[1], 0, -1), 2); 4484 // decode HTML entities, they're encoded later again 4485 $attrib = GeneralUtility::get_tag_attributes('<' . $fwParts[0] . '>', true); 4486 $str_content = $backParts[0]; 4487 $sameBeginEnd = substr(strtolower($backParts[1]), 1, strlen($tagName)) === strtolower($tagName); 4488 } 4489 } 4490 if ($sameBeginEnd && in_array(strtolower($tagName), $encapTags)) { 4491 $uTagName = strtoupper($tagName); 4492 $uTagName = strtoupper($conf['remapTag.'][$uTagName] ?? $uTagName); 4493 } else { 4494 $uTagName = strtoupper($nonWrappedTag); 4495 // The line will be wrapped: $uTagName should not be an empty tag 4496 $emptyTag = false; 4497 $str_content = $lParts[$k]; 4498 $nonWrapped = true; 4499 $attrib = []; 4500 } 4501 // Wrapping all inner-content: 4502 if (is_array($conf['innerStdWrap_all.'])) { 4503 $str_content = $this->stdWrap($str_content, $conf['innerStdWrap_all.']); 4504 } 4505 if ($uTagName) { 4506 // Setting common attributes 4507 if (isset($conf['addAttributes.'][$uTagName . '.']) && is_array($conf['addAttributes.'][$uTagName . '.'])) { 4508 foreach ($conf['addAttributes.'][$uTagName . '.'] as $kk => $vv) { 4509 if (!is_array($vv)) { 4510 if ((string)$conf['addAttributes.'][$uTagName . '.'][$kk . '.']['setOnly'] === 'blank') { 4511 if ((string)($attrib[$kk] ?? '') === '') { 4512 $attrib[$kk] = $vv; 4513 } 4514 } elseif ((string)$conf['addAttributes.'][$uTagName . '.'][$kk . '.']['setOnly'] === 'exists') { 4515 if (!isset($attrib[$kk])) { 4516 $attrib[$kk] = $vv; 4517 } 4518 } else { 4519 $attrib[$kk] = $vv; 4520 } 4521 } 4522 } 4523 } 4524 // Wrapping all inner-content: 4525 if (isset($conf['encapsLinesStdWrap.'][$uTagName . '.']) && is_array($conf['encapsLinesStdWrap.'][$uTagName . '.'])) { 4526 $str_content = $this->stdWrap($str_content, $conf['encapsLinesStdWrap.'][$uTagName . '.']); 4527 } 4528 // Default align 4529 if ((!isset($attrib['align']) || !$attrib['align']) && $defaultAlign) { 4530 $attrib['align'] = $defaultAlign; 4531 } 4532 // implode (insecure) attributes, that's why `htmlspecialchars` is used here 4533 $params = GeneralUtility::implodeAttributes($attrib, true); 4534 if (!isset($conf['removeWrapping']) || !$conf['removeWrapping'] || ($emptyTag && $conf['removeWrapping.']['keepSingleTag'])) { 4535 $selfClosingTagList = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr']; 4536 if ($emptyTag && in_array(strtolower($uTagName), $selfClosingTagList, true)) { 4537 $str_content = '<' . strtolower($uTagName) . (trim($params) ? ' ' . trim($params) : '') . ' />'; 4538 } else { 4539 $str_content = '<' . strtolower($uTagName) . (trim($params) ? ' ' . trim($params) : '') . '>' . $str_content . '</' . strtolower($uTagName) . '>'; 4540 } 4541 } 4542 } 4543 if ($nonWrapped && isset($conf['wrapNonWrappedLines']) && $conf['wrapNonWrappedLines']) { 4544 $str_content = $this->wrap($str_content, $conf['wrapNonWrappedLines']); 4545 } 4546 $lParts[$k] = $str_content; 4547 } 4548 return implode(LF, $lParts); 4549 } 4550 4551 /** 4552 * Finds URLS in text and makes it to a real link. 4553 * Will find all strings prefixed with "http://" and "https://" in the $data string and make them into a link, 4554 * linking to the URL we should have found. 4555 * 4556 * @param string $data The string in which to search for "http:// 4557 * @param array $conf Configuration for makeLinks, see link 4558 * @return string The processed input string, being returned. 4559 * @see _parseFunc() 4560 */ 4561 public function http_makelinks($data, $conf) 4562 { 4563 $aTagParams = $this->getATagParams($conf); 4564 $textstr = ''; 4565 foreach (['http://', 'https://'] as $scheme) { 4566 $textpieces = explode($scheme, $data); 4567 $pieces = count($textpieces); 4568 $textstr = $textpieces[0]; 4569 for ($i = 1; $i < $pieces; $i++) { 4570 $len = strcspn($textpieces[$i], chr(32) . "\t" . CRLF); 4571 if (trim(substr($textstr, -1)) === '' && $len) { 4572 $lastChar = substr($textpieces[$i], $len - 1, 1); 4573 if (!preg_match('/[A-Za-z0-9\\/#_-]/', $lastChar)) { 4574 $len--; 4575 } 4576 // Included '\/' 3/12 4577 $parts[0] = substr($textpieces[$i], 0, $len); 4578 $parts[1] = substr($textpieces[$i], $len); 4579 $keep = $conf['keep']; 4580 $linkParts = parse_url($scheme . $parts[0]); 4581 $linktxt = ''; 4582 if (strstr($keep, 'scheme')) { 4583 $linktxt = $scheme; 4584 } 4585 $linktxt .= $linkParts['host']; 4586 if (strstr($keep, 'path')) { 4587 $linktxt .= $linkParts['path']; 4588 // Added $linkParts['query'] 3/12 4589 if (strstr($keep, 'query') && $linkParts['query']) { 4590 $linktxt .= '?' . $linkParts['query']; 4591 } elseif ($linkParts['path'] === '/') { 4592 $linktxt = substr($linktxt, 0, -1); 4593 } 4594 } 4595 if (isset($conf['extTarget'])) { 4596 if (isset($conf['extTarget.'])) { 4597 $target = $this->stdWrap($conf['extTarget'], $conf['extTarget.']); 4598 } else { 4599 $target = $conf['extTarget']; 4600 } 4601 } else { 4602 $target = $this->getTypoScriptFrontendController()->extTarget; 4603 } 4604 4605 // check for jump URLs or similar 4606 $linkUrl = $this->processUrl(UrlProcessorInterface::CONTEXT_COMMON, $scheme . $parts[0], $conf); 4607 4608 $res = '<a href="' . htmlspecialchars($linkUrl) . '"' 4609 . ($target !== '' ? ' target="' . htmlspecialchars($target) . '"' : '') 4610 . $aTagParams . $this->extLinkATagParams('http://' . $parts[0], 'url') . '>'; 4611 4612 $wrap = isset($conf['wrap.']) ? $this->stdWrap($conf['wrap'], $conf['wrap.']) : $conf['wrap']; 4613 if ((string)$conf['ATagBeforeWrap'] !== '') { 4614 $res = $res . $this->wrap($linktxt, $wrap) . '</a>'; 4615 } else { 4616 $res = $this->wrap($res . $linktxt . '</a>', $wrap); 4617 } 4618 $textstr .= $res . $parts[1]; 4619 } else { 4620 $textstr .= $scheme . $textpieces[$i]; 4621 } 4622 } 4623 $data = $textstr; 4624 } 4625 return $textstr; 4626 } 4627 4628 /** 4629 * Will find all strings prefixed with "mailto:" in the $data string and make them into a link, 4630 * linking to the email address they point to. 4631 * 4632 * @param string $data The string in which to search for "mailto: 4633 * @param array $conf Configuration for makeLinks, see link 4634 * @return string The processed input string, being returned. 4635 * @see _parseFunc() 4636 */ 4637 public function mailto_makelinks($data, $conf) 4638 { 4639 // http-split 4640 $aTagParams = $this->getATagParams($conf); 4641 $textpieces = explode('mailto:', $data); 4642 $pieces = count($textpieces); 4643 $textstr = $textpieces[0]; 4644 $tsfe = $this->getTypoScriptFrontendController(); 4645 for ($i = 1; $i < $pieces; $i++) { 4646 $len = strcspn($textpieces[$i], chr(32) . "\t" . CRLF); 4647 if (trim(substr($textstr, -1)) === '' && $len) { 4648 $lastChar = substr($textpieces[$i], $len - 1, 1); 4649 if (!preg_match('/[A-Za-z0-9]/', $lastChar)) { 4650 $len--; 4651 } 4652 $parts[0] = substr($textpieces[$i], 0, $len); 4653 $parts[1] = substr($textpieces[$i], $len); 4654 $linktxt = preg_replace('/\\?.*/', '', $parts[0]); 4655 list($mailToUrl, $linktxt) = $this->getMailTo($parts[0], $linktxt); 4656 $mailToUrl = $tsfe->spamProtectEmailAddresses === 'ascii' ? $mailToUrl : htmlspecialchars($mailToUrl); 4657 $res = '<a href="' . $mailToUrl . '"' . $aTagParams . '>'; 4658 $wrap = isset($conf['wrap.']) ? $this->stdWrap($conf['wrap'], $conf['wrap.']) : $conf['wrap']; 4659 if ((string)$conf['ATagBeforeWrap'] !== '') { 4660 $res = $res . $this->wrap($linktxt, $wrap) . '</a>'; 4661 } else { 4662 $res = $this->wrap($res . $linktxt . '</a>', $wrap); 4663 } 4664 $textstr .= $res . $parts[1]; 4665 } else { 4666 $textstr .= 'mailto:' . $textpieces[$i]; 4667 } 4668 } 4669 return $textstr; 4670 } 4671 4672 /** 4673 * Creates and returns a TypoScript "imgResource". 4674 * The value ($file) can either be a file reference (TypoScript resource) or the string "GIFBUILDER". 4675 * In the first case a current image is returned, possibly scaled down or otherwise processed. 4676 * In the latter case a GIFBUILDER image is returned; This means an image is made by TYPO3 from layers of elements as GIFBUILDER defines. 4677 * In the function IMG_RESOURCE() this function is called like $this->getImgResource($conf['file'], $conf['file.']); 4678 * 4679 * Structure of the returned info array: 4680 * 0 => width 4681 * 1 => height 4682 * 2 => file extension 4683 * 3 => file name 4684 * origFile => original file name 4685 * origFile_mtime => original file mtime 4686 * -- only available if processed via FAL: -- 4687 * originalFile => original file object 4688 * processedFile => processed file object 4689 * fileCacheHash => checksum of processed file 4690 * 4691 * @param string|File|FileReference $file A "imgResource" TypoScript data type. Either a TypoScript file resource, a file or a file reference object or the string GIFBUILDER. See description above. 4692 * @param array $fileArray TypoScript properties for the imgResource type 4693 * @return array|null Returns info-array 4694 * @see IMG_RESOURCE(), cImage(), \TYPO3\CMS\Frontend\Imaging\GifBuilder 4695 */ 4696 public function getImgResource($file, $fileArray) 4697 { 4698 if (empty($file) && empty($fileArray)) { 4699 return null; 4700 } 4701 if (!is_array($fileArray)) { 4702 $fileArray = (array)$fileArray; 4703 } 4704 $imageResource = null; 4705 if ($file === 'GIFBUILDER') { 4706 $gifCreator = GeneralUtility::makeInstance(GifBuilder::class); 4707 $theImage = ''; 4708 if ($GLOBALS['TYPO3_CONF_VARS']['GFX']['gdlib']) { 4709 $gifCreator->start($fileArray, $this->data); 4710 $theImage = $gifCreator->gifBuild(); 4711 } 4712 $imageResource = $gifCreator->getImageDimensions($theImage); 4713 $imageResource['origFile'] = $theImage; 4714 } else { 4715 if ($file instanceof File) { 4716 $fileObject = $file; 4717 } elseif ($file instanceof FileReference) { 4718 $fileObject = $file->getOriginalFile(); 4719 } else { 4720 try { 4721 if (isset($fileArray['import.']) && $fileArray['import.']) { 4722 $importedFile = trim($this->stdWrap('', $fileArray['import.'])); 4723 if (!empty($importedFile)) { 4724 $file = $importedFile; 4725 } 4726 } 4727 4728 if (MathUtility::canBeInterpretedAsInteger($file)) { 4729 $treatIdAsReference = isset($fileArray['treatIdAsReference.']) ? $this->stdWrap($fileArray['treatIdAsReference'], $fileArray['treatIdAsReference.']) : $fileArray['treatIdAsReference']; 4730 if (!empty($treatIdAsReference)) { 4731 $file = $this->getResourceFactory()->getFileReferenceObject($file); 4732 $fileObject = $file->getOriginalFile(); 4733 } else { 4734 $fileObject = $this->getResourceFactory()->getFileObject($file); 4735 } 4736 } elseif (preg_match('/^(0|[1-9][0-9]*):/', $file)) { // combined identifier 4737 $fileObject = $this->getResourceFactory()->retrieveFileOrFolderObject($file); 4738 } else { 4739 if (isset($importedFile) && !empty($importedFile) && !empty($fileArray['import'])) { 4740 $file = $fileArray['import'] . $file; 4741 } 4742 $fileObject = $this->getResourceFactory()->retrieveFileOrFolderObject($file); 4743 } 4744 } catch (Exception $exception) { 4745 $this->logger->warning('The image "' . $file . '" could not be found and won\'t be included in frontend output', ['exception' => $exception]); 4746 return null; 4747 } 4748 } 4749 if ($fileObject instanceof File) { 4750 $processingConfiguration = []; 4751 $processingConfiguration['width'] = isset($fileArray['width.']) ? $this->stdWrap($fileArray['width'], $fileArray['width.']) : $fileArray['width']; 4752 $processingConfiguration['height'] = isset($fileArray['height.']) ? $this->stdWrap($fileArray['height'], $fileArray['height.']) : $fileArray['height']; 4753 $processingConfiguration['fileExtension'] = isset($fileArray['ext.']) ? $this->stdWrap($fileArray['ext'], $fileArray['ext.']) : $fileArray['ext']; 4754 $processingConfiguration['maxWidth'] = isset($fileArray['maxW.']) ? (int)$this->stdWrap($fileArray['maxW'], $fileArray['maxW.']) : (int)$fileArray['maxW']; 4755 $processingConfiguration['maxHeight'] = isset($fileArray['maxH.']) ? (int)$this->stdWrap($fileArray['maxH'], $fileArray['maxH.']) : (int)$fileArray['maxH']; 4756 $processingConfiguration['minWidth'] = isset($fileArray['minW.']) ? (int)$this->stdWrap($fileArray['minW'], $fileArray['minW.']) : (int)$fileArray['minW']; 4757 $processingConfiguration['minHeight'] = isset($fileArray['minH.']) ? (int)$this->stdWrap($fileArray['minH'], $fileArray['minH.']) : (int)$fileArray['minH']; 4758 $processingConfiguration['noScale'] = isset($fileArray['noScale.']) ? $this->stdWrap($fileArray['noScale'], $fileArray['noScale.']) : $fileArray['noScale']; 4759 $processingConfiguration['additionalParameters'] = isset($fileArray['params.']) ? $this->stdWrap($fileArray['params'], $fileArray['params.']) : $fileArray['params']; 4760 $processingConfiguration['frame'] = isset($fileArray['frame.']) ? (int)$this->stdWrap($fileArray['frame'], $fileArray['frame.']) : (int)$fileArray['frame']; 4761 if ($file instanceof FileReference) { 4762 $processingConfiguration['crop'] = $this->getCropAreaFromFileReference($file, $fileArray); 4763 } else { 4764 $processingConfiguration['crop'] = $this->getCropAreaFromFromTypoScriptSettings($fileObject, $fileArray); 4765 } 4766 4767 // Possibility to cancel/force profile extraction 4768 // see $GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_stripColorProfileCommand'] 4769 if (isset($fileArray['stripProfile'])) { 4770 $processingConfiguration['stripProfile'] = $fileArray['stripProfile']; 4771 } 4772 // Check if we can handle this type of file for editing 4773 if (GeneralUtility::inList($GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'], $fileObject->getExtension())) { 4774 $maskArray = $fileArray['m.']; 4775 // Must render mask images and include in hash-calculating 4776 // - otherwise we cannot be sure the filename is unique for the setup! 4777 if (is_array($maskArray)) { 4778 $mask = $this->getImgResource($maskArray['mask'], $maskArray['mask.']); 4779 $bgImg = $this->getImgResource($maskArray['bgImg'], $maskArray['bgImg.']); 4780 $bottomImg = $this->getImgResource($maskArray['bottomImg'], $maskArray['bottomImg.']); 4781 $bottomImg_mask = $this->getImgResource($maskArray['bottomImg_mask'], $maskArray['bottomImg_mask.']); 4782 4783 $processingConfiguration['maskImages']['maskImage'] = $mask['processedFile']; 4784 $processingConfiguration['maskImages']['backgroundImage'] = $bgImg['processedFile']; 4785 $processingConfiguration['maskImages']['maskBottomImage'] = $bottomImg['processedFile']; 4786 $processingConfiguration['maskImages']['maskBottomImageMask'] = $bottomImg_mask['processedFile']; 4787 } 4788 $processedFileObject = $fileObject->process(ProcessedFile::CONTEXT_IMAGECROPSCALEMASK, $processingConfiguration); 4789 if ($processedFileObject->isProcessed()) { 4790 $imageResource = [ 4791 0 => (int)$processedFileObject->getProperty('width'), 4792 1 => (int)$processedFileObject->getProperty('height'), 4793 2 => $processedFileObject->getExtension(), 4794 3 => $processedFileObject->getPublicUrl(), 4795 'origFile' => $fileObject->getPublicUrl(), 4796 'origFile_mtime' => $fileObject->getModificationTime(), 4797 // This is needed by \TYPO3\CMS\Frontend\Imaging\GifBuilder, 4798 // in order for the setup-array to create a unique filename hash. 4799 'originalFile' => $fileObject, 4800 'processedFile' => $processedFileObject 4801 ]; 4802 } 4803 } 4804 } 4805 } 4806 // If image was processed by GIFBUILDER: 4807 // ($imageResource indicates that it was processed the regular way) 4808 if (!isset($imageResource)) { 4809 try { 4810 $theImage = GeneralUtility::makeInstance(FilePathSanitizer::class)->sanitize((string)$file); 4811 $info = GeneralUtility::makeInstance(GifBuilder::class)->imageMagickConvert($theImage, 'WEB'); 4812 $info['origFile'] = $theImage; 4813 // This is needed by \TYPO3\CMS\Frontend\Imaging\GifBuilder, ln 100ff in order for the setup-array to create a unique filename hash. 4814 $info['origFile_mtime'] = @filemtime($theImage); 4815 $imageResource = $info; 4816 } catch (\TYPO3\CMS\Core\Resource\Exception $e) { 4817 // do nothing in case the file path is invalid 4818 } 4819 } 4820 // Hook 'getImgResource': Post-processing of image resources 4821 if (isset($imageResource)) { 4822 /** @var ContentObjectGetImageResourceHookInterface $hookObject */ 4823 foreach ($this->getGetImgResourceHookObjects() as $hookObject) { 4824 $imageResource = $hookObject->getImgResourcePostProcess($file, (array)$fileArray, $imageResource, $this); 4825 } 4826 } 4827 return $imageResource; 4828 } 4829 4830 /** 4831 * Returns an ImageManipulation\Area object for the given cropVariant (or 'default') 4832 * or null if the crop settings or crop area is empty. 4833 * 4834 * The cropArea from file reference is used, if not set in TypoScript. 4835 * 4836 * Example TypoScript settings: 4837 * file.crop = 4838 * OR 4839 * file.crop = 50,50,100,100 4840 * OR 4841 * file.crop.data = file:current:crop 4842 * 4843 * @param FileReference $fileReference 4844 * @param array $fileArray TypoScript properties for the imgResource type 4845 * @return Area|null 4846 */ 4847 protected function getCropAreaFromFileReference(FileReference $fileReference, array $fileArray) 4848 { 4849 // Use cropping area from file reference if nothing is configured in TypoScript. 4850 if (!isset($fileArray['crop']) && !isset($fileArray['crop.'])) { 4851 // Set crop variant from TypoScript settings. If not set, use default. 4852 $cropVariant = $fileArray['cropVariant'] ?? 'default'; 4853 $fileCropArea = $this->createCropAreaFromJsonString((string)$fileReference->getProperty('crop'), $cropVariant); 4854 return $fileCropArea->isEmpty() ? null : $fileCropArea->makeAbsoluteBasedOnFile($fileReference); 4855 } 4856 4857 return $this->getCropAreaFromFromTypoScriptSettings($fileReference, $fileArray); 4858 } 4859 4860 /** 4861 * Returns an ImageManipulation\Area object for the given cropVariant (or 'default') 4862 * or null if the crop settings or crop area is empty. 4863 * 4864 * @param FileInterface $file 4865 * @param array $fileArray 4866 * @return Area|null 4867 */ 4868 protected function getCropAreaFromFromTypoScriptSettings(FileInterface $file, array $fileArray) 4869 { 4870 /** @var Area $cropArea */ 4871 $cropArea = null; 4872 // Resolve TypoScript configured cropping. 4873 $cropSettings = isset($fileArray['crop.']) 4874 ? $this->stdWrap($fileArray['crop'], $fileArray['crop.']) 4875 : ($fileArray['crop'] ?? null); 4876 4877 if (is_string($cropSettings)) { 4878 // Set crop variant from TypoScript settings. If not set, use default. 4879 $cropVariant = $fileArray['cropVariant'] ?? 'default'; 4880 // Get cropArea from CropVariantCollection, if cropSettings is a valid json. 4881 // CropVariantCollection::create does json_decode. 4882 $jsonCropArea = $this->createCropAreaFromJsonString($cropSettings, $cropVariant); 4883 $cropArea = $jsonCropArea->isEmpty() ? null : $jsonCropArea->makeAbsoluteBasedOnFile($file); 4884 4885 // Cropping is configured in TypoScript in the following way: file.crop = 50,50,100,100 4886 if ($jsonCropArea->isEmpty() && preg_match('/^[0-9]+,[0-9]+,[0-9]+,[0-9]+$/', $cropSettings)) { 4887 $cropSettings = explode(',', $cropSettings); 4888 if (count($cropSettings) === 4) { 4889 $stringCropArea = GeneralUtility::makeInstance( 4890 Area::class, 4891 ...$cropSettings 4892 ); 4893 $cropArea = $stringCropArea->isEmpty() ? null : $stringCropArea; 4894 } 4895 } 4896 } 4897 4898 return $cropArea; 4899 } 4900 4901 /** 4902 * Takes a JSON string and creates CropVariantCollection and fetches the corresponding 4903 * CropArea for that. 4904 * 4905 * @param string $cropSettings 4906 * @param string $cropVariant 4907 * @return Area 4908 */ 4909 protected function createCropAreaFromJsonString(string $cropSettings, string $cropVariant): Area 4910 { 4911 return CropVariantCollection::create($cropSettings)->getCropArea($cropVariant); 4912 } 4913 4914 /*********************************************** 4915 * 4916 * Data retrieval etc. 4917 * 4918 ***********************************************/ 4919 /** 4920 * Returns the value for the field from $this->data. If "//" is found in the $field value that token will split the field values apart and the first field having a non-blank value will be returned. 4921 * 4922 * @param string $field The fieldname, eg. "title" or "navtitle // title" (in the latter case the value of $this->data[navtitle] is returned if not blank, otherwise $this->data[title] will be) 4923 * @return string|null 4924 */ 4925 public function getFieldVal($field) 4926 { 4927 if (!strstr($field, '//')) { 4928 return $this->data[trim($field)] ?? null; 4929 } 4930 $sections = GeneralUtility::trimExplode('//', $field, true); 4931 foreach ($sections as $k) { 4932 if ((string)$this->data[$k] !== '') { 4933 return $this->data[$k]; 4934 } 4935 } 4936 4937 return ''; 4938 } 4939 4940 /** 4941 * Implements the TypoScript data type "getText". This takes a string with parameters and based on those a value from somewhere in the system is returned. 4942 * 4943 * @param string $string The parameter string, eg. "field : title" or "field : navtitle // field : title" (in the latter case and example of how the value is FIRST splitted by "//" is shown) 4944 * @param array|null $fieldArray Alternative field array; If you set this to an array this variable will be used to look up values for the "field" key. Otherwise the current page record in $GLOBALS['TSFE']->page is used. 4945 * @return string The value fetched 4946 * @see getFieldVal() 4947 */ 4948 public function getData($string, $fieldArray = null) 4949 { 4950 $tsfe = $this->getTypoScriptFrontendController(); 4951 if (!is_array($fieldArray)) { 4952 $fieldArray = $tsfe->page; 4953 } 4954 $retVal = ''; 4955 $sections = explode('//', $string); 4956 foreach ($sections as $secKey => $secVal) { 4957 if ($retVal) { 4958 break; 4959 } 4960 $parts = explode(':', $secVal, 2); 4961 $type = strtolower(trim($parts[0])); 4962 $typesWithOutParameters = ['level', 'date', 'current', 'pagelayout']; 4963 $key = trim($parts[1] ?? ''); 4964 if (($key != '') || in_array($type, $typesWithOutParameters)) { 4965 switch ($type) { 4966 case 'gp': 4967 // Merge GET and POST and get $key out of the merged array 4968 $getPostArray = GeneralUtility::_GET(); 4969 ArrayUtility::mergeRecursiveWithOverrule($getPostArray, GeneralUtility::_POST()); 4970 $retVal = $this->getGlobal($key, $getPostArray); 4971 break; 4972 case 'tsfe': 4973 $retVal = $this->getGlobal('TSFE|' . $key); 4974 break; 4975 case 'getenv': 4976 $retVal = getenv($key); 4977 break; 4978 case 'getindpenv': 4979 $retVal = $this->getEnvironmentVariable($key); 4980 break; 4981 case 'field': 4982 $retVal = $this->getGlobal($key, $fieldArray); 4983 break; 4984 case 'file': 4985 $retVal = $this->getFileDataKey($key); 4986 break; 4987 case 'parameters': 4988 $retVal = $this->parameters[$key]; 4989 break; 4990 case 'register': 4991 $retVal = $tsfe->register[$key] ?? null; 4992 break; 4993 case 'global': 4994 $retVal = $this->getGlobal($key); 4995 break; 4996 case 'level': 4997 $retVal = count($tsfe->tmpl->rootLine) - 1; 4998 break; 4999 case 'leveltitle': 5000 $keyParts = GeneralUtility::trimExplode(',', $key); 5001 $numericKey = $this->getKey($keyParts[0], $tsfe->tmpl->rootLine); 5002 $retVal = $this->rootLineValue($numericKey, 'title', strtolower($keyParts[1] ?? '') === 'slide'); 5003 break; 5004 case 'levelmedia': 5005 $keyParts = GeneralUtility::trimExplode(',', $key); 5006 $numericKey = $this->getKey($keyParts[0], $tsfe->tmpl->rootLine); 5007 $retVal = $this->rootLineValue($numericKey, 'media', strtolower($keyParts[1] ?? '') === 'slide'); 5008 break; 5009 case 'leveluid': 5010 $numericKey = $this->getKey($key, $tsfe->tmpl->rootLine); 5011 $retVal = $this->rootLineValue($numericKey, 'uid'); 5012 break; 5013 case 'levelfield': 5014 $keyParts = GeneralUtility::trimExplode(',', $key); 5015 $numericKey = $this->getKey($keyParts[0], $tsfe->tmpl->rootLine); 5016 $retVal = $this->rootLineValue($numericKey, $keyParts[1], strtolower($keyParts[2] ?? '') === 'slide'); 5017 break; 5018 case 'fullrootline': 5019 $keyParts = GeneralUtility::trimExplode(',', $key); 5020 $fullKey = (int)$keyParts[0] - count($tsfe->tmpl->rootLine) + count($tsfe->rootLine); 5021 if ($fullKey >= 0) { 5022 $retVal = $this->rootLineValue($fullKey, $keyParts[1], stristr($keyParts[2] ?? '', 'slide'), $tsfe->rootLine); 5023 } 5024 break; 5025 case 'date': 5026 if (!$key) { 5027 $key = 'd/m Y'; 5028 } 5029 $retVal = date($key, $GLOBALS['EXEC_TIME']); 5030 break; 5031 case 'page': 5032 $retVal = $tsfe->page[$key]; 5033 break; 5034 case 'pagelayout': 5035 // Check if the current page has a value in the DB field "backend_layout" 5036 // if empty, check the root line for "backend_layout_next_level" 5037 // same as 5038 // field = backend_layout 5039 // ifEmpty.data = levelfield:-2, backend_layout_next_level, slide 5040 // ifEmpty.ifEmpty = default 5041 $retVal = $tsfe->page['backend_layout']; 5042 5043 // If it is set to "none" - don't use any 5044 if ($retVal === '-1') { 5045 $retVal = 'none'; 5046 } elseif ($retVal === '' || $retVal === '0') { 5047 // If it not set check the root-line for a layout on next level and use this 5048 // Remove first element, which is the current page 5049 // See also \TYPO3\CMS\Backend\View\BackendLayoutView::getSelectedCombinedIdentifier() 5050 $rootLine = $tsfe->rootLine; 5051 array_shift($rootLine); 5052 foreach ($rootLine as $rootLinePage) { 5053 $retVal = (string)$rootLinePage['backend_layout_next_level']; 5054 // If layout for "next level" is set to "none" - don't use any and stop searching 5055 if ($retVal === '-1') { 5056 $retVal = 'none'; 5057 break; 5058 } 5059 if ($retVal !== '' && $retVal !== '0') { 5060 // Stop searching if a layout for "next level" is set 5061 break; 5062 } 5063 } 5064 } 5065 if ($retVal === '0' || $retVal === '') { 5066 $retVal = 'default'; 5067 } 5068 break; 5069 case 'current': 5070 $retVal = $this->data[$this->currentValKey] ?? null; 5071 break; 5072 case 'db': 5073 $selectParts = GeneralUtility::trimExplode(':', $key); 5074 $db_rec = $tsfe->sys_page->getRawRecord($selectParts[0], $selectParts[1]); 5075 if (is_array($db_rec) && $selectParts[2]) { 5076 $retVal = $db_rec[$selectParts[2]]; 5077 } 5078 break; 5079 case 'lll': 5080 $retVal = $tsfe->sL('LLL:' . $key); 5081 break; 5082 case 'path': 5083 try { 5084 $retVal = GeneralUtility::makeInstance(FilePathSanitizer::class)->sanitize($key); 5085 } catch (\TYPO3\CMS\Core\Resource\Exception $e) { 5086 // do nothing in case the file path is invalid 5087 $retVal = null; 5088 } 5089 break; 5090 case 'cobj': 5091 switch ($key) { 5092 case 'parentRecordNumber': 5093 $retVal = $this->parentRecordNumber; 5094 break; 5095 } 5096 break; 5097 case 'debug': 5098 switch ($key) { 5099 case 'rootLine': 5100 $retVal = DebugUtility::viewArray($tsfe->tmpl->rootLine); 5101 break; 5102 case 'fullRootLine': 5103 $retVal = DebugUtility::viewArray($tsfe->rootLine); 5104 break; 5105 case 'data': 5106 $retVal = DebugUtility::viewArray($this->data); 5107 break; 5108 case 'register': 5109 $retVal = DebugUtility::viewArray($tsfe->register); 5110 break; 5111 case 'page': 5112 $retVal = DebugUtility::viewArray($tsfe->page); 5113 break; 5114 } 5115 break; 5116 case 'flexform': 5117 $keyParts = GeneralUtility::trimExplode(':', $key, true); 5118 if (count($keyParts) === 2 && isset($this->data[$keyParts[0]])) { 5119 $flexFormContent = $this->data[$keyParts[0]]; 5120 if (!empty($flexFormContent)) { 5121 $flexFormService = GeneralUtility::makeInstance(FlexFormService::class); 5122 $flexFormKey = str_replace('.', '|', $keyParts[1]); 5123 $settings = $flexFormService->convertFlexFormContentToArray($flexFormContent); 5124 $retVal = $this->getGlobal($flexFormKey, $settings); 5125 } 5126 } 5127 break; 5128 case 'session': 5129 $keyParts = GeneralUtility::trimExplode('|', $key, true); 5130 $sessionKey = array_shift($keyParts); 5131 $retVal = $this->getTypoScriptFrontendController()->fe_user->getSessionData($sessionKey); 5132 foreach ($keyParts as $keyPart) { 5133 if (is_object($retVal)) { 5134 $retVal = $retVal->{$keyPart}; 5135 } elseif (is_array($retVal)) { 5136 $retVal = $retVal[$keyPart]; 5137 } else { 5138 $retVal = ''; 5139 break; 5140 } 5141 } 5142 if (!is_scalar($retVal)) { 5143 $retVal = ''; 5144 } 5145 break; 5146 case 'context': 5147 $context = GeneralUtility::makeInstance(Context::class); 5148 list($aspectName, $propertyName) = GeneralUtility::trimExplode(':', $key, true, 2); 5149 $retVal = $context->getPropertyFromAspect($aspectName, $propertyName, ''); 5150 if (is_array($retVal)) { 5151 $retVal = implode(',', $retVal); 5152 } 5153 if (!is_scalar($retVal)) { 5154 $retVal = ''; 5155 } 5156 break; 5157 case 'site': 5158 $request = $GLOBALS['TYPO3_REQUEST'] ?? null; 5159 $site = $request ? $request->getAttribute('site') : null; 5160 if ($site instanceof Site) { 5161 if ($key === 'identifier') { 5162 $retVal = $site->getIdentifier(); 5163 } elseif ($key === 'base') { 5164 $retVal = $site->getBase(); 5165 } else { 5166 try { 5167 $retVal = ArrayUtility::getValueByPath($site->getConfiguration(), $key, '.'); 5168 } catch (MissingArrayPathException $exception) { 5169 $this->logger->warning(sprintf('getData() with "%s" failed', $key), ['exception' => $exception]); 5170 } 5171 } 5172 } 5173 break; 5174 case 'sitelanguage': 5175 $request = $GLOBALS['TYPO3_REQUEST'] ?? null; 5176 $siteLanguage = $request ? $request->getAttribute('language') : null; 5177 if ($siteLanguage instanceof SiteLanguage) { 5178 $config = $siteLanguage->toArray(); 5179 if (isset($config[$key])) { 5180 $retVal = $config[$key]; 5181 } 5182 } 5183 break; 5184 } 5185 } 5186 5187 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['getData'] ?? [] as $className) { 5188 $hookObject = GeneralUtility::makeInstance($className); 5189 if (!$hookObject instanceof ContentObjectGetDataHookInterface) { 5190 throw new \UnexpectedValueException('$hookObject must implement interface ' . ContentObjectGetDataHookInterface::class, 1195044480); 5191 } 5192 $retVal = $hookObject->getDataExtension($string, $fieldArray, $secVal, $retVal, $this); 5193 } 5194 } 5195 return $retVal; 5196 } 5197 5198 /** 5199 * Gets file information. This is a helper function for the getData() method above, which resolves e.g. 5200 * page.10.data = file:current:title 5201 * or 5202 * page.10.data = file:17:title 5203 * 5204 * @param string $key A colon-separated key, e.g. 17:name or current:sha1, with the first part being a sys_file uid or the keyword "current" and the second part being the key of information to get from file (e.g. "title", "size", "description", etc.) 5205 * @return string|int The value as retrieved from the file object. 5206 */ 5207 protected function getFileDataKey($key) 5208 { 5209 list($fileUidOrCurrentKeyword, $requestedFileInformationKey) = explode(':', $key, 3); 5210 try { 5211 if ($fileUidOrCurrentKeyword === 'current') { 5212 $fileObject = $this->getCurrentFile(); 5213 } elseif (MathUtility::canBeInterpretedAsInteger($fileUidOrCurrentKeyword)) { 5214 /** @var ResourceFactory $fileFactory */ 5215 $fileFactory = GeneralUtility::makeInstance(ResourceFactory::class); 5216 $fileObject = $fileFactory->getFileObject($fileUidOrCurrentKeyword); 5217 } else { 5218 $fileObject = null; 5219 } 5220 } catch (Exception $exception) { 5221 $this->logger->warning('The file "' . $fileUidOrCurrentKeyword . '" could not be found and won\'t be included in frontend output', ['exception' => $exception]); 5222 $fileObject = null; 5223 } 5224 5225 if ($fileObject instanceof FileInterface) { 5226 // All properties of the \TYPO3\CMS\Core\Resource\FileInterface are available here: 5227 switch ($requestedFileInformationKey) { 5228 case 'name': 5229 return $fileObject->getName(); 5230 case 'uid': 5231 if (method_exists($fileObject, 'getUid')) { 5232 return $fileObject->getUid(); 5233 } 5234 return 0; 5235 case 'originalUid': 5236 if ($fileObject instanceof FileReference) { 5237 return $fileObject->getOriginalFile()->getUid(); 5238 } 5239 return null; 5240 case 'size': 5241 return $fileObject->getSize(); 5242 case 'sha1': 5243 return $fileObject->getSha1(); 5244 case 'extension': 5245 return $fileObject->getExtension(); 5246 case 'mimetype': 5247 return $fileObject->getMimeType(); 5248 case 'contents': 5249 return $fileObject->getContents(); 5250 case 'publicUrl': 5251 return $fileObject->getPublicUrl(); 5252 default: 5253 // Generic alternative here 5254 return $fileObject->getProperty($requestedFileInformationKey); 5255 } 5256 } else { 5257 // @todo fail silently as is common in tslib_content 5258 return 'Error: no file object'; 5259 } 5260 } 5261 5262 /** 5263 * Returns a value from the current rootline (site) from $GLOBALS['TSFE']->tmpl->rootLine; 5264 * 5265 * @param string $key Which level in the root line 5266 * @param string $field The field in the rootline record to return (a field from the pages table) 5267 * @param bool $slideBack If set, then we will traverse through the rootline from outer level towards the root level until the value found is TRUE 5268 * @param mixed $altRootLine If you supply an array for this it will be used as an alternative root line array 5269 * @return string The value from the field of the rootline. 5270 * @internal 5271 * @see getData() 5272 */ 5273 public function rootLineValue($key, $field, $slideBack = false, $altRootLine = '') 5274 { 5275 $rootLine = is_array($altRootLine) ? $altRootLine : $this->getTypoScriptFrontendController()->tmpl->rootLine; 5276 if (!$slideBack) { 5277 return $rootLine[$key][$field]; 5278 } 5279 for ($a = $key; $a >= 0; $a--) { 5280 $val = $rootLine[$a][$field]; 5281 if ($val) { 5282 return $val; 5283 } 5284 } 5285 5286 return ''; 5287 } 5288 5289 /** 5290 * Return global variable where the input string $var defines array keys separated by "|" 5291 * Example: $var = "HTTP_SERVER_VARS | something" will return the value $GLOBALS['HTTP_SERVER_VARS']['something'] value 5292 * 5293 * @param string $keyString Global var key, eg. "HTTP_GET_VAR" or "HTTP_GET_VARS|id" to get the GET parameter "id" back. 5294 * @param array $source Alternative array than $GLOBAL to get variables from. 5295 * @return mixed Whatever value. If none, then blank string. 5296 * @see getData() 5297 */ 5298 public function getGlobal($keyString, $source = null) 5299 { 5300 $keys = explode('|', $keyString); 5301 $numberOfLevels = count($keys); 5302 $rootKey = trim($keys[0]); 5303 $value = isset($source) ? $source[$rootKey] : $GLOBALS[$rootKey]; 5304 for ($i = 1; $i < $numberOfLevels && isset($value); $i++) { 5305 $currentKey = trim($keys[$i]); 5306 if (is_object($value)) { 5307 $value = $value->{$currentKey}; 5308 } elseif (is_array($value)) { 5309 $value = $value[$currentKey]; 5310 } else { 5311 $value = ''; 5312 break; 5313 } 5314 } 5315 if (!is_scalar($value)) { 5316 $value = ''; 5317 } 5318 return $value; 5319 } 5320 5321 /** 5322 * Processing of key values pointing to entries in $arr; Here negative values are converted to positive keys pointer to an entry in the array but from behind (based on the negative value). 5323 * Example: entrylevel = -1 means that entryLevel ends up pointing at the outermost-level, -2 means the level before the outermost... 5324 * 5325 * @param int $key The integer to transform 5326 * @param array $arr array in which the key should be found. 5327 * @return int The processed integer key value. 5328 * @internal 5329 * @see getData() 5330 */ 5331 public function getKey($key, $arr) 5332 { 5333 $key = (int)$key; 5334 if (is_array($arr)) { 5335 if ($key < 0) { 5336 $key = count($arr) + $key; 5337 } 5338 if ($key < 0) { 5339 $key = 0; 5340 } 5341 } 5342 return $key; 5343 } 5344 5345 /*********************************************** 5346 * 5347 * Link functions (typolink) 5348 * 5349 ***********************************************/ 5350 5351 /** 5352 * called from the typoLink() function 5353 * 5354 * does the magic to split the full "typolink" string like "15,13 _blank myclass &more=1" 5355 * into separate parts 5356 * 5357 * @param string $linkText The string (text) to link 5358 * @param string $mixedLinkParameter destination data like "15,13 _blank myclass &more=1" used to create the link 5359 * @param array $configuration TypoScript configuration 5360 * @return array|string 5361 * @see typoLink() 5362 * 5363 * @todo the functionality of the "file:" syntax + the hook should be marked as deprecated, an upgrade wizard should handle existing links 5364 */ 5365 protected function resolveMixedLinkParameter($linkText, $mixedLinkParameter, &$configuration = []) 5366 { 5367 $linkParameter = null; 5368 5369 // Link parameter value = first part 5370 $linkParameterParts = GeneralUtility::makeInstance(TypoLinkCodecService::class)->decode($mixedLinkParameter); 5371 5372 // Check for link-handler keyword 5373 $linkHandlerExploded = explode(':', $linkParameterParts['url'], 2); 5374 $linkHandlerKeyword = $linkHandlerExploded[0] ?? null; 5375 $linkHandlerValue = $linkHandlerExploded[1] ?? null; 5376 if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['typolinkLinkHandler'][$linkHandlerKeyword]) 5377 && (string)$linkHandlerValue !== '' 5378 ) { 5379 $linkHandlerObj = GeneralUtility::makeInstance($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['typolinkLinkHandler'][$linkHandlerKeyword]); 5380 if (method_exists($linkHandlerObj, 'main')) { 5381 return $linkHandlerObj->main($linkText, $configuration, $linkHandlerKeyword, $linkHandlerValue, $mixedLinkParameter, $this); 5382 } 5383 } 5384 5385 if (in_array(strtolower(preg_replace('#\s|[[:cntrl:]]#', '', $linkHandlerKeyword)), ['javascript', 'data'], true)) { 5386 // Disallow insecure scheme's like javascript: or data: 5387 return $linkText; 5388 } 5389 $linkParameter = $linkParameterParts['url']; 5390 5391 // additional parameters that need to be set 5392 if ($linkParameterParts['additionalParams'] !== '') { 5393 $forceParams = $linkParameterParts['additionalParams']; 5394 // params value 5395 $configuration['additionalParams'] .= $forceParams[0] === '&' ? $forceParams : '&' . $forceParams; 5396 } 5397 5398 return [ 5399 'href' => $linkParameter, 5400 'target' => $linkParameterParts['target'], 5401 'class' => $linkParameterParts['class'], 5402 'title' => $linkParameterParts['title'] 5403 ]; 5404 } 5405 5406 /** 5407 * Implements the "typolink" property of stdWrap (and others) 5408 * Basically the input string, $linktext, is (typically) wrapped in a <a>-tag linking to some page, email address, file or URL based on a parameter defined by the configuration array $conf. 5409 * This function is best used from internal functions as is. There are some API functions defined after this function which is more suited for general usage in external applications. 5410 * Generally the concept "typolink" should be used in your own applications as an API for making links to pages with parameters and more. The reason for this is that you will then automatically make links compatible with all the centralized functions for URL simulation and manipulation of parameters into hashes and more. 5411 * For many more details on the parameters and how they are interpreted, please see the link to TSref below. 5412 * 5413 * the FAL API is handled with the namespace/prefix "file:..." 5414 * 5415 * @param string $linkText The string (text) to link 5416 * @param array $conf TypoScript configuration (see link below) 5417 * @return string A link-wrapped string. 5418 * @see stdWrap(), \TYPO3\CMS\Frontend\Plugin\AbstractPlugin::pi_linkTP() 5419 */ 5420 public function typoLink($linkText, $conf) 5421 { 5422 $linkText = (string)$linkText; 5423 $tsfe = $this->getTypoScriptFrontendController(); 5424 5425 $linkParameter = trim( 5426 (isset($conf['parameter.']) ?? '') 5427 ? $this->stdWrap($conf['parameter'] ?? '', $conf['parameter.']) 5428 : ($conf['parameter'] ?? '') 5429 ); 5430 $this->lastTypoLinkUrl = ''; 5431 $this->lastTypoLinkTarget = ''; 5432 5433 $resolvedLinkParameters = $this->resolveMixedLinkParameter($linkText, $linkParameter, $conf); 5434 // check if the link handler hook has resolved the link completely already 5435 if (!is_array($resolvedLinkParameters)) { 5436 return $resolvedLinkParameters; 5437 } 5438 $linkParameter = $resolvedLinkParameters['href']; 5439 $target = $resolvedLinkParameters['target']; 5440 $title = $resolvedLinkParameters['title']; 5441 5442 if (!$linkParameter) { 5443 return $this->resolveAnchorLink($linkText, $conf ?? []); 5444 } 5445 5446 // Detecting kind of link and resolve all necessary parameters 5447 $linkService = GeneralUtility::makeInstance(LinkService::class); 5448 try { 5449 $linkDetails = $linkService->resolve($linkParameter); 5450 } catch (Exception\InvalidPathException $exception) { 5451 $this->logger->warning('The link could not be generated', ['exception' => $exception]); 5452 return $linkText; 5453 } 5454 5455 $linkDetails['typoLinkParameter'] = $linkParameter; 5456 if (isset($linkDetails['type']) && isset($GLOBALS['TYPO3_CONF_VARS']['FE']['typolinkBuilder'][$linkDetails['type']])) { 5457 /** @var AbstractTypolinkBuilder $linkBuilder */ 5458 $linkBuilder = GeneralUtility::makeInstance( 5459 $GLOBALS['TYPO3_CONF_VARS']['FE']['typolinkBuilder'][$linkDetails['type']], 5460 $this, 5461 $tsfe 5462 ); 5463 try { 5464 list($this->lastTypoLinkUrl, $linkText, $target) = $linkBuilder->build($linkDetails, $linkText, $target, $conf); 5465 $this->lastTypoLinkTarget = htmlspecialchars($target); 5466 $this->lastTypoLinkLD['target'] = htmlspecialchars($target); 5467 $this->lastTypoLinkLD['totalUrl'] = $this->lastTypoLinkUrl; 5468 } catch (UnableToLinkException $e) { 5469 $this->logger->debug(sprintf('Unable to link "%s": %s', $e->getLinkText(), $e->getMessage()), ['exception' => $e]); 5470 5471 // Only return the link text directly 5472 return $e->getLinkText(); 5473 } 5474 } elseif (isset($linkDetails['url'])) { 5475 $this->lastTypoLinkUrl = $linkDetails['url']; 5476 $this->lastTypoLinkTarget = htmlspecialchars($target); 5477 $this->lastTypoLinkLD['target'] = htmlspecialchars($target); 5478 $this->lastTypoLinkLD['totalUrl'] = $this->lastTypoLinkUrl; 5479 } else { 5480 return $linkText; 5481 } 5482 5483 // We need to backup the URL because ATagParams might call typolink again and change the last URL. 5484 $url = $this->lastTypoLinkUrl; 5485 $finalTagParts = [ 5486 'aTagParams' => $this->getATagParams($conf) . $this->extLinkATagParams($this->lastTypoLinkUrl, $linkDetails['type']), 5487 'url' => $url, 5488 'TYPE' => $linkDetails['type'] 5489 ]; 5490 5491 // Ensure "href" is not in the list of aTagParams to avoid double tags, usually happens within buggy parseFunc settings 5492 if (!empty($finalTagParts['aTagParams'])) { 5493 $aTagParams = GeneralUtility::get_tag_attributes($finalTagParts['aTagParams'], true); 5494 if (isset($aTagParams['href'])) { 5495 unset($aTagParams['href']); 5496 $finalTagParts['aTagParams'] = GeneralUtility::implodeAttributes($aTagParams, true); 5497 } 5498 } 5499 5500 // Building the final <a href=".."> tag 5501 $tagAttributes = []; 5502 5503 // Title attribute 5504 if (empty($title)) { 5505 $title = $conf['title'] ?? ''; 5506 if (isset($conf['title.']) && is_array($conf['title.'])) { 5507 $title = $this->stdWrap($title, $conf['title.']); 5508 } 5509 } 5510 5511 // Check, if the target is coded as a JS open window link: 5512 $JSwindowParts = []; 5513 $JSwindowParams = ''; 5514 if ($target && preg_match('/^([0-9]+)x([0-9]+)(:(.*)|.*)$/', $target, $JSwindowParts)) { 5515 // Take all pre-configured and inserted parameters and compile parameter list, including width+height: 5516 $JSwindow_tempParamsArr = GeneralUtility::trimExplode(',', strtolower($conf['JSwindow_params'] . ',' . $JSwindowParts[4]), true); 5517 $JSwindow_paramsArr = []; 5518 foreach ($JSwindow_tempParamsArr as $JSv) { 5519 list($JSp, $JSv) = explode('=', $JSv, 2); 5520 $JSwindow_paramsArr[$JSp] = $JSp . '=' . $JSv; 5521 } 5522 // Add width/height: 5523 $JSwindow_paramsArr['width'] = 'width=' . $JSwindowParts[1]; 5524 $JSwindow_paramsArr['height'] = 'height=' . $JSwindowParts[2]; 5525 // Imploding into string: 5526 $JSwindowParams = implode(',', $JSwindow_paramsArr); 5527 } 5528 5529 if (!$JSwindowParams && $linkDetails['type'] === LinkService::TYPE_EMAIL && $tsfe->spamProtectEmailAddresses === 'ascii') { 5530 $tagAttributes['href'] = $finalTagParts['url']; 5531 } else { 5532 $tagAttributes['href'] = htmlspecialchars($finalTagParts['url']); 5533 } 5534 if (!empty($title)) { 5535 $tagAttributes['title'] = htmlspecialchars($title); 5536 } 5537 5538 // Target attribute 5539 if (!empty($target)) { 5540 $tagAttributes['target'] = htmlspecialchars($target); 5541 } elseif ($JSwindowParams && !in_array($tsfe->xhtmlDoctype, ['xhtml_strict', 'xhtml_11'], true)) { 5542 // Create TARGET-attribute only if the right doctype is used 5543 $tagAttributes['target'] = 'FEopenLink'; 5544 } 5545 5546 if ($JSwindowParams) { 5547 $onClick = 'openPic(' . GeneralUtility::quoteJSvalue($tsfe->baseUrlWrap($finalTagParts['url'])) . ',\'FEopenLink\',' . GeneralUtility::quoteJSvalue($JSwindowParams) . ');return false;'; 5548 $tagAttributes['onclick'] = htmlspecialchars($onClick); 5549 $this->getTypoScriptFrontendController()->setJS('openPic'); 5550 } 5551 5552 if (!empty($resolvedLinkParameters['class'])) { 5553 $tagAttributes['class'] = htmlspecialchars($resolvedLinkParameters['class']); 5554 } 5555 5556 // Prevent trouble with double and missing spaces between attributes and merge params before implode 5557 // (skip decoding HTML entities, since `$tagAttributes` are expected to be encoded already) 5558 $finalTagAttributes = array_merge($tagAttributes, GeneralUtility::get_tag_attributes($finalTagParts['aTagParams'])); 5559 $finalAnchorTag = '<a ' . GeneralUtility::implodeAttributes($finalTagAttributes) . '>'; 5560 5561 // kept for backwards-compatibility in hooks 5562 $finalTagParts['targetParams'] = !empty($tagAttributes['target']) ? ' target="' . $tagAttributes['target'] . '"' : ''; 5563 $this->lastTypoLinkTarget = $target; 5564 5565 // Call user function: 5566 if ($conf['userFunc'] ?? false) { 5567 $finalTagParts['TAG'] = $finalAnchorTag; 5568 $finalAnchorTag = $this->callUserFunction($conf['userFunc'], $conf['userFunc.'], $finalTagParts); 5569 } 5570 5571 // Hook: Call post processing function for link rendering: 5572 $_params = [ 5573 'conf' => &$conf, 5574 'linktxt' => &$linkText, 5575 'finalTag' => &$finalAnchorTag, 5576 'finalTagParts' => &$finalTagParts, 5577 'linkDetails' => &$linkDetails, 5578 'tagAttributes' => &$finalTagAttributes 5579 ]; 5580 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['typoLink_PostProc'] ?? [] as $_funcRef) { 5581 GeneralUtility::callUserFunction($_funcRef, $_params, $this); 5582 } 5583 5584 // If flag "returnLastTypoLinkUrl" set, then just return the latest URL made: 5585 if ($conf['returnLast'] ?? false) { 5586 switch ($conf['returnLast']) { 5587 case 'url': 5588 return $this->lastTypoLinkUrl; 5589 case 'target': 5590 return $this->lastTypoLinkTarget; 5591 } 5592 } 5593 5594 $wrap = isset($conf['wrap.']) 5595 ? $this->stdWrap($conf['wrap'] ?? '', $conf['wrap.']) 5596 : $conf['wrap'] ?? ''; 5597 5598 if ($conf['ATagBeforeWrap'] ?? false) { 5599 return $finalAnchorTag . $this->wrap($linkText, $wrap) . '</a>'; 5600 } 5601 return $this->wrap($finalAnchorTag . $linkText . '</a>', $wrap); 5602 } 5603 5604 /** 5605 * Based on the input "TypoLink" TypoScript configuration this will return the generated URL 5606 * 5607 * @param array $conf TypoScript properties for "typolink 5608 * @return string The URL of the link-tag that typolink() would by itself return 5609 * @see typoLink() 5610 */ 5611 public function typoLink_URL($conf) 5612 { 5613 $this->typoLink('|', $conf); 5614 return $this->lastTypoLinkUrl; 5615 } 5616 5617 /** 5618 * Returns a linked string made from typoLink parameters. 5619 * 5620 * This function takes $label as a string, wraps it in a link-tag based on the $params string, which should contain data like that you would normally pass to the popular <LINK>-tag in the TSFE. 5621 * Optionally you can supply $urlParameters which is an array with key/value pairs that are rawurlencoded and appended to the resulting url. 5622 * 5623 * @param string $label Text string being wrapped by the link. 5624 * @param string $params Link parameter; eg. "123" for page id, "kasperYYYY@typo3.com" for email address, "http://...." for URL, "fileadmin/example.txt" for file. 5625 * @param array|string $urlParameters As an array key/value pairs represent URL parameters to set. Values NOT URL-encoded yet, keys should be URL-encoded if needed. As a string the parameter is expected to be URL-encoded already. 5626 * @param string $target Specific target set, if any. (Default is using the current) 5627 * @return string The wrapped $label-text string 5628 * @see getTypoLink_URL() 5629 */ 5630 public function getTypoLink($label, $params, $urlParameters = [], $target = '') 5631 { 5632 $conf = []; 5633 $conf['parameter'] = $params; 5634 if ($target) { 5635 $conf['target'] = $target; 5636 $conf['extTarget'] = $target; 5637 $conf['fileTarget'] = $target; 5638 } 5639 if (is_array($urlParameters)) { 5640 if (!empty($urlParameters)) { 5641 $conf['additionalParams'] .= HttpUtility::buildQueryString($urlParameters, '&'); 5642 } 5643 } else { 5644 $conf['additionalParams'] .= $urlParameters; 5645 } 5646 $out = $this->typoLink($label, $conf); 5647 return $out; 5648 } 5649 5650 /** 5651 * Returns the canonical URL to the current "location", which include the current page ID and type 5652 * and optionally the query string 5653 * 5654 * @param bool $addQueryString Whether additional GET arguments in the query string should be included or not 5655 * @return string 5656 */ 5657 public function getUrlToCurrentLocation($addQueryString = true) 5658 { 5659 $conf = []; 5660 $conf['parameter'] = $this->getTypoScriptFrontendController()->id . ',' . $this->getTypoScriptFrontendController()->type; 5661 if ($addQueryString) { 5662 $conf['addQueryString'] = '1'; 5663 $linkVars = implode(',', array_keys(GeneralUtility::explodeUrl2Array($this->getTypoScriptFrontendController()->linkVars))); 5664 $conf['addQueryString.'] = [ 5665 'method' => 'GET', 5666 'exclude' => 'id,type,cHash' . ($linkVars ? ',' . $linkVars : '') 5667 ]; 5668 $conf['useCacheHash'] = GeneralUtility::_GET('cHash') ? '1' : '0'; 5669 } 5670 5671 return $this->typoLink_URL($conf); 5672 } 5673 5674 /** 5675 * Returns the URL of a "typolink" create from the input parameter string, url-parameters and target 5676 * 5677 * @param string $params Link parameter; eg. "123" for page id, "kasperYYYY@typo3.com" for email address, "http://...." for URL, "fileadmin/example.txt" for file. 5678 * @param array|string $urlParameters As an array key/value pairs represent URL parameters to set. Values NOT URL-encoded yet, keys should be URL-encoded if needed. As a string the parameter is expected to be URL-encoded already. 5679 * @param string $target Specific target set, if any. (Default is using the current) 5680 * @return string The URL 5681 * @see getTypoLink() 5682 */ 5683 public function getTypoLink_URL($params, $urlParameters = [], $target = '') 5684 { 5685 $this->getTypoLink('', $params, $urlParameters, $target); 5686 return $this->lastTypoLinkUrl; 5687 } 5688 5689 /** 5690 * Generates a typolink and returns the two link tags - start and stop - in an array 5691 * 5692 * @param array $conf "typolink" TypoScript properties 5693 * @return array An array with two values in key 0+1, each value being the start and close <a>-tag of the typolink properties being inputted in $conf 5694 * @see typolink() 5695 * @deprecated since TYPO3 v9.5, will be removed in TYPO3 v10.0. Use typoLink() instead. 5696 */ 5697 public function typolinkWrap($conf) 5698 { 5699 trigger_error('ContentObjectRenderer->typolinkWrap() will be removed in TYPO3 v10.0. Use $cObj->typoLink() instead.', E_USER_DEPRECATED); 5700 $k = md5(microtime()); 5701 return explode($k, $this->typoLink($k, $conf)); 5702 } 5703 5704 /** 5705 * Returns the current page URL 5706 * 5707 * @param array|string $urlParameters As an array key/value pairs represent URL parameters to set. Values NOT URL-encoded yet, keys should be URL-encoded if needed. As a string the parameter is expected to be URL-encoded already. 5708 * @param int $id An alternative ID to the current id ($GLOBALS['TSFE']->id) 5709 * @return string The URL 5710 * @see getTypoLink_URL() 5711 * @deprecated since TYPO3 v9.5, will be removed in TYPO3 v10.0. Use getTypoLink_URL() instead. 5712 */ 5713 public function currentPageUrl($urlParameters = [], $id = 0) 5714 { 5715 trigger_error('ContentObjectRenderer->currentPageUrl() will be removed in TYPO3 v10.0. Use $cObj->getTypoLink_URL() instead.', E_USER_DEPRECATED); 5716 $tsfe = $this->getTypoScriptFrontendController(); 5717 return $this->getTypoLink_URL($id ?: $tsfe->id, $urlParameters, $tsfe->sPre); 5718 } 5719 5720 /** 5721 * Loops over all configured URL modifier hooks (if available) and returns the generated URL or NULL if no URL was generated. 5722 * 5723 * @param string $context The context in which the method is called (e.g. typoLink). 5724 * @param string $url The URL that should be processed. 5725 * @param array $typolinkConfiguration The current link configuration array. 5726 * @return string|null Returns NULL if URL was not processed or the processed URL as a string. 5727 * @throws \RuntimeException if a hook was registered but did not fulfill the correct parameters. 5728 */ 5729 protected function processUrl($context, $url, $typolinkConfiguration = []) 5730 { 5731 $urlProcessors = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['urlProcessing']['urlProcessors'] ?? []; 5732 if (empty($urlProcessors)) { 5733 return $url; 5734 } 5735 5736 foreach ($urlProcessors as $identifier => $configuration) { 5737 if (empty($configuration) || !is_array($configuration)) { 5738 throw new \RuntimeException('Missing configuration for URI processor "' . $identifier . '".', 1442050529); 5739 } 5740 if (!is_string($configuration['processor']) || empty($configuration['processor']) || !class_exists($configuration['processor']) || !is_subclass_of($configuration['processor'], UrlProcessorInterface::class)) { 5741 throw new \RuntimeException('The URI processor "' . $identifier . '" defines an invalid provider. Ensure the class exists and implements the "' . UrlProcessorInterface::class . '".', 1442050579); 5742 } 5743 } 5744 5745 $orderedProcessors = GeneralUtility::makeInstance(DependencyOrderingService::class)->orderByDependencies($urlProcessors); 5746 $keepProcessing = true; 5747 5748 foreach ($orderedProcessors as $configuration) { 5749 /** @var UrlProcessorInterface $urlProcessor */ 5750 $urlProcessor = GeneralUtility::makeInstance($configuration['processor']); 5751 $url = $urlProcessor->process($context, $url, $typolinkConfiguration, $this, $keepProcessing); 5752 if (!$keepProcessing) { 5753 break; 5754 } 5755 } 5756 5757 return $url; 5758 } 5759 5760 /** 5761 * Creates a href attibute for given $mailAddress. 5762 * The function uses spamProtectEmailAddresses for encoding the mailto statement. 5763 * If spamProtectEmailAddresses is disabled, it'll just return a string like "mailto:user@example.tld". 5764 * 5765 * @param string $mailAddress Email address 5766 * @param string $linktxt Link text, default will be the email address. 5767 * @return array A numerical array with two elements: 1) $mailToUrl, string ready to be inserted into the href attribute of the <a> tag, b) $linktxt: The string between starting and ending <a> tag. 5768 */ 5769 public function getMailTo($mailAddress, $linktxt) 5770 { 5771 $mailAddress = (string)$mailAddress; 5772 if ((string)$linktxt === '') { 5773 $linktxt = htmlspecialchars($mailAddress); 5774 } 5775 5776 $originalMailToUrl = 'mailto:' . $mailAddress; 5777 $mailToUrl = $this->processUrl(UrlProcessorInterface::CONTEXT_MAIL, $originalMailToUrl); 5778 5779 // no processing happened, therefore, the default processing kicks in 5780 if ($mailToUrl === $originalMailToUrl) { 5781 $tsfe = $this->getTypoScriptFrontendController(); 5782 if ($tsfe->spamProtectEmailAddresses) { 5783 $mailToUrl = $this->encryptEmail($mailToUrl, $tsfe->spamProtectEmailAddresses); 5784 if ($tsfe->spamProtectEmailAddresses !== 'ascii') { 5785 $encodedForJsAndHref = rawurlencode(GeneralUtility::quoteJSvalue($mailToUrl)); 5786 $mailToUrl = 'javascript:linkTo_UnCryptMailto(' . $encodedForJsAndHref . ');'; 5787 } 5788 $atLabel = trim($tsfe->config['config']['spamProtectEmailAddresses_atSubst']) ?: '(at)'; 5789 $spamProtectedMailAddress = str_replace('@', $atLabel, htmlspecialchars($mailAddress)); 5790 if ($tsfe->config['config']['spamProtectEmailAddresses_lastDotSubst']) { 5791 $lastDotLabel = trim($tsfe->config['config']['spamProtectEmailAddresses_lastDotSubst']); 5792 $lastDotLabel = $lastDotLabel ? $lastDotLabel : '(dot)'; 5793 $spamProtectedMailAddress = preg_replace('/\\.([^\\.]+)$/', $lastDotLabel . '$1', $spamProtectedMailAddress); 5794 } 5795 $linktxt = str_ireplace($mailAddress, $spamProtectedMailAddress, $linktxt); 5796 } 5797 } 5798 5799 return [$mailToUrl, $linktxt]; 5800 } 5801 5802 /** 5803 * Encryption of email addresses for <A>-tags See the spam protection setup in TS 'config.' 5804 * 5805 * @param string $string Input string to en/decode: "mailto:some@example.com 5806 * @param mixed $type - either "ascii" or a number between -10 and 10, taken from config.spamProtectEmailAddresses 5807 * @return string encoded version of $string 5808 */ 5809 protected function encryptEmail(string $string, $type): string 5810 { 5811 $out = ''; 5812 // obfuscates using the decimal HTML entity references for each character 5813 if ($type === 'ascii') { 5814 foreach (preg_split('//u', $string, -1, PREG_SPLIT_NO_EMPTY) as $char) { 5815 $out .= '&#' . mb_ord($char) . ';'; 5816 } 5817 } else { 5818 // like str_rot13() but with a variable offset and a wider character range 5819 $len = strlen($string); 5820 $offset = (int)$type; 5821 for ($i = 0; $i < $len; $i++) { 5822 $charValue = ord($string[$i]); 5823 // 0-9 . , - + / : 5824 if ($charValue >= 43 && $charValue <= 58) { 5825 $out .= $this->encryptCharcode($charValue, 43, 58, $offset); 5826 } elseif ($charValue >= 64 && $charValue <= 90) { 5827 // A-Z @ 5828 $out .= $this->encryptCharcode($charValue, 64, 90, $offset); 5829 } elseif ($charValue >= 97 && $charValue <= 122) { 5830 // a-z 5831 $out .= $this->encryptCharcode($charValue, 97, 122, $offset); 5832 } else { 5833 $out .= $string[$i]; 5834 } 5835 } 5836 } 5837 return $out; 5838 } 5839 5840 /** 5841 * Decryption of email addresses for <A>-tags See the spam protection setup in TS 'config.' 5842 * 5843 * @param string $string Input string to en/decode: "mailto:some@example.com 5844 * @param mixed $type - either "ascii" or a number between -10 and 10 taken from config.spamProtectEmailAddresses 5845 * @return string decoded version of $string 5846 */ 5847 protected function decryptEmail(string $string, $type): string 5848 { 5849 $out = ''; 5850 // obfuscates using the decimal HTML entity references for each character 5851 if ($type === 'ascii') { 5852 foreach (preg_split('//u', $string, -1, PREG_SPLIT_NO_EMPTY) as $char) { 5853 $out .= '&#' . mb_ord($char) . ';'; 5854 } 5855 } else { 5856 // like str_rot13() but with a variable offset and a wider character range 5857 $len = strlen($string); 5858 $offset = (int)$type * -1; 5859 for ($i = 0; $i < $len; $i++) { 5860 $charValue = ord($string[$i]); 5861 // 0-9 . , - + / : 5862 if ($charValue >= 43 && $charValue <= 58) { 5863 $out .= $this->encryptCharcode($charValue, 43, 58, $offset); 5864 } elseif ($charValue >= 64 && $charValue <= 90) { 5865 // A-Z @ 5866 $out .= $this->encryptCharcode($charValue, 64, 90, $offset); 5867 } elseif ($charValue >= 97 && $charValue <= 122) { 5868 // a-z 5869 $out .= $this->encryptCharcode($charValue, 97, 122, $offset); 5870 } else { 5871 $out .= $string[$i]; 5872 } 5873 } 5874 } 5875 return $out; 5876 } 5877 5878 /** 5879 * Encryption (or decryption) of a single character. 5880 * Within the given range the character is shifted with the supplied offset. 5881 * 5882 * @param int $n Ordinal of input character 5883 * @param int $start Start of range 5884 * @param int $end End of range 5885 * @param int $offset Offset 5886 * @return string encoded/decoded version of character 5887 */ 5888 protected function encryptCharcode($n, $start, $end, $offset) 5889 { 5890 $n = $n + $offset; 5891 if ($offset > 0 && $n > $end) { 5892 $n = $start + ($n - $end - 1); 5893 } elseif ($offset < 0 && $n < $start) { 5894 $n = $end - ($start - $n - 1); 5895 } 5896 return chr($n); 5897 } 5898 5899 /** 5900 * Gets the query arguments and assembles them for URLs. 5901 * Arguments may be removed or set, depending on configuration. 5902 * 5903 * @param array $conf Configuration 5904 * @param array $overruleQueryArguments Multidimensional key/value pairs that overrule incoming query arguments 5905 * @param bool $forceOverruleArguments If set, key/value pairs not in the query but the overrule array will be set 5906 * @return string The URL query part (starting with a &) 5907 */ 5908 public function getQueryArguments($conf, $overruleQueryArguments = [], $forceOverruleArguments = false) 5909 { 5910 $method = (string)($conf['method'] ?? ''); 5911 switch ($method) { 5912 case 'GET': 5913 $currentQueryArray = GeneralUtility::_GET(); 5914 break; 5915 case 'POST': 5916 $currentQueryArray = GeneralUtility::_POST(); 5917 break; 5918 case 'GET,POST': 5919 $currentQueryArray = GeneralUtility::_GET(); 5920 ArrayUtility::mergeRecursiveWithOverrule($currentQueryArray, GeneralUtility::_POST()); 5921 break; 5922 case 'POST,GET': 5923 $currentQueryArray = GeneralUtility::_POST(); 5924 ArrayUtility::mergeRecursiveWithOverrule($currentQueryArray, GeneralUtility::_GET()); 5925 break; 5926 default: 5927 $currentQueryArray = []; 5928 parse_str($this->getEnvironmentVariable('QUERY_STRING'), $currentQueryArray); 5929 } 5930 if ($conf['exclude'] ?? false) { 5931 $excludeString = str_replace(',', '&', $conf['exclude']); 5932 $excludedQueryParts = []; 5933 parse_str($excludeString, $excludedQueryParts); 5934 // never repeat id 5935 $exclude['id'] = 0; 5936 $newQueryArray = ArrayUtility::arrayDiffAssocRecursive($currentQueryArray, $excludedQueryParts); 5937 } else { 5938 $newQueryArray = $currentQueryArray; 5939 } 5940 ArrayUtility::mergeRecursiveWithOverrule($newQueryArray, $overruleQueryArguments, $forceOverruleArguments); 5941 return HttpUtility::buildQueryString($newQueryArray, '&'); 5942 } 5943 5944 /*********************************************** 5945 * 5946 * Miscellaneous functions, stand alone 5947 * 5948 ***********************************************/ 5949 /** 5950 * Wrapping a string. 5951 * Implements the TypoScript "wrap" property. 5952 * Example: $content = "HELLO WORLD" and $wrap = "<strong> | </strong>", result: "<strong>HELLO WORLD</strong>" 5953 * 5954 * @param string $content The content to wrap 5955 * @param string $wrap The wrap value, eg. "<strong> | </strong> 5956 * @param string $char The char used to split the wrapping value, default is "| 5957 * @return string Wrapped input string 5958 * @see noTrimWrap() 5959 */ 5960 public function wrap($content, $wrap, $char = '|') 5961 { 5962 if ($wrap) { 5963 $wrapArr = explode($char, $wrap); 5964 $content = trim($wrapArr[0] ?? '') . $content . trim($wrapArr[1] ?? ''); 5965 } 5966 return $content; 5967 } 5968 5969 /** 5970 * Wrapping a string, preserving whitespace in wrap value. 5971 * Notice that the wrap value uses part 1/2 to wrap (and not 0/1 which wrap() does) 5972 * 5973 * @param string $content The content to wrap, eg. "HELLO WORLD 5974 * @param string $wrap The wrap value, eg. " | <strong> | </strong> 5975 * @param string $char The char used to split the wrapping value, default is "|" 5976 * @return string Wrapped input string, eg. " <strong> HELLO WORD </strong> 5977 * @see wrap() 5978 */ 5979 public function noTrimWrap($content, $wrap, $char = '|') 5980 { 5981 if ($wrap) { 5982 // expects to be wrapped with (at least) 3 characters (before, middle, after) 5983 // anything else is not taken into account 5984 $wrapArr = explode($char, $wrap, 4); 5985 $content = $wrapArr[1] . $content . $wrapArr[2]; 5986 } 5987 return $content; 5988 } 5989 5990 /** 5991 * Calling a user function/class-method 5992 * Notice: For classes the instantiated object will have the internal variable, $cObj, set to be a *reference* to $this (the parent/calling object). 5993 * 5994 * @param string $funcName The functionname, eg "user_myfunction" or "user_myclass->main". Notice that there are rules for the names of functions/classes you can instantiate. If a function cannot be called for some reason it will be seen in the TypoScript log in the AdminPanel. 5995 * @param array $conf The TypoScript configuration to pass the function 5996 * @param string $content The content string to pass the function 5997 * @return string The return content from the function call. Should probably be a string. 5998 * @see USER(), stdWrap(), typoLink(), _parseFunc() 5999 */ 6000 public function callUserFunction($funcName, $conf, $content) 6001 { 6002 // Split parts 6003 $parts = explode('->', $funcName); 6004 if (count($parts) === 2) { 6005 // Check whether PHP class is available 6006 if (class_exists($parts[0])) { 6007 $classObj = GeneralUtility::makeInstance($parts[0]); 6008 if (is_object($classObj) && method_exists($classObj, $parts[1])) { 6009 $classObj->cObj = $this; 6010 $content = call_user_func_array([ 6011 $classObj, 6012 $parts[1] 6013 ], [ 6014 $content, 6015 $conf 6016 ]); 6017 } else { 6018 $this->getTimeTracker()->setTSlogMessage('Method "' . $parts[1] . '" did not exist in class "' . $parts[0] . '"', 3); 6019 } 6020 } else { 6021 $this->getTimeTracker()->setTSlogMessage('Class "' . $parts[0] . '" did not exist', 3); 6022 } 6023 } elseif (function_exists($funcName)) { 6024 $content = call_user_func($funcName, $content, $conf); 6025 } else { 6026 $this->getTimeTracker()->setTSlogMessage('Function "' . $funcName . '" did not exist', 3); 6027 } 6028 return $content; 6029 } 6030 6031 /** 6032 * Cleans up a string of keywords. Keywords at splitted by "," (comma) ";" (semi colon) and linebreak 6033 * 6034 * @param string $content String of keywords 6035 * @return string Cleaned up string, keywords will be separated by a comma only. 6036 */ 6037 public function keywords($content) 6038 { 6039 $listArr = preg_split('/[,;' . LF . ']/', $content); 6040 foreach ($listArr as $k => $v) { 6041 $listArr[$k] = trim($v); 6042 } 6043 return implode(',', $listArr); 6044 } 6045 6046 /** 6047 * Changing character case of a string, converting typically used western charset characters as well. 6048 * 6049 * @param string $theValue The string to change case for. 6050 * @param string $case The direction; either "upper" or "lower 6051 * @return string 6052 * @see HTMLcaseshift() 6053 */ 6054 public function caseshift($theValue, $case) 6055 { 6056 switch (strtolower($case)) { 6057 case 'upper': 6058 $theValue = mb_strtoupper($theValue, 'utf-8'); 6059 break; 6060 case 'lower': 6061 $theValue = mb_strtolower($theValue, 'utf-8'); 6062 break; 6063 case 'capitalize': 6064 $theValue = mb_convert_case($theValue, MB_CASE_TITLE, 'utf-8'); 6065 break; 6066 case 'ucfirst': 6067 $firstChar = mb_substr($theValue, 0, 1, 'utf-8'); 6068 $firstChar = mb_strtoupper($firstChar, 'utf-8'); 6069 $remainder = mb_substr($theValue, 1, null, 'utf-8'); 6070 $theValue = $firstChar . $remainder; 6071 break; 6072 case 'lcfirst': 6073 $firstChar = mb_substr($theValue, 0, 1, 'utf-8'); 6074 $firstChar = mb_strtolower($firstChar, 'utf-8'); 6075 $remainder = mb_substr($theValue, 1, null, 'utf-8'); 6076 $theValue = $firstChar . $remainder; 6077 break; 6078 case 'uppercamelcase': 6079 $theValue = GeneralUtility::underscoredToUpperCamelCase($theValue); 6080 break; 6081 case 'lowercamelcase': 6082 $theValue = GeneralUtility::underscoredToLowerCamelCase($theValue); 6083 break; 6084 } 6085 return $theValue; 6086 } 6087 6088 /** 6089 * Shifts the case of characters outside of HTML tags in the input string 6090 * 6091 * @param string $theValue The string to change case for. 6092 * @param string $case The direction; either "upper" or "lower 6093 * @return string 6094 * @see caseshift() 6095 */ 6096 public function HTMLcaseshift($theValue, $case) 6097 { 6098 $inside = 0; 6099 $newVal = ''; 6100 $pointer = 0; 6101 $totalLen = strlen($theValue); 6102 do { 6103 if (!$inside) { 6104 $len = strcspn(substr($theValue, $pointer), '<'); 6105 $newVal .= $this->caseshift(substr($theValue, $pointer, $len), $case); 6106 $inside = 1; 6107 } else { 6108 $len = strcspn(substr($theValue, $pointer), '>') + 1; 6109 $newVal .= substr($theValue, $pointer, $len); 6110 $inside = 0; 6111 } 6112 $pointer += $len; 6113 } while ($pointer < $totalLen); 6114 return $newVal; 6115 } 6116 6117 /** 6118 * Returns the 'age' of the tstamp $seconds 6119 * 6120 * @param int $seconds Seconds to return age for. Example: "70" => "1 min", "3601" => "1 hrs 6121 * @param string $labels The labels of the individual units. Defaults to : ' min| hrs| days| yrs' 6122 * @return string The formatted string 6123 */ 6124 public function calcAge($seconds, $labels) 6125 { 6126 if (MathUtility::canBeInterpretedAsInteger($labels)) { 6127 $labels = ' min| hrs| days| yrs| min| hour| day| year'; 6128 } else { 6129 $labels = str_replace('"', '', $labels); 6130 } 6131 $labelArr = explode('|', $labels); 6132 if (count($labelArr) === 4) { 6133 $labelArr = array_merge($labelArr, $labelArr); 6134 } 6135 $absSeconds = abs($seconds); 6136 $sign = $seconds > 0 ? 1 : -1; 6137 if ($absSeconds < 3600) { 6138 $val = round($absSeconds / 60); 6139 $seconds = $sign * $val . ($val == 1 ? $labelArr[4] : $labelArr[0]); 6140 } elseif ($absSeconds < 24 * 3600) { 6141 $val = round($absSeconds / 3600); 6142 $seconds = $sign * $val . ($val == 1 ? $labelArr[5] : $labelArr[1]); 6143 } elseif ($absSeconds < 365 * 24 * 3600) { 6144 $val = round($absSeconds / (24 * 3600)); 6145 $seconds = $sign * $val . ($val == 1 ? $labelArr[6] : $labelArr[2]); 6146 } else { 6147 $val = round($absSeconds / (365 * 24 * 3600)); 6148 $seconds = $sign * $val . ($val == 1 ? ($labelArr[7] ?? null) : ($labelArr[3] ?? null)); 6149 } 6150 return $seconds; 6151 } 6152 6153 /** 6154 * Sends a notification email 6155 * 6156 * @param string $message The message content. If blank, no email is sent. 6157 * @param string $recipients Comma list of recipient email addresses 6158 * @param string $cc Email address of recipient of an extra mail. The same mail will be sent ONCE more; not using a CC header but sending twice. 6159 * @param string $senderAddress "From" email address 6160 * @param string $senderName Optional "From" name 6161 * @param string $replyTo Optional "Reply-To" header email address. 6162 * @return bool Returns TRUE if sent 6163 */ 6164 public function sendNotifyEmail($message, $recipients, $cc, $senderAddress, $senderName = '', $replyTo = '') 6165 { 6166 /** @var MailMessage $mail */ 6167 $mail = GeneralUtility::makeInstance(MailMessage::class); 6168 $senderName = trim($senderName); 6169 $senderAddress = trim($senderAddress); 6170 if ($senderName !== '' && $senderAddress !== '') { 6171 $mail->setFrom([$senderAddress => $senderName]); 6172 } elseif ($senderAddress !== '') { 6173 $mail->setFrom([$senderAddress]); 6174 } 6175 $parsedReplyTo = MailUtility::parseAddresses($replyTo); 6176 if (!empty($parsedReplyTo)) { 6177 $mail->setReplyTo($parsedReplyTo); 6178 } 6179 $message = trim($message); 6180 if ($message !== '') { 6181 // First line is subject 6182 $messageParts = explode(LF, $message, 2); 6183 $subject = trim($messageParts[0]); 6184 $plainMessage = trim($messageParts[1]); 6185 $parsedRecipients = MailUtility::parseAddresses($recipients); 6186 if (!empty($parsedRecipients)) { 6187 $mail->setTo($parsedRecipients) 6188 ->setSubject($subject) 6189 ->setBody($plainMessage); 6190 $mail->send(); 6191 } 6192 $parsedCc = MailUtility::parseAddresses($cc); 6193 if (!empty($parsedCc)) { 6194 $from = $mail->getFrom(); 6195 /** @var MailMessage $mail */ 6196 $mail = GeneralUtility::makeInstance(MailMessage::class); 6197 if (!empty($parsedReplyTo)) { 6198 $mail->setReplyTo($parsedReplyTo); 6199 } 6200 $mail->setFrom($from) 6201 ->setTo($parsedCc) 6202 ->setSubject($subject) 6203 ->setBody($plainMessage); 6204 $mail->send(); 6205 } 6206 return true; 6207 } 6208 return false; 6209 } 6210 6211 /** 6212 * Resolves a TypoScript reference value to the full set of properties BUT overridden with any local properties set. 6213 * So the reference is resolved but overlaid with local TypoScript properties of the reference value. 6214 * 6215 * @param array $confArr The TypoScript array 6216 * @param string $prop The property name: If this value is a reference (eg. " < plugins.tx_something") then the reference will be retrieved and inserted at that position (into the properties only, not the value...) AND overlaid with the old properties if any. 6217 * @return array The modified TypoScript array 6218 */ 6219 public function mergeTSRef($confArr, $prop) 6220 { 6221 if ($confArr[$prop][0] === '<') { 6222 $key = trim(substr($confArr[$prop], 1)); 6223 $cF = GeneralUtility::makeInstance(TypoScriptParser::class); 6224 // $name and $conf is loaded with the referenced values. 6225 $old_conf = $confArr[$prop . '.']; 6226 list(, $conf) = $cF->getVal($key, $this->getTypoScriptFrontendController()->tmpl->setup); 6227 if (is_array($old_conf) && !empty($old_conf)) { 6228 $conf = is_array($conf) ? array_replace_recursive($conf, $old_conf) : $old_conf; 6229 } 6230 $confArr[$prop . '.'] = $conf; 6231 } 6232 return $confArr; 6233 } 6234 6235 /*********************************************** 6236 * 6237 * Database functions, making of queries 6238 * 6239 ***********************************************/ 6240 6241 /** 6242 * Returns a part of a WHERE clause which will filter out records with start/end times or hidden/fe_groups fields 6243 * set to values that should de-select them according to the current time, preview settings or user login. 6244 * Definitely a frontend function. 6245 * THIS IS A VERY IMPORTANT FUNCTION: Basically you must add the output from this function for EVERY select query you create 6246 * for selecting records of tables in your own applications - thus they will always be filtered according to the "enablefields" 6247 * configured in TCA 6248 * Simply calls \TYPO3\CMS\Frontend\Page\PageRepository::enableFields() BUT will send the show_hidden flag along! 6249 * This means this function will work in conjunction with the preview facilities of the frontend engine/Admin Panel. 6250 * 6251 * @param string $table The table for which to get the where clause 6252 * @param bool $show_hidden If set, then you want NOT to filter out hidden records. Otherwise hidden record are filtered based on the current preview settings. 6253 * @param array $ignore_array Array you can pass where keys can be "disabled", "starttime", "endtime", "fe_group" (keys from "enablefields" in TCA) and if set they will make sure that part of the clause is not added. Thus disables the specific part of the clause. For previewing etc. 6254 * @return string The part of the where clause on the form " AND [fieldname]=0 AND ...". Eg. " AND hidden=0 AND starttime < 123345567 6255 * @deprecated since TYPO3 v9.4, will be removed in TYPO3 v10.0. 6256 */ 6257 public function enableFields($table, $show_hidden = false, array $ignore_array = []) 6258 { 6259 trigger_error('cObj->enableFields() will be removed in TYPO3 v10.0. should be used from the PageRepository->enableFields() functionality directly.', E_USER_DEPRECATED); 6260 return $this->getTypoScriptFrontendController()->sys_page->enableFields($table, $show_hidden ? true : -1, $ignore_array); 6261 } 6262 6263 /** 6264 * Generates a list of Page-uid's from $id. List does not include $id itself 6265 * (unless the id specified is negative in which case it does!) 6266 * The only pages WHICH PREVENTS DECENDING in a branch are 6267 * - deleted pages, 6268 * - pages in a recycler (doktype = 255) or of the Backend User Section (doktpe = 6) type 6269 * - pages that has the extendToSubpages set, WHERE start/endtime, hidden 6270 * and fe_users would hide the records. 6271 * Apart from that, pages with enable-fields excluding them, will also be 6272 * removed. HOWEVER $dontCheckEnableFields set will allow 6273 * enableFields-excluded pages to be included anyway - including 6274 * extendToSubpages sections! 6275 * Mount Pages are also descended but notice that these ID numbers are not 6276 * useful for links unless the correct MPvar is set. 6277 * 6278 * @param int $id The id of the start page from which point in the page tree to descend. IF NEGATIVE the id itself is included in the end of the list (only if $begin is 0) AND the output does NOT contain a last comma. Recommended since it will resolve the input ID for mount pages correctly and also check if the start ID actually exists! 6279 * @param int $depth The number of levels to descend. If you want to descend infinitely, just set this to 100 or so. Should be at least "1" since zero will just make the function return (no decend...) 6280 * @param int $begin Is an optional integer that determines at which level in the tree to start collecting uid's. Zero means 'start right away', 1 = 'next level and out' 6281 * @param bool $dontCheckEnableFields See function description 6282 * @param string $addSelectFields Additional fields to select. Syntax: ",[fieldname],[fieldname],... 6283 * @param string $moreWhereClauses Additional where clauses. Syntax: " AND [fieldname]=[value] AND ... 6284 * @param array $prevId_array array of IDs from previous recursions. In order to prevent infinite loops with mount pages. 6285 * @param int $recursionLevel Internal: Zero for the first recursion, incremented for each recursive call. 6286 * @return string Returns the list of ids as a comma separated string 6287 * @see TypoScriptFrontendController::checkEnableFields(), TypoScriptFrontendController::checkPagerecordForIncludeSection() 6288 */ 6289 public function getTreeList($id, $depth, $begin = 0, $dontCheckEnableFields = false, $addSelectFields = '', $moreWhereClauses = '', array $prevId_array = [], $recursionLevel = 0) 6290 { 6291 $id = (int)$id; 6292 if (!$id) { 6293 return ''; 6294 } 6295 6296 // Init vars: 6297 $allFields = 'uid,hidden,starttime,endtime,fe_group,extendToSubpages,doktype,php_tree_stop,mount_pid,mount_pid_ol,t3ver_state' . $addSelectFields; 6298 $depth = (int)$depth; 6299 $begin = (int)$begin; 6300 $theList = []; 6301 $addId = 0; 6302 $requestHash = ''; 6303 6304 // First level, check id (second level, this is done BEFORE the recursive call) 6305 $tsfe = $this->getTypoScriptFrontendController(); 6306 if (!$recursionLevel) { 6307 // Check tree list cache 6308 // First, create the hash for this request - not sure yet whether we need all these parameters though 6309 $parameters = [ 6310 $id, 6311 $depth, 6312 $begin, 6313 $dontCheckEnableFields, 6314 $addSelectFields, 6315 $moreWhereClauses, 6316 $prevId_array, 6317 GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('frontend.user', 'groupIds', [0, -1]) 6318 ]; 6319 $requestHash = md5(serialize($parameters)); 6320 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) 6321 ->getQueryBuilderForTable('cache_treelist'); 6322 $cacheEntry = $queryBuilder->select('treelist') 6323 ->from('cache_treelist') 6324 ->where( 6325 $queryBuilder->expr()->eq( 6326 'md5hash', 6327 $queryBuilder->createNamedParameter($requestHash, \PDO::PARAM_STR) 6328 ), 6329 $queryBuilder->expr()->orX( 6330 $queryBuilder->expr()->gt( 6331 'expires', 6332 $queryBuilder->createNamedParameter($GLOBALS['EXEC_TIME'], \PDO::PARAM_INT) 6333 ), 6334 $queryBuilder->expr()->eq('expires', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)) 6335 ) 6336 ) 6337 ->setMaxResults(1) 6338 ->execute() 6339 ->fetch(); 6340 6341 if (is_array($cacheEntry)) { 6342 // Cache hit 6343 return $cacheEntry['treelist']; 6344 } 6345 // If Id less than zero it means we should add the real id to list: 6346 if ($id < 0) { 6347 $addId = $id = abs($id); 6348 } 6349 // Check start page: 6350 if ($tsfe->sys_page->getRawRecord('pages', $id, 'uid')) { 6351 // Find mount point if any: 6352 $mount_info = $tsfe->sys_page->getMountPointInfo($id); 6353 if (is_array($mount_info)) { 6354 $id = $mount_info['mount_pid']; 6355 // In Overlay mode, use the mounted page uid as added ID!: 6356 if ($addId && $mount_info['overlay']) { 6357 $addId = $id; 6358 } 6359 } 6360 } else { 6361 // Return blank if the start page was NOT found at all! 6362 return ''; 6363 } 6364 } 6365 // Add this ID to the array of IDs 6366 if ($begin <= 0) { 6367 $prevId_array[] = $id; 6368 } 6369 // Select sublevel: 6370 if ($depth > 0) { 6371 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages'); 6372 $queryBuilder->getRestrictions() 6373 ->removeAll() 6374 ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); 6375 $queryBuilder->select(...GeneralUtility::trimExplode(',', $allFields, true)) 6376 ->from('pages') 6377 ->where( 6378 $queryBuilder->expr()->eq( 6379 'pid', 6380 $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT) 6381 ), 6382 // tree is only built by language=0 pages 6383 $queryBuilder->expr()->eq('sys_language_uid', 0) 6384 ) 6385 ->orderBy('sorting'); 6386 6387 if (!empty($moreWhereClauses)) { 6388 $queryBuilder->andWhere(QueryHelper::stripLogicalOperatorPrefix($moreWhereClauses)); 6389 } 6390 6391 $result = $queryBuilder->execute(); 6392 while ($row = $result->fetch()) { 6393 /** @var VersionState $versionState */ 6394 $versionState = VersionState::cast($row['t3ver_state']); 6395 $tsfe->sys_page->versionOL('pages', $row); 6396 if ((int)$row['doktype'] === PageRepository::DOKTYPE_RECYCLER 6397 || (int)$row['doktype'] === PageRepository::DOKTYPE_BE_USER_SECTION 6398 || $versionState->indicatesPlaceholder() 6399 ) { 6400 // Doing this after the overlay to make sure changes 6401 // in the overlay are respected. 6402 // However, we do not process pages below of and 6403 // including of type recycler and BE user section 6404 continue; 6405 } 6406 // Find mount point if any: 6407 $next_id = $row['uid']; 6408 $mount_info = $tsfe->sys_page->getMountPointInfo($next_id, $row); 6409 // Overlay mode: 6410 if (is_array($mount_info) && $mount_info['overlay']) { 6411 $next_id = $mount_info['mount_pid']; 6412 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) 6413 ->getQueryBuilderForTable('pages'); 6414 $queryBuilder->getRestrictions() 6415 ->removeAll() 6416 ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); 6417 $queryBuilder->select(...GeneralUtility::trimExplode(',', $allFields, true)) 6418 ->from('pages') 6419 ->where( 6420 $queryBuilder->expr()->eq( 6421 'uid', 6422 $queryBuilder->createNamedParameter($next_id, \PDO::PARAM_INT) 6423 ) 6424 ) 6425 ->orderBy('sorting') 6426 ->setMaxResults(1); 6427 6428 if (!empty($moreWhereClauses)) { 6429 $queryBuilder->andWhere(QueryHelper::stripLogicalOperatorPrefix($moreWhereClauses)); 6430 } 6431 6432 $row = $queryBuilder->execute()->fetch(); 6433 $tsfe->sys_page->versionOL('pages', $row); 6434 if ((int)$row['doktype'] === PageRepository::DOKTYPE_RECYCLER 6435 || (int)$row['doktype'] === PageRepository::DOKTYPE_BE_USER_SECTION 6436 || $versionState->indicatesPlaceholder() 6437 ) { 6438 // Doing this after the overlay to make sure 6439 // changes in the overlay are respected. 6440 // see above 6441 continue; 6442 } 6443 } 6444 // Add record: 6445 if ($dontCheckEnableFields || $tsfe->checkPagerecordForIncludeSection($row)) { 6446 // Add ID to list: 6447 if ($begin <= 0) { 6448 if ($dontCheckEnableFields || $tsfe->checkEnableFields($row)) { 6449 $theList[] = $next_id; 6450 } 6451 } 6452 // Next level: 6453 if ($depth > 1 && !$row['php_tree_stop']) { 6454 // Normal mode: 6455 if (is_array($mount_info) && !$mount_info['overlay']) { 6456 $next_id = $mount_info['mount_pid']; 6457 } 6458 // Call recursively, if the id is not in prevID_array: 6459 if (!in_array($next_id, $prevId_array)) { 6460 $theList = array_merge( 6461 GeneralUtility::intExplode( 6462 ',', 6463 $this->getTreeList( 6464 $next_id, 6465 $depth - 1, 6466 $begin - 1, 6467 $dontCheckEnableFields, 6468 $addSelectFields, 6469 $moreWhereClauses, 6470 $prevId_array, 6471 $recursionLevel + 1 6472 ), 6473 true 6474 ), 6475 $theList 6476 ); 6477 } 6478 } 6479 } 6480 } 6481 } 6482 // If first run, check if the ID should be returned: 6483 if (!$recursionLevel) { 6484 if ($addId) { 6485 if ($begin > 0) { 6486 $theList[] = 0; 6487 } else { 6488 $theList[] = $addId; 6489 } 6490 } 6491 6492 $cacheEntry = [ 6493 'md5hash' => $requestHash, 6494 'pid' => $id, 6495 'treelist' => implode(',', $theList), 6496 'tstamp' => $GLOBALS['EXEC_TIME'], 6497 ]; 6498 6499 // Only add to cache if not logged into TYPO3 Backend 6500 if (!$this->getFrontendBackendUser() instanceof AbstractUserAuthentication) { 6501 $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('cache_treelist'); 6502 try { 6503 $connection->transactional(function ($connection) use ($cacheEntry) { 6504 $connection->insert('cache_treelist', $cacheEntry); 6505 }); 6506 } catch (\Throwable $e) { 6507 } 6508 } 6509 } 6510 6511 return implode(',', $theList); 6512 } 6513 6514 /** 6515 * Generates a search where clause based on the input search words (AND operation - all search words must be found in record.) 6516 * Example: The $sw is "content management, system" (from an input form) and the $searchFieldList is "bodytext,header" then the output will be ' AND (bodytext LIKE "%content%" OR header LIKE "%content%") AND (bodytext LIKE "%management%" OR header LIKE "%management%") AND (bodytext LIKE "%system%" OR header LIKE "%system%")' 6517 * 6518 * @param string $searchWords The search words. These will be separated by space and comma. 6519 * @param string $searchFieldList The fields to search in 6520 * @param string $searchTable The table name you search in (recommended for DBAL compliance. Will be prepended field names as well) 6521 * @return string The WHERE clause. 6522 */ 6523 public function searchWhere($searchWords, $searchFieldList, $searchTable) 6524 { 6525 if (!$searchWords) { 6526 return ''; 6527 } 6528 6529 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) 6530 ->getQueryBuilderForTable($searchTable); 6531 6532 $prefixTableName = $searchTable ? $searchTable . '.' : ''; 6533 6534 $where = $queryBuilder->expr()->andX(); 6535 $searchFields = explode(',', $searchFieldList); 6536 $searchWords = preg_split('/[ ,]/', $searchWords); 6537 foreach ($searchWords as $searchWord) { 6538 $searchWord = trim($searchWord); 6539 if (strlen($searchWord) < 3) { 6540 continue; 6541 } 6542 $searchWordConstraint = $queryBuilder->expr()->orX(); 6543 $searchWord = $queryBuilder->escapeLikeWildcards($searchWord); 6544 foreach ($searchFields as $field) { 6545 $searchWordConstraint->add( 6546 $queryBuilder->expr()->like($prefixTableName . $field, $queryBuilder->quote('%' . $searchWord . '%')) 6547 ); 6548 } 6549 6550 if ($searchWordConstraint->count()) { 6551 $where->add($searchWordConstraint); 6552 } 6553 } 6554 6555 if ((string)$where === '') { 6556 return ''; 6557 } 6558 6559 return ' AND (' . (string)$where . ')'; 6560 } 6561 6562 /** 6563 * Executes a SELECT query for records from $table and with conditions based on the configuration in the $conf array 6564 * This function is preferred over ->getQuery() if you just need to create and then execute a query. 6565 * 6566 * @param string $table The table name 6567 * @param array $conf The TypoScript configuration properties 6568 * @return Statement 6569 * @see getQuery() 6570 */ 6571 public function exec_getQuery($table, $conf) 6572 { 6573 $statement = $this->getQuery($table, $conf); 6574 $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table); 6575 6576 return $connection->executeQuery($statement); 6577 } 6578 6579 /** 6580 * Executes a SELECT query for records from $table and with conditions based on the configuration in the $conf array 6581 * and overlays with translation and version if available 6582 * 6583 * @param string $tableName the name of the TCA database table 6584 * @param array $queryConfiguration The TypoScript configuration properties, see .select in TypoScript reference 6585 * @return array The records 6586 * @throws \UnexpectedValueException 6587 */ 6588 public function getRecords($tableName, array $queryConfiguration) 6589 { 6590 $records = []; 6591 6592 $statement = $this->exec_getQuery($tableName, $queryConfiguration); 6593 6594 $tsfe = $this->getTypoScriptFrontendController(); 6595 while ($row = $statement->fetch()) { 6596 // Versioning preview: 6597 $tsfe->sys_page->versionOL($tableName, $row, true); 6598 6599 // Language overlay: 6600 if (is_array($row)) { 6601 $row = $tsfe->sys_page->getLanguageOverlay($tableName, $row); 6602 } 6603 6604 // Might be unset in the language overlay 6605 if (is_array($row)) { 6606 $records[] = $row; 6607 } 6608 } 6609 6610 return $records; 6611 } 6612 6613 /** 6614 * Creates and returns a SELECT query for records from $table and with conditions based on the configuration in the $conf array 6615 * Implements the "select" function in TypoScript 6616 * 6617 * @param string $table See ->exec_getQuery() 6618 * @param array $conf See ->exec_getQuery() 6619 * @param bool $returnQueryArray If set, the function will return the query not as a string but array with the various parts. RECOMMENDED! 6620 * @return mixed A SELECT query if $returnQueryArray is FALSE, otherwise the SELECT query in an array as parts. 6621 * @throws \RuntimeException 6622 * @throws \InvalidArgumentException 6623 * @internal 6624 * @see CONTENT(), numRows() 6625 */ 6626 public function getQuery($table, $conf, $returnQueryArray = false) 6627 { 6628 // Resolve stdWrap in these properties first 6629 $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table); 6630 $properties = [ 6631 'pidInList', 6632 'uidInList', 6633 'languageField', 6634 'selectFields', 6635 'max', 6636 'begin', 6637 'groupBy', 6638 'orderBy', 6639 'join', 6640 'leftjoin', 6641 'rightjoin', 6642 'recursive', 6643 'where' 6644 ]; 6645 foreach ($properties as $property) { 6646 $conf[$property] = trim( 6647 isset($conf[$property . '.']) 6648 ? $this->stdWrap($conf[$property], $conf[$property . '.']) 6649 : $conf[$property] 6650 ); 6651 if ($conf[$property] === '') { 6652 unset($conf[$property]); 6653 } elseif (in_array($property, ['languageField', 'selectFields', 'join', 'leftjoin', 'rightjoin', 'where'], true)) { 6654 $conf[$property] = QueryHelper::quoteDatabaseIdentifiers($connection, $conf[$property]); 6655 } 6656 if (isset($conf[$property . '.'])) { 6657 // stdWrapping already done, so remove the sub-array 6658 unset($conf[$property . '.']); 6659 } 6660 } 6661 // Handle PDO-style named parameter markers first 6662 $queryMarkers = $this->getQueryMarkers($table, $conf); 6663 // Replace the markers in the non-stdWrap properties 6664 foreach ($queryMarkers as $marker => $markerValue) { 6665 $properties = [ 6666 'uidInList', 6667 'selectFields', 6668 'where', 6669 'max', 6670 'begin', 6671 'groupBy', 6672 'orderBy', 6673 'join', 6674 'leftjoin', 6675 'rightjoin' 6676 ]; 6677 foreach ($properties as $property) { 6678 if ($conf[$property]) { 6679 $conf[$property] = str_replace('###' . $marker . '###', $markerValue, $conf[$property]); 6680 } 6681 } 6682 } 6683 6684 // Construct WHERE clause: 6685 // Handle recursive function for the pidInList 6686 if (isset($conf['recursive'])) { 6687 $conf['recursive'] = (int)$conf['recursive']; 6688 if ($conf['recursive'] > 0) { 6689 $pidList = GeneralUtility::trimExplode(',', $conf['pidInList'], true); 6690 array_walk($pidList, function (&$storagePid) { 6691 if ($storagePid === 'this') { 6692 $storagePid = $this->getTypoScriptFrontendController()->id; 6693 } 6694 if ($storagePid > 0) { 6695 $storagePid = -$storagePid; 6696 } 6697 }); 6698 $expandedPidList = []; 6699 foreach ($pidList as $value) { 6700 // Implementation of getTreeList allows to pass the id negative to include 6701 // it into the result otherwise only childpages are returned 6702 $expandedPidList = array_merge( 6703 GeneralUtility::intExplode(',', $this->getTreeList($value, $conf['recursive'])), 6704 $expandedPidList 6705 ); 6706 } 6707 $conf['pidInList'] = implode(',', $expandedPidList); 6708 } 6709 } 6710 if ((string)$conf['pidInList'] === '') { 6711 $conf['pidInList'] = 'this'; 6712 } 6713 6714 $queryParts = $this->getQueryConstraints($table, $conf); 6715 6716 $queryBuilder = $connection->createQueryBuilder(); 6717 // @todo Check against getQueryConstraints, can probably use FrontendRestrictions 6718 // @todo here and remove enableFields there. 6719 $queryBuilder->getRestrictions()->removeAll(); 6720 $queryBuilder->select('*')->from($table); 6721 6722 if ($queryParts['where']) { 6723 $queryBuilder->where($queryParts['where']); 6724 } 6725 6726 if ($queryParts['groupBy']) { 6727 $queryBuilder->groupBy(...$queryParts['groupBy']); 6728 } 6729 6730 if (is_array($queryParts['orderBy'])) { 6731 foreach ($queryParts['orderBy'] as $orderBy) { 6732 $queryBuilder->addOrderBy(...$orderBy); 6733 } 6734 } 6735 6736 // Fields: 6737 if ($conf['selectFields']) { 6738 $queryBuilder->selectLiteral($this->sanitizeSelectPart($conf['selectFields'], $table)); 6739 } 6740 6741 // Setting LIMIT: 6742 $error = false; 6743 if ($conf['max'] || $conf['begin']) { 6744 // Finding the total number of records, if used: 6745 if (strpos(strtolower($conf['begin'] . $conf['max']), 'total') !== false) { 6746 $countQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table); 6747 $countQueryBuilder->getRestrictions()->removeAll(); 6748 $countQueryBuilder->count('*') 6749 ->from($table) 6750 ->where($queryParts['where']); 6751 6752 if ($queryParts['groupBy']) { 6753 $countQueryBuilder->groupBy(...$queryParts['groupBy']); 6754 } 6755 6756 try { 6757 $count = $countQueryBuilder->execute()->fetchColumn(0); 6758 $conf['max'] = str_ireplace('total', $count, $conf['max']); 6759 $conf['begin'] = str_ireplace('total', $count, $conf['begin']); 6760 } catch (DBALException $e) { 6761 $this->getTimeTracker()->setTSlogMessage($e->getPrevious()->getMessage()); 6762 $error = true; 6763 } 6764 } 6765 6766 if (!$error) { 6767 $conf['begin'] = MathUtility::forceIntegerInRange(ceil($this->calc($conf['begin'])), 0); 6768 $conf['max'] = MathUtility::forceIntegerInRange(ceil($this->calc($conf['max'])), 0); 6769 if ($conf['begin'] > 0) { 6770 $queryBuilder->setFirstResult($conf['begin']); 6771 } 6772 $queryBuilder->setMaxResults($conf['max'] ?: 100000); 6773 } 6774 } 6775 6776 if (!$error) { 6777 // Setting up tablejoins: 6778 if ($conf['join']) { 6779 $joinParts = QueryHelper::parseJoin($conf['join']); 6780 $queryBuilder->join( 6781 $table, 6782 $joinParts['tableName'], 6783 $joinParts['tableAlias'], 6784 $joinParts['joinCondition'] 6785 ); 6786 } elseif ($conf['leftjoin']) { 6787 $joinParts = QueryHelper::parseJoin($conf['leftjoin']); 6788 $queryBuilder->leftJoin( 6789 $table, 6790 $joinParts['tableName'], 6791 $joinParts['tableAlias'], 6792 $joinParts['joinCondition'] 6793 ); 6794 } elseif ($conf['rightjoin']) { 6795 $joinParts = QueryHelper::parseJoin($conf['rightjoin']); 6796 $queryBuilder->rightJoin( 6797 $table, 6798 $joinParts['tableName'], 6799 $joinParts['tableAlias'], 6800 $joinParts['joinCondition'] 6801 ); 6802 } 6803 6804 // Convert the QueryBuilder object into a SQL statement. 6805 $query = $queryBuilder->getSQL(); 6806 6807 // Replace the markers in the queryParts to handle stdWrap enabled properties 6808 foreach ($queryMarkers as $marker => $markerValue) { 6809 // @todo Ugly hack that needs to be cleaned up, with the current architecture 6810 // @todo for exec_Query / getQuery it's the best we can do. 6811 $query = str_replace('###' . $marker . '###', $markerValue, $query); 6812 foreach ($queryParts as $queryPartKey => &$queryPartValue) { 6813 $queryPartValue = str_replace('###' . $marker . '###', $markerValue, $queryPartValue); 6814 } 6815 unset($queryPartValue); 6816 } 6817 6818 return $returnQueryArray ? $this->getQueryArray($queryBuilder) : $query; 6819 } 6820 6821 return ''; 6822 } 6823 6824 /** 6825 * Helper to transform a QueryBuilder object into a queryParts array that can be used 6826 * with exec_SELECT_queryArray 6827 * 6828 * @param \TYPO3\CMS\Core\Database\Query\QueryBuilder $queryBuilder 6829 * @return array 6830 * @throws \RuntimeException 6831 */ 6832 protected function getQueryArray(QueryBuilder $queryBuilder) 6833 { 6834 $fromClauses = []; 6835 $knownAliases = []; 6836 $queryParts = []; 6837 6838 // Loop through all FROM clauses 6839 foreach ($queryBuilder->getQueryPart('from') as $from) { 6840 if ($from['alias'] === null) { 6841 $tableSql = $from['table']; 6842 $tableReference = $from['table']; 6843 } else { 6844 $tableSql = $from['table'] . ' ' . $from['alias']; 6845 $tableReference = $from['alias']; 6846 } 6847 6848 $knownAliases[$tableReference] = true; 6849 6850 $fromClauses[$tableReference] = $tableSql . $this->getQueryArrayJoinHelper( 6851 $tableReference, 6852 $queryBuilder->getQueryPart('join'), 6853 $knownAliases 6854 ); 6855 } 6856 6857 $queryParts['SELECT'] = implode(', ', $queryBuilder->getQueryPart('select')); 6858 $queryParts['FROM'] = implode(', ', $fromClauses); 6859 $queryParts['WHERE'] = (string)$queryBuilder->getQueryPart('where') ?: ''; 6860 $queryParts['GROUPBY'] = implode(', ', $queryBuilder->getQueryPart('groupBy')); 6861 $queryParts['ORDERBY'] = implode(', ', $queryBuilder->getQueryPart('orderBy')); 6862 if ($queryBuilder->getFirstResult() > 0) { 6863 $queryParts['LIMIT'] = $queryBuilder->getFirstResult() . ',' . $queryBuilder->getMaxResults(); 6864 } elseif ($queryBuilder->getMaxResults() > 0) { 6865 $queryParts['LIMIT'] = $queryBuilder->getMaxResults(); 6866 } 6867 6868 return $queryParts; 6869 } 6870 6871 /** 6872 * Helper to transform the QueryBuilder join part into a SQL fragment. 6873 * 6874 * @param string $fromAlias 6875 * @param array $joinParts 6876 * @param array $knownAliases 6877 * @return string 6878 * @throws \RuntimeException 6879 */ 6880 protected function getQueryArrayJoinHelper(string $fromAlias, array $joinParts, array &$knownAliases): string 6881 { 6882 $sql = ''; 6883 6884 if (isset($joinParts['join'][$fromAlias])) { 6885 foreach ($joinParts['join'][$fromAlias] as $join) { 6886 if (array_key_exists($join['joinAlias'], $knownAliases)) { 6887 throw new \RuntimeException( 6888 'Non unique join alias: "' . $join['joinAlias'] . '" found.', 6889 1472748872 6890 ); 6891 } 6892 $sql .= ' ' . strtoupper($join['joinType']) 6893 . ' JOIN ' . $join['joinTable'] . ' ' . $join['joinAlias'] 6894 . ' ON ' . ((string)$join['joinCondition']); 6895 $knownAliases[$join['joinAlias']] = true; 6896 } 6897 6898 foreach ($joinParts['join'][$fromAlias] as $join) { 6899 $sql .= $this->getQueryArrayJoinHelper($join['joinAlias'], $joinParts, $knownAliases); 6900 } 6901 } 6902 6903 return $sql; 6904 } 6905 /** 6906 * Helper function for getQuery(), creating the WHERE clause of the SELECT query 6907 * 6908 * @param string $table The table name 6909 * @param array $conf The TypoScript configuration properties 6910 * @return array Associative array containing the prepared data for WHERE, ORDER BY and GROUP BY fragments 6911 * @throws \InvalidArgumentException 6912 * @see getQuery() 6913 */ 6914 protected function getQueryConstraints(string $table, array $conf): array 6915 { 6916 // Init: 6917 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table); 6918 $expressionBuilder = $queryBuilder->expr(); 6919 $tsfe = $this->getTypoScriptFrontendController(); 6920 $constraints = []; 6921 $pid_uid_flag = 0; 6922 $enableFieldsIgnore = []; 6923 $queryParts = [ 6924 'where' => null, 6925 'groupBy' => null, 6926 'orderBy' => null, 6927 ]; 6928 6929 $isInWorkspace = GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('workspace', 'isOffline'); 6930 $considerMovePlaceholders = ( 6931 $isInWorkspace && $table !== 'pages' 6932 && !empty($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) 6933 ); 6934 6935 if (trim($conf['uidInList'])) { 6936 $listArr = GeneralUtility::intExplode(',', str_replace('this', $tsfe->contentPid, $conf['uidInList'])); 6937 6938 // If move placeholder shall be considered, select via t3ver_move_id 6939 if ($considerMovePlaceholders) { 6940 $constraints[] = (string)$expressionBuilder->orX( 6941 $expressionBuilder->in($table . '.uid', $listArr), 6942 $expressionBuilder->andX( 6943 $expressionBuilder->eq( 6944 $table . '.t3ver_state', 6945 (int)(string)VersionState::cast(VersionState::MOVE_PLACEHOLDER) 6946 ), 6947 $expressionBuilder->in($table . '.t3ver_move_id', $listArr) 6948 ) 6949 ); 6950 } else { 6951 $constraints[] = (string)$expressionBuilder->in($table . '.uid', $listArr); 6952 } 6953 $pid_uid_flag++; 6954 } 6955 6956 // Static_* tables are allowed to be fetched from root page 6957 if (strpos($table, 'static_') === 0) { 6958 $pid_uid_flag++; 6959 } 6960 6961 if (trim($conf['pidInList'])) { 6962 $listArr = GeneralUtility::intExplode(',', str_replace('this', $tsfe->contentPid, $conf['pidInList'])); 6963 // Removes all pages which are not visible for the user! 6964 $listArr = $this->checkPidArray($listArr); 6965 if (GeneralUtility::inList($conf['pidInList'], 'root')) { 6966 $listArr[] = 0; 6967 } 6968 if (GeneralUtility::inList($conf['pidInList'], '-1')) { 6969 $listArr[] = -1; 6970 $enableFieldsIgnore['pid'] = true; 6971 } 6972 if (!empty($listArr)) { 6973 $constraints[] = $expressionBuilder->in($table . '.pid', array_map('intval', $listArr)); 6974 $pid_uid_flag++; 6975 } else { 6976 // If not uid and not pid then uid is set to 0 - which results in nothing!! 6977 $pid_uid_flag = 0; 6978 } 6979 } 6980 6981 // If not uid and not pid then uid is set to 0 - which results in nothing!! 6982 if (!$pid_uid_flag) { 6983 $constraints[] = $expressionBuilder->eq($table . '.uid', 0); 6984 } 6985 6986 $where = isset($conf['where.']) ? trim($this->stdWrap($conf['where'], $conf['where.'])) : trim($conf['where']); 6987 if ($where) { 6988 $constraints[] = QueryHelper::stripLogicalOperatorPrefix($where); 6989 } 6990 6991 // Check if the default language should be fetched (= doing overlays), or if only the records of a language should be fetched 6992 // but only do this for TCA tables that have languages enabled 6993 $languageConstraint = $this->getLanguageRestriction($expressionBuilder, $table, $conf, GeneralUtility::makeInstance(Context::class)); 6994 if ($languageConstraint !== null) { 6995 $constraints[] = $languageConstraint; 6996 } 6997 6998 // Enablefields 6999 if ($table === 'pages') { 7000 $constraints[] = QueryHelper::stripLogicalOperatorPrefix($tsfe->sys_page->where_hid_del); 7001 $constraints[] = QueryHelper::stripLogicalOperatorPrefix($tsfe->sys_page->where_groupAccess); 7002 } else { 7003 $constraints[] = QueryHelper::stripLogicalOperatorPrefix($tsfe->sys_page->enableFields($table, -1, $enableFieldsIgnore)); 7004 } 7005 7006 // MAKE WHERE: 7007 if (count($constraints) !== 0) { 7008 $queryParts['where'] = $expressionBuilder->andX(...$constraints); 7009 } 7010 // GROUP BY 7011 if (trim($conf['groupBy'])) { 7012 $groupBy = isset($conf['groupBy.']) 7013 ? trim($this->stdWrap($conf['groupBy'], $conf['groupBy.'])) 7014 : trim($conf['groupBy']); 7015 $queryParts['groupBy'] = QueryHelper::parseGroupBy($groupBy); 7016 } 7017 7018 // ORDER BY 7019 if (trim($conf['orderBy'])) { 7020 $orderByString = isset($conf['orderBy.']) 7021 ? trim($this->stdWrap($conf['orderBy'], $conf['orderBy.'])) 7022 : trim($conf['orderBy']); 7023 7024 $queryParts['orderBy'] = QueryHelper::parseOrderBy($orderByString); 7025 } 7026 7027 // Return result: 7028 return $queryParts; 7029 } 7030 7031 /** 7032 * Adds parts to the WHERE clause that are related to language. 7033 * This only works on TCA tables which have the [ctrl][languageField] field set or if they 7034 * have select.languageField = my_language_field set explicitly. 7035 * 7036 * It is also possible to disable the language restriction for a query by using select.languageField = 0, 7037 * if select.languageField is not explicitly set, the TCA default values are taken. 7038 * 7039 * If the table is "localizeable" (= any of the criteria above is met), then the DB query is restricted: 7040 * 7041 * If the current language aspect has overlays enabled, then the only records with language "0" or "-1" are 7042 * fetched (the overlays are taken care of later-on). 7043 * if the current language has overlays but also records without localization-parent (free mode) available, 7044 * then these are fetched as well. This can explicitly set via select.includeRecordsWithoutDefaultTranslation = 1 7045 * which overrules the overlayType within the language aspect. 7046 * 7047 * If the language aspect has NO overlays enabled, it behaves as in "free mode" (= only fetch the records 7048 * for the current language. 7049 * 7050 * @param ExpressionBuilder $expressionBuilder 7051 * @param string $table 7052 * @param array $conf 7053 * @param Context $context 7054 * @return string|\TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression|null 7055 * @throws \TYPO3\CMS\Core\Context\Exception\AspectNotFoundException 7056 */ 7057 protected function getLanguageRestriction(ExpressionBuilder $expressionBuilder, string $table, array $conf, Context $context) 7058 { 7059 $languageField = ''; 7060 $localizationParentField = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] ?? null; 7061 // Check if the table is translatable, and set the language field by default from the TCA information 7062 if (!empty($conf['languageField']) || !isset($conf['languageField'])) { 7063 if (isset($conf['languageField']) && !empty($GLOBALS['TCA'][$table]['columns'][$conf['languageField']])) { 7064 $languageField = $conf['languageField']; 7065 } elseif (!empty($GLOBALS['TCA'][$table]['ctrl']['languageField']) && !empty($localizationParentField)) { 7066 $languageField = $table . '.' . $GLOBALS['TCA'][$table]['ctrl']['languageField']; 7067 } 7068 } 7069 7070 // No language restriction enabled explicitly or available via TCA 7071 if (empty($languageField)) { 7072 return null; 7073 } 7074 7075 /** @var LanguageAspect $languageAspect */ 7076 $languageAspect = $context->getAspect('language'); 7077 if ($languageAspect->doOverlays() && !empty($localizationParentField)) { 7078 // Sys language content is set to zero/-1 - and it is expected that whatever routine processes the output will 7079 // OVERLAY the records with localized versions! 7080 $languageQuery = $expressionBuilder->in($languageField, [0, -1]); 7081 // Use this option to include records that don't have a default language counterpart ("free mode") 7082 // (originalpointerfield is 0 and the language field contains the requested language) 7083 if (isset($conf['includeRecordsWithoutDefaultTranslation']) || $conf['includeRecordsWithoutDefaultTranslation.']) { 7084 $includeRecordsWithoutDefaultTranslation = isset($conf['includeRecordsWithoutDefaultTranslation.']) ? 7085 $this->stdWrap($conf['includeRecordsWithoutDefaultTranslation'], $conf['includeRecordsWithoutDefaultTranslation.']) : $conf['includeRecordsWithoutDefaultTranslation']; 7086 $includeRecordsWithoutDefaultTranslation = trim($includeRecordsWithoutDefaultTranslation) !== ''; 7087 } else { 7088 // Option was not explicitly set, check what's in for the language overlay type. 7089 $includeRecordsWithoutDefaultTranslation = $languageAspect->getOverlayType() === $languageAspect::OVERLAYS_ON_WITH_FLOATING; 7090 } 7091 if ($includeRecordsWithoutDefaultTranslation) { 7092 $languageQuery = $expressionBuilder->orX( 7093 $languageQuery, 7094 $expressionBuilder->andX( 7095 $expressionBuilder->eq($table . '.' . $localizationParentField, 0), 7096 $expressionBuilder->eq($languageField, $languageAspect->getContentId()) 7097 ) 7098 ); 7099 } 7100 return $languageQuery; 7101 } 7102 // No overlays = only fetch records given for the requested language and "all languages" 7103 return $expressionBuilder->in($languageField, [$languageAspect->getContentId(), -1]); 7104 } 7105 7106 /** 7107 * Helper function for getQuery, sanitizing the select part 7108 * 7109 * This functions checks if the necessary fields are part of the select 7110 * and adds them if necessary. 7111 * 7112 * @param string $selectPart Select part 7113 * @param string $table Table to select from 7114 * @return string Sanitized select part 7115 * @internal 7116 * @see getQuery 7117 */ 7118 protected function sanitizeSelectPart($selectPart, $table) 7119 { 7120 $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table); 7121 7122 // Pattern matching parts 7123 $matchStart = '/(^\\s*|,\\s*|' . $table . '\\.)'; 7124 $matchEnd = '(\\s*,|\\s*$)/'; 7125 $necessaryFields = ['uid', 'pid']; 7126 $wsFields = ['t3ver_state']; 7127 if (isset($GLOBALS['TCA'][$table]) && !preg_match($matchStart . '\\*' . $matchEnd, $selectPart) && !preg_match('/(count|max|min|avg|sum)\\([^\\)]+\\)|distinct/i', $selectPart)) { 7128 foreach ($necessaryFields as $field) { 7129 $match = $matchStart . $field . $matchEnd; 7130 if (!preg_match($match, $selectPart)) { 7131 $selectPart .= ', ' . $connection->quoteIdentifier($table . '.' . $field) . ' AS ' . $connection->quoteIdentifier($field); 7132 } 7133 } 7134 if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) { 7135 foreach ($wsFields as $field) { 7136 $match = $matchStart . $field . $matchEnd; 7137 if (!preg_match($match, $selectPart)) { 7138 $selectPart .= ', ' . $connection->quoteIdentifier($table . '.' . $field) . ' AS ' . $connection->quoteIdentifier($field); 7139 } 7140 } 7141 } 7142 } 7143 return $selectPart; 7144 } 7145 7146 /** 7147 * Removes Page UID numbers from the input array which are not available due to enableFields() or the list of bad doktype numbers ($this->checkPid_badDoktypeList) 7148 * 7149 * @param array $listArr Array of Page UID numbers for select and for which pages with enablefields and bad doktypes should be removed. 7150 * @return array Returns the array of remaining page UID numbers 7151 * @internal 7152 * @see checkPid() 7153 */ 7154 public function checkPidArray($listArr) 7155 { 7156 if (!is_array($listArr) || empty($listArr)) { 7157 return []; 7158 } 7159 $outArr = []; 7160 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages'); 7161 $queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class)); 7162 $queryBuilder->select('uid') 7163 ->from('pages') 7164 ->where( 7165 $queryBuilder->expr()->in( 7166 'uid', 7167 $queryBuilder->createNamedParameter($listArr, Connection::PARAM_INT_ARRAY) 7168 ), 7169 $queryBuilder->expr()->notIn( 7170 'doktype', 7171 $queryBuilder->createNamedParameter( 7172 GeneralUtility::intExplode(',', $this->checkPid_badDoktypeList, true), 7173 Connection::PARAM_INT_ARRAY 7174 ) 7175 ) 7176 ); 7177 try { 7178 $result = $queryBuilder->execute(); 7179 while ($row = $result->fetch()) { 7180 $outArr[] = $row['uid']; 7181 } 7182 } catch (DBALException $e) { 7183 $this->getTimeTracker()->setTSlogMessage($e->getMessage() . ': ' . $queryBuilder->getSQL(), 3); 7184 } 7185 7186 return $outArr; 7187 } 7188 7189 /** 7190 * Checks if a page UID is available due to enableFields() AND the list of bad doktype numbers ($this->checkPid_badDoktypeList) 7191 * 7192 * @param int $uid Page UID to test 7193 * @return bool TRUE if OK 7194 * @internal 7195 * @see checkPidArray() 7196 */ 7197 public function checkPid($uid) 7198 { 7199 $uid = (int)$uid; 7200 if (!isset($this->checkPid_cache[$uid])) { 7201 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages'); 7202 $queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class)); 7203 $count = $queryBuilder->count('*') 7204 ->from('pages') 7205 ->where( 7206 $queryBuilder->expr()->eq( 7207 'uid', 7208 $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT) 7209 ), 7210 $queryBuilder->expr()->notIn( 7211 'doktype', 7212 $queryBuilder->createNamedParameter( 7213 GeneralUtility::intExplode(',', $this->checkPid_badDoktypeList, true), 7214 Connection::PARAM_INT_ARRAY 7215 ) 7216 ) 7217 ) 7218 ->execute() 7219 ->fetchColumn(0); 7220 7221 $this->checkPid_cache[$uid] = (bool)$count; 7222 } 7223 return $this->checkPid_cache[$uid]; 7224 } 7225 7226 /** 7227 * Builds list of marker values for handling PDO-like parameter markers in select parts. 7228 * Marker values support stdWrap functionality thus allowing a way to use stdWrap functionality in various properties of 'select' AND prevents SQL-injection problems by quoting and escaping of numeric values, strings, NULL values and comma separated lists. 7229 * 7230 * @param string $table Table to select records from 7231 * @param array $conf Select part of CONTENT definition 7232 * @return array List of values to replace markers with 7233 * @internal 7234 * @see getQuery() 7235 */ 7236 public function getQueryMarkers($table, $conf) 7237 { 7238 if (!is_array($conf['markers.'])) { 7239 return []; 7240 } 7241 // Parse markers and prepare their values 7242 $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table); 7243 $markerValues = []; 7244 foreach ($conf['markers.'] as $dottedMarker => $dummy) { 7245 $marker = rtrim($dottedMarker, '.'); 7246 if ($dottedMarker != $marker . '.') { 7247 continue; 7248 } 7249 // Parse definition 7250 $tempValue = isset($conf['markers.'][$dottedMarker]) 7251 ? $this->stdWrap($conf['markers.'][$dottedMarker]['value'], $conf['markers.'][$dottedMarker]) 7252 : $conf['markers.'][$dottedMarker]['value']; 7253 // Quote/escape if needed 7254 if (is_numeric($tempValue)) { 7255 if ((int)$tempValue == $tempValue) { 7256 // Handle integer 7257 $markerValues[$marker] = (int)$tempValue; 7258 } else { 7259 // Handle float 7260 $markerValues[$marker] = (float)$tempValue; 7261 } 7262 } elseif ($tempValue === null) { 7263 // It represents NULL 7264 $markerValues[$marker] = 'NULL'; 7265 } elseif (!empty($conf['markers.'][$dottedMarker]['commaSeparatedList'])) { 7266 // See if it is really a comma separated list of values 7267 $explodeValues = GeneralUtility::trimExplode(',', $tempValue); 7268 if (count($explodeValues) > 1) { 7269 // Handle each element of list separately 7270 $tempArray = []; 7271 foreach ($explodeValues as $listValue) { 7272 if (is_numeric($listValue)) { 7273 if ((int)$listValue == $listValue) { 7274 $tempArray[] = (int)$listValue; 7275 } else { 7276 $tempArray[] = (float)$listValue; 7277 } 7278 } else { 7279 // If quoted, remove quotes before 7280 // escaping. 7281 if (preg_match('/^\'([^\']*)\'$/', $listValue, $matches)) { 7282 $listValue = $matches[1]; 7283 } elseif (preg_match('/^\\"([^\\"]*)\\"$/', $listValue, $matches)) { 7284 $listValue = $matches[1]; 7285 } 7286 $tempArray[] = $connection->quote($listValue); 7287 } 7288 } 7289 $markerValues[$marker] = implode(',', $tempArray); 7290 } else { 7291 // Handle remaining values as string 7292 $markerValues[$marker] = $connection->quote($tempValue); 7293 } 7294 } else { 7295 // Handle remaining values as string 7296 $markerValues[$marker] = $connection->quote($tempValue); 7297 } 7298 } 7299 return $markerValues; 7300 } 7301 7302 /*********************************************** 7303 * 7304 * Frontend editing functions 7305 * 7306 ***********************************************/ 7307 /** 7308 * Generates the "edit panels" which can be shown for a page or records on a page when the Admin Panel is enabled for a backend users surfing the frontend. 7309 * With the "edit panel" the user will see buttons with links to editing, moving, hiding, deleting the element 7310 * This function is used for the cObject EDITPANEL and the stdWrap property ".editPanel" 7311 * 7312 * @param string $content A content string containing the content related to the edit panel. For cObject "EDITPANEL" this is empty but not so for the stdWrap property. The edit panel is appended to this string and returned. 7313 * @param array $conf TypoScript configuration properties for the editPanel 7314 * @param string $currentRecord The "table:uid" of the record being shown. If empty string then $this->currentRecord is used. For new records (set by $conf['newRecordFromTable']) it's auto-generated to "[tablename]:NEW 7315 * @param array $dataArray Alternative data array to use. Default is $this->data 7316 * @return string The input content string with the editPanel appended. This function returns only an edit panel appended to the content string if a backend user is logged in (and has the correct permissions). Otherwise the content string is directly returned. 7317 */ 7318 public function editPanel($content, $conf, $currentRecord = '', $dataArray = []) 7319 { 7320 if (!$this->getTypoScriptFrontendController()->isBackendUserLoggedIn()) { 7321 return $content; 7322 } 7323 if (!$this->getTypoScriptFrontendController()->displayEditIcons) { 7324 return $content; 7325 } 7326 7327 if (!$currentRecord) { 7328 $currentRecord = $this->currentRecord; 7329 } 7330 if (empty($dataArray)) { 7331 $dataArray = $this->data; 7332 } 7333 7334 if ($conf['newRecordFromTable']) { 7335 $currentRecord = $conf['newRecordFromTable'] . ':NEW'; 7336 $conf['allow'] = 'new'; 7337 $checkEditAccessInternals = false; 7338 } else { 7339 $checkEditAccessInternals = true; 7340 } 7341 list($table, $uid) = explode(':', $currentRecord); 7342 // Page ID for new records, 0 if not specified 7343 $newRecordPid = (int)$conf['newRecordInPid']; 7344 $newUid = null; 7345 if (!$conf['onlyCurrentPid'] || $dataArray['pid'] == $this->getTypoScriptFrontendController()->id) { 7346 if ($table === 'pages') { 7347 $newUid = $uid; 7348 } else { 7349 if ($conf['newRecordFromTable']) { 7350 $newUid = $this->getTypoScriptFrontendController()->id; 7351 if ($newRecordPid) { 7352 $newUid = $newRecordPid; 7353 } 7354 } else { 7355 $newUid = -1 * $uid; 7356 } 7357 } 7358 } 7359 if ($table && $this->getFrontendBackendUser()->allowedToEdit($table, $dataArray, $conf, $checkEditAccessInternals) && $this->getFrontendBackendUser()->allowedToEditLanguage($table, $dataArray)) { 7360 $editClass = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/classes/class.frontendedit.php']['edit']; 7361 if ($editClass) { 7362 $edit = GeneralUtility::makeInstance($editClass); 7363 $allowedActions = $this->getFrontendBackendUser()->getAllowedEditActions($table, $conf, $dataArray['pid']); 7364 $content = $edit->editPanel($content, $conf, $currentRecord, $dataArray, $table, $allowedActions, $newUid, []); 7365 } 7366 } 7367 return $content; 7368 } 7369 7370 /** 7371 * Adds an edit icon to the content string. The edit icon links to FormEngine with proper parameters for editing the table/fields of the context. 7372 * This implements TYPO3 context sensitive editing facilities. Only backend users will have access (if properly configured as well). 7373 * 7374 * @param string $content The content to which the edit icons should be appended 7375 * @param string $params The parameters defining which table and fields to edit. Syntax is [tablename]:[fieldname],[fieldname],[fieldname],... OR [fieldname],[fieldname],[fieldname],... (basically "[tablename]:" is optional, default table is the one of the "current record" used in the function). The fieldlist is sent as "&columnsOnly=" parameter to FormEngine 7376 * @param array $conf TypoScript properties for configuring the edit icons. 7377 * @param string $currentRecord The "table:uid" of the record being shown. If empty string then $this->currentRecord is used. For new records (set by $conf['newRecordFromTable']) it's auto-generated to "[tablename]:NEW 7378 * @param array $dataArray Alternative data array to use. Default is $this->data 7379 * @param string $addUrlParamStr Additional URL parameters for the link pointing to FormEngine 7380 * @return string The input content string, possibly with edit icons added (not necessarily in the end but just after the last string of normal content. 7381 */ 7382 public function editIcons($content, $params, array $conf = [], $currentRecord = '', $dataArray = [], $addUrlParamStr = '') 7383 { 7384 if (!$this->getTypoScriptFrontendController()->isBackendUserLoggedIn()) { 7385 return $content; 7386 } 7387 if (!$this->getTypoScriptFrontendController()->displayFieldEditIcons) { 7388 return $content; 7389 } 7390 if (!$currentRecord) { 7391 $currentRecord = $this->currentRecord; 7392 } 7393 if (empty($dataArray)) { 7394 $dataArray = $this->data; 7395 } 7396 // Check incoming params: 7397 list($currentRecordTable, $currentRecordUID) = explode(':', $currentRecord); 7398 list($fieldList, $table) = array_reverse(GeneralUtility::trimExplode(':', $params, true)); 7399 // Reverse the array because table is optional 7400 if (!$table) { 7401 $table = $currentRecordTable; 7402 } elseif ($table != $currentRecordTable) { 7403 // If the table is set as the first parameter, and does not match the table of the current record, then just return. 7404 return $content; 7405 } 7406 7407 $editUid = $dataArray['_LOCALIZED_UID'] ?: $currentRecordUID; 7408 // Edit icons imply that the editing action is generally allowed, assuming page and content element permissions permit it. 7409 if (!array_key_exists('allow', $conf)) { 7410 $conf['allow'] = 'edit'; 7411 } 7412 if ($table && $this->getFrontendBackendUser()->allowedToEdit($table, $dataArray, $conf, true) && $fieldList && $this->getFrontendBackendUser()->allowedToEditLanguage($table, $dataArray)) { 7413 $editClass = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/classes/class.frontendedit.php']['edit']; 7414 if ($editClass) { 7415 $edit = GeneralUtility::makeInstance($editClass); 7416 $content = $edit->editIcons($content, $params, $conf, $currentRecord, $dataArray, $addUrlParamStr, $table, $editUid, $fieldList); 7417 } 7418 } 7419 return $content; 7420 } 7421 7422 /** 7423 * Returns TRUE if the input table/row would be hidden in the frontend (according nto the current time and simulate user group) 7424 * 7425 * @param string $table The table name 7426 * @param array $row The data record 7427 * @return bool 7428 * @internal 7429 * @see editPanelPreviewBorder() 7430 */ 7431 public function isDisabled($table, $row) 7432 { 7433 $tsfe = $this->getTypoScriptFrontendController(); 7434 $enablecolumns = $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']; 7435 return $enablecolumns['disabled'] && $row[$enablecolumns['disabled']] 7436 || $enablecolumns['fe_group'] && $tsfe->simUserGroup && (int)$row[$enablecolumns['fe_group']] === (int)$tsfe->simUserGroup 7437 || $enablecolumns['starttime'] && $row[$enablecolumns['starttime']] > $GLOBALS['EXEC_TIME'] 7438 || $enablecolumns['endtime'] && $row[$enablecolumns['endtime']] && $row[$enablecolumns['endtime']] < $GLOBALS['EXEC_TIME']; 7439 } 7440 7441 /** 7442 * Get instance of FAL resource factory 7443 * 7444 * @return ResourceFactory 7445 */ 7446 protected function getResourceFactory() 7447 { 7448 return ResourceFactory::getInstance(); 7449 } 7450 7451 /** 7452 * Wrapper function for GeneralUtility::getIndpEnv() 7453 * 7454 * @see GeneralUtility::getIndpEnv 7455 * @param string $key Name of the "environment variable"/"server variable" you wish to get. 7456 * @return string 7457 */ 7458 protected function getEnvironmentVariable($key) 7459 { 7460 return GeneralUtility::getIndpEnv($key); 7461 } 7462 7463 /** 7464 * Fetches content from cache 7465 * 7466 * @param array $configuration Array 7467 * @return string|bool FALSE on cache miss 7468 * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException 7469 */ 7470 protected function getFromCache(array $configuration) 7471 { 7472 $content = false; 7473 7474 if ($this->getTypoScriptFrontendController()->no_cache) { 7475 return $content; 7476 } 7477 $cacheKey = $this->calculateCacheKey($configuration); 7478 if (!empty($cacheKey)) { 7479 /** @var \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface $cacheFrontend */ 7480 $cacheFrontend = GeneralUtility::makeInstance(CacheManager::class) 7481 ->getCache('cache_hash'); 7482 $content = $cacheFrontend->get($cacheKey); 7483 } 7484 return $content; 7485 } 7486 7487 /** 7488 * Calculates the lifetime of a cache entry based on the given configuration 7489 * 7490 * @param array $configuration 7491 * @return int|null 7492 */ 7493 protected function calculateCacheLifetime(array $configuration) 7494 { 7495 $lifetimeConfiguration = $configuration['lifetime'] ?? ''; 7496 $lifetimeConfiguration = isset($configuration['lifetime.']) 7497 ? $this->stdWrap($lifetimeConfiguration, $configuration['lifetime.']) 7498 : $lifetimeConfiguration; 7499 7500 $lifetime = null; // default lifetime 7501 if (strtolower($lifetimeConfiguration) === 'unlimited') { 7502 $lifetime = 0; // unlimited 7503 } elseif ($lifetimeConfiguration > 0) { 7504 $lifetime = (int)$lifetimeConfiguration; // lifetime in seconds 7505 } 7506 return $lifetime; 7507 } 7508 7509 /** 7510 * Calculates the tags for a cache entry bases on the given configuration 7511 * 7512 * @param array $configuration 7513 * @return array 7514 */ 7515 protected function calculateCacheTags(array $configuration) 7516 { 7517 $tags = $configuration['tags'] ?? ''; 7518 $tags = isset($configuration['tags.']) 7519 ? $this->stdWrap($tags, $configuration['tags.']) 7520 : $tags; 7521 return empty($tags) ? [] : GeneralUtility::trimExplode(',', $tags); 7522 } 7523 7524 /** 7525 * Applies stdWrap to the cache key 7526 * 7527 * @param array $configuration 7528 * @return string 7529 */ 7530 protected function calculateCacheKey(array $configuration) 7531 { 7532 $key = $configuration['key'] ?? ''; 7533 return isset($configuration['key.']) 7534 ? $this->stdWrap($key, $configuration['key.']) 7535 : $key; 7536 } 7537 7538 /** 7539 * Returns the current BE user. 7540 * 7541 * @return \TYPO3\CMS\Backend\FrontendBackendUserAuthentication 7542 */ 7543 protected function getFrontendBackendUser() 7544 { 7545 return $GLOBALS['BE_USER']; 7546 } 7547 7548 /** 7549 * @return TimeTracker 7550 */ 7551 protected function getTimeTracker() 7552 { 7553 return GeneralUtility::makeInstance(TimeTracker::class); 7554 } 7555 7556 /** 7557 * @return \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController 7558 */ 7559 protected function getTypoScriptFrontendController() 7560 { 7561 return $this->typoScriptFrontendController ?: $GLOBALS['TSFE']; 7562 } 7563 7564 /** 7565 * Support anchors without href value 7566 * Changes ContentObjectRenderer::typolink to render a tag without href, 7567 * if id or name attribute is present. 7568 * 7569 * @param string $linkText 7570 * @param array $conf Typolink configuration decoded as array 7571 * @return string Full a-Tag or just the linktext if id or name are not set. 7572 */ 7573 protected function resolveAnchorLink(string $linkText, array $conf): string 7574 { 7575 $anchorTag = '<a ' . $this->getATagParams($conf) . '>'; 7576 $aTagParams = GeneralUtility::get_tag_attributes($anchorTag); 7577 // If it looks like a anchor tag, render it anyway 7578 if (isset($aTagParams['id']) || isset($aTagParams['name'])) { 7579 return $anchorTag . $linkText . '</a>'; 7580 } 7581 // Otherwise just return the link text 7582 return $linkText; 7583 } 7584 7585 protected function shallDebug(): bool 7586 { 7587 $tsfe = $this->getTypoScriptFrontendController(); 7588 if ($tsfe !== null && isset($tsfe->config['config']['debug'])) { 7589 return (bool)($tsfe->config['config']['debug']); 7590 } 7591 return !empty($GLOBALS['TYPO3_CONF_VARS']['FE']['debug']); 7592 } 7593} 7594