‪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 
474  public static function ‪hmac($input, $additionalSecret = '')
475  {
476  $hashAlgorithm = 'sha1';
477  $secret = ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'] . $additionalSecret;
478  return hash_hmac($hashAlgorithm, $input, $secret);
479  }
480 
487  public static function split_fileref($fileNameWithPath)
488  {
489  $info = [];
490  $reg = [];
491  if (preg_match('/(.*\\/)(.*)$/', $fileNameWithPath, $reg)) {
492  $info['path'] = $reg[1];
493  $info['file'] = $reg[2];
494  } else {
495  $info['path'] = '';
496  $info['file'] = $fileNameWithPath;
497  }
498  $reg = '';
499  // If open_basedir is set and the fileName was supplied without a path the is_dir check fails
500  if (!is_dir($fileNameWithPath) && preg_match('/(.*)\\.([^\\.]*$)/', $info['file'], $reg)) {
501  $info['filebody'] = $reg[1];
502  $info['fileext'] = strtolower($reg[2]);
503  $info['realFileext'] = $reg[2];
504  } else {
505  $info['filebody'] = $info['file'];
506  $info['fileext'] = '';
507  }
508  reset($info);
509  return $info;
510  }
511 
527  public static function dirname($path)
528  {
529  $p = ‪self::revExplode('/', $path, 2);
530  return count($p) === 2 ? $p[0] : '';
531  }
532 
541  public static function formatSize(‪$sizeInBytes, $labels = '', $base = 0)
542  {
543  $defaultFormats = [
544  'iec' => ['base' => 1024, 'labels' => [' ', ' Ki', ' Mi', ' Gi', ' Ti', ' Pi', ' Ei', ' Zi', ' Yi']],
545  'si' => ['base' => 1000, 'labels' => [' ', ' k', ' M', ' G', ' T', ' P', ' E', ' Z', ' Y']],
546  ];
547  // Set labels and base:
548  if (empty($labels)) {
549  $labels = 'iec';
550  }
551  if (isset($defaultFormats[$labels])) {
552  $base = $defaultFormats[$labels]['base'];
553  ‪$labelArr = $defaultFormats[$labels]['labels'];
554  } else {
555  $base = (int)$base;
556  if ($base !== 1000 && $base !== 1024) {
557  $base = 1024;
558  }
559  ‪$labelArr = explode('|', str_replace('"', '', $labels));
560  }
561  // This is set via Site Handling and in the Locales class via setlocale()
562  // LC_NUMERIC is not set because of side effects when calculating with floats
563  // see @\TYPO3\CMS\Core\Localization\Locales::setLocale
564  ‪$currentLocale = setlocale(LC_MONETARY, '0');
565  ‪$oldLocale = setlocale(LC_NUMERIC, '0');
566  setlocale(LC_NUMERIC, ‪$currentLocale);
567  ‪$localeInfo = localeconv();
568  setlocale(LC_NUMERIC, ‪$oldLocale);
569 
571  ‪$multiplier = floor((‪$sizeInBytes ? log(‪$sizeInBytes) : 0) / log($base));
573  if (‪$sizeInUnits > ($base * .9)) {
574  ‪$multiplier++;
575  }
578  return number_format(‪$sizeInUnits, ((‪$multiplier > 0) && (‪$sizeInUnits < 20)) ? 2 : 0, ‪$localeInfo['decimal_point'], '') . ‪$labelArr[‪$multiplier];
579  }
580 
590  public static function splitCalc($string, $operators)
591  {
592  $res = [];
593  $sign = '+';
594  while ($string) {
595  $valueLen = strcspn($string, $operators);
596  $value = substr($string, 0, $valueLen);
597  $res[] = [$sign, trim($value)];
598  $sign = substr($string, $valueLen, 1);
599  $string = substr($string, $valueLen + 1);
600  }
601  reset($res);
602  return $res;
603  }
604 
611  public static function validEmail(string $email): bool
612  {
613  if (trim($email) !== $email) {
614  return false;
615  }
616  if (!str_contains($email, '@')) {
617  return false;
618  }
619 
620  $validators = [];
621  foreach (‪$GLOBALS['TYPO3_CONF_VARS']['MAIL']['validators'] ?? [RFCValidation::class] as $className) {
622  ‪$validator = new $className();
623  if (‪$validator instanceof EmailValidation) {
624  $validators[] = ‪$validator;
625  }
626  }
627 
628  $emailValidator = new EmailValidator();
629  ‪$isValid = $emailValidator->isValid($email, new MultipleValidationWithAnd($validators, MultipleValidationWithAnd::STOP_ON_ERROR));
630 
631  // Currently, the RFCValidation doesn't recognise "email @example.com"
632  // as an invalid email for historic reasons - catch it here
633  // see https://github.com/egulias/EmailValidator/issues/374
634  if (‪$isValid) {
635  // If email is valid, check if we have CFWSNearAt warning and
636  // treat it as an invalid email, i.e "email @example.com"
637  foreach ($emailValidator->getWarnings() as $warning) {
638  if ($warning instanceof CFWSNearAt) {
639  return false;
640  }
641  }
642  }
643 
644  return ‪$isValid;
645  }
646 
654  public static function ‪underscoredToUpperCamelCase($string)
655  {
656  return str_replace(' ', '', ucwords(str_replace('_', ' ', strtolower($string))));
657  }
658 
666  public static function ‪underscoredToLowerCamelCase($string)
667  {
668  return lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', strtolower($string)))));
669  }
670 
678  public static function ‪camelCaseToLowerCaseUnderscored($string)
679  {
680  $value = preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $string) ?? '';
681  return mb_strtolower($value, 'utf-8');
682  }
683 
708  public static function ‪isValidUrl(string ‪$url): bool
709  {
710  $parsedUrl = parse_url(‪$url);
711  if (!$parsedUrl || !isset($parsedUrl['scheme'])) {
712  return false;
713  }
714  // HttpUtility::buildUrl() will always build urls with <scheme>://
715  // our original $url might only contain <scheme>: (e.g. mail:)
716  // so we convert that to the double-slashed version to ensure
717  // our check against the $recomposedUrl is proper
718  if (!str_starts_with(‪$url, $parsedUrl['scheme'] . '://')) {
719  ‪$url = str_replace($parsedUrl['scheme'] . ':', $parsedUrl['scheme'] . '://', ‪$url);
720  }
721  $recomposedUrl = ‪HttpUtility::buildUrl($parsedUrl);
722  if ($recomposedUrl !== ‪$url) {
723  // The parse_url() had to modify characters, so the URL is invalid
724  return false;
725  }
726  if (isset($parsedUrl['host']) && !preg_match('/^[a-z0-9.\\-]*$/i', $parsedUrl['host'])) {
727  $host = idn_to_ascii($parsedUrl['host']);
728  if ($host === false) {
729  return false;
730  }
731  $parsedUrl['host'] = $host;
732  }
733  return filter_var(‪HttpUtility::buildUrl($parsedUrl), FILTER_VALIDATE_URL) !== false;
734  }
735 
736  /*************************
737  *
738  * ARRAY FUNCTIONS
739  *
740  *************************/
741 
751  public static function ‪intExplode(string $delimiter, string $string, bool $removeEmptyValues = false): array
752  {
753  $result = explode($delimiter, $string);
754  foreach ($result as $key => &$value) {
755  if ($removeEmptyValues && trim($value) === '') {
756  unset($result[$key]);
757  } else {
758  $value = (int)$value;
759  }
760  }
761  unset($value);
762 
764  return array_values($result);
765  }
766 
782  public static function ‪revExplode(string $delimiter, string $string, int $limit = 0): array
783  {
784  // 2 is the (currently, as of 2014-02) most-used value for `$limit` in the core, therefore we check it first
785  if ($limit === 2) {
786  $position = strrpos($string, strrev($delimiter));
787  if ($position !== false) {
788  return [substr($string, 0, $position), substr($string, $position + strlen($delimiter))];
789  }
790  return [$string];
791  }
792  if ($limit <= 1) {
793  return [$string];
794  }
795  $explodedValues = explode($delimiter, strrev($string), $limit);
796  $explodedValues = array_map(strrev(...), $explodedValues);
797  return array_reverse($explodedValues);
798  }
799 
817  public static function ‪trimExplode(string $delim, string $string, bool $removeEmptyValues = false, int $limit = 0): array
818  {
819  $result = explode($delim, $string);
820  if ($removeEmptyValues) {
821  // Remove items that are just whitespace, but leave whitespace intact for the rest.
822  $result = array_values(array_filter($result, static fn(string $item): bool => trim($item) !== ''));
823  }
824 
825  if ($limit === 0) {
826  // Return everything.
827  return array_map(trim(...), $result);
828  }
829 
830  if ($limit < 0) {
831  // Trim and return just the first $limit elements and ignore the rest.
832  return array_map(trim(...), array_slice($result, 0, $limit));
833  }
834 
835  // Fold the last length - $limit elements into a single trailing item, then trim and return the result.
836  $tail = array_slice($result, $limit - 1);
837  $result = array_slice($result, 0, $limit - 1);
838  if ($tail) {
839  $result[] = implode($delim, $tail);
840  }
841  return array_map(trim(...), $result);
842  }
843 
855  public static function ‪implodeArrayForUrl(string $name, array $theArray, string $str = '', bool $skipBlank = false, bool $rawurlencodeParamName = false): string
856  {
857  foreach ($theArray as $Akey => $AVal) {
858  $thisKeyName = $name ? $name . '[' . $Akey . ']' : $Akey;
859  if (is_array($AVal)) {
860  $str = ‪self::implodeArrayForUrl($thisKeyName, $AVal, $str, $skipBlank, $rawurlencodeParamName);
861  } else {
862  $stringValue = (string)$AVal;
863  if (!$skipBlank || $stringValue !== '') {
864  $parameterName = $rawurlencodeParamName ? rawurlencode($thisKeyName) : $thisKeyName;
865  $parameterValue = rawurlencode($stringValue);
866  $str .= '&' . $parameterName . '=' . $parameterValue;
867  }
868  }
869  }
870  return $str;
871  }
872 
888  public static function explodeUrl2Array(string $string): array
889  {
890  ‪$output = [];
891  $p = explode('&', $string);
892  foreach ($p as $v) {
893  if ($v !== '') {
894  $nameAndValue = explode('=', $v, 2);
895  ‪$output[rawurldecode($nameAndValue[0])] = isset($nameAndValue[1]) ? rawurldecode($nameAndValue[1]) : '';
896  }
897  }
898  return ‪$output;
899  }
900 
908  public static function removeDotsFromTS(array $ts): array
909  {
910  $out = [];
911  foreach ($ts as $key => $value) {
912  if (is_array($value)) {
913  $key = rtrim($key, '.');
914  $out[$key] = self::removeDotsFromTS($value);
915  } else {
916  $out[$key] = $value;
917  }
918  }
919  return $out;
920  }
921 
922  /*************************
923  *
924  * HTML/XML PROCESSING
925  *
926  *************************/
936  public static function get_tag_attributes(string $tag, bool $decodeEntities = false): array
937  {
938  $components = self::split_tag_attributes($tag);
939  // Attribute name is stored here
940  $name = '';
941  $valuemode = false;
942  $attributes = [];
943  foreach ($components as $key => $val) {
944  // 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
945  if ($val !== '=') {
946  if ($valuemode) {
947  if ($name) {
948  $attributes[$name] = $decodeEntities ? htmlspecialchars_decode($val) : $val;
949  $name = '';
950  }
951  } else {
952  if ($key = strtolower(preg_replace('/[^[:alnum:]_\\:\\-]/', '', $val) ?? '')) {
953  $attributes[$key] = '';
954  $name = $key;
955  }
956  }
957  $valuemode = false;
958  } else {
959  $valuemode = true;
960  }
961  }
962  return $attributes;
963  }
964 
972  public static function split_tag_attributes(string $tag): array
973  {
974  $tag_tmp = trim(preg_replace('/^<[^[:space:]]*/', '', trim($tag)) ?? '');
975  // Removes any > in the end of the string
976  $tag_tmp = trim(rtrim($tag_tmp, '>'));
977  $value = [];
978  // Compared with empty string instead , 030102
979  while ($tag_tmp !== '') {
980  $firstChar = $tag_tmp[0];
981  if ($firstChar === '"' || $firstChar === '\'') {
982  $reg = explode($firstChar, $tag_tmp, 3);
983  $value[] = $reg[1];
984  $tag_tmp = trim($reg[2] ?? '');
985  } elseif ($firstChar === '=') {
986  $value[] = '=';
987  // Removes = chars.
988  $tag_tmp = trim(substr($tag_tmp, 1));
989  } else {
990  // There are '' around the value. We look for the next ' ' or '>'
991  $reg = preg_split('/[[:space:]=]/', $tag_tmp, 2);
992  $value[] = trim($reg[0]);
993  $tag_tmp = trim(substr($tag_tmp, strlen($reg[0]), 1) . ($reg[1] ?? ''));
994  }
995  }
996  reset($value);
997  return $value;
998  }
999 
1008  public static function implodeAttributes(array $arr, bool $xhtmlSafe = false, bool $keepBlankAttributes = false): string
1009  {
1010  if ($xhtmlSafe) {
1011  $newArr = [];
1012  foreach ($arr as $attributeName => $attributeValue) {
1013  $attributeName = strtolower($attributeName);
1014  if (!isset($newArr[$attributeName])) {
1015  $newArr[$attributeName] = htmlspecialchars((string)$attributeValue);
1016  }
1017  }
1018  $arr = $newArr;
1019  }
1020  $list = [];
1021  foreach ($arr as $attributeName => $attributeValue) {
1022  if ((string)$attributeValue !== '' || $keepBlankAttributes) {
1023  $list[] = $attributeName . '="' . $attributeValue . '"';
1024  }
1025  }
1026  return implode(' ', $list);
1027  }
1028 
1038  public static function wrapJS(string $string, array $attributes = []): string
1039  {
1040  if (trim($string)) {
1041  // remove nl from the beginning
1042  $string = ltrim($string, LF);
1043  // re-ident to one tab using the first line as reference
1044  $match = [];
1045  if (preg_match('/^(\\t+)/', $string, $match)) {
1046  $string = str_replace($match[1], "\t", $string);
1047  }
1048  return '<script ' . GeneralUtility::implodeAttributes($attributes, true) . '>
1049 /*<![CDATA[*/
1050 ' . $string . '
1051 /*]]>*/
1052 </script>';
1053  }
1054  return '';
1055  }
1056 
1065  public static function xml2tree(string $string, int $depth = 999, array $parserOptions = []): array|string
1066  {
1067  ‪$parser = xml_parser_create();
1068  $vals = [];
1069  $index = [];
1070  xml_parser_set_option(‪$parser, XML_OPTION_CASE_FOLDING, 0);
1071  xml_parser_set_option(‪$parser, XML_OPTION_SKIP_WHITE, 0);
1072  foreach ($parserOptions as $option => $value) {
1073  xml_parser_set_option(‪$parser, $option, $value);
1074  }
1075  xml_parse_into_struct(‪$parser, $string, $vals, $index);
1076  if (xml_get_error_code(‪$parser)) {
1077  return 'Line ' . xml_get_current_line_number(‪$parser) . ': ' . xml_error_string(xml_get_error_code(‪$parser));
1078  }
1079  xml_parser_free(‪$parser);
1080  $stack = [[]];
1081  $stacktop = 0;
1082  $startPoint = 0;
1083  $tagi = [];
1084  foreach ($vals as $key => $val) {
1085  $type = $val['type'];
1086  // open tag:
1087  if ($type === 'open' || $type === 'complete') {
1088  $stack[$stacktop++] = $tagi;
1089  if ($depth == $stacktop) {
1090  $startPoint = $key;
1091  }
1092  $tagi = ['tag' => $val['tag']];
1093  if (isset($val['attributes'])) {
1094  $tagi['attrs'] = $val['attributes'];
1095  }
1096  if (isset($val['value'])) {
1097  $tagi['values'][] = $val['value'];
1098  }
1099  }
1100  // finish tag:
1101  if ($type === 'complete' || $type === 'close') {
1102  $oldtagi = $tagi;
1103  $tagi = $stack[--$stacktop];
1104  $oldtag = $oldtagi['tag'];
1105  unset($oldtagi['tag']);
1106  if ($depth == $stacktop + 1) {
1107  if ($key - $startPoint > 0) {
1108  $partArray = array_slice($vals, $startPoint + 1, $key - $startPoint - 1);
1109  $oldtagi['XMLvalue'] = ‪self::xmlRecompileFromStructValArray($partArray);
1110  } else {
1111  $oldtagi['XMLvalue'] = $oldtagi['values'][0];
1112  }
1113  }
1114  $tagi['ch'][$oldtag][] = $oldtagi;
1115  unset($oldtagi);
1116  }
1117  // cdata
1118  if ($type === 'cdata') {
1119  $tagi['values'][] = $val['value'];
1120  }
1121  }
1122  return $tagi['ch'];
1123  }
1124 
1145  public static function array2xml(array $array, string $NSprefix = '', int $level = 0, string $docTag = 'phparray', int ‪$spaceInd = 0, array $options = [], array $stackData = []): string
1146  {
1147  // 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
1148  $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);
1149  // Set indenting mode:
1150  $indentChar = ‪$spaceInd ? ' ' : "\t";
1151  $indentN = ‪$spaceInd > 0 ? ‪$spaceInd : 1;
1152  ‪$nl = ‪$spaceInd >= 0 ? LF : '';
1153  // Init output variable:
1155  // Traverse the input array
1156  foreach ($array as $k => $v) {
1157  $attr = '';
1158  $tagName = (string)$k;
1159  // Construct the tag name.
1160  // Use tag based on grand-parent + parent tag name
1161  if (isset($stackData['grandParentTagName'], $stackData['parentTagName'], $options['grandParentTagMap'][$stackData['grandParentTagName'] . '/' . $stackData['parentTagName']])) {
1162  $attr .= ' index="' . htmlspecialchars($tagName) . '"';
1163  $tagName = (string)$options['grandParentTagMap'][$stackData['grandParentTagName'] . '/' . $stackData['parentTagName']];
1164  } elseif (isset($stackData['parentTagName'], $options['parentTagMap'][$stackData['parentTagName'] . ':_IS_NUM']) && ‪MathUtility::canBeInterpretedAsInteger($tagName)) {
1165  // Use tag based on parent tag name + if current tag is numeric
1166  $attr .= ' index="' . htmlspecialchars($tagName) . '"';
1167  $tagName = (string)$options['parentTagMap'][$stackData['parentTagName'] . ':_IS_NUM'];
1168  } elseif (isset($stackData['parentTagName'], $options['parentTagMap'][$stackData['parentTagName'] . ':' . $tagName])) {
1169  // Use tag based on parent tag name + current tag
1170  $attr .= ' index="' . htmlspecialchars($tagName) . '"';
1171  $tagName = (string)$options['parentTagMap'][$stackData['parentTagName'] . ':' . $tagName];
1172  } elseif (isset($stackData['parentTagName'], $options['parentTagMap'][$stackData['parentTagName']])) {
1173  // Use tag based on parent tag name:
1174  $attr .= ' index="' . htmlspecialchars($tagName) . '"';
1175  $tagName = (string)$options['parentTagMap'][$stackData['parentTagName']];
1176  } elseif (‪MathUtility::canBeInterpretedAsInteger($tagName)) {
1177  // If integer...;
1178  if ($options['useNindex'] ?? false) {
1179  // If numeric key, prefix "n"
1180  $tagName = 'n' . $tagName;
1181  } else {
1182  // Use special tag for num. keys:
1183  $attr .= ' index="' . $tagName . '"';
1184  $tagName = ($options['useIndexTagForNum'] ?? false) ?: 'numIndex';
1185  }
1186  } elseif (!empty($options['useIndexTagForAssoc'])) {
1187  // Use tag for all associative keys:
1188  $attr .= ' index="' . htmlspecialchars($tagName) . '"';
1189  $tagName = $options['useIndexTagForAssoc'];
1190  }
1191  // The tag name is cleaned up so only alphanumeric chars (plus - and _) are in there and not longer than 100 chars either.
1192  $tagName = substr(preg_replace('/[^[:alnum:]_-]/', '', $tagName), 0, 100);
1193  // If the value is an array then we will call this function recursively:
1194  if (is_array($v)) {
1195  // Sub elements:
1196  if (isset($options['alt_options']) && ($options['alt_options'][($stackData['path'] ?? '') . '/' . $tagName] ?? false)) {
1197  $subOptions = $options['alt_options'][($stackData['path'] ?? '') . '/' . $tagName];
1198  $clearStackPath = (bool)($subOptions['clearStackPath'] ?? false);
1199  } else {
1200  $subOptions = $options;
1201  $clearStackPath = false;
1202  }
1203  if (empty($v)) {
1204  $content = '';
1205  } else {
1206  $content = ‪$nl . self::array2xml($v, $NSprefix, $level + 1, '', ‪$spaceInd, $subOptions, [
1207  'parentTagName' => $tagName,
1208  'grandParentTagName' => $stackData['parentTagName'] ?? '',
1209  'path' => $clearStackPath ? '' : ($stackData['path'] ?? '') . '/' . $tagName,
1210  ]) . (‪$spaceInd >= 0 ? str_pad('', ($level + 1) * $indentN, $indentChar) : '');
1211  }
1212  // Do not set "type = array". Makes prettier XML but means that empty arrays are not restored with xml2array
1213  if (!isset($options['disableTypeAttrib']) || (int)$options['disableTypeAttrib'] != 2) {
1214  $attr .= ' type="array"';
1215  }
1216  } else {
1217  $stringValue = (string)$v;
1218  // Just a value:
1219  // Look for binary chars:
1220  $vLen = strlen($stringValue);
1221  // Go for base64 encoding if the initial segment NOT matching any binary char has the same length as the whole string!
1222  if ($vLen && strcspn($stringValue, $binaryChars) != $vLen) {
1223  // If the value contained binary chars then we base64-encode it and set an attribute to notify this situation:
1224  $content = ‪$nl . chunk_split(base64_encode($stringValue));
1225  $attr .= ' base64="1"';
1226  } else {
1227  // Otherwise, just htmlspecialchar the stuff:
1228  $content = htmlspecialchars($stringValue);
1229  $dType = gettype($v);
1230  if ($dType !== 'string' && !($options['disableTypeAttrib'] ?? false)) {
1231  $attr .= ' type="' . $dType . '"';
1232  }
1233  }
1234  }
1235  if ($tagName !== '') {
1236  // Add the element to the output string:
1237  ‪$output .= (‪$spaceInd >= 0 ? str_pad('', ($level + 1) * $indentN, $indentChar) : '')
1238  . '<' . $NSprefix . $tagName . $attr . '>' . $content . '</' . $NSprefix . $tagName . '>' . ‪$nl;
1239  }
1240  }
1241  // If we are at the outer-most level, then we finally wrap it all in the document tags and return that as the value:
1242  if (!$level) {
1243  ‪$output = '<' . $docTag . '>' . ‪$nl . ‪$output . '</' . $docTag . '>';
1244  }
1245  return ‪$output;
1246  }
1247 
1260  public static function ‪xml2array(string $string, string $NSprefix = '', bool $reportDocTag = false): array|string
1261  {
1262  $runtimeCache = static::makeInstance(CacheManager::class)->getCache('runtime');
1263  $firstLevelCache = $runtimeCache->get('generalUtilityXml2Array') ?: [];
1264  ‪$identifier = md5($string . $NSprefix . ($reportDocTag ? '1' : '0'));
1265  // Look up in first level cache
1266  if (empty($firstLevelCache[‪$identifier])) {
1267  $firstLevelCache[‪$identifier] = ‪self::xml2arrayProcess($string, $NSprefix, $reportDocTag);
1268  $runtimeCache->set('generalUtilityXml2Array', $firstLevelCache);
1269  }
1270  return $firstLevelCache[‪$identifier];
1271  }
1272 
1283  public static function ‪xml2arrayProcess(string $string, string $NSprefix = '', bool $reportDocTag = false): array|string
1284  {
1285  $string = trim((string)$string);
1286  // Create parser:
1287  ‪$parser = xml_parser_create();
1288  $vals = [];
1289  $index = [];
1290  xml_parser_set_option(‪$parser, XML_OPTION_CASE_FOLDING, 0);
1291  xml_parser_set_option(‪$parser, XML_OPTION_SKIP_WHITE, 0);
1292  // Default output charset is UTF-8, only ASCII, ISO-8859-1 and UTF-8 are supported!!!
1293  $match = [];
1294  preg_match('/^[[:space:]]*<\\?xml[^>]*encoding[[:space:]]*=[[:space:]]*"([^"]*)"/', substr($string, 0, 200), $match);
1295  $theCharset = $match[1] ?? 'utf-8';
1296  // us-ascii / utf-8 / iso-8859-1
1297  xml_parser_set_option(‪$parser, XML_OPTION_TARGET_ENCODING, $theCharset);
1298  // Parse content:
1299  xml_parse_into_struct(‪$parser, $string, $vals, $index);
1300  // If error, return error message:
1301  if (xml_get_error_code(‪$parser)) {
1302  return 'Line ' . xml_get_current_line_number(‪$parser) . ': ' . xml_error_string(xml_get_error_code(‪$parser));
1303  }
1304  xml_parser_free(‪$parser);
1305  // Init vars:
1306  $stack = [[]];
1307  $stacktop = 0;
1308  $current = [];
1309  $tagName = '';
1310  $documentTag = '';
1311  // Traverse the parsed XML structure:
1312  foreach ($vals as $key => $val) {
1313  // First, process the tag-name (which is used in both cases, whether "complete" or "close")
1314  $tagName = $val['tag'];
1315  if (!$documentTag) {
1316  $documentTag = $tagName;
1317  }
1318  // Test for name space:
1319  $tagName = $NSprefix && str_starts_with($tagName, $NSprefix) ? substr($tagName, strlen($NSprefix)) : $tagName;
1320  // Test for numeric tag, encoded on the form "nXXX":
1321  $testNtag = substr($tagName, 1);
1322  // Closing tag.
1323  $tagName = $tagName[0] === 'n' && ‪MathUtility::canBeInterpretedAsInteger($testNtag) ? (int)$testNtag : $tagName;
1324  // Test for alternative index value:
1325  if ((string)($val['attributes']['index'] ?? '') !== '') {
1326  $tagName = $val['attributes']['index'];
1327  }
1328  // Setting tag-values, manage stack:
1329  switch ($val['type']) {
1330  case 'open':
1331  // If open tag it means there is an array stored in sub-elements. Therefore increase the stackpointer and reset the accumulation array:
1332  // Setting blank place holder
1333  $current[$tagName] = [];
1334  $stack[$stacktop++] = $current;
1335  $current = [];
1336  break;
1337  case 'close':
1338  // If the tag is "close" then it is an array which is closing and we decrease the stack pointer.
1339  $oldCurrent = $current;
1340  $current = $stack[--$stacktop];
1341  // Going to the end of array to get placeholder key, key($current), and fill in array next:
1342  end($current);
1343  $current[key($current)] = $oldCurrent;
1344  unset($oldCurrent);
1345  break;
1346  case 'complete':
1347  // If "complete", then it's a value. If the attribute "base64" is set, then decode the value, otherwise just set it.
1348  if (!empty($val['attributes']['base64'])) {
1349  $current[$tagName] = base64_decode($val['value']);
1350  } else {
1351  // Had to cast it as a string - otherwise it would be evaluate FALSE if tested with isset()!!
1352  $current[$tagName] = (string)($val['value'] ?? '');
1353  // Cast type:
1354  switch ((string)($val['attributes']['type'] ?? '')) {
1355  case 'integer':
1356  $current[$tagName] = (int)$current[$tagName];
1357  break;
1358  case 'double':
1359  $current[$tagName] = (float)$current[$tagName];
1360  break;
1361  case 'boolean':
1362  $current[$tagName] = (bool)$current[$tagName];
1363  break;
1364  case 'NULL':
1365  $current[$tagName] = null;
1366  break;
1367  case 'array':
1368  // 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...
1369  $current[$tagName] = [];
1370  break;
1371  }
1372  }
1373  break;
1374  }
1375  }
1376  if ($reportDocTag) {
1377  $current[$tagName]['_DOCUMENT_TAG'] = $documentTag;
1378  }
1379  // Finally return the content of the document tag.
1380  return $current[$tagName];
1381  }
1382 
1389  public static function ‪xmlRecompileFromStructValArray(array $vals): string
1390  {
1391  $XMLcontent = '';
1392  foreach ($vals as $val) {
1393  $type = $val['type'];
1394  // Open tag:
1395  if ($type === 'open' || $type === 'complete') {
1396  $XMLcontent .= '<' . $val['tag'];
1397  if (isset($val['attributes'])) {
1398  foreach ($val['attributes'] as $k => $v) {
1399  $XMLcontent .= ' ' . $k . '="' . htmlspecialchars($v) . '"';
1400  }
1401  }
1402  if ($type === 'complete') {
1403  if (isset($val['value'])) {
1404  $XMLcontent .= '>' . htmlspecialchars($val['value']) . '</' . $val['tag'] . '>';
1405  } else {
1406  $XMLcontent .= '/>';
1407  }
1408  } else {
1409  $XMLcontent .= '>';
1410  }
1411  if ($type === 'open' && isset($val['value'])) {
1412  $XMLcontent .= htmlspecialchars($val['value']);
1413  }
1414  }
1415  // Finish tag:
1416  if ($type === 'close') {
1417  $XMLcontent .= '</' . $val['tag'] . '>';
1418  }
1419  // Cdata
1420  if ($type === 'cdata') {
1421  $XMLcontent .= htmlspecialchars($val['value']);
1422  }
1423  }
1424  return $XMLcontent;
1425  }
1426 
1427  /*************************
1428  *
1429  * FILES FUNCTIONS
1430  *
1431  *************************/
1439  public static function ‪getUrl(string ‪$url): string|false
1440  {
1441  // Looks like it's an external file, use Guzzle by default
1442  if (preg_match('/^(?:http|ftp)s?|s(?:ftp|cp):/', ‪$url)) {
1443  $requestFactory = static::makeInstance(RequestFactory::class);
1444  try {
1445  $response = $requestFactory->request(‪$url);
1446  } catch (RequestException $exception) {
1447  return false;
1448  }
1449  $content = $response->getBody()->getContents();
1450  } else {
1451  $content = @file_get_contents(‪$url);
1452  }
1453  return $content;
1454  }
1455 
1464  public static function ‪writeFile(string $file, string $content, bool $changePermissions = false): bool
1465  {
1466  if (!@is_file($file)) {
1467  $changePermissions = true;
1468  }
1469  if ($fd = fopen($file, 'wb')) {
1470  $res = fwrite($fd, $content);
1471  fclose($fd);
1472  if ($res === false) {
1473  return false;
1474  }
1475  // Change the permissions only if the file has just been created
1476  if ($changePermissions) {
1477  static::fixPermissions($file);
1478  }
1479  return true;
1480  }
1481  return false;
1482  }
1483 
1491  public static function ‪fixPermissions(string $path, bool $recursive = false): bool
1492  {
1493  $targetPermissions = null;
1494  if (‪Environment::isWindows()) {
1495  return true;
1496  }
1497  $result = false;
1498  // Make path absolute
1499  if (!‪PathUtility::isAbsolutePath($path)) {
1500  $path = static::getFileAbsFileName($path);
1501  }
1502  if (static::isAllowedAbsPath($path)) {
1503  if (@is_file($path)) {
1504  $targetPermissions = (string)(‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['fileCreateMask'] ?? '0644');
1505  } elseif (@is_dir($path)) {
1506  $targetPermissions = (string)(‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['folderCreateMask'] ?? '0755');
1507  }
1508  if (!empty($targetPermissions)) {
1509  // make sure it's always 4 digits
1510  $targetPermissions = str_pad($targetPermissions, 4, '0', STR_PAD_LEFT);
1511  $targetPermissions = octdec($targetPermissions);
1512  // "@" is there because file is not necessarily OWNED by the user
1513  $result = @chmod($path, (int)$targetPermissions);
1514  }
1515  // Set createGroup if not empty
1516  if (
1517  isset(‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['createGroup'])
1518  && ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['createGroup'] !== ''
1519  ) {
1520  // "@" is there because file is not necessarily OWNED by the user
1521  $changeGroupResult = @chgrp($path, ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['createGroup']);
1522  $result = $changeGroupResult ? $result : false;
1523  }
1524  // Call recursive if recursive flag if set and $path is directory
1525  if ($recursive && @is_dir($path)) {
1526  $handle = opendir($path);
1527  if (is_resource($handle)) {
1528  while (($file = readdir($handle)) !== false) {
1529  $recursionResult = null;
1530  if ($file !== '.' && $file !== '..') {
1531  if (@is_file($path . '/' . $file)) {
1532  $recursionResult = static::fixPermissions($path . '/' . $file);
1533  } elseif (@is_dir($path . '/' . $file)) {
1534  $recursionResult = static::fixPermissions($path . '/' . $file, true);
1535  }
1536  if (isset($recursionResult) && !$recursionResult) {
1537  $result = false;
1538  }
1539  }
1540  }
1541  closedir($handle);
1542  }
1543  }
1544  }
1545  return $result;
1546  }
1547 
1556  public static function ‪writeFileToTypo3tempDir(string $filepath, string $content): ?string
1557  {
1558  // Parse filepath into directory and basename:
1559  $fI = pathinfo($filepath);
1560  $fI['dirname'] .= '/';
1561  // Check parts:
1562  if (!static::validPathStr($filepath) || !$fI['basename'] || strlen($fI['basename']) >= 60) {
1563  return 'Input filepath "' . $filepath . '" was generally invalid!';
1564  }
1565 
1566  // Setting main temporary directory name (standard)
1567  $allowedPathPrefixes = [
1568  ‪Environment::getPublicPath() . '/typo3temp' => 'Environment::getPublicPath() + "/typo3temp/"',
1569  ];
1570  // Also allow project-path + /var/
1571  if (‪Environment::getVarPath() !== ‪Environment::getPublicPath() . '/typo3temp/var') {
1572  $relPath = substr(‪Environment::getVarPath(), strlen(‪Environment::getProjectPath()) + 1);
1573  $allowedPathPrefixes[‪Environment::getVarPath()] = 'ProjectPath + ' . $relPath;
1574  }
1575 
1576  $errorMessage = null;
1577  foreach ($allowedPathPrefixes as $pathPrefix => $prefixLabel) {
1578  $dirName = $pathPrefix . '/';
1579  // Invalid file path, let's check for the other path, if it exists
1580  if (!str_starts_with($fI['dirname'], $dirName)) {
1581  if ($errorMessage === null) {
1582  $errorMessage = '"' . $fI['dirname'] . '" was not within directory ' . $prefixLabel;
1583  }
1584  continue;
1585  }
1586  // This resets previous error messages from the first path
1587  $errorMessage = null;
1588 
1589  if (!@is_dir($dirName)) {
1590  $errorMessage = $prefixLabel . ' was not a directory!';
1591  // continue and see if the next iteration resets the errorMessage above
1592  continue;
1593  }
1594  // Checking if the "subdir" is found
1595  $subdir = substr($fI['dirname'], strlen($dirName));
1596  if ($subdir) {
1597  if (preg_match('#^(?:[[:alnum:]_]+/)+$#', $subdir)) {
1598  $dirName .= $subdir;
1599  if (!@is_dir($dirName)) {
1600  static::mkdir_deep($pathPrefix . '/' . $subdir);
1601  }
1602  } else {
1603  $errorMessage = 'Subdir, "' . $subdir . '", was NOT on the form "[[:alnum:]_]/+"';
1604  break;
1605  }
1606  }
1607  // Checking dir-name again (sub-dir might have been created)
1608  if (@is_dir($dirName)) {
1609  if ($filepath === $dirName . $fI['basename']) {
1610  static::writeFile($filepath, $content);
1611  if (!@is_file($filepath)) {
1612  $errorMessage = 'The file was not written to the disk. Please, check that you have write permissions to the ' . $prefixLabel . ' directory.';
1613  }
1614  break;
1615  }
1616  $errorMessage = 'Calculated file location didn\'t match input "' . $filepath . '".';
1617  break;
1618  }
1619  $errorMessage = '"' . $dirName . '" is not a directory!';
1620  break;
1621  }
1622  return $errorMessage;
1623  }
1624 
1633  public static function ‪mkdir(string $newFolder): bool
1634  {
1635  $result = @‪mkdir($newFolder, (int)octdec((string)(‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['folderCreateMask'] ?? '0')));
1636  if ($result) {
1637  static::fixPermissions($newFolder);
1638  }
1639  return $result;
1640  }
1641 
1649  public static function ‪mkdir_deep(string $directory): void
1650  {
1651  // Ensure there is only one slash
1652  $fullPath = rtrim($directory, '/') . '/';
1653  if ($fullPath !== '/' && !is_dir($fullPath)) {
1654  $firstCreatedPath = static::createDirectoryPath($fullPath);
1655  if ($firstCreatedPath !== '') {
1656  static::fixPermissions($firstCreatedPath, true);
1657  }
1658  }
1659  }
1660 
1670  protected static function ‪createDirectoryPath(string $fullDirectoryPath): string
1671  {
1672  $currentPath = $fullDirectoryPath;
1673  $firstCreatedPath = '';
1674  $permissionMask = (int)octdec((string)(‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['folderCreateMask'] ?? '0'));
1675  if (!@is_dir($currentPath)) {
1676  do {
1677  $firstCreatedPath = $currentPath;
1678  $separatorPosition = (int)strrpos($currentPath, DIRECTORY_SEPARATOR);
1679  $currentPath = substr($currentPath, 0, $separatorPosition);
1680  } while (!is_dir($currentPath) && $separatorPosition > 0);
1681  $result = @‪mkdir($fullDirectoryPath, $permissionMask, true);
1682  // Check existence of directory again to avoid race condition. Directory could have get created by another process between previous is_dir() and mkdir()
1683  if (!$result && !@is_dir($fullDirectoryPath)) {
1684  throw new \RuntimeException('Could not create directory "' . $fullDirectoryPath . '"!', 1170251401);
1685  }
1686  }
1687  return $firstCreatedPath;
1688  }
1689 
1697  public static function ‪rmdir(string $path, bool $removeNonEmpty = false): bool
1698  {
1699  $OK = false;
1700  // Remove trailing slash
1701  $path = preg_replace('|/$|', '', $path) ?? '';
1702  $isWindows = DIRECTORY_SEPARATOR === '\\';
1703  if (file_exists($path)) {
1704  $OK = true;
1705  if (!is_link($path) && is_dir($path)) {
1706  if ($removeNonEmpty === true && ($handle = @opendir($path))) {
1707  $entries = [];
1708 
1709  while (false !== ($file = readdir($handle))) {
1710  if ($file === '.' || $file === '..') {
1711  continue;
1712  }
1713 
1714  $entries[] = $path . '/' . $file;
1715  }
1716 
1717  closedir($handle);
1718 
1719  foreach ($entries as $entry) {
1720  if (!static::rmdir($entry, $removeNonEmpty)) {
1721  $OK = false;
1722  }
1723  }
1724  }
1725  if ($OK) {
1726  $OK = @‪rmdir($path);
1727  }
1728  } elseif (is_link($path) && is_dir($path) && $isWindows) {
1729  $OK = @‪rmdir($path);
1730  } else {
1731  // If $path is a file, simply remove it
1732  $OK = @unlink($path);
1733  }
1734  clearstatcache();
1735  } elseif (is_link($path)) {
1736  $OK = @unlink($path);
1737  if (!$OK && $isWindows) {
1738  // Try to delete dead folder links on Windows systems
1739  $OK = @‪rmdir($path);
1740  }
1741  clearstatcache();
1742  }
1743  return $OK;
1744  }
1745 
1754  public static function ‪get_dirs(string $path): array|string|null
1755  {
1756  $dirs = null;
1757  if ($path) {
1758  if (is_dir($path)) {
1759  ‪$dir = scandir($path);
1760  $dirs = [];
1761  foreach (‪$dir as $entry) {
1762  if (is_dir($path . '/' . $entry) && $entry !== '..' && $entry !== '.') {
1763  $dirs[] = $entry;
1764  }
1765  }
1766  } else {
1767  $dirs = 'error';
1768  }
1769  }
1770  return $dirs;
1771  }
1772 
1785  public static function getFilesInDir(string $path, string $extensionList = '', bool $prependPath = false, string $order = '', string $excludePattern = ''): array|string
1786  {
1787  $excludePattern = (string)$excludePattern;
1788  $path = rtrim($path, '/');
1789  if (!@is_dir($path)) {
1790  return [];
1791  }
1792 
1793  $rawFileList = scandir($path);
1794  if ($rawFileList === false) {
1795  return 'error opening path: "' . $path . '"';
1796  }
1797 
1798  $pathPrefix = $path . '/';
1799  $allowedFileExtensionArray = ‪self::trimExplode(',', $extensionList);
1800  $extensionList = ',' . str_replace(' ', '', $extensionList) . ',';
1801  $files = [];
1802  foreach ($rawFileList as $entry) {
1803  $completePathToEntry = $pathPrefix . $entry;
1804  if (!@is_file($completePathToEntry)) {
1805  continue;
1806  }
1807 
1808  foreach ($allowedFileExtensionArray as $allowedFileExtension) {
1809  if (
1810  ($extensionList === ',,' || str_ends_with(mb_strtolower($entry), mb_strtolower('.' . $allowedFileExtension)))
1811  && ($excludePattern === '' || !preg_match('/^' . $excludePattern . '$/', $entry))
1812  ) {
1813  if ($order !== 'mtime') {
1814  $files[] = $entry;
1815  } else {
1816  // Store the value in the key so we can do a fast asort later.
1817  $files[$entry] = filemtime($completePathToEntry);
1818  }
1819  }
1820  }
1821  }
1822 
1823  $valueName = 'value';
1824  if ($order === 'mtime') {
1825  asort($files);
1826  $valueName = 'key';
1827  }
1828 
1829  $valuePathPrefix = $prependPath ? $pathPrefix : '';
1830  $foundFiles = [];
1831  foreach ($files as $key => $value) {
1832  // Don't change this ever - extensions may depend on the fact that the hash is an md5 of the path! (import/export extension)
1833  $foundFiles[md5($pathPrefix . ${$valueName})] = $valuePathPrefix . ${$valueName};
1834  }
1835 
1836  return $foundFiles;
1837  }
1838 
1850  public static function getAllFilesAndFoldersInPath(array $fileArr, string $path, string $extList = '', bool $regDirs = false, int $recursivityLevels = 99, string $excludePattern = ''): array
1851  {
1852  if ($regDirs) {
1853  $fileArr[md5($path)] = $path;
1854  }
1855  $fileArr = array_merge($fileArr, (array)self::getFilesInDir($path, $extList, true, '', $excludePattern));
1856  $dirs = ‪self::get_dirs($path);
1857  if ($recursivityLevels > 0 && is_array($dirs)) {
1858  foreach ($dirs as $subdirs) {
1859  if ((string)$subdirs !== '' && ($excludePattern === '' || !preg_match('/^' . $excludePattern . '$/', $subdirs))) {
1860  $fileArr = self::getAllFilesAndFoldersInPath($fileArr, $path . $subdirs . '/', $extList, $regDirs, $recursivityLevels - 1, $excludePattern);
1861  }
1862  }
1863  }
1864  return $fileArr;
1865  }
1866 
1874  public static function removePrefixPathFromList(array $fileArr, string $prefixToRemove): array|string
1875  {
1876  foreach ($fileArr as &$absFileRef) {
1877  if (str_starts_with($absFileRef, $prefixToRemove)) {
1878  $absFileRef = substr($absFileRef, strlen($prefixToRemove));
1879  } else {
1880  return 'ERROR: One or more of the files was NOT prefixed with the prefix-path!';
1881  }
1882  }
1883  unset($absFileRef);
1884  return $fileArr;
1885  }
1886 
1890  public static function fixWindowsFilePath(string $theFile): string
1891  {
1892  return str_replace(['\\', '//'], '/', $theFile);
1893  }
1894 
1901  public static function resolveBackPath(string $pathStr): string
1902  {
1903  if (!str_contains($pathStr, '..')) {
1904  return $pathStr;
1905  }
1906  $parts = explode('/', $pathStr);
1907  ‪$output = [];
1908  $c = 0;
1909  foreach ($parts as $part) {
1910  if ($part === '..') {
1911  if ($c) {
1912  array_pop(‪$output);
1913  --$c;
1914  } else {
1915  ‪$output[] = $part;
1916  }
1917  } else {
1918  ++$c;
1919  ‪$output[] = $part;
1920  }
1921  }
1922  return implode('/', ‪$output);
1923  }
1924 
1934  public static function locationHeaderUrl(string $path): string
1935  {
1936  if (str_starts_with($path, '//')) {
1937  return $path;
1938  }
1939 
1940  // relative to HOST
1941  if (str_starts_with($path, '/')) {
1942  return self::getIndpEnv('TYPO3_REQUEST_HOST') . $path;
1943  }
1944 
1945  $urlComponents = parse_url($path);
1946  if (!($urlComponents['scheme'] ?? false)) {
1947  // No scheme either
1948  return self::getIndpEnv('TYPO3_REQUEST_DIR') . $path;
1949  }
1950 
1951  return $path;
1952  }
1953 
1961  public static function getMaxUploadFileSize(): int
1962  {
1963  $uploadMaxFilesize = (string)ini_get('upload_max_filesize');
1964  $postMaxSize = (string)ini_get('post_max_size');
1965  // Check for PHP restrictions of the maximum size of one of the $_FILES
1966  $phpUploadLimit = self::getBytesFromSizeMeasurement($uploadMaxFilesize);
1967  // Check for PHP restrictions of the maximum $_POST size
1968  $phpPostLimit = self::getBytesFromSizeMeasurement($postMaxSize);
1969  // If the total amount of post data is smaller (!) than the upload_max_filesize directive,
1970  // then this is the real limit in PHP
1971  $phpUploadLimit = $phpPostLimit > 0 && $phpPostLimit < $phpUploadLimit ? $phpPostLimit : $phpUploadLimit;
1972  return (int)(floor($phpUploadLimit) / 1024);
1973  }
1974 
1981  public static function getBytesFromSizeMeasurement(string $measurement): int
1982  {
1983  $bytes = (float)$measurement;
1984  if (stripos($measurement, 'G')) {
1985  $bytes *= 1024 * 1024 * 1024;
1986  } elseif (stripos($measurement, 'M')) {
1987  $bytes *= 1024 * 1024;
1988  } elseif (stripos($measurement, 'K')) {
1989  $bytes *= 1024;
1990  }
1991  return (int)$bytes;
1992  }
1993 
2010  public static function createVersionNumberedFilename(string $file): string
2011  {
2012  $isFrontend = (‪$GLOBALS['TYPO3_REQUEST'] ?? null) instanceof ServerRequestInterface
2013  && ‪ApplicationType::fromRequest(‪$GLOBALS['TYPO3_REQUEST'])->isFrontend();
2014  $lookupFile = explode('?', $file);
2015  $path = $lookupFile[0];
2016 
2017  // @todo: in v13 this should be resolved by using Environment::getPublicPath() only
2018  if ($isFrontend) {
2019  // Frontend should still allow /static/myfile.css - see #98106
2020  // This should happen regardless of the incoming path is absolute or not
2021  $path = self::resolveBackPath(self::dirname(‪Environment::getCurrentScript()) . '/' . $path);
2022  } elseif (!‪PathUtility::isAbsolutePath($path)) {
2023  // Backend and non-absolute path
2024  $path = self::resolveBackPath(self::dirname(‪Environment::getCurrentScript()) . '/' . $path);
2025  }
2026 
2027  if ($isFrontend) {
2028  $configValue = (bool)(‪$GLOBALS['TYPO3_CONF_VARS']['FE']['versionNumberInFilename'] ?? false);
2029  } else {
2030  $configValue = (bool)(‪$GLOBALS['TYPO3_CONF_VARS']['BE']['versionNumberInFilename'] ?? false);
2031  }
2032  try {
2033  $fileExists = file_exists($path);
2034  } catch (\Throwable $e) {
2035  $fileExists = false;
2036  }
2037  if (!$fileExists) {
2038  // File not found, return filename unaltered
2039  $fullName = $file;
2040  } else {
2041  if (!$configValue) {
2042  // If .htaccess rule is not configured,
2043  // use the default query-string method
2044  if (!empty($lookupFile[1])) {
2045  $separator = '&';
2046  } else {
2047  $separator = '?';
2048  }
2049  $fullName = $file . $separator . filemtime($path);
2050  } else {
2051  // Change the filename
2052  $name = explode('.', $lookupFile[0]);
2053  $extension = array_pop($name);
2054  array_push($name, filemtime($path), $extension);
2055  $fullName = implode('.', $name);
2056  // Append potential query string
2057  $fullName .= !empty($lookupFile[1]) ? '?' . $lookupFile[1] : '';
2058  }
2059  }
2060  return $fullName;
2061  }
2062 
2070  public static function writeJavaScriptContentToTemporaryFile(string $content): string
2071  {
2072  $script = 'typo3temp/assets/js/' . md5($content) . '.js';
2073  if (!@is_file(‪Environment::getPublicPath() . '/' . $script)) {
2075  }
2076  return $script;
2077  }
2078 
2086  public static function writeStyleSheetContentToTemporaryFile(string $content): string
2087  {
2088  $script = 'typo3temp/assets/css/' . md5($content) . '.css';
2089  if (!@is_file(‪Environment::getPublicPath() . '/' . $script)) {
2091  }
2092  return $script;
2093  }
2094 
2102  public static function setIndpEnv(string $envName, string|bool|array|null $value)
2103  {
2104  self::$indpEnvCache[$envName] = $value;
2105  }
2106 
2115  public static function getIndpEnv(string $getEnvName): string|bool|array|null
2116  {
2117  if (array_key_exists($getEnvName, self::$indpEnvCache)) {
2118  return self::$indpEnvCache[$getEnvName];
2119  }
2120 
2121  /*
2122  Conventions:
2123  output from parse_url():
2124  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
2125  [scheme] => 'http'
2126  [user] => 'username'
2127  [pass] => 'password'
2128  [host] => '192.168.1.4'
2129  [port] => '8080'
2130  [path] => '/typo3/32/temp/phpcheck/index.php/arg1/arg2/arg3/'
2131  [query] => 'arg1,arg2,arg3&p1=parameter1&p2[key]=value'
2132  [fragment] => 'link1'Further definition: [path_script] = '/typo3/32/temp/phpcheck/index.php'
2133  [path_dir] = '/typo3/32/temp/phpcheck/'
2134  [path_info] = '/arg1/arg2/arg3/'
2135  [path] = [path_script/path_dir][path_info]Keys supported:URI______:
2136  REQUEST_URI = [path]?[query] = /typo3/32/temp/phpcheck/index.php/arg1/arg2/arg3/?arg1,arg2,arg3&p1=parameter1&p2[key]=value
2137  HTTP_HOST = [host][:[port]] = 192.168.1.4:8080
2138  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')!
2139  PATH_INFO = [path_info] = /arg1/arg2/arg3/
2140  QUERY_STRING = [query] = arg1,arg2,arg3&p1=parameter1&p2[key]=value
2141  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
2142  (Notice: NO username/password + NO fragment)CLIENT____:
2143  REMOTE_ADDR = (client IP)
2144  REMOTE_HOST = (client host)
2145  HTTP_USER_AGENT = (client user agent)
2146  HTTP_ACCEPT_LANGUAGE = (client accept language)SERVER____:
2147  SCRIPT_FILENAME = Absolute filename of script (Differs between windows/unix). On windows 'C:\\some\\path\\' will be converted to 'C:/some/path/'Special extras:
2148  TYPO3_HOST_ONLY = [host] = 192.168.1.4
2149  TYPO3_PORT = [port] = 8080 (blank if 80, taken from host value)
2150  TYPO3_REQUEST_HOST = [scheme]://[host][:[port]]
2151  TYPO3_REQUEST_URL = [scheme]://[host][:[port]][path]?[query] (scheme will by default be "http" until we can detect something different)
2152  TYPO3_REQUEST_SCRIPT = [scheme]://[host][:[port]][path_script]
2153  TYPO3_REQUEST_DIR = [scheme]://[host][:[port]][path_dir]
2154  TYPO3_SITE_URL = [scheme]://[host][:[port]][path_dir] of the TYPO3 website frontend
2155  TYPO3_SITE_PATH = [path_dir] of the TYPO3 website frontend
2156  TYPO3_SITE_SCRIPT = [script / Speaking URL] of the TYPO3 website
2157  TYPO3_DOCUMENT_ROOT = Absolute path of root of documents: TYPO3_DOCUMENT_ROOT.SCRIPT_NAME = SCRIPT_FILENAME (typically)
2158  TYPO3_SSL = Returns TRUE if this session uses SSL/TLS (https)
2159  TYPO3_PROXY = Returns TRUE if this session runs over a well known proxyNotice: [fragment] is apparently NEVER available to the script!Testing suggestions:
2160  - Output all the values.
2161  - 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
2162  - ALSO TRY the script from the ROOT of a site (like 'http://www.mytest.com/' and not 'http://www.mytest.com/test/' !!)
2163  */
2164  $retVal = '';
2165  switch ((string)$getEnvName) {
2166  case 'SCRIPT_NAME':
2167  $retVal = ‪$_SERVER['SCRIPT_NAME'] ?? '';
2168  // Add a prefix if TYPO3 is behind a proxy: ext-domain.com => int-server.com/prefix
2169  if (self::cmpIP(‪$_SERVER['REMOTE_ADDR'] ?? '', ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP'] ?? '')) {
2170  if (self::getIndpEnv('TYPO3_SSL') && ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefixSSL']) {
2171  $retVal = ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefixSSL'] . $retVal;
2172  } elseif (‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefix']) {
2173  $retVal = ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefix'] . $retVal;
2174  }
2175  }
2176  $retVal = self::encodeFileSystemPathComponentForUrlPath($retVal);
2177  break;
2178  case 'SCRIPT_FILENAME':
2180  break;
2181  case 'REQUEST_URI':
2182  // Typical application of REQUEST_URI is return urls, forms submitting to itself etc. Example: returnUrl='.rawurlencode(\TYPO3\CMS\Core\Utility\GeneralUtility::getIndpEnv('REQUEST_URI'))
2183  if (!empty(‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['requestURIvar'])) {
2184  // This is for URL rewriters that store the original URI in a server variable (eg ISAPI_Rewriter for IIS: HTTP_X_REWRITE_URL)
2185  [$v, $n] = explode('|', ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['requestURIvar']);
2186  $retVal = ‪$GLOBALS[$v][$n];
2187  } elseif (empty(‪$_SERVER['REQUEST_URI'])) {
2188  // This is for ISS/CGI which does not have the REQUEST_URI available.
2189  $retVal = '/' . ltrim(self::getIndpEnv('SCRIPT_NAME'), '/') . (!empty(‪$_SERVER['QUERY_STRING']) ? '?' . ‪$_SERVER['QUERY_STRING'] : '');
2190  } else {
2191  $retVal = '/' . ltrim(‪$_SERVER['REQUEST_URI'], '/');
2192  }
2193  // Add a prefix if TYPO3 is behind a proxy: ext-domain.com => int-server.com/prefix
2194  if (isset(‪$_SERVER['REMOTE_ADDR'], ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP'])
2195  && self::cmpIP(‪$_SERVER['REMOTE_ADDR'], ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP'])
2196  ) {
2197  if (self::getIndpEnv('TYPO3_SSL') && ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefixSSL']) {
2198  $retVal = ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefixSSL'] . $retVal;
2199  } elseif (‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefix']) {
2200  $retVal = ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefix'] . $retVal;
2201  }
2202  }
2203  break;
2204  case 'PATH_INFO':
2205  $retVal = ‪$_SERVER['PATH_INFO'] ?? '';
2206  break;
2207  case 'TYPO3_REV_PROXY':
2208  $retVal = ‪self::cmpIP(‪$_SERVER['REMOTE_ADDR'] ?? '', ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP']);
2209  break;
2210  case 'REMOTE_ADDR':
2211  $retVal = ‪$_SERVER['REMOTE_ADDR'] ?? '';
2212  if (self::cmpIP($retVal, ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP'] ?? '')) {
2213  $ip = ‪self::trimExplode(',', ‪$_SERVER['HTTP_X_FORWARDED_FOR'] ?? '');
2214  // Choose which IP in list to use
2215  if (!empty($ip)) {
2216  switch (‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyHeaderMultiValue']) {
2217  case 'last':
2218  $ip = array_pop($ip);
2219  break;
2220  case 'first':
2221  $ip = array_shift($ip);
2222  break;
2223  case 'none':
2224 
2225  default:
2226  $ip = '';
2227  }
2228  }
2229  if (self::validIP((string)$ip)) {
2230  $retVal = $ip;
2231  }
2232  }
2233  break;
2234  case 'HTTP_HOST':
2235  // if it is not set we're most likely on the cli
2236  $retVal = ‪$_SERVER['HTTP_HOST'] ?? '';
2237  if (isset(‪$_SERVER['REMOTE_ADDR']) && static::cmpIP(‪$_SERVER['REMOTE_ADDR'], ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP'])) {
2238  $host = ‪self::trimExplode(',', ‪$_SERVER['HTTP_X_FORWARDED_HOST'] ?? '');
2239  // Choose which host in list to use
2240  if (!empty($host)) {
2241  switch (‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyHeaderMultiValue']) {
2242  case 'last':
2243  $host = array_pop($host);
2244  break;
2245  case 'first':
2246  $host = array_shift($host);
2247  break;
2248  case 'none':
2249 
2250  default:
2251  $host = '';
2252  }
2253  }
2254  if ($host) {
2255  $retVal = $host;
2256  }
2257  }
2258  break;
2259  case 'HTTP_REFERER':
2260 
2261  case 'HTTP_USER_AGENT':
2262 
2263  case 'HTTP_ACCEPT_ENCODING':
2264 
2265  case 'HTTP_ACCEPT_LANGUAGE':
2266 
2267  case 'REMOTE_HOST':
2268 
2269  case 'QUERY_STRING':
2270  $retVal = ‪$_SERVER[$getEnvName] ?? '';
2271  break;
2272  case 'TYPO3_DOCUMENT_ROOT':
2273  // Get the web root (it is not the root of the TYPO3 installation)
2274  // The absolute path of the script can be calculated with TYPO3_DOCUMENT_ROOT + SCRIPT_FILENAME
2275  // 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.
2276  // Therefore the DOCUMENT_ROOT is now always calculated as the SCRIPT_FILENAME minus the end part shared with SCRIPT_NAME.
2277  $SFN = self::getIndpEnv('SCRIPT_FILENAME');
2278  // Use rawurldecode to reverse the result of self::encodeFileSystemPathComponentForUrlPath()
2279  // which has been applied to getIndpEnv(SCRIPT_NAME) for web URI usage.
2280  // We compare with a file system path (SCRIPT_FILENAME) in here and therefore need to undo the encoding.
2281  $SN_A = array_map(rawurldecode(...), explode('/', strrev(self::getIndpEnv('SCRIPT_NAME'))));
2282  $SFN_A = explode('/', strrev($SFN));
2283  $acc = [];
2284  foreach ($SN_A as $kk => $vv) {
2285  if ((string)$SFN_A[$kk] === (string)$vv) {
2286  $acc[] = $vv;
2287  } else {
2288  break;
2289  }
2290  }
2291  $commonEnd = strrev(implode('/', $acc));
2292  if ((string)$commonEnd !== '') {
2293  $retVal = substr($SFN, 0, -(strlen($commonEnd) + 1));
2294  }
2295  break;
2296  case 'TYPO3_HOST_ONLY':
2297  $httpHost = self::getIndpEnv('HTTP_HOST');
2298  $httpHostBracketPosition = strpos($httpHost, ']');
2299  $httpHostParts = explode(':', $httpHost);
2300  $retVal = $httpHostBracketPosition !== false ? substr($httpHost, 0, $httpHostBracketPosition + 1) : array_shift($httpHostParts);
2301  break;
2302  case 'TYPO3_PORT':
2303  $httpHost = self::getIndpEnv('HTTP_HOST');
2304  $httpHostOnly = self::getIndpEnv('TYPO3_HOST_ONLY');
2305  $retVal = strlen($httpHost) > strlen($httpHostOnly) ? substr($httpHost, strlen($httpHostOnly) + 1) : '';
2306  break;
2307  case 'TYPO3_REQUEST_HOST':
2308  $retVal = (self::getIndpEnv('TYPO3_SSL') ? 'https://' : 'http://') . self::getIndpEnv('HTTP_HOST');
2309  break;
2310  case 'TYPO3_REQUEST_URL':
2311  $retVal = self::getIndpEnv('TYPO3_REQUEST_HOST') . self::getIndpEnv('REQUEST_URI');
2312  break;
2313  case 'TYPO3_REQUEST_SCRIPT':
2314  $retVal = self::getIndpEnv('TYPO3_REQUEST_HOST') . self::getIndpEnv('SCRIPT_NAME');
2315  break;
2316  case 'TYPO3_REQUEST_DIR':
2317  $retVal = self::getIndpEnv('TYPO3_REQUEST_HOST') . self::dirname(self::getIndpEnv('SCRIPT_NAME')) . '/';
2318  break;
2319  case 'TYPO3_SITE_URL':
2322  ‪$url = self::getIndpEnv('TYPO3_REQUEST_DIR');
2323  $siteUrl = substr(‪$url, 0, -strlen($lPath));
2324  if (substr($siteUrl, -1) !== '/') {
2325  $siteUrl .= '/';
2326  }
2327  $retVal = $siteUrl;
2328  }
2329  break;
2330  case 'TYPO3_SITE_PATH':
2331  $retVal = substr(self::getIndpEnv('TYPO3_SITE_URL'), strlen(self::getIndpEnv('TYPO3_REQUEST_HOST')));
2332  break;
2333  case 'TYPO3_SITE_SCRIPT':
2334  $retVal = substr(self::getIndpEnv('TYPO3_REQUEST_URL'), strlen(self::getIndpEnv('TYPO3_SITE_URL')));
2335  break;
2336  case 'TYPO3_SSL':
2337  $proxySSL = trim(‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxySSL'] ?? '');
2338  if ($proxySSL === '*') {
2339  $proxySSL = ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP'];
2340  }
2341  if (self::cmpIP(‪$_SERVER['REMOTE_ADDR'] ?? '', $proxySSL)) {
2342  $retVal = true;
2343  } else {
2344  $retVal = self::webserverUsesHttps();
2345  }
2346  break;
2347  case '_ARRAY':
2348  $out = [];
2349  // Here, list ALL possible keys to this function for debug display.
2350  $envTestVars = [
2351  'HTTP_HOST',
2352  'TYPO3_HOST_ONLY',
2353  'TYPO3_PORT',
2354  'PATH_INFO',
2355  'QUERY_STRING',
2356  'REQUEST_URI',
2357  'HTTP_REFERER',
2358  'TYPO3_REQUEST_HOST',
2359  'TYPO3_REQUEST_URL',
2360  'TYPO3_REQUEST_SCRIPT',
2361  'TYPO3_REQUEST_DIR',
2362  'TYPO3_SITE_URL',
2363  'TYPO3_SITE_SCRIPT',
2364  'TYPO3_SSL',
2365  'TYPO3_REV_PROXY',
2366  'SCRIPT_NAME',
2367  'TYPO3_DOCUMENT_ROOT',
2368  'SCRIPT_FILENAME',
2369  'REMOTE_ADDR',
2370  'REMOTE_HOST',
2371  'HTTP_USER_AGENT',
2372  'HTTP_ACCEPT_LANGUAGE',
2373  ];
2374  foreach ($envTestVars as $v) {
2375  $out[$v] = self::getIndpEnv($v);
2376  }
2377  reset($out);
2378  $retVal = $out;
2379  break;
2380  }
2381  self::$indpEnvCache[$getEnvName] = $retVal;
2382  return $retVal;
2383  }
2384 
2393  protected static function webserverUsesHttps(): bool
2394  {
2395  if (!empty(‪$_SERVER['SSL_SESSION_ID'])) {
2396  return true;
2397  }
2398 
2399  // https://secure.php.net/manual/en/reserved.variables.server.php
2400  // "Set to a non-empty value if the script was queried through the HTTPS protocol."
2401  return !empty(‪$_SERVER['HTTPS']) && strtolower(‪$_SERVER['HTTPS']) !== 'off';
2402  }
2403 
2404  protected static function encodeFileSystemPathComponentForUrlPath(string $path): string
2405  {
2406  return implode('/', array_map(rawurlencode(...), explode('/', $path)));
2407  }
2408 
2409  /*************************
2410  *
2411  * TYPO3 SPECIFIC FUNCTIONS
2412  *
2413  *************************/
2423  public static function getFileAbsFileName(string $fileName): string
2424  {
2425  if ($fileName === '') {
2426  return '';
2427  }
2428  $checkForBackPath = fn(string $fileName): string => $fileName !== '' && static::validPathStr($fileName) ? $fileName : '';
2429 
2430  // Extension "EXT:" path resolving.
2431  if (‪PathUtility::isExtensionPath($fileName)) {
2432  try {
2434  } catch (‪PackageException) {
2435  $fileName = '';
2436  }
2437  return $checkForBackPath($fileName);
2438  }
2439 
2440  // Absolute path, but set to blank if not inside allowed directories.
2441  if (‪PathUtility::isAbsolutePath($fileName)) {
2442  if (str_starts_with($fileName, ‪Environment::getProjectPath()) ||
2443  str_starts_with($fileName, ‪Environment::getPublicPath())) {
2444  return $checkForBackPath($fileName);
2445  }
2446  return '';
2447  }
2448 
2449  // Relative path. Prepend with the public web folder.
2450  $fileName = ‪Environment::getPublicPath() . '/' . $fileName;
2451  return $checkForBackPath($fileName);
2452  }
2453 
2465  public static function validPathStr(string $theFile): bool
2466  {
2467  return !str_contains($theFile, '//') && !str_contains($theFile, '\\')
2468  && preg_match('#(?:^\\.\\.|/\\.\\./|[[:cntrl:]])#u', $theFile) === 0;
2469  }
2470 
2476  public static function isAllowedAbsPath(string $path): bool
2477  {
2478  if (substr($path, 0, 6) === 'vfs://') {
2479  return true;
2480  }
2481  return ‪PathUtility::isAbsolutePath($path) && static::validPathStr($path)
2482  && (
2483  str_starts_with($path, ‪Environment::getProjectPath())
2484  || str_starts_with($path, ‪Environment::getPublicPath())
2486  );
2487  }
2488 
2495  public static function copyDirectory(string $source, string $destination): void
2496  {
2497  if (!str_contains($source, ‪Environment::getProjectPath() . '/')) {
2498  $source = ‪Environment::getPublicPath() . '/' . $source;
2499  }
2500  if (!str_contains($destination, ‪Environment::getProjectPath() . '/')) {
2501  $destination = ‪Environment::getPublicPath() . '/' . $destination;
2502  }
2503  if (static::isAllowedAbsPath($source) && static::isAllowedAbsPath($destination)) {
2504  static::mkdir_deep($destination);
2505  $iterator = new \RecursiveIteratorIterator(
2506  new \RecursiveDirectoryIterator($source, \RecursiveDirectoryIterator::SKIP_DOTS),
2507  \RecursiveIteratorIterator::SELF_FIRST
2508  );
2510  foreach ($iterator as $item) {
2511  $target = $destination . '/' . static::fixWindowsFilePath((string)$iterator->getSubPathName());
2512  if ($item->isDir()) {
2513  static::mkdir($target);
2514  } else {
2515  static::upload_copy_move(static::fixWindowsFilePath($item->getPathname()), $target);
2516  }
2517  }
2518  }
2519  }
2520 
2531  public static function sanitizeLocalUrl(string ‪$url): string
2532  {
2533  $sanitizedUrl = '';
2534  if (!empty(‪$url)) {
2535  $decodedUrl = rawurldecode(‪$url);
2536  $parsedUrl = parse_url($decodedUrl);
2537  $testAbsoluteUrl = self::resolveBackPath($decodedUrl);
2538  $testRelativeUrl = self::resolveBackPath(self::dirname(self::getIndpEnv('SCRIPT_NAME')) . '/' . $decodedUrl);
2539  // Pass if URL is on the current host:
2540  if (self::isValidUrl($decodedUrl)) {
2541  if (self::isOnCurrentHost($decodedUrl) && str_starts_with($decodedUrl, self::getIndpEnv('TYPO3_SITE_URL'))) {
2542  $sanitizedUrl = ‪$url;
2543  }
2544  } elseif (‪PathUtility::isAbsolutePath($decodedUrl) && self::isAllowedAbsPath($decodedUrl)) {
2545  $sanitizedUrl = ‪$url;
2546  } elseif (str_starts_with($testAbsoluteUrl, self::getIndpEnv('TYPO3_SITE_PATH')) && $decodedUrl[0] === '/' &&
2547  substr($decodedUrl, 0, 2) !== '//'
2548  ) {
2549  $sanitizedUrl = ‪$url;
2550  } elseif (empty($parsedUrl['scheme']) && str_starts_with($testRelativeUrl, self::getIndpEnv('TYPO3_SITE_PATH'))
2551  && $decodedUrl[0] !== '/' && strpbrk($decodedUrl, '*:|"<>') === false && !str_contains($decodedUrl, '\\\\')
2552  ) {
2553  $sanitizedUrl = ‪$url;
2554  }
2555  }
2556  if (!empty(‪$url) && empty($sanitizedUrl)) {
2557  static::getLogger()->notice('The URL "{url}" is not considered to be local and was denied.', ['url' => ‪$url]);
2558  }
2559  return $sanitizedUrl;
2560  }
2561 
2570  public static function upload_copy_move(string $source, string $destination): bool
2571  {
2572  if (is_array(‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][\‪TYPO3\CMS\Core\Utility\GeneralUtility::class]['moveUploadedFile'] ?? null)) {
2573  $params = ['source' => $source, 'destination' => $destination, 'method' => 'upload_copy_move'];
2574  foreach (‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][\‪TYPO3\CMS\Core\Utility\GeneralUtility::class]['moveUploadedFile'] as $hookMethod) {
2575  $fakeThis = null;
2576  self::callUserFunction($hookMethod, $params, $fakeThis);
2577  }
2578  }
2579 
2580  $result = false;
2581  if (is_uploaded_file($source)) {
2582  // Return the value of move_uploaded_file, and if FALSE the temporary $source is still
2583  // around so the user can use unlink to delete it:
2584  $result = move_uploaded_file($source, $destination);
2585  } else {
2586  @copy($source, $destination);
2587  }
2588  // Change the permissions of the file
2589  ‪self::fixPermissions($destination);
2590  // If here the file is copied and the temporary $source is still around,
2591  // so when returning FALSE the user can try unlink to delete the $source
2592  return $result;
2593  }
2594 
2605  public static function upload_to_tempfile(string $uploadedFileName): string
2606  {
2607  if (is_uploaded_file($uploadedFileName)) {
2608  $tempFile = self::tempnam('upload_temp_');
2609  if (is_array(‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][\‪TYPO3\CMS\Core\Utility\GeneralUtility::class]['moveUploadedFile'] ?? null)) {
2610  $params = ['source' => $uploadedFileName, 'destination' => $tempFile, 'method' => 'upload_to_tempfile'];
2611  foreach (‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][\‪TYPO3\CMS\Core\Utility\GeneralUtility::class]['moveUploadedFile'] as $hookMethod) {
2612  $fakeThis = null;
2613  self::callUserFunction($hookMethod, $params, $fakeThis);
2614  }
2615  }
2616 
2617  move_uploaded_file($uploadedFileName, $tempFile);
2618  return @is_file($tempFile) ? $tempFile : '';
2619  }
2620 
2621  return '';
2622  }
2623 
2634  public static function unlink_tempfile(string $uploadedTempFileName): ?bool
2635  {
2636  if ($uploadedTempFileName) {
2637  $uploadedTempFileName = self::fixWindowsFilePath((string)$uploadedTempFileName);
2638  if (
2639  self::validPathStr($uploadedTempFileName)
2640  && (
2641  str_starts_with($uploadedTempFileName, ‪Environment::getPublicPath() . '/typo3temp/')
2642  || str_starts_with($uploadedTempFileName, ‪Environment::getVarPath() . '/')
2643  )
2644  && @is_file($uploadedTempFileName)
2645  ) {
2646  if (unlink($uploadedTempFileName)) {
2647  return true;
2648  }
2649  }
2650  }
2651 
2652  return null;
2653  }
2654 
2666  public static function tempnam(string $filePrefix, string $fileSuffix = ''): string
2667  {
2668  $temporaryPath = ‪Environment::getVarPath() . '/transient/';
2669  if (!is_dir($temporaryPath)) {
2670  ‪self::mkdir_deep($temporaryPath);
2671  }
2672  if ($fileSuffix === '') {
2673  $path = (string)tempnam($temporaryPath, $filePrefix);
2674  $tempFileName = $temporaryPath . ‪PathUtility::basename($path);
2675  } else {
2676  do {
2677  $tempFileName = $temporaryPath . $filePrefix . random_int(1, PHP_INT_MAX) . $fileSuffix;
2678  } while (file_exists($tempFileName));
2679  touch($tempFileName);
2680  clearstatcache(false, $tempFileName);
2681  }
2682  return $tempFileName;
2683  }
2684 
2695  public static function callUserFunction(string|\Closure $funcName, mixed &$params, ?object $ref = null): mixed
2696  {
2697  // Check if we're using a closure and invoke it directly.
2698  if (is_object($funcName) && is_a($funcName, \Closure::class)) {
2699  return call_user_func_array($funcName, [&$params, &$ref]);
2700  }
2701  $funcName = trim($funcName);
2702  $parts = explode('->', $funcName);
2703  // Call function or method
2704  if (count($parts) === 2) {
2705  // It's a class/method
2706  // Check if class/method exists:
2707  if (class_exists($parts[0])) {
2708  // Create object
2709  $classObj = self::makeInstance($parts[0]);
2710  $methodName = (string)$parts[1];
2711  $callable = [$classObj, $methodName];
2712  if (is_callable($callable)) {
2713  // Call method:
2714  $content = call_user_func_array($callable, [&$params, &$ref]);
2715  } else {
2716  throw new \InvalidArgumentException('No method name \'' . $parts[1] . '\' in class ' . $parts[0], 1294585865);
2717  }
2718  } else {
2719  throw new \InvalidArgumentException('No class named ' . $parts[0], 1294585866);
2720  }
2721  } elseif (function_exists($funcName) && is_callable($funcName)) {
2722  // It's a function
2723  $content = call_user_func_array($funcName, [&$params, &$ref]);
2724  } else {
2725  // Usually this will be annotated by static code analysis tools, but there's no native "not empty string" type
2726  throw new \InvalidArgumentException('No function named: ' . $funcName, 1294585867);
2727  }
2728  return $content;
2729  }
2730 
2734  public static function setContainer(ContainerInterface ‪$container): void
2735  {
2736  self::$container = ‪$container;
2737  }
2738 
2742  public static function getContainer(): ContainerInterface
2743  {
2744  if (self::$container === null) {
2745  throw new \LogicException('PSR-11 Container is not available', 1549404144);
2746  }
2747  return ‪self::$container;
2748  }
2749 
2764  public static function makeInstance(string $className, mixed ...$constructorArguments): object
2765  {
2766  // PHPStan will complain about this check. That's okay as we're checking a contract violation here.
2767  if ($className === '') {
2768  throw new \InvalidArgumentException('$className must be a non empty string.', 1288965219);
2769  }
2770  // Never instantiate with a beginning backslash, otherwise things like singletons won't work.
2771  if (str_starts_with($className, '\\')) {
2772  throw new \InvalidArgumentException(
2773  '$className "' . $className . '" must not start with a backslash.',
2774  1420281366
2775  );
2776  }
2777  if (isset(static::$finalClassNameCache[$className])) {
2778  $finalClassName = static::$finalClassNameCache[$className];
2779  } else {
2780  $finalClassName = self::getClassName($className);
2781  static::$finalClassNameCache[$className] = $finalClassName;
2782  }
2783  // Return singleton instance if it is already registered
2784  if (isset(self::$singletonInstances[$finalClassName])) {
2785  return self::$singletonInstances[$finalClassName];
2786  }
2787  // Return instance if it has been injected by addInstance()
2788  if (
2789  isset(self::$nonSingletonInstances[$finalClassName])
2790  && !empty(self::$nonSingletonInstances[$finalClassName])
2791  ) {
2792  return array_shift(self::$nonSingletonInstances[$finalClassName]);
2793  }
2794 
2795  // Read service and prototypes from the DI container, this is required to
2796  // support classes that require dependency injection.
2797  // We operate on the original class name on purpose, as class overrides
2798  // are resolved inside the container
2799  if (self::$container !== null && $constructorArguments === [] && self::$container->has($className)) {
2800  return self::$container->get($className);
2801  }
2802 
2803  // Create new instance and call constructor with parameters
2804  $instance = new $finalClassName(...$constructorArguments);
2805  // Register new singleton instance, but only if it is not a known PSR-11 container service
2806  if ($instance instanceof SingletonInterface && !(self::$container !== null && self::$container->has($className))) {
2807  self::$singletonInstances[$finalClassName] = $instance;
2808  }
2809  if ($instance instanceof LoggerAwareInterface) {
2810  $instance->setLogger(static::makeInstance(LogManager::class)->getLogger($className));
2811  }
2812  return $instance;
2813  }
2814 
2826  public static function makeInstanceForDi(string $className, mixed ...$constructorArguments): object
2827  {
2828  $finalClassName = static::$finalClassNameCache[$className] ?? static::$finalClassNameCache[$className] = self::getClassName($className);
2829 
2830  // Return singleton instance if it is already registered (currently required for unit and functional tests)
2831  if (isset(self::$singletonInstances[$finalClassName])) {
2832  return self::$singletonInstances[$finalClassName];
2833  }
2834  // Create new instance and call constructor with parameters
2835  return new $finalClassName(...$constructorArguments);
2836  }
2837 
2847  public static function getClassName(string $className): string
2848  {
2849  if (class_exists($className)) {
2850  while (static::classHasImplementation($className)) {
2851  $className = static::getImplementationForClass($className);
2852  }
2853  }
2855  }
2856 
2863  protected static function getImplementationForClass(string $className): string
2864  {
2865  return ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['Objects'][$className]['className'];
2866  }
2867 
2873  protected static function classHasImplementation(string $className): bool
2874  {
2875  return !empty(‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['Objects'][$className]['className']);
2876  }
2877 
2894  public static function setSingletonInstance(string $className, SingletonInterface $instance): void
2895  {
2896  self::checkInstanceClassName($className, $instance);
2897  // Check for XCLASS registration (same is done in makeInstance() in order to store the singleton of the final class name)
2898  $finalClassName = self::getClassName($className);
2899  self::$singletonInstances[$finalClassName] = $instance;
2900  }
2901 
2916  public static function removeSingletonInstance(string $className, SingletonInterface $instance): void
2917  {
2918  self::checkInstanceClassName($className, $instance);
2919  if (!isset(self::$singletonInstances[$className])) {
2920  throw new \InvalidArgumentException('No Instance registered for ' . $className . '.', 1394099179);
2921  }
2922  if ($instance !== self::$singletonInstances[$className]) {
2923  throw new \InvalidArgumentException('The instance you are trying to remove has not been registered before.', 1394099256);
2924  }
2925  unset(self::$singletonInstances[$className]);
2926  }
2927 
2941  public static function resetSingletonInstances(array $newSingletonInstances): void
2942  {
2943  static::$singletonInstances = [];
2944  foreach ($newSingletonInstances as $className => $instance) {
2945  static::setSingletonInstance($className, $instance);
2946  }
2947  }
2948 
2961  public static function getSingletonInstances(): array
2962  {
2963  return static::$singletonInstances;
2964  }
2965 
2977  public static function getInstances(): array
2978  {
2979  return static::$nonSingletonInstances;
2980  }
2981 
2995  public static function addInstance(string $className, object $instance): void
2996  {
2997  self::checkInstanceClassName($className, $instance);
2998  if ($instance instanceof SingletonInterface) {
2999  throw new \InvalidArgumentException('$instance must not be an instance of TYPO3\\CMS\\Core\\SingletonInterface. For setting singletons, please use setSingletonInstance.', 1288969325);
3000  }
3001  if (!isset(self::$nonSingletonInstances[$className])) {
3002  self::$nonSingletonInstances[$className] = [];
3003  }
3004  self::$nonSingletonInstances[$className][] = $instance;
3005  }
3006 
3012  protected static function checkInstanceClassName(string $className, object $instance): void
3013  {
3014  if ($className === '') {
3015  throw new \InvalidArgumentException('$className must not be empty.', 1288967479);
3016  }
3017  if (!$instance instanceof $className) {
3018  throw new \InvalidArgumentException('$instance must be an instance of ' . $className . ', but actually is an instance of ' . get_class($instance) . '.', 1288967686);
3019  }
3020  }
3021 
3032  public static function purgeInstances(): void
3033  {
3034  self::$container = null;
3035  self::$singletonInstances = [];
3036  self::$nonSingletonInstances = [];
3037  }
3038 
3048  public static function flushInternalRuntimeCaches(): void
3049  {
3050  self::$finalClassNameCache = [];
3051  self::$indpEnvCache = [];
3052  }
3053 
3068  public static function makeInstanceService(string $serviceType, string $serviceSubType = '', array $excludeServiceKeys = []): array|object|false
3069  {
3070  $error = false;
3071  $requestInfo = [
3072  'requestedServiceType' => $serviceType,
3073  'requestedServiceSubType' => $serviceSubType,
3074  'requestedExcludeServiceKeys' => $excludeServiceKeys,
3075  ];
3076  while ($info = ‪ExtensionManagementUtility::findService($serviceType, $serviceSubType, $excludeServiceKeys)) {
3077  // provide information about requested service to service object
3078  $info = array_merge($info, $requestInfo);
3079 
3081  $className = $info['className'];
3083  $obj = self::makeInstance($className);
3084  if (is_object($obj)) {
3085  if (!is_callable([$obj, 'init'])) {
3086  self::getLogger()->error('Requested service {class} has no init() method.', [
3087  'class' => $info['className'],
3088  'service' => $info,
3089  ]);
3090  throw new \RuntimeException('Broken service: ' . $info['className'], 1568119209);
3091  }
3092  $obj->info = $info;
3093  // service available?
3094  if ($obj->init()) {
3095  return $obj;
3096  }
3097  $error = $obj->getLastErrorArray();
3098  unset($obj);
3099  }
3100 
3101  // deactivate the service
3102  ‪ExtensionManagementUtility::deactivateService($info['serviceType'], $info['serviceKey']);
3103  }
3104  return $error;
3105  }
3106 
3113  public static function quoteJSvalue(string $value): string
3114  {
3115  $json = (string)json_encode(
3116  $value,
3117  JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_TAG
3118  );
3119 
3120  return strtr(
3121  $json,
3122  [
3123  '"' => '\'',
3124  '\\\\' => '\\u005C',
3125  ' ' => '\\u0020',
3126  '!' => '\\u0021',
3127  '\\t' => '\\u0009',
3128  '\\n' => '\\u000A',
3129  '\\r' => '\\u000D',
3130  ]
3131  );
3132  }
3133 
3140  public static function jsonEncodeForHtmlAttribute(mixed $value, bool $useHtmlEntities = true): string
3141  {
3142  $json = (string)json_encode($value, JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_TAG);
3143  return $useHtmlEntities ? htmlspecialchars($json) : $json;
3144  }
3145 
3152  public static function jsonEncodeForJavaScript(mixed $value): string
3153  {
3154  $json = (string)json_encode($value, JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_TAG);
3155  return strtr(
3156  $json,
3157  [
3158  // comments below refer to JSON-encoded data
3159  '\\\\' => '\\\\u005C', // `"\\Vendor\\Package"` -> `"\\u005CVendor\\u005CPackage"`
3160  '\\t' => '\\u0009', // `"\t"` -> `"\u0009"`
3161  '\\n' => '\\u000A', // `"\n"` -> `"\u000A"`
3162  '\\r' => '\\u000D', // `"\r"` -> `"\u000D"`
3163  ]
3164  );
3165  }
3166 
3171  public static function sanitizeCssVariableValue(string $value): string
3172  {
3173  $value = str_replace(['{', '}', "\n", "\r"], '', $value);
3174  // keep quotes, e.g. for `background: url("/res/background.png")`
3175  return htmlspecialchars($value, ENT_SUBSTITUTE);
3176  }
3177 
3178  protected static function getLogger(): LoggerInterface
3179  {
3180  return static::makeInstance(LogManager::class)->getLogger(__CLASS__);
3181  }
3182 }
‪TYPO3\CMS\Core\Utility\GeneralUtility\underscoredToLowerCamelCase
‪static string underscoredToLowerCamelCase($string)
Definition: GeneralUtility.php:666
‪TYPO3\CMS\Core\Utility\GeneralUtility\mkdir
‪static bool mkdir(string $newFolder)
Definition: GeneralUtility.php:1633
‪TYPO3\CMS\Core\Utility\PathUtility\stripPathSitePrefix
‪static stripPathSitePrefix(string $path)
Definition: PathUtility.php:428
‪TYPO3\CMS\Core\Utility\GeneralUtility\$nl
‪$nl
Definition: GeneralUtility.php:1152
‪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:634
‪TYPO3\CMS\Core\Utility\GeneralUtility\get_dirs
‪static string[] string null get_dirs(string $path)
Definition: GeneralUtility.php:1754
‪TYPO3\CMS\Core\Utility\GeneralUtility\implodeArrayForUrl
‪static string implodeArrayForUrl(string $name, array $theArray, string $str='', bool $skipBlank=false, bool $rawurlencodeParamName=false)
Definition: GeneralUtility.php:855
‪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:567
‪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:559
‪$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:571
‪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:678
‪$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:1283
‪TYPO3\CMS\Core\Authentication\AbstractAuthenticationService
Definition: AbstractAuthenticationService.php:29
‪TYPO3\CMS\Core\Utility\GeneralUtility\mkdir_deep
‪static mkdir_deep(string $directory)
Definition: GeneralUtility.php:1649
‪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:565
‪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:1151
‪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:572
‪TYPO3\CMS\Core\Utility\GeneralUtility\writeFileToTypo3tempDir
‪static string null writeFileToTypo3tempDir(string $filepath, string $content)
Definition: GeneralUtility.php:1556
‪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:1242
‪TYPO3\CMS\Core\Utility\GeneralUtility\writeFile
‪static bool writeFile(string $file, string $content, bool $changePermissions=false)
Definition: GeneralUtility.php:1464
‪TYPO3\CMS\Core\Utility\GeneralUtility\getUrl
‪static string false getUrl(string $url)
Definition: GeneralUtility.php:1439
‪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:474
‪TYPO3\CMS\Core\Utility\GeneralUtility\rmdir
‪static bool rmdir(string $path, bool $removeNonEmpty=false)
Definition: GeneralUtility.php:1697
‪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:570
‪TYPO3\CMS\Core\Http\RequestFactory
Definition: RequestFactory.php:30
‪TYPO3\CMS\Core\Utility\GeneralUtility\$currentLocale
‪$currentLocale
Definition: GeneralUtility.php:564
‪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:1491
‪TYPO3\CMS\Core\Utility\GeneralUtility\$output
‪$output
Definition: GeneralUtility.php:1154
‪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:1670
‪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:782
‪TYPO3\CMS\Core\Utility\GeneralUtility\isValidUrl
‪static bool isValidUrl(string $url)
Definition: GeneralUtility.php:708
‪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:1389
‪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:67
‪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:751
‪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:654
‪TYPO3\CMS\Core\Utility\GeneralUtility\xml2array
‪static array string xml2array(string $string, string $NSprefix='', bool $reportDocTag=false)
Definition: GeneralUtility.php:1260
‪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:817
‪TYPO3\CMS\Webhooks\Message\$identifier
‪identifier readonly string $identifier
Definition: FileAddedMessage.php:37
‪TYPO3\CMS\Core\Http\ApplicationType
‪ApplicationType
Definition: ApplicationType.php:56
‪TYPO3\CMS\Core\Core\Environment\isWindows
‪static isWindows()
Definition: Environment.php:276