‪TYPO3CMS  ‪main
GeneralUtility.php
Go to the documentation of this file.
1 <?php
2 
3 declare(strict_types=1);
4 
5 /*
6  * This file is part of the TYPO3 CMS project.
7  *
8  * It is free software; you can redistribute it and/or modify it under
9  * the terms of the GNU General Public License, either version 2
10  * of the License, or any later version.
11  *
12  * For the full copyright and license information, please read the
13  * LICENSE.txt file that was distributed with this source code.
14  *
15  * The TYPO3 project - inspiring people to share!
16  */
17 
19 
20 use Egulias\EmailValidator\EmailValidator;
21 use Egulias\EmailValidator\Validation\EmailValidation;
22 use Egulias\EmailValidator\Validation\MultipleValidationWithAnd;
23 use Egulias\EmailValidator\Validation\RFCValidation;
24 use Egulias\EmailValidator\Warning\CFWSNearAt;
25 use GuzzleHttp\Exception\RequestException;
26 use Psr\Container\ContainerInterface;
27 use Psr\Http\Message\ServerRequestInterface;
28 use Psr\Log\LoggerAwareInterface;
29 use Psr\Log\LoggerInterface;
39 
52 {
53  protected static ?ContainerInterface ‪$container = null;
54 
60  protected static array ‪$singletonInstances = [];
61 
67  protected static array ‪$nonSingletonInstances = [];
68 
75  protected static array ‪$finalClassNameCache = [];
76 
80  protected static array ‪$indpEnvCache = [];
81 
82  final private function ‪__construct() {}
83 
92  public static function ‪fixed_lgd_cs(string $string, int $chars, string $appendString = '...'): string
93  {
94  if ($chars === 0 || mb_strlen($string, 'utf-8') <= abs($chars)) {
95  return $string;
96  }
97  if ($chars > 0) {
98  $string = mb_substr($string, 0, $chars, 'utf-8') . $appendString;
99  } else {
100  $string = $appendString . mb_substr($string, $chars, mb_strlen($string, 'utf-8'), 'utf-8');
101  }
102  return $string;
103  }
104 
113  public static function ‪cmpIP(string $baseIP, string $list): bool
114  {
115  $list = trim($list);
116  if ($list === '') {
117  return false;
118  }
119  if ($list === '*') {
120  return true;
121  }
122  if (str_contains($baseIP, ':') && self::validIPv6($baseIP)) {
123  return ‪self::cmpIPv6($baseIP, $list);
124  }
125  return ‪self::cmpIPv4($baseIP, $list);
126  }
127 
135  public static function ‪cmpIPv4(string $baseIP, string $list): bool
136  {
137  $IPpartsReq = explode('.', $baseIP);
138  if (count($IPpartsReq) === 4) {
139  $values = ‪self::trimExplode(',', $list, true);
140  foreach ($values as $test) {
141  $testList = explode('/', $test);
142  if (count($testList) === 2) {
143  [$test, $mask] = $testList;
144  } else {
145  $mask = false;
146  }
147  if ((int)$mask) {
148  $mask = (int)$mask;
149  // "192.168.3.0/24"
150  $lnet = (int)ip2long($test);
151  $lip = (int)ip2long($baseIP);
152  $binnet = str_pad(decbin($lnet), 32, '0', STR_PAD_LEFT);
153  $firstpart = substr($binnet, 0, $mask);
154  $binip = str_pad(decbin($lip), 32, '0', STR_PAD_LEFT);
155  $firstip = substr($binip, 0, $mask);
156  $yes = $firstpart === $firstip;
157  } else {
158  // "192.168.*.*"
159  $IPparts = explode('.', $test);
160  $yes = 1;
161  foreach ($IPparts as $index => $val) {
162  $val = trim($val);
163  if ($val !== '*' && $IPpartsReq[$index] !== $val) {
164  $yes = 0;
165  }
166  }
167  }
168  if ($yes) {
169  return true;
170  }
171  }
172  }
173  return false;
174  }
175 
184  public static function ‪cmpIPv6(string $baseIP, string $list): bool
185  {
186  // Policy default: Deny connection
187  $success = false;
188  $baseIP = ‪self::normalizeIPv6($baseIP);
189  $values = ‪self::trimExplode(',', $list, true);
190  foreach ($values as $test) {
191  $testList = explode('/', $test);
192  if (count($testList) === 2) {
193  [$test, $mask] = $testList;
194  } else {
195  $mask = false;
196  }
197  if (self::validIPv6($test)) {
198  $test = ‪self::normalizeIPv6($test);
199  $maskInt = (int)$mask ?: 128;
200  // Special case; /0 is an allowed mask - equals a wildcard
201  if ($mask === '0') {
202  $success = true;
203  } elseif ($maskInt == 128) {
204  $success = $test === $baseIP;
205  } else {
206  $testBin = (string)inet_pton($test);
207  $baseIPBin = (string)inet_pton($baseIP);
208 
209  $success = true;
210  // Modulo is 0 if this is a 8-bit-boundary
211  $maskIntModulo = $maskInt % 8;
212  $numFullCharactersUntilBoundary = (int)($maskInt / 8);
213  $substring = (string)substr($baseIPBin, 0, $numFullCharactersUntilBoundary);
214  if (!str_starts_with($testBin, $substring)) {
215  $success = false;
216  } elseif ($maskIntModulo > 0) {
217  // If not an 8-bit-boundary, check bits of last character
218  $testLastBits = str_pad(decbin(ord(substr($testBin, $numFullCharactersUntilBoundary, 1))), 8, '0', STR_PAD_LEFT);
219  $baseIPLastBits = str_pad(decbin(ord(substr($baseIPBin, $numFullCharactersUntilBoundary, 1))), 8, '0', STR_PAD_LEFT);
220  if (strncmp($testLastBits, $baseIPLastBits, $maskIntModulo) != 0) {
221  $success = false;
222  }
223  }
224  }
225  }
226  if ($success) {
227  return true;
228  }
229  }
230  return false;
231  }
232 
239  public static function ‪normalizeIPv6(string $address): string
240  {
241  $normalizedAddress = '';
242  // According to RFC lowercase-representation is recommended
243  $address = strtolower($address);
244  // Normalized representation has 39 characters (0000:0000:0000:0000:0000:0000:0000:0000)
245  if (strlen($address) === 39) {
246  // Already in full expanded form
247  return $address;
248  }
249  // Count 2 if if address has hidden zero blocks
250  $chunks = explode('::', $address);
251  if (count($chunks) === 2) {
252  $chunksLeft = explode(':', $chunks[0]);
253  $chunksRight = explode(':', $chunks[1]);
254  $left = count($chunksLeft);
255  $right = count($chunksRight);
256  // Special case: leading zero-only blocks count to 1, should be 0
257  if ($left === 1 && strlen($chunksLeft[0]) === 0) {
258  $left = 0;
259  }
260  $hiddenBlocks = 8 - ($left + $right);
261  $hiddenPart = '';
262  $h = 0;
263  while ($h < $hiddenBlocks) {
264  $hiddenPart .= '0000:';
265  $h++;
266  }
267  if ($left === 0) {
268  $stageOneAddress = $hiddenPart . $chunks[1];
269  } else {
270  $stageOneAddress = $chunks[0] . ':' . $hiddenPart . $chunks[1];
271  }
272  } else {
273  $stageOneAddress = $address;
274  }
275  // Normalize the blocks:
276  $blocks = explode(':', $stageOneAddress);
277  $divCounter = 0;
278  foreach ($blocks as $block) {
279  $tmpBlock = '';
280  $i = 0;
281  $hiddenZeros = 4 - strlen($block);
282  while ($i < $hiddenZeros) {
283  $tmpBlock .= '0';
284  $i++;
285  }
286  $normalizedAddress .= $tmpBlock . $block;
287  if ($divCounter < 7) {
288  $normalizedAddress .= ':';
289  $divCounter++;
290  }
291  }
292  return $normalizedAddress;
293  }
294 
303  public static function ‪validIP(string $ip): bool
304  {
305  return filter_var($ip, FILTER_VALIDATE_IP) !== false;
306  }
307 
316  public static function ‪validIPv4(string $ip): bool
317  {
318  return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false;
319  }
320 
329  public static function ‪validIPv6(string $ip): bool
330  {
331  return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false;
332  }
333 
341  public static function ‪cmpFQDN(string $baseHost, string $list): bool
342  {
343  $baseHost = trim($baseHost);
344  if (empty($baseHost)) {
345  return false;
346  }
347  if (self::validIPv4($baseHost) || self::validIPv6($baseHost)) {
348  // Resolve hostname
349  // Note: this is reverse-lookup and can be randomly set as soon as somebody is able to set
350  // the reverse-DNS for his IP (security when for example used with REMOTE_ADDR)
351  $baseHostName = (string)gethostbyaddr($baseHost);
352  if ($baseHostName === $baseHost) {
353  // Unable to resolve hostname
354  return false;
355  }
356  } else {
357  $baseHostName = $baseHost;
358  }
359  $baseHostNameParts = explode('.', $baseHostName);
360  $values = ‪self::trimExplode(',', $list, true);
361  foreach ($values as $test) {
362  $hostNameParts = explode('.', $test);
363  // To match hostNameParts can only be shorter (in case of wildcards) or equal
364  $hostNamePartsCount = count($hostNameParts);
365  $baseHostNamePartsCount = count($baseHostNameParts);
366  if ($hostNamePartsCount > $baseHostNamePartsCount) {
367  continue;
368  }
369  $yes = true;
370  foreach ($hostNameParts as $index => $val) {
371  $val = trim($val);
372  if ($val === '*') {
373  // Wildcard valid for one or more hostname-parts
374  $wildcardStart = $index + 1;
375  // Wildcard as last/only part always matches, otherwise perform recursive checks
376  if ($wildcardStart < $hostNamePartsCount) {
377  $wildcardMatched = false;
378  $tempHostName = implode('.', array_slice($hostNameParts, $index + 1));
379  while ($wildcardStart < $baseHostNamePartsCount && !$wildcardMatched) {
380  $tempBaseHostName = implode('.', array_slice($baseHostNameParts, $wildcardStart));
381  $wildcardMatched = ‪self::cmpFQDN($tempBaseHostName, $tempHostName);
382  $wildcardStart++;
383  }
384  if ($wildcardMatched) {
385  // Match found by recursive compare
386  return true;
387  }
388  $yes = false;
389  }
390  } elseif ($baseHostNameParts[$index] !== $val) {
391  // In case of no match
392  $yes = false;
393  }
394  }
395  if ($yes) {
396  return true;
397  }
398  }
399  return false;
400  }
401 
409  public static function ‪isOnCurrentHost(string ‪$url): bool
410  {
411  return stripos(‪$url . '/', self::getIndpEnv('TYPO3_REQUEST_HOST') . '/') === 0;
412  }
413 
422  public static function ‪inList($list, $item)
423  {
424  return str_contains(',' . $list . ',', ',' . $item . ',');
425  }
426 
434  public static function ‪expandList($list)
435  {
436  $items = explode(',', $list);
437  $list = [];
438  foreach ($items as $item) {
439  $range = explode('-', $item);
440  if (isset($range[1])) {
441  $runAwayBrake = 1000;
442  for ($n = $range[0]; $n <= $range[1]; $n++) {
443  $list[] = $n;
444  $runAwayBrake--;
445  if ($runAwayBrake <= 0) {
446  break;
447  }
448  }
449  } else {
450  $list[] = $item;
451  }
452  }
453  return implode(',', $list);
454  }
455 
462  public static function ‪md5int($str)
463  {
464  return hexdec(substr(md5($str), 0, 7));
465  }
466 
475  public static function ‪hmac($input, $additionalSecret = '')
476  {
477  trigger_error(
478  'GeneralUtility::hmac() is deprecated and will be removed in TYPO3 v14. Use TYPO3\CMS\Core\Crypto\HashService instead.',
479  E_USER_DEPRECATED
480  );
481  $hashAlgorithm = 'sha1';
482  $secret = ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'] . $additionalSecret;
483  return hash_hmac($hashAlgorithm, $input, $secret);
484  }
485 
492  public static function split_fileref($fileNameWithPath)
493  {
494  $info = [];
495  $reg = [];
496  if (preg_match('/(.*\\/)(.*)$/', $fileNameWithPath, $reg)) {
497  $info['path'] = $reg[1];
498  $info['file'] = $reg[2];
499  } else {
500  $info['path'] = '';
501  $info['file'] = $fileNameWithPath;
502  }
503  $reg = '';
504  // If open_basedir is set and the fileName was supplied without a path the is_dir check fails
505  if (!is_dir($fileNameWithPath) && preg_match('/(.*)\\.([^\\.]*$)/', $info['file'], $reg)) {
506  $info['filebody'] = $reg[1];
507  $info['fileext'] = strtolower($reg[2]);
508  $info['realFileext'] = $reg[2];
509  } else {
510  $info['filebody'] = $info['file'];
511  $info['fileext'] = '';
512  }
513  reset($info);
514  return $info;
515  }
516 
532  public static function dirname($path)
533  {
534  $p = ‪self::revExplode('/', $path, 2);
535  return count($p) === 2 ? $p[0] : '';
536  }
537 
546  public static function formatSize(‪$sizeInBytes, $labels = '', $base = 0)
547  {
548  $defaultFormats = [
549  'iec' => ['base' => 1024, 'labels' => [' ', ' Ki', ' Mi', ' Gi', ' Ti', ' Pi', ' Ei', ' Zi', ' Yi']],
550  'si' => ['base' => 1000, 'labels' => [' ', ' k', ' M', ' G', ' T', ' P', ' E', ' Z', ' Y']],
551  ];
552  // Set labels and base:
553  if (empty($labels)) {
554  $labels = 'iec';
555  }
556  if (isset($defaultFormats[$labels])) {
557  $base = $defaultFormats[$labels]['base'];
558  ‪$labelArr = $defaultFormats[$labels]['labels'];
559  } else {
560  $base = (int)$base;
561  if ($base !== 1000 && $base !== 1024) {
562  $base = 1024;
563  }
564  ‪$labelArr = explode('|', str_replace('"', '', $labels));
565  }
566  // This is set via Site Handling and in the Locales class via setlocale()
567  // LC_NUMERIC is not set because of side effects when calculating with floats
568  // see @\TYPO3\CMS\Core\Localization\Locales::setLocale
569  ‪$currentLocale = setlocale(LC_MONETARY, '0');
570  ‪$oldLocale = setlocale(LC_NUMERIC, '0');
571  setlocale(LC_NUMERIC, ‪$currentLocale);
572  ‪$localeInfo = localeconv();
573  setlocale(LC_NUMERIC, ‪$oldLocale);
574 
576  ‪$multiplier = floor((‪$sizeInBytes ? log(‪$sizeInBytes) : 0) / log($base));
578  if (‪$sizeInUnits > ($base * .9)) {
579  ‪$multiplier++;
580  }
583  return number_format(‪$sizeInUnits, ((‪$multiplier > 0) && (‪$sizeInUnits < 20)) ? 2 : 0, ‪$localeInfo['decimal_point'], '') . ‪$labelArr[‪$multiplier];
584  }
585 
595  public static function splitCalc($string, $operators)
596  {
597  $res = [];
598  $sign = '+';
599  while ($string) {
600  $valueLen = strcspn($string, $operators);
601  $value = substr($string, 0, $valueLen);
602  $res[] = [$sign, trim($value)];
603  $sign = substr($string, $valueLen, 1);
604  $string = substr($string, $valueLen + 1);
605  }
606  reset($res);
607  return $res;
608  }
609 
616  public static function validEmail(string $email): bool
617  {
618  if (trim($email) !== $email) {
619  return false;
620  }
621  if (!str_contains($email, '@')) {
622  return false;
623  }
624 
625  $validators = [];
626  foreach (‪$GLOBALS['TYPO3_CONF_VARS']['MAIL']['validators'] ?? [RFCValidation::class] as $className) {
627  ‪$validator = new $className();
628  if (‪$validator instanceof EmailValidation) {
629  $validators[] = ‪$validator;
630  }
631  }
632 
633  $emailValidator = new EmailValidator();
634  ‪$isValid = $emailValidator->isValid($email, new MultipleValidationWithAnd($validators, MultipleValidationWithAnd::STOP_ON_ERROR));
635 
636  // Currently, the RFCValidation doesn't recognise "email @example.com"
637  // as an invalid email for historic reasons - catch it here
638  // see https://github.com/egulias/EmailValidator/issues/374
639  if (‪$isValid) {
640  // If email is valid, check if we have CFWSNearAt warning and
641  // treat it as an invalid email, i.e "email @example.com"
642  foreach ($emailValidator->getWarnings() as $warning) {
643  if ($warning instanceof CFWSNearAt) {
644  return false;
645  }
646  }
647  }
648 
649  return ‪$isValid;
650  }
651 
659  public static function ‪underscoredToUpperCamelCase($string)
660  {
661  return str_replace(' ', '', ucwords(str_replace('_', ' ', strtolower($string))));
662  }
663 
671  public static function ‪underscoredToLowerCamelCase($string)
672  {
673  return lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', strtolower($string)))));
674  }
675 
683  public static function ‪camelCaseToLowerCaseUnderscored($string)
684  {
685  $value = preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $string) ?? '';
686  return mb_strtolower($value, 'utf-8');
687  }
688 
713  public static function ‪isValidUrl(string ‪$url): bool
714  {
715  $parsedUrl = parse_url(‪$url);
716  if (!$parsedUrl || !isset($parsedUrl['scheme'])) {
717  return false;
718  }
719  // HttpUtility::buildUrl() will always build urls with <scheme>://
720  // our original $url might only contain <scheme>: (e.g. mail:)
721  // so we convert that to the double-slashed version to ensure
722  // our check against the $recomposedUrl is proper
723  if (!str_starts_with(‪$url, $parsedUrl['scheme'] . '://')) {
724  ‪$url = str_replace($parsedUrl['scheme'] . ':', $parsedUrl['scheme'] . '://', ‪$url);
725  }
726  $recomposedUrl = ‪HttpUtility::buildUrl($parsedUrl);
727  if ($recomposedUrl !== ‪$url) {
728  // The parse_url() had to modify characters, so the URL is invalid
729  return false;
730  }
731  if (isset($parsedUrl['host']) && !preg_match('/^[a-z0-9.\\-]*$/i', $parsedUrl['host'])) {
732  $host = idn_to_ascii($parsedUrl['host']);
733  if ($host === false) {
734  return false;
735  }
736  $parsedUrl['host'] = $host;
737  }
738  return filter_var(‪HttpUtility::buildUrl($parsedUrl), FILTER_VALIDATE_URL) !== false;
739  }
740 
741  /*************************
742  *
743  * ARRAY FUNCTIONS
744  *
745  *************************/
746 
756  public static function ‪intExplode(string $delimiter, string $string, bool $removeEmptyValues = false): array
757  {
758  $result = explode($delimiter, $string);
759  foreach ($result as $key => &$value) {
760  if ($removeEmptyValues && trim($value) === '') {
761  unset($result[$key]);
762  } else {
763  $value = (int)$value;
764  }
765  }
766  unset($value);
767 
769  return array_values($result);
770  }
771 
787  public static function ‪revExplode(string $delimiter, string $string, int $limit = 0): array
788  {
789  // 2 is the (currently, as of 2014-02) most-used value for `$limit` in the core, therefore we check it first
790  if ($limit === 2) {
791  $position = strrpos($string, strrev($delimiter));
792  if ($position !== false) {
793  return [substr($string, 0, $position), substr($string, $position + strlen($delimiter))];
794  }
795  return [$string];
796  }
797  if ($limit <= 1) {
798  return [$string];
799  }
800  $explodedValues = explode($delimiter, strrev($string), $limit);
801  $explodedValues = array_map(strrev(...), $explodedValues);
802  return array_reverse($explodedValues);
803  }
804 
822  public static function ‪trimExplode(string $delim, string $string, bool $removeEmptyValues = false, int $limit = 0): array
823  {
824  $result = explode($delim, $string);
825  if ($removeEmptyValues) {
826  // Remove items that are just whitespace, but leave whitespace intact for the rest.
827  $result = array_values(array_filter($result, static fn(string $item): bool => trim($item) !== ''));
828  }
829 
830  if ($limit === 0) {
831  // Return everything.
832  return array_map(trim(...), $result);
833  }
834 
835  if ($limit < 0) {
836  // Trim and return just the first $limit elements and ignore the rest.
837  return array_map(trim(...), array_slice($result, 0, $limit));
838  }
839 
840  // Fold the last length - $limit elements into a single trailing item, then trim and return the result.
841  $tail = array_slice($result, $limit - 1);
842  $result = array_slice($result, 0, $limit - 1);
843  if ($tail) {
844  $result[] = implode($delim, $tail);
845  }
846  return array_map(trim(...), $result);
847  }
848 
860  public static function ‪implodeArrayForUrl(string $name, array $theArray, string $str = '', bool $skipBlank = false, bool $rawurlencodeParamName = false): string
861  {
862  foreach ($theArray as $Akey => $AVal) {
863  $thisKeyName = $name ? $name . '[' . $Akey . ']' : $Akey;
864  if (is_array($AVal)) {
865  $str = ‪self::implodeArrayForUrl($thisKeyName, $AVal, $str, $skipBlank, $rawurlencodeParamName);
866  } else {
867  $stringValue = (string)$AVal;
868  if (!$skipBlank || $stringValue !== '') {
869  $parameterName = $rawurlencodeParamName ? rawurlencode($thisKeyName) : $thisKeyName;
870  $parameterValue = rawurlencode($stringValue);
871  $str .= '&' . $parameterName . '=' . $parameterValue;
872  }
873  }
874  }
875  return $str;
876  }
877 
893  public static function explodeUrl2Array(string $string): array
894  {
895  ‪$output = [];
896  $p = explode('&', $string);
897  foreach ($p as $v) {
898  if ($v !== '') {
899  $nameAndValue = explode('=', $v, 2);
900  ‪$output[rawurldecode($nameAndValue[0])] = isset($nameAndValue[1]) ? rawurldecode($nameAndValue[1]) : '';
901  }
902  }
903  return ‪$output;
904  }
905 
913  public static function removeDotsFromTS(array $ts): array
914  {
915  $out = [];
916  foreach ($ts as $key => $value) {
917  if (is_array($value)) {
918  $key = rtrim($key, '.');
919  $out[$key] = self::removeDotsFromTS($value);
920  } else {
921  $out[$key] = $value;
922  }
923  }
924  return $out;
925  }
926 
927  /*************************
928  *
929  * HTML/XML PROCESSING
930  *
931  *************************/
941  public static function get_tag_attributes(string $tag, bool $decodeEntities = false): array
942  {
943  $components = self::split_tag_attributes($tag);
944  // Attribute name is stored here
945  $name = '';
946  $valuemode = false;
947  $attributes = [];
948  foreach ($components as $key => $val) {
949  // 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
950  if ($val !== '=') {
951  if ($valuemode) {
952  if ($name) {
953  $attributes[$name] = $decodeEntities ? htmlspecialchars_decode($val) : $val;
954  $name = '';
955  }
956  } else {
957  if ($key = strtolower(preg_replace('/[^[:alnum:]_\\:\\-]/', '', $val) ?? '')) {
958  $attributes[$key] = '';
959  $name = $key;
960  }
961  }
962  $valuemode = false;
963  } else {
964  $valuemode = true;
965  }
966  }
967  return $attributes;
968  }
969 
977  public static function split_tag_attributes(string $tag): array
978  {
979  $tag_tmp = trim(preg_replace('/^<[^[:space:]]*/', '', trim($tag)) ?? '');
980  // Removes any > in the end of the string
981  $tag_tmp = trim(rtrim($tag_tmp, '>'));
982  $value = [];
983  // Compared with empty string instead , 030102
984  while ($tag_tmp !== '') {
985  $firstChar = $tag_tmp[0];
986  if ($firstChar === '"' || $firstChar === '\'') {
987  $reg = explode($firstChar, $tag_tmp, 3);
988  $value[] = $reg[1];
989  $tag_tmp = trim($reg[2] ?? '');
990  } elseif ($firstChar === '=') {
991  $value[] = '=';
992  // Removes = chars.
993  $tag_tmp = trim(substr($tag_tmp, 1));
994  } else {
995  // There are '' around the value. We look for the next ' ' or '>'
996  $reg = preg_split('/[[:space:]=]/', $tag_tmp, 2);
997  $value[] = trim($reg[0]);
998  $tag_tmp = trim(substr($tag_tmp, strlen($reg[0]), 1) . ($reg[1] ?? ''));
999  }
1000  }
1001  reset($value);
1002  return $value;
1003  }
1004 
1013  public static function implodeAttributes(array $arr, bool $xhtmlSafe = false, bool $keepBlankAttributes = false): string
1014  {
1015  if ($xhtmlSafe) {
1016  $newArr = [];
1017  foreach ($arr as $attributeName => $attributeValue) {
1018  $attributeName = strtolower($attributeName);
1019  if (!isset($newArr[$attributeName])) {
1020  $newArr[$attributeName] = htmlspecialchars((string)$attributeValue);
1021  }
1022  }
1023  $arr = $newArr;
1024  }
1025  $list = [];
1026  foreach ($arr as $attributeName => $attributeValue) {
1027  if ((string)$attributeValue !== '' || $keepBlankAttributes) {
1028  $list[] = $attributeName . '="' . $attributeValue . '"';
1029  }
1030  }
1031  return implode(' ', $list);
1032  }
1033 
1043  public static function wrapJS(string $string, array $attributes = []): string
1044  {
1045  if (trim($string)) {
1046  // remove nl from the beginning
1047  $string = ltrim($string, LF);
1048  // re-ident to one tab using the first line as reference
1049  $match = [];
1050  if (preg_match('/^(\\t+)/', $string, $match)) {
1051  $string = str_replace($match[1], "\t", $string);
1052  }
1053  return '<script ' . GeneralUtility::implodeAttributes($attributes, true) . '>
1054 /*<![CDATA[*/
1055 ' . $string . '
1056 /*]]>*/
1057 </script>';
1058  }
1059  return '';
1060  }
1061 
1070  public static function xml2tree(string $string, int $depth = 999, array $parserOptions = []): array|string
1071  {
1072  ‪$parser = xml_parser_create();
1073  $vals = [];
1074  $index = [];
1075  xml_parser_set_option(‪$parser, XML_OPTION_CASE_FOLDING, 0);
1076  xml_parser_set_option(‪$parser, XML_OPTION_SKIP_WHITE, 0);
1077  foreach ($parserOptions as $option => $value) {
1078  xml_parser_set_option(‪$parser, $option, $value);
1079  }
1080  xml_parse_into_struct(‪$parser, $string, $vals, $index);
1081  if (xml_get_error_code(‪$parser)) {
1082  return 'Line ' . xml_get_current_line_number(‪$parser) . ': ' . xml_error_string(xml_get_error_code(‪$parser));
1083  }
1084  xml_parser_free(‪$parser);
1085  $stack = [[]];
1086  $stacktop = 0;
1087  $startPoint = 0;
1088  $tagi = [];
1089  foreach ($vals as $key => $val) {
1090  $type = $val['type'];
1091  // open tag:
1092  if ($type === 'open' || $type === 'complete') {
1093  $stack[$stacktop++] = $tagi;
1094  if ($depth == $stacktop) {
1095  $startPoint = $key;
1096  }
1097  $tagi = ['tag' => $val['tag']];
1098  if (isset($val['attributes'])) {
1099  $tagi['attrs'] = $val['attributes'];
1100  }
1101  if (isset($val['value'])) {
1102  $tagi['values'][] = $val['value'];
1103  }
1104  }
1105  // finish tag:
1106  if ($type === 'complete' || $type === 'close') {
1107  $oldtagi = $tagi;
1108  $tagi = $stack[--$stacktop];
1109  $oldtag = $oldtagi['tag'];
1110  unset($oldtagi['tag']);
1111  if ($depth == $stacktop + 1) {
1112  if ($key - $startPoint > 0) {
1113  $partArray = array_slice($vals, $startPoint + 1, $key - $startPoint - 1);
1114  $oldtagi['XMLvalue'] = ‪self::xmlRecompileFromStructValArray($partArray);
1115  } else {
1116  $oldtagi['XMLvalue'] = $oldtagi['values'][0];
1117  }
1118  }
1119  $tagi['ch'][$oldtag][] = $oldtagi;
1120  unset($oldtagi);
1121  }
1122  // cdata
1123  if ($type === 'cdata') {
1124  $tagi['values'][] = $val['value'];
1125  }
1126  }
1127  return $tagi['ch'];
1128  }
1129 
1150  public static function array2xml(array $array, string $NSprefix = '', int $level = 0, string $docTag = 'phparray', int ‪$spaceInd = 0, array $options = [], array $stackData = []): string
1151  {
1152  // 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
1153  $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);
1154  // Set indenting mode:
1155  $indentChar = ‪$spaceInd ? ' ' : "\t";
1156  $indentN = ‪$spaceInd > 0 ? ‪$spaceInd : 1;
1157  ‪$nl = ‪$spaceInd >= 0 ? LF : '';
1158  // Init output variable:
1160  // Traverse the input array
1161  foreach ($array as $k => $v) {
1162  $attr = '';
1163  $tagName = (string)$k;
1164  // Construct the tag name.
1165  // Use tag based on grand-parent + parent tag name
1166  if (isset($stackData['grandParentTagName'], $stackData['parentTagName'], $options['grandParentTagMap'][$stackData['grandParentTagName'] . '/' . $stackData['parentTagName']])) {
1167  $attr .= ' index="' . htmlspecialchars($tagName) . '"';
1168  $tagName = (string)$options['grandParentTagMap'][$stackData['grandParentTagName'] . '/' . $stackData['parentTagName']];
1169  } elseif (isset($stackData['parentTagName'], $options['parentTagMap'][$stackData['parentTagName'] . ':_IS_NUM']) && ‪MathUtility::canBeInterpretedAsInteger($tagName)) {
1170  // Use tag based on parent tag name + if current tag is numeric
1171  $attr .= ' index="' . htmlspecialchars($tagName) . '"';
1172  $tagName = (string)$options['parentTagMap'][$stackData['parentTagName'] . ':_IS_NUM'];
1173  } elseif (isset($stackData['parentTagName'], $options['parentTagMap'][$stackData['parentTagName'] . ':' . $tagName])) {
1174  // Use tag based on parent tag name + current tag
1175  $attr .= ' index="' . htmlspecialchars($tagName) . '"';
1176  $tagName = (string)$options['parentTagMap'][$stackData['parentTagName'] . ':' . $tagName];
1177  } elseif (isset($stackData['parentTagName'], $options['parentTagMap'][$stackData['parentTagName']])) {
1178  // Use tag based on parent tag name:
1179  $attr .= ' index="' . htmlspecialchars($tagName) . '"';
1180  $tagName = (string)$options['parentTagMap'][$stackData['parentTagName']];
1181  } elseif (‪MathUtility::canBeInterpretedAsInteger($tagName)) {
1182  // If integer...;
1183  if ($options['useNindex'] ?? false) {
1184  // If numeric key, prefix "n"
1185  $tagName = 'n' . $tagName;
1186  } else {
1187  // Use special tag for num. keys:
1188  $attr .= ' index="' . $tagName . '"';
1189  $tagName = ($options['useIndexTagForNum'] ?? false) ?: 'numIndex';
1190  }
1191  } elseif (!empty($options['useIndexTagForAssoc'])) {
1192  // Use tag for all associative keys:
1193  $attr .= ' index="' . htmlspecialchars($tagName) . '"';
1194  $tagName = $options['useIndexTagForAssoc'];
1195  }
1196  // The tag name is cleaned up so only alphanumeric chars (plus - and _) are in there and not longer than 100 chars either.
1197  $tagName = substr(preg_replace('/[^[:alnum:]_-]/', '', $tagName), 0, 100);
1198  // If the value is an array then we will call this function recursively:
1199  if (is_array($v)) {
1200  // Sub elements:
1201  if (isset($options['alt_options']) && ($options['alt_options'][($stackData['path'] ?? '') . '/' . $tagName] ?? false)) {
1202  $subOptions = $options['alt_options'][($stackData['path'] ?? '') . '/' . $tagName];
1203  $clearStackPath = (bool)($subOptions['clearStackPath'] ?? false);
1204  } else {
1205  $subOptions = $options;
1206  $clearStackPath = false;
1207  }
1208  if (empty($v)) {
1209  $content = '';
1210  } else {
1211  $content = ‪$nl . self::array2xml($v, $NSprefix, $level + 1, '', ‪$spaceInd, $subOptions, [
1212  'parentTagName' => $tagName,
1213  'grandParentTagName' => $stackData['parentTagName'] ?? '',
1214  'path' => $clearStackPath ? '' : ($stackData['path'] ?? '') . '/' . $tagName,
1215  ]) . (‪$spaceInd >= 0 ? str_pad('', ($level + 1) * $indentN, $indentChar) : '');
1216  }
1217  // Do not set "type = array". Makes prettier XML but means that empty arrays are not restored with xml2array
1218  if (!isset($options['disableTypeAttrib']) || (int)$options['disableTypeAttrib'] != 2) {
1219  $attr .= ' type="array"';
1220  }
1221  } else {
1222  $stringValue = (string)$v;
1223  // Just a value:
1224  // Look for binary chars:
1225  $vLen = strlen($stringValue);
1226  // Go for base64 encoding if the initial segment NOT matching any binary char has the same length as the whole string!
1227  if ($vLen && strcspn($stringValue, $binaryChars) != $vLen) {
1228  // If the value contained binary chars then we base64-encode it and set an attribute to notify this situation:
1229  $content = ‪$nl . chunk_split(base64_encode($stringValue));
1230  $attr .= ' base64="1"';
1231  } else {
1232  // Otherwise, just htmlspecialchar the stuff:
1233  $content = htmlspecialchars($stringValue);
1234  $dType = gettype($v);
1235  if ($dType !== 'string' && !($options['disableTypeAttrib'] ?? false)) {
1236  $attr .= ' type="' . $dType . '"';
1237  }
1238  }
1239  }
1240  if ($tagName !== '') {
1241  // Add the element to the output string:
1242  ‪$output .= (‪$spaceInd >= 0 ? str_pad('', ($level + 1) * $indentN, $indentChar) : '')
1243  . '<' . $NSprefix . $tagName . $attr . '>' . $content . '</' . $NSprefix . $tagName . '>' . ‪$nl;
1244  }
1245  }
1246  // If we are at the outer-most level, then we finally wrap it all in the document tags and return that as the value:
1247  if (!$level) {
1248  ‪$output = '<' . $docTag . '>' . ‪$nl . ‪$output . '</' . $docTag . '>';
1249  }
1250  return ‪$output;
1251  }
1252 
1265  public static function ‪xml2array(string $string, string $NSprefix = '', bool $reportDocTag = false): array|string
1266  {
1267  $runtimeCache = static::makeInstance(CacheManager::class)->getCache('runtime');
1268  $firstLevelCache = $runtimeCache->get('generalUtilityXml2Array') ?: [];
1269  ‪$identifier = md5($string . $NSprefix . ($reportDocTag ? '1' : '0'));
1270  // Look up in first level cache
1271  if (empty($firstLevelCache[‪$identifier])) {
1272  $firstLevelCache[‪$identifier] = ‪self::xml2arrayProcess($string, $NSprefix, $reportDocTag);
1273  $runtimeCache->set('generalUtilityXml2Array', $firstLevelCache);
1274  }
1275  return $firstLevelCache[‪$identifier];
1276  }
1277 
1288  public static function ‪xml2arrayProcess(string $string, string $NSprefix = '', bool $reportDocTag = false): array|string
1289  {
1290  $string = trim((string)$string);
1291  // Create parser:
1292  ‪$parser = xml_parser_create();
1293  $vals = [];
1294  $index = [];
1295  xml_parser_set_option(‪$parser, XML_OPTION_CASE_FOLDING, 0);
1296  xml_parser_set_option(‪$parser, XML_OPTION_SKIP_WHITE, 0);
1297  // Default output charset is UTF-8, only ASCII, ISO-8859-1 and UTF-8 are supported!!!
1298  $match = [];
1299  preg_match('/^[[:space:]]*<\\?xml[^>]*encoding[[:space:]]*=[[:space:]]*"([^"]*)"/', substr($string, 0, 200), $match);
1300  $theCharset = $match[1] ?? 'utf-8';
1301  // us-ascii / utf-8 / iso-8859-1
1302  xml_parser_set_option(‪$parser, XML_OPTION_TARGET_ENCODING, $theCharset);
1303  // Parse content:
1304  xml_parse_into_struct(‪$parser, $string, $vals, $index);
1305  // If error, return error message:
1306  if (xml_get_error_code(‪$parser)) {
1307  return 'Line ' . xml_get_current_line_number(‪$parser) . ': ' . xml_error_string(xml_get_error_code(‪$parser));
1308  }
1309  xml_parser_free(‪$parser);
1310  // Init vars:
1311  $stack = [[]];
1312  $stacktop = 0;
1313  $current = [];
1314  $tagName = '';
1315  $documentTag = '';
1316  // Traverse the parsed XML structure:
1317  foreach ($vals as $key => $val) {
1318  // First, process the tag-name (which is used in both cases, whether "complete" or "close")
1319  $tagName = $val['tag'];
1320  if (!$documentTag) {
1321  $documentTag = $tagName;
1322  }
1323  // Test for name space:
1324  $tagName = $NSprefix && str_starts_with($tagName, $NSprefix) ? substr($tagName, strlen($NSprefix)) : $tagName;
1325  // Test for numeric tag, encoded on the form "nXXX":
1326  $testNtag = substr($tagName, 1);
1327  // Closing tag.
1328  $tagName = $tagName[0] === 'n' && ‪MathUtility::canBeInterpretedAsInteger($testNtag) ? (int)$testNtag : $tagName;
1329  // Test for alternative index value:
1330  if ((string)($val['attributes']['index'] ?? '') !== '') {
1331  $tagName = $val['attributes']['index'];
1332  }
1333  // Setting tag-values, manage stack:
1334  switch ($val['type']) {
1335  case 'open':
1336  // If open tag it means there is an array stored in sub-elements. Therefore increase the stackpointer and reset the accumulation array:
1337  // Setting blank place holder
1338  $current[$tagName] = [];
1339  $stack[$stacktop++] = $current;
1340  $current = [];
1341  break;
1342  case 'close':
1343  // If the tag is "close" then it is an array which is closing and we decrease the stack pointer.
1344  $oldCurrent = $current;
1345  $current = $stack[--$stacktop];
1346  // Going to the end of array to get placeholder key, key($current), and fill in array next:
1347  end($current);
1348  $current[key($current)] = $oldCurrent;
1349  unset($oldCurrent);
1350  break;
1351  case 'complete':
1352  // If "complete", then it's a value. If the attribute "base64" is set, then decode the value, otherwise just set it.
1353  if (!empty($val['attributes']['base64'])) {
1354  $current[$tagName] = base64_decode($val['value']);
1355  } else {
1356  // Had to cast it as a string - otherwise it would be evaluate FALSE if tested with isset()!!
1357  $current[$tagName] = (string)($val['value'] ?? '');
1358  // Cast type:
1359  switch ((string)($val['attributes']['type'] ?? '')) {
1360  case 'integer':
1361  $current[$tagName] = (int)$current[$tagName];
1362  break;
1363  case 'double':
1364  $current[$tagName] = (float)$current[$tagName];
1365  break;
1366  case 'boolean':
1367  $current[$tagName] = (bool)$current[$tagName];
1368  break;
1369  case 'NULL':
1370  $current[$tagName] = null;
1371  break;
1372  case 'array':
1373  // 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...
1374  $current[$tagName] = [];
1375  break;
1376  }
1377  }
1378  break;
1379  }
1380  }
1381  if ($reportDocTag) {
1382  $current[$tagName]['_DOCUMENT_TAG'] = $documentTag;
1383  }
1384  // Finally return the content of the document tag.
1385  return $current[$tagName];
1386  }
1387 
1394  public static function ‪xmlRecompileFromStructValArray(array $vals): string
1395  {
1396  $XMLcontent = '';
1397  foreach ($vals as $val) {
1398  $type = $val['type'];
1399  // Open tag:
1400  if ($type === 'open' || $type === 'complete') {
1401  $XMLcontent .= '<' . $val['tag'];
1402  if (isset($val['attributes'])) {
1403  foreach ($val['attributes'] as $k => $v) {
1404  $XMLcontent .= ' ' . $k . '="' . htmlspecialchars($v) . '"';
1405  }
1406  }
1407  if ($type === 'complete') {
1408  if (isset($val['value'])) {
1409  $XMLcontent .= '>' . htmlspecialchars($val['value']) . '</' . $val['tag'] . '>';
1410  } else {
1411  $XMLcontent .= '/>';
1412  }
1413  } else {
1414  $XMLcontent .= '>';
1415  }
1416  if ($type === 'open' && isset($val['value'])) {
1417  $XMLcontent .= htmlspecialchars($val['value']);
1418  }
1419  }
1420  // Finish tag:
1421  if ($type === 'close') {
1422  $XMLcontent .= '</' . $val['tag'] . '>';
1423  }
1424  // Cdata
1425  if ($type === 'cdata') {
1426  $XMLcontent .= htmlspecialchars($val['value']);
1427  }
1428  }
1429  return $XMLcontent;
1430  }
1431 
1432  /*************************
1433  *
1434  * FILES FUNCTIONS
1435  *
1436  *************************/
1444  public static function ‪getUrl(string ‪$url): string|false
1445  {
1446  // Looks like it's an external file, use Guzzle by default
1447  if (preg_match('/^(?:http|ftp)s?|s(?:ftp|cp):/', ‪$url)) {
1448  $requestFactory = static::makeInstance(RequestFactory::class);
1449  try {
1450  $response = $requestFactory->request(‪$url);
1451  } catch (RequestException $exception) {
1452  return false;
1453  }
1454  $content = $response->getBody()->getContents();
1455  } else {
1456  $content = @file_get_contents(‪$url);
1457  }
1458  return $content;
1459  }
1460 
1469  public static function ‪writeFile(string $file, string $content, bool $changePermissions = false): bool
1470  {
1471  if (!@is_file($file)) {
1472  $changePermissions = true;
1473  }
1474  if ($fd = fopen($file, 'wb')) {
1475  $res = fwrite($fd, $content);
1476  fclose($fd);
1477  if ($res === false) {
1478  return false;
1479  }
1480  // Change the permissions only if the file has just been created
1481  if ($changePermissions) {
1482  static::fixPermissions($file);
1483  }
1484  return true;
1485  }
1486  return false;
1487  }
1488 
1496  public static function ‪fixPermissions(string $path, bool $recursive = false): bool
1497  {
1498  $targetPermissions = null;
1499  if (‪Environment::isWindows()) {
1500  return true;
1501  }
1502  $result = false;
1503  // Make path absolute
1504  if (!‪PathUtility::isAbsolutePath($path)) {
1505  $path = static::getFileAbsFileName($path);
1506  }
1507  if (static::isAllowedAbsPath($path)) {
1508  if (@is_file($path)) {
1509  $targetPermissions = (string)(‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['fileCreateMask'] ?? '0644');
1510  } elseif (@is_dir($path)) {
1511  $targetPermissions = (string)(‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['folderCreateMask'] ?? '0755');
1512  }
1513  if (!empty($targetPermissions)) {
1514  // make sure it's always 4 digits
1515  $targetPermissions = str_pad($targetPermissions, 4, '0', STR_PAD_LEFT);
1516  $targetPermissions = octdec($targetPermissions);
1517  // "@" is there because file is not necessarily OWNED by the user
1518  $result = @chmod($path, (int)$targetPermissions);
1519  }
1520  // Set createGroup if not empty
1521  if (
1522  isset(‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['createGroup'])
1523  && ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['createGroup'] !== ''
1524  ) {
1525  // "@" is there because file is not necessarily OWNED by the user
1526  $changeGroupResult = @chgrp($path, ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['createGroup']);
1527  $result = $changeGroupResult ? $result : false;
1528  }
1529  // Call recursive if recursive flag if set and $path is directory
1530  if ($recursive && @is_dir($path)) {
1531  $handle = opendir($path);
1532  if (is_resource($handle)) {
1533  while (($file = readdir($handle)) !== false) {
1534  $recursionResult = null;
1535  if ($file !== '.' && $file !== '..') {
1536  if (@is_file($path . '/' . $file)) {
1537  $recursionResult = static::fixPermissions($path . '/' . $file);
1538  } elseif (@is_dir($path . '/' . $file)) {
1539  $recursionResult = static::fixPermissions($path . '/' . $file, true);
1540  }
1541  if (isset($recursionResult) && !$recursionResult) {
1542  $result = false;
1543  }
1544  }
1545  }
1546  closedir($handle);
1547  }
1548  }
1549  }
1550  return $result;
1551  }
1552 
1561  public static function ‪writeFileToTypo3tempDir(string $filepath, string $content): ?string
1562  {
1563  // Parse filepath into directory and basename:
1564  $fI = pathinfo($filepath);
1565  $fI['dirname'] .= '/';
1566  // Check parts:
1567  if (!static::validPathStr($filepath) || !$fI['basename'] || strlen($fI['basename']) >= 60) {
1568  return 'Input filepath "' . $filepath . '" was generally invalid!';
1569  }
1570 
1571  // Setting main temporary directory name (standard)
1572  $allowedPathPrefixes = [
1573  ‪Environment::getPublicPath() . '/typo3temp' => 'Environment::getPublicPath() + "/typo3temp/"',
1574  ];
1575  // Also allow project-path + /var/
1576  if (‪Environment::getVarPath() !== ‪Environment::getPublicPath() . '/typo3temp/var') {
1577  $relPath = substr(‪Environment::getVarPath(), strlen(‪Environment::getProjectPath()) + 1);
1578  $allowedPathPrefixes[‪Environment::getVarPath()] = 'ProjectPath + ' . $relPath;
1579  }
1580 
1581  $errorMessage = null;
1582  foreach ($allowedPathPrefixes as $pathPrefix => $prefixLabel) {
1583  $dirName = $pathPrefix . '/';
1584  // Invalid file path, let's check for the other path, if it exists
1585  if (!str_starts_with($fI['dirname'], $dirName)) {
1586  if ($errorMessage === null) {
1587  $errorMessage = '"' . $fI['dirname'] . '" was not within directory ' . $prefixLabel;
1588  }
1589  continue;
1590  }
1591  // This resets previous error messages from the first path
1592  $errorMessage = null;
1593 
1594  if (!@is_dir($dirName)) {
1595  $errorMessage = $prefixLabel . ' was not a directory!';
1596  // continue and see if the next iteration resets the errorMessage above
1597  continue;
1598  }
1599  // Checking if the "subdir" is found
1600  $subdir = substr($fI['dirname'], strlen($dirName));
1601  if ($subdir) {
1602  if (preg_match('#^(?:[[:alnum:]_]+/)+$#', $subdir)) {
1603  $dirName .= $subdir;
1604  if (!@is_dir($dirName)) {
1605  static::mkdir_deep($pathPrefix . '/' . $subdir);
1606  }
1607  } else {
1608  $errorMessage = 'Subdir, "' . $subdir . '", was NOT on the form "[[:alnum:]_]/+"';
1609  break;
1610  }
1611  }
1612  // Checking dir-name again (sub-dir might have been created)
1613  if (@is_dir($dirName)) {
1614  if ($filepath === $dirName . $fI['basename']) {
1615  static::writeFile($filepath, $content);
1616  if (!@is_file($filepath)) {
1617  $errorMessage = 'The file was not written to the disk. Please, check that you have write permissions to the ' . $prefixLabel . ' directory.';
1618  }
1619  break;
1620  }
1621  $errorMessage = 'Calculated file location didn\'t match input "' . $filepath . '".';
1622  break;
1623  }
1624  $errorMessage = '"' . $dirName . '" is not a directory!';
1625  break;
1626  }
1627  return $errorMessage;
1628  }
1629 
1638  public static function ‪mkdir(string $newFolder): bool
1639  {
1640  $result = @‪mkdir($newFolder, (int)octdec((string)(‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['folderCreateMask'] ?? '0')));
1641  if ($result) {
1642  static::fixPermissions($newFolder);
1643  }
1644  return $result;
1645  }
1646 
1654  public static function ‪mkdir_deep(string $directory): void
1655  {
1656  // Ensure there is only one slash
1657  $fullPath = rtrim($directory, '/') . '/';
1658  if ($fullPath !== '/' && !is_dir($fullPath)) {
1659  $firstCreatedPath = static::createDirectoryPath($fullPath);
1660  if ($firstCreatedPath !== '') {
1661  static::fixPermissions($firstCreatedPath, true);
1662  }
1663  }
1664  }
1665 
1675  protected static function ‪createDirectoryPath(string $fullDirectoryPath): string
1676  {
1677  $currentPath = $fullDirectoryPath;
1678  $firstCreatedPath = '';
1679  $permissionMask = (int)octdec((string)(‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['folderCreateMask'] ?? '0'));
1680  if (!@is_dir($currentPath)) {
1681  do {
1682  $firstCreatedPath = $currentPath;
1683  $separatorPosition = (int)strrpos($currentPath, DIRECTORY_SEPARATOR);
1684  $currentPath = substr($currentPath, 0, $separatorPosition);
1685  } while (!is_dir($currentPath) && $separatorPosition > 0);
1686  $result = @‪mkdir($fullDirectoryPath, $permissionMask, true);
1687  // Check existence of directory again to avoid race condition. Directory could have get created by another process between previous is_dir() and mkdir()
1688  if (!$result && !@is_dir($fullDirectoryPath)) {
1689  throw new \RuntimeException('Could not create directory "' . $fullDirectoryPath . '"!', 1170251401);
1690  }
1691  }
1692  return $firstCreatedPath;
1693  }
1694 
1702  public static function ‪rmdir(string $path, bool $removeNonEmpty = false): bool
1703  {
1704  $OK = false;
1705  // Remove trailing slash
1706  $path = preg_replace('|/$|', '', $path) ?? '';
1707  $isWindows = DIRECTORY_SEPARATOR === '\\';
1708  if (file_exists($path)) {
1709  $OK = true;
1710  if (!is_link($path) && is_dir($path)) {
1711  if ($removeNonEmpty === true && ($handle = @opendir($path))) {
1712  $entries = [];
1713 
1714  while (false !== ($file = readdir($handle))) {
1715  if ($file === '.' || $file === '..') {
1716  continue;
1717  }
1718 
1719  $entries[] = $path . '/' . $file;
1720  }
1721 
1722  closedir($handle);
1723 
1724  foreach ($entries as $entry) {
1725  if (!static::rmdir($entry, $removeNonEmpty)) {
1726  $OK = false;
1727  }
1728  }
1729  }
1730  if ($OK) {
1731  $OK = @‪rmdir($path);
1732  }
1733  } elseif (is_link($path) && is_dir($path) && $isWindows) {
1734  $OK = @‪rmdir($path);
1735  } else {
1736  // If $path is a file, simply remove it
1737  $OK = @unlink($path);
1738  }
1739  clearstatcache();
1740  } elseif (is_link($path)) {
1741  $OK = @unlink($path);
1742  if (!$OK && $isWindows) {
1743  // Try to delete dead folder links on Windows systems
1744  $OK = @‪rmdir($path);
1745  }
1746  clearstatcache();
1747  }
1748  return $OK;
1749  }
1750 
1759  public static function ‪get_dirs(string $path): array|string|null
1760  {
1761  $dirs = null;
1762  if ($path) {
1763  if (is_dir($path)) {
1764  ‪$dir = scandir($path);
1765  $dirs = [];
1766  foreach (‪$dir as $entry) {
1767  if (is_dir($path . '/' . $entry) && $entry !== '..' && $entry !== '.') {
1768  $dirs[] = $entry;
1769  }
1770  }
1771  } else {
1772  $dirs = 'error';
1773  }
1774  }
1775  return $dirs;
1776  }
1777 
1790  public static function getFilesInDir(string $path, string $extensionList = '', bool $prependPath = false, string $order = '', string $excludePattern = ''): array|string
1791  {
1792  $excludePattern = (string)$excludePattern;
1793  $path = rtrim($path, '/');
1794  if (!@is_dir($path)) {
1795  return [];
1796  }
1797 
1798  $rawFileList = scandir($path);
1799  if ($rawFileList === false) {
1800  return 'error opening path: "' . $path . '"';
1801  }
1802 
1803  $pathPrefix = $path . '/';
1804  $allowedFileExtensionArray = ‪self::trimExplode(',', $extensionList);
1805  $extensionList = ',' . str_replace(' ', '', $extensionList) . ',';
1806  $files = [];
1807  foreach ($rawFileList as $entry) {
1808  $completePathToEntry = $pathPrefix . $entry;
1809  if (!@is_file($completePathToEntry)) {
1810  continue;
1811  }
1812 
1813  foreach ($allowedFileExtensionArray as $allowedFileExtension) {
1814  if (
1815  ($extensionList === ',,' || str_ends_with(mb_strtolower($entry), mb_strtolower('.' . $allowedFileExtension)))
1816  && ($excludePattern === '' || !preg_match('/^' . $excludePattern . '$/', $entry))
1817  ) {
1818  if ($order !== 'mtime') {
1819  $files[] = $entry;
1820  } else {
1821  // Store the value in the key so we can do a fast asort later.
1822  $files[$entry] = filemtime($completePathToEntry);
1823  }
1824  }
1825  }
1826  }
1827 
1828  $valueName = 'value';
1829  if ($order === 'mtime') {
1830  asort($files);
1831  $valueName = 'key';
1832  }
1833 
1834  $valuePathPrefix = $prependPath ? $pathPrefix : '';
1835  $foundFiles = [];
1836  foreach ($files as $key => $value) {
1837  // Don't change this ever - extensions may depend on the fact that the hash is an md5 of the path! (import/export extension)
1838  $foundFiles[md5($pathPrefix . ${$valueName})] = $valuePathPrefix . ${$valueName};
1839  }
1840 
1841  return $foundFiles;
1842  }
1843 
1855  public static function getAllFilesAndFoldersInPath(array $fileArr, string $path, string $extList = '', bool $regDirs = false, int $recursivityLevels = 99, string $excludePattern = ''): array
1856  {
1857  if ($regDirs) {
1858  $fileArr[md5($path)] = $path;
1859  }
1860  $fileArr = array_merge($fileArr, (array)self::getFilesInDir($path, $extList, true, '', $excludePattern));
1861  $dirs = ‪self::get_dirs($path);
1862  if ($recursivityLevels > 0 && is_array($dirs)) {
1863  foreach ($dirs as $subdirs) {
1864  if ((string)$subdirs !== '' && ($excludePattern === '' || !preg_match('/^' . $excludePattern . '$/', $subdirs))) {
1865  $fileArr = self::getAllFilesAndFoldersInPath($fileArr, $path . $subdirs . '/', $extList, $regDirs, $recursivityLevels - 1, $excludePattern);
1866  }
1867  }
1868  }
1869  return $fileArr;
1870  }
1871 
1879  public static function removePrefixPathFromList(array $fileArr, string $prefixToRemove): array|string
1880  {
1881  foreach ($fileArr as &$absFileRef) {
1882  if (str_starts_with($absFileRef, $prefixToRemove)) {
1883  $absFileRef = substr($absFileRef, strlen($prefixToRemove));
1884  } else {
1885  return 'ERROR: One or more of the files was NOT prefixed with the prefix-path!';
1886  }
1887  }
1888  unset($absFileRef);
1889  return $fileArr;
1890  }
1891 
1895  public static function fixWindowsFilePath(string $theFile): string
1896  {
1897  return str_replace(['\\', '//'], '/', $theFile);
1898  }
1899 
1906  public static function resolveBackPath(string $pathStr): string
1907  {
1908  if (!str_contains($pathStr, '..')) {
1909  return $pathStr;
1910  }
1911  $parts = explode('/', $pathStr);
1912  ‪$output = [];
1913  $c = 0;
1914  foreach ($parts as $part) {
1915  if ($part === '..') {
1916  if ($c) {
1917  array_pop(‪$output);
1918  --$c;
1919  } else {
1920  ‪$output[] = $part;
1921  }
1922  } else {
1923  ++$c;
1924  ‪$output[] = $part;
1925  }
1926  }
1927  return implode('/', ‪$output);
1928  }
1929 
1939  public static function locationHeaderUrl(string $path): string
1940  {
1941  if (str_starts_with($path, '//')) {
1942  return $path;
1943  }
1944 
1945  // relative to HOST
1946  if (str_starts_with($path, '/')) {
1947  return self::getIndpEnv('TYPO3_REQUEST_HOST') . $path;
1948  }
1949 
1950  $urlComponents = parse_url($path);
1951  if (!($urlComponents['scheme'] ?? false)) {
1952  // No scheme either
1953  return self::getIndpEnv('TYPO3_REQUEST_DIR') . $path;
1954  }
1955 
1956  return $path;
1957  }
1958 
1966  public static function getMaxUploadFileSize(): int
1967  {
1968  $uploadMaxFilesize = (string)ini_get('upload_max_filesize');
1969  $postMaxSize = (string)ini_get('post_max_size');
1970  // Check for PHP restrictions of the maximum size of one of the $_FILES
1971  $phpUploadLimit = self::getBytesFromSizeMeasurement($uploadMaxFilesize);
1972  // Check for PHP restrictions of the maximum $_POST size
1973  $phpPostLimit = self::getBytesFromSizeMeasurement($postMaxSize);
1974  // If the total amount of post data is smaller (!) than the upload_max_filesize directive,
1975  // then this is the real limit in PHP
1976  $phpUploadLimit = $phpPostLimit > 0 && $phpPostLimit < $phpUploadLimit ? $phpPostLimit : $phpUploadLimit;
1977  return (int)(floor($phpUploadLimit) / 1024);
1978  }
1979 
1986  public static function getBytesFromSizeMeasurement(string $measurement): int
1987  {
1988  $bytes = (float)$measurement;
1989  if (stripos($measurement, 'G')) {
1990  $bytes *= 1024 * 1024 * 1024;
1991  } elseif (stripos($measurement, 'M')) {
1992  $bytes *= 1024 * 1024;
1993  } elseif (stripos($measurement, 'K')) {
1994  $bytes *= 1024;
1995  }
1996  return (int)$bytes;
1997  }
1998 
2015  public static function createVersionNumberedFilename(string $file): string
2016  {
2017  $isFrontend = (‪$GLOBALS['TYPO3_REQUEST'] ?? null) instanceof ServerRequestInterface
2018  && ‪ApplicationType::fromRequest(‪$GLOBALS['TYPO3_REQUEST'])->isFrontend();
2019  $lookupFile = explode('?', $file);
2020  $path = $lookupFile[0];
2021 
2022  // @todo: in v13 this should be resolved by using Environment::getPublicPath() only
2023  if ($isFrontend) {
2024  // Frontend should still allow /static/myfile.css - see #98106
2025  // This should happen regardless of the incoming path is absolute or not
2026  $path = self::resolveBackPath(self::dirname(‪Environment::getCurrentScript()) . '/' . $path);
2027  } elseif (!‪PathUtility::isAbsolutePath($path)) {
2028  // Backend and non-absolute path
2029  $path = self::resolveBackPath(self::dirname(‪Environment::getCurrentScript()) . '/' . $path);
2030  }
2031 
2032  if ($isFrontend) {
2033  $configValue = (bool)(‪$GLOBALS['TYPO3_CONF_VARS']['FE']['versionNumberInFilename'] ?? false);
2034  } else {
2035  $configValue = (bool)(‪$GLOBALS['TYPO3_CONF_VARS']['BE']['versionNumberInFilename'] ?? false);
2036  }
2037  try {
2038  $fileExists = file_exists($path);
2039  } catch (\Throwable $e) {
2040  $fileExists = false;
2041  }
2042  if (!$fileExists) {
2043  // File not found, return filename unaltered
2044  $fullName = $file;
2045  } else {
2046  if (!$configValue) {
2047  // If .htaccess rule is not configured,
2048  // use the default query-string method
2049  if (!empty($lookupFile[1])) {
2050  $separator = '&';
2051  } else {
2052  $separator = '?';
2053  }
2054  $fullName = $file . $separator . filemtime($path);
2055  } else {
2056  // Change the filename
2057  $name = explode('.', $lookupFile[0]);
2058  $extension = array_pop($name);
2059  array_push($name, filemtime($path), $extension);
2060  $fullName = implode('.', $name);
2061  // Append potential query string
2062  $fullName .= !empty($lookupFile[1]) ? '?' . $lookupFile[1] : '';
2063  }
2064  }
2065  return $fullName;
2066  }
2067 
2075  public static function writeJavaScriptContentToTemporaryFile(string $content): string
2076  {
2077  $script = 'typo3temp/assets/js/' . md5($content) . '.js';
2078  if (!@is_file(‪Environment::getPublicPath() . '/' . $script)) {
2080  }
2081  return $script;
2082  }
2083 
2091  public static function writeStyleSheetContentToTemporaryFile(string $content): string
2092  {
2093  $script = 'typo3temp/assets/css/' . md5($content) . '.css';
2094  if (!@is_file(‪Environment::getPublicPath() . '/' . $script)) {
2096  }
2097  return $script;
2098  }
2099 
2107  public static function setIndpEnv(string $envName, string|bool|array|null $value)
2108  {
2109  self::$indpEnvCache[$envName] = $value;
2110  }
2111 
2120  public static function getIndpEnv(string $getEnvName): string|bool|array|null
2121  {
2122  if (array_key_exists($getEnvName, self::$indpEnvCache)) {
2123  return self::$indpEnvCache[$getEnvName];
2124  }
2125 
2126  /*
2127  Conventions:
2128  output from parse_url():
2129  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
2130  [scheme] => 'http'
2131  [user] => 'username'
2132  [pass] => 'password'
2133  [host] => '192.168.1.4'
2134  [port] => '8080'
2135  [path] => '/typo3/32/temp/phpcheck/index.php/arg1/arg2/arg3/'
2136  [query] => 'arg1,arg2,arg3&p1=parameter1&p2[key]=value'
2137  [fragment] => 'link1'Further definition: [path_script] = '/typo3/32/temp/phpcheck/index.php'
2138  [path_dir] = '/typo3/32/temp/phpcheck/'
2139  [path_info] = '/arg1/arg2/arg3/'
2140  [path] = [path_script/path_dir][path_info]Keys supported:URI______:
2141  REQUEST_URI = [path]?[query] = /typo3/32/temp/phpcheck/index.php/arg1/arg2/arg3/?arg1,arg2,arg3&p1=parameter1&p2[key]=value
2142  HTTP_HOST = [host][:[port]] = 192.168.1.4:8080
2143  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')!
2144  PATH_INFO = [path_info] = /arg1/arg2/arg3/
2145  QUERY_STRING = [query] = arg1,arg2,arg3&p1=parameter1&p2[key]=value
2146  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
2147  (Notice: NO username/password + NO fragment)CLIENT____:
2148  REMOTE_ADDR = (client IP)
2149  REMOTE_HOST = (client host)
2150  HTTP_USER_AGENT = (client user agent)
2151  HTTP_ACCEPT_LANGUAGE = (client accept language)SERVER____:
2152  SCRIPT_FILENAME = Absolute filename of script (Differs between windows/unix). On windows 'C:\\some\\path\\' will be converted to 'C:/some/path/'Special extras:
2153  TYPO3_HOST_ONLY = [host] = 192.168.1.4
2154  TYPO3_PORT = [port] = 8080 (blank if 80, taken from host value)
2155  TYPO3_REQUEST_HOST = [scheme]://[host][:[port]]
2156  TYPO3_REQUEST_URL = [scheme]://[host][:[port]][path]?[query] (scheme will by default be "http" until we can detect something different)
2157  TYPO3_REQUEST_SCRIPT = [scheme]://[host][:[port]][path_script]
2158  TYPO3_REQUEST_DIR = [scheme]://[host][:[port]][path_dir]
2159  TYPO3_SITE_URL = [scheme]://[host][:[port]][path_dir] of the TYPO3 website frontend
2160  TYPO3_SITE_PATH = [path_dir] of the TYPO3 website frontend
2161  TYPO3_SITE_SCRIPT = [script / Speaking URL] of the TYPO3 website
2162  TYPO3_DOCUMENT_ROOT = Absolute path of root of documents: TYPO3_DOCUMENT_ROOT.SCRIPT_NAME = SCRIPT_FILENAME (typically)
2163  TYPO3_SSL = Returns TRUE if this session uses SSL/TLS (https)
2164  TYPO3_PROXY = Returns TRUE if this session runs over a well known proxyNotice: [fragment] is apparently NEVER available to the script!Testing suggestions:
2165  - Output all the values.
2166  - 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
2167  - ALSO TRY the script from the ROOT of a site (like 'http://www.mytest.com/' and not 'http://www.mytest.com/test/' !!)
2168  */
2169  $retVal = '';
2170  switch ((string)$getEnvName) {
2171  case 'SCRIPT_NAME':
2172  $retVal = ‪$_SERVER['SCRIPT_NAME'] ?? '';
2173  // Add a prefix if TYPO3 is behind a proxy: ext-domain.com => int-server.com/prefix
2174  if (self::cmpIP(‪$_SERVER['REMOTE_ADDR'] ?? '', ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP'] ?? '')) {
2175  if (self::getIndpEnv('TYPO3_SSL') && ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefixSSL']) {
2176  $retVal = ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefixSSL'] . $retVal;
2177  } elseif (‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefix']) {
2178  $retVal = ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefix'] . $retVal;
2179  }
2180  }
2181  $retVal = self::encodeFileSystemPathComponentForUrlPath($retVal);
2182  break;
2183  case 'SCRIPT_FILENAME':
2185  break;
2186  case 'REQUEST_URI':
2187  // Typical application of REQUEST_URI is return urls, forms submitting to itself etc. Example: returnUrl='.rawurlencode(\TYPO3\CMS\Core\Utility\GeneralUtility::getIndpEnv('REQUEST_URI'))
2188  if (!empty(‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['requestURIvar'])) {
2189  // This is for URL rewriters that store the original URI in a server variable (eg ISAPI_Rewriter for IIS: HTTP_X_REWRITE_URL)
2190  [$v, $n] = explode('|', ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['requestURIvar']);
2191  $retVal = ‪$GLOBALS[$v][$n];
2192  } elseif (empty(‪$_SERVER['REQUEST_URI'])) {
2193  // This is for ISS/CGI which does not have the REQUEST_URI available.
2194  $retVal = '/' . ltrim(self::getIndpEnv('SCRIPT_NAME'), '/') . (!empty(‪$_SERVER['QUERY_STRING']) ? '?' . ‪$_SERVER['QUERY_STRING'] : '');
2195  } else {
2196  $retVal = '/' . ltrim(‪$_SERVER['REQUEST_URI'], '/');
2197  }
2198  // Add a prefix if TYPO3 is behind a proxy: ext-domain.com => int-server.com/prefix
2199  if (isset(‪$_SERVER['REMOTE_ADDR'], ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP'])
2200  && self::cmpIP(‪$_SERVER['REMOTE_ADDR'], ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP'])
2201  ) {
2202  if (self::getIndpEnv('TYPO3_SSL') && ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefixSSL']) {
2203  $retVal = ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefixSSL'] . $retVal;
2204  } elseif (‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefix']) {
2205  $retVal = ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefix'] . $retVal;
2206  }
2207  }
2208  break;
2209  case 'PATH_INFO':
2210  $retVal = ‪$_SERVER['PATH_INFO'] ?? '';
2211  break;
2212  case 'TYPO3_REV_PROXY':
2213  $retVal = ‪self::cmpIP(‪$_SERVER['REMOTE_ADDR'] ?? '', ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP']);
2214  break;
2215  case 'REMOTE_ADDR':
2216  $retVal = ‪$_SERVER['REMOTE_ADDR'] ?? '';
2217  if (self::cmpIP($retVal, ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP'] ?? '')) {
2218  $ip = ‪self::trimExplode(',', ‪$_SERVER['HTTP_X_FORWARDED_FOR'] ?? '');
2219  // Choose which IP in list to use
2220  if (!empty($ip)) {
2221  switch (‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyHeaderMultiValue']) {
2222  case 'last':
2223  $ip = array_pop($ip);
2224  break;
2225  case 'first':
2226  $ip = array_shift($ip);
2227  break;
2228  case 'none':
2229 
2230  default:
2231  $ip = '';
2232  }
2233  }
2234  if (self::validIP((string)$ip)) {
2235  $retVal = $ip;
2236  }
2237  }
2238  break;
2239  case 'HTTP_HOST':
2240  // if it is not set we're most likely on the cli
2241  $retVal = ‪$_SERVER['HTTP_HOST'] ?? '';
2242  if (isset(‪$_SERVER['REMOTE_ADDR']) && static::cmpIP(‪$_SERVER['REMOTE_ADDR'], ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP'])) {
2243  $host = ‪self::trimExplode(',', ‪$_SERVER['HTTP_X_FORWARDED_HOST'] ?? '');
2244  // Choose which host in list to use
2245  if (!empty($host)) {
2246  switch (‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyHeaderMultiValue']) {
2247  case 'last':
2248  $host = array_pop($host);
2249  break;
2250  case 'first':
2251  $host = array_shift($host);
2252  break;
2253  case 'none':
2254 
2255  default:
2256  $host = '';
2257  }
2258  }
2259  if ($host) {
2260  $retVal = $host;
2261  }
2262  }
2263  break;
2264  case 'HTTP_REFERER':
2265 
2266  case 'HTTP_USER_AGENT':
2267 
2268  case 'HTTP_ACCEPT_ENCODING':
2269 
2270  case 'HTTP_ACCEPT_LANGUAGE':
2271 
2272  case 'REMOTE_HOST':
2273 
2274  case 'QUERY_STRING':
2275  $retVal = ‪$_SERVER[$getEnvName] ?? '';
2276  break;
2277  case 'TYPO3_DOCUMENT_ROOT':
2278  // Get the web root (it is not the root of the TYPO3 installation)
2279  // The absolute path of the script can be calculated with TYPO3_DOCUMENT_ROOT + SCRIPT_FILENAME
2280  // 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.
2281  // Therefore the DOCUMENT_ROOT is now always calculated as the SCRIPT_FILENAME minus the end part shared with SCRIPT_NAME.
2282  $SFN = self::getIndpEnv('SCRIPT_FILENAME');
2283  // Use rawurldecode to reverse the result of self::encodeFileSystemPathComponentForUrlPath()
2284  // which has been applied to getIndpEnv(SCRIPT_NAME) for web URI usage.
2285  // We compare with a file system path (SCRIPT_FILENAME) in here and therefore need to undo the encoding.
2286  $SN_A = array_map(rawurldecode(...), explode('/', strrev(self::getIndpEnv('SCRIPT_NAME'))));
2287  $SFN_A = explode('/', strrev($SFN));
2288  $acc = [];
2289  foreach ($SN_A as $kk => $vv) {
2290  if ((string)$SFN_A[$kk] === (string)$vv) {
2291  $acc[] = $vv;
2292  } else {
2293  break;
2294  }
2295  }
2296  $commonEnd = strrev(implode('/', $acc));
2297  if ((string)$commonEnd !== '') {
2298  $retVal = substr($SFN, 0, -(strlen($commonEnd) + 1));
2299  }
2300  break;
2301  case 'TYPO3_HOST_ONLY':
2302  $httpHost = self::getIndpEnv('HTTP_HOST');
2303  $httpHostBracketPosition = strpos($httpHost, ']');
2304  $httpHostParts = explode(':', $httpHost);
2305  $retVal = $httpHostBracketPosition !== false ? substr($httpHost, 0, $httpHostBracketPosition + 1) : array_shift($httpHostParts);
2306  break;
2307  case 'TYPO3_PORT':
2308  $httpHost = self::getIndpEnv('HTTP_HOST');
2309  $httpHostOnly = self::getIndpEnv('TYPO3_HOST_ONLY');
2310  $retVal = strlen($httpHost) > strlen($httpHostOnly) ? substr($httpHost, strlen($httpHostOnly) + 1) : '';
2311  break;
2312  case 'TYPO3_REQUEST_HOST':
2313  $retVal = (self::getIndpEnv('TYPO3_SSL') ? 'https://' : 'http://') . self::getIndpEnv('HTTP_HOST');
2314  break;
2315  case 'TYPO3_REQUEST_URL':
2316  $retVal = self::getIndpEnv('TYPO3_REQUEST_HOST') . self::getIndpEnv('REQUEST_URI');
2317  break;
2318  case 'TYPO3_REQUEST_SCRIPT':
2319  $retVal = self::getIndpEnv('TYPO3_REQUEST_HOST') . self::getIndpEnv('SCRIPT_NAME');
2320  break;
2321  case 'TYPO3_REQUEST_DIR':
2322  $retVal = self::getIndpEnv('TYPO3_REQUEST_HOST') . self::dirname(self::getIndpEnv('SCRIPT_NAME')) . '/';
2323  break;
2324  case 'TYPO3_SITE_URL':
2327  ‪$url = self::getIndpEnv('TYPO3_REQUEST_DIR');
2328  $siteUrl = substr(‪$url, 0, -strlen($lPath));
2329  if (substr($siteUrl, -1) !== '/') {
2330  $siteUrl .= '/';
2331  }
2332  $retVal = $siteUrl;
2333  }
2334  break;
2335  case 'TYPO3_SITE_PATH':
2336  $retVal = substr(self::getIndpEnv('TYPO3_SITE_URL'), strlen(self::getIndpEnv('TYPO3_REQUEST_HOST')));
2337  break;
2338  case 'TYPO3_SITE_SCRIPT':
2339  $retVal = substr(self::getIndpEnv('TYPO3_REQUEST_URL'), strlen(self::getIndpEnv('TYPO3_SITE_URL')));
2340  break;
2341  case 'TYPO3_SSL':
2342  $proxySSL = trim(‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxySSL'] ?? '');
2343  if ($proxySSL === '*') {
2344  $proxySSL = ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP'];
2345  }
2346  if (self::cmpIP(‪$_SERVER['REMOTE_ADDR'] ?? '', $proxySSL)) {
2347  $retVal = true;
2348  } else {
2349  $retVal = self::webserverUsesHttps();
2350  }
2351  break;
2352  case '_ARRAY':
2353  $out = [];
2354  // Here, list ALL possible keys to this function for debug display.
2355  $envTestVars = [
2356  'HTTP_HOST',
2357  'TYPO3_HOST_ONLY',
2358  'TYPO3_PORT',
2359  'PATH_INFO',
2360  'QUERY_STRING',
2361  'REQUEST_URI',
2362  'HTTP_REFERER',
2363  'TYPO3_REQUEST_HOST',
2364  'TYPO3_REQUEST_URL',
2365  'TYPO3_REQUEST_SCRIPT',
2366  'TYPO3_REQUEST_DIR',
2367  'TYPO3_SITE_URL',
2368  'TYPO3_SITE_SCRIPT',
2369  'TYPO3_SSL',
2370  'TYPO3_REV_PROXY',
2371  'SCRIPT_NAME',
2372  'TYPO3_DOCUMENT_ROOT',
2373  'SCRIPT_FILENAME',
2374  'REMOTE_ADDR',
2375  'REMOTE_HOST',
2376  'HTTP_USER_AGENT',
2377  'HTTP_ACCEPT_LANGUAGE',
2378  ];
2379  foreach ($envTestVars as $v) {
2380  $out[$v] = self::getIndpEnv($v);
2381  }
2382  reset($out);
2383  $retVal = $out;
2384  break;
2385  }
2386  self::$indpEnvCache[$getEnvName] = $retVal;
2387  return $retVal;
2388  }
2389 
2398  protected static function webserverUsesHttps(): bool
2399  {
2400  if (!empty(‪$_SERVER['SSL_SESSION_ID'])) {
2401  return true;
2402  }
2403 
2404  // https://secure.php.net/manual/en/reserved.variables.server.php
2405  // "Set to a non-empty value if the script was queried through the HTTPS protocol."
2406  return !empty(‪$_SERVER['HTTPS']) && strtolower(‪$_SERVER['HTTPS']) !== 'off';
2407  }
2408 
2409  protected static function encodeFileSystemPathComponentForUrlPath(string $path): string
2410  {
2411  return implode('/', array_map(rawurlencode(...), explode('/', $path)));
2412  }
2413 
2414  /*************************
2415  *
2416  * TYPO3 SPECIFIC FUNCTIONS
2417  *
2418  *************************/
2428  public static function getFileAbsFileName(string $fileName): string
2429  {
2430  if ($fileName === '') {
2431  return '';
2432  }
2433  $checkForBackPath = fn(string $fileName): string => $fileName !== '' && static::validPathStr($fileName) ? $fileName : '';
2434 
2435  // Extension "EXT:" path resolving.
2436  if (‪PathUtility::isExtensionPath($fileName)) {
2437  try {
2439  } catch (‪PackageException) {
2440  $fileName = '';
2441  }
2442  return $checkForBackPath($fileName);
2443  }
2444 
2445  // Absolute path, but set to blank if not inside allowed directories.
2446  if (‪PathUtility::isAbsolutePath($fileName)) {
2447  if (str_starts_with($fileName, ‪Environment::getProjectPath()) ||
2448  str_starts_with($fileName, ‪Environment::getPublicPath())) {
2449  return $checkForBackPath($fileName);
2450  }
2451  return '';
2452  }
2453 
2454  // Relative path. Prepend with the public web folder.
2455  $fileName = ‪Environment::getPublicPath() . '/' . $fileName;
2456  return $checkForBackPath($fileName);
2457  }
2458 
2470  public static function validPathStr(string $theFile): bool
2471  {
2472  return !str_contains($theFile, '//') && !str_contains($theFile, '\\')
2473  && preg_match('#(?:^\\.\\.|/\\.\\./|[[:cntrl:]])#u', $theFile) === 0;
2474  }
2475 
2481  public static function isAllowedAbsPath(string $path): bool
2482  {
2483  if (substr($path, 0, 6) === 'vfs://') {
2484  return true;
2485  }
2486  return ‪PathUtility::isAbsolutePath($path) && static::validPathStr($path)
2487  && (
2488  str_starts_with($path, ‪Environment::getProjectPath())
2489  || str_starts_with($path, ‪Environment::getPublicPath())
2491  );
2492  }
2493 
2500  public static function copyDirectory(string $source, string $destination): void
2501  {
2502  if (!str_contains($source, ‪Environment::getProjectPath() . '/')) {
2503  $source = ‪Environment::getPublicPath() . '/' . $source;
2504  }
2505  if (!str_contains($destination, ‪Environment::getProjectPath() . '/')) {
2506  $destination = ‪Environment::getPublicPath() . '/' . $destination;
2507  }
2508  if (static::isAllowedAbsPath($source) && static::isAllowedAbsPath($destination)) {
2509  static::mkdir_deep($destination);
2510  $iterator = new \RecursiveIteratorIterator(
2511  new \RecursiveDirectoryIterator($source, \RecursiveDirectoryIterator::SKIP_DOTS),
2512  \RecursiveIteratorIterator::SELF_FIRST
2513  );
2515  foreach ($iterator as $item) {
2516  $target = $destination . '/' . static::fixWindowsFilePath((string)$iterator->getSubPathName());
2517  if ($item->isDir()) {
2518  static::mkdir($target);
2519  } else {
2520  static::upload_copy_move(static::fixWindowsFilePath($item->getPathname()), $target);
2521  }
2522  }
2523  }
2524  }
2525 
2536  public static function sanitizeLocalUrl(string ‪$url): string
2537  {
2538  $sanitizedUrl = '';
2539  if (!empty(‪$url)) {
2540  $decodedUrl = rawurldecode(‪$url);
2541  $parsedUrl = parse_url($decodedUrl);
2542  $testAbsoluteUrl = self::resolveBackPath($decodedUrl);
2543  $testRelativeUrl = self::resolveBackPath(self::dirname(self::getIndpEnv('SCRIPT_NAME')) . '/' . $decodedUrl);
2544  // Pass if URL is on the current host:
2545  if (self::isValidUrl($decodedUrl)) {
2546  if (self::isOnCurrentHost($decodedUrl) && str_starts_with($decodedUrl, self::getIndpEnv('TYPO3_SITE_URL'))) {
2547  $sanitizedUrl = ‪$url;
2548  }
2549  } elseif (‪PathUtility::isAbsolutePath($decodedUrl) && self::isAllowedAbsPath($decodedUrl)) {
2550  $sanitizedUrl = ‪$url;
2551  } elseif (str_starts_with($testAbsoluteUrl, self::getIndpEnv('TYPO3_SITE_PATH')) && $decodedUrl[0] === '/' &&
2552  substr($decodedUrl, 0, 2) !== '//'
2553  ) {
2554  $sanitizedUrl = ‪$url;
2555  } elseif (empty($parsedUrl['scheme']) && str_starts_with($testRelativeUrl, self::getIndpEnv('TYPO3_SITE_PATH'))
2556  && $decodedUrl[0] !== '/' && strpbrk($decodedUrl, '*:|"<>') === false && !str_contains($decodedUrl, '\\\\')
2557  ) {
2558  $sanitizedUrl = ‪$url;
2559  }
2560  }
2561  if (!empty(‪$url) && empty($sanitizedUrl)) {
2562  static::getLogger()->notice('The URL "{url}" is not considered to be local and was denied.', ['url' => ‪$url]);
2563  }
2564  return $sanitizedUrl;
2565  }
2566 
2575  public static function upload_copy_move(string $source, string $destination): bool
2576  {
2577  if (is_array(‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][\‪TYPO3\CMS\Core\Utility\GeneralUtility::class]['moveUploadedFile'] ?? null)) {
2578  $params = ['source' => $source, 'destination' => $destination, 'method' => 'upload_copy_move'];
2579  foreach (‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][\‪TYPO3\CMS\Core\Utility\GeneralUtility::class]['moveUploadedFile'] as $hookMethod) {
2580  $fakeThis = null;
2581  self::callUserFunction($hookMethod, $params, $fakeThis);
2582  }
2583  }
2584 
2585  $result = false;
2586  if (is_uploaded_file($source)) {
2587  // Return the value of move_uploaded_file, and if FALSE the temporary $source is still
2588  // around so the user can use unlink to delete it:
2589  $result = move_uploaded_file($source, $destination);
2590  } else {
2591  @copy($source, $destination);
2592  }
2593  // Change the permissions of the file
2594  ‪self::fixPermissions($destination);
2595  // If here the file is copied and the temporary $source is still around,
2596  // so when returning FALSE the user can try unlink to delete the $source
2597  return $result;
2598  }
2599 
2610  public static function upload_to_tempfile(string $uploadedFileName): string
2611  {
2612  if (is_uploaded_file($uploadedFileName)) {
2613  $tempFile = self::tempnam('upload_temp_');
2614  if (is_array(‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][\‪TYPO3\CMS\Core\Utility\GeneralUtility::class]['moveUploadedFile'] ?? null)) {
2615  $params = ['source' => $uploadedFileName, 'destination' => $tempFile, 'method' => 'upload_to_tempfile'];
2616  foreach (‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][\‪TYPO3\CMS\Core\Utility\GeneralUtility::class]['moveUploadedFile'] as $hookMethod) {
2617  $fakeThis = null;
2618  self::callUserFunction($hookMethod, $params, $fakeThis);
2619  }
2620  }
2621 
2622  move_uploaded_file($uploadedFileName, $tempFile);
2623  return @is_file($tempFile) ? $tempFile : '';
2624  }
2625 
2626  return '';
2627  }
2628 
2639  public static function unlink_tempfile(string $uploadedTempFileName): ?bool
2640  {
2641  if ($uploadedTempFileName) {
2642  $uploadedTempFileName = self::fixWindowsFilePath((string)$uploadedTempFileName);
2643  if (
2644  self::validPathStr($uploadedTempFileName)
2645  && (
2646  str_starts_with($uploadedTempFileName, ‪Environment::getPublicPath() . '/typo3temp/')
2647  || str_starts_with($uploadedTempFileName, ‪Environment::getVarPath() . '/')
2648  )
2649  && @is_file($uploadedTempFileName)
2650  ) {
2651  if (unlink($uploadedTempFileName)) {
2652  return true;
2653  }
2654  }
2655  }
2656 
2657  return null;
2658  }
2659 
2671  public static function tempnam(string $filePrefix, string $fileSuffix = ''): string
2672  {
2673  $temporaryPath = ‪Environment::getVarPath() . '/transient/';
2674  if (!is_dir($temporaryPath)) {
2675  ‪self::mkdir_deep($temporaryPath);
2676  }
2677  if ($fileSuffix === '') {
2678  $path = (string)tempnam($temporaryPath, $filePrefix);
2679  $tempFileName = $temporaryPath . ‪PathUtility::basename($path);
2680  } else {
2681  do {
2682  $tempFileName = $temporaryPath . $filePrefix . random_int(1, PHP_INT_MAX) . $fileSuffix;
2683  } while (file_exists($tempFileName));
2684  touch($tempFileName);
2685  clearstatcache(false, $tempFileName);
2686  }
2687  return $tempFileName;
2688  }
2689 
2700  public static function callUserFunction(string|\Closure $funcName, mixed &$params, ?object $ref = null): mixed
2701  {
2702  // Check if we're using a closure and invoke it directly.
2703  if (is_object($funcName) && is_a($funcName, \Closure::class)) {
2704  return call_user_func_array($funcName, [&$params, &$ref]);
2705  }
2706  $funcName = trim($funcName);
2707  $parts = explode('->', $funcName);
2708  // Call function or method
2709  if (count($parts) === 2) {
2710  // It's a class/method
2711  // Check if class/method exists:
2712  if (class_exists($parts[0])) {
2713  // Create object
2714  $classObj = self::makeInstance($parts[0]);
2715  $methodName = (string)$parts[1];
2716  $callable = [$classObj, $methodName];
2717  if (is_callable($callable)) {
2718  // Call method:
2719  $content = call_user_func_array($callable, [&$params, &$ref]);
2720  } else {
2721  throw new \InvalidArgumentException('No method name \'' . $parts[1] . '\' in class ' . $parts[0], 1294585865);
2722  }
2723  } else {
2724  throw new \InvalidArgumentException('No class named ' . $parts[0], 1294585866);
2725  }
2726  } elseif (function_exists($funcName) && is_callable($funcName)) {
2727  // It's a function
2728  $content = call_user_func_array($funcName, [&$params, &$ref]);
2729  } else {
2730  // Usually this will be annotated by static code analysis tools, but there's no native "not empty string" type
2731  throw new \InvalidArgumentException('No function named: ' . $funcName, 1294585867);
2732  }
2733  return $content;
2734  }
2735 
2739  public static function setContainer(ContainerInterface ‪$container): void
2740  {
2741  self::$container = ‪$container;
2742  }
2743 
2747  public static function getContainer(): ContainerInterface
2748  {
2749  if (self::$container === null) {
2750  throw new \LogicException('PSR-11 Container is not available', 1549404144);
2751  }
2752  return ‪self::$container;
2753  }
2754 
2769  public static function makeInstance(string $className, mixed ...$constructorArguments): object
2770  {
2771  // PHPStan will complain about this check. That's okay as we're checking a contract violation here.
2772  if ($className === '') {
2773  throw new \InvalidArgumentException('$className must be a non empty string.', 1288965219);
2774  }
2775  // Never instantiate with a beginning backslash, otherwise things like singletons won't work.
2776  if (str_starts_with($className, '\\')) {
2777  throw new \InvalidArgumentException(
2778  '$className "' . $className . '" must not start with a backslash.',
2779  1420281366
2780  );
2781  }
2782  if (isset(static::$finalClassNameCache[$className])) {
2783  $finalClassName = static::$finalClassNameCache[$className];
2784  } else {
2785  $finalClassName = self::getClassName($className);
2786  static::$finalClassNameCache[$className] = $finalClassName;
2787  }
2788  // Return singleton instance if it is already registered
2789  if (isset(self::$singletonInstances[$finalClassName])) {
2790  return self::$singletonInstances[$finalClassName];
2791  }
2792  // Return instance if it has been injected by addInstance()
2793  if (
2794  isset(self::$nonSingletonInstances[$finalClassName])
2795  && !empty(self::$nonSingletonInstances[$finalClassName])
2796  ) {
2797  return array_shift(self::$nonSingletonInstances[$finalClassName]);
2798  }
2799 
2800  // Read service and prototypes from the DI container, this is required to
2801  // support classes that require dependency injection.
2802  // We operate on the original class name on purpose, as class overrides
2803  // are resolved inside the container
2804  if (self::$container !== null && $constructorArguments === [] && self::$container->has($className)) {
2805  return self::$container->get($className);
2806  }
2807 
2808  // Create new instance and call constructor with parameters
2809  $instance = new $finalClassName(...$constructorArguments);
2810  // Register new singleton instance, but only if it is not a known PSR-11 container service
2811  if ($instance instanceof SingletonInterface && !(self::$container !== null && self::$container->has($className))) {
2812  self::$singletonInstances[$finalClassName] = $instance;
2813  }
2814  if ($instance instanceof LoggerAwareInterface) {
2815  $instance->setLogger(static::makeInstance(LogManager::class)->getLogger($className));
2816  }
2817  return $instance;
2818  }
2819 
2831  public static function makeInstanceForDi(string $className, mixed ...$constructorArguments): object
2832  {
2833  $finalClassName = static::$finalClassNameCache[$className] ?? static::$finalClassNameCache[$className] = self::getClassName($className);
2834 
2835  // Return singleton instance if it is already registered (currently required for unit and functional tests)
2836  if (isset(self::$singletonInstances[$finalClassName])) {
2837  return self::$singletonInstances[$finalClassName];
2838  }
2839  // Create new instance and call constructor with parameters
2840  return new $finalClassName(...$constructorArguments);
2841  }
2842 
2852  public static function getClassName(string $className): string
2853  {
2854  if (class_exists($className)) {
2855  while (static::classHasImplementation($className)) {
2856  $className = static::getImplementationForClass($className);
2857  }
2858  }
2860  }
2861 
2868  protected static function getImplementationForClass(string $className): string
2869  {
2870  return ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['Objects'][$className]['className'];
2871  }
2872 
2878  protected static function classHasImplementation(string $className): bool
2879  {
2880  return !empty(‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['Objects'][$className]['className']);
2881  }
2882 
2899  public static function setSingletonInstance(string $className, SingletonInterface $instance): void
2900  {
2901  self::checkInstanceClassName($className, $instance);
2902  // Check for XCLASS registration (same is done in makeInstance() in order to store the singleton of the final class name)
2903  $finalClassName = self::getClassName($className);
2904  self::$singletonInstances[$finalClassName] = $instance;
2905  }
2906 
2921  public static function removeSingletonInstance(string $className, SingletonInterface $instance): void
2922  {
2923  self::checkInstanceClassName($className, $instance);
2924  if (!isset(self::$singletonInstances[$className])) {
2925  throw new \InvalidArgumentException('No Instance registered for ' . $className . '.', 1394099179);
2926  }
2927  if ($instance !== self::$singletonInstances[$className]) {
2928  throw new \InvalidArgumentException('The instance you are trying to remove has not been registered before.', 1394099256);
2929  }
2930  unset(self::$singletonInstances[$className]);
2931  }
2932 
2946  public static function resetSingletonInstances(array $newSingletonInstances): void
2947  {
2948  static::$singletonInstances = [];
2949  foreach ($newSingletonInstances as $className => $instance) {
2950  static::setSingletonInstance($className, $instance);
2951  }
2952  }
2953 
2966  public static function getSingletonInstances(): array
2967  {
2968  return static::$singletonInstances;
2969  }
2970 
2982  public static function getInstances(): array
2983  {
2984  return static::$nonSingletonInstances;
2985  }
2986 
3000  public static function addInstance(string $className, object $instance): void
3001  {
3002  self::checkInstanceClassName($className, $instance);
3003  if ($instance instanceof SingletonInterface) {
3004  throw new \InvalidArgumentException('$instance must not be an instance of TYPO3\\CMS\\Core\\SingletonInterface. For setting singletons, please use setSingletonInstance.', 1288969325);
3005  }
3006  if (!isset(self::$nonSingletonInstances[$className])) {
3007  self::$nonSingletonInstances[$className] = [];
3008  }
3009  self::$nonSingletonInstances[$className][] = $instance;
3010  }
3011 
3017  protected static function checkInstanceClassName(string $className, object $instance): void
3018  {
3019  if ($className === '') {
3020  throw new \InvalidArgumentException('$className must not be empty.', 1288967479);
3021  }
3022  if (!$instance instanceof $className) {
3023  throw new \InvalidArgumentException('$instance must be an instance of ' . $className . ', but actually is an instance of ' . get_class($instance) . '.', 1288967686);
3024  }
3025  }
3026 
3037  public static function purgeInstances(): void
3038  {
3039  self::$container = null;
3040  self::$singletonInstances = [];
3041  self::$nonSingletonInstances = [];
3042  }
3043 
3053  public static function flushInternalRuntimeCaches(): void
3054  {
3055  self::$finalClassNameCache = [];
3056  self::$indpEnvCache = [];
3057  }
3058 
3073  public static function makeInstanceService(string $serviceType, string $serviceSubType = '', array $excludeServiceKeys = []): array|object|false
3074  {
3075  $error = false;
3076  $requestInfo = [
3077  'requestedServiceType' => $serviceType,
3078  'requestedServiceSubType' => $serviceSubType,
3079  'requestedExcludeServiceKeys' => $excludeServiceKeys,
3080  ];
3081  while ($info = ‪ExtensionManagementUtility::findService($serviceType, $serviceSubType, $excludeServiceKeys)) {
3082  // provide information about requested service to service object
3083  $info = array_merge($info, $requestInfo);
3084 
3086  $className = $info['className'];
3088  $obj = self::makeInstance($className);
3089  if (is_object($obj)) {
3090  if (!is_callable([$obj, 'init'])) {
3091  self::getLogger()->error('Requested service {class} has no init() method.', [
3092  'class' => $info['className'],
3093  'service' => $info,
3094  ]);
3095  throw new \RuntimeException('Broken service: ' . $info['className'], 1568119209);
3096  }
3097  $obj->info = $info;
3098  // service available?
3099  if ($obj->init()) {
3100  return $obj;
3101  }
3102  $error = $obj->getLastErrorArray();
3103  unset($obj);
3104  }
3105 
3106  // deactivate the service
3107  ‪ExtensionManagementUtility::deactivateService($info['serviceType'], $info['serviceKey']);
3108  }
3109  return $error;
3110  }
3111 
3118  public static function quoteJSvalue(string $value): string
3119  {
3120  $json = (string)json_encode(
3121  $value,
3122  JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_TAG
3123  );
3124 
3125  return strtr(
3126  $json,
3127  [
3128  '"' => '\'',
3129  '\\\\' => '\\u005C',
3130  ' ' => '\\u0020',
3131  '!' => '\\u0021',
3132  '\\t' => '\\u0009',
3133  '\\n' => '\\u000A',
3134  '\\r' => '\\u000D',
3135  ]
3136  );
3137  }
3138 
3145  public static function jsonEncodeForHtmlAttribute(mixed $value, bool $useHtmlEntities = true): string
3146  {
3147  $json = (string)json_encode($value, JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_TAG);
3148  return $useHtmlEntities ? htmlspecialchars($json) : $json;
3149  }
3150 
3157  public static function jsonEncodeForJavaScript(mixed $value): string
3158  {
3159  $json = (string)json_encode($value, JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_TAG);
3160  return strtr(
3161  $json,
3162  [
3163  // comments below refer to JSON-encoded data
3164  '\\\\' => '\\\\u005C', // `"\\Vendor\\Package"` -> `"\\u005CVendor\\u005CPackage"`
3165  '\\t' => '\\u0009', // `"\t"` -> `"\u0009"`
3166  '\\n' => '\\u000A', // `"\n"` -> `"\u000A"`
3167  '\\r' => '\\u000D', // `"\r"` -> `"\u000D"`
3168  ]
3169  );
3170  }
3171 
3176  public static function sanitizeCssVariableValue(string $value): string
3177  {
3178  $value = str_replace(['{', '}', "\n", "\r"], '', $value);
3179  // keep quotes, e.g. for `background: url("/res/background.png")`
3180  return htmlspecialchars($value, ENT_SUBSTITUTE);
3181  }
3182 
3183  protected static function getLogger(): LoggerInterface
3184  {
3185  return static::makeInstance(LogManager::class)->getLogger(__CLASS__);
3186  }
3187 }
‪TYPO3\CMS\Core\Utility\GeneralUtility\underscoredToLowerCamelCase
‪static string underscoredToLowerCamelCase($string)
Definition: GeneralUtility.php:671
‪TYPO3\CMS\Core\Utility\GeneralUtility\mkdir
‪static bool mkdir(string $newFolder)
Definition: GeneralUtility.php:1638
‪TYPO3\CMS\Core\Utility\PathUtility\stripPathSitePrefix
‪static stripPathSitePrefix(string $path)
Definition: PathUtility.php:428
‪TYPO3\CMS\Core\Utility\GeneralUtility\$nl
‪$nl
Definition: GeneralUtility.php:1157
‪TYPO3\CMS\Core\Utility\PathUtility\isExtensionPath
‪static isExtensionPath(string $path)
Definition: PathUtility.php:117
‪TYPO3\CMS\Core\Utility\GeneralUtility\$container
‪static ContainerInterface $container
Definition: GeneralUtility.php:53
‪TYPO3\CMS\Core\Utility\HttpUtility\buildUrl
‪static buildUrl(array $urlParts)
Definition: HttpUtility.php:102
‪TYPO3\CMS\Core\Utility\GeneralUtility\fixed_lgd_cs
‪static string fixed_lgd_cs(string $string, int $chars, string $appendString='...')
Definition: GeneralUtility.php:92
‪TYPO3\CMS\Core\Utility\PathUtility\isAbsolutePath
‪static isAbsolutePath(string $path)
Definition: PathUtility.php:286
‪TYPO3\CMS\Core\Utility\GeneralUtility\$isValid
‪if($isValid) return $isValid
Definition: GeneralUtility.php:639
‪TYPO3\CMS\Core\Utility\GeneralUtility\get_dirs
‪static string[] string null get_dirs(string $path)
Definition: GeneralUtility.php:1759
‪TYPO3\CMS\Core\Utility\GeneralUtility\implodeArrayForUrl
‪static string implodeArrayForUrl(string $name, array $theArray, string $str='', bool $skipBlank=false, bool $rawurlencodeParamName=false)
Definition: GeneralUtility.php:860
‪TYPO3\CMS\Core\Utility\PathUtility\isAllowedAdditionalPath
‪static isAllowedAdditionalPath(string $path)
Definition: PathUtility.php:457
‪TYPO3
‪TYPO3\CMS\Core\Core\Environment\getPublicPath
‪static getPublicPath()
Definition: Environment.php:187
‪TYPO3\CMS\Core\Utility\GeneralUtility\$localeInfo
‪$localeInfo
Definition: GeneralUtility.php:572
‪TYPO3\CMS\Core\Utility
Definition: ArrayUtility.php:18
‪TYPO3\CMS\Core\Core\ClassLoadingInformation
Definition: ClassLoadingInformation.php:35
‪TYPO3\CMS\Core\Utility\GeneralUtility\$labelArr
‪if($base !==1000 && $base !==1024) $labelArr
Definition: GeneralUtility.php:564
‪$parser
‪$parser
Definition: annotationChecker.php:103
‪TYPO3\CMS\Core\Core\Environment\getCurrentScript
‪static getCurrentScript()
Definition: Environment.php:220
‪TYPO3\CMS\Core\Utility\GeneralUtility\cmpIP
‪static bool cmpIP(string $baseIP, string $list)
Definition: GeneralUtility.php:113
‪TYPO3\CMS\Core\Utility\GeneralUtility\$multiplier
‪$multiplier
Definition: GeneralUtility.php:576
‪TYPO3\CMS\Core\Utility\ExtensionManagementUtility\deactivateService
‪static deactivateService(string $serviceType, string $serviceKey)
Definition: ExtensionManagementUtility.php:791
‪TYPO3\CMS\Core\Utility\GeneralUtility\camelCaseToLowerCaseUnderscored
‪static string camelCaseToLowerCaseUnderscored($string)
Definition: GeneralUtility.php:683
‪$dir
‪$dir
Definition: validateRstFiles.php:257
‪TYPO3\CMS\Core\Utility\GeneralUtility\cmpFQDN
‪static bool cmpFQDN(string $baseHost, string $list)
Definition: GeneralUtility.php:341
‪TYPO3\CMS\Core\Utility\GeneralUtility\xml2arrayProcess
‪static array string xml2arrayProcess(string $string, string $NSprefix='', bool $reportDocTag=false)
Definition: GeneralUtility.php:1288
‪TYPO3\CMS\Core\Authentication\AbstractAuthenticationService
Definition: AbstractAuthenticationService.php:29
‪TYPO3\CMS\Core\Utility\GeneralUtility\mkdir_deep
‪static mkdir_deep(string $directory)
Definition: GeneralUtility.php:1654
‪TYPO3\CMS\Core\Core\Environment\getVarPath
‪static getVarPath()
Definition: Environment.php:197
‪TYPO3\CMS\Core\Utility\PathUtility\basename
‪static basename(string $path)
Definition: PathUtility.php:219
‪TYPO3\CMS\Core\Utility\GeneralUtility\$oldLocale
‪$oldLocale
Definition: GeneralUtility.php:570
‪TYPO3\CMS\Core\Utility\GeneralUtility\validIPv4
‪static bool validIPv4(string $ip)
Definition: GeneralUtility.php:316
‪TYPO3\CMS\Core\Utility\GeneralUtility\$nonSingletonInstances
‪static array $nonSingletonInstances
Definition: GeneralUtility.php:67
‪TYPO3\CMS\Core\Utility\GeneralUtility\$spaceInd
‪static array< array-key, function explodeUrl2Array(string $string):array { $output=[];$p=explode('&', $string);foreach( $p as $v) { if( $v !=='') { $nameAndValue=explode('=', $v, 2);$output[rawurldecode( $nameAndValue[0])]=isset( $nameAndValue[1]) ? rawurldecode( $nameAndValue[1]) :'';} } return $output;} public static array function removeDotsFromTS(array $ts):array { $out=[];foreach( $ts as $key=> $value) { if(is_array( $value)) { $key=rtrim( $key, '.');$out[ $key]=self::removeDotsFromTS( $value);} else { $out[ $key]=$value;} } return $out;} public static array< string, function get_tag_attributes(string $tag, bool $decodeEntities=false):array { $components=self::split_tag_attributes( $tag);$name='';$valuemode=false;$attributes=[];foreach( $components as $key=> $val) { if( $val !=='=') { if( $valuemode) { if( $name) { $attributes[ $name]=$decodeEntities ? htmlspecialchars_decode( $val) :$val;$name='';} } else { if( $key=strtolower(preg_replace('/[^[:alnum:]_\\:\\-]/', '', $val) ?? '')) { $attributes[ $key]='';$name=$key;} } $valuemode=false;} else { $valuemode=true;} } return $attributes;} public static string[] function split_tag_attributes(string $tag):array { $tag_tmp=trim(preg_replace('/^<[^[:space:]] */', '', trim( $tag)) ?? '');$tag_tmp=trim(rtrim( $tag_tmp, '>'));$value=[];while( $tag_tmp !=='') { $firstChar=$tag_tmp[0];if( $firstChar==='"' || $firstChar === '\'') { $reg = explode($firstChar, $tag_tmp, 3); $value[] = $reg[1]; $tag_tmp = trim($reg[2] ?? ''); } elseif ($firstChar === '=') { $value[] = '='; $tag_tmp = trim(substr($tag_tmp, 1)); } else { $reg = preg_split('/[[:space:]=]/', $tag_tmp, 2); $value[] = trim($reg[0]); $tag_tmp = trim(substr($tag_tmp, strlen($reg[0]), 1) . ($reg[1] ?? '')); } } reset($value); return $value; } public static string function implodeAttributes(array $arr, bool $xhtmlSafe = false, bool $keepBlankAttributes = false): string { if ($xhtmlSafe) { $newArr = []; foreach ($arr as $attributeName => $attributeValue) { $attributeName = strtolower($attributeName); if (!isset($newArr[$attributeName])) { $newArr[$attributeName] = htmlspecialchars((string)$attributeValue); } } $arr = $newArr; } $list = []; foreach ($arr as $attributeName => $attributeValue) { if ((string)$attributeValue !== '' || $keepBlankAttributes) { $list[] = $attributeName . '="' . $attributeValue . '"'; } } return implode(' ', $list); } public static string function wrapJS(string $string, array $attributes = []): string { if (trim($string)) { $string = ltrim($string, LF); $match = []; if (preg_match('/^(\\t+)/', $string, $match)) { $string = str_replace($match[1], "\t", $string); } return '<script ' . GeneralUtility::implodeAttributes($attributes, true) . '>' . $string . '</script>'; } return ''; } public static array|string function xml2tree(string $string, int $depth = 999, array $parserOptions = []): array|string { $parser = xml_parser_create(); $vals = []; $index = []; xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0); xml_parser_set_option($parser, XML_OPTION_SKIP_WHITE, 0); foreach ($parserOptions as $option => $value) { xml_parser_set_option($parser, $option, $value); } xml_parse_into_struct($parser, $string, $vals, $index); if (xml_get_error_code($parser)) { return 'Line ' . xml_get_current_line_number($parser) . ': ' . xml_error_string(xml_get_error_code($parser)); } xml_parser_free($parser); $stack = [[]]; $stacktop = 0; $startPoint = 0; $tagi = []; foreach ($vals as $key => $val) { $type = $val['type']; if ($type === 'open' || $type === 'complete') { $stack[$stacktop++] = $tagi; if ($depth == $stacktop) { $startPoint = $key; } $tagi = ['tag' => $val['tag']]; if (isset($val['attributes'])) { $tagi['attrs'] = $val['attributes']; } if (isset($val['value'])) { $tagi['values'][] = $val['value']; } } if ($type === 'complete' || $type === 'close') { $oldtagi = $tagi; $tagi = $stack[--$stacktop]; $oldtag = $oldtagi['tag']; unset($oldtagi['tag']); if ($depth == $stacktop + 1) { if ($key - $startPoint > 0) { $partArray = array_slice($vals, $startPoint + 1, $key - $startPoint - 1); $oldtagi['XMLvalue'] = self::xmlRecompileFromStructValArray($partArray); } else { $oldtagi['XMLvalue'] = $oldtagi['values'][0]; } } $tagi['ch'][$oldtag][] = $oldtagi; unset($oldtagi); } if ($type === 'cdata') { $tagi['values'][] = $val['value']; } } return $tagi['ch']; } public static string function array2xml(array $array, string $NSprefix = '', int $level = 0, string $docTag = 'phparray', int $spaceInd = 0, array $options = [], array $stackData = []): string { $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); $indentChar = $spaceInd ? ' ' : "\t"; $indentN = $spaceInd > $spaceInd
Definition: GeneralUtility.php:1156
‪TYPO3\CMS\Core\Utility\GeneralUtility\$indpEnvCache
‪static array $indpEnvCache
Definition: GeneralUtility.php:80
‪TYPO3\CMS\Core\Utility\MathUtility\canBeInterpretedAsInteger
‪static bool canBeInterpretedAsInteger(mixed $var)
Definition: MathUtility.php:69
‪TYPO3\CMS\Core\Utility\GeneralUtility\$sizeInUnits
‪$sizeInUnits
Definition: GeneralUtility.php:577
‪TYPO3\CMS\Core\Utility\GeneralUtility\writeFileToTypo3tempDir
‪static string null writeFileToTypo3tempDir(string $filepath, string $content)
Definition: GeneralUtility.php:1561
‪TYPO3\CMS\Core\Core\Environment\getProjectPath
‪static string getProjectPath()
Definition: Environment.php:160
‪TYPO3\CMS\Core\Utility\GeneralUtility\$output
‪foreach($array as $k=> $v) if(! $level) return $output
Definition: GeneralUtility.php:1247
‪TYPO3\CMS\Core\Utility\GeneralUtility\writeFile
‪static bool writeFile(string $file, string $content, bool $changePermissions=false)
Definition: GeneralUtility.php:1469
‪TYPO3\CMS\Core\Utility\GeneralUtility\getUrl
‪static string false getUrl(string $url)
Definition: GeneralUtility.php:1444
‪TYPO3\CMS\Core\Utility\GeneralUtility\validIPv6
‪static bool validIPv6(string $ip)
Definition: GeneralUtility.php:329
‪TYPO3\CMS\Core\Utility\GeneralUtility\expandList
‪static string expandList($list)
Definition: GeneralUtility.php:434
‪TYPO3\CMS\Core\Utility\ExtensionManagementUtility\findService
‪static array false findService(string $serviceType, string $serviceSubType='', array $excludeServiceKeys=[])
Definition: ExtensionManagementUtility.php:691
‪$validator
‪if(isset($args['d'])) $validator
Definition: validateRstFiles.php:262
‪TYPO3\CMS\Core\Utility\GeneralUtility\cmpIPv4
‪static bool cmpIPv4(string $baseIP, string $list)
Definition: GeneralUtility.php:135
‪TYPO3\CMS\Core\Utility\GeneralUtility\$singletonInstances
‪static array $singletonInstances
Definition: GeneralUtility.php:60
‪TYPO3\CMS\Core\Utility\PathUtility\dirnameDuringBootstrap
‪static string dirnameDuringBootstrap(string $path)
Definition: PathUtility.php:337
‪TYPO3\CMS\Core\Utility\GeneralUtility\hmac
‪static string hmac($input, $additionalSecret='')
Definition: GeneralUtility.php:475
‪TYPO3\CMS\Core\Utility\GeneralUtility\rmdir
‪static bool rmdir(string $path, bool $removeNonEmpty=false)
Definition: GeneralUtility.php:1702
‪TYPO3\CMS\Core\Cache\CacheManager
Definition: CacheManager.php:36
‪$_SERVER
‪$_SERVER['TYPO3_DEPRECATED_ENTRYPOINT']
Definition: legacy-backend.php:20
‪TYPO3\CMS\Core\Core\ClassLoadingInformation\getClassNameForAlias
‪static class string getClassNameForAlias($alias)
Definition: ClassLoadingInformation.php:179
‪TYPO3\CMS\Core\Utility\GeneralUtility\cmpIPv6
‪static bool cmpIPv6(string $baseIP, string $list)
Definition: GeneralUtility.php:184
‪TYPO3\CMS\Core\Utility\GeneralUtility\$sizeInBytes
‪$sizeInBytes
Definition: GeneralUtility.php:575
‪TYPO3\CMS\Core\Http\RequestFactory
Definition: RequestFactory.php:30
‪TYPO3\CMS\Core\Utility\GeneralUtility\$currentLocale
‪$currentLocale
Definition: GeneralUtility.php:569
‪TYPO3\CMS\Core\Utility\GeneralUtility\$finalClassNameCache
‪static array $finalClassNameCache
Definition: GeneralUtility.php:75
‪TYPO3\CMS\Core\Utility\GeneralUtility\fixPermissions
‪static bool fixPermissions(string $path, bool $recursive=false)
Definition: GeneralUtility.php:1496
‪TYPO3\CMS\Core\Utility\GeneralUtility\$output
‪$output
Definition: GeneralUtility.php:1159
‪TYPO3\CMS\Webhooks\Message\$url
‪identifier readonly UriInterface $url
Definition: LoginErrorOccurredMessage.php:36
‪TYPO3\CMS\Core\Utility\GeneralUtility\normalizeIPv6
‪static string normalizeIPv6(string $address)
Definition: GeneralUtility.php:239
‪TYPO3\CMS\Core\Utility\GeneralUtility\createDirectoryPath
‪static string createDirectoryPath(string $fullDirectoryPath)
Definition: GeneralUtility.php:1675
‪TYPO3\CMS\Core\SingletonInterface
Definition: SingletonInterface.php:22
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Core\Log\LogManager
Definition: LogManager.php:33
‪TYPO3\CMS\Core\Core\Environment
Definition: Environment.php:41
‪TYPO3\CMS\Core\Utility\GeneralUtility\revExplode
‪static list< string > revExplode(string $delimiter, string $string, int $limit=0)
Definition: GeneralUtility.php:787
‪TYPO3\CMS\Core\Utility\GeneralUtility\isValidUrl
‪static bool isValidUrl(string $url)
Definition: GeneralUtility.php:713
‪TYPO3\CMS\Core\Utility\GeneralUtility\inList
‪static bool inList($list, $item)
Definition: GeneralUtility.php:422
‪TYPO3\CMS\Core\Utility\ExtensionManagementUtility\resolvePackagePath
‪static resolvePackagePath(string $path)
Definition: ExtensionManagementUtility.php:70
‪TYPO3\CMS\Core\Utility\GeneralUtility\xmlRecompileFromStructValArray
‪static string xmlRecompileFromStructValArray(array $vals)
Definition: GeneralUtility.php:1394
‪TYPO3\CMS\Core\Utility\GeneralUtility\validIP
‪static bool validIP(string $ip)
Definition: GeneralUtility.php:303
‪TYPO3\CMS\Core\Package\Exception
Definition: ImportRequirementsException.php:18
‪TYPO3\CMS\Core\Http\fromRequest
‪@ fromRequest
Definition: ApplicationType.php:66
‪TYPO3\CMS\Core\Utility\GeneralUtility\md5int
‪static int md5int($str)
Definition: GeneralUtility.php:462
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:52
‪TYPO3\CMS\Core\Utility\GeneralUtility\intExplode
‪static list< int > intExplode(string $delimiter, string $string, bool $removeEmptyValues=false)
Definition: GeneralUtility.php:756
‪TYPO3\CMS\Core\Utility\GeneralUtility\isOnCurrentHost
‪static bool isOnCurrentHost(string $url)
Definition: GeneralUtility.php:409
‪TYPO3\CMS\Core\Utility\GeneralUtility\underscoredToUpperCamelCase
‪static string underscoredToUpperCamelCase($string)
Definition: GeneralUtility.php:659
‪TYPO3\CMS\Core\Utility\GeneralUtility\xml2array
‪static array string xml2array(string $string, string $NSprefix='', bool $reportDocTag=false)
Definition: GeneralUtility.php:1265
‪TYPO3\CMS\Core\Utility\GeneralUtility\__construct
‪__construct()
Definition: GeneralUtility.php:82
‪TYPO3\CMS\Core\Utility\GeneralUtility\trimExplode
‪static list< string > trimExplode(string $delim, string $string, bool $removeEmptyValues=false, int $limit=0)
Definition: GeneralUtility.php:822
‪TYPO3\CMS\Webhooks\Message\$identifier
‪identifier readonly string $identifier
Definition: FileAddedMessage.php:37
‪TYPO3\CMS\Core\Http\ApplicationType
‪ApplicationType
Definition: ApplicationType.php:55
‪TYPO3\CMS\Core\Core\Environment\isWindows
‪static isWindows()
Definition: Environment.php:276