[BUGFIX] Ensure manually updated slug is saved correctly
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Utility / GeneralUtility.php
1 <?php
2 namespace TYPO3\CMS\Core\Utility;
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
17 use GuzzleHttp\Exception\RequestException;
18 use Psr\Container\ContainerInterface;
19 use Psr\Log\LoggerAwareInterface;
20 use Psr\Log\LoggerInterface;
21 use TYPO3\CMS\Core\Cache\CacheManager;
22 use TYPO3\CMS\Core\Core\ApplicationContext;
23 use TYPO3\CMS\Core\Core\Bootstrap;
24 use TYPO3\CMS\Core\Core\ClassLoadingInformation;
25 use TYPO3\CMS\Core\Core\Environment;
26 use TYPO3\CMS\Core\Http\RequestFactory;
27 use TYPO3\CMS\Core\Log\LogManager;
28 use TYPO3\CMS\Core\Service\OpcodeCacheService;
29 use TYPO3\CMS\Core\SingletonInterface;
30
31 /**
32 * The legendary "t3lib_div" class - Miscellaneous functions for general purpose.
33 * Most of the functions do not relate specifically to TYPO3
34 * However a section of functions requires certain TYPO3 features available
35 * See comments in the source.
36 * You are encouraged to use this library in your own scripts!
37 *
38 * USE:
39 * The class is intended to be used without creating an instance of it.
40 * So: Don't instantiate - call functions with "\TYPO3\CMS\Core\Utility\GeneralUtility::" prefixed the function name.
41 * So use \TYPO3\CMS\Core\Utility\GeneralUtility::[method-name] to refer to the functions, eg. '\TYPO3\CMS\Core\Utility\GeneralUtility::milliseconds()'
42 */
43 class GeneralUtility
44 {
45 const ENV_TRUSTED_HOSTS_PATTERN_ALLOW_ALL = '.*';
46 const ENV_TRUSTED_HOSTS_PATTERN_SERVER_NAME = 'SERVER_NAME';
47
48 /**
49 * State of host header value security check
50 * in order to avoid unnecessary multiple checks during one request
51 *
52 * @var bool
53 */
54 protected static $allowHostHeaderValue = false;
55
56 /**
57 * @var ContainerInterface|null
58 */
59 protected static $container;
60
61 /**
62 * Singleton instances returned by makeInstance, using the class names as
63 * array keys
64 *
65 * @var array<\TYPO3\CMS\Core\SingletonInterface>
66 */
67 protected static $singletonInstances = [];
68
69 /**
70 * Instances returned by makeInstance, using the class names as array keys
71 *
72 * @var array<array><object>
73 */
74 protected static $nonSingletonInstances = [];
75
76 /**
77 * Cache for makeInstance with given class name and final class names to reduce number of self::getClassName() calls
78 *
79 * @var array Given class name => final class name
80 */
81 protected static $finalClassNameCache = [];
82
83 /**
84 * The application context
85 *
86 * @var \TYPO3\CMS\Core\Core\ApplicationContext
87 */
88 protected static $applicationContext;
89
90 /**
91 * A list of supported CGI server APIs
92 * NOTICE: This is a duplicate of the SAME array in SystemEnvironmentBuilder
93 * @var array
94 */
95 protected static $supportedCgiServerApis = [
96 'fpm-fcgi',
97 'cgi',
98 'isapi',
99 'cgi-fcgi',
100 'srv', // HHVM with fastcgi
101 ];
102
103 /**
104 * @var array
105 */
106 protected static $indpEnvCache = [];
107
108 /*************************
109 *
110 * GET/POST Variables
111 *
112 * Background:
113 * Input GET/POST variables in PHP may have their quotes escaped with "\" or not depending on configuration.
114 * TYPO3 has always converted quotes to BE escaped if the configuration told that they would not be so.
115 * But the clean solution is that quotes are never escaped and that is what the functions below offers.
116 * Eventually TYPO3 should provide this in the global space as well.
117 * In the transitional phase (or forever..?) we need to encourage EVERY to read and write GET/POST vars through the API functions below.
118 * This functionality was previously needed to normalize between magic quotes logic, which was removed from PHP 5.4,
119 * so these methods are still in use, but not tackle the slash problem anymore.
120 *
121 *************************/
122 /**
123 * Returns the 'GLOBAL' value of incoming data from POST or GET, with priority to POST (that is equalent to 'GP' order)
124 * To enhance security in your scripts, please consider using GeneralUtility::_GET or GeneralUtility::_POST if you already
125 * know by which method your data is arriving to the scripts!
126 *
127 * @param string $var GET/POST var to return
128 * @return mixed POST var named $var and if not set, the GET var of the same name.
129 */
130 public static function _GP($var)
131 {
132 if (empty($var)) {
133 return;
134 }
135 if (isset($_POST[$var])) {
136 $value = $_POST[$var];
137 } elseif (isset($_GET[$var])) {
138 $value = $_GET[$var];
139 } else {
140 $value = null;
141 }
142 // This is there for backwards-compatibility, in order to avoid NULL
143 if (isset($value) && !is_array($value)) {
144 $value = (string)$value;
145 }
146 return $value;
147 }
148
149 /**
150 * Returns the global arrays $_GET and $_POST merged with $_POST taking precedence.
151 *
152 * @param string $parameter Key (variable name) from GET or POST vars
153 * @return array Returns the GET vars merged recursively onto the POST vars.
154 */
155 public static function _GPmerged($parameter)
156 {
157 $postParameter = isset($_POST[$parameter]) && is_array($_POST[$parameter]) ? $_POST[$parameter] : [];
158 $getParameter = isset($_GET[$parameter]) && is_array($_GET[$parameter]) ? $_GET[$parameter] : [];
159 $mergedParameters = $getParameter;
160 ArrayUtility::mergeRecursiveWithOverrule($mergedParameters, $postParameter);
161 return $mergedParameters;
162 }
163
164 /**
165 * Returns the global $_GET array (or value from) normalized to contain un-escaped values.
166 * ALWAYS use this API function to acquire the GET variables!
167 * This function was previously used to normalize between magic quotes logic, which was removed from PHP 5.5
168 *
169 * @param string $var Optional pointer to value in GET array (basically name of GET var)
170 * @return mixed If $var is set it returns the value of $_GET[$var]. If $var is NULL (default), returns $_GET itself. In any case *slashes are stipped from the output!*
171 * @see _POST(), _GP()
172 */
173 public static function _GET($var = null)
174 {
175 $value = $var === null
176 ? $_GET
177 : (empty($var) ? null : ($_GET[$var] ?? null));
178 // This is there for backwards-compatibility, in order to avoid NULL
179 if (isset($value) && !is_array($value)) {
180 $value = (string)$value;
181 }
182 return $value;
183 }
184
185 /**
186 * Returns the global $_POST array (or value from) normalized to contain un-escaped values.
187 * ALWAYS use this API function to acquire the $_POST variables!
188 *
189 * @param string $var Optional pointer to value in POST array (basically name of POST var)
190 * @return mixed If $var is set it returns the value of $_POST[$var]. If $var is NULL (default), returns $_POST itself. In any case *slashes are stipped from the output!*
191 * @see _GET(), _GP()
192 */
193 public static function _POST($var = null)
194 {
195 $value = $var === null ? $_POST : (empty($var) || !isset($_POST[$var]) ? null : $_POST[$var]);
196 // This is there for backwards-compatibility, in order to avoid NULL
197 if (isset($value) && !is_array($value)) {
198 $value = (string)$value;
199 }
200 return $value;
201 }
202
203 /*************************
204 *
205 * STRING FUNCTIONS
206 *
207 *************************/
208 /**
209 * Truncates a string with appended/prepended "..." and takes current character set into consideration.
210 *
211 * @param string $string String to truncate
212 * @param int $chars Must be an integer with an absolute value of at least 4. if negative the string is cropped from the right end.
213 * @param string $appendString Appendix to the truncated string
214 * @return string Cropped string
215 */
216 public static function fixed_lgd_cs($string, $chars, $appendString = '...')
217 {
218 if ((int)$chars === 0 || mb_strlen($string, 'utf-8') <= abs($chars)) {
219 return $string;
220 }
221 if ($chars > 0) {
222 $string = mb_substr($string, 0, $chars, 'utf-8') . $appendString;
223 } else {
224 $string = $appendString . mb_substr($string, $chars, mb_strlen($string, 'utf-8'), 'utf-8');
225 }
226 return $string;
227 }
228
229 /**
230 * Match IP number with list of numbers with wildcard
231 * Dispatcher method for switching into specialised IPv4 and IPv6 methods.
232 *
233 * @param string $baseIP Is the current remote IP address for instance, typ. REMOTE_ADDR
234 * @param string $list Is a comma-list of IP-addresses to match with. *-wildcard allowed instead of number, plus leaving out parts in the IP number is accepted as wildcard (eg. 192.168.*.* equals 192.168). If list is "*" no check is done and the function returns TRUE immediately. An empty list always returns FALSE.
235 * @return bool TRUE if an IP-mask from $list matches $baseIP
236 */
237 public static function cmpIP($baseIP, $list)
238 {
239 $list = trim($list);
240 if ($list === '') {
241 return false;
242 }
243 if ($list === '*') {
244 return true;
245 }
246 if (strpos($baseIP, ':') !== false && self::validIPv6($baseIP)) {
247 return self::cmpIPv6($baseIP, $list);
248 }
249 return self::cmpIPv4($baseIP, $list);
250 }
251
252 /**
253 * Match IPv4 number with list of numbers with wildcard
254 *
255 * @param string $baseIP Is the current remote IP address for instance, typ. REMOTE_ADDR
256 * @param string $list Is a comma-list of IP-addresses to match with. *-wildcard allowed instead of number, plus leaving out parts in the IP number is accepted as wildcard (eg. 192.168.*.* equals 192.168), could also contain IPv6 addresses
257 * @return bool TRUE if an IP-mask from $list matches $baseIP
258 */
259 public static function cmpIPv4($baseIP, $list)
260 {
261 $IPpartsReq = explode('.', $baseIP);
262 if (count($IPpartsReq) === 4) {
263 $values = self::trimExplode(',', $list, true);
264 foreach ($values as $test) {
265 $testList = explode('/', $test);
266 if (count($testList) === 2) {
267 list($test, $mask) = $testList;
268 } else {
269 $mask = false;
270 }
271 if ((int)$mask) {
272 // "192.168.3.0/24"
273 $lnet = ip2long($test);
274 $lip = ip2long($baseIP);
275 $binnet = str_pad(decbin($lnet), 32, '0', STR_PAD_LEFT);
276 $firstpart = substr($binnet, 0, $mask);
277 $binip = str_pad(decbin($lip), 32, '0', STR_PAD_LEFT);
278 $firstip = substr($binip, 0, $mask);
279 $yes = $firstpart === $firstip;
280 } else {
281 // "192.168.*.*"
282 $IPparts = explode('.', $test);
283 $yes = 1;
284 foreach ($IPparts as $index => $val) {
285 $val = trim($val);
286 if ($val !== '*' && $IPpartsReq[$index] !== $val) {
287 $yes = 0;
288 }
289 }
290 }
291 if ($yes) {
292 return true;
293 }
294 }
295 }
296 return false;
297 }
298
299 /**
300 * Match IPv6 address with a list of IPv6 prefixes
301 *
302 * @param string $baseIP Is the current remote IP address for instance
303 * @param string $list Is a comma-list of IPv6 prefixes, could also contain IPv4 addresses
304 * @return bool TRUE If an baseIP matches any prefix
305 */
306 public static function cmpIPv6($baseIP, $list)
307 {
308 // Policy default: Deny connection
309 $success = false;
310 $baseIP = self::normalizeIPv6($baseIP);
311 $values = self::trimExplode(',', $list, true);
312 foreach ($values as $test) {
313 $testList = explode('/', $test);
314 if (count($testList) === 2) {
315 list($test, $mask) = $testList;
316 } else {
317 $mask = false;
318 }
319 if (self::validIPv6($test)) {
320 $test = self::normalizeIPv6($test);
321 $maskInt = (int)$mask ?: 128;
322 // Special case; /0 is an allowed mask - equals a wildcard
323 if ($mask === '0') {
324 $success = true;
325 } elseif ($maskInt == 128) {
326 $success = $test === $baseIP;
327 } else {
328 $testBin = self::IPv6Hex2Bin($test);
329 $baseIPBin = self::IPv6Hex2Bin($baseIP);
330 $success = true;
331 // Modulo is 0 if this is a 8-bit-boundary
332 $maskIntModulo = $maskInt % 8;
333 $numFullCharactersUntilBoundary = (int)($maskInt / 8);
334 if (strpos($testBin, substr($baseIPBin, 0, $numFullCharactersUntilBoundary)) !== 0) {
335 $success = false;
336 } elseif ($maskIntModulo > 0) {
337 // If not an 8-bit-boundary, check bits of last character
338 $testLastBits = str_pad(decbin(ord(substr($testBin, $numFullCharactersUntilBoundary, 1))), 8, '0', STR_PAD_LEFT);
339 $baseIPLastBits = str_pad(decbin(ord(substr($baseIPBin, $numFullCharactersUntilBoundary, 1))), 8, '0', STR_PAD_LEFT);
340 if (strncmp($testLastBits, $baseIPLastBits, $maskIntModulo) != 0) {
341 $success = false;
342 }
343 }
344 }
345 }
346 if ($success) {
347 return true;
348 }
349 }
350 return false;
351 }
352
353 /**
354 * Transform a regular IPv6 address from hex-representation into binary
355 *
356 * @param string $hex IPv6 address in hex-presentation
357 * @return string Binary representation (16 characters, 128 characters)
358 * @see IPv6Bin2Hex()
359 */
360 public static function IPv6Hex2Bin($hex)
361 {
362 return inet_pton($hex);
363 }
364
365 /**
366 * Transform an IPv6 address from binary to hex-representation
367 *
368 * @param string $bin IPv6 address in hex-presentation
369 * @return string Binary representation (16 characters, 128 characters)
370 * @see IPv6Hex2Bin()
371 */
372 public static function IPv6Bin2Hex($bin)
373 {
374 return inet_ntop($bin);
375 }
376
377 /**
378 * Normalize an IPv6 address to full length
379 *
380 * @param string $address Given IPv6 address
381 * @return string Normalized address
382 * @see compressIPv6()
383 */
384 public static function normalizeIPv6($address)
385 {
386 $normalizedAddress = '';
387 // According to RFC lowercase-representation is recommended
388 $address = strtolower($address);
389 // Normalized representation has 39 characters (0000:0000:0000:0000:0000:0000:0000:0000)
390 if (strlen($address) === 39) {
391 // Already in full expanded form
392 return $address;
393 }
394 // Count 2 if if address has hidden zero blocks
395 $chunks = explode('::', $address);
396 if (count($chunks) === 2) {
397 $chunksLeft = explode(':', $chunks[0]);
398 $chunksRight = explode(':', $chunks[1]);
399 $left = count($chunksLeft);
400 $right = count($chunksRight);
401 // Special case: leading zero-only blocks count to 1, should be 0
402 if ($left === 1 && strlen($chunksLeft[0]) === 0) {
403 $left = 0;
404 }
405 $hiddenBlocks = 8 - ($left + $right);
406 $hiddenPart = '';
407 $h = 0;
408 while ($h < $hiddenBlocks) {
409 $hiddenPart .= '0000:';
410 $h++;
411 }
412 if ($left === 0) {
413 $stageOneAddress = $hiddenPart . $chunks[1];
414 } else {
415 $stageOneAddress = $chunks[0] . ':' . $hiddenPart . $chunks[1];
416 }
417 } else {
418 $stageOneAddress = $address;
419 }
420 // Normalize the blocks:
421 $blocks = explode(':', $stageOneAddress);
422 $divCounter = 0;
423 foreach ($blocks as $block) {
424 $tmpBlock = '';
425 $i = 0;
426 $hiddenZeros = 4 - strlen($block);
427 while ($i < $hiddenZeros) {
428 $tmpBlock .= '0';
429 $i++;
430 }
431 $normalizedAddress .= $tmpBlock . $block;
432 if ($divCounter < 7) {
433 $normalizedAddress .= ':';
434 $divCounter++;
435 }
436 }
437 return $normalizedAddress;
438 }
439
440 /**
441 * Compress an IPv6 address to the shortest notation
442 *
443 * @param string $address Given IPv6 address
444 * @return string Compressed address
445 * @see normalizeIPv6()
446 */
447 public static function compressIPv6($address)
448 {
449 return inet_ntop(inet_pton($address));
450 }
451
452 /**
453 * Validate a given IP address.
454 *
455 * Possible format are IPv4 and IPv6.
456 *
457 * @param string $ip IP address to be tested
458 * @return bool TRUE if $ip is either of IPv4 or IPv6 format.
459 */
460 public static function validIP($ip)
461 {
462 return filter_var($ip, FILTER_VALIDATE_IP) !== false;
463 }
464
465 /**
466 * Validate a given IP address to the IPv4 address format.
467 *
468 * Example for possible format: 10.0.45.99
469 *
470 * @param string $ip IP address to be tested
471 * @return bool TRUE if $ip is of IPv4 format.
472 */
473 public static function validIPv4($ip)
474 {
475 return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false;
476 }
477
478 /**
479 * Validate a given IP address to the IPv6 address format.
480 *
481 * Example for possible format: 43FB::BB3F:A0A0:0 | ::1
482 *
483 * @param string $ip IP address to be tested
484 * @return bool TRUE if $ip is of IPv6 format.
485 */
486 public static function validIPv6($ip)
487 {
488 return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false;
489 }
490
491 /**
492 * Match fully qualified domain name with list of strings with wildcard
493 *
494 * @param string $baseHost A hostname or an IPv4/IPv6-address (will by reverse-resolved; typically REMOTE_ADDR)
495 * @param string $list A comma-list of domain names to match with. *-wildcard allowed but cannot be part of a string, so it must match the full host name (eg. myhost.*.com => correct, myhost.*domain.com => wrong)
496 * @return bool TRUE if a domain name mask from $list matches $baseIP
497 */
498 public static function cmpFQDN($baseHost, $list)
499 {
500 $baseHost = trim($baseHost);
501 if (empty($baseHost)) {
502 return false;
503 }
504 if (self::validIPv4($baseHost) || self::validIPv6($baseHost)) {
505 // Resolve hostname
506 // Note: this is reverse-lookup and can be randomly set as soon as somebody is able to set
507 // the reverse-DNS for his IP (security when for example used with REMOTE_ADDR)
508 $baseHostName = gethostbyaddr($baseHost);
509 if ($baseHostName === $baseHost) {
510 // Unable to resolve hostname
511 return false;
512 }
513 } else {
514 $baseHostName = $baseHost;
515 }
516 $baseHostNameParts = explode('.', $baseHostName);
517 $values = self::trimExplode(',', $list, true);
518 foreach ($values as $test) {
519 $hostNameParts = explode('.', $test);
520 // To match hostNameParts can only be shorter (in case of wildcards) or equal
521 $hostNamePartsCount = count($hostNameParts);
522 $baseHostNamePartsCount = count($baseHostNameParts);
523 if ($hostNamePartsCount > $baseHostNamePartsCount) {
524 continue;
525 }
526 $yes = true;
527 foreach ($hostNameParts as $index => $val) {
528 $val = trim($val);
529 if ($val === '*') {
530 // Wildcard valid for one or more hostname-parts
531 $wildcardStart = $index + 1;
532 // Wildcard as last/only part always matches, otherwise perform recursive checks
533 if ($wildcardStart < $hostNamePartsCount) {
534 $wildcardMatched = false;
535 $tempHostName = implode('.', array_slice($hostNameParts, $index + 1));
536 while ($wildcardStart < $baseHostNamePartsCount && !$wildcardMatched) {
537 $tempBaseHostName = implode('.', array_slice($baseHostNameParts, $wildcardStart));
538 $wildcardMatched = self::cmpFQDN($tempBaseHostName, $tempHostName);
539 $wildcardStart++;
540 }
541 if ($wildcardMatched) {
542 // Match found by recursive compare
543 return true;
544 }
545 $yes = false;
546 }
547 } elseif ($baseHostNameParts[$index] !== $val) {
548 // In case of no match
549 $yes = false;
550 }
551 }
552 if ($yes) {
553 return true;
554 }
555 }
556 return false;
557 }
558
559 /**
560 * Checks if a given URL matches the host that currently handles this HTTP request.
561 * Scheme, hostname and (optional) port of the given URL are compared.
562 *
563 * @param string $url URL to compare with the TYPO3 request host
564 * @return bool Whether the URL matches the TYPO3 request host
565 */
566 public static function isOnCurrentHost($url)
567 {
568 return stripos($url . '/', self::getIndpEnv('TYPO3_REQUEST_HOST') . '/') === 0;
569 }
570
571 /**
572 * Check for item in list
573 * Check if an item exists in a comma-separated list of items.
574 *
575 * @param string $list Comma-separated list of items (string)
576 * @param string $item Item to check for
577 * @return bool TRUE if $item is in $list
578 */
579 public static function inList($list, $item)
580 {
581 return strpos(',' . $list . ',', ',' . $item . ',') !== false;
582 }
583
584 /**
585 * Removes an item from a comma-separated list of items.
586 *
587 * If $element contains a comma, the behaviour of this method is undefined.
588 * Empty elements in the list are preserved.
589 *
590 * @param string $element Element to remove
591 * @param string $list Comma-separated list of items (string)
592 * @return string New comma-separated list of items
593 */
594 public static function rmFromList($element, $list)
595 {
596 $items = explode(',', $list);
597 foreach ($items as $k => $v) {
598 if ($v == $element) {
599 unset($items[$k]);
600 }
601 }
602 return implode(',', $items);
603 }
604
605 /**
606 * Expand a comma-separated list of integers with ranges (eg 1,3-5,7 becomes 1,3,4,5,7).
607 * Ranges are limited to 1000 values per range.
608 *
609 * @param string $list Comma-separated list of integers with ranges (string)
610 * @return string New comma-separated list of items
611 */
612 public static function expandList($list)
613 {
614 $items = explode(',', $list);
615 $list = [];
616 foreach ($items as $item) {
617 $range = explode('-', $item);
618 if (isset($range[1])) {
619 $runAwayBrake = 1000;
620 for ($n = $range[0]; $n <= $range[1]; $n++) {
621 $list[] = $n;
622 $runAwayBrake--;
623 if ($runAwayBrake <= 0) {
624 break;
625 }
626 }
627 } else {
628 $list[] = $item;
629 }
630 }
631 return implode(',', $list);
632 }
633
634 /**
635 * Makes a positive integer hash out of the first 7 chars from the md5 hash of the input
636 *
637 * @param string $str String to md5-hash
638 * @return int Returns 28bit integer-hash
639 */
640 public static function md5int($str)
641 {
642 return hexdec(substr(md5($str), 0, 7));
643 }
644
645 /**
646 * Returns the first 10 positions of the MD5-hash (changed from 6 to 10 recently)
647 *
648 * @param string $input Input string to be md5-hashed
649 * @param int $len The string-length of the output
650 * @return string Substring of the resulting md5-hash, being $len chars long (from beginning)
651 */
652 public static function shortMD5($input, $len = 10)
653 {
654 return substr(md5($input), 0, $len);
655 }
656
657 /**
658 * Returns a proper HMAC on a given input string and secret TYPO3 encryption key.
659 *
660 * @param string $input Input string to create HMAC from
661 * @param string $additionalSecret additionalSecret to prevent hmac being used in a different context
662 * @return string resulting (hexadecimal) HMAC currently with a length of 40 (HMAC-SHA-1)
663 */
664 public static function hmac($input, $additionalSecret = '')
665 {
666 $hashAlgorithm = 'sha1';
667 $hashBlocksize = 64;
668 $secret = $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'] . $additionalSecret;
669 if (extension_loaded('hash') && function_exists('hash_hmac') && function_exists('hash_algos') && in_array($hashAlgorithm, hash_algos())) {
670 $hmac = hash_hmac($hashAlgorithm, $input, $secret);
671 } else {
672 // Outer padding
673 $opad = str_repeat(chr(92), $hashBlocksize);
674 // Inner padding
675 $ipad = str_repeat(chr(54), $hashBlocksize);
676 if (strlen($secret) > $hashBlocksize) {
677 // Keys longer than block size are shorten
678 $key = str_pad(pack('H*', call_user_func($hashAlgorithm, $secret)), $hashBlocksize, "\0");
679 } else {
680 // Keys shorter than block size are zero-padded
681 $key = str_pad($secret, $hashBlocksize, "\0");
682 }
683 $hmac = call_user_func($hashAlgorithm, ($key ^ $opad) . pack('H*', call_user_func(
684 $hashAlgorithm,
685 ($key ^ $ipad) . $input
686 )));
687 }
688 return $hmac;
689 }
690
691 /**
692 * Takes comma-separated lists and arrays and removes all duplicates
693 * If a value in the list is trim(empty), the value is ignored.
694 *
695 * @param string $in_list Accept multiple parameters which can be comma-separated lists of values and arrays.
696 * @param mixed $secondParameter Dummy field, which if set will show a warning!
697 * @return string Returns the list without any duplicates of values, space around values are trimmed
698 */
699 public static function uniqueList($in_list, $secondParameter = null)
700 {
701 if (is_array($in_list)) {
702 throw new \InvalidArgumentException('TYPO3 Fatal Error: TYPO3\\CMS\\Core\\Utility\\GeneralUtility::uniqueList() does NOT support array arguments anymore! Only string comma lists!', 1270853885);
703 }
704 if (isset($secondParameter)) {
705 throw new \InvalidArgumentException('TYPO3 Fatal Error: TYPO3\\CMS\\Core\\Utility\\GeneralUtility::uniqueList() does NOT support more than a single argument value anymore. You have specified more than one!', 1270853886);
706 }
707 return implode(',', array_unique(self::trimExplode(',', $in_list, true)));
708 }
709
710 /**
711 * Splits a reference to a file in 5 parts
712 *
713 * @param string $fileNameWithPath File name with path to be analyzed (must exist if open_basedir is set)
714 * @return array Contains keys [path], [file], [filebody], [fileext], [realFileext]
715 */
716 public static function split_fileref($fileNameWithPath)
717 {
718 $reg = [];
719 if (preg_match('/(.*\\/)(.*)$/', $fileNameWithPath, $reg)) {
720 $info['path'] = $reg[1];
721 $info['file'] = $reg[2];
722 } else {
723 $info['path'] = '';
724 $info['file'] = $fileNameWithPath;
725 }
726 $reg = '';
727 // If open_basedir is set and the fileName was supplied without a path the is_dir check fails
728 if (!is_dir($fileNameWithPath) && preg_match('/(.*)\\.([^\\.]*$)/', $info['file'], $reg)) {
729 $info['filebody'] = $reg[1];
730 $info['fileext'] = strtolower($reg[2]);
731 $info['realFileext'] = $reg[2];
732 } else {
733 $info['filebody'] = $info['file'];
734 $info['fileext'] = '';
735 }
736 reset($info);
737 return $info;
738 }
739
740 /**
741 * Returns the directory part of a path without trailing slash
742 * If there is no dir-part, then an empty string is returned.
743 * Behaviour:
744 *
745 * '/dir1/dir2/script.php' => '/dir1/dir2'
746 * '/dir1/' => '/dir1'
747 * 'dir1/script.php' => 'dir1'
748 * 'd/script.php' => 'd'
749 * '/script.php' => ''
750 * '' => ''
751 *
752 * @param string $path Directory name / path
753 * @return string Processed input value. See function description.
754 */
755 public static function dirname($path)
756 {
757 $p = self::revExplode('/', $path, 2);
758 return count($p) === 2 ? $p[0] : '';
759 }
760
761 /**
762 * Returns TRUE if the first part of $str matches the string $partStr
763 *
764 * @param string $str Full string to check
765 * @param string $partStr Reference string which must be found as the "first part" of the full string
766 * @return bool TRUE if $partStr was found to be equal to the first part of $str
767 */
768 public static function isFirstPartOfStr($str, $partStr)
769 {
770 $str = is_array($str) ? '' : (string)$str;
771 $partStr = is_array($partStr) ? '' : (string)$partStr;
772 return $partStr !== '' && strpos($str, $partStr, 0) === 0;
773 }
774
775 /**
776 * Formats the input integer $sizeInBytes as bytes/kilobytes/megabytes (-/K/M)
777 *
778 * @param int $sizeInBytes Number of bytes to format.
779 * @param string $labels Binary unit name "iec", decimal unit name "si" or labels for bytes, kilo, mega, giga, and so on separated by vertical bar (|) and possibly encapsulated in "". Eg: " | K| M| G". Defaults to "iec".
780 * @param int $base The unit base if not using a unit name. Defaults to 1024.
781 * @return string Formatted representation of the byte number, for output.
782 */
783 public static function formatSize($sizeInBytes, $labels = '', $base = 0)
784 {
785 $defaultFormats = [
786 'iec' => ['base' => 1024, 'labels' => [' ', ' Ki', ' Mi', ' Gi', ' Ti', ' Pi', ' Ei', ' Zi', ' Yi']],
787 'si' => ['base' => 1000, 'labels' => [' ', ' k', ' M', ' G', ' T', ' P', ' E', ' Z', ' Y']],
788 ];
789 // Set labels and base:
790 if (empty($labels)) {
791 $labels = 'iec';
792 }
793 if (isset($defaultFormats[$labels])) {
794 $base = $defaultFormats[$labels]['base'];
795 $labelArr = $defaultFormats[$labels]['labels'];
796 } else {
797 $base = (int)$base;
798 if ($base !== 1000 && $base !== 1024) {
799 $base = 1024;
800 }
801 $labelArr = explode('|', str_replace('"', '', $labels));
802 }
803 // @todo find out which locale is used for current BE user to cover the BE case as well
804 $oldLocale = setlocale(LC_NUMERIC, 0);
805 $newLocale = $GLOBALS['TSFE']->config['config']['locale_all'] ?? '';
806 if ($newLocale) {
807 setlocale(LC_NUMERIC, $newLocale);
808 }
809 $localeInfo = localeconv();
810 if ($newLocale) {
811 setlocale(LC_NUMERIC, $oldLocale);
812 }
813 $sizeInBytes = max($sizeInBytes, 0);
814 $multiplier = floor(($sizeInBytes ? log($sizeInBytes) : 0) / log($base));
815 $sizeInUnits = $sizeInBytes / pow($base, $multiplier);
816 if ($sizeInUnits > ($base * .9)) {
817 $multiplier++;
818 }
819 $multiplier = min($multiplier, count($labelArr) - 1);
820 $sizeInUnits = $sizeInBytes / pow($base, $multiplier);
821 return number_format($sizeInUnits, (($multiplier > 0) && ($sizeInUnits < 20)) ? 2 : 0, $localeInfo['decimal_point'], '') . $labelArr[$multiplier];
822 }
823
824 /**
825 * This splits a string by the chars in $operators (typical /+-*) and returns an array with them in
826 *
827 * @param string $string Input string, eg "123 + 456 / 789 - 4
828 * @param string $operators Operators to split by, typically "/+-*
829 * @return array Array with operators and operands separated.
830 * @see \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::calc(), \TYPO3\CMS\Frontend\Imaging\GifBuilder::calcOffset()
831 */
832 public static function splitCalc($string, $operators)
833 {
834 $res = [];
835 $sign = '+';
836 while ($string) {
837 $valueLen = strcspn($string, $operators);
838 $value = substr($string, 0, $valueLen);
839 $res[] = [$sign, trim($value)];
840 $sign = substr($string, $valueLen, 1);
841 $string = substr($string, $valueLen + 1);
842 }
843 reset($res);
844 return $res;
845 }
846
847 /**
848 * Checking syntax of input email address
849 *
850 * http://tools.ietf.org/html/rfc3696
851 * International characters are allowed in email. So the whole address needs
852 * to be converted to punicode before passing it to filter_var().
853 *
854 * Also the @ sign may appear multiple times in an address. If not used as
855 * a boundary marker between the user- and domain part, it must be escaped
856 * with a backslash: \@. This mean we can not just explode on the @ sign and
857 * expect to get just two parts. So we pop off the domain and then glue the
858 * rest together again.
859 *
860 * @param string $email Input string to evaluate
861 * @return bool Returns TRUE if the $email address (input string) is valid
862 */
863 public static function validEmail($email)
864 {
865 // Early return in case input is not a string
866 if (!is_string($email)) {
867 return false;
868 }
869 $atPosition = strrpos($email, '@');
870 if (!$atPosition || $atPosition + 1 === strlen($email)) {
871 // Return if no @ found or it is placed at the very beginning or end of the email
872 return false;
873 }
874 $domain = substr($email, $atPosition + 1);
875 $user = substr($email, 0, $atPosition);
876 if (!preg_match('/^[a-z0-9.\\-]*$/i', $domain)) {
877 $domain = HttpUtility::idn_to_ascii($domain);
878 if ($domain === false) {
879 return false;
880 }
881 }
882 return filter_var($user . '@' . $domain, FILTER_VALIDATE_EMAIL) !== false;
883 }
884
885 /**
886 * Returns an ASCII string (punicode) representation of $value
887 *
888 * @param string $value
889 * @return string An ASCII encoded (punicode) string
890 * @deprecated since TYPO3 v10.0, will be removed in TYPO3 v11.0, use PHP's native idn_to_ascii($domain, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46) function directly.
891 */
892 public static function idnaEncode($value)
893 {
894 trigger_error(__METHOD__ . ' will be removed in TYPO3 v11.0. Use PHPs native "idn_to_ascii($domain, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46)" function directly instead.', E_USER_DEPRECATED);
895 // Early return in case input is not a string or empty
896 if (!is_string($value) || empty($value)) {
897 return (string)$value;
898 }
899 // Split on the last "@" since addresses like "foo@bar"@example.org are valid where the only focus
900 // is an email address
901 $atPosition = strrpos($value, '@');
902 if ($atPosition !== false) {
903 $domain = substr($value, $atPosition + 1);
904 $local = substr($value, 0, $atPosition);
905 $domain = (string)HttpUtility::idn_to_ascii($domain);
906 // Return if no @ found or it is placed at the very beginning or end of the email
907 return $local . '@' . $domain;
908 }
909 return (string)HttpUtility::idn_to_ascii($value);
910 }
911
912 /**
913 * Returns a given string with underscores as UpperCamelCase.
914 * Example: Converts blog_example to BlogExample
915 *
916 * @param string $string String to be converted to camel case
917 * @return string UpperCamelCasedWord
918 */
919 public static function underscoredToUpperCamelCase($string)
920 {
921 return str_replace(' ', '', ucwords(str_replace('_', ' ', strtolower($string))));
922 }
923
924 /**
925 * Returns a given string with underscores as lowerCamelCase.
926 * Example: Converts minimal_value to minimalValue
927 *
928 * @param string $string String to be converted to camel case
929 * @return string lowerCamelCasedWord
930 */
931 public static function underscoredToLowerCamelCase($string)
932 {
933 return lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', strtolower($string)))));
934 }
935
936 /**
937 * Returns a given CamelCasedString as an lowercase string with underscores.
938 * Example: Converts BlogExample to blog_example, and minimalValue to minimal_value
939 *
940 * @param string $string String to be converted to lowercase underscore
941 * @return string lowercase_and_underscored_string
942 */
943 public static function camelCaseToLowerCaseUnderscored($string)
944 {
945 $value = preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $string);
946 return mb_strtolower($value, 'utf-8');
947 }
948
949 /**
950 * Checks if a given string is a Uniform Resource Locator (URL).
951 *
952 * On seriously malformed URLs, parse_url may return FALSE and emit an
953 * E_WARNING.
954 *
955 * filter_var() requires a scheme to be present.
956 *
957 * http://www.faqs.org/rfcs/rfc2396.html
958 * Scheme names consist of a sequence of characters beginning with a
959 * lower case letter and followed by any combination of lower case letters,
960 * digits, plus ("+"), period ("."), or hyphen ("-"). For resiliency,
961 * programs interpreting URI should treat upper case letters as equivalent to
962 * lower case in scheme names (e.g., allow "HTTP" as well as "http").
963 * scheme = alpha *( alpha | digit | "+" | "-" | "." )
964 *
965 * Convert the domain part to punicode if it does not look like a regular
966 * domain name. Only the domain part because RFC3986 specifies the the rest of
967 * the url may not contain special characters:
968 * http://tools.ietf.org/html/rfc3986#appendix-A
969 *
970 * @param string $url The URL to be validated
971 * @return bool Whether the given URL is valid
972 */
973 public static function isValidUrl($url)
974 {
975 $parsedUrl = parse_url($url);
976 if (!$parsedUrl || !isset($parsedUrl['scheme'])) {
977 return false;
978 }
979 // HttpUtility::buildUrl() will always build urls with <scheme>://
980 // our original $url might only contain <scheme>: (e.g. mail:)
981 // so we convert that to the double-slashed version to ensure
982 // our check against the $recomposedUrl is proper
983 if (!self::isFirstPartOfStr($url, $parsedUrl['scheme'] . '://')) {
984 $url = str_replace($parsedUrl['scheme'] . ':', $parsedUrl['scheme'] . '://', $url);
985 }
986 $recomposedUrl = HttpUtility::buildUrl($parsedUrl);
987 if ($recomposedUrl !== $url) {
988 // The parse_url() had to modify characters, so the URL is invalid
989 return false;
990 }
991 if (isset($parsedUrl['host']) && !preg_match('/^[a-z0-9.\\-]*$/i', $parsedUrl['host'])) {
992 $host = HttpUtility::idn_to_ascii($parsedUrl['host']);
993 if ($host === false) {
994 return false;
995 }
996 $parsedUrl['host'] = $host;
997 }
998 return filter_var(HttpUtility::buildUrl($parsedUrl), FILTER_VALIDATE_URL) !== false;
999 }
1000
1001 /*************************
1002 *
1003 * ARRAY FUNCTIONS
1004 *
1005 *************************/
1006
1007 /**
1008 * Explodes a $string delimited by $delimiter and casts each item in the array to (int).
1009 * Corresponds to \TYPO3\CMS\Core\Utility\GeneralUtility::trimExplode(), but with conversion to integers for all values.
1010 *
1011 * @param string $delimiter Delimiter string to explode with
1012 * @param string $string The string to explode
1013 * @param bool $removeEmptyValues If set, all empty values (='') will NOT be set in output
1014 * @param int $limit If positive, the result will contain a maximum of limit elements,
1015 * @return array Exploded values, all converted to integers
1016 */
1017 public static function intExplode($delimiter, $string, $removeEmptyValues = false, $limit = 0)
1018 {
1019 $result = explode($delimiter, $string);
1020 foreach ($result as $key => &$value) {
1021 if ($removeEmptyValues && ($value === '' || trim($value) === '')) {
1022 unset($result[$key]);
1023 } else {
1024 $value = (int)$value;
1025 }
1026 }
1027 unset($value);
1028 if ($limit !== 0) {
1029 if ($limit < 0) {
1030 $result = array_slice($result, 0, $limit);
1031 } elseif (count($result) > $limit) {
1032 $lastElements = array_slice($result, $limit - 1);
1033 $result = array_slice($result, 0, $limit - 1);
1034 $result[] = implode($delimiter, $lastElements);
1035 }
1036 }
1037 return $result;
1038 }
1039
1040 /**
1041 * Reverse explode which explodes the string counting from behind.
1042 *
1043 * Note: The delimiter has to given in the reverse order as
1044 * it is occurring within the string.
1045 *
1046 * GeneralUtility::revExplode('[]', '[my][words][here]', 2)
1047 * ==> array('[my][words', 'here]')
1048 *
1049 * @param string $delimiter Delimiter string to explode with
1050 * @param string $string The string to explode
1051 * @param int $count Number of array entries
1052 * @return array Exploded values
1053 */
1054 public static function revExplode($delimiter, $string, $count = 0)
1055 {
1056 // 2 is the (currently, as of 2014-02) most-used value for $count in the core, therefore we check it first
1057 if ($count === 2) {
1058 $position = strrpos($string, strrev($delimiter));
1059 if ($position !== false) {
1060 return [substr($string, 0, $position), substr($string, $position + strlen($delimiter))];
1061 }
1062 return [$string];
1063 }
1064 if ($count <= 1) {
1065 return [$string];
1066 }
1067 $explodedValues = explode($delimiter, strrev($string), $count);
1068 $explodedValues = array_map('strrev', $explodedValues);
1069 return array_reverse($explodedValues);
1070 }
1071
1072 /**
1073 * Explodes a string and trims all values for whitespace in the end.
1074 * If $onlyNonEmptyValues is set, then all blank ('') values are removed.
1075 *
1076 * @param string $delim Delimiter string to explode with
1077 * @param string $string The string to explode
1078 * @param bool $removeEmptyValues If set, all empty values will be removed in output
1079 * @param int $limit If limit is set and positive, the returned array will contain a maximum of limit elements with
1080 * the last element containing the rest of string. If the limit parameter is negative, all components
1081 * except the last -limit are returned.
1082 * @return array Exploded values
1083 */
1084 public static function trimExplode($delim, $string, $removeEmptyValues = false, $limit = 0)
1085 {
1086 $result = explode($delim, $string);
1087 if ($removeEmptyValues) {
1088 $temp = [];
1089 foreach ($result as $value) {
1090 if (trim($value) !== '') {
1091 $temp[] = $value;
1092 }
1093 }
1094 $result = $temp;
1095 }
1096 if ($limit > 0 && count($result) > $limit) {
1097 $lastElements = array_splice($result, $limit - 1);
1098 $result[] = implode($delim, $lastElements);
1099 } elseif ($limit < 0) {
1100 $result = array_slice($result, 0, $limit);
1101 }
1102 $result = array_map('trim', $result);
1103 return $result;
1104 }
1105
1106 /**
1107 * Implodes a multidim-array into GET-parameters (eg. &param[key][key2]=value2&param[key][key3]=value3)
1108 *
1109 * @param string $name Name prefix for entries. Set to blank if you wish none.
1110 * @param array $theArray The (multidimensional) array to implode
1111 * @param string $str (keep blank)
1112 * @param bool $skipBlank If set, parameters which were blank strings would be removed.
1113 * @param bool $rawurlencodeParamName If set, the param name itself (for example "param[key][key2]") would be rawurlencoded as well.
1114 * @return string Imploded result, fx. &param[key][key2]=value2&param[key][key3]=value3
1115 * @see explodeUrl2Array()
1116 */
1117 public static function implodeArrayForUrl($name, array $theArray, $str = '', $skipBlank = false, $rawurlencodeParamName = false)
1118 {
1119 foreach ($theArray as $Akey => $AVal) {
1120 $thisKeyName = $name ? $name . '[' . $Akey . ']' : $Akey;
1121 if (is_array($AVal)) {
1122 $str = self::implodeArrayForUrl($thisKeyName, $AVal, $str, $skipBlank, $rawurlencodeParamName);
1123 } else {
1124 if (!$skipBlank || (string)$AVal !== '') {
1125 $str .= '&' . ($rawurlencodeParamName ? rawurlencode($thisKeyName) : $thisKeyName) . '=' . rawurlencode($AVal);
1126 }
1127 }
1128 }
1129 return $str;
1130 }
1131
1132 /**
1133 * Explodes a string with GETvars (eg. "&id=1&type=2&ext[mykey]=3") into an array.
1134 *
1135 * Note! If you want to use a multi-dimensional string, consider this plain simple PHP code instead:
1136 *
1137 * $result = [];
1138 * parse_str($queryParametersAsString, $result);
1139 *
1140 * However, if you do magic with a flat structure (e.g. keeping "ext[mykey]" as flat key in a one-dimensional array)
1141 * then this method is for you.
1142 *
1143 * @param string $string GETvars string
1144 * @return array Array of values. All values AND keys are rawurldecoded() as they properly should be. But this means that any implosion of the array again must rawurlencode it!
1145 * @see implodeArrayForUrl()
1146 */
1147 public static function explodeUrl2Array($string)
1148 {
1149 $output = [];
1150 $p = explode('&', $string);
1151 foreach ($p as $v) {
1152 if ($v !== '') {
1153 list($pK, $pV) = explode('=', $v, 2);
1154 $output[rawurldecode($pK)] = rawurldecode($pV);
1155 }
1156 }
1157 return $output;
1158 }
1159
1160 /**
1161 * Returns an array with selected keys from incoming data.
1162 * (Better read source code if you want to find out...)
1163 *
1164 * @param string $varList List of variable/key names
1165 * @param array $getArray Array from where to get values based on the keys in $varList
1166 * @param bool $GPvarAlt If set, then \TYPO3\CMS\Core\Utility\GeneralUtility::_GP() is used to fetch the value if not found (isset) in the $getArray
1167 * @return array Output array with selected variables.
1168 */
1169 public static function compileSelectedGetVarsFromArray($varList, array $getArray, $GPvarAlt = true)
1170 {
1171 $keys = self::trimExplode(',', $varList, true);
1172 $outArr = [];
1173 foreach ($keys as $v) {
1174 if (isset($getArray[$v])) {
1175 $outArr[$v] = $getArray[$v];
1176 } elseif ($GPvarAlt) {
1177 $outArr[$v] = self::_GP($v);
1178 }
1179 }
1180 return $outArr;
1181 }
1182
1183 /**
1184 * Removes dots "." from end of a key identifier of TypoScript styled array.
1185 * array('key.' => array('property.' => 'value')) --> array('key' => array('property' => 'value'))
1186 *
1187 * @param array $ts TypoScript configuration array
1188 * @return array TypoScript configuration array without dots at the end of all keys
1189 */
1190 public static function removeDotsFromTS(array $ts)
1191 {
1192 $out = [];
1193 foreach ($ts as $key => $value) {
1194 if (is_array($value)) {
1195 $key = rtrim($key, '.');
1196 $out[$key] = self::removeDotsFromTS($value);
1197 } else {
1198 $out[$key] = $value;
1199 }
1200 }
1201 return $out;
1202 }
1203
1204 /*************************
1205 *
1206 * HTML/XML PROCESSING
1207 *
1208 *************************/
1209 /**
1210 * Returns an array with all attributes of the input HTML tag as key/value pairs. Attributes are only lowercase a-z
1211 * $tag is either a whole tag (eg '<TAG OPTION ATTRIB=VALUE>') or the parameter list (ex ' OPTION ATTRIB=VALUE>')
1212 * If an attribute is empty, then the value for the key is empty. You can check if it existed with isset()
1213 *
1214 * @param string $tag HTML-tag string (or attributes only)
1215 * @return array Array with the attribute values.
1216 */
1217 public static function get_tag_attributes($tag)
1218 {
1219 $components = self::split_tag_attributes($tag);
1220 // Attribute name is stored here
1221 $name = '';
1222 $valuemode = false;
1223 $attributes = [];
1224 foreach ($components as $key => $val) {
1225 // Only if $name is set (if there is an attribute, that waits for a value), that valuemode is enabled. This ensures that the attribute is assigned it's value
1226 if ($val !== '=') {
1227 if ($valuemode) {
1228 if ($name) {
1229 $attributes[$name] = $val;
1230 $name = '';
1231 }
1232 } else {
1233 if ($key = strtolower(preg_replace('/[^[:alnum:]_\\:\\-]/', '', $val))) {
1234 $attributes[$key] = '';
1235 $name = $key;
1236 }
1237 }
1238 $valuemode = false;
1239 } else {
1240 $valuemode = true;
1241 }
1242 }
1243 return $attributes;
1244 }
1245
1246 /**
1247 * Returns an array with the 'components' from an attribute list from an HTML tag. The result is normally analyzed by get_tag_attributes
1248 * Removes tag-name if found
1249 *
1250 * @param string $tag HTML-tag string (or attributes only)
1251 * @return array Array with the attribute values.
1252 */
1253 public static function split_tag_attributes($tag)
1254 {
1255 $tag_tmp = trim(preg_replace('/^<[^[:space:]]*/', '', trim($tag)));
1256 // Removes any > in the end of the string
1257 $tag_tmp = trim(rtrim($tag_tmp, '>'));
1258 $value = [];
1259 // Compared with empty string instead , 030102
1260 while ($tag_tmp !== '') {
1261 $firstChar = $tag_tmp[0];
1262 if ($firstChar === '"' || $firstChar === '\'') {
1263 $reg = explode($firstChar, $tag_tmp, 3);
1264 $value[] = $reg[1];
1265 $tag_tmp = trim($reg[2]);
1266 } elseif ($firstChar === '=') {
1267 $value[] = '=';
1268 // Removes = chars.
1269 $tag_tmp = trim(substr($tag_tmp, 1));
1270 } else {
1271 // There are '' around the value. We look for the next ' ' or '>'
1272 $reg = preg_split('/[[:space:]=]/', $tag_tmp, 2);
1273 $value[] = trim($reg[0]);
1274 $tag_tmp = trim(substr($tag_tmp, strlen($reg[0]), 1) . ($reg[1] ?? ''));
1275 }
1276 }
1277 reset($value);
1278 return $value;
1279 }
1280
1281 /**
1282 * Implodes attributes in the array $arr for an attribute list in eg. and HTML tag (with quotes)
1283 *
1284 * @param array $arr Array with attribute key/value pairs, eg. "bgcolor"=>"red", "border"=>0
1285 * @param bool $xhtmlSafe If set the resulting attribute list will have a) all attributes in lowercase (and duplicates weeded out, first entry taking precedence) and b) all values htmlspecialchar()'ed. It is recommended to use this switch!
1286 * @param bool $dontOmitBlankAttribs If TRUE, don't check if values are blank. Default is to omit attributes with blank values.
1287 * @return string Imploded attributes, eg. 'bgcolor="red" border="0"'
1288 */
1289 public static function implodeAttributes(array $arr, $xhtmlSafe = false, $dontOmitBlankAttribs = false)
1290 {
1291 if ($xhtmlSafe) {
1292 $newArr = [];
1293 foreach ($arr as $p => $v) {
1294 if (!isset($newArr[strtolower($p)])) {
1295 $newArr[strtolower($p)] = htmlspecialchars($v);
1296 }
1297 }
1298 $arr = $newArr;
1299 }
1300 $list = [];
1301 foreach ($arr as $p => $v) {
1302 if ((string)$v !== '' || $dontOmitBlankAttribs) {
1303 $list[] = $p . '="' . $v . '"';
1304 }
1305 }
1306 return implode(' ', $list);
1307 }
1308
1309 /**
1310 * Wraps JavaScript code XHTML ready with <script>-tags
1311 * Automatic re-indenting of the JS code is done by using the first line as indent reference.
1312 * This is nice for indenting JS code with PHP code on the same level.
1313 *
1314 * @param string $string JavaScript code
1315 * @return string The wrapped JS code, ready to put into a XHTML page
1316 */
1317 public static function wrapJS($string)
1318 {
1319 if (trim($string)) {
1320 // remove nl from the beginning
1321 $string = ltrim($string, LF);
1322 // re-ident to one tab using the first line as reference
1323 $match = [];
1324 if (preg_match('/^(\\t+)/', $string, $match)) {
1325 $string = str_replace($match[1], "\t", $string);
1326 }
1327 return '<script>
1328 /*<![CDATA[*/
1329 ' . $string . '
1330 /*]]>*/
1331 </script>';
1332 }
1333 return '';
1334 }
1335
1336 /**
1337 * Parses XML input into a PHP array with associative keys
1338 *
1339 * @param string $string XML data input
1340 * @param int $depth Number of element levels to resolve the XML into an array. Any further structure will be set as XML.
1341 * @param array $parserOptions Options that will be passed to PHP's xml_parser_set_option()
1342 * @return mixed The array with the parsed structure unless the XML parser returns with an error in which case the error message string is returned.
1343 */
1344 public static function xml2tree($string, $depth = 999, $parserOptions = [])
1345 {
1346 // Disables the functionality to allow external entities to be loaded when parsing the XML, must be kept
1347 $previousValueOfEntityLoader = libxml_disable_entity_loader(true);
1348 $parser = xml_parser_create();
1349 $vals = [];
1350 $index = [];
1351 xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0);
1352 xml_parser_set_option($parser, XML_OPTION_SKIP_WHITE, 0);
1353 foreach ($parserOptions as $option => $value) {
1354 xml_parser_set_option($parser, $option, $value);
1355 }
1356 xml_parse_into_struct($parser, $string, $vals, $index);
1357 libxml_disable_entity_loader($previousValueOfEntityLoader);
1358 if (xml_get_error_code($parser)) {
1359 return 'Line ' . xml_get_current_line_number($parser) . ': ' . xml_error_string(xml_get_error_code($parser));
1360 }
1361 xml_parser_free($parser);
1362 $stack = [[]];
1363 $stacktop = 0;
1364 $startPoint = 0;
1365 $tagi = [];
1366 foreach ($vals as $key => $val) {
1367 $type = $val['type'];
1368 // open tag:
1369 if ($type === 'open' || $type === 'complete') {
1370 $stack[$stacktop++] = $tagi;
1371 if ($depth == $stacktop) {
1372 $startPoint = $key;
1373 }
1374 $tagi = ['tag' => $val['tag']];
1375 if (isset($val['attributes'])) {
1376 $tagi['attrs'] = $val['attributes'];
1377 }
1378 if (isset($val['value'])) {
1379 $tagi['values'][] = $val['value'];
1380 }
1381 }
1382 // finish tag:
1383 if ($type === 'complete' || $type === 'close') {
1384 $oldtagi = $tagi;
1385 $tagi = $stack[--$stacktop];
1386 $oldtag = $oldtagi['tag'];
1387 unset($oldtagi['tag']);
1388 if ($depth == $stacktop + 1) {
1389 if ($key - $startPoint > 0) {
1390 $partArray = array_slice($vals, $startPoint + 1, $key - $startPoint - 1);
1391 $oldtagi['XMLvalue'] = self::xmlRecompileFromStructValArray($partArray);
1392 } else {
1393 $oldtagi['XMLvalue'] = $oldtagi['values'][0];
1394 }
1395 }
1396 $tagi['ch'][$oldtag][] = $oldtagi;
1397 unset($oldtagi);
1398 }
1399 // cdata
1400 if ($type === 'cdata') {
1401 $tagi['values'][] = $val['value'];
1402 }
1403 }
1404 return $tagi['ch'];
1405 }
1406
1407 /**
1408 * Converts a PHP array into an XML string.
1409 * The XML output is optimized for readability since associative keys are used as tag names.
1410 * This also means that only alphanumeric characters are allowed in the tag names AND only keys NOT starting with numbers (so watch your usage of keys!). However there are options you can set to avoid this problem.
1411 * Numeric keys are stored with the default tag name "numIndex" but can be overridden to other formats)
1412 * The function handles input values from the PHP array in a binary-safe way; All characters below 32 (except 9,10,13) will trigger the content to be converted to a base64-string
1413 * The PHP variable type of the data IS preserved as long as the types are strings, arrays, integers and booleans. Strings are the default type unless the "type" attribute is set.
1414 * The output XML has been tested with the PHP XML-parser and parses OK under all tested circumstances with 4.x versions. However, with PHP5 there seems to be the need to add an XML prologue a la <?xml version="1.0" encoding="[charset]" standalone="yes" ?> - otherwise UTF-8 is assumed! Unfortunately, many times the output from this function is used without adding that prologue meaning that non-ASCII characters will break the parsing!! This suchs of course! Effectively it means that the prologue should always be prepended setting the right characterset, alternatively the system should always run as utf-8!
1415 * However using MSIE to read the XML output didn't always go well: One reason could be that the character encoding is not observed in the PHP data. The other reason may be if the tag-names are invalid in the eyes of MSIE. Also using the namespace feature will make MSIE break parsing. There might be more reasons...
1416 *
1417 * @param array $array The input PHP array with any kind of data; text, binary, integers. Not objects though.
1418 * @param string $NSprefix tag-prefix, eg. a namespace prefix like "T3:"
1419 * @param int $level Current recursion level. Don't change, stay at zero!
1420 * @param string $docTag Alternative document tag. Default is "phparray".
1421 * @param int $spaceInd If greater than zero, then the number of spaces corresponding to this number is used for indenting, if less than zero - no indentation, if zero - a single TAB is used
1422 * @param array $options Options for the compilation. Key "useNindex" => 0/1 (boolean: whether to use "n0, n1, n2" for num. indexes); Key "useIndexTagForNum" => "[tag for numerical indexes]"; Key "useIndexTagForAssoc" => "[tag for associative indexes"; Key "parentTagMap" => array('parentTag' => 'thisLevelTag')
1423 * @param array $stackData Stack data. Don't touch.
1424 * @return string An XML string made from the input content in the array.
1425 * @see xml2array()
1426 */
1427 public static function array2xml(array $array, $NSprefix = '', $level = 0, $docTag = 'phparray', $spaceInd = 0, array $options = [], array $stackData = [])
1428 {
1429 // The list of byte values which will trigger binary-safe storage. If any value has one of these char values in it, it will be encoded in base64
1430 $binaryChars = "\0" . chr(1) . chr(2) . chr(3) . chr(4) . chr(5) . chr(6) . chr(7) . chr(8) . chr(11) . chr(12) . chr(14) . chr(15) . chr(16) . chr(17) . chr(18) . chr(19) . chr(20) . chr(21) . chr(22) . chr(23) . chr(24) . chr(25) . chr(26) . chr(27) . chr(28) . chr(29) . chr(30) . chr(31);
1431 // Set indenting mode:
1432 $indentChar = $spaceInd ? ' ' : "\t";
1433 $indentN = $spaceInd > 0 ? $spaceInd : 1;
1434 $nl = $spaceInd >= 0 ? LF : '';
1435 // Init output variable:
1436 $output = '';
1437 // Traverse the input array
1438 foreach ($array as $k => $v) {
1439 $attr = '';
1440 $tagName = $k;
1441 // Construct the tag name.
1442 // Use tag based on grand-parent + parent tag name
1443 if (isset($stackData['grandParentTagName'], $stackData['parentTagName'], $options['grandParentTagMap'][$stackData['grandParentTagName'] . '/' . $stackData['parentTagName']])) {
1444 $attr .= ' index="' . htmlspecialchars($tagName) . '"';
1445 $tagName = (string)$options['grandParentTagMap'][$stackData['grandParentTagName'] . '/' . $stackData['parentTagName']];
1446 } elseif (isset($stackData['parentTagName'], $options['parentTagMap'][$stackData['parentTagName'] . ':_IS_NUM']) && MathUtility::canBeInterpretedAsInteger($tagName)) {
1447 // Use tag based on parent tag name + if current tag is numeric
1448 $attr .= ' index="' . htmlspecialchars($tagName) . '"';
1449 $tagName = (string)$options['parentTagMap'][$stackData['parentTagName'] . ':_IS_NUM'];
1450 } elseif (isset($stackData['parentTagName'], $options['parentTagMap'][$stackData['parentTagName'] . ':' . $tagName])) {
1451 // Use tag based on parent tag name + current tag
1452 $attr .= ' index="' . htmlspecialchars($tagName) . '"';
1453 $tagName = (string)$options['parentTagMap'][$stackData['parentTagName'] . ':' . $tagName];
1454 } elseif (isset($stackData['parentTagName'], $options['parentTagMap'][$stackData['parentTagName']])) {
1455 // Use tag based on parent tag name:
1456 $attr .= ' index="' . htmlspecialchars($tagName) . '"';
1457 $tagName = (string)$options['parentTagMap'][$stackData['parentTagName']];
1458 } elseif (MathUtility::canBeInterpretedAsInteger($tagName)) {
1459 // If integer...;
1460 if ($options['useNindex']) {
1461 // If numeric key, prefix "n"
1462 $tagName = 'n' . $tagName;
1463 } else {
1464 // Use special tag for num. keys:
1465 $attr .= ' index="' . $tagName . '"';
1466 $tagName = $options['useIndexTagForNum'] ?: 'numIndex';
1467 }
1468 } elseif (!empty($options['useIndexTagForAssoc'])) {
1469 // Use tag for all associative keys:
1470 $attr .= ' index="' . htmlspecialchars($tagName) . '"';
1471 $tagName = $options['useIndexTagForAssoc'];
1472 }
1473 // The tag name is cleaned up so only alphanumeric chars (plus - and _) are in there and not longer than 100 chars either.
1474 $tagName = substr(preg_replace('/[^[:alnum:]_-]/', '', $tagName), 0, 100);
1475 // If the value is an array then we will call this function recursively:
1476 if (is_array($v)) {
1477 // Sub elements:
1478 if (isset($options['alt_options']) && $options['alt_options'][($stackData['path'] ?? '') . '/' . $tagName]) {
1479 $subOptions = $options['alt_options'][$stackData['path'] . '/' . $tagName];
1480 $clearStackPath = $subOptions['clearStackPath'];
1481 } else {
1482 $subOptions = $options;
1483 $clearStackPath = false;
1484 }
1485 if (empty($v)) {
1486 $content = '';
1487 } else {
1488 $content = $nl . self::array2xml($v, $NSprefix, $level + 1, '', $spaceInd, $subOptions, [
1489 'parentTagName' => $tagName,
1490 'grandParentTagName' => $stackData['parentTagName'] ?? '',
1491 'path' => $clearStackPath ? '' : ($stackData['path'] ?? '') . '/' . $tagName
1492 ]) . ($spaceInd >= 0 ? str_pad('', ($level + 1) * $indentN, $indentChar) : '');
1493 }
1494 // Do not set "type = array". Makes prettier XML but means that empty arrays are not restored with xml2array
1495 if (!isset($options['disableTypeAttrib']) || (int)$options['disableTypeAttrib'] != 2) {
1496 $attr .= ' type="array"';
1497 }
1498 } else {
1499 // Just a value:
1500 // Look for binary chars:
1501 $vLen = strlen($v);
1502 // Go for base64 encoding if the initial segment NOT matching any binary char has the same length as the whole string!
1503 if ($vLen && strcspn($v, $binaryChars) != $vLen) {
1504 // If the value contained binary chars then we base64-encode it an set an attribute to notify this situation:
1505 $content = $nl . chunk_split(base64_encode($v));
1506 $attr .= ' base64="1"';
1507 } else {
1508 // Otherwise, just htmlspecialchar the stuff:
1509 $content = htmlspecialchars($v);
1510 $dType = gettype($v);
1511 if ($dType === 'string') {
1512 if (isset($options['useCDATA']) && $options['useCDATA'] && $content != $v) {
1513 $content = '<![CDATA[' . $v . ']]>';
1514 }
1515 } elseif (!$options['disableTypeAttrib']) {
1516 $attr .= ' type="' . $dType . '"';
1517 }
1518 }
1519 }
1520 if ((string)$tagName !== '') {
1521 // Add the element to the output string:
1522 $output .= ($spaceInd >= 0 ? str_pad('', ($level + 1) * $indentN, $indentChar) : '')
1523 . '<' . $NSprefix . $tagName . $attr . '>' . $content . '</' . $NSprefix . $tagName . '>' . $nl;
1524 }
1525 }
1526 // If we are at the outer-most level, then we finally wrap it all in the document tags and return that as the value:
1527 if (!$level) {
1528 $output = '<' . $docTag . '>' . $nl . $output . '</' . $docTag . '>';
1529 }
1530 return $output;
1531 }
1532
1533 /**
1534 * Converts an XML string to a PHP array.
1535 * This is the reverse function of array2xml()
1536 * This is a wrapper for xml2arrayProcess that adds a two-level cache
1537 *
1538 * @param string $string XML content to convert into an array
1539 * @param string $NSprefix The tag-prefix resolve, eg. a namespace like "T3:"
1540 * @param bool $reportDocTag If set, the document tag will be set in the key "_DOCUMENT_TAG" of the output array
1541 * @return mixed If the parsing had errors, a string with the error message is returned. Otherwise an array with the content.
1542 * @see array2xml(),xml2arrayProcess()
1543 */
1544 public static function xml2array($string, $NSprefix = '', $reportDocTag = false)
1545 {
1546 $runtimeCache = static::makeInstance(CacheManager::class)->getCache('runtime');
1547 $firstLevelCache = $runtimeCache->get('generalUtilityXml2Array') ?: [];
1548 $identifier = md5($string . $NSprefix . ($reportDocTag ? '1' : '0'));
1549 // Look up in first level cache
1550 if (empty($firstLevelCache[$identifier])) {
1551 $firstLevelCache[$identifier] = self::xml2arrayProcess(trim($string), $NSprefix, $reportDocTag);
1552 $runtimeCache->set('generalUtilityXml2Array', $firstLevelCache);
1553 }
1554 return $firstLevelCache[$identifier];
1555 }
1556
1557 /**
1558 * Converts an XML string to a PHP array.
1559 * This is the reverse function of array2xml()
1560 *
1561 * @param string $string XML content to convert into an array
1562 * @param string $NSprefix The tag-prefix resolve, eg. a namespace like "T3:"
1563 * @param bool $reportDocTag If set, the document tag will be set in the key "_DOCUMENT_TAG" of the output array
1564 * @return mixed If the parsing had errors, a string with the error message is returned. Otherwise an array with the content.
1565 * @see array2xml()
1566 */
1567 protected static function xml2arrayProcess($string, $NSprefix = '', $reportDocTag = false)
1568 {
1569 // Disables the functionality to allow external entities to be loaded when parsing the XML, must be kept
1570 $previousValueOfEntityLoader = libxml_disable_entity_loader(true);
1571 // Create parser:
1572 $parser = xml_parser_create();
1573 $vals = [];
1574 $index = [];
1575 xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0);
1576 xml_parser_set_option($parser, XML_OPTION_SKIP_WHITE, 0);
1577 // Default output charset is UTF-8, only ASCII, ISO-8859-1 and UTF-8 are supported!!!
1578 $match = [];
1579 preg_match('/^[[:space:]]*<\\?xml[^>]*encoding[[:space:]]*=[[:space:]]*"([^"]*)"/', substr($string, 0, 200), $match);
1580 $theCharset = $match[1] ?? 'utf-8';
1581 // us-ascii / utf-8 / iso-8859-1
1582 xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, $theCharset);
1583 // Parse content:
1584 xml_parse_into_struct($parser, $string, $vals, $index);
1585 libxml_disable_entity_loader($previousValueOfEntityLoader);
1586 // If error, return error message:
1587 if (xml_get_error_code($parser)) {
1588 return 'Line ' . xml_get_current_line_number($parser) . ': ' . xml_error_string(xml_get_error_code($parser));
1589 }
1590 xml_parser_free($parser);
1591 // Init vars:
1592 $stack = [[]];
1593 $stacktop = 0;
1594 $current = [];
1595 $tagName = '';
1596 $documentTag = '';
1597 // Traverse the parsed XML structure:
1598 foreach ($vals as $key => $val) {
1599 // First, process the tag-name (which is used in both cases, whether "complete" or "close")
1600 $tagName = $val['tag'];
1601 if (!$documentTag) {
1602 $documentTag = $tagName;
1603 }
1604 // Test for name space:
1605 $tagName = $NSprefix && strpos($tagName, $NSprefix) === 0 ? substr($tagName, strlen($NSprefix)) : $tagName;
1606 // Test for numeric tag, encoded on the form "nXXX":
1607 $testNtag = substr($tagName, 1);
1608 // Closing tag.
1609 $tagName = $tagName[0] === 'n' && MathUtility::canBeInterpretedAsInteger($testNtag) ? (int)$testNtag : $tagName;
1610 // Test for alternative index value:
1611 if ((string)($val['attributes']['index'] ?? '') !== '') {
1612 $tagName = $val['attributes']['index'];
1613 }
1614 // Setting tag-values, manage stack:
1615 switch ($val['type']) {
1616 case 'open':
1617 // If open tag it means there is an array stored in sub-elements. Therefore increase the stackpointer and reset the accumulation array:
1618 // Setting blank place holder
1619 $current[$tagName] = [];
1620 $stack[$stacktop++] = $current;
1621 $current = [];
1622 break;
1623 case 'close':
1624 // If the tag is "close" then it is an array which is closing and we decrease the stack pointer.
1625 $oldCurrent = $current;
1626 $current = $stack[--$stacktop];
1627 // Going to the end of array to get placeholder key, key($current), and fill in array next:
1628 end($current);
1629 $current[key($current)] = $oldCurrent;
1630 unset($oldCurrent);
1631 break;
1632 case 'complete':
1633 // If "complete", then it's a value. If the attribute "base64" is set, then decode the value, otherwise just set it.
1634 if (!empty($val['attributes']['base64'])) {
1635 $current[$tagName] = base64_decode($val['value']);
1636 } else {
1637 // Had to cast it as a string - otherwise it would be evaluate FALSE if tested with isset()!!
1638 $current[$tagName] = (string)($val['value'] ?? '');
1639 // Cast type:
1640 switch ((string)($val['attributes']['type'] ?? '')) {
1641 case 'integer':
1642 $current[$tagName] = (int)$current[$tagName];
1643 break;
1644 case 'double':
1645 $current[$tagName] = (double)$current[$tagName];
1646 break;
1647 case 'boolean':
1648 $current[$tagName] = (bool)$current[$tagName];
1649 break;
1650 case 'NULL':
1651 $current[$tagName] = null;
1652 break;
1653 case 'array':
1654 // MUST be an empty array since it is processed as a value; Empty arrays would end up here because they would have no tags inside...
1655 $current[$tagName] = [];
1656 break;
1657 }
1658 }
1659 break;
1660 }
1661 }
1662 if ($reportDocTag) {
1663 $current[$tagName]['_DOCUMENT_TAG'] = $documentTag;
1664 }
1665 // Finally return the content of the document tag.
1666 return $current[$tagName];
1667 }
1668
1669 /**
1670 * This implodes an array of XML parts (made with xml_parse_into_struct()) into XML again.
1671 *
1672 * @param array $vals An array of XML parts, see xml2tree
1673 * @return string Re-compiled XML data.
1674 */
1675 public static function xmlRecompileFromStructValArray(array $vals)
1676 {
1677 $XMLcontent = '';
1678 foreach ($vals as $val) {
1679 $type = $val['type'];
1680 // Open tag:
1681 if ($type === 'open' || $type === 'complete') {
1682 $XMLcontent .= '<' . $val['tag'];
1683 if (isset($val['attributes'])) {
1684 foreach ($val['attributes'] as $k => $v) {
1685 $XMLcontent .= ' ' . $k . '="' . htmlspecialchars($v) . '"';
1686 }
1687 }
1688 if ($type === 'complete') {
1689 if (isset($val['value'])) {
1690 $XMLcontent .= '>' . htmlspecialchars($val['value']) . '</' . $val['tag'] . '>';
1691 } else {
1692 $XMLcontent .= '/>';
1693 }
1694 } else {
1695 $XMLcontent .= '>';
1696 }
1697 if ($type === 'open' && isset($val['value'])) {
1698 $XMLcontent .= htmlspecialchars($val['value']);
1699 }
1700 }
1701 // Finish tag:
1702 if ($type === 'close') {
1703 $XMLcontent .= '</' . $val['tag'] . '>';
1704 }
1705 // Cdata
1706 if ($type === 'cdata') {
1707 $XMLcontent .= htmlspecialchars($val['value']);
1708 }
1709 }
1710 return $XMLcontent;
1711 }
1712
1713 /**
1714 * Minifies JavaScript
1715 *
1716 * @param string $script Script to minify
1717 * @param string $error Error message (if any)
1718 * @return string Minified script or source string if error happened
1719 */
1720 public static function minifyJavaScript($script, &$error = '')
1721 {
1722 $fakeThis = false;
1723 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_div.php']['minifyJavaScript'] ?? [] as $hookMethod) {
1724 try {
1725 $parameters = ['script' => $script];
1726 $script = static::callUserFunction($hookMethod, $parameters, $fakeThis);
1727 } catch (\Exception $e) {
1728 $errorMessage = 'Error minifying java script: ' . $e->getMessage();
1729 $error .= $errorMessage;
1730 static::getLogger()->warning($errorMessage, [
1731 'JavaScript' => $script,
1732 'hook' => $hookMethod,
1733 'exception' => $e,
1734 ]);
1735 }
1736 }
1737 return $script;
1738 }
1739
1740 /*************************
1741 *
1742 * FILES FUNCTIONS
1743 *
1744 *************************/
1745 /**
1746 * Reads the file or url $url and returns the content
1747 * If you are having trouble with proxies when reading URLs you can configure your way out of that with settings within $GLOBALS['TYPO3_CONF_VARS']['HTTP'].
1748 *
1749 * @param string $url File/URL to read
1750 * @param int $includeHeader Whether the HTTP header should be fetched or not. 0=disable, 1=fetch header+content, 2=fetch header only
1751 * @param array $requestHeaders HTTP headers to be used in the request
1752 * @param array $report Error code/message and, if $includeHeader is 1, response meta data (HTTP status and content type)
1753 * @return mixed The content from the resource given as input. FALSE if an error has occurred.
1754 */
1755 public static function getUrl($url, $includeHeader = 0, $requestHeaders = null, &$report = null)
1756 {
1757 if (isset($report)) {
1758 $report['error'] = 0;
1759 $report['message'] = '';
1760 }
1761 // Looks like it's an external file, use Guzzle by default
1762 if (preg_match('/^(?:http|ftp)s?|s(?:ftp|cp):/', $url)) {
1763 $requestFactory = static::makeInstance(RequestFactory::class);
1764 if (is_array($requestHeaders)) {
1765 $configuration = ['headers' => $requestHeaders];
1766 } else {
1767 $configuration = [];
1768 }
1769 $includeHeader = (int)$includeHeader;
1770 $method = $includeHeader === 2 ? 'HEAD' : 'GET';
1771 try {
1772 if (isset($report)) {
1773 $report['lib'] = 'GuzzleHttp';
1774 }
1775 $response = $requestFactory->request($url, $method, $configuration);
1776 } catch (RequestException $exception) {
1777 if (isset($report)) {
1778 $report['error'] = $exception->getCode() ?: 1518707554;
1779 $report['message'] = $exception->getMessage();
1780 $report['exception'] = $exception;
1781 }
1782 return false;
1783 }
1784 $content = '';
1785 // Add the headers to the output
1786 if ($includeHeader) {
1787 $parsedURL = parse_url($url);
1788 $content = $method . ' ' . ($parsedURL['path'] ?? '/')
1789 . (!empty($parsedURL['query']) ? '?' . $parsedURL['query'] : '') . ' HTTP/1.0' . CRLF
1790 . 'Host: ' . $parsedURL['host'] . CRLF
1791 . 'Connection: close' . CRLF;
1792 if (is_array($requestHeaders)) {
1793 $content .= implode(CRLF, $requestHeaders) . CRLF;
1794 }
1795 foreach ($response->getHeaders() as $headerName => $headerValues) {
1796 $content .= $headerName . ': ' . implode(', ', $headerValues) . CRLF;
1797 }
1798 // Headers are separated from the body with two CRLFs
1799 $content .= CRLF;
1800 }
1801
1802 $content .= $response->getBody()->getContents();
1803
1804 if (isset($report)) {
1805 if ($response->getStatusCode() >= 300 && $response->getStatusCode() < 400) {
1806 $report['http_code'] = $response->getStatusCode();
1807 $report['content_type'] = $response->getHeaderLine('Content-Type');
1808 $report['error'] = $response->getStatusCode();
1809 $report['message'] = $response->getReasonPhrase();
1810 } elseif (empty($content)) {
1811 $report['error'] = $response->getStatusCode();
1812 $report['message'] = $response->getReasonPhrase();
1813 } elseif ($includeHeader) {
1814 // Set only for $includeHeader to work exactly like PHP variant
1815 $report['http_code'] = $response->getStatusCode();
1816 $report['content_type'] = $response->getHeaderLine('Content-Type');
1817 }
1818 }
1819 } else {
1820 if (isset($report)) {
1821 $report['lib'] = 'file';
1822 }
1823 $content = @file_get_contents($url);
1824 if ($content === false && isset($report)) {
1825 $report['error'] = -1;
1826 $report['message'] = 'Couldn\'t get URL: ' . $url;
1827 }
1828 }
1829 return $content;
1830 }
1831
1832 /**
1833 * Split an array of MIME header strings into an associative array.
1834 * Multiple headers with the same name have their values merged as an array.
1835 *
1836 * @static
1837 * @param array $headers List of headers, eg. ['Foo: Bar', 'Foo: Baz']
1838 * @return array Key/Value(s) pairs of headers, eg. ['Foo' => ['Bar', 'Baz']]
1839 */
1840 protected static function splitHeaderLines(array $headers): array
1841 {
1842 $newHeaders = [];
1843 foreach ($headers as $header) {
1844 $parts = preg_split('/:[ \t]*/', $header, 2, PREG_SPLIT_NO_EMPTY);
1845 if (count($parts) !== 2) {
1846 continue;
1847 }
1848 $key = &$parts[0];
1849 $value = &$parts[1];
1850 if (array_key_exists($key, $newHeaders)) {
1851 if (is_array($newHeaders[$key])) {
1852 $newHeaders[$key][] = $value;
1853 } else {
1854 $prevValue = &$newHeaders[$key];
1855 $newHeaders[$key] = [$prevValue, $value];
1856 }
1857 } else {
1858 $newHeaders[$key] = $value;
1859 }
1860 }
1861 return $newHeaders;
1862 }
1863
1864 /**
1865 * Writes $content to the file $file
1866 *
1867 * @param string $file Filepath to write to
1868 * @param string $content Content to write
1869 * @param bool $changePermissions If TRUE, permissions are forced to be set
1870 * @return bool TRUE if the file was successfully opened and written to.
1871 */
1872 public static function writeFile($file, $content, $changePermissions = false)
1873 {
1874 if (!@is_file($file)) {
1875 $changePermissions = true;
1876 }
1877 if ($fd = fopen($file, 'wb')) {
1878 $res = fwrite($fd, $content);
1879 fclose($fd);
1880 if ($res === false) {
1881 return false;
1882 }
1883 // Change the permissions only if the file has just been created
1884 if ($changePermissions) {
1885 static::fixPermissions($file);
1886 }
1887 return true;
1888 }
1889 return false;
1890 }
1891
1892 /**
1893 * Sets the file system mode and group ownership of a file or a folder.
1894 *
1895 * @param string $path Path of file or folder, must not be escaped. Path can be absolute or relative
1896 * @param bool $recursive If set, also fixes permissions of files and folders in the folder (if $path is a folder)
1897 * @return mixed TRUE on success, FALSE on error, always TRUE on Windows OS
1898 */
1899 public static function fixPermissions($path, $recursive = false)
1900 {
1901 if (Environment::isWindows()) {
1902 return true;
1903 }
1904 $result = false;
1905 // Make path absolute
1906 if (!static::isAbsPath($path)) {
1907 $path = static::getFileAbsFileName($path);
1908 }
1909 if (static::isAllowedAbsPath($path)) {
1910 if (@is_file($path)) {
1911 $targetPermissions = $GLOBALS['TYPO3_CONF_VARS']['SYS']['fileCreateMask'] ?? '0644';
1912 } elseif (@is_dir($path)) {
1913 $targetPermissions = $GLOBALS['TYPO3_CONF_VARS']['SYS']['folderCreateMask'] ?? '0755';
1914 }
1915 if (!empty($targetPermissions)) {
1916 // make sure it's always 4 digits
1917 $targetPermissions = str_pad($targetPermissions, 4, 0, STR_PAD_LEFT);
1918 $targetPermissions = octdec($targetPermissions);
1919 // "@" is there because file is not necessarily OWNED by the user
1920 $result = @chmod($path, $targetPermissions);
1921 }
1922 // Set createGroup if not empty
1923 if (
1924 isset($GLOBALS['TYPO3_CONF_VARS']['SYS']['createGroup'])
1925 && $GLOBALS['TYPO3_CONF_VARS']['SYS']['createGroup'] !== ''
1926 ) {
1927 // "@" is there because file is not necessarily OWNED by the user
1928 $changeGroupResult = @chgrp($path, $GLOBALS['TYPO3_CONF_VARS']['SYS']['createGroup']);
1929 $result = $changeGroupResult ? $result : false;
1930 }
1931 // Call recursive if recursive flag if set and $path is directory
1932 if ($recursive && @is_dir($path)) {
1933 $handle = opendir($path);
1934 if (is_resource($handle)) {
1935 while (($file = readdir($handle)) !== false) {
1936 $recursionResult = null;
1937 if ($file !== '.' && $file !== '..') {
1938 if (@is_file($path . '/' . $file)) {
1939 $recursionResult = static::fixPermissions($path . '/' . $file);
1940 } elseif (@is_dir($path . '/' . $file)) {
1941 $recursionResult = static::fixPermissions($path . '/' . $file, true);
1942 }
1943 if (isset($recursionResult) && !$recursionResult) {
1944 $result = false;
1945 }
1946 }
1947 }
1948 closedir($handle);
1949 }
1950 }
1951 }
1952 return $result;
1953 }
1954
1955 /**
1956 * Writes $content to a filename in the typo3temp/ folder (and possibly one or two subfolders...)
1957 * Accepts an additional subdirectory in the file path!
1958 *
1959 * @param string $filepath Absolute file path to write within the typo3temp/ or Environment::getVarPath() folder - the file path must be prefixed with this path
1960 * @param string $content Content string to write
1961 * @return string Returns NULL on success, otherwise an error string telling about the problem.
1962 */
1963 public static function writeFileToTypo3tempDir($filepath, $content)
1964 {
1965 // Parse filepath into directory and basename:
1966 $fI = pathinfo($filepath);
1967 $fI['dirname'] .= '/';
1968 // Check parts:
1969 if (!static::validPathStr($filepath) || !$fI['basename'] || strlen($fI['basename']) >= 60) {
1970 return 'Input filepath "' . $filepath . '" was generally invalid!';
1971 }
1972
1973 // Setting main temporary directory name (standard)
1974 $allowedPathPrefixes = [
1975 Environment::getPublicPath() . '/typo3temp' => 'Environment::getPublicPath() + "/typo3temp/"'
1976 ];
1977 // Also allow project-path + /var/
1978 if (Environment::getVarPath() !== Environment::getPublicPath() . '/typo3temp/var') {
1979 $relPath = substr(Environment::getVarPath(), strlen(Environment::getProjectPath()) + 1);
1980 $allowedPathPrefixes[Environment::getVarPath()] = 'ProjectPath + ' . $relPath;
1981 }
1982
1983 $errorMessage = null;
1984 foreach ($allowedPathPrefixes as $pathPrefix => $prefixLabel) {
1985 $dirName = $pathPrefix . '/';
1986 // Invalid file path, let's check for the other path, if it exists
1987 if (!static::isFirstPartOfStr($fI['dirname'], $dirName)) {
1988 if ($errorMessage === null) {
1989 $errorMessage = '"' . $fI['dirname'] . '" was not within directory ' . $prefixLabel;
1990 }
1991 continue;
1992 }
1993 // This resets previous error messages from the first path
1994 $errorMessage = null;
1995
1996 if (!@is_dir($dirName)) {
1997 $errorMessage = $prefixLabel . ' was not a directory!';
1998 // continue and see if the next iteration resets the errorMessage above
1999 continue;
2000 }
2001 // Checking if the "subdir" is found
2002 $subdir = substr($fI['dirname'], strlen($dirName));
2003 if ($subdir) {
2004 if (preg_match('#^(?:[[:alnum:]_]+/)+$#', $subdir)) {
2005 $dirName .= $subdir;
2006 if (!@is_dir($dirName)) {
2007 static::mkdir_deep($pathPrefix . '/' . $subdir);
2008 }
2009 } else {
2010 $errorMessage = 'Subdir, "' . $subdir . '", was NOT on the form "[[:alnum:]_]/+"';
2011 break;
2012 }
2013 }
2014 // Checking dir-name again (sub-dir might have been created)
2015 if (@is_dir($dirName)) {
2016 if ($filepath === $dirName . $fI['basename']) {
2017 static::writeFile($filepath, $content);
2018 if (!@is_file($filepath)) {
2019 $errorMessage = 'The file was not written to the disk. Please, check that you have write permissions to the ' . $prefixLabel . ' directory.';
2020 break;
2021 }
2022 } else {
2023 $errorMessage = 'Calculated file location didn\'t match input "' . $filepath . '".';
2024 break;
2025 }
2026 } else {
2027 $errorMessage = '"' . $dirName . '" is not a directory!';
2028 break;
2029 }
2030 }
2031 return $errorMessage;
2032 }
2033
2034 /**
2035 * Wrapper function for mkdir.
2036 * Sets folder permissions according to $GLOBALS['TYPO3_CONF_VARS']['SYS']['folderCreateMask']
2037 * and group ownership according to $GLOBALS['TYPO3_CONF_VARS']['SYS']['createGroup']
2038 *
2039 * @param string $newFolder Absolute path to folder, see PHP mkdir() function. Removes trailing slash internally.
2040 * @return bool TRUE if @mkdir went well!
2041 */
2042 public static function mkdir($newFolder)
2043 {
2044 $result = @mkdir($newFolder, octdec($GLOBALS['TYPO3_CONF_VARS']['SYS']['folderCreateMask']));
2045 if ($result) {
2046 static::fixPermissions($newFolder);
2047 }
2048 return $result;
2049 }
2050
2051 /**
2052 * Creates a directory - including parent directories if necessary and
2053 * sets permissions on newly created directories.
2054 *
2055 * @param string $directory Target directory to create. Must a have trailing slash
2056 * @throws \InvalidArgumentException If $directory or $deepDirectory are not strings
2057 * @throws \RuntimeException If directory could not be created
2058 */
2059 public static function mkdir_deep($directory)
2060 {
2061 if (!is_string($directory)) {
2062 throw new \InvalidArgumentException('The specified directory is of type "' . gettype($directory) . '" but a string is expected.', 1303662955);
2063 }
2064 // Ensure there is only one slash
2065 $fullPath = rtrim($directory, '/') . '/';
2066 if ($fullPath !== '/' && !is_dir($fullPath)) {
2067 $firstCreatedPath = static::createDirectoryPath($fullPath);
2068 if ($firstCreatedPath !== '') {
2069 static::fixPermissions($firstCreatedPath, true);
2070 }
2071 }
2072 }
2073
2074 /**
2075 * Creates directories for the specified paths if they do not exist. This
2076 * functions sets proper permission mask but does not set proper user and
2077 * group.
2078 *
2079 * @static
2080 * @param string $fullDirectoryPath
2081 * @return string Path to the the first created directory in the hierarchy
2082 * @see \TYPO3\CMS\Core\Utility\GeneralUtility::mkdir_deep
2083 * @throws \RuntimeException If directory could not be created
2084 */
2085 protected static function createDirectoryPath($fullDirectoryPath)
2086 {
2087 $currentPath = $fullDirectoryPath;
2088 $firstCreatedPath = '';
2089 $permissionMask = octdec($GLOBALS['TYPO3_CONF_VARS']['SYS']['folderCreateMask']);
2090 if (!@is_dir($currentPath)) {
2091 do {
2092 $firstCreatedPath = $currentPath;
2093 $separatorPosition = strrpos($currentPath, DIRECTORY_SEPARATOR);
2094 $currentPath = substr($currentPath, 0, $separatorPosition);
2095 } while (!is_dir($currentPath) && $separatorPosition !== false);
2096 $result = @mkdir($fullDirectoryPath, $permissionMask, true);
2097 // Check existence of directory again to avoid race condition. Directory could have get created by another process between previous is_dir() and mkdir()
2098 if (!$result && !@is_dir($fullDirectoryPath)) {
2099 throw new \RuntimeException('Could not create directory "' . $fullDirectoryPath . '"!', 1170251401);
2100 }
2101 }
2102 return $firstCreatedPath;
2103 }
2104
2105 /**
2106 * Wrapper function for rmdir, allowing recursive deletion of folders and files
2107 *
2108 * @param string $path Absolute path to folder, see PHP rmdir() function. Removes trailing slash internally.
2109 * @param bool $removeNonEmpty Allow deletion of non-empty directories
2110 * @return bool TRUE if @rmdir went well!
2111 */
2112 public static function rmdir($path, $removeNonEmpty = false)
2113 {
2114 $OK = false;
2115 // Remove trailing slash
2116 $path = preg_replace('|/$|', '', $path);
2117 $isWindows = DIRECTORY_SEPARATOR === '\\';
2118 if (file_exists($path)) {
2119 $OK = true;
2120 if (!is_link($path) && is_dir($path)) {
2121 if ($removeNonEmpty === true && ($handle = @opendir($path))) {
2122 $entries = [];
2123
2124 while (false !== ($file = readdir($handle))) {
2125 if ($file === '.' || $file === '..') {
2126 continue;
2127 }
2128
2129 $entries[] = $path . '/' . $file;
2130 }
2131
2132 closedir($handle);
2133
2134 foreach ($entries as $entry) {
2135 if (!static::rmdir($entry, $removeNonEmpty)) {
2136 $OK = false;
2137 }
2138 }
2139 }
2140 if ($OK) {
2141 $OK = @rmdir($path);
2142 }
2143 } elseif (is_link($path) && is_dir($path) && $isWindows) {
2144 $OK = @rmdir($path);
2145 } else {
2146 // If $path is a file, simply remove it
2147 $OK = @unlink($path);
2148 }
2149 clearstatcache();
2150 } elseif (is_link($path)) {
2151 $OK = @unlink($path);
2152 if (!$OK && $isWindows) {
2153 // Try to delete dead folder links on Windows systems
2154 $OK = @rmdir($path);
2155 }
2156 clearstatcache();
2157 }
2158 return $OK;
2159 }
2160
2161 /**
2162 * Flushes a directory by first moving to a temporary resource, and then
2163 * triggering the remove process. This way directories can be flushed faster
2164 * to prevent race conditions on concurrent processes accessing the same directory.
2165 *
2166 * @param string $directory The directory to be renamed and flushed
2167 * @param bool $keepOriginalDirectory Whether to only empty the directory and not remove it
2168 * @param bool $flushOpcodeCache Also flush the opcode cache right after renaming the directory.
2169 * @return bool Whether the action was successful
2170 */
2171 public static function flushDirectory($directory, $keepOriginalDirectory = false, $flushOpcodeCache = false)
2172 {
2173 $result = false;
2174
2175 if (is_link($directory)) {
2176 // Avoid attempting to rename the symlink see #87367
2177 $directory = realpath($directory);
2178 }
2179
2180 if (is_dir($directory)) {
2181 $temporaryDirectory = rtrim($directory, '/') . '.' . StringUtility::getUniqueId('remove');
2182 if (rename($directory, $temporaryDirectory)) {
2183 if ($flushOpcodeCache) {
2184 self::makeInstance(OpcodeCacheService::class)->clearAllActive($directory);
2185 }
2186 if ($keepOriginalDirectory) {
2187 static::mkdir($directory);
2188 }
2189 clearstatcache();
2190 $result = static::rmdir($temporaryDirectory, true);
2191 }
2192 }
2193
2194 return $result;
2195 }
2196
2197 /**
2198 * Returns an array with the names of folders in a specific path
2199 * Will return 'error' (string) if there were an error with reading directory content.
2200 *
2201 * @param string $path Path to list directories from
2202 * @return array Returns an array with the directory entries as values. If no path, the return value is nothing.
2203 */
2204 public static function get_dirs($path)
2205 {
2206 $dirs = null;
2207 if ($path) {
2208 if (is_dir($path)) {
2209 $dir = scandir($path);
2210 $dirs = [];
2211 foreach ($dir as $entry) {
2212 if (is_dir($path . '/' . $entry) && $entry !== '..' && $entry !== '.') {
2213 $dirs[] = $entry;
2214 }
2215 }
2216 } else {
2217 $dirs = 'error';
2218 }
2219 }
2220 return $dirs;
2221 }
2222
2223 /**
2224 * Finds all files in a given path and returns them as an array. Each
2225 * array key is a md5 hash of the full path to the file. This is done because
2226 * 'some' extensions like the import/export extension depend on this.
2227 *
2228 * @param string $path The path to retrieve the files from.
2229 * @param string $extensionList A comma-separated list of file extensions. Only files of the specified types will be retrieved. When left blank, files of any type will be retrieved.
2230 * @param bool $prependPath If TRUE, the full path to the file is returned. If FALSE only the file name is returned.
2231 * @param string $order The sorting order. The default sorting order is alphabetical. Setting $order to 'mtime' will sort the files by modification time.
2232 * @param string $excludePattern A regular expression pattern of file names to exclude. For example: 'clear.gif' or '(clear.gif|.htaccess)'. The pattern will be wrapped with: '/^' and '$/'.
2233 * @return array|string Array of the files found, or an error message in case the path could not be opened.
2234 */
2235 public static function getFilesInDir($path, $extensionList = '', $prependPath = false, $order = '', $excludePattern = '')
2236 {
2237 $excludePattern = (string)$excludePattern;
2238 $path = rtrim($path, '/');
2239 if (!@is_dir($path)) {
2240 return [];
2241 }
2242
2243 $rawFileList = scandir($path);
2244 if ($rawFileList === false) {
2245 return 'error opening path: "' . $path . '"';
2246 }
2247
2248 $pathPrefix = $path . '/';
2249 $allowedFileExtensionArray = self::trimExplode(',', $extensionList);
2250 $extensionList = ',' . str_replace(' ', '', $extensionList) . ',';
2251 $files = [];
2252 foreach ($rawFileList as $entry) {
2253 $completePathToEntry = $pathPrefix . $entry;
2254 if (!@is_file($completePathToEntry)) {
2255 continue;
2256 }
2257
2258 foreach ($allowedFileExtensionArray as $allowedFileExtension) {
2259 if (
2260 ($extensionList === ',,' || stripos($extensionList, ',' . substr($entry, strlen($allowedFileExtension) * -1, strlen($allowedFileExtension)) . ',') !== false)
2261 && ($excludePattern === '' || !preg_match('/^' . $excludePattern . '$/', $entry))
2262 ) {
2263 if ($order !== 'mtime') {
2264 $files[] = $entry;
2265 } else {
2266 // Store the value in the key so we can do a fast asort later.
2267 $files[$entry] = filemtime($completePathToEntry);
2268 }
2269 }
2270 }
2271 }
2272
2273 $valueName = 'value';
2274 if ($order === 'mtime') {
2275 asort($files);
2276 $valueName = 'key';
2277 }
2278
2279 $valuePathPrefix = $prependPath ? $pathPrefix : '';
2280 $foundFiles = [];
2281 foreach ($files as $key => $value) {
2282 // Don't change this ever - extensions may depend on the fact that the hash is an md5 of the path! (import/export extension)
2283 $foundFiles[md5($pathPrefix . ${$valueName})] = $valuePathPrefix . ${$valueName};
2284 }
2285
2286 return $foundFiles;
2287 }
2288
2289 /**
2290 * Recursively gather all files and folders of a path.
2291 *
2292 * @param array $fileArr Empty input array (will have files added to it)
2293 * @param string $path The path to read recursively from (absolute) (include trailing slash!)
2294 * @param string $extList Comma list of file extensions: Only files with extensions in this list (if applicable) will be selected.
2295 * @param bool $regDirs If set, directories are also included in output.
2296 * @param int $recursivityLevels The number of levels to dig down...
2297 * @param string $excludePattern regex pattern of files/directories to exclude
2298 * @return array An array with the found files/directories.
2299 */
2300 public static function getAllFilesAndFoldersInPath(array $fileArr, $path, $extList = '', $regDirs = false, $recursivityLevels = 99, $excludePattern = '')
2301 {
2302 if ($regDirs) {
2303 $fileArr[md5($path)] = $path;
2304 }
2305 $fileArr = array_merge($fileArr, self::getFilesInDir($path, $extList, 1, 1, $excludePattern));
2306 $dirs = self::get_dirs($path);
2307 if ($recursivityLevels > 0 && is_array($dirs)) {
2308 foreach ($dirs as $subdirs) {
2309 if ((string)$subdirs !== '' && ($excludePattern === '' || !preg_match('/^' . $excludePattern . '$/', $subdirs))) {
2310 $fileArr = self::getAllFilesAndFoldersInPath($fileArr, $path . $subdirs . '/', $extList, $regDirs, $recursivityLevels - 1, $excludePattern);
2311 }
2312 }
2313 }
2314 return $fileArr;
2315 }
2316
2317 /**
2318 * Removes the absolute part of all files/folders in fileArr
2319 *
2320 * @param array $fileArr The file array to remove the prefix from
2321 * @param string $prefixToRemove The prefix path to remove (if found as first part of string!)
2322 * @return array|string The input $fileArr processed, or a string with an error message, when an error occurred.
2323 */
2324 public static function removePrefixPathFromList(array $fileArr, $prefixToRemove)
2325 {
2326 foreach ($fileArr as $k => &$absFileRef) {
2327 if (self::isFirstPartOfStr($absFileRef, $prefixToRemove)) {
2328 $absFileRef = substr($absFileRef, strlen($prefixToRemove));
2329 } else {
2330 return 'ERROR: One or more of the files was NOT prefixed with the prefix-path!';
2331 }
2332 }
2333 unset($absFileRef);
2334 return $fileArr;
2335 }
2336
2337 /**
2338 * Fixes a path for windows-backslashes and reduces double-slashes to single slashes
2339 *
2340 * @param string $theFile File path to process
2341 * @return string
2342 */
2343 public static function fixWindowsFilePath($theFile)
2344 {
2345 return str_replace(['\\', '//'], '/', $theFile);
2346 }
2347
2348 /**
2349 * Resolves "../" sections in the input path string.
2350 * For example "fileadmin/directory/../other_directory/" will be resolved to "fileadmin/other_directory/"
2351 *
2352 * @param string $pathStr File path in which "/../" is resolved
2353 * @return string
2354 */
2355 public static function resolveBackPath($pathStr)
2356 {
2357 if (strpos($pathStr, '..') === false) {
2358 return $pathStr;
2359 }
2360 $parts = explode('/', $pathStr);
2361 $output = [];
2362 $c = 0;
2363 foreach ($parts as $part) {
2364 if ($part === '..') {
2365 if ($c) {
2366 array_pop($output);
2367 --$c;
2368 } else {
2369 $output[] = $part;
2370 }
2371 } else {
2372 ++$c;
2373 $output[] = $part;
2374 }
2375 }
2376 return implode('/', $output);
2377 }
2378
2379 /**
2380 * Prefixes a URL used with 'header-location' with 'http://...' depending on whether it has it already.
2381 * - If already having a scheme, nothing is prepended
2382 * - If having REQUEST_URI slash '/', then prefixing 'http://[host]' (relative to host)
2383 * - Otherwise prefixed with TYPO3_REQUEST_DIR (relative to current dir / TYPO3_REQUEST_DIR)
2384 *
2385 * @param string $path URL / path to prepend full URL addressing to.
2386 * @return string
2387 */
2388 public static function locationHeaderUrl($path)
2389 {
2390 if (strpos($path, '//') === 0) {
2391 return $path;
2392 }
2393
2394 // relative to HOST
2395 if (strpos($path, '/') === 0) {
2396 return self::getIndpEnv('TYPO3_REQUEST_HOST') . $path;
2397 }
2398
2399 $urlComponents = parse_url($path);
2400 if (!($urlComponents['scheme'] ?? false)) {
2401 // No scheme either
2402 return self::getIndpEnv('TYPO3_REQUEST_DIR') . $path;
2403 }
2404
2405 return $path;
2406 }
2407
2408 /**
2409 * Returns the maximum upload size for a file that is allowed. Measured in KB.
2410 * This might be handy to find out the real upload limit that is possible for this
2411 * TYPO3 installation.
2412 *
2413 * @return int The maximum size of uploads that are allowed (measured in kilobytes)
2414 */
2415 public static function getMaxUploadFileSize()
2416 {
2417 // Check for PHP restrictions of the maximum size of one of the $_FILES
2418 $phpUploadLimit = self::getBytesFromSizeMeasurement(ini_get('upload_max_filesize'));
2419 // Check for PHP restrictions of the maximum $_POST size
2420 $phpPostLimit = self::getBytesFromSizeMeasurement(ini_get('post_max_size'));
2421 // If the total amount of post data is smaller (!) than the upload_max_filesize directive,
2422 // then this is the real limit in PHP
2423 $phpUploadLimit = $phpPostLimit > 0 && $phpPostLimit < $phpUploadLimit ? $phpPostLimit : $phpUploadLimit;
2424 return floor($phpUploadLimit) / 1024;
2425 }
2426
2427 /**
2428 * Gets the bytes value from a measurement string like "100k".
2429 *
2430 * @param string $measurement The measurement (e.g. "100k")
2431 * @return int The bytes value (e.g. 102400)
2432 */
2433 public static function getBytesFromSizeMeasurement($measurement)
2434 {
2435 $bytes = (float)$measurement;
2436 if (stripos($measurement, 'G')) {
2437 $bytes *= 1024 * 1024 * 1024;
2438 } elseif (stripos($measurement, 'M')) {
2439 $bytes *= 1024 * 1024;
2440 } elseif (stripos($measurement, 'K')) {
2441 $bytes *= 1024;
2442 }
2443 return $bytes;
2444 }
2445
2446 /**
2447 * Function for static version numbers on files, based on the filemtime
2448 *
2449 * This will make the filename automatically change when a file is
2450 * changed, and by that re-cached by the browser. If the file does not
2451 * exist physically the original file passed to the function is
2452 * returned without the timestamp.
2453 *
2454 * Behaviour is influenced by the setting
2455 * TYPO3_CONF_VARS[TYPO3_MODE][versionNumberInFilename]
2456 * = TRUE (BE) / "embed" (FE) : modify filename
2457 * = FALSE (BE) / "querystring" (FE) : add timestamp as parameter
2458 *
2459 * @param string $file Relative path to file including all potential query parameters (not htmlspecialchared yet)
2460 * @return string Relative path with version filename including the timestamp
2461 */
2462 public static function createVersionNumberedFilename($file)
2463 {
2464 $lookupFile = explode('?', $file);
2465 $path = self::resolveBackPath(self::dirname(Environment::getCurrentScript()) . '/' . $lookupFile[0]);
2466
2467 $doNothing = false;
2468 if (TYPO3_MODE === 'FE') {
2469 $mode = strtolower($GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['versionNumberInFilename']);
2470 if ($mode === 'embed') {
2471 $mode = true;
2472 } else {
2473 if ($mode === 'querystring') {
2474 $mode = false;
2475 } else {
2476 $doNothing = true;
2477 }
2478 }
2479 } else {
2480 $mode = $GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['versionNumberInFilename'];
2481 }
2482 if ($doNothing || !file_exists($path)) {
2483 // File not found, return filename unaltered
2484 $fullName = $file;
2485 } else {
2486 if (!$mode) {
2487 // If use of .htaccess rule is not configured,
2488 // we use the default query-string method
2489 if (!empty($lookupFile[1])) {
2490 $separator = '&';
2491 } else {
2492 $separator = '?';
2493 }
2494 $fullName = $file . $separator . filemtime($path);
2495 } else {
2496 // Change the filename
2497 $name = explode('.', $lookupFile[0]);
2498 $extension = array_pop($name);
2499 array_push($name, filemtime($path), $extension);
2500 $fullName = implode('.', $name);
2501 // Append potential query string
2502 $fullName .= $lookupFile[1] ? '?' . $lookupFile[1] : '';
2503 }
2504 }
2505 return $fullName;
2506 }
2507
2508 /**
2509 * Writes string to a temporary file named after the md5-hash of the string
2510 * Quite useful for extensions adding their custom built JavaScript during runtime.
2511 *
2512 * @param string $content JavaScript to write to file.
2513 * @return string filename to include in the <script> tag
2514 */
2515 public static function writeJavaScriptContentToTemporaryFile(string $content)
2516 {
2517 $script = 'typo3temp/assets/js/' . GeneralUtility::shortMD5($content) . '.js';
2518 if (!@is_file(Environment::getPublicPath() . '/' . $script)) {
2519 self::writeFileToTypo3tempDir(Environment::getPublicPath() . '/' . $script, $content);
2520 }
2521 return $script;
2522 }
2523
2524 /**
2525 * Writes string to a temporary file named after the md5-hash of the string
2526 * Quite useful for extensions adding their custom built StyleSheet during runtime.
2527 *
2528 * @param string $content CSS styles to write to file.
2529 * @return string filename to include in the <link> tag
2530 */
2531 public static function writeStyleSheetContentToTemporaryFile(string $content)
2532 {
2533 $script = 'typo3temp/assets/css/' . self::shortMD5($content) . '.css';
2534 if (!@is_file(Environment::getPublicPath() . '/' . $script)) {
2535 self::writeFileToTypo3tempDir(Environment::getPublicPath() . '/' . $script, $content);
2536 }
2537 return $script;
2538 }
2539
2540 /*************************
2541 *
2542 * SYSTEM INFORMATION
2543 *
2544 *************************/
2545
2546 /**
2547 * Returns the link-url to the current script.
2548 * In $getParams you can set associative keys corresponding to the GET-vars you wish to add to the URL. If you set them empty, they will remove existing GET-vars from the current URL.
2549 * REMEMBER to always use htmlspecialchars() for content in href-properties to get ampersands converted to entities (XHTML requirement and XSS precaution)
2550 *
2551 * @param array $getParams Array of GET parameters to include
2552 * @return string
2553 */
2554 public static function linkThisScript(array $getParams = [])
2555 {
2556 $parts = self::getIndpEnv('SCRIPT_NAME');
2557 $params = self::_GET();
2558 foreach ($getParams as $key => $value) {
2559 if ($value !== '') {
2560 $params[$key] = $value;
2561 } else {
2562 unset($params[$key]);
2563 }
2564 }
2565 $pString = self::implodeArrayForUrl('', $params);
2566 return $pString ? $parts . '?' . ltrim($pString, '&') : $parts;
2567 }
2568
2569 /**
2570 * Takes a full URL, $url, possibly with a querystring and overlays the $getParams arrays values onto the querystring, packs it all together and returns the URL again.
2571 * So basically it adds the parameters in $getParams to an existing URL, $url
2572 *
2573 * @param string $url URL string
2574 * @param array $getParams Array of key/value pairs for get parameters to add/overrule with. Can be multidimensional.
2575 * @return string Output URL with added getParams.
2576 */
2577 public static function linkThisUrl($url, array $getParams = [])
2578 {
2579 $parts = parse_url($url);
2580 $getP = [];
2581 if ($parts['query']) {
2582 parse_str($parts['query'], $getP);
2583 }
2584 ArrayUtility::mergeRecursiveWithOverrule($getP, $getParams);
2585 $uP = explode('?', $url);
2586 $params = self::implodeArrayForUrl('', $getP);
2587 $outurl = $uP[0] . ($params ? '?' . substr($params, 1) : '');
2588 return $outurl;
2589 }
2590
2591 /**
2592 * This method is only for testing and should never be used outside tests-
2593 *
2594 * @param $envName
2595 * @param $value
2596 * @internal
2597 */
2598 public static function setIndpEnv($envName, $value)
2599 {
2600 self::$indpEnvCache[$envName] = $value;
2601 }
2602
2603 /**
2604 * Abstraction method which returns System Environment Variables regardless of server OS, CGI/MODULE version etc. Basically this is SERVER variables for most of them.
2605 * This should be used instead of getEnv() and $_SERVER/ENV_VARS to get reliable values for all situations.
2606 *
2607 * @param string $getEnvName Name of the "environment variable"/"server variable" you wish to use. Valid values are SCRIPT_NAME, SCRIPT_FILENAME, REQUEST_URI, PATH_INFO, REMOTE_ADDR, REMOTE_HOST, HTTP_REFERER, HTTP_HOST, HTTP_USER_AGENT, HTTP_ACCEPT_LANGUAGE, QUERY_STRING, TYPO3_DOCUMENT_ROOT, TYPO3_HOST_ONLY, TYPO3_HOST_ONLY, TYPO3_REQUEST_HOST, TYPO3_REQUEST_URL, TYPO3_REQUEST_SCRIPT, TYPO3_REQUEST_DIR, TYPO3_SITE_URL, _ARRAY
2608 * @return string Value based on the input key, independent of server/os environment.
2609 * @throws \UnexpectedValueException
2610 */
2611 public static function getIndpEnv($getEnvName)
2612 {
2613 if (array_key_exists($getEnvName, self::$indpEnvCache)) {
2614 return self::$indpEnvCache[$getEnvName];
2615 }
2616
2617 /*
2618 Conventions:
2619 output from parse_url():
2620 URL: http://username:password@192.168.1.4:8080/typo3/32/temp/phpcheck/index.php/arg1/arg2/arg3/?arg1,arg2,arg3&p1=parameter1&p2[key]=value#link1
2621 [scheme] => 'http'
2622 [user] => 'username'
2623 [pass] => 'password'
2624 [host] => '192.168.1.4'
2625 [port] => '8080'
2626 [path] => '/typo3/32/temp/phpcheck/index.php/arg1/arg2/arg3/'
2627 [query] => 'arg1,arg2,arg3&p1=parameter1&p2[key]=value'
2628 [fragment] => 'link1'Further definition: [path_script] = '/typo3/32/temp/phpcheck/index.php'
2629 [path_dir] = '/typo3/32/temp/phpcheck/'
2630 [path_info] = '/arg1/arg2/arg3/'
2631 [path] = [path_script/path_dir][path_info]Keys supported:URI______:
2632 REQUEST_URI = [path]?[query] = /typo3/32/temp/phpcheck/index.php/arg1/arg2/arg3/?arg1,arg2,arg3&p1=parameter1&p2[key]=value
2633 HTTP_HOST = [host][:[port]] = 192.168.1.4:8080
2634 SCRIPT_NAME = [path_script]++ = /typo3/32/temp/phpcheck/index.php // NOTICE THAT SCRIPT_NAME will return the php-script name ALSO. [path_script] may not do that (eg. '/somedir/' may result in SCRIPT_NAME '/somedir/index.php')!
2635 PATH_INFO = [path_info] = /arg1/arg2/arg3/
2636 QUERY_STRING = [query] = arg1,arg2,arg3&p1=parameter1&p2[key]=value
2637 HTTP_REFERER = [scheme]://[host][:[port]][path] = http://192.168.1.4:8080/typo3/32/temp/phpcheck/index.php/arg1/arg2/arg3/?arg1,arg2,arg3&p1=parameter1&p2[key]=value
2638 (Notice: NO username/password + NO fragment)CLIENT____:
2639 REMOTE_ADDR = (client IP)
2640 REMOTE_HOST = (client host)
2641 HTTP_USER_AGENT = (client user agent)
2642 HTTP_ACCEPT_LANGUAGE = (client accept language)SERVER____:
2643 SCRIPT_FILENAME = Absolute filename of script (Differs between windows/unix). On windows 'C:\\blabla\\blabl\\' will be converted to 'C:/blabla/blabl/'Special extras:
2644 TYPO3_HOST_ONLY = [host] = 192.168.1.4
2645 TYPO3_PORT = [port] = 8080 (blank if 80, taken from host value)
2646 TYPO3_REQUEST_HOST = [scheme]://[host][:[port]]
2647 TYPO3_REQUEST_URL = [scheme]://[host][:[port]][path]?[query] (scheme will by default be "http" until we can detect something different)
2648 TYPO3_REQUEST_SCRIPT = [scheme]://[host][:[port]][path_script]
2649 TYPO3_REQUEST_DIR = [scheme]://[host][:[port]][path_dir]
2650 TYPO3_SITE_URL = [scheme]://[host][:[port]][path_dir] of the TYPO3 website frontend
2651 TYPO3_SITE_PATH = [path_dir] of the TYPO3 website frontend
2652 TYPO3_SITE_SCRIPT = [script / Speaking URL] of the TYPO3 website
2653 TYPO3_DOCUMENT_ROOT = Absolute path of root of documents: TYPO3_DOCUMENT_ROOT.SCRIPT_NAME = SCRIPT_FILENAME (typically)
2654 TYPO3_SSL = Returns TRUE if this session uses SSL/TLS (https)
2655 TYPO3_PROXY = Returns TRUE if this session runs over a well known proxyNotice: [fragment] is apparently NEVER available to the script!Testing suggestions:
2656 - Output all the values.
2657 - In the script, make a link to the script it self, maybe add some parameters and click the link a few times so HTTP_REFERER is seen
2658 - ALSO TRY the script from the ROOT of a site (like 'http://www.mytest.com/' and not 'http://www.mytest.com/test/' !!)
2659 */
2660 $retVal = '';
2661 switch ((string)$getEnvName) {
2662 case 'SCRIPT_NAME':
2663 $retVal = self::isRunningOnCgiServerApi()
2664 && (($_SERVER['ORIG_PATH_INFO'] ?? false) ?: ($_SERVER['PATH_INFO'] ?? false))
2665 ? (($_SERVER['ORIG_PATH_INFO'] ?? '') ?: ($_SERVER['PATH_INFO'] ?? ''))
2666 : (($_SERVER['ORIG_SCRIPT_NAME'] ?? '') ?: ($_SERVER['SCRIPT_NAME'] ?? ''));
2667 // Add a prefix if TYPO3 is behind a proxy: ext-domain.com => int-server.com/prefix
2668 if (self::cmpIP($_SERVER['REMOTE_ADDR'] ?? '', $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP'] ?? '')) {
2669 if (self::getIndpEnv('TYPO3_SSL') && $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefixSSL']) {
2670 $retVal = $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefixSSL'] . $retVal;
2671 } elseif ($GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefix']) {
2672 $retVal = $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefix'] . $retVal;
2673 }
2674 }
2675 break;
2676 case 'SCRIPT_FILENAME':
2677 $retVal = Environment::getCurrentScript();
2678 break;
2679 case 'REQUEST_URI':
2680 // Typical application of REQUEST_URI is return urls, forms submitting to itself etc. Example: returnUrl='.rawurlencode(\TYPO3\CMS\Core\Utility\GeneralUtility::getIndpEnv('REQUEST_URI'))
2681 if (!empty($GLOBALS['TYPO3_CONF_VARS']['SYS']['requestURIvar'])) {
2682 // This is for URL rewriters that store the original URI in a server variable (eg ISAPI_Rewriter for IIS: HTTP_X_REWRITE_URL)
2683 list($v, $n) = explode('|', $GLOBALS['TYPO3_CONF_VARS']['SYS']['requestURIvar']);
2684 $retVal = $GLOBALS[$v][$n];
2685 } elseif (empty($_SERVER['REQUEST_URI'])) {
2686 // This is for ISS/CGI which does not have the REQUEST_URI available.
2687 $retVal = '/' . ltrim(self::getIndpEnv('SCRIPT_NAME'), '/') . (!empty($_SERVER['QUERY_STRING']) ? '?' . $_SERVER['QUERY_STRING'] : '');
2688 } else {
2689 $retVal = '/' . ltrim($_SERVER['REQUEST_URI'], '/');
2690 }
2691 // Add a prefix if TYPO3 is behind a proxy: ext-domain.com => int-server.com/prefix
2692 if (isset($_SERVER['REMOTE_ADDR'], $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP'])
2693 && self::cmpIP($_SERVER['REMOTE_ADDR'], $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP'])
2694 ) {
2695 if (self::getIndpEnv('TYPO3_SSL') && $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefixSSL']) {
2696 $retVal = $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefixSSL'] . $retVal;
2697 } elseif ($GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefix']) {
2698 $retVal = $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefix'] . $retVal;
2699 }
2700 }
2701 break;
2702 case 'PATH_INFO':
2703 // $_SERVER['PATH_INFO'] != $_SERVER['SCRIPT_NAME'] is necessary because some servers (Windows/CGI)
2704 // are seen to set PATH_INFO equal to script_name
2705 // Further, there must be at least one '/' in the path - else the PATH_INFO value does not make sense.
2706 // IF 'PATH_INFO' never works for our purpose in TYPO3 with CGI-servers,
2707 // then 'PHP_SAPI=='cgi'' might be a better check.
2708 // Right now strcmp($_SERVER['PATH_INFO'], GeneralUtility::getIndpEnv('SCRIPT_NAME')) will always
2709 // return FALSE for CGI-versions, but that is only as long as SCRIPT_NAME is set equal to PATH_INFO
2710 // because of PHP_SAPI=='cgi' (see above)
2711 if (!self::isRunningOnCgiServerApi()) {
2712 $retVal = $_SERVER['PATH_INFO'];
2713 }
2714 break;
2715 case 'TYPO3_REV_PROXY':
2716 $retVal = self::cmpIP($_SERVER['REMOTE_ADDR'], $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP']);
2717 break;
2718 case 'REMOTE_ADDR':
2719 $retVal = $_SERVER['REMOTE_ADDR'] ?? null;
2720 if (self::cmpIP($retVal, $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP'] ?? '')) {
2721 $ip = self::trimExplode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
2722 // Choose which IP in list to use
2723 if (!empty($ip)) {
2724 switch ($GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyHeaderMultiValue']) {
2725 case 'last':
2726 $ip = array_pop($ip);
2727 break;
2728 case 'first':
2729 $ip = array_shift($ip);
2730 break;
2731 case 'none':
2732
2733 default:
2734 $ip = '';
2735 }
2736 }
2737 if (self::validIP($ip)) {
2738 $retVal = $ip;
2739 }
2740 }
2741 break;
2742 case 'HTTP_HOST':
2743 // if it is not set we're most likely on the cli
2744 $retVal = $_SERVER['HTTP_HOST'] ?? null;
2745 if (isset($_SERVER['REMOTE_ADDR']) && static::cmpIP($_SERVER['REMOTE_ADDR'], $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP'])) {
2746 $host = self::trimExplode(',', $_SERVER['HTTP_X_FORWARDED_HOST']);
2747 // Choose which host in list to use
2748 if (!empty($host)) {
2749 switch ($GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyHeaderMultiValue']) {
2750 case 'last':
2751 $host = array_pop($host);
2752 break;
2753 case 'first':
2754 $host = array_shift($host);
2755 break;
2756 case 'none':
2757
2758 default:
2759 $host = '';
2760 }
2761 }
2762 if ($host) {
2763 $retVal = $host;
2764 }
2765 }
2766 if (!static::isAllowedHostHeaderValue($retVal)) {
2767 throw new \UnexpectedValueException(
2768 'The current host header value does not match the configured trusted hosts pattern! Check the pattern defined in $GLOBALS[\'TYPO3_CONF_VARS\'][\'SYS\'][\'trustedHostsPattern\'] and adapt it, if you want to allow the current host header \'' . $retVal . '\' for your installation.',
2769 1396795884
2770 );
2771 }
2772 break;
2773 case 'HTTP_REFERER':
2774
2775 case 'HTTP_USER_AGENT':
2776
2777 case 'HTTP_ACCEPT_ENCODING':
2778
2779 case 'HTTP_ACCEPT_LANGUAGE':
2780
2781 case 'REMOTE_HOST':
2782
2783 case 'QUERY_STRING':
2784 $retVal = $_SERVER[$getEnvName] ?? '';
2785 break;
2786 case 'TYPO3_DOCUMENT_ROOT':
2787 // Get the web root (it is not the root of the TYPO3 installation)
2788 // The absolute path of the script can be calculated with TYPO3_DOCUMENT_ROOT + SCRIPT_FILENAME
2789 // Some CGI-versions (LA13CGI) and mod-rewrite rules on MODULE versions will deliver a 'wrong' DOCUMENT_ROOT (according to our description). Further various aliases/mod_rewrite rules can disturb this as well.
2790 // Therefore the DOCUMENT_ROOT is now always calculated as the SCRIPT_FILENAME minus the end part shared with SCRIPT_NAME.
2791 $SFN = self::getIndpEnv('SCRIPT_FILENAME');
2792 $SN_A = explode('/', strrev(self::getIndpEnv('SCRIPT_NAME')));
2793 $SFN_A = explode('/', strrev($SFN));
2794 $acc = [];
2795 foreach ($SN_A as $kk => $vv) {
2796 if ((string)$SFN_A[$kk] === (string)$vv) {
2797 $acc[] = $vv;
2798 } else {
2799 break;
2800 }
2801 }
2802 $commonEnd = strrev(implode('/', $acc));
2803 if ((string)$commonEnd !== '') {
2804 $retVal = substr($SFN, 0, -(strlen($commonEnd) + 1));
2805 }
2806 break;
2807 case 'TYPO3_HOST_ONLY':
2808 $httpHost = self::getIndpEnv('HTTP_HOST');
2809 $httpHostBracketPosition = strpos($httpHost, ']');
2810 $httpHostParts = explode(':', $httpHost);
2811 $retVal = $httpHostBracketPosition !== false ? substr($httpHost, 0, $httpHostBracketPosition + 1) : array_shift($httpHostParts);
2812 break;
2813 case 'TYPO3_PORT':
2814 $httpHost = self::getIndpEnv('HTTP_HOST');
2815 $httpHostOnly = self::getIndpEnv('TYPO3_HOST_ONLY');
2816 $retVal = strlen($httpHost) > strlen($httpHostOnly) ? substr($httpHost, strlen($httpHostOnly) + 1) : '';
2817 break;
2818 case 'TYPO3_REQUEST_HOST':
2819 $retVal = (self::getIndpEnv('TYPO3_SSL') ? 'https://' : 'http://') . self::getIndpEnv('HTTP_HOST');
2820 break;
2821 case 'TYPO3_REQUEST_URL':
2822 $retVal = self::getIndpEnv('TYPO3_REQUEST_HOST') . self::getIndpEnv('REQUEST_URI');
2823 break;
2824 case 'TYPO3_REQUEST_SCRIPT':
2825 $retVal = self::getIndpEnv('TYPO3_REQUEST_HOST') . self::getIndpEnv('SCRIPT_NAME');
2826 break;
2827 case 'TYPO3_REQUEST_DIR':
2828 $retVal = self::getIndpEnv('TYPO3_REQUEST_HOST') . self::dirname(self::getIndpEnv('SCRIPT_NAME')) . '/';
2829 break;
2830 case 'TYPO3_SITE_URL':
2831 $url = self::getIndpEnv('TYPO3_REQUEST_DIR');
2832 // This can only be set by external entry scripts
2833 if (defined('TYPO3_PATH_WEB')) {
2834 $retVal = $url;
2835 } elseif (Environment::getCurrentScript()) {
2836 $lPath = PathUtility::stripPathSitePrefix(PathUtility::dirnameDuringBootstrap(Environment::getCurrentScript())) . '/';
2837 $siteUrl = substr($url, 0, -strlen($lPath));
2838 if (substr($siteUrl, -1) !== '/') {
2839 $siteUrl .= '/';
2840 }
2841 $retVal = $siteUrl;
2842 }
2843 break;
2844 case 'TYPO3_SITE_PATH':
2845 $retVal = substr(self::getIndpEnv('TYPO3_SITE_URL'), strlen(self::getIndpEnv('TYPO3_REQUEST_HOST')));
2846 break;
2847 case 'TYPO3_SITE_SCRIPT':
2848 $retVal = substr(self::getIndpEnv('TYPO3_REQUEST_URL'), strlen(self::getIndpEnv('TYPO3_SITE_URL')));
2849 break;
2850 case 'TYPO3_SSL':
2851 $proxySSL = trim($GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxySSL'] ?? null);
2852 if ($proxySSL === '*') {
2853 $proxySSL = $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP'];
2854 }
2855 if (self::cmpIP($_SERVER['REMOTE_ADDR'] ?? '', $proxySSL)) {
2856 $retVal = true;
2857 } else {
2858 // https://secure.php.net/manual/en/reserved.variables.server.php
2859 // "Set to a non-empty value if the script was queried through the HTTPS protocol."
2860 $retVal = !empty($_SERVER['SSL_SESSION_ID'])
2861 || (!empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) !== 'off');
2862 }
2863 break;
2864 case '_ARRAY':
2865 $out = [];
2866 // Here, list ALL possible keys to this function for debug display.
2867 $envTestVars = [
2868 'HTTP_HOST',
2869 'TYPO3_HOST_ONLY',
2870 'TYPO3_PORT',
2871 'PATH_INFO',
2872 'QUERY_STRING',
2873 'REQUEST_URI',
2874 'HTTP_REFERER',
2875 'TYPO3_REQUEST_HOST',
2876 'TYPO3_REQUEST_URL',
2877 'TYPO3_REQUEST_SCRIPT',
2878 'TYPO3_REQUEST_DIR',
2879 'TYPO3_SITE_URL',
2880 'TYPO3_SITE_SCRIPT',
2881 'TYPO3_SSL',
2882 'TYPO3_REV_PROXY',
2883 'SCRIPT_NAME',
2884 'TYPO3_DOCUMENT_ROOT',
2885 'SCRIPT_FILENAME',
2886 'REMOTE_ADDR',
2887 'REMOTE_HOST',
2888 'HTTP_USER_AGENT',
2889 'HTTP_ACCEPT_LANGUAGE'
2890 ];
2891 foreach ($envTestVars as $v) {
2892 $out[$v] = self::getIndpEnv($v);
2893 }
2894 reset($out);
2895 $retVal = $out;
2896 break;
2897 }
2898 self::$indpEnvCache[$getEnvName] = $retVal;
2899 return $retVal;
2900 }
2901
2902 /**
2903 * Checks if the provided host header value matches the trusted hosts pattern.
2904 * If the pattern is not defined (which only can happen early in the bootstrap), deny any value.
2905 * The result is saved, so the check needs to be executed only once.
2906 *
2907 * @param string $hostHeaderValue HTTP_HOST header value as sent during the request (may include port)
2908 * @return bool
2909 */
2910 public static function isAllowedHostHeaderValue($hostHeaderValue)
2911 {
2912 if (static::$allowHostHeaderValue === true) {
2913 return true;
2914 }
2915
2916 if (static::isInternalRequestType()) {
2917 return static::$allowHostHeaderValue = true;
2918 }
2919
2920 // Deny the value if trusted host patterns is empty, which means we are early in the bootstrap
2921 if (empty($GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern'])) {
2922 return false;
2923 }
2924
2925 if ($GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern'] === self::ENV_TRUSTED_HOSTS_PATTERN_ALLOW_ALL) {
2926 static::$allowHostHeaderValue = true;
2927 } else {
2928 static::$allowHostHeaderValue = static::hostHeaderValueMatchesTrustedHostsPattern($hostHeaderValue);
2929 }
2930
2931 return static::$allowHostHeaderValue;
2932 }
2933
2934 /**
2935 * Checks if the provided host header value matches the trusted hosts pattern without any preprocessing.
2936 *
2937 * @param string $hostHeaderValue
2938 * @return bool
2939 * @internal
2940 */
2941 public static function hostHeaderValueMatchesTrustedHostsPattern($hostHeaderValue)
2942 {
2943 if ($GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern'] === self::ENV_TRUSTED_HOSTS_PATTERN_SERVER_NAME) {
2944 // Allow values that equal the server name
2945 // Note that this is only secure if name base virtual host are configured correctly in the webserver
2946 $defaultPort = self::getIndpEnv('TYPO3_SSL') ? '443' : '80';
2947 $parsedHostValue = parse_url('http://' . $hostHeaderValue);
2948 if (isset($parsedHostValue['port'])) {
2949 $hostMatch = (strtolower($parsedHostValue['host']) === strtolower($_SERVER['SERVER_NAME']) && (string)$parsedHostValue['port'] === $_SERVER['SERVER_PORT']);
2950 } else {
2951 $hostMatch = (strtolower($hostHeaderValue) === strtolower($_SERVER['SERVER_NAME']) && $defaultPort === $_SERVER['SERVER_PORT']);
2952 }
2953 } else {
2954 // In case name based virtual hosts are not possible, we allow setting a trusted host pattern
2955 // See https://typo3.org/teams/security/security-bulletins/typo3-core/typo3-core-sa-2014-001/ for further details
2956 $hostMatch = (bool)preg_match('/^' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern'] . '$/i', $hostHeaderValue);
2957 }
2958
2959 return $hostMatch;
2960 }
2961
2962 /**
2963 * Allows internal requests to the install tool and from the command line.
2964 * We accept this risk to have the install tool always available.
2965 * Also CLI needs to be allowed as unfortunately AbstractUserAuthentication::getAuthInfoArray()
2966 * accesses HTTP_HOST without reason on CLI
2967 * Additionally, allows requests when no REQUESTTYPE is set, which can happen quite early in the
2968 * Bootstrap. See Application.php in EXT:backend/Classes/Http/.
2969 *
2970 * @return bool
2971 */
2972 protected static function isInternalRequestType()
2973 {
2974 return Environment::isCli() || !defined('TYPO3_REQUESTTYPE') || (defined('TYPO3_REQUESTTYPE') && TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_INSTALL);
2975 }
2976
2977 /**
2978 * Gets the unixtime as milliseconds.
2979 *
2980 * @return int The unixtime as milliseconds
2981 */
2982 public static function milliseconds()
2983 {
2984 return round(microtime(true) * 1000);
2985 }
2986
2987 /*************************
2988 *
2989 * TYPO3 SPECIFIC FUNCTIONS
2990 *
2991 *************************/
2992 /**
2993 * Returns the absolute filename of a relative reference, resolves the "EXT:" prefix
2994 * (way of referring to files inside extensions) and checks that the file is inside
2995 * the TYPO3's base folder and implies a check with
2996 * \TYPO3\CMS\Core\Utility\GeneralUtility::validPathStr().
2997 *
2998 * @param string $filename The input filename/filepath to evaluate
2999 * @return string Returns the absolute filename of $filename if valid, otherwise blank string.
3000 */
3001 public static function getFileAbsFileName($filename)
3002 {
3003 if ((string)$filename === '') {
3004 return '';
3005 }
3006 // Extension
3007 if (strpos($filename, 'EXT:') === 0) {
3008 list($extKey, $local) = explode('/', substr($filename, 4), 2);
3009 $filename = '';
3010 if ((string)$extKey !== '' && ExtensionManagementUtility::isLoaded($extKey) && (string)$local !== '') {
3011 $filename = ExtensionManagementUtility::extPath($extKey) . $local;
3012 }
3013 } elseif (!static::isAbsPath($filename)) {
3014 // is relative. Prepended with the public web folder
3015 $filename = Environment::getPublicPath() . '/' . $filename;
3016 } elseif (!(
3017 static::isFirstPartOfStr($filename, Environment::getProjectPath())
3018 || static::isFirstPartOfStr($filename, Environment::getPublicPath())
3019 )) {
3020 // absolute, but set to blank if not allowed
3021 $filename = '';
3022 }
3023 if ((string)$filename !== '' && static::validPathStr($filename)) {
3024 // checks backpath.
3025 return $filename;
3026 }
3027 return '';
3028 }
3029
3030 /**
3031 * Checks for malicious file paths.
3032 *
3033 * Returns TRUE if no '//', '..', '\' or control characters are found in the $theFile.
3034 * This should make sure that the path is not pointing 'backwards' and further doesn't contain double/back slashes.
3035 * So it's compatible with the UNIX style path strings valid for TYPO3 internally.
3036 *
3037 * @param string $theFile File path to evaluate
3038 * @return bool TRUE, $theFile is allowed path string, FALSE otherwise
3039 * @see http://php.net/manual/en/security.filesystem.nullbytes.php
3040 */
3041 public static function validPathStr($theFile)
3042 {
3043 return strpos($theFile, '//') === false && strpos($theFile, '\\') === false
3044 && preg_match('#(?:^\\.\\.|/\\.\\./|[[:cntrl:]])#u', $theFile) === 0;
3045 }
3046
3047 /**
3048 * Checks if the $path is absolute or relative (detecting either '/' or 'x:/' as first part of string) and returns TRUE if so.
3049 *
3050 * @param string $path File path to evaluate
3051 * @return bool
3052 */
3053 public static function isAbsPath($path)
3054 {
3055 return isset($path[0]) && $path[0] === '/' || Environment::isWindows() && (strpos($path, ':/') === 1 || strpos($path, ':\\') === 1);
3056 }
3057
3058 /**
3059 * Returns TRUE if the path is absolute, without backpath '..' and within TYPO3s project or public folder OR within the lockRootPath
3060 *
3061 * @param string $path File path to evaluate
3062 * @return bool
3063 */
3064 public static function isAllowedAbsPath($path)
3065 {
3066 $lockRootPath = $GLOBALS['TYPO3_CONF_VARS']['BE']['lockRootPath'];
3067 return static::isAbsPath($path) && static::validPathStr($path)
3068 && (
3069 static::isFirstPartOfStr($path, Environment::getProjectPath())
3070 || static::isFirstPartOfStr($path, Environment::getPublicPath())
3071 || $lockRootPath && static::isFirstPartOfStr($path, $lockRootPath)
3072 );
3073 }
3074
3075 /**
3076 * Verifies the input filename against the 'fileDenyPattern'. Returns TRUE if OK.
3077 *
3078 * Filenames are not allowed to contain control characters. Therefore we
3079 * always filter on [[:cntrl:]].
3080 *
3081 * @param string $filename File path to evaluate
3082 * @return bool
3083 */
3084 public static function verifyFilenameAgainstDenyPattern($filename)
3085 {
3086 $pattern = '/[[:cntrl:]]/';
3087 if ((string)$filename !== '' && (string)$GLOBALS['TYPO3_CONF_VARS']['BE']['fileDenyPattern'] !== '') {
3088 $pattern = '/(?:[[:cntrl:]]|' . $GLOBALS['TYPO3_CONF_VARS']['BE']['fileDenyPattern'] . ')/iu';
3089 }
3090 return preg_match($pattern, $filename) === 0;
3091 }
3092
3093 /**
3094 * Low level utility function to copy directories and content recursive
3095 *
3096 * @param string $source Path to source directory, relative to document root or absolute
3097 * @param string $destination Path to destination directory, relative to document root or absolute
3098 */
3099 public static function copyDirectory($source, $destination)
3100 {
3101 if (strpos($source, Environment::getProjectPath() . '/') === false) {
3102 $source = Environment::getPublicPath() . '/' . $source;
3103 }
3104 if (strpos($destination, Environment::getProjectPath() . '/') === false) {
3105 $destination = Environment::getPublicPath() . '/' . $destination;
3106 }
3107 if (static::isAllowedAbsPath($source) && static::isAllowedAbsPath($destination)) {
3108 static::mkdir_deep($destination);
3109 $iterator = new \RecursiveIteratorIterator(
3110 new \RecursiveDirectoryIterator($source, \RecursiveDirectoryIterator::SKIP_DOTS),
3111 \RecursiveIteratorIterator::SELF_FIRST
3112 );
3113 /** @var \SplFileInfo $item */
3114 foreach ($iterator as $item) {
3115 $target = $destination . '/' . static::fixWindowsFilePath($iterator->getSubPathName());
3116 if ($item->isDir()) {
3117 static::mkdir($target);
3118 } else {
3119 static::upload_copy_move(static::fixWindowsFilePath($item->getPathname()), $target);
3120 }
3121 }
3122 }
3123 }
3124
3125 /**
3126 * Checks if a given string is a valid frame URL to be loaded in the
3127 * backend.
3128 *
3129 * If the given url is empty or considered to be harmless, it is returned
3130 * as is, else the event is logged and an empty string is returned.
3131 *
3132 * @param string $url potential URL to check
3133 * @return string $url or empty string
3134 */
3135 public static function sanitizeLocalUrl($url = '')
3136 {
3137 $sanitizedUrl = '';
3138 if (!empty($url)) {
3139 $decodedUrl = rawurldecode($url);
3140 $parsedUrl = parse_url($decodedUrl);
3141 $testAbsoluteUrl = self::resolveBackPath($decodedUrl);
3142 $testRelativeUrl = self::resolveBackPath(self::dirname(self::getIndpEnv('SCRIPT_NAME')) . '/' . $decodedUrl);
3143 // Pass if URL is on the current host:
3144 if (self::isValidUrl($decodedUrl)) {
3145 if (self::isOnCurrentHost($decodedUrl) && strpos($decodedUrl, self::getIndpEnv('TYPO3_SITE_URL')) === 0) {
3146 $sanitizedUrl = $url;
3147 }
3148 } elseif (self::isAbsPath($decodedUrl) && self::isAllowedAbsPath($decodedUrl)) {
3149 $sanitizedUrl = $url;
3150 } elseif (strpos($testAbsoluteUrl, self::getIndpEnv('TYPO3_SITE_PATH')) === 0 && $decodedUrl[0] === '/') {
3151 $sanitizedUrl = $url;
3152 } elseif (empty($parsedUrl['scheme']) && strpos($testRelativeUrl, self::getIndpEnv('TYPO3_SITE_PATH')) === 0
3153 && $decodedUrl[0] !== '/' && strpbrk($decodedUrl, '*:|"<>') === false && strpos($decodedUrl, '\\\\') === false
3154 ) {
3155 $sanitizedUrl = $url;
3156 }
3157 }
3158 if (!empty($url) && empty($sanitizedUrl)) {
3159 static::getLogger()->notice('The URL "' . $url . '" is not considered to be local and was denied.');
3160 }
3161 return $sanitizedUrl;
3162 }
3163
3164 /**
3165 * Moves $source file to $destination if uploaded, otherwise try to make a copy
3166 *
3167 * @param string $source Source file, absolute path
3168 * @param string $destination Destination file, absolute path
3169 * @return bool Returns TRUE if the file was moved.
3170 * @see upload_to_tempfile()
3171 */
3172 public static function upload_copy_move($source, $destination)
3173 {
3174 if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\CMS\Core\Utility\GeneralUtility']['moveUploadedFile'] ?? null)) {
3175 $params = ['source' => $source, 'destination' => $destination, 'method' => 'upload_copy_move'];
3176 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\CMS\Core\Utility\GeneralUtility']['moveUploadedFile'] as $hookMethod) {
3177 $fakeThis = false;
3178 self::callUserFunction($hookMethod, $params, $fakeThis);
3179 }
3180 }
3181
3182 $result = false;
3183 if (is_uploaded_file($source)) {
3184 // Return the value of move_uploaded_file, and if FALSE the temporary $source is still
3185 // around so the user can use unlink to delete it:
3186 $result = move_uploaded_file($source, $destination);
3187 } else {
3188 @copy($source, $destination);
3189 }
3190 // Change the permissions of the file
3191 self::fixPermissions($destination);
3192 // If here the file is copied and the temporary $source is still around,
3193 // so when returning FALSE the user can try unlink to delete the $source
3194 return $result;
3195 }
3196
3197 /**
3198 * Will move an uploaded file (normally in "/tmp/xxxxx") to a temporary filename in Environment::getProjectPath() . "var/" from where TYPO3 can use it.
3199 * Use this function to move uploaded files to where you can work on them.
3200 * REMEMBER to use \TYPO3\CMS\Core\Utility\GeneralUtility::unlink_tempfile() afterwards - otherwise temp-files will build up! They are NOT automatically deleted in the temporary folder!
3201 *
3202 * @param string $uploadedFileName The temporary uploaded filename, eg. $_FILES['[upload field name here]']['tmp_name']
3203 * @return string If a new file was successfully created, return its filename, otherwise blank string.
3204 * @see unlink_tempfile(), upload_copy_move()
3205 */
3206 public static function upload_to_tempfile($uploadedFileName)
3207 {
3208 if (is_uploaded_file($uploadedFileName)) {
3209 $tempFile = self::tempnam('upload_temp_');
3210 if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\CMS\Core\Utility\GeneralUtility']['moveUploadedFile'] ?? null)) {
3211 $params = ['source' => $uploadedFileName, 'destination' => $tempFile, 'method' => 'upload_to_tempfile'];
3212 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\CMS\Core\Utility\GeneralUtility']['moveUploadedFile'] as $hookMethod) {
3213 $fakeThis = false;
3214 self::callUserFunction($hookMethod, $params, $fakeThis);
3215 }
3216 }
3217
3218 move_uploaded_file($uploadedFileName, $tempFile);
3219 return @is_file($tempFile) ? $tempFile : '';
3220 }
3221 }
3222
3223 /**
3224 * Deletes (unlink) a temporary filename in the var/ or typo3temp folder given as input.
3225 * The function will check that the file exists, is within TYPO3's var/ or typo3temp/ folder and does not contain back-spaces ("../") so it should be pretty safe.
3226 * Use this after upload_to_tempfile() or tempnam() from this class!
3227 *
3228 * @param string $uploadedTempFileName absolute file path - must reside within var/ or typo3temp/ folder.
3229 * @return bool Returns TRUE if the file was unlink()'ed
3230 * @see upload_to_tempfile(), tempnam()
3231 */
3232 public static function unlink_tempfile($uploadedTempFileName)
3233 {
3234 if ($uploadedTempFileName) {
3235 $uploadedTempFileName = self::fixWindowsFilePath($uploadedTempFileName);
3236 if (
3237 self::validPathStr($uploadedTempFileName)
3238 && (
3239 self::isFirstPartOfStr($uploadedTempFileName, Environment::getPublicPath() . '/typo3temp/')
3240 || self::isFirstPartOfStr($uploadedTempFileName, Environment::getVarPath() . '/')
3241 )
3242 && @is_file($uploadedTempFileName)
3243 ) {
3244 if (unlink($uploadedTempFileName)) {
3245 return true;
3246 }
3247 }
3248 }
3249 }
3250
3251 /**
3252 * Create temporary filename (Create file with unique file name)
3253 * This function should be used for getting temporary file names - will make your applications safe for open_basedir = on
3254 * REMEMBER to delete the temporary files after use! This is done by \TYPO3\CMS\Core\Utility\GeneralUtility::unlink_tempfile()
3255 *
3256 * @param string $filePrefix Prefix for temporary file
3257 * @param string $fileSuffix Suffix for temporary file, for example a special file extension
3258 * @return string result from PHP function tempnam() with the temp/var folder prefixed.
3259 * @see unlink_tempfile(), upload_to_tempfile()
3260 */
3261 public static function tempnam($filePrefix, $fileSuffix = '')
3262 {
3263 $temporaryPath = Environment::getVarPath() . '/transient/';
3264 if (!is_dir($temporaryPath)) {
3265 self::mkdir_deep($temporaryPath);
3266 }
3267 if ($fileSuffix === '') {
3268 $tempFileName = $temporaryPath . PathUtility::basename(tempnam($temporaryPath, $filePrefix));
3269 } else {
3270 do {
3271 $tempFileName = $temporaryPath . $filePrefix . mt_rand(1, PHP_INT_MAX) . $fileSuffix;
3272 } while (file_exists($tempFileName));
3273 touch($tempFileName);
3274 clearstatcache(null, $tempFileName);
3275 }
3276 return $tempFileName;
3277 }
3278
3279 /**
3280 * Standard authentication code (used in Direct Mail, checkJumpUrl and setfixed links computations)
3281 *
3282 * @param mixed $uid_or_record Uid (int) or record (array)
3283 * @param string $fields List of fields from the record if that is given.
3284 * @param int $codeLength Length of returned authentication code.
3285 * @return string MD5 hash of 8 chars.
3286 */
3287 public static function stdAuthCode($uid_or_record, $fields = '', $codeLength = 8)
3288 {
3289 if (is_array($uid_or_record)) {
3290 $recCopy_temp = [];
3291 if ($fields) {
3292 $fieldArr = self::trimExplode(',', $fields, true);
3293 foreach ($fieldArr as $k => $v) {
3294 $recCopy_temp[$k] = $uid_or_record[$v];
3295 }
3296 } else {
3297 $recCopy_temp = $uid_or_record;
3298 }
3299 $preKey = implode('|', $recCopy_temp);
3300 } else {
3301 $preKey = $uid_or_record;
3302 }
3303 $authCode = $preKey . '||' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'];
3304 $authCode = substr(md5($authCode), 0, $codeLength);
3305 return $authCode;
3306 }
3307
3308 /**
3309 * Responds on input localization setting value whether the page it comes from should be hidden if no translation exists or not.
3310 *
3311 * @param int $l18n_cfg_fieldValue Value from "l18n_cfg" field of a page record
3312 * @return bool TRUE if the page should be hidden
3313 */
3314 public static function hideIfNotTranslated($l18n_cfg_fieldValue)
3315 {
3316 return $GLOBALS['TYPO3_CONF_VARS']['FE']['hidePagesIfNotTranslatedByDefault'] xor ($l18n_cfg_fieldValue & 2);
3317 }
3318
3319 /**
3320 * Returns true if the "l18n_cfg" field value is not set to hide
3321 * pages in the default language
3322 *
3323 * @param int $localizationConfiguration
3324 * @return bool
3325 */
3326 public static function hideIfDefaultLanguage($localizationConfiguration)
3327 {
3328 return (bool)($localizationConfiguration & 1);
3329 }
3330
3331 /**
3332 * Calls a user-defined function/method in class
3333 * Such a function/method should look like this: "function proc(&$params, &$ref) {...}"
3334 *
3335 * @param string $funcName Function/Method reference or Closure.
3336 * @param mixed $params Parameters to be pass along (typically an array) (REFERENCE!)
3337 * @param mixed $ref Reference to be passed along (typically "$this" - being a reference to the calling object) (REFERENCE!)
3338 * @return mixed Content from method/function call
3339 * @throws \InvalidArgumentException
3340 */
3341 public static function callUserFunction($funcName, &$params, &$ref)
3342 {
3343 // Check if we're using a closure and invoke it directly.
3344 if (is_object($funcName) && is_a($funcName, 'Closure')) {
3345 return call_user_func_array($funcName, [&$params, &$ref]);
3346 }
3347 $funcName = trim($funcName);
3348 $parts = explode('->', $funcName);
3349 // Call function or method
3350 if (count($parts) === 2) {
3351 // It's a class/method
3352 // Check if class/method exists:
3353 if (class_exists($parts[0])) {
3354