[BUGFIX] Fix several typos in php comments
[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, which is equivalent to 'GP' order
124 * In case you already know by which method your data is arriving consider using GeneralUtility::_GET or GeneralUtility::_POST.
125 *
126 * @param string $var GET/POST var to return
127 * @return mixed POST var named $var, if not set, the GET var of the same name and if also not set, NULL.
128 */
129 public static function _GP($var)
130 {
131 if (empty($var)) {
132 return;
133 }
134 if (isset($_POST[$var])) {
135 $value = $_POST[$var];
136 } elseif (isset($_GET[$var])) {
137 $value = $_GET[$var];
138 } else {
139 $value = null;
140 }
141 // This is there for backwards-compatibility, in order to avoid NULL
142 if (isset($value) && !is_array($value)) {
143 $value = (string)$value;
144 }
145 return $value;
146 }
147
148 /**
149 * Returns the global arrays $_GET and $_POST merged with $_POST taking precedence.
150 *
151 * @param string $parameter Key (variable name) from GET or POST vars
152 * @return array Returns the GET vars merged recursively onto the POST vars.
153 */
154 public static function _GPmerged($parameter)
155 {
156 $postParameter = isset($_POST[$parameter]) && is_array($_POST[$parameter]) ? $_POST[$parameter] : [];
157 $getParameter = isset($_GET[$parameter]) && is_array($_GET[$parameter]) ? $_GET[$parameter] : [];
158 $mergedParameters = $getParameter;
159 ArrayUtility::mergeRecursiveWithOverrule($mergedParameters, $postParameter);
160 return $mergedParameters;
161 }
162
163 /**
164 * Returns the global $_GET array (or value from) normalized to contain un-escaped values.
165 * This function was previously used to normalize between magic quotes logic, which was removed from PHP 5.5
166 *
167 * @param string $var Optional pointer to value in GET array (basically name of GET var)
168 * @return mixed If $var is set it returns the value of $_GET[$var]. If $var is NULL (default), returns $_GET itself.
169 * @see _POST()
170 * @see _GP()
171 */
172 public static function _GET($var = null)
173 {
174 $value = $var === null
175 ? $_GET
176 : (empty($var) ? null : ($_GET[$var] ?? null));
177 // This is there for backwards-compatibility, in order to avoid NULL
178 if (isset($value) && !is_array($value)) {
179 $value = (string)$value;
180 }
181 return $value;
182 }
183
184 /**
185 * Returns the global $_POST array (or value from) normalized to contain un-escaped values.
186 *
187 * @param string $var Optional pointer to value in POST array (basically name of POST var)
188 * @return mixed If $var is set it returns the value of $_POST[$var]. If $var is NULL (default), returns $_POST itself.
189 * @see _GET()
190 * @see _GP()
191 */
192 public static function _POST($var = null)
193 {
194 $value = $var === null ? $_POST : (empty($var) || !isset($_POST[$var]) ? null : $_POST[$var]);
195 // This is there for backwards-compatibility, in order to avoid NULL
196 if (isset($value) && !is_array($value)) {
197 $value = (string)$value;
198 }
199 return $value;
200 }
201
202 /*************************
203 *
204 * STRING FUNCTIONS
205 *
206 *************************/
207 /**
208 * Truncates a string with appended/prepended "..." and takes current character set into consideration.
209 *
210 * @param string $string String to truncate
211 * @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.
212 * @param string $appendString Appendix to the truncated string
213 * @return string Cropped string
214 */
215 public static function fixed_lgd_cs($string, $chars, $appendString = '...')
216 {
217 if ((int)$chars === 0 || mb_strlen($string, 'utf-8') <= abs($chars)) {
218 return $string;
219 }
220 if ($chars > 0) {
221 $string = mb_substr($string, 0, $chars, 'utf-8') . $appendString;
222 } else {
223 $string = $appendString . mb_substr($string, $chars, mb_strlen($string, 'utf-8'), 'utf-8');
224 }
225 return $string;
226 }
227
228 /**
229 * Match IP number with list of numbers with wildcard
230 * Dispatcher method for switching into specialised IPv4 and IPv6 methods.
231 *
232 * @param string $baseIP Is the current remote IP address for instance, typ. REMOTE_ADDR
233 * @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.
234 * @return bool TRUE if an IP-mask from $list matches $baseIP
235 */
236 public static function cmpIP($baseIP, $list)
237 {
238 $list = trim($list);
239 if ($list === '') {
240 return false;
241 }
242 if ($list === '*') {
243 return true;
244 }
245 if (strpos($baseIP, ':') !== false && self::validIPv6($baseIP)) {
246 return self::cmpIPv6($baseIP, $list);
247 }
248 return self::cmpIPv4($baseIP, $list);
249 }
250
251 /**
252 * Match IPv4 number with list of numbers with wildcard
253 *
254 * @param string $baseIP Is the current remote IP address for instance, typ. REMOTE_ADDR
255 * @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
256 * @return bool TRUE if an IP-mask from $list matches $baseIP
257 */
258 public static function cmpIPv4($baseIP, $list)
259 {
260 $IPpartsReq = explode('.', $baseIP);
261 if (count($IPpartsReq) === 4) {
262 $values = self::trimExplode(',', $list, true);
263 foreach ($values as $test) {
264 $testList = explode('/', $test);
265 if (count($testList) === 2) {
266 list($test, $mask) = $testList;
267 } else {
268 $mask = false;
269 }
270 if ((int)$mask) {
271 // "192.168.3.0/24"
272 $lnet = ip2long($test);
273 $lip = ip2long($baseIP);
274 $binnet = str_pad(decbin($lnet), 32, '0', STR_PAD_LEFT);
275 $firstpart = substr($binnet, 0, $mask);
276 $binip = str_pad(decbin($lip), 32, '0', STR_PAD_LEFT);
277 $firstip = substr($binip, 0, $mask);
278 $yes = $firstpart === $firstip;
279 } else {
280 // "192.168.*.*"
281 $IPparts = explode('.', $test);
282 $yes = 1;
283 foreach ($IPparts as $index => $val) {
284 $val = trim($val);
285 if ($val !== '*' && $IPpartsReq[$index] !== $val) {
286 $yes = 0;
287 }
288 }
289 }
290 if ($yes) {
291 return true;
292 }
293 }
294 }
295 return false;
296 }
297
298 /**
299 * Match IPv6 address with a list of IPv6 prefixes
300 *
301 * @param string $baseIP Is the current remote IP address for instance
302 * @param string $list Is a comma-list of IPv6 prefixes, could also contain IPv4 addresses
303 * @return bool TRUE If a baseIP matches any prefix
304 */
305 public static function cmpIPv6($baseIP, $list)
306 {
307 // Policy default: Deny connection
308 $success = false;
309 $baseIP = self::normalizeIPv6($baseIP);
310 $values = self::trimExplode(',', $list, true);
311 foreach ($values as $test) {
312 $testList = explode('/', $test);
313 if (count($testList) === 2) {
314 list($test, $mask) = $testList;
315 } else {
316 $mask = false;
317 }
318 if (self::validIPv6($test)) {
319 $test = self::normalizeIPv6($test);
320 $maskInt = (int)$mask ?: 128;
321 // Special case; /0 is an allowed mask - equals a wildcard
322 if ($mask === '0') {
323 $success = true;
324 } elseif ($maskInt == 128) {
325 $success = $test === $baseIP;
326 } else {
327 $testBin = self::IPv6Hex2Bin($test);
328 $baseIPBin = self::IPv6Hex2Bin($baseIP);
329 $success = true;
330 // Modulo is 0 if this is a 8-bit-boundary
331 $maskIntModulo = $maskInt % 8;
332 $numFullCharactersUntilBoundary = (int)($maskInt / 8);
333 if (strpos($testBin, substr($baseIPBin, 0, $numFullCharactersUntilBoundary)) !== 0) {
334 $success = false;
335 } elseif ($maskIntModulo > 0) {
336 // If not an 8-bit-boundary, check bits of last character
337 $testLastBits = str_pad(decbin(ord(substr($testBin, $numFullCharactersUntilBoundary, 1))), 8, '0', STR_PAD_LEFT);
338 $baseIPLastBits = str_pad(decbin(ord(substr($baseIPBin, $numFullCharactersUntilBoundary, 1))), 8, '0', STR_PAD_LEFT);
339 if (strncmp($testLastBits, $baseIPLastBits, $maskIntModulo) != 0) {
340 $success = false;
341 }
342 }
343 }
344 }
345 if ($success) {
346 return true;
347 }
348 }
349 return false;
350 }
351
352 /**
353 * Transform a regular IPv6 address from hex-representation into binary
354 *
355 * @param string $hex IPv6 address in hex-presentation
356 * @return string Binary representation (16 characters, 128 characters)
357 * @see IPv6Bin2Hex()
358 */
359 public static function IPv6Hex2Bin($hex)
360 {
361 return inet_pton($hex);
362 }
363
364 /**
365 * Transform an IPv6 address from binary to hex-representation
366 *
367 * @param string $bin IPv6 address in hex-presentation
368 * @return string Binary representation (16 characters, 128 characters)
369 * @see IPv6Hex2Bin()
370 */
371 public static function IPv6Bin2Hex($bin)
372 {
373 return inet_ntop($bin);
374 }
375
376 /**
377 * Normalize an IPv6 address to full length
378 *
379 * @param string $address Given IPv6 address
380 * @return string Normalized address
381 * @see compressIPv6()
382 */
383 public static function normalizeIPv6($address)
384 {
385 $normalizedAddress = '';
386 // According to RFC lowercase-representation is recommended
387 $address = strtolower($address);
388 // Normalized representation has 39 characters (0000:0000:0000:0000:0000:0000:0000:0000)
389 if (strlen($address) === 39) {
390 // Already in full expanded form
391 return $address;
392 }
393 // Count 2 if if address has hidden zero blocks
394 $chunks = explode('::', $address);
395 if (count($chunks) === 2) {
396 $chunksLeft = explode(':', $chunks[0]);
397 $chunksRight = explode(':', $chunks[1]);
398 $left = count($chunksLeft);
399 $right = count($chunksRight);
400 // Special case: leading zero-only blocks count to 1, should be 0
401 if ($left === 1 && strlen($chunksLeft[0]) === 0) {
402 $left = 0;
403 }
404 $hiddenBlocks = 8 - ($left + $right);
405 $hiddenPart = '';
406 $h = 0;
407 while ($h < $hiddenBlocks) {
408 $hiddenPart .= '0000:';
409 $h++;
410 }
411 if ($left === 0) {
412 $stageOneAddress = $hiddenPart . $chunks[1];
413 } else {
414 $stageOneAddress = $chunks[0] . ':' . $hiddenPart . $chunks[1];
415 }
416 } else {
417 $stageOneAddress = $address;
418 }
419 // Normalize the blocks:
420 $blocks = explode(':', $stageOneAddress);
421 $divCounter = 0;
422 foreach ($blocks as $block) {
423 $tmpBlock = '';
424 $i = 0;
425 $hiddenZeros = 4 - strlen($block);
426 while ($i < $hiddenZeros) {
427 $tmpBlock .= '0';
428 $i++;
429 }
430 $normalizedAddress .= $tmpBlock . $block;
431 if ($divCounter < 7) {
432 $normalizedAddress .= ':';
433 $divCounter++;
434 }
435 }
436 return $normalizedAddress;
437 }
438
439 /**
440 * Compress an IPv6 address to the shortest notation
441 *
442 * @param string $address Given IPv6 address
443 * @return string Compressed address
444 * @see normalizeIPv6()
445 */
446 public static function compressIPv6($address)
447 {
448 return inet_ntop(inet_pton($address));
449 }
450
451 /**
452 * Validate a given IP address.
453 *
454 * Possible format are IPv4 and IPv6.
455 *
456 * @param string $ip IP address to be tested
457 * @return bool TRUE if $ip is either of IPv4 or IPv6 format.
458 */
459 public static function validIP($ip)
460 {
461 return filter_var($ip, FILTER_VALIDATE_IP) !== false;
462 }
463
464 /**
465 * Validate a given IP address to the IPv4 address format.
466 *
467 * Example for possible format: 10.0.45.99
468 *
469 * @param string $ip IP address to be tested
470 * @return bool TRUE if $ip is of IPv4 format.
471 */
472 public static function validIPv4($ip)
473 {
474 return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false;
475 }
476
477 /**
478 * Validate a given IP address to the IPv6 address format.
479 *
480 * Example for possible format: 43FB::BB3F:A0A0:0 | ::1
481 *
482 * @param string $ip IP address to be tested
483 * @return bool TRUE if $ip is of IPv6 format.
484 */
485 public static function validIPv6($ip)
486 {
487 return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false;
488 }
489
490 /**
491 * Match fully qualified domain name with list of strings with wildcard
492 *
493 * @param string $baseHost A hostname or an IPv4/IPv6-address (will by reverse-resolved; typically REMOTE_ADDR)
494 * @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)
495 * @return bool TRUE if a domain name mask from $list matches $baseIP
496 */
497 public static function cmpFQDN($baseHost, $list)
498 {
499 $baseHost = trim($baseHost);
500 if (empty($baseHost)) {
501 return false;
502 }
503 if (self::validIPv4($baseHost) || self::validIPv6($baseHost)) {
504 // Resolve hostname
505 // Note: this is reverse-lookup and can be randomly set as soon as somebody is able to set
506 // the reverse-DNS for his IP (security when for example used with REMOTE_ADDR)
507 $baseHostName = gethostbyaddr($baseHost);
508 if ($baseHostName === $baseHost) {
509 // Unable to resolve hostname
510 return false;
511 }
512 } else {
513 $baseHostName = $baseHost;
514 }
515 $baseHostNameParts = explode('.', $baseHostName);
516 $values = self::trimExplode(',', $list, true);
517 foreach ($values as $test) {
518 $hostNameParts = explode('.', $test);
519 // To match hostNameParts can only be shorter (in case of wildcards) or equal
520 $hostNamePartsCount = count($hostNameParts);
521 $baseHostNamePartsCount = count($baseHostNameParts);
522 if ($hostNamePartsCount > $baseHostNamePartsCount) {
523 continue;
524 }
525 $yes = true;
526 foreach ($hostNameParts as $index => $val) {
527 $val = trim($val);
528 if ($val === '*') {
529 // Wildcard valid for one or more hostname-parts
530 $wildcardStart = $index + 1;
531 // Wildcard as last/only part always matches, otherwise perform recursive checks
532 if ($wildcardStart < $hostNamePartsCount) {
533 $wildcardMatched = false;
534 $tempHostName = implode('.', array_slice($hostNameParts, $index + 1));
535 while ($wildcardStart < $baseHostNamePartsCount && !$wildcardMatched) {
536 $tempBaseHostName = implode('.', array_slice($baseHostNameParts, $wildcardStart));
537 $wildcardMatched = self::cmpFQDN($tempBaseHostName, $tempHostName);
538 $wildcardStart++;
539 }
540 if ($wildcardMatched) {
541 // Match found by recursive compare
542 return true;
543 }
544 $yes = false;
545 }
546 } elseif ($baseHostNameParts[$index] !== $val) {
547 // In case of no match
548 $yes = false;
549 }
550 }
551 if ($yes) {
552 return true;
553 }
554 }
555 return false;
556 }
557
558 /**
559 * Checks if a given URL matches the host that currently handles this HTTP request.
560 * Scheme, hostname and (optional) port of the given URL are compared.
561 *
562 * @param string $url URL to compare with the TYPO3 request host
563 * @return bool Whether the URL matches the TYPO3 request host
564 */
565 public static function isOnCurrentHost($url)
566 {
567 return stripos($url . '/', self::getIndpEnv('TYPO3_REQUEST_HOST') . '/') === 0;
568 }
569
570 /**
571 * Check for item in list
572 * Check if an item exists in a comma-separated list of items.
573 *
574 * @param string $list Comma-separated list of items (string)
575 * @param string $item Item to check for
576 * @return bool TRUE if $item is in $list
577 */
578 public static function inList($list, $item)
579 {
580 return strpos(',' . $list . ',', ',' . $item . ',') !== false;
581 }
582
583 /**
584 * Removes an item from a comma-separated list of items.
585 *
586 * If $element contains a comma, the behaviour of this method is undefined.
587 * Empty elements in the list are preserved.
588 *
589 * @param string $element Element to remove
590 * @param string $list Comma-separated list of items (string)
591 * @return string New comma-separated list of items
592 */
593 public static function rmFromList($element, $list)
594 {
595 $items = explode(',', $list);
596 foreach ($items as $k => $v) {
597 if ($v == $element) {
598 unset($items[$k]);
599 }
600 }
601 return implode(',', $items);
602 }
603
604 /**
605 * Expand a comma-separated list of integers with ranges (eg 1,3-5,7 becomes 1,3,4,5,7).
606 * Ranges are limited to 1000 values per range.
607 *
608 * @param string $list Comma-separated list of integers with ranges (string)
609 * @return string New comma-separated list of items
610 */
611 public static function expandList($list)
612 {
613 $items = explode(',', $list);
614 $list = [];
615 foreach ($items as $item) {
616 $range = explode('-', $item);
617 if (isset($range[1])) {
618 $runAwayBrake = 1000;
619 for ($n = $range[0]; $n <= $range[1]; $n++) {
620 $list[] = $n;
621 $runAwayBrake--;
622 if ($runAwayBrake <= 0) {
623 break;
624 }
625 }
626 } else {
627 $list[] = $item;
628 }
629 }
630 return implode(',', $list);
631 }
632
633 /**
634 * Makes a positive integer hash out of the first 7 chars from the md5 hash of the input
635 *
636 * @param string $str String to md5-hash
637 * @return int Returns 28bit integer-hash
638 */
639 public static function md5int($str)
640 {
641 return hexdec(substr(md5($str), 0, 7));
642 }
643
644 /**
645 * Returns the first 10 positions of the MD5-hash (changed from 6 to 10 recently)
646 *
647 * @param string $input Input string to be md5-hashed
648 * @param int $len The string-length of the output
649 * @return string Substring of the resulting md5-hash, being $len chars long (from beginning)
650 */
651 public static function shortMD5($input, $len = 10)
652 {
653 return substr(md5($input), 0, $len);
654 }
655
656 /**
657 * Returns a proper HMAC on a given input string and secret TYPO3 encryption key.
658 *
659 * @param string $input Input string to create HMAC from
660 * @param string $additionalSecret additionalSecret to prevent hmac being used in a different context
661 * @return string resulting (hexadecimal) HMAC currently with a length of 40 (HMAC-SHA-1)
662 */
663 public static function hmac($input, $additionalSecret = '')
664 {
665 $hashAlgorithm = 'sha1';
666 $hashBlocksize = 64;
667 $secret = $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'] . $additionalSecret;
668 if (extension_loaded('hash') && function_exists('hash_hmac') && function_exists('hash_algos') && in_array($hashAlgorithm, hash_algos())) {
669 $hmac = hash_hmac($hashAlgorithm, $input, $secret);
670 } else {
671 // Outer padding
672 $opad = str_repeat(chr(92), $hashBlocksize);
673 // Inner padding
674 $ipad = str_repeat(chr(54), $hashBlocksize);
675 if (strlen($secret) > $hashBlocksize) {
676 // Keys longer than block size are shorten
677 $key = str_pad(pack('H*', call_user_func($hashAlgorithm, $secret)), $hashBlocksize, "\0");
678 } else {
679 // Keys shorter than block size are zero-padded
680 $key = str_pad($secret, $hashBlocksize, "\0");
681 }
682 $hmac = call_user_func($hashAlgorithm, ($key ^ $opad) . pack('H*', call_user_func(
683 $hashAlgorithm,
684 ($key ^ $ipad) . $input
685 )));
686 }
687 return $hmac;
688 }
689
690 /**
691 * Takes comma-separated lists and arrays and removes all duplicates
692 * If a value in the list is trim(empty), the value is ignored.
693 *
694 * @param string $in_list Accept multiple parameters which can be comma-separated lists of values and arrays.
695 * @param mixed $secondParameter Dummy field, which if set will show a warning!
696 * @return string Returns the list without any duplicates of values, space around values are trimmed
697 */
698 public static function uniqueList($in_list, $secondParameter = null)
699 {
700 if (is_array($in_list)) {
701 throw new \InvalidArgumentException('TYPO3 Fatal Error: TYPO3\\CMS\\Core\\Utility\\GeneralUtility::uniqueList() does NOT support array arguments anymore! Only string comma lists!', 1270853885);
702 }
703 if (isset($secondParameter)) {
704 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);
705 }
706 return implode(',', array_unique(self::trimExplode(',', $in_list, true)));
707 }
708
709 /**
710 * Splits a reference to a file in 5 parts
711 *
712 * @param string $fileNameWithPath File name with path to be analyzed (must exist if open_basedir is set)
713 * @return array Contains keys [path], [file], [filebody], [fileext], [realFileext]
714 */
715 public static function split_fileref($fileNameWithPath)
716 {
717 $reg = [];
718 if (preg_match('/(.*\\/)(.*)$/', $fileNameWithPath, $reg)) {
719 $info['path'] = $reg[1];
720 $info['file'] = $reg[2];
721 } else {
722 $info['path'] = '';
723 $info['file'] = $fileNameWithPath;
724 }
725 $reg = '';
726 // If open_basedir is set and the fileName was supplied without a path the is_dir check fails
727 if (!is_dir($fileNameWithPath) && preg_match('/(.*)\\.([^\\.]*$)/', $info['file'], $reg)) {
728 $info['filebody'] = $reg[1];
729 $info['fileext'] = strtolower($reg[2]);
730 $info['realFileext'] = $reg[2];
731 } else {
732 $info['filebody'] = $info['file'];
733 $info['fileext'] = '';
734 }
735 reset($info);
736 return $info;
737 }
738
739 /**
740 * Returns the directory part of a path without trailing slash
741 * If there is no dir-part, then an empty string is returned.
742 * Behaviour:
743 *
744 * '/dir1/dir2/script.php' => '/dir1/dir2'
745 * '/dir1/' => '/dir1'
746 * 'dir1/script.php' => 'dir1'
747 * 'd/script.php' => 'd'
748 * '/script.php' => ''
749 * '' => ''
750 *
751 * @param string $path Directory name / path
752 * @return string Processed input value. See function description.
753 */
754 public static function dirname($path)
755 {
756 $p = self::revExplode('/', $path, 2);
757 return count($p) === 2 ? $p[0] : '';
758 }
759
760 /**
761 * Returns TRUE if the first part of $str matches the string $partStr
762 *
763 * @param string $str Full string to check
764 * @param string $partStr Reference string which must be found as the "first part" of the full string
765 * @return bool TRUE if $partStr was found to be equal to the first part of $str
766 */
767 public static function isFirstPartOfStr($str, $partStr)
768 {
769 $str = is_array($str) ? '' : (string)$str;
770 $partStr = is_array($partStr) ? '' : (string)$partStr;
771 return $partStr !== '' && strpos($str, $partStr, 0) === 0;
772 }
773
774 /**
775 * Formats the input integer $sizeInBytes as bytes/kilobytes/megabytes (-/K/M)
776 *
777 * @param int $sizeInBytes Number of bytes to format.
778 * @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".
779 * @param int $base The unit base if not using a unit name. Defaults to 1024.
780 * @return string Formatted representation of the byte number, for output.
781 */
782 public static function formatSize($sizeInBytes, $labels = '', $base = 0)
783 {
784 $defaultFormats = [
785 'iec' => ['base' => 1024, 'labels' => [' ', ' Ki', ' Mi', ' Gi', ' Ti', ' Pi', ' Ei', ' Zi', ' Yi']],
786 'si' => ['base' => 1000, 'labels' => [' ', ' k', ' M', ' G', ' T', ' P', ' E', ' Z', ' Y']],
787 ];
788 // Set labels and base:
789 if (empty($labels)) {
790 $labels = 'iec';
791 }
792 if (isset($defaultFormats[$labels])) {
793 $base = $defaultFormats[$labels]['base'];
794 $labelArr = $defaultFormats[$labels]['labels'];
795 } else {
796 $base = (int)$base;
797 if ($base !== 1000 && $base !== 1024) {
798 $base = 1024;
799 }
800 $labelArr = explode('|', str_replace('"', '', $labels));
801 }
802 // @todo find out which locale is used for current BE user to cover the BE case as well
803 $oldLocale = setlocale(LC_NUMERIC, 0);
804 $newLocale = $GLOBALS['TSFE']->config['config']['locale_all'] ?? '';
805 if ($newLocale) {
806 setlocale(LC_NUMERIC, $newLocale);
807 }
808 $localeInfo = localeconv();
809 if ($newLocale) {
810 setlocale(LC_NUMERIC, $oldLocale);
811 }
812 $sizeInBytes = max($sizeInBytes, 0);
813 $multiplier = floor(($sizeInBytes ? log($sizeInBytes) : 0) / log($base));
814 $sizeInUnits = $sizeInBytes / pow($base, $multiplier);
815 if ($sizeInUnits > ($base * .9)) {
816 $multiplier++;
817 }
818 $multiplier = min($multiplier, count($labelArr) - 1);
819 $sizeInUnits = $sizeInBytes / pow($base, $multiplier);
820 return number_format($sizeInUnits, (($multiplier > 0) && ($sizeInUnits < 20)) ? 2 : 0, $localeInfo['decimal_point'], '') . $labelArr[$multiplier];
821 }
822
823 /**
824 * This splits a string by the chars in $operators (typical /+-*) and returns an array with them in
825 *
826 * @param string $string Input string, eg "123 + 456 / 789 - 4
827 * @param string $operators Operators to split by, typically "/+-*
828 * @return array Array with operators and operands separated.
829 * @see \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::calc()
830 * @see \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 a 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 sucks 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 and 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()
1543 * @see xml2arrayProcess()
1544 */
1545 public static function xml2array($string, $NSprefix = '', $reportDocTag = false)
1546 {
1547 $runtimeCache = static::makeInstance(CacheManager::class)->getCache('runtime');
1548 $firstLevelCache = $runtimeCache->get('generalUtilityXml2Array') ?: [];
1549 $identifier = md5($string . $NSprefix . ($reportDocTag ? '1' : '0'));
1550 // Look up in first level cache
1551 if (empty($firstLevelCache[$identifier])) {
1552 $firstLevelCache[$identifier] = self::xml2arrayProcess(trim($string), $NSprefix, $reportDocTag);
1553 $runtimeCache->set('generalUtilityXml2Array', $firstLevelCache);
1554 }
1555 return $firstLevelCache[$identifier];
1556 }
1557
1558 /**
1559 * Converts an XML string to a PHP array.
1560 * This is the reverse function of array2xml()
1561 *
1562 * @param string $string XML content to convert into an array
1563 * @param string $NSprefix The tag-prefix resolve, eg. a namespace like "T3:"
1564 * @param bool $reportDocTag If set, the document tag will be set in the key "_DOCUMENT_TAG" of the output array
1565 * @return mixed If the parsing had errors, a string with the error message is returned. Otherwise an array with the content.
1566 * @see array2xml()
1567 */
1568 protected static function xml2arrayProcess($string, $NSprefix = '', $reportDocTag = false)
1569 {
1570 // Disables the functionality to allow external entities to be loaded when parsing the XML, must be kept
1571 $previousValueOfEntityLoader = libxml_disable_entity_loader(true);
1572 // Create parser:
1573 $parser = xml_parser_create();
1574 $vals = [];
1575 $index = [];
1576 xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0);
1577 xml_parser_set_option($parser, XML_OPTION_SKIP_WHITE, 0);
1578 // Default output charset is UTF-8, only ASCII, ISO-8859-1 and UTF-8 are supported!!!
1579 $match = [];
1580 preg_match('/^[[:space:]]*<\\?xml[^>]*encoding[[:space:]]*=[[:space:]]*"([^"]*)"/', substr($string, 0, 200), $match);
1581 $theCharset = $match[1] ?? 'utf-8';
1582 // us-ascii / utf-8 / iso-8859-1
1583 xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, $theCharset);
1584 // Parse content:
1585 xml_parse_into_struct($parser, $string, $vals, $index);
1586 libxml_disable_entity_loader($previousValueOfEntityLoader);
1587 // If error, return error message:
1588 if (xml_get_error_code($parser)) {
1589 return 'Line ' . xml_get_current_line_number($parser) . ': ' . xml_error_string(xml_get_error_code($parser));
1590 }
1591 xml_parser_free($parser);
1592 // Init vars:
1593 $stack = [[]];
1594 $stacktop = 0;
1595 $current = [];
1596 $tagName = '';
1597 $documentTag = '';
1598 // Traverse the parsed XML structure:
1599 foreach ($vals as $key => $val) {
1600 // First, process the tag-name (which is used in both cases, whether "complete" or "close")
1601 $tagName = $val['tag'];
1602 if (!$documentTag) {
1603 $documentTag = $tagName;
1604 }
1605 // Test for name space:
1606 $tagName = $NSprefix && strpos($tagName, $NSprefix) === 0 ? substr($tagName, strlen($NSprefix)) : $tagName;
1607 // Test for numeric tag, encoded on the form "nXXX":
1608 $testNtag = substr($tagName, 1);
1609 // Closing tag.
1610 $tagName = $tagName[0] === 'n' && MathUtility::canBeInterpretedAsInteger($testNtag) ? (int)$testNtag : $tagName;
1611 // Test for alternative index value:
1612 if ((string)($val['attributes']['index'] ?? '') !== '') {
1613 $tagName = $val['attributes']['index'];
1614 }
1615 // Setting tag-values, manage stack:
1616 switch ($val['type']) {
1617 case 'open':
1618 // If open tag it means there is an array stored in sub-elements. Therefore increase the stackpointer and reset the accumulation array:
1619 // Setting blank place holder
1620 $current[$tagName] = [];
1621 $stack[$stacktop++] = $current;
1622 $current = [];
1623 break;
1624 case 'close':
1625 // If the tag is "close" then it is an array which is closing and we decrease the stack pointer.
1626 $oldCurrent = $current;
1627 $current = $stack[--$stacktop];
1628 // Going to the end of array to get placeholder key, key($current), and fill in array next:
1629 end($current);
1630 $current[key($current)] = $oldCurrent;
1631 unset($oldCurrent);
1632 break;
1633 case 'complete':
1634 // If "complete", then it's a value. If the attribute "base64" is set, then decode the value, otherwise just set it.
1635 if (!empty($val['attributes']['base64'])) {
1636 $current[$tagName] = base64_decode($val['value']);
1637 } else {
1638 // Had to cast it as a string - otherwise it would be evaluate FALSE if tested with isset()!!
1639 $current[$tagName] = (string)($val['value'] ?? '');
1640 // Cast type:
1641 switch ((string)($val['attributes']['type'] ?? '')) {
1642 case 'integer':
1643 $current[$tagName] = (int)$current[$tagName];
1644 break;
1645 case 'double':
1646 $current[$tagName] = (double)$current[$tagName];
1647 break;
1648 case 'boolean':
1649 $current[$tagName] = (bool)$current[$tagName];
1650 break;
1651 case 'NULL':
1652 $current[$tagName] = null;
1653 break;
1654 case 'array':
1655 // 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...
1656 $current[$tagName] = [];
1657 break;
1658 }
1659 }
1660 break;
1661 }
1662 }
1663 if ($reportDocTag) {
1664 $current[$tagName]['_DOCUMENT_TAG'] = $documentTag;
1665 }
1666 // Finally return the content of the document tag.
1667 return $current[$tagName];
1668 }
1669
1670 /**
1671 * This implodes an array of XML parts (made with xml_parse_into_struct()) into XML again.
1672 *
1673 * @param array $vals An array of XML parts, see xml2tree
1674 * @return string Re-compiled XML data.
1675 */
1676 public static function xmlRecompileFromStructValArray(array $vals)
1677 {
1678 $XMLcontent = '';
1679 foreach ($vals as $val) {
1680 $type = $val['type'];
1681 // Open tag:
1682 if ($type === 'open' || $type === 'complete') {
1683 $XMLcontent .= '<' . $val['tag'];
1684 if (isset($val['attributes'])) {
1685 foreach ($val['attributes'] as $k => $v) {
1686 $XMLcontent .= ' ' . $k . '="' . htmlspecialchars($v) . '"';
1687 }
1688 }
1689 if ($type === 'complete') {
1690 if (isset($val['value'])) {
1691 $XMLcontent .= '>' . htmlspecialchars($val['value']) . '</' . $val['tag'] . '>';
1692 } else {
1693 $XMLcontent .= '/>';
1694 }
1695 } else {
1696 $XMLcontent .= '>';
1697 }
1698 if ($type === 'open' && isset($val['value'])) {
1699 $XMLcontent .= htmlspecialchars($val['value']);
1700 }
1701 }
1702 // Finish tag:
1703 if ($type === 'close') {
1704 $XMLcontent .= '</' . $val['tag'] . '>';
1705 }
1706 // Cdata
1707 if ($type === 'cdata') {
1708 $XMLcontent .= htmlspecialchars($val['value']);
1709 }
1710 }
1711 return $XMLcontent;
1712 }
1713
1714 /**
1715 * Minifies JavaScript
1716 *
1717 * @param string $script Script to minify
1718 * @param string $error Error message (if any)
1719 * @return string Minified script or source string if error happened
1720 */
1721 public static function minifyJavaScript($script, &$error = '')
1722 {
1723 $fakeThis = false;
1724 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_div.php']['minifyJavaScript'] ?? [] as $hookMethod) {
1725 try {
1726 $parameters = ['script' => $script];
1727 $script = static::callUserFunction($hookMethod, $parameters, $fakeThis);
1728 } catch (\Exception $e) {
1729 $errorMessage = 'Error minifying java script: ' . $e->getMessage();
1730 $error .= $errorMessage;
1731 static::getLogger()->warning($errorMessage, [
1732 'JavaScript' => $script,
1733 'hook' => $hookMethod,
1734 'exception' => $e,
1735 ]);
1736 }
1737 }
1738 return $script;
1739 }
1740
1741 /*************************
1742 *
1743 * FILES FUNCTIONS
1744 *
1745 *************************/
1746 /**
1747 * Reads the file or url $url and returns the content
1748 * 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'].
1749 *
1750 * @param string $url File/URL to read
1751 * @param int $includeHeader Whether the HTTP header should be fetched or not. 0=disable, 1=fetch header+content, 2=fetch header only
1752 * @param array $requestHeaders HTTP headers to be used in the request
1753 * @param array $report Error code/message and, if $includeHeader is 1, response meta data (HTTP status and content type)
1754 * @return mixed The content from the resource given as input. FALSE if an error has occurred.
1755 */
1756 public static function getUrl($url, $includeHeader = 0, $requestHeaders = null, &$report = null)
1757 {
1758 if (isset($report)) {
1759 $report['error'] = 0;
1760 $report['message'] = '';
1761 }
1762 // Looks like it's an external file, use Guzzle by default
1763 if (preg_match('/^(?:http|ftp)s?|s(?:ftp|cp):/', $url)) {
1764 $requestFactory = static::makeInstance(RequestFactory::class);
1765 if (is_array($requestHeaders)) {
1766 $configuration = ['headers' => $requestHeaders];
1767 } else {
1768 $configuration = [];
1769 }
1770 $includeHeader = (int)$includeHeader;
1771 $method = $includeHeader === 2 ? 'HEAD' : 'GET';
1772 try {
1773 if (isset($report)) {
1774 $report['lib'] = 'GuzzleHttp';
1775 }
1776 $response = $requestFactory->request($url, $method, $configuration);
1777 } catch (RequestException $exception) {
1778 if (isset($report)) {
1779 $report['error'] = $exception->getCode() ?: 1518707554;
1780 $report['message'] = $exception->getMessage();
1781 $report['exception'] = $exception;
1782 }
1783 return false;
1784 }
1785 $content = '';
1786 // Add the headers to the output
1787 if ($includeHeader) {
1788 $parsedURL = parse_url($url);
1789 $content = $method . ' ' . ($parsedURL['path'] ?? '/')
1790 . (!empty($parsedURL['query']) ? '?' . $parsedURL['query'] : '') . ' HTTP/1.0' . CRLF
1791 . 'Host: ' . $parsedURL['host'] . CRLF
1792 . 'Connection: close' . CRLF;
1793 if (is_array($requestHeaders)) {
1794 $content .= implode(CRLF, $requestHeaders) . CRLF;
1795 }
1796 foreach ($response->getHeaders() as $headerName => $headerValues) {
1797 $content .= $headerName . ': ' . implode(', ', $headerValues) . CRLF;
1798 }
1799 // Headers are separated from the body with two CRLFs
1800 $content .= CRLF;
1801 }
1802
1803 $content .= $response->getBody()->getContents();
1804
1805 if (isset($report)) {
1806 if ($response->getStatusCode() >= 300 && $response->getStatusCode() < 400) {
1807 $report['http_code'] = $response->getStatusCode();
1808 $report['content_type'] = $response->getHeaderLine('Content-Type');
1809 $report['error'] = $response->getStatusCode();
1810 $report['message'] = $response->getReasonPhrase();
1811 } elseif (empty($content)) {
1812 $report['error'] = $response->getStatusCode();
1813 $report['message'] = $response->getReasonPhrase();
1814 } elseif ($includeHeader) {
1815 // Set only for $includeHeader to work exactly like PHP variant
1816 $report['http_code'] = $response->getStatusCode();
1817 $report['content_type'] = $response->getHeaderLine('Content-Type');
1818 }
1819 }
1820 } else {
1821 if (isset($report)) {
1822 $report['lib'] = 'file';
1823 }
1824 $content = @file_get_contents($url);
1825 if ($content === false && isset($report)) {
1826 $report['error'] = -1;
1827 $report['message'] = 'Couldn\'t get URL: ' . $url;
1828 }
1829 }
1830 return $content;
1831 }
1832
1833 /**
1834 * Split an array of MIME header strings into an associative array.
1835 * Multiple headers with the same name have their values merged as an array.
1836 *
1837 * @static
1838 * @param array $headers List of headers, eg. ['Foo: Bar', 'Foo: Baz']
1839 * @return array Key/Value(s) pairs of headers, eg. ['Foo' => ['Bar', 'Baz']]
1840 */
1841 protected static function splitHeaderLines(array $headers): array
1842 {
1843 $newHeaders = [];
1844 foreach ($headers as $header) {
1845 $parts = preg_split('/:[ \t]*/', $header, 2, PREG_SPLIT_NO_EMPTY);
1846 if (count($parts) !== 2) {
1847 continue;
1848 }
1849 $key = &$parts[0];
1850 $value = &$parts[1];
1851 if (array_key_exists($key, $newHeaders)) {
1852 if (is_array($newHeaders[$key])) {
1853 $newHeaders[$key][] = $value;
1854 } else {
1855 $prevValue = &$newHeaders[$key];
1856 $newHeaders[$key] = [$prevValue, $value];
1857 }
1858 } else {
1859 $newHeaders[$key] = $value;
1860 }
1861 }
1862 return $newHeaders;
1863 }
1864
1865 /**
1866 * Writes $content to the file $file
1867 *
1868 * @param string $file Filepath to write to
1869 * @param string $content Content to write
1870 * @param bool $changePermissions If TRUE, permissions are forced to be set
1871 * @return bool TRUE if the file was successfully opened and written to.
1872 */
1873 public static function writeFile($file, $content, $changePermissions = false)
1874 {
1875 if (!@is_file($file)) {
1876 $changePermissions = true;
1877 }
1878 if ($fd = fopen($file, 'wb')) {
1879 $res = fwrite($fd, $content);
1880 fclose($fd);
1881 if ($res === false) {
1882 return false;
1883 }
1884 // Change the permissions only if the file has just been created
1885 if ($changePermissions) {
1886 static::fixPermissions($file);
1887 }
1888 return true;
1889 }
1890 return false;
1891 }
1892
1893 /**
1894 * Sets the file system mode and group ownership of a file or a folder.
1895 *
1896 * @param string $path Path of file or folder, must not be escaped. Path can be absolute or relative
1897 * @param bool $recursive If set, also fixes permissions of files and folders in the folder (if $path is a folder)
1898 * @return mixed TRUE on success, FALSE on error, always TRUE on Windows OS
1899 */
1900 public static function fixPermissions($path, $recursive = false)
1901 {
1902 if (Environment::isWindows()) {
1903 return true;
1904 }
1905 $result = false;
1906 // Make path absolute
1907 if (!static::isAbsPath($path)) {
1908 $path = static::getFileAbsFileName($path);
1909 }
1910 if (static::isAllowedAbsPath($path)) {
1911 if (@is_file($path)) {
1912 $targetPermissions = $GLOBALS['TYPO3_CONF_VARS']['SYS']['fileCreateMask'] ?? '0644';
1913 } elseif (@is_dir($path)) {
1914 $targetPermissions = $GLOBALS['TYPO3_CONF_VARS']['SYS']['folderCreateMask'] ?? '0755';
1915 }
1916 if (!empty($targetPermissions)) {
1917 // make sure it's always 4 digits
1918 $targetPermissions = str_pad($targetPermissions, 4, 0, STR_PAD_LEFT);
1919 $targetPermissions = octdec($targetPermissions);
1920 // "@" is there because file is not necessarily OWNED by the user
1921 $result = @chmod($path, $targetPermissions);
1922 }
1923 // Set createGroup if not empty
1924 if (
1925 isset($GLOBALS['TYPO3_CONF_VARS']['SYS']['createGroup'])
1926 && $GLOBALS['TYPO3_CONF_VARS']['SYS']['createGroup'] !== ''
1927 ) {
1928 // "@" is there because file is not necessarily OWNED by the user
1929 $changeGroupResult = @chgrp($path, $GLOBALS['TYPO3_CONF_VARS']['SYS']['createGroup']);
1930 $result = $changeGroupResult ? $result : false;
1931 }
1932 // Call recursive if recursive flag if set and $path is directory
1933 if ($recursive && @is_dir($path)) {
1934 $handle = opendir($path);
1935 if (is_resource($handle)) {
1936 while (($file = readdir($handle)) !== false) {
1937 $recursionResult = null;
1938 if ($file !== '.' && $file !== '..') {
1939 if (@is_file($path . '/' . $file)) {
1940 $recursionResult = static::fixPermissions($path . '/' . $file);
1941 } elseif (@is_dir($path . '/' . $file)) {
1942 $recursionResult = static::fixPermissions($path . '/' . $file, true);
1943 }
1944 if (isset($recursionResult) && !$recursionResult) {
1945 $result = false;
1946 }
1947 }
1948 }
1949 closedir($handle);
1950 }
1951 }
1952 }
1953 return $result;
1954 }
1955
1956 /**
1957 * Writes $content to a filename in the typo3temp/ folder (and possibly one or two subfolders...)
1958 * Accepts an additional subdirectory in the file path!
1959 *
1960 * @param string $filepath Absolute file path to write within the typo3temp/ or Environment::getVarPath() folder - the file path must be prefixed with this path
1961 * @param string $content Content string to write
1962 * @return string Returns NULL on success, otherwise an error string telling about the problem.
1963 */
1964 public static function writeFileToTypo3tempDir($filepath, $content)
1965 {
1966 // Parse filepath into directory and basename:
1967 $fI = pathinfo($filepath);
1968 $fI['dirname'] .= '/';
1969 // Check parts:
1970 if (!static::validPathStr($filepath) || !$fI['basename'] || strlen($fI['basename']) >= 60) {
1971 return 'Input filepath "' . $filepath . '" was generally invalid!';
1972 }
1973
1974 // Setting main temporary directory name (standard)
1975 $allowedPathPrefixes = [
1976 Environment::getPublicPath() . '/typo3temp' => 'Environment::getPublicPath() + "/typo3temp/"'
1977 ];
1978 // Also allow project-path + /var/
1979 if (Environment::getVarPath() !== Environment::getPublicPath() . '/typo3temp/var') {
1980 $relPath = substr(Environment::getVarPath(), strlen(Environment::getProjectPath()) + 1);
1981 $allowedPathPrefixes[Environment::getVarPath()] = 'ProjectPath + ' . $relPath;
1982 }
1983
1984 $errorMessage = null;
1985 foreach ($allowedPathPrefixes as $pathPrefix => $prefixLabel) {
1986 $dirName = $pathPrefix . '/';
1987 // Invalid file path, let's check for the other path, if it exists
1988 if (!static::isFirstPartOfStr($fI['dirname'], $dirName)) {
1989 if ($errorMessage === null) {
1990 $errorMessage = '"' . $fI['dirname'] . '" was not within directory ' . $prefixLabel;
1991 }
1992 continue;
1993 }
1994 // This resets previous error messages from the first path
1995 $errorMessage = null;
1996
1997 if (!@is_dir($dirName)) {
1998 $errorMessage = $prefixLabel . ' was not a directory!';
1999 // continue and see if the next iteration resets the errorMessage above
2000 continue;
2001 }
2002 // Checking if the "subdir" is found
2003 $subdir = substr($fI['dirname'], strlen($dirName));
2004 if ($subdir) {
2005 if (preg_match('#^(?:[[:alnum:]_]+/)+$#', $subdir)) {
2006 $dirName .= $subdir;
2007 if (!@is_dir($dirName)) {
2008 static::mkdir_deep($pathPrefix . '/' . $subdir);
2009 }
2010 } else {
2011 $errorMessage = 'Subdir, "' . $subdir . '", was NOT on the form "[[:alnum:]_]/+"';
2012 break;
2013 }
2014 }
2015 // Checking dir-name again (sub-dir might have been created)
2016 if (@is_dir($dirName)) {
2017 if ($filepath === $dirName . $fI['basename']) {
2018 static::writeFile($filepath, $content);
2019 if (!@is_file($filepath)) {
2020 $errorMessage = 'The file was not written to the disk. Please, check that you have write permissions to the ' . $prefixLabel . ' directory.';
2021 break;
2022 }
2023 } else {
2024 $errorMessage = 'Calculated file location didn\'t match input "' . $filepath . '".';
2025 break;
2026 }
2027 } else {
2028 $errorMessage = '"' . $dirName . '" is not a directory!';
2029 break;
2030 }
2031 }
2032 return $errorMessage;
2033 }
2034
2035 /**
2036 * Wrapper function for mkdir.
2037 * Sets folder permissions according to $GLOBALS['TYPO3_CONF_VARS']['SYS']['folderCreateMask']
2038 * and group ownership according to $GLOBALS['TYPO3_CONF_VARS']['SYS']['createGroup']
2039 *
2040 * @param string $newFolder Absolute path to folder, see PHP mkdir() function. Removes trailing slash internally.
2041 * @return bool TRUE if operation was successful
2042 */
2043 public static function mkdir($newFolder)
2044 {
2045 $result = @mkdir($newFolder, octdec($GLOBALS['TYPO3_CONF_VARS']['SYS']['folderCreateMask']));
2046 if ($result) {
2047 static::fixPermissions($newFolder);
2048 }
2049 return $result;
2050 }
2051
2052 /**
2053 * Creates a directory - including parent directories if necessary and
2054 * sets permissions on newly created directories.
2055 *
2056 * @param string $directory Target directory to create. Must a have trailing slash
2057 * @throws \InvalidArgumentException If $directory or $deepDirectory are not strings
2058 * @throws \RuntimeException If directory could not be created
2059 */
2060 public static function mkdir_deep($directory)
2061 {
2062 if (!is_string($directory)) {
2063 throw new \InvalidArgumentException('The specified directory is of type "' . gettype($directory) . '" but a string is expected.', 1303662955);
2064 }
2065 // Ensure there is only one slash
2066 $fullPath = rtrim($directory, '/') . '/';
2067 if ($fullPath !== '/' && !is_dir($fullPath)) {
2068 $firstCreatedPath = static::createDirectoryPath($fullPath);
2069 if ($firstCreatedPath !== '') {
2070 static::fixPermissions($firstCreatedPath, true);
2071 }
2072 }
2073 }
2074
2075 /**
2076 * Creates directories for the specified paths if they do not exist. This
2077 * functions sets proper permission mask but does not set proper user and
2078 * group.
2079 *
2080 * @static
2081 * @param string $fullDirectoryPath
2082 * @return string Path to the the first created directory in the hierarchy
2083 * @see \TYPO3\CMS\Core\Utility\GeneralUtility::mkdir_deep
2084 * @throws \RuntimeException If directory could not be created
2085 */
2086 protected static function createDirectoryPath($fullDirectoryPath)
2087 {
2088 $currentPath = $fullDirectoryPath;
2089 $firstCreatedPath = '';
2090 $permissionMask = octdec($GLOBALS['TYPO3_CONF_VARS']['SYS']['folderCreateMask']);
2091 if (!@is_dir($currentPath)) {
2092 do {
2093 $firstCreatedPath = $currentPath;
2094 $separatorPosition = strrpos($currentPath, DIRECTORY_SEPARATOR);
2095 $currentPath = substr($currentPath, 0, $separatorPosition);
2096 } while (!is_dir($currentPath) && $separatorPosition !== false);
2097 $result = @mkdir($fullDirectoryPath, $permissionMask, true);
2098 // Check existence of directory again to avoid race condition. Directory could have get created by another process between previous is_dir() and mkdir()
2099 if (!$result && !@is_dir($fullDirectoryPath)) {
2100 throw new \RuntimeException('Could not create directory "' . $fullDirectoryPath . '"!', 1170251401);
2101 }
2102 }
2103 return $firstCreatedPath;
2104 }
2105
2106 /**
2107 * Wrapper function for rmdir, allowing recursive deletion of folders and files
2108 *
2109 * @param string $path Absolute path to folder, see PHP rmdir() function. Removes trailing slash internally.
2110 * @param bool $removeNonEmpty Allow deletion of non-empty directories
2111 * @return bool TRUE if operation was successful
2112 */
2113 public static function rmdir($path, $removeNonEmpty = false)
2114 {
2115 $OK = false;
2116 // Remove trailing slash
2117 $path = preg_replace('|/$|', '', $path);
2118 $isWindows = DIRECTORY_SEPARATOR === '\\';
2119 if (file_exists($path)) {
2120 $OK = true;
2121 if (!is_link($path) && is_dir($path)) {
2122 if ($removeNonEmpty === true && ($handle = @opendir($path))) {
2123 $entries = [];
2124
2125 while (false !== ($file = readdir($handle))) {
2126 if ($file === '.' || $file === '..') {
2127 continue;
2128 }
2129
2130 $entries[] = $path . '/' . $file;
2131 }
2132
2133 closedir($handle);
2134
2135 foreach ($entries as $entry) {
2136 if (!static::rmdir($entry, $removeNonEmpty)) {
2137 $OK = false;
2138 }
2139 }
2140 }
2141 if ($OK) {
2142 $OK = @rmdir($path);
2143 }
2144 } elseif (is_link($path) && is_dir($path) && $isWindows) {
2145 $OK = @rmdir($path);
2146 } else {
2147 // If $path is a file, simply remove it
2148 $OK = @unlink($path);
2149 }
2150 clearstatcache();
2151 } elseif (is_link($path)) {
2152 $OK = @unlink($path);
2153 if (!$OK && $isWindows) {
2154 // Try to delete dead folder links on Windows systems
2155 $OK = @rmdir($path);
2156 }
2157 clearstatcache();
2158 }
2159 return $OK;
2160 }
2161
2162 /**
2163 * Flushes a directory by first moving to a temporary resource, and then
2164 * triggering the remove process. This way directories can be flushed faster
2165 * to prevent race conditions on concurrent processes accessing the same directory.
2166 *
2167 * @param string $directory The directory to be renamed and flushed
2168 * @param bool $keepOriginalDirectory Whether to only empty the directory and not remove it
2169 * @param bool $flushOpcodeCache Also flush the opcode cache right after renaming the directory.
2170 * @return bool Whether the action was successful
2171 */
2172 public static function flushDirectory($directory, $keepOriginalDirectory = false, $flushOpcodeCache = false)
2173 {
2174 $result = false;
2175
2176 if (is_link($directory)) {
2177 // Avoid attempting to rename the symlink see #87367
2178 $directory = realpath($directory);
2179 }
2180
2181 if (is_dir($directory)) {
2182 $temporaryDirectory = rtrim($directory, '/') . '.' . StringUtility::getUniqueId('remove');
2183 if (rename($directory, $temporaryDirectory)) {
2184 if ($flushOpcodeCache) {
2185 self::makeInstance(OpcodeCacheService::class)->clearAllActive($directory);
2186 }
2187 if ($keepOriginalDirectory) {
2188 static::mkdir($directory);
2189 }
2190 clearstatcache();
2191 $result = static::rmdir($temporaryDirectory, true);
2192 }
2193 }
2194
2195 return $result;
2196 }
2197
2198 /**
2199 * Returns an array with the names of folders in a specific path
2200 * Will return 'error' (string) if there were an error with reading directory content.
2201 *
2202 * @param string $path Path to list directories from
2203 * @return array Returns an array with the directory entries as values. If no path, the return value is nothing.
2204 */
2205 public static function get_dirs($path)
2206 {
2207 $dirs = null;
2208 if ($path) {
2209 if (is_dir($path)) {
2210 $dir = scandir($path);
2211 $dirs = [];
2212 foreach ($dir as $entry) {
2213 if (is_dir($path . '/' . $entry) && $entry !== '..' && $entry !== '.') {
2214 $dirs[] = $entry;
2215 }
2216 }
2217 } else {
2218 $dirs = 'error';
2219 }
2220 }
2221 return $dirs;
2222 }
2223
2224 /**
2225 * Finds all files in a given path and returns them as an array. Each
2226 * array key is a md5 hash of the full path to the file. This is done because
2227 * 'some' extensions like the import/export extension depend on this.
2228 *
2229 * @param string $path The path to retrieve the files from.
2230 * @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.
2231 * @param bool $prependPath If TRUE, the full path to the file is returned. If FALSE only the file name is returned.
2232 * @param string $order The sorting order. The default sorting order is alphabetical. Setting $order to 'mtime' will sort the files by modification time.
2233 * @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 '$/'.
2234 * @return array|string Array of the files found, or an error message in case the path could not be opened.
2235 */
2236 public static function getFilesInDir($path, $extensionList = '', $prependPath = false, $order = '', $excludePattern = '')
2237 {
2238 $excludePattern = (string)$excludePattern;
2239 $path = rtrim($path, '/');
2240 if (!@is_dir($path)) {
2241 return [];
2242 }
2243
2244 $rawFileList = scandir($path);
2245 if ($rawFileList === false) {
2246 return 'error opening path: "' . $path . '"';
2247 }
2248
2249 $pathPrefix = $path . '/';
2250 $allowedFileExtensionArray = self::trimExplode(',', $extensionList);
2251 $extensionList = ',' . str_replace(' ', '', $extensionList) . ',';
2252 $files = [];
2253 foreach ($rawFileList as $entry) {
2254 $completePathToEntry = $pathPrefix . $entry;
2255 if (!@is_file($completePathToEntry)) {
2256 continue;
2257 }
2258
2259 foreach ($allowedFileExtensionArray as $allowedFileExtension) {
2260 if (
2261 ($extensionList === ',,' || stripos($extensionList, ',' . substr($entry, strlen($allowedFileExtension) * -1, strlen($allowedFileExtension)) . ',') !== false)
2262 && ($excludePattern === '' || !preg_match('/^' . $excludePattern . '$/', $entry))
2263 ) {
2264 if ($order !== 'mtime') {
2265 $files[] = $entry;
2266 } else {
2267 // Store the value in the key so we can do a fast asort later.
2268 $files[$entry] = filemtime($completePathToEntry);
2269 }
2270 }
2271 }
2272 }
2273
2274 $valueName = 'value';
2275 if ($order === 'mtime') {
2276 asort($files);
2277 $valueName = 'key';
2278 }
2279
2280 $valuePathPrefix = $prependPath ? $pathPrefix : '';
2281 $foundFiles = [];
2282 foreach ($files as $key => $value) {
2283 // Don't change this ever - extensions may depend on the fact that the hash is an md5 of the path! (import/export extension)
2284 $foundFiles[md5($pathPrefix . ${$valueName})] = $valuePathPrefix . ${$valueName};
2285 }
2286
2287 return $foundFiles;
2288 }
2289
2290 /**
2291 * Recursively gather all files and folders of a path.
2292 *
2293 * @param array $fileArr Empty input array (will have files added to it)
2294 * @param string $path The path to read recursively from (absolute) (include trailing slash!)
2295 * @param string $extList Comma list of file extensions: Only files with extensions in this list (if applicable) will be selected.
2296 * @param bool $regDirs If set, directories are also included in output.
2297 * @param int $recursivityLevels The number of levels to dig down...
2298 * @param string $excludePattern regex pattern of files/directories to exclude
2299 * @return array An array with the found files/directories.
2300 */
2301 public static function getAllFilesAndFoldersInPath(array $fileArr, $path, $extList = '', $regDirs = false, $recursivityLevels = 99, $excludePattern = '')
2302 {
2303 if ($regDirs) {
2304 $fileArr[md5($path)] = $path;
2305 }
2306 $fileArr = array_merge($fileArr, self::getFilesInDir($path, $extList, 1, 1, $excludePattern));
2307 $dirs = self::get_dirs($path);
2308 if ($recursivityLevels > 0 && is_array($dirs)) {
2309 foreach ($dirs as $subdirs) {
2310 if ((string)$subdirs !== '' && ($excludePattern === '' || !preg_match('/^' . $excludePattern . '$/', $subdirs))) {
2311 $fileArr = self::getAllFilesAndFoldersInPath($fileArr, $path . $subdirs . '/', $extList, $regDirs, $recursivityLevels - 1, $excludePattern);
2312 }
2313 }
2314 }
2315 return $fileArr;
2316 }
2317
2318 /**
2319 * Removes the absolute part of all files/folders in fileArr
2320 *
2321 * @param array $fileArr The file array to remove the prefix from
2322 * @param string $prefixToRemove The prefix path to remove (if found as first part of string!)
2323 * @return array|string The input $fileArr processed, or a string with an error message, when an error occurred.
2324 */
2325 public static function removePrefixPathFromList(array $fileArr, $prefixToRemove)
2326 {
2327 foreach ($fileArr as $k => &$absFileRef) {
2328 if (self::isFirstPartOfStr($absFileRef, $prefixToRemove)) {
2329 $absFileRef = substr($absFileRef, strlen($prefixToRemove));
2330 } else {
2331 return 'ERROR: One or more of the files was NOT prefixed with the prefix-path!';
2332 }
2333 }
2334 unset($absFileRef);
2335 return $fileArr;
2336 }
2337
2338 /**
2339 * Fixes a path for windows-backslashes and reduces double-slashes to single slashes
2340 *
2341 * @param string $theFile File path to process
2342 * @return string
2343 */
2344 public static function fixWindowsFilePath($theFile)
2345 {
2346 return str_replace(['\\', '//'], '/', $theFile);
2347 }
2348
2349 /**
2350 * Resolves "../" sections in the input path string.
2351 * For example "fileadmin/directory/../other_directory/" will be resolved to "fileadmin/other_directory/"
2352 *
2353 * @param string $pathStr File path in which "/../" is resolved
2354 * @return string
2355 */
2356 public static function resolveBackPath($pathStr)
2357 {
2358 if (strpos($pathStr, '..') === false) {
2359 return $pathStr;
2360 }
2361 $parts = explode('/', $pathStr);
2362 $output = [];
2363 $c = 0;
2364 foreach ($parts as $part) {
2365 if ($part === '..') {
2366 if ($c) {
2367 array_pop($output);
2368 --$c;
2369 } else {
2370 $output[] = $part;
2371 }
2372 } else {
2373 ++$c;
2374 $output[] = $part;
2375 }
2376 }
2377 return implode('/', $output);
2378 }
2379
2380 /**
2381 * Prefixes a URL used with 'header-location' with 'http://...' depending on whether it has it already.
2382 * - If already having a scheme, nothing is prepended
2383 * - If having REQUEST_URI slash '/', then prefixing 'http://[host]' (relative to host)
2384 * - Otherwise prefixed with TYPO3_REQUEST_DIR (relative to current dir / TYPO3_REQUEST_DIR)
2385 *
2386 * @param string $path URL / path to prepend full URL addressing to.
2387 * @return string
2388 */
2389 public static function locationHeaderUrl($path)
2390 {
2391 if (strpos($path, '//') === 0) {
2392 return $path;
2393 }
2394
2395 // relative to HOST
2396 if (strpos($path, '/') === 0) {
2397 return self::getIndpEnv('TYPO3_REQUEST_HOST') . $path;
2398 }
2399
2400 $urlComponents = parse_url($path);
2401 if (!($urlComponents['scheme'] ?? false)) {
2402 // No scheme either
2403 return self::getIndpEnv('TYPO3_REQUEST_DIR') . $path;
2404 }
2405
2406 return $path;
2407 }
2408
2409 /**
2410 * Returns the maximum upload size for a file that is allowed. Measured in KB.
2411 * This might be handy to find out the real upload limit that is possible for this
2412 * TYPO3 installation.
2413 *
2414 * @return int The maximum size of uploads that are allowed (measured in kilobytes)
2415 */
2416 public static function getMaxUploadFileSize()
2417 {
2418 // Check for PHP restrictions of the maximum size of one of the $_FILES
2419 $phpUploadLimit = self::getBytesFromSizeMeasurement(ini_get('upload_max_filesize'));
2420 // Check for PHP restrictions of the maximum $_POST size
2421 $phpPostLimit = self::getBytesFromSizeMeasurement(ini_get('post_max_size'));
2422 // If the total amount of post data is smaller (!) than the upload_max_filesize directive,
2423 // then this is the real limit in PHP
2424 $phpUploadLimit = $phpPostLimit > 0 && $phpPostLimit < $phpUploadLimit ? $phpPostLimit : $phpUploadLimit;
2425 return floor($phpUploadLimit) / 1024;
2426 }
2427
2428 /**
2429 * Gets the bytes value from a measurement string like "100k".
2430 *
2431 * @param string $measurement The measurement (e.g. "100k")
2432 * @return int The bytes value (e.g. 102400)
2433 */
2434 public static function getBytesFromSizeMeasurement($measurement)
2435 {
2436 $bytes = (float)$measurement;
2437 if (stripos($measurement, 'G')) {
2438 $bytes *= 1024 * 1024 * 1024;
2439 } elseif (stripos($measurement, 'M')) {
2440 $bytes *= 1024 * 1024;
2441 } elseif (stripos($measurement, 'K')) {
2442 $bytes *= 1024;
2443 }
2444 return $bytes;
2445 }
2446
2447 /**
2448 * Function for static version numbers on files, based on the filemtime
2449 *
2450 * This will make the filename automatically change when a file is
2451 * changed, and by that re-cached by the browser. If the file does not
2452 * exist physically the original file passed to the function is
2453 * returned without the timestamp.
2454 *
2455 * Behaviour is influenced by the setting
2456 * TYPO3_CONF_VARS[TYPO3_MODE][versionNumberInFilename]
2457 * = TRUE (BE) / "embed" (FE) : modify filename
2458 * = FALSE (BE) / "querystring" (FE) : add timestamp as parameter
2459 *
2460 * @param string $file Relative path to file including all potential query parameters (not htmlspecialchared yet)
2461 * @return string Relative path with version filename including the timestamp
2462 */
2463 public static function createVersionNumberedFilename($file)
2464 {
2465 $lookupFile = explode('?', $file);
2466 $path = self::resolveBackPath(self::dirname(Environment::getCurrentScript()) . '/' . $lookupFile[0]);
2467
2468 $doNothing = false;
2469 if (TYPO3_MODE === 'FE') {
2470 $mode = strtolower($GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['versionNumberInFilename']);
2471 if ($mode === 'embed') {
2472 $mode = true;
2473 } else {
2474 if ($mode === 'querystring') {
2475 $mode = false;
2476 } else {
2477 $doNothing = true;
2478 }
2479 }
2480 } else {
2481 $mode = $GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['versionNumberInFilename'];
2482 }
2483 if ($doNothing || !file_exists($path)) {
2484 // File not found, return filename unaltered
2485 $fullName = $file;
2486 } else {
2487 if (!$mode) {
2488 // If use of .htaccess rule is not configured,
2489 // we use the default query-string method
2490 if (!empty($lookupFile[1])) {
2491 $separator = '&';
2492 } else {
2493 $separator = '?';
2494 }
2495 $fullName = $file . $separator . filemtime($path);
2496 } else {
2497 // Change the filename
2498 $name = explode('.', $lookupFile[0]);
2499 $extension = array_pop($name);
2500 array_push($name, filemtime($path), $extension);
2501 $fullName = implode('.', $name);
2502 // Append potential query string
2503 $fullName .= $lookupFile[1] ? '?' . $lookupFile[1] : '';
2504 }
2505 }
2506 return $fullName;
2507 }
2508
2509 /**
2510 * Writes string to a temporary file named after the md5-hash of the string
2511 * Quite useful for extensions adding their custom built JavaScript during runtime.
2512 *
2513 * @param string $content JavaScript to write to file.
2514 * @return string filename to include in the <script> tag
2515 */
2516 public static function writeJavaScriptContentToTemporaryFile(string $content)
2517 {
2518 $script = 'typo3temp/assets/js/' . GeneralUtility::shortMD5($content) . '.js';
2519 if (!@is_file(Environment::getPublicPath() . '/' . $script)) {
2520 self::writeFileToTypo3tempDir(Environment::getPublicPath() . '/' . $script, $content);
2521 }
2522 return $script;
2523 }
2524
2525 /**
2526 * Writes string to a temporary file named after the md5-hash of the string
2527 * Quite useful for extensions adding their custom built StyleSheet during runtime.
2528 *
2529 * @param string $content CSS styles to write to file.
2530 * @return string filename to include in the <link> tag
2531 */
2532 public static function writeStyleSheetContentToTemporaryFile(string $content)
2533 {
2534 $script = 'typo3temp/assets/css/' . self::shortMD5($content) . '.css';
2535 if (!@is_file(Environment::getPublicPath() . '/' . $script)) {
2536 self::writeFileToTypo3tempDir(Environment::getPublicPath() . '/' . $script, $content);
2537 }
2538 return $script;
2539 }
2540
2541 /*************************
2542 *
2543 * SYSTEM INFORMATION
2544 *
2545 *************************/
2546
2547 /**
2548 * Returns the link-url to the current script.
2549 * 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.
2550 * REMEMBER to always use htmlspecialchars() for content in href-properties to get ampersands converted to entities (XHTML requirement and XSS precaution)
2551 *
2552 * @param array $getParams Array of GET parameters to include
2553 * @return string
2554 */
2555 public static function linkThisScript(array $getParams = [])
2556 {
2557 $parts = self::getIndpEnv('SCRIPT_NAME');
2558 $params = self::_GET();
2559 foreach ($getParams as $key => $value) {
2560 if ($value !== '') {
2561 $params[$key] = $value;
2562 } else {
2563 unset($params[$key]);
2564 }
2565 }
2566 $pString = self::implodeArrayForUrl('', $params);
2567 return $pString ? $parts . '?' . ltrim($pString, '&') : $parts;
2568 }
2569
2570 /**
2571 * 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.
2572 * So basically it adds the parameters in $getParams to an existing URL, $url
2573 *
2574 * @param string $url URL string
2575 * @param array $getParams Array of key/value pairs for get parameters to add/overrule with. Can be multidimensional.
2576 * @return string Output URL with added getParams.
2577 */
2578 public static function linkThisUrl($url, array $getParams = [])
2579 {
2580 $parts = parse_url($url);
2581 $getP = [];
2582 if ($parts['query']) {
2583 parse_str($parts['query'], $getP);
2584 }
2585 ArrayUtility::mergeRecursiveWithOverrule($getP, $getParams);
2586 $uP = explode('?', $url);
2587 $params = self::implodeArrayForUrl('', $getP);
2588 $outurl = $uP[0] . ($params ? '?' . substr($params, 1) : '');
2589 return $outurl;
2590 }
2591
2592 /**
2593 * This method is only for testing and should never be used outside tests-
2594 *
2595 * @param $envName
2596 * @param $value
2597 * @internal
2598 */
2599 public static function setIndpEnv($envName, $value)
2600 {
2601 self::$indpEnvCache[$envName] = $value;
2602 }
2603
2604 /**
2605 * Abstraction method which returns System Environment Variables regardless of server OS, CGI/MODULE version etc. Basically this is SERVER variables for most of them.
2606 * This should be used instead of getEnv() and $_SERVER/ENV_VARS to get reliable values for all situations.
2607 *
2608 * @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
2609 * @return string Value based on the input key, independent of server/os environment.
2610 * @throws \UnexpectedValueException
2611 */
2612 public static function getIndpEnv($getEnvName)
2613 {
2614 if (array_key_exists($getEnvName, self::$indpEnvCache)) {
2615 return self::$indpEnvCache[$getEnvName];
2616 }
2617
2618 /*
2619 Conventions:
2620 output from parse_url():
2621 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
2622 [scheme] => 'http'
2623 [user] => 'username'
2624 [pass] => 'password'
2625 [host] => '192.168.1.4'
2626 [port] => '8080'
2627 [path] => '/typo3/32/temp/phpcheck/index.php/arg1/arg2/arg3/'
2628 [query] => 'arg1,arg2,arg3&p1=parameter1&p2[key]=value'
2629 [fragment] => 'link1'Further definition: [path_script] = '/typo3/32/temp/phpcheck/index.php'
2630 [path_dir] = '/typo3/32/temp/phpcheck/'
2631 [path_info] = '/arg1/arg2/arg3/'
2632 [path] = [path_script/path_dir][path_info]Keys supported:URI______:
2633 REQUEST_URI = [path]?[query] = /typo3/32/temp/phpcheck/index.php/arg1/arg2/arg3/?arg1,arg2,arg3&p1=parameter1&p2[key]=value
2634 HTTP_HOST = [host][:[port]] = 192.168.1.4:8080
2635 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')!
2636 PATH_INFO = [path_info] = /arg1/arg2/arg3/
2637 QUERY_STRING = [query] = arg1,arg2,arg3&p1=parameter1&p2[key]=value
2638 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
2639 (Notice: NO username/password + NO fragment)CLIENT____:
2640 REMOTE_ADDR = (client IP)
2641 REMOTE_HOST = (client host)
2642 HTTP_USER_AGENT = (client user agent)
2643 HTTP_ACCEPT_LANGUAGE = (client accept language)SERVER____:
2644 SCRIPT_FILENAME = Absolute filename of script (Differs between windows/unix). On windows 'C:\\blabla\\blabl\\' will be converted to 'C:/blabla/blabl/'Special extras:
2645 TYPO3_HOST_ONLY = [host] = 192.168.1.4
2646 TYPO3_PORT = [port] = 8080 (blank if 80, taken from host value)
2647 TYPO3_REQUEST_HOST = [scheme]://[host][:[port]]
2648 TYPO3_REQUEST_URL = [scheme]://[host][:[port]][path]?[query] (scheme will by default be "http" until we can detect something different)
2649 TYPO3_REQUEST_SCRIPT = [scheme]://[host][:[port]][path_script]
2650 TYPO3_REQUEST_DIR = [scheme]://[host][:[port]][path_dir]
2651 TYPO3_SITE_URL = [scheme]://[host][:[port]][path_dir] of the TYPO3 website frontend
2652 TYPO3_SITE_PATH = [path_dir] of the TYPO3 website frontend
2653 TYPO3_SITE_SCRIPT = [script / Speaking URL] of the TYPO3 website
2654 TYPO3_DOCUMENT_ROOT = Absolute path of root of documents: TYPO3_DOCUMENT_ROOT.SCRIPT_NAME = SCRIPT_FILENAME (typically)
2655 TYPO3_SSL = Returns TRUE if this session uses SSL/TLS (https)
2656 TYPO3_PROXY = Returns TRUE if this session runs over a well known proxyNotice: [fragment] is apparently NEVER available to the script!Testing suggestions:
2657 - Output all the values.
2658 - 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
2659 - ALSO TRY the script from the ROOT of a site (like 'http://www.mytest.com/' and not 'http://www.mytest.com/test/' !!)
2660 */
2661 $retVal = '';
2662 switch ((string)$getEnvName) {
2663 case 'SCRIPT_NAME':
2664 $retVal = self::isRunningOnCgiServerApi()
2665 && (($_SERVER['ORIG_PATH_INFO'] ?? false) ?: ($_SERVER['PATH_INFO'] ?? false))
2666 ? (($_SERVER['ORIG_PATH_INFO'] ?? '') ?: ($_SERVER['PATH_INFO'] ?? ''))
2667 : (($_SERVER['ORIG_SCRIPT_NAME'] ?? '') ?: ($_SERVER['SCRIPT_NAME'] ?? ''));
2668 // Add a prefix if TYPO3 is behind a proxy: ext-domain.com => int-server.com/prefix
2669 if (self::cmpIP($_SERVER['REMOTE_ADDR'] ?? '', $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP'] ?? '')) {
2670 if (self::getIndpEnv('TYPO3_SSL') && $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefixSSL']) {
2671 $retVal = $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefixSSL'] . $retVal;
2672 } elseif ($GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefix']) {
2673 $retVal = $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefix'] . $retVal;
2674 }
2675 }
2676 break;
2677 case 'SCRIPT_FILENAME':
2678 $retVal = Environment::getCurrentScript();
2679 break;
2680 case 'REQUEST_URI':
2681 // Typical application of REQUEST_URI is return urls, forms submitting to itself etc. Example: returnUrl='.rawurlencode(\TYPO3\CMS\Core\Utility\GeneralUtility::getIndpEnv('REQUEST_URI'))
2682 if (!empty($GLOBALS['TYPO3_CONF_VARS']['SYS']['requestURIvar'])) {
2683 // This is for URL rewriters that store the original URI in a server variable (eg ISAPI_Rewriter for IIS: HTTP_X_REWRITE_URL)
2684 list($v, $n) = explode('|', $GLOBALS['TYPO3_CONF_VARS']['SYS']['requestURIvar']);
2685 $retVal = $GLOBALS[$v][$n];
2686 } elseif (empty($_SERVER['REQUEST_URI'])) {
2687 // This is for ISS/CGI which does not have the REQUEST_URI available.
2688 $retVal = '/' . ltrim(self::getIndpEnv('SCRIPT_NAME'), '/') . (!empty($_SERVER['QUERY_STRING']) ? '?' . $_SERVER['QUERY_STRING'] : '');
2689 } else {
2690 $retVal = '/' . ltrim($_SERVER['REQUEST_URI'], '/');
2691 }
2692 // Add a prefix if TYPO3 is behind a proxy: ext-domain.com => int-server.com/prefix
2693 if (isset($_SERVER['REMOTE_ADDR'], $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP'])
2694 && self::cmpIP($_SERVER['REMOTE_ADDR'], $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP'])
2695 ) {
2696 if (self::getIndpEnv('TYPO3_SSL') && $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefixSSL']) {
2697 $retVal = $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefixSSL'] . $retVal;
2698 } elseif ($GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefix']) {
2699 $retVal = $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefix'] . $retVal;
2700 }
2701 }
2702 break;
2703 case 'PATH_INFO':
2704 // $_SERVER['PATH_INFO'] != $_SERVER['SCRIPT_NAME'] is necessary because some servers (Windows/CGI)
2705 // are seen to set PATH_INFO equal to script_name
2706 // Further, there must be at least one '/' in the path - else the PATH_INFO value does not make sense.
2707 // IF 'PATH_INFO' never works for our purpose in TYPO3 with CGI-servers,
2708 // then 'PHP_SAPI=='cgi'' might be a better check.
2709 // Right now strcmp($_SERVER['PATH_INFO'], GeneralUtility::getIndpEnv('SCRIPT_NAME')) will always
2710 // return FALSE for CGI-versions, but that is only as long as SCRIPT_NAME is set equal to PATH_INFO
2711 // because of PHP_SAPI=='cgi' (see above)
2712 if (!self::isRunningOnCgiServerApi()) {
2713 $retVal = $_SERVER['PATH_INFO'];
2714 }
2715 break;
2716 case 'TYPO3_REV_PROXY':
2717 $retVal = self::cmpIP($_SERVER['REMOTE_ADDR'], $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP']);
2718 break;
2719 case 'REMOTE_ADDR':
2720 $retVal = $_SERVER['REMOTE_ADDR'] ?? null;
2721 if (self::cmpIP($retVal, $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP'] ?? '')) {
2722 $ip = self::trimExplode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
2723 // Choose which IP in list to use
2724 if (!empty($ip)) {
2725 switch ($GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyHeaderMultiValue']) {
2726 case 'last':
2727 $ip = array_pop($ip);
2728 break;
2729 case 'first':
2730 $ip = array_shift($ip);
2731 break;
2732 case 'none':
2733
2734 default:
2735 $ip = '';
2736 }
2737 }
2738 if (self::validIP($ip)) {
2739 $retVal = $ip;
2740 }
2741 }
2742 break;
2743 case 'HTTP_HOST':
2744 // if it is not set we're most likely on the cli
2745 $retVal = $_SERVER['HTTP_HOST'] ?? null;
2746 if (isset($_SERVER['REMOTE_ADDR']) && static::cmpIP($_SERVER['REMOTE_ADDR'], $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP'])) {
2747 $host = self::trimExplode(',', $_SERVER['HTTP_X_FORWARDED_HOST']);
2748 // Choose which host in list to use
2749 if (!empty($host)) {
2750 switch ($GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyHeaderMultiValue']) {
2751 case 'last':
2752 $host = array_pop($host);
2753 break;
2754 case 'first':
2755 $host = array_shift($host);
2756 break;
2757 case 'none':
2758
2759 default:
2760 $host = '';
2761 }
2762 }
2763 if ($host) {
2764 $retVal = $host;
2765 }
2766 }
2767 if (!static::isAllowedHostHeaderValue($retVal)) {
2768 throw new \UnexpectedValueException(
2769 '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.',
2770 1396795884
2771 );
2772 }
2773 break;
2774 case 'HTTP_REFERER':
2775
2776 case 'HTTP_USER_AGENT':
2777
2778 case 'HTTP_ACCEPT_ENCODING':
2779
2780 case 'HTTP_ACCEPT_LANGUAGE':
2781
2782 case 'REMOTE_HOST':
2783
2784 case 'QUERY_STRING':
2785 $retVal = $_SERVER[$getEnvName] ?? '';
2786 break;
2787 case 'TYPO3_DOCUMENT_ROOT':
2788 // Get the web root (it is not the root of the TYPO3 installation)
2789 // The absolute path of the script can be calculated with TYPO3_DOCUMENT_ROOT + SCRIPT_FILENAME
2790 // 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.
2791 // Therefore the DOCUMENT_ROOT is now always calculated as the SCRIPT_FILENAME minus the end part shared with SCRIPT_NAME.
2792 $SFN = self::getIndpEnv('SCRIPT_FILENAME');
2793 $SN_A = explode('/', strrev(self::getIndpEnv('SCRIPT_NAME')));
2794 $SFN_A = explode('/', strrev($SFN));
2795 $acc = [];
2796 foreach ($SN_A as $kk => $vv) {
2797 if ((string)$SFN_A[$kk] === (string)$vv) {
2798 $acc[] = $vv;
2799 } else {
2800 break;
2801 }
2802 }
2803 $commonEnd = strrev(implode('/', $acc));
2804 if ((string)$commonEnd !== '') {
2805 $retVal = substr($SFN, 0, -(strlen($commonEnd) + 1));
2806 }
2807 break;
2808 case 'TYPO3_HOST_ONLY':
2809 $httpHost = self::getIndpEnv('HTTP_HOST');
2810 $httpHostBracketPosition = strpos($httpHost, ']');
2811 $httpHostParts = explode(':', $httpHost);
2812 $retVal = $httpHostBracketPosition !== false ? substr($httpHost, 0, $httpHostBracketPosition + 1) : array_shift($httpHostParts);
2813 break;
2814 case 'TYPO3_PORT':
2815 $httpHost = self::getIndpEnv('HTTP_HOST');
2816 $httpHostOnly = self::getIndpEnv('TYPO3_HOST_ONLY');
2817 $retVal = strlen($httpHost) > strlen($httpHostOnly) ? substr($httpHost, strlen($httpHostOnly) + 1) : '';
2818 break;
2819 case 'TYPO3_REQUEST_HOST':
2820 $retVal = (self::getIndpEnv('TYPO3_SSL') ? 'https://' : 'http://') . self::getIndpEnv('HTTP_HOST');
2821 break;
2822 case 'TYPO3_REQUEST_URL':
2823 $retVal = self::getIndpEnv('TYPO3_REQUEST_HOST') . self::getIndpEnv('REQUEST_URI');
2824 break;
2825 case 'TYPO3_REQUEST_SCRIPT':
2826 $retVal = self::getIndpEnv('TYPO3_REQUEST_HOST') . self::getIndpEnv('SCRIPT_NAME');
2827 break;
2828 case 'TYPO3_REQUEST_DIR':
2829 $retVal = self::getIndpEnv('TYPO3_REQUEST_HOST') . self::dirname(self::getIndpEnv('SCRIPT_NAME')) . '/';
2830 break;
2831 case 'TYPO3_SITE_URL':
2832 $url = self::getIndpEnv('TYPO3_REQUEST_DIR');
2833 // This can only be set by external entry scripts
2834 if (defined('TYPO3_PATH_WEB')) {
2835 $retVal = $url;
2836 } elseif (Environment::getCurrentScript()) {
2837 $lPath = PathUtility::stripPathSitePrefix(PathUtility::dirnameDuringBootstrap(Environment::getCurrentScript())) . '/';
2838 $siteUrl = substr($url, 0, -strlen($lPath));
2839 if (substr($siteUrl, -1) !== '/') {
2840 $siteUrl .= '/';
2841 }
2842 $retVal = $siteUrl;
2843 }
2844 break;
2845 case 'TYPO3_SITE_PATH':
2846 $retVal = substr(self::getIndpEnv('TYPO3_SITE_URL'), strlen(self::getIndpEnv('TYPO3_REQUEST_HOST')));
2847 break;
2848 case 'TYPO3_SITE_SCRIPT':
2849 $retVal = substr(self::getIndpEnv('TYPO3_REQUEST_URL'), strlen(self::getIndpEnv('TYPO3_SITE_URL')));
2850 break;
2851 case 'TYPO3_SSL':
2852 $proxySSL = trim($GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxySSL'] ?? null);
2853 if ($proxySSL === '*') {
2854 $proxySSL = $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP'];
2855 }
2856 if (self::cmpIP($_SERVER['REMOTE_ADDR'] ?? '', $proxySSL)) {
2857 $retVal = true;
2858 } else {
2859 // https://secure.php.net/manual/en/reserved.variables.server.php
2860 // "Set to a non-empty value if the script was queried through the HTTPS protocol."
2861 $retVal = !empty($_SERVER['SSL_SESSION_ID'])
2862 || (!empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) !== 'off');
2863 }
2864 break;
2865 case '_ARRAY':
2866 $out = [];
2867 // Here, list ALL possible keys to this function for debug display.
2868 $envTestVars = [
2869 'HTTP_HOST',
2870 'TYPO3_HOST_ONLY',
2871 'TYPO3_PORT',
2872 'PATH_INFO',
2873 'QUERY_STRING',
2874 'REQUEST_URI',
2875 'HTTP_REFERER',
2876 'TYPO3_REQUEST_HOST',
2877 'TYPO3_REQUEST_URL',
2878 'TYPO3_REQUEST_SCRIPT',
2879 'TYPO3_REQUEST_DIR',
2880 'TYPO3_SITE_URL',
2881 'TYPO3_SITE_SCRIPT',
2882 'TYPO3_SSL',
2883 'TYPO3_REV_PROXY',
2884 'SCRIPT_NAME',
2885 'TYPO3_DOCUMENT_ROOT',
2886 'SCRIPT_FILENAME',
2887 'REMOTE_ADDR',
2888 'REMOTE_HOST',
2889 'HTTP_USER_AGENT',
2890 'HTTP_ACCEPT_LANGUAGE'
2891 ];
2892 foreach ($envTestVars as $v) {
2893 $out[$v] = self::getIndpEnv($v);
2894 }
2895 reset($out);
2896 $retVal = $out;
2897 break;
2898 }
2899 self::$indpEnvCache[$getEnvName] = $retVal;
2900 return $retVal;
2901 }
2902
2903 /**
2904 * Checks if the provided host header value matches the trusted hosts pattern.
2905 * If the pattern is not defined (which only can happen early in the bootstrap), deny any value.
2906 * The result is saved, so the check needs to be executed only once.
2907 *
2908 * @param string $hostHeaderValue HTTP_HOST header value as sent during the request (may include port)
2909 * @return bool
2910 */
2911 public static function isAllowedHostHeaderValue($hostHeaderValue)
2912 {
2913 if (static::$allowHostHeaderValue === true) {
2914 return true;
2915 }
2916
2917 if (static::isInternalRequestType()) {
2918 return static::$allowHostHeaderValue = true;
2919 }
2920
2921 // Deny the value if trusted host patterns is empty, which means we are early in the bootstrap
2922 if (empty($GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern'])) {
2923 return false;
2924 }
2925
2926 if ($GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern'] === self::ENV_TRUSTED_HOSTS_PATTERN_ALLOW_ALL) {
2927 static::$allowHostHeaderValue = true;
2928 } else {
2929 static::$allowHostHeaderValue = static::hostHeaderValueMatchesTrustedHostsPattern($hostHeaderValue);
2930 }
2931
2932 return static::$allowHostHeaderValue;
2933 }
2934
2935 /**
2936 * Checks if the provided host header value matches the trusted hosts pattern without any preprocessing.
2937 *
2938 * @param string $hostHeaderValue
2939 * @return bool
2940 * @internal
2941 */
2942 public static function hostHeaderValueMatchesTrustedHostsPattern($hostHeaderValue)
2943 {
2944 if ($GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern'] === self::ENV_TRUSTED_HOSTS_PATTERN_SERVER_NAME) {
2945 // Allow values that equal the server name
2946 // Note that this is only secure if name base virtual host are configured correctly in the webserver
2947 $defaultPort = self::getIndpEnv('TYPO3_SSL') ? '443' : '80';
2948 $parsedHostValue = parse_url('http://' . $hostHeaderValue);
2949 if (isset($parsedHostValue['port'])) {
2950 $hostMatch = (strtolower($parsedHostValue['host']) === strtolower($_SERVER['SERVER_NAME']) && (string)$parsedHostValue['port'] === $_SERVER['SERVER_PORT']);
2951 } else {
2952 $hostMatch = (strtolower($hostHeaderValue) === strtolower($_SERVER['SERVER_NAME']) && $defaultPort === $_SERVER['SERVER_PORT']);
2953 }
2954 } else {
2955 // In case name based virtual hosts are not possible, we allow setting a trusted host pattern
2956 // See https://typo3.org/teams/security/security-bulletins/typo3-core/typo3-core-sa-2014-001/ for further details
2957 $hostMatch = (bool)preg_match('/^' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern'] . '$/i', $hostHeaderValue);
2958 }
2959
2960 return $hostMatch;
2961 }
2962
2963 /**
2964 * Allows internal requests to the install tool and from the command line.
2965 * We accept this risk to have the install tool always available.
2966 * Also CLI needs to be allowed as unfortunately AbstractUserAuthentication::getAuthInfoArray()
2967 * accesses HTTP_HOST without reason on CLI
2968 * Additionally, allows requests when no REQUESTTYPE is set, which can happen quite early in the
2969 * Bootstrap. See Application.php in EXT:backend/Classes/Http/.
2970 *
2971 * @return bool
2972 */
2973 protected static function isInternalRequestType()
2974 {
2975 return Environment::isCli() || !defined('TYPO3_REQUESTTYPE') || (defined('TYPO3_REQUESTTYPE') && TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_INSTALL);
2976 }
2977
2978 /**
2979 * Gets the unixtime as milliseconds.
2980 *
2981 * @return int The unixtime as milliseconds
2982 */
2983 public static function milliseconds()
2984 {
2985 return round(microtime(true) * 1000);
2986 }
2987
2988 /*************************
2989 *
2990 * TYPO3 SPECIFIC FUNCTIONS
2991 *
2992 *************************/
2993 /**
2994 * Returns the absolute filename of a relative reference, resolves the "EXT:" prefix
2995 * (way of referring to files inside extensions) and checks that the file is inside
2996 * the TYPO3's base folder and implies a check with
2997 * \TYPO3\CMS\Core\Utility\GeneralUtility::validPathStr().
2998 *
2999 * @param string $filename The input filename/filepath to evaluate
3000 * @return string Returns the absolute filename of $filename if valid, otherwise blank string.
3001 */
3002 public static function getFileAbsFileName($filename)
3003 {
3004 if ((string)$filename === '') {
3005 return '';
3006 }
3007 // Extension
3008 if (strpos($filename, 'EXT:') === 0) {
3009 list($extKey, $local) = explode('/', substr($filename, 4), 2);
3010