GeneralUtility::removeDotsFromTS()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 8
dl 0
loc 12
rs 10
c 0
b 0
f 0
cc 3
nc 3
nop 1
1
<?php
2
3
/*
4
 * This file is part of the TYPO3 CMS project.
5
 *
6
 * It is free software; you can redistribute it and/or modify it under
7
 * the terms of the GNU General Public License, either version 2
8
 * of the License, or any later version.
9
 *
10
 * For the full copyright and license information, please read the
11
 * LICENSE.txt file that was distributed with this source code.
12
 *
13
 * The TYPO3 project - inspiring people to share!
14
 */
15
16
namespace TYPO3\CMS\Core\Utility;
17
18
use Egulias\EmailValidator\EmailValidator;
19
use Egulias\EmailValidator\Validation\EmailValidation;
20
use Egulias\EmailValidator\Validation\MultipleValidationWithAnd;
21
use Egulias\EmailValidator\Validation\RFCValidation;
22
use GuzzleHttp\Exception\RequestException;
23
use Psr\Container\ContainerInterface;
24
use Psr\Http\Message\ServerRequestInterface;
25
use Psr\Log\LoggerAwareInterface;
26
use Psr\Log\LoggerInterface;
27
use TYPO3\CMS\Core\Cache\CacheManager;
28
use TYPO3\CMS\Core\Core\ClassLoadingInformation;
29
use TYPO3\CMS\Core\Core\Environment;
30
use TYPO3\CMS\Core\Http\ApplicationType;
31
use TYPO3\CMS\Core\Http\RequestFactory;
32
use TYPO3\CMS\Core\Log\LogManager;
33
use TYPO3\CMS\Core\Middleware\VerifyHostHeader;
34
use TYPO3\CMS\Core\Package\Exception as PackageException;
35
use TYPO3\CMS\Core\SingletonInterface;
36
37
/**
38
 * The legendary "t3lib_div" class - Miscellaneous functions for general purpose.
39
 * Most of the functions do not relate specifically to TYPO3
40
 * However a section of functions requires certain TYPO3 features available
41
 * See comments in the source.
42
 * You are encouraged to use this library in your own scripts!
43
 *
44
 * USE:
45
 * All methods in this class are meant to be called statically.
46
 * So use \TYPO3\CMS\Core\Utility\GeneralUtility::[method-name] to refer to the functions, eg. '\TYPO3\CMS\Core\Utility\GeneralUtility::milliseconds()'
47
 */
48
class GeneralUtility
49
{
50
    /* @deprecated since v11, will be removed in v12. */
51
    const ENV_TRUSTED_HOSTS_PATTERN_ALLOW_ALL = '.*';
52
    /* @deprecated since v11, will be removed in v12. */
53
    const ENV_TRUSTED_HOSTS_PATTERN_SERVER_NAME = 'SERVER_NAME';
54
55
    /**
56
     * @var ContainerInterface|null
57
     */
58
    protected static $container;
59
60
    /**
61
     * Singleton instances returned by makeInstance, using the class names as
62
     * array keys
63
     *
64
     * @var array<string, SingletonInterface>
65
     */
66
    protected static $singletonInstances = [];
67
68
    /**
69
     * Instances returned by makeInstance, using the class names as array keys
70
     *
71
     * @var array<string, array<object>>
72
     */
73
    protected static $nonSingletonInstances = [];
74
75
    /**
76
     * Cache for makeInstance with given class name and final class names to reduce number of self::getClassName() calls
77
     *
78
     * @var array<string, class-string> Given class name => final class name
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<string, class-string> at position 4 could not be parsed: Unknown type name 'class-string' at position 4 in array<string, class-string>.
Loading history...
79
     */
80
    protected static $finalClassNameCache = [];
81
82
    /**
83
     * @var array<string, mixed>
84
     */
85
    protected static $indpEnvCache = [];
86
87
    final private function __construct()
88
    {
89
    }
90
91
    /*************************
92
     *
93
     * GET/POST Variables
94
     *
95
     * Background:
96
     * Input GET/POST variables in PHP may have their quotes escaped with "\" or not depending on configuration.
97
     * TYPO3 has always converted quotes to BE escaped if the configuration told that they would not be so.
98
     * But the clean solution is that quotes are never escaped and that is what the functions below offers.
99
     * Eventually TYPO3 should provide this in the global space as well.
100
     * In the transitional phase (or forever..?) we need to encourage EVERY to read and write GET/POST vars through the API functions below.
101
     * This functionality was previously needed to normalize between magic quotes logic, which was removed from PHP 5.4,
102
     * so these methods are still in use, but not tackle the slash problem anymore.
103
     *
104
     *************************/
105
    /**
106
     * Returns the 'GLOBAL' value of incoming data from POST or GET, with priority to POST, which is equivalent to 'GP' order
107
     * In case you already know by which method your data is arriving consider using GeneralUtility::_GET or GeneralUtility::_POST.
108
     *
109
     * @param string $var GET/POST var to return
110
     * @return mixed POST var named $var, if not set, the GET var of the same name and if also not set, NULL.
111
     */
112
    public static function _GP($var)
113
    {
114
        if (empty($var)) {
115
            return;
116
        }
117
118
        $value = $_POST[$var] ?? $_GET[$var] ?? null;
119
120
        // This is there for backwards-compatibility, in order to avoid NULL
121
        if (isset($value) && !is_array($value)) {
122
            $value = (string)$value;
123
        }
124
        return $value;
125
    }
126
127
    /**
128
     * Returns the global arrays $_GET and $_POST merged with $_POST taking precedence.
129
     *
130
     * @param string $parameter Key (variable name) from GET or POST vars
131
     * @return array Returns the GET vars merged recursively onto the POST vars.
132
     */
133
    public static function _GPmerged($parameter)
134
    {
135
        $postParameter = isset($_POST[$parameter]) && is_array($_POST[$parameter]) ? $_POST[$parameter] : [];
136
        $getParameter = isset($_GET[$parameter]) && is_array($_GET[$parameter]) ? $_GET[$parameter] : [];
137
        $mergedParameters = $getParameter;
138
        ArrayUtility::mergeRecursiveWithOverrule($mergedParameters, $postParameter);
139
        return $mergedParameters;
140
    }
141
142
    /**
143
     * Returns the global $_GET array (or value from) normalized to contain un-escaped values.
144
     * This function was previously used to normalize between magic quotes logic, which was removed from PHP 5.5
145
     *
146
     * @param string $var Optional pointer to value in GET array (basically name of GET var)
147
     * @return mixed If $var is set it returns the value of $_GET[$var]. If $var is NULL (default), returns $_GET itself.
148
     * @see _POST()
149
     * @see _GP()
150
     */
151
    public static function _GET($var = null)
152
    {
153
        $value = $var === null
154
            ? $_GET
155
            : (empty($var) ? null : ($_GET[$var] ?? null));
156
        // This is there for backwards-compatibility, in order to avoid NULL
157
        if (isset($value) && !is_array($value)) {
158
            $value = (string)$value;
159
        }
160
        return $value;
161
    }
162
163
    /**
164
     * Returns the global $_POST array (or value from) normalized to contain un-escaped values.
165
     *
166
     * @param string $var Optional pointer to value in POST array (basically name of POST var)
167
     * @return mixed If $var is set it returns the value of $_POST[$var]. If $var is NULL (default), returns $_POST itself.
168
     * @see _GET()
169
     * @see _GP()
170
     */
171
    public static function _POST($var = null)
172
    {
173
        $value = $var === null ? $_POST : (empty($var) || !isset($_POST[$var]) ? null : $_POST[$var]);
174
        // This is there for backwards-compatibility, in order to avoid NULL
175
        if (isset($value) && !is_array($value)) {
176
            $value = (string)$value;
177
        }
178
        return $value;
179
    }
180
181
    /*************************
182
     *
183
     * STRING FUNCTIONS
184
     *
185
     *************************/
186
    /**
187
     * Truncates a string with appended/prepended "..." and takes current character set into consideration.
188
     *
189
     * @param string $string String to truncate
190
     * @param int $chars Must be an integer with an absolute value of at least 4. if negative the string is cropped from the right end.
191
     * @param string $appendString Appendix to the truncated string
192
     * @return string Cropped string
193
     * @todo Add strict types and return types as breaking change in v12.
194
     */
195
    public static function fixed_lgd_cs($string, $chars, $appendString = '...')
196
    {
197
        $string = (string)$string;
198
        if ((int)$chars === 0 || mb_strlen($string, 'utf-8') <= abs($chars)) {
199
            return $string;
200
        }
201
        if ($chars > 0) {
202
            $string = mb_substr($string, 0, $chars, 'utf-8') . $appendString;
203
        } else {
204
            $string = $appendString . mb_substr($string, $chars, mb_strlen($string, 'utf-8'), 'utf-8');
205
        }
206
        return $string;
207
    }
208
209
    /**
210
     * Match IP number with list of numbers with wildcard
211
     * Dispatcher method for switching into specialised IPv4 and IPv6 methods.
212
     *
213
     * @param string $baseIP Is the current remote IP address for instance, typ. REMOTE_ADDR
214
     * @param string $list Is a comma-list of IP-addresses to match with. CIDR-notation should be used. For IPv4 addresses only, the *-wildcard is also allowed instead of number, plus leaving out parts in the IP number is accepted as wildcard (eg. 192.168.*.* equals 192.168). If list is "*" no check is done and the function returns TRUE immediately. An empty list always returns FALSE.
215
     * @return bool TRUE if an IP-mask from $list matches $baseIP
216
     */
217
    public static function cmpIP($baseIP, $list)
218
    {
219
        $list = trim($list);
220
        if ($list === '') {
221
            return false;
222
        }
223
        if ($list === '*') {
224
            return true;
225
        }
226
        if (str_contains($baseIP, ':') && self::validIPv6($baseIP)) {
227
            return self::cmpIPv6($baseIP, $list);
228
        }
229
        return self::cmpIPv4($baseIP, $list);
230
    }
231
232
    /**
233
     * Match IPv4 number with list of numbers with wildcard
234
     *
235
     * @param string $baseIP Is the current remote IP address for instance, typ. REMOTE_ADDR
236
     * @param string $list Is a comma-list of IP-addresses to match with. CIDR-notation, *-wildcard allowed instead of number, plus leaving out parts in the IP number is accepted as wildcard (eg. 192.168.0.0/16 equals 192.168.*.* equals 192.168), could also contain IPv6 addresses
237
     * @return bool TRUE if an IP-mask from $list matches $baseIP
238
     */
239
    public static function cmpIPv4($baseIP, $list)
240
    {
241
        $IPpartsReq = explode('.', $baseIP);
242
        if (count($IPpartsReq) === 4) {
243
            $values = self::trimExplode(',', $list, true);
244
            foreach ($values as $test) {
245
                $testList = explode('/', $test);
246
                if (count($testList) === 2) {
247
                    [$test, $mask] = $testList;
248
                } else {
249
                    $mask = false;
250
                }
251
                if ((int)$mask) {
252
                    $mask = (int)$mask;
253
                    // "192.168.3.0/24"
254
                    $lnet = (int)ip2long($test);
255
                    $lip = (int)ip2long($baseIP);
256
                    $binnet = str_pad(decbin($lnet), 32, '0', STR_PAD_LEFT);
257
                    $firstpart = substr($binnet, 0, $mask);
258
                    $binip = str_pad(decbin($lip), 32, '0', STR_PAD_LEFT);
259
                    $firstip = substr($binip, 0, $mask);
260
                    $yes = $firstpart === $firstip;
261
                } else {
262
                    // "192.168.*.*"
263
                    $IPparts = explode('.', $test);
264
                    $yes = 1;
265
                    foreach ($IPparts as $index => $val) {
266
                        $val = trim($val);
267
                        if ($val !== '*' && $IPpartsReq[$index] !== $val) {
268
                            $yes = 0;
269
                        }
270
                    }
271
                }
272
                if ($yes) {
273
                    return true;
274
                }
275
            }
276
        }
277
        return false;
278
    }
279
280
    /**
281
     * Match IPv6 address with a list of IPv6 prefixes
282
     *
283
     * @param string $baseIP Is the current remote IP address for instance
284
     * @param string $list Is a comma-list of IPv6 prefixes, could also contain IPv4 addresses. IPv6 addresses
285
     *   must be specified in CIDR-notation, not with * wildcard, otherwise self::validIPv6() will fail.
286
     * @return bool TRUE If a baseIP matches any prefix
287
     */
288
    public static function cmpIPv6($baseIP, $list)
289
    {
290
        // Policy default: Deny connection
291
        $success = false;
292
        $baseIP = self::normalizeIPv6($baseIP);
293
        $values = self::trimExplode(',', $list, true);
294
        foreach ($values as $test) {
295
            $testList = explode('/', $test);
296
            if (count($testList) === 2) {
297
                [$test, $mask] = $testList;
298
            } else {
299
                $mask = false;
300
            }
301
            if (self::validIPv6($test)) {
302
                $test = self::normalizeIPv6($test);
303
                $maskInt = (int)$mask ?: 128;
304
                // Special case; /0 is an allowed mask - equals a wildcard
305
                if ($mask === '0') {
306
                    $success = true;
307
                } elseif ($maskInt == 128) {
308
                    $success = $test === $baseIP;
309
                } else {
310
                    $testBin = (string)inet_pton($test);
311
                    $baseIPBin = (string)inet_pton($baseIP);
312
313
                    $success = true;
314
                    // Modulo is 0 if this is a 8-bit-boundary
315
                    $maskIntModulo = $maskInt % 8;
316
                    $numFullCharactersUntilBoundary = (int)($maskInt / 8);
317
                    $substring = (string)substr($baseIPBin, 0, $numFullCharactersUntilBoundary);
318
                    if (strpos($testBin, $substring) !== 0) {
319
                        $success = false;
320
                    } elseif ($maskIntModulo > 0) {
321
                        // If not an 8-bit-boundary, check bits of last character
322
                        $testLastBits = str_pad(decbin(ord(substr($testBin, $numFullCharactersUntilBoundary, 1))), 8, '0', STR_PAD_LEFT);
323
                        $baseIPLastBits = str_pad(decbin(ord(substr($baseIPBin, $numFullCharactersUntilBoundary, 1))), 8, '0', STR_PAD_LEFT);
324
                        if (strncmp($testLastBits, $baseIPLastBits, $maskIntModulo) != 0) {
325
                            $success = false;
326
                        }
327
                    }
328
                }
329
            }
330
            if ($success) {
331
                return true;
332
            }
333
        }
334
        return false;
335
    }
336
337
    /**
338
     * Normalize an IPv6 address to full length
339
     *
340
     * @param string $address Given IPv6 address
341
     * @return string Normalized address
342
     */
343
    public static function normalizeIPv6($address)
344
    {
345
        $normalizedAddress = '';
346
        // According to RFC lowercase-representation is recommended
347
        $address = strtolower($address);
348
        // Normalized representation has 39 characters (0000:0000:0000:0000:0000:0000:0000:0000)
349
        if (strlen($address) === 39) {
350
            // Already in full expanded form
351
            return $address;
352
        }
353
        // Count 2 if if address has hidden zero blocks
354
        $chunks = explode('::', $address);
355
        if (count($chunks) === 2) {
356
            $chunksLeft = explode(':', $chunks[0]);
357
            $chunksRight = explode(':', $chunks[1]);
358
            $left = count($chunksLeft);
359
            $right = count($chunksRight);
360
            // Special case: leading zero-only blocks count to 1, should be 0
361
            if ($left === 1 && strlen($chunksLeft[0]) === 0) {
362
                $left = 0;
363
            }
364
            $hiddenBlocks = 8 - ($left + $right);
365
            $hiddenPart = '';
366
            $h = 0;
367
            while ($h < $hiddenBlocks) {
368
                $hiddenPart .= '0000:';
369
                $h++;
370
            }
371
            if ($left === 0) {
372
                $stageOneAddress = $hiddenPart . $chunks[1];
373
            } else {
374
                $stageOneAddress = $chunks[0] . ':' . $hiddenPart . $chunks[1];
375
            }
376
        } else {
377
            $stageOneAddress = $address;
378
        }
379
        // Normalize the blocks:
380
        $blocks = explode(':', $stageOneAddress);
381
        $divCounter = 0;
382
        foreach ($blocks as $block) {
383
            $tmpBlock = '';
384
            $i = 0;
385
            $hiddenZeros = 4 - strlen($block);
386
            while ($i < $hiddenZeros) {
387
                $tmpBlock .= '0';
388
                $i++;
389
            }
390
            $normalizedAddress .= $tmpBlock . $block;
391
            if ($divCounter < 7) {
392
                $normalizedAddress .= ':';
393
                $divCounter++;
394
            }
395
        }
396
        return $normalizedAddress;
397
    }
398
399
    /**
400
     * Validate a given IP address.
401
     *
402
     * Possible format are IPv4 and IPv6.
403
     *
404
     * @param string $ip IP address to be tested
405
     * @return bool TRUE if $ip is either of IPv4 or IPv6 format.
406
     */
407
    public static function validIP($ip)
408
    {
409
        return filter_var($ip, FILTER_VALIDATE_IP) !== false;
410
    }
411
412
    /**
413
     * Validate a given IP address to the IPv4 address format.
414
     *
415
     * Example for possible format: 10.0.45.99
416
     *
417
     * @param string $ip IP address to be tested
418
     * @return bool TRUE if $ip is of IPv4 format.
419
     */
420
    public static function validIPv4($ip)
421
    {
422
        return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false;
423
    }
424
425
    /**
426
     * Validate a given IP address to the IPv6 address format.
427
     *
428
     * Example for possible format: 43FB::BB3F:A0A0:0 | ::1
429
     *
430
     * @param string $ip IP address to be tested
431
     * @return bool TRUE if $ip is of IPv6 format.
432
     */
433
    public static function validIPv6($ip)
434
    {
435
        return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false;
436
    }
437
438
    /**
439
     * Match fully qualified domain name with list of strings with wildcard
440
     *
441
     * @param string $baseHost A hostname or an IPv4/IPv6-address (will by reverse-resolved; typically REMOTE_ADDR)
442
     * @param string $list A comma-list of domain names to match with. *-wildcard allowed but cannot be part of a string, so it must match the full host name (eg. myhost.*.com => correct, myhost.*domain.com => wrong)
443
     * @return bool TRUE if a domain name mask from $list matches $baseIP
444
     */
445
    public static function cmpFQDN($baseHost, $list)
446
    {
447
        $baseHost = trim($baseHost);
448
        if (empty($baseHost)) {
449
            return false;
450
        }
451
        if (self::validIPv4($baseHost) || self::validIPv6($baseHost)) {
452
            // Resolve hostname
453
            // Note: this is reverse-lookup and can be randomly set as soon as somebody is able to set
454
            // the reverse-DNS for his IP (security when for example used with REMOTE_ADDR)
455
            $baseHostName = (string)gethostbyaddr($baseHost);
456
            if ($baseHostName === $baseHost) {
457
                // Unable to resolve hostname
458
                return false;
459
            }
460
        } else {
461
            $baseHostName = $baseHost;
462
        }
463
        $baseHostNameParts = explode('.', $baseHostName);
464
        $values = self::trimExplode(',', $list, true);
465
        foreach ($values as $test) {
466
            $hostNameParts = explode('.', $test);
467
            // To match hostNameParts can only be shorter (in case of wildcards) or equal
468
            $hostNamePartsCount = count($hostNameParts);
469
            $baseHostNamePartsCount = count($baseHostNameParts);
470
            if ($hostNamePartsCount > $baseHostNamePartsCount) {
471
                continue;
472
            }
473
            $yes = true;
474
            foreach ($hostNameParts as $index => $val) {
475
                $val = trim($val);
476
                if ($val === '*') {
477
                    // Wildcard valid for one or more hostname-parts
478
                    $wildcardStart = $index + 1;
479
                    // Wildcard as last/only part always matches, otherwise perform recursive checks
480
                    if ($wildcardStart < $hostNamePartsCount) {
481
                        $wildcardMatched = false;
482
                        $tempHostName = implode('.', array_slice($hostNameParts, $index + 1));
483
                        while ($wildcardStart < $baseHostNamePartsCount && !$wildcardMatched) {
484
                            $tempBaseHostName = implode('.', array_slice($baseHostNameParts, $wildcardStart));
485
                            $wildcardMatched = self::cmpFQDN($tempBaseHostName, $tempHostName);
486
                            $wildcardStart++;
487
                        }
488
                        if ($wildcardMatched) {
489
                            // Match found by recursive compare
490
                            return true;
491
                        }
492
                        $yes = false;
493
                    }
494
                } elseif ($baseHostNameParts[$index] !== $val) {
495
                    // In case of no match
496
                    $yes = false;
497
                }
498
            }
499
            if ($yes) {
500
                return true;
501
            }
502
        }
503
        return false;
504
    }
505
506
    /**
507
     * Checks if a given URL matches the host that currently handles this HTTP request.
508
     * Scheme, hostname and (optional) port of the given URL are compared.
509
     *
510
     * @param string $url URL to compare with the TYPO3 request host
511
     * @return bool Whether the URL matches the TYPO3 request host
512
     */
513
    public static function isOnCurrentHost($url)
514
    {
515
        return stripos($url . '/', self::getIndpEnv('TYPO3_REQUEST_HOST') . '/') === 0;
516
    }
517
518
    /**
519
     * Check for item in list
520
     * Check if an item exists in a comma-separated list of items.
521
     *
522
     * @param string $list Comma-separated list of items (string)
523
     * @param string $item Item to check for
524
     * @return bool TRUE if $item is in $list
525
     */
526
    public static function inList($list, $item)
527
    {
528
        return str_contains(',' . $list . ',', ',' . $item . ',');
529
    }
530
531
    /**
532
     * Expand a comma-separated list of integers with ranges (eg 1,3-5,7 becomes 1,3,4,5,7).
533
     * Ranges are limited to 1000 values per range.
534
     *
535
     * @param string $list Comma-separated list of integers with ranges (string)
536
     * @return string New comma-separated list of items
537
     */
538
    public static function expandList($list)
539
    {
540
        $items = explode(',', $list);
541
        $list = [];
542
        foreach ($items as $item) {
543
            $range = explode('-', $item);
544
            if (isset($range[1])) {
545
                $runAwayBrake = 1000;
546
                for ($n = $range[0]; $n <= $range[1]; $n++) {
547
                    $list[] = $n;
548
                    $runAwayBrake--;
549
                    if ($runAwayBrake <= 0) {
550
                        break;
551
                    }
552
                }
553
            } else {
554
                $list[] = $item;
555
            }
556
        }
557
        return implode(',', $list);
558
    }
559
560
    /**
561
     * Makes a positive integer hash out of the first 7 chars from the md5 hash of the input
562
     *
563
     * @param string $str String to md5-hash
564
     * @return int Returns 28bit integer-hash
565
     */
566
    public static function md5int($str)
567
    {
568
        return hexdec(substr(md5($str), 0, 7));
569
    }
570
571
    /**
572
     * Returns the first 10 positions of the MD5-hash		(changed from 6 to 10 recently)
573
     *
574
     * @param string $input Input string to be md5-hashed
575
     * @param int $len The string-length of the output
576
     * @return string Substring of the resulting md5-hash, being $len chars long (from beginning)
577
     * @deprecated since v11, will be removed in v12.
578
     */
579
    public static function shortMD5($input, $len = 10)
580
    {
581
        trigger_error(__METHOD__ . ' will be removed in TYPO3 v12, use md5() instead.', E_USER_DEPRECATED);
582
        return substr(md5($input), 0, $len);
583
    }
584
585
    /**
586
     * Returns a proper HMAC on a given input string and secret TYPO3 encryption key.
587
     *
588
     * @param string $input Input string to create HMAC from
589
     * @param string $additionalSecret additionalSecret to prevent hmac being used in a different context
590
     * @return string resulting (hexadecimal) HMAC currently with a length of 40 (HMAC-SHA-1)
591
     */
592
    public static function hmac($input, $additionalSecret = '')
593
    {
594
        $hashAlgorithm = 'sha1';
595
        $hashBlocksize = 64;
596
        $secret = $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'] . $additionalSecret;
597
        if (extension_loaded('hash') && function_exists('hash_hmac') && function_exists('hash_algos') && in_array($hashAlgorithm, hash_algos())) {
598
            $hmac = hash_hmac($hashAlgorithm, $input, $secret);
599
        } else {
600
            // Outer padding
601
            $opad = str_repeat(chr(92), $hashBlocksize);
602
            // Inner padding
603
            $ipad = str_repeat(chr(54), $hashBlocksize);
604
            if (strlen($secret) > $hashBlocksize) {
605
                // Keys longer than block size are shorten
606
                $key = str_pad(pack('H*', $hashAlgorithm($secret)), $hashBlocksize, "\0");
607
            } else {
608
                // Keys shorter than block size are zero-padded
609
                $key = str_pad($secret, $hashBlocksize, "\0");
610
            }
611
            $hmac = $hashAlgorithm(($key ^ $opad) . pack('H*', $hashAlgorithm(($key ^ $ipad) . $input)));
612
        }
613
        return $hmac;
614
    }
615
616
    /**
617
     * Splits a reference to a file in 5 parts
618
     *
619
     * @param string $fileNameWithPath File name with path to be analyzed (must exist if open_basedir is set)
620
     * @return array<string, string> Contains keys [path], [file], [filebody], [fileext], [realFileext]
621
     */
622
    public static function split_fileref($fileNameWithPath)
623
    {
624
        $info = [];
625
        $reg = [];
626
        if (preg_match('/(.*\\/)(.*)$/', $fileNameWithPath, $reg)) {
627
            $info['path'] = $reg[1];
628
            $info['file'] = $reg[2];
629
        } else {
630
            $info['path'] = '';
631
            $info['file'] = $fileNameWithPath;
632
        }
633
        $reg = '';
634
        // If open_basedir is set and the fileName was supplied without a path the is_dir check fails
635
        if (!is_dir($fileNameWithPath) && preg_match('/(.*)\\.([^\\.]*$)/', $info['file'], $reg)) {
0 ignored issues
show
Bug introduced by
$reg of type string is incompatible with the type string[] expected by parameter $matches of preg_match(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

635
        if (!is_dir($fileNameWithPath) && preg_match('/(.*)\\.([^\\.]*$)/', $info['file'], /** @scrutinizer ignore-type */ $reg)) {
Loading history...
636
            $info['filebody'] = $reg[1];
637
            $info['fileext'] = strtolower($reg[2]);
638
            $info['realFileext'] = $reg[2];
639
        } else {
640
            $info['filebody'] = $info['file'];
641
            $info['fileext'] = '';
642
        }
643
        reset($info);
644
        return $info;
645
    }
646
647
    /**
648
     * Returns the directory part of a path without trailing slash
649
     * If there is no dir-part, then an empty string is returned.
650
     * Behaviour:
651
     *
652
     * '/dir1/dir2/script.php' => '/dir1/dir2'
653
     * '/dir1/' => '/dir1'
654
     * 'dir1/script.php' => 'dir1'
655
     * 'd/script.php' => 'd'
656
     * '/script.php' => ''
657
     * '' => ''
658
     *
659
     * @param string $path Directory name / path
660
     * @return string Processed input value. See function description.
661
     */
662
    public static function dirname($path)
663
    {
664
        $p = self::revExplode('/', $path, 2);
665
        return count($p) === 2 ? $p[0] : '';
666
    }
667
668
    /**
669
     * Formats the input integer $sizeInBytes as bytes/kilobytes/megabytes (-/K/M)
670
     *
671
     * @param int $sizeInBytes Number of bytes to format.
672
     * @param string $labels Binary unit name "iec", decimal unit name "si" or labels for bytes, kilo, mega, giga, and so on separated by vertical bar (|) and possibly encapsulated in "". Eg: " | K| M| G". Defaults to "iec".
673
     * @param int $base The unit base if not using a unit name. Defaults to 1024.
674
     * @return string Formatted representation of the byte number, for output.
675
     */
676
    public static function formatSize($sizeInBytes, $labels = '', $base = 0)
677
    {
678
        $defaultFormats = [
679
            'iec' => ['base' => 1024, 'labels' => [' ', ' Ki', ' Mi', ' Gi', ' Ti', ' Pi', ' Ei', ' Zi', ' Yi']],
680
            'si' => ['base' => 1000, 'labels' => [' ', ' k', ' M', ' G', ' T', ' P', ' E', ' Z', ' Y']],
681
        ];
682
        // Set labels and base:
683
        if (empty($labels)) {
684
            $labels = 'iec';
685
        }
686
        if (isset($defaultFormats[$labels])) {
687
            $base = $defaultFormats[$labels]['base'];
688
            $labelArr = $defaultFormats[$labels]['labels'];
689
        } else {
690
            $base = (int)$base;
691
            if ($base !== 1000 && $base !== 1024) {
692
                $base = 1024;
693
            }
694
            $labelArr = explode('|', str_replace('"', '', $labels));
695
        }
696
        // This is set via Site Handling and in the Locales class via setlocale()
697
        $localeInfo = localeconv();
698
        $sizeInBytes = max($sizeInBytes, 0);
699
        $multiplier = floor(($sizeInBytes ? log($sizeInBytes) : 0) / log($base));
700
        $sizeInUnits = $sizeInBytes / $base ** $multiplier;
701
        if ($sizeInUnits > ($base * .9)) {
702
            $multiplier++;
703
        }
704
        $multiplier = min($multiplier, count($labelArr) - 1);
705
        $sizeInUnits = $sizeInBytes / $base ** $multiplier;
706
        return number_format($sizeInUnits, (($multiplier > 0) && ($sizeInUnits < 20)) ? 2 : 0, $localeInfo['decimal_point'], '') . $labelArr[$multiplier];
707
    }
708
709
    /**
710
     * This splits a string by the chars in $operators (typical /+-*) and returns an array with them in
711
     *
712
     * @param string $string Input string, eg "123 + 456 / 789 - 4
713
     * @param string $operators Operators to split by, typically "/+-*
714
     * @return array<int, array<int, string>> Array with operators and operands separated.
715
     * @see \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::calc()
716
     * @see \TYPO3\CMS\Frontend\Imaging\GifBuilder::calcOffset()
717
     */
718
    public static function splitCalc($string, $operators)
719
    {
720
        $res = [];
721
        $sign = '+';
722
        while ($string) {
723
            $valueLen = strcspn($string, $operators);
724
            $value = substr($string, 0, $valueLen);
725
            $res[] = [$sign, trim($value)];
726
            $sign = substr($string, $valueLen, 1);
727
            $string = substr($string, $valueLen + 1);
728
        }
729
        reset($res);
730
        return $res;
731
    }
732
733
    /**
734
     * Checking syntax of input email address
735
     *
736
     * @param string $email Input string to evaluate
737
     * @return bool Returns TRUE if the $email address (input string) is valid
738
     */
739
    public static function validEmail($email)
740
    {
741
        // Early return in case input is not a string
742
        if (!is_string($email)) {
0 ignored issues
show
introduced by
The condition is_string($email) is always true.
Loading history...
743
            return false;
744
        }
745
        if (trim($email) !== $email) {
746
            return false;
747
        }
748
        if (!str_contains($email, '@')) {
749
            return false;
750
        }
751
        $validators = [];
752
        foreach ($GLOBALS['TYPO3_CONF_VARS']['MAIL']['validators'] ?? [RFCValidation::class] as $className) {
753
            $validator = new $className();
754
            if ($validator instanceof EmailValidation) {
755
                $validators[] = $validator;
756
            }
757
        }
758
        return (new EmailValidator())->isValid($email, new MultipleValidationWithAnd($validators, MultipleValidationWithAnd::STOP_ON_ERROR));
759
    }
760
761
    /**
762
     * Returns a given string with underscores as UpperCamelCase.
763
     * Example: Converts blog_example to BlogExample
764
     *
765
     * @param string $string String to be converted to camel case
766
     * @return string UpperCamelCasedWord
767
     */
768
    public static function underscoredToUpperCamelCase($string)
769
    {
770
        return str_replace(' ', '', ucwords(str_replace('_', ' ', strtolower($string))));
771
    }
772
773
    /**
774
     * Returns a given string with underscores as lowerCamelCase.
775
     * Example: Converts minimal_value to minimalValue
776
     *
777
     * @param string $string String to be converted to camel case
778
     * @return string lowerCamelCasedWord
779
     */
780
    public static function underscoredToLowerCamelCase($string)
781
    {
782
        return lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', strtolower($string)))));
783
    }
784
785
    /**
786
     * Returns a given CamelCasedString as a lowercase string with underscores.
787
     * Example: Converts BlogExample to blog_example, and minimalValue to minimal_value
788
     *
789
     * @param string $string String to be converted to lowercase underscore
790
     * @return string lowercase_and_underscored_string
791
     */
792
    public static function camelCaseToLowerCaseUnderscored($string)
793
    {
794
        $value = preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $string) ?? '';
795
        return mb_strtolower($value, 'utf-8');
796
    }
797
798
    /**
799
     * Checks if a given string is a Uniform Resource Locator (URL).
800
     *
801
     * On seriously malformed URLs, parse_url may return FALSE and emit an
802
     * E_WARNING.
803
     *
804
     * filter_var() requires a scheme to be present.
805
     *
806
     * http://www.faqs.org/rfcs/rfc2396.html
807
     * Scheme names consist of a sequence of characters beginning with a
808
     * lower case letter and followed by any combination of lower case letters,
809
     * digits, plus ("+"), period ("."), or hyphen ("-").  For resiliency,
810
     * programs interpreting URI should treat upper case letters as equivalent to
811
     * lower case in scheme names (e.g., allow "HTTP" as well as "http").
812
     * scheme = alpha *( alpha | digit | "+" | "-" | "." )
813
     *
814
     * Convert the domain part to punicode if it does not look like a regular
815
     * domain name. Only the domain part because RFC3986 specifies the the rest of
816
     * the url may not contain special characters:
817
     * https://tools.ietf.org/html/rfc3986#appendix-A
818
     *
819
     * @param string $url The URL to be validated
820
     * @return bool Whether the given URL is valid
821
     */
822
    public static function isValidUrl($url)
823
    {
824
        $parsedUrl = parse_url($url);
825
        if (!$parsedUrl || !isset($parsedUrl['scheme'])) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $parsedUrl of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
826
            return false;
827
        }
828
        // HttpUtility::buildUrl() will always build urls with <scheme>://
829
        // our original $url might only contain <scheme>: (e.g. mail:)
830
        // so we convert that to the double-slashed version to ensure
831
        // our check against the $recomposedUrl is proper
832
        if (!str_starts_with($url, $parsedUrl['scheme'] . '://')) {
833
            $url = str_replace($parsedUrl['scheme'] . ':', $parsedUrl['scheme'] . '://', $url);
834
        }
835
        $recomposedUrl = HttpUtility::buildUrl($parsedUrl);
836
        if ($recomposedUrl !== $url) {
837
            // The parse_url() had to modify characters, so the URL is invalid
838
            return false;
839
        }
840
        if (isset($parsedUrl['host']) && !preg_match('/^[a-z0-9.\\-]*$/i', $parsedUrl['host'])) {
841
            $host = (string)idn_to_ascii($parsedUrl['host']);
842
            if ($host === false) {
0 ignored issues
show
introduced by
The condition $host === false is always false.
Loading history...
843
                return false;
844
            }
845
            $parsedUrl['host'] = $host;
846
        }
847
        return filter_var(HttpUtility::buildUrl($parsedUrl), FILTER_VALIDATE_URL) !== false;
848
    }
849
850
    /*************************
851
     *
852
     * ARRAY FUNCTIONS
853
     *
854
     *************************/
855
856
    /**
857
     * Explodes a $string delimited by $delimiter and casts each item in the array to (int).
858
     * Corresponds to \TYPO3\CMS\Core\Utility\GeneralUtility::trimExplode(), but with conversion to integers for all values.
859
     *
860
     * @param string $delimiter Delimiter string to explode with
861
     * @param string $string The string to explode
862
     * @param bool $removeEmptyValues If set, all empty values (='') will NOT be set in output
863
     * @param int $limit If positive, the result will contain a maximum of limit elements,
864
     * @return int[] Exploded values, all converted to integers
865
     */
866
    public static function intExplode($delimiter, $string, $removeEmptyValues = false, $limit = 0)
867
    {
868
        $result = explode($delimiter, $string) ?: [];
869
        foreach ($result as $key => &$value) {
870
            if ($removeEmptyValues && ($value === '' || trim($value) === '')) {
871
                unset($result[$key]);
872
            } else {
873
                $value = (int)$value;
874
            }
875
        }
876
        unset($value);
877
        if ($limit !== 0) {
878
            if ($limit < 0) {
879
                $result = array_slice($result, 0, $limit);
880
            } elseif (count($result) > $limit) {
881
                $lastElements = array_slice($result, $limit - 1);
882
                $result = array_slice($result, 0, $limit - 1);
883
                $result[] = implode($delimiter, $lastElements);
884
            }
885
        }
886
        return $result;
887
    }
888
889
    /**
890
     * Reverse explode which explodes the string counting from behind.
891
     *
892
     * Note: The delimiter has to given in the reverse order as
893
     *       it is occurring within the string.
894
     *
895
     * GeneralUtility::revExplode('[]', '[my][words][here]', 2)
896
     *   ==> array('[my][words', 'here]')
897
     *
898
     * @param string $delimiter Delimiter string to explode with
899
     * @param string $string The string to explode
900
     * @param int $count Number of array entries
901
     * @return string[] Exploded values
902
     */
903
    public static function revExplode($delimiter, $string, $count = 0)
904
    {
905
        // 2 is the (currently, as of 2014-02) most-used value for $count in the core, therefore we check it first
906
        if ($count === 2) {
907
            $position = strrpos($string, strrev($delimiter));
908
            if ($position !== false) {
909
                return [substr($string, 0, $position), substr($string, $position + strlen($delimiter))];
910
            }
911
            return [$string];
912
        }
913
        if ($count <= 1) {
914
            return [$string];
915
        }
916
        $explodedValues = explode($delimiter, strrev($string), $count) ?: [];
917
        $explodedValues = array_map('strrev', $explodedValues);
918
        return array_reverse($explodedValues);
919
    }
920
921
    /**
922
     * Explodes a string and removes whitespace-only values.
923
     *
924
     * If $removeEmptyValues is set, then all values that contain only whitespace are removed.
925
     *
926
     * Each item will have leading and trailing whitespace removed. However, if the tail items are
927
     * returned as a single array item, their internal whitespace will not be modified.
928
     *
929
     * @param string $delim Delimiter string to explode with
930
     * @param string $string The string to explode
931
     * @param bool $removeEmptyValues If set, all empty values will be removed in output
932
     * @param int $limit If limit is set and positive, the returned array will contain a maximum of limit elements with
933
     *                   the last element containing the rest of string. If the limit parameter is negative, all components
934
     *                   except the last -limit are returned.
935
     * @return string[] Exploded values
936
     */
937
    public static function trimExplode($delim, $string, $removeEmptyValues = false, $limit = 0): array
938
    {
939
        $result = explode($delim, (string)$string) ?: [];
940
        if ($removeEmptyValues) {
941
            // Remove items that are just whitespace, but leave whitespace intact for the rest.
942
            $result = array_values(array_filter($result, static fn ($item) => trim($item) !== ''));
943
        }
944
945
        if ($limit === 0) {
946
            // Return everything.
947
            return array_map('trim', $result);
948
        }
949
950
        if ($limit < 0) {
951
            // Trim and return just the first $limit elements and ignore the rest.
952
            return array_map('trim', array_slice($result, 0, $limit));
953
        }
954
955
        // Fold the last length - $limit elements into a single trailing item, then trim and return the result.
956
        $tail = array_slice($result, $limit - 1);
957
        $result = array_slice($result, 0, $limit - 1);
958
        if ($tail) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $tail of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
959
            $result[] = implode($delim, $tail);
960
        }
961
        return array_map('trim', $result);
962
    }
963
964
    /**
965
     * Implodes a multidim-array into GET-parameters (eg. &param[key][key2]=value2&param[key][key3]=value3)
966
     *
967
     * @param string $name Name prefix for entries. Set to blank if you wish none.
968
     * @param array $theArray The (multidimensional) array to implode
969
     * @param string $str (keep blank)
970
     * @param bool $skipBlank If set, parameters which were blank strings would be removed.
971
     * @param bool $rawurlencodeParamName If set, the param name itself (for example "param[key][key2]") would be rawurlencoded as well.
972
     * @return string Imploded result, fx. &param[key][key2]=value2&param[key][key3]=value3
973
     * @see explodeUrl2Array()
974
     */
975
    public static function implodeArrayForUrl($name, array $theArray, $str = '', $skipBlank = false, $rawurlencodeParamName = false)
976
    {
977
        foreach ($theArray as $Akey => $AVal) {
978
            $thisKeyName = $name ? $name . '[' . $Akey . ']' : $Akey;
979
            if (is_array($AVal)) {
980
                $str = self::implodeArrayForUrl($thisKeyName, $AVal, $str, $skipBlank, $rawurlencodeParamName);
981
            } else {
982
                if (!$skipBlank || (string)$AVal !== '') {
983
                    $str .= '&' . ($rawurlencodeParamName ? rawurlencode($thisKeyName) : $thisKeyName) . '=' . rawurlencode($AVal);
984
                }
985
            }
986
        }
987
        return $str;
988
    }
989
990
    /**
991
     * Explodes a string with GETvars (eg. "&id=1&type=2&ext[mykey]=3") into an array.
992
     *
993
     * Note! If you want to use a multi-dimensional string, consider this plain simple PHP code instead:
994
     *
995
     * $result = [];
996
     * parse_str($queryParametersAsString, $result);
997
     *
998
     * However, if you do magic with a flat structure (e.g. keeping "ext[mykey]" as flat key in a one-dimensional array)
999
     * then this method is for you.
1000
     *
1001
     * @param string $string GETvars string
1002
     * @return array<string, string> Array of values. All values AND keys are rawurldecoded() as they properly should be. But this means that any implosion of the array again must rawurlencode it!
1003
     * @see implodeArrayForUrl()
1004
     */
1005
    public static function explodeUrl2Array($string)
1006
    {
1007
        $output = [];
1008
        $p = explode('&', $string);
1009
        foreach ($p as $v) {
1010
            if ($v !== '') {
1011
                [$pK, $pV] = explode('=', $v, 2);
1012
                $output[rawurldecode($pK)] = rawurldecode($pV);
1013
            }
1014
        }
1015
        return $output;
1016
    }
1017
1018
    /**
1019
     * Returns an array with selected keys from incoming data.
1020
     * (Better read source code if you want to find out...)
1021
     *
1022
     * @param string $varList List of variable/key names
1023
     * @param array $getArray Array from where to get values based on the keys in $varList
1024
     * @param bool $GPvarAlt If set, then \TYPO3\CMS\Core\Utility\GeneralUtility::_GP() is used to fetch the value if not found (isset) in the $getArray
1025
     * @return array Output array with selected variables.
1026
     * @deprecated since v11, will be removed in v12.
1027
     */
1028
    public static function compileSelectedGetVarsFromArray($varList, array $getArray, $GPvarAlt = true)
1029
    {
1030
        trigger_error(
1031
            'GeneralUtility::compileSelectedGetVarsFromArray() is deprecated and will be removed in v12.',
1032
            E_USER_DEPRECATED
1033
        );
1034
1035
        $keys = self::trimExplode(',', $varList, true);
1036
        $outArr = [];
1037
        foreach ($keys as $v) {
1038
            if (isset($getArray[$v])) {
1039
                $outArr[$v] = $getArray[$v];
1040
            } elseif ($GPvarAlt) {
1041
                $outArr[$v] = self::_GP($v);
1042
            }
1043
        }
1044
        return $outArr;
1045
    }
1046
1047
    /**
1048
     * Removes dots "." from end of a key identifier of TypoScript styled array.
1049
     * array('key.' => array('property.' => 'value')) --> array('key' => array('property' => 'value'))
1050
     *
1051
     * @param array $ts TypoScript configuration array
1052
     * @return array TypoScript configuration array without dots at the end of all keys
1053
     */
1054
    public static function removeDotsFromTS(array $ts)
1055
    {
1056
        $out = [];
1057
        foreach ($ts as $key => $value) {
1058
            if (is_array($value)) {
1059
                $key = rtrim($key, '.');
1060
                $out[$key] = self::removeDotsFromTS($value);
1061
            } else {
1062
                $out[$key] = $value;
1063
            }
1064
        }
1065
        return $out;
1066
    }
1067
1068
    /*************************
1069
     *
1070
     * HTML/XML PROCESSING
1071
     *
1072
     *************************/
1073
    /**
1074
     * Returns an array with all attributes of the input HTML tag as key/value pairs. Attributes are only lowercase a-z
1075
     * $tag is either a whole tag (eg '<TAG OPTION ATTRIB=VALUE>') or the parameter list (ex ' OPTION ATTRIB=VALUE>')
1076
     * If an attribute is empty, then the value for the key is empty. You can check if it existed with isset()
1077
     *
1078
     * @param string $tag HTML-tag string (or attributes only)
1079
     * @param bool $decodeEntities Whether to decode HTML entities
1080
     * @return array<string, string> Array with the attribute values.
1081
     */
1082
    public static function get_tag_attributes($tag, bool $decodeEntities = false)
1083
    {
1084
        $components = self::split_tag_attributes($tag);
1085
        // Attribute name is stored here
1086
        $name = '';
1087
        $valuemode = false;
1088
        $attributes = [];
1089
        foreach ($components as $key => $val) {
1090
            // 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
1091
            if ($val !== '=') {
1092
                if ($valuemode) {
1093
                    if ($name) {
1094
                        $attributes[$name] = $decodeEntities ? htmlspecialchars_decode($val) : $val;
1095
                        $name = '';
1096
                    }
1097
                } else {
1098
                    if ($key = strtolower(preg_replace('/[^[:alnum:]_\\:\\-]/', '', $val) ?? '')) {
1099
                        $attributes[$key] = '';
1100
                        $name = $key;
1101
                    }
1102
                }
1103
                $valuemode = false;
1104
            } else {
1105
                $valuemode = true;
1106
            }
1107
        }
1108
        return $attributes;
1109
    }
1110
1111
    /**
1112
     * Returns an array with the 'components' from an attribute list from an HTML tag. The result is normally analyzed by get_tag_attributes
1113
     * Removes tag-name if found
1114
     *
1115
     * @param string $tag HTML-tag string (or attributes only)
1116
     * @return string[] Array with the attribute values.
1117
     */
1118
    public static function split_tag_attributes($tag)
1119
    {
1120
        $tag_tmp = trim(preg_replace('/^<[^[:space:]]*/', '', trim($tag)) ?? '');
1121
        // Removes any > in the end of the string
1122
        $tag_tmp = trim(rtrim($tag_tmp, '>'));
1123
        $value = [];
1124
        // Compared with empty string instead , 030102
1125
        while ($tag_tmp !== '') {
1126
            $firstChar = $tag_tmp[0];
1127
            if ($firstChar === '"' || $firstChar === '\'') {
1128
                $reg = explode($firstChar, $tag_tmp, 3);
1129
                $value[] = $reg[1];
1130
                $tag_tmp = trim($reg[2]);
1131
            } elseif ($firstChar === '=') {
1132
                $value[] = '=';
1133
                // Removes = chars.
1134
                $tag_tmp = trim(substr($tag_tmp, 1));
1135
            } else {
1136
                // There are '' around the value. We look for the next ' ' or '>'
1137
                $reg = preg_split('/[[:space:]=]/', $tag_tmp, 2);
1138
                $value[] = trim($reg[0]);
1139
                $tag_tmp = trim(substr($tag_tmp, strlen($reg[0]), 1) . ($reg[1] ?? ''));
1140
            }
1141
        }
1142
        reset($value);
1143
        return $value;
1144
    }
1145
1146
    /**
1147
     * Implodes attributes in the array $arr for an attribute list in eg. and HTML tag (with quotes)
1148
     *
1149
     * @param array<string, string> $arr Array with attribute key/value pairs, eg. "bgcolor" => "red", "border" => "0"
1150
     * @param bool $xhtmlSafe If set the resulting attribute list will have a) all attributes in lowercase (and duplicates weeded out, first entry taking precedence) and b) all values htmlspecialchar()'ed. It is recommended to use this switch!
1151
     * @param bool $dontOmitBlankAttribs If TRUE, don't check if values are blank. Default is to omit attributes with blank values.
1152
     * @return string Imploded attributes, eg. 'bgcolor="red" border="0"'
1153
     */
1154
    public static function implodeAttributes(array $arr, $xhtmlSafe = false, $dontOmitBlankAttribs = false)
1155
    {
1156
        if ($xhtmlSafe) {
1157
            $newArr = [];
1158
            foreach ($arr as $p => $v) {
1159
                if (!isset($newArr[strtolower($p)])) {
1160
                    $newArr[strtolower($p)] = htmlspecialchars((string)$v);
1161
                }
1162
            }
1163
            $arr = $newArr;
1164
        }
1165
        $list = [];
1166
        foreach ($arr as $p => $v) {
1167
            if ((string)$v !== '' || $dontOmitBlankAttribs) {
1168
                $list[] = $p . '="' . $v . '"';
1169
            }
1170
        }
1171
        return implode(' ', $list);
1172
    }
1173
1174
    /**
1175
     * Wraps JavaScript code XHTML ready with <script>-tags
1176
     * Automatic re-indenting of the JS code is done by using the first line as indent reference.
1177
     * This is nice for indenting JS code with PHP code on the same level.
1178
     *
1179
     * @param string $string JavaScript code
1180
     * @return string The wrapped JS code, ready to put into a XHTML page
1181
     */
1182
    public static function wrapJS($string)
1183
    {
1184
        if (trim($string)) {
1185
            // remove nl from the beginning
1186
            $string = ltrim($string, LF);
1187
            // re-ident to one tab using the first line as reference
1188
            $match = [];
1189
            if (preg_match('/^(\\t+)/', $string, $match)) {
1190
                $string = str_replace($match[1], "\t", $string);
1191
            }
1192
            return '<script>
1193
/*<![CDATA[*/
1194
' . $string . '
1195
/*]]>*/
1196
</script>';
1197
        }
1198
        return '';
1199
    }
1200
1201
    /**
1202
     * Parses XML input into a PHP array with associative keys
1203
     *
1204
     * @param string $string XML data input
1205
     * @param int $depth Number of element levels to resolve the XML into an array. Any further structure will be set as XML.
1206
     * @param array $parserOptions Options that will be passed to PHP's xml_parser_set_option()
1207
     * @return mixed The array with the parsed structure unless the XML parser returns with an error in which case the error message string is returned.
1208
     */
1209
    public static function xml2tree($string, $depth = 999, $parserOptions = [])
1210
    {
1211
        // Disables the functionality to allow external entities to be loaded when parsing the XML, must be kept
1212
        $previousValueOfEntityLoader = null;
1213
        if (PHP_MAJOR_VERSION < 8) {
1214
            $previousValueOfEntityLoader = libxml_disable_entity_loader(true);
1215
        }
1216
        $parser = xml_parser_create();
1217
        $vals = [];
1218
        $index = [];
1219
        xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0);
1220
        xml_parser_set_option($parser, XML_OPTION_SKIP_WHITE, 0);
1221
        foreach ($parserOptions as $option => $value) {
1222
            xml_parser_set_option($parser, $option, $value);
1223
        }
1224
        xml_parse_into_struct($parser, $string, $vals, $index);
1225
        if (PHP_MAJOR_VERSION < 8) {
1226
            libxml_disable_entity_loader($previousValueOfEntityLoader);
0 ignored issues
show
Bug introduced by
It seems like $previousValueOfEntityLoader can also be of type null; however, parameter $disable of libxml_disable_entity_loader() does only seem to accept boolean, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1226
            libxml_disable_entity_loader(/** @scrutinizer ignore-type */ $previousValueOfEntityLoader);
Loading history...
1227
        }
1228
        if (xml_get_error_code($parser)) {
1229
            return 'Line ' . xml_get_current_line_number($parser) . ': ' . xml_error_string(xml_get_error_code($parser));
1230
        }
1231
        xml_parser_free($parser);
1232
        $stack = [[]];
1233
        $stacktop = 0;
1234
        $startPoint = 0;
1235
        $tagi = [];
1236
        foreach ($vals as $key => $val) {
1237
            $type = $val['type'];
1238
            // open tag:
1239
            if ($type === 'open' || $type === 'complete') {
1240
                $stack[$stacktop++] = $tagi;
1241
                if ($depth == $stacktop) {
1242
                    $startPoint = $key;
1243
                }
1244
                $tagi = ['tag' => $val['tag']];
1245
                if (isset($val['attributes'])) {
1246
                    $tagi['attrs'] = $val['attributes'];
1247
                }
1248
                if (isset($val['value'])) {
1249
                    $tagi['values'][] = $val['value'];
1250
                }
1251
            }
1252
            // finish tag:
1253
            if ($type === 'complete' || $type === 'close') {
1254
                $oldtagi = $tagi;
1255
                $tagi = $stack[--$stacktop];
1256
                $oldtag = $oldtagi['tag'];
1257
                unset($oldtagi['tag']);
1258
                if ($depth == $stacktop + 1) {
1259
                    if ($key - $startPoint > 0) {
1260
                        $partArray = array_slice($vals, $startPoint + 1, $key - $startPoint - 1);
1261
                        $oldtagi['XMLvalue'] = self::xmlRecompileFromStructValArray($partArray);
1262
                    } else {
1263
                        $oldtagi['XMLvalue'] = $oldtagi['values'][0];
1264
                    }
1265
                }
1266
                $tagi['ch'][$oldtag][] = $oldtagi;
1267
                unset($oldtagi);
1268
            }
1269
            // cdata
1270
            if ($type === 'cdata') {
1271
                $tagi['values'][] = $val['value'];
1272
            }
1273
        }
1274
        return $tagi['ch'];
1275
    }
1276
1277
    /**
1278
     * Converts a PHP array into an XML string.
1279
     * The XML output is optimized for readability since associative keys are used as tag names.
1280
     * This also means that only alphanumeric characters are allowed in the tag names AND only keys NOT starting with numbers (so watch your usage of keys!). However there are options you can set to avoid this problem.
1281
     * Numeric keys are stored with the default tag name "numIndex" but can be overridden to other formats)
1282
     * The function handles input values from the PHP array in a binary-safe way; All characters below 32 (except 9,10,13) will trigger the content to be converted to a base64-string
1283
     * The PHP variable type of the data IS preserved as long as the types are strings, arrays, integers and booleans. Strings are the default type unless the "type" attribute is set.
1284
     * The output XML has been tested with the PHP XML-parser and parses OK under all tested circumstances with 4.x versions. However, with PHP5 there seems to be the need to add an XML prologue a la <?xml version="1.0" encoding="[charset]" standalone="yes" ?> - otherwise UTF-8 is assumed! Unfortunately, many times the output from this function is used without adding that prologue meaning that non-ASCII characters will break the parsing!! This sucks of course! Effectively it means that the prologue should always be prepended setting the right characterset, alternatively the system should always run as utf-8!
1285
     * However using MSIE to read the XML output didn't always go well: One reason could be that the character encoding is not observed in the PHP data. The other reason may be if the tag-names are invalid in the eyes of MSIE. Also using the namespace feature will make MSIE break parsing. There might be more reasons...
1286
     *
1287
     * @param array $array The input PHP array with any kind of data; text, binary, integers. Not objects though.
1288
     * @param string $NSprefix tag-prefix, eg. a namespace prefix like "T3:"
1289
     * @param int $level Current recursion level. Don't change, stay at zero!
1290
     * @param string $docTag Alternative document tag. Default is "phparray".
1291
     * @param int $spaceInd If greater than zero, then the number of spaces corresponding to this number is used for indenting, if less than zero - no indentation, if zero - a single TAB is used
1292
     * @param array $options Options for the compilation. Key "useNindex" => 0/1 (boolean: whether to use "n0, n1, n2" for num. indexes); Key "useIndexTagForNum" => "[tag for numerical indexes]"; Key "useIndexTagForAssoc" => "[tag for associative indexes"; Key "parentTagMap" => array('parentTag' => 'thisLevelTag')
1293
     * @param array $stackData Stack data. Don't touch.
1294
     * @return string An XML string made from the input content in the array.
1295
     * @see xml2array()
1296
     */
1297
    public static function array2xml(array $array, $NSprefix = '', $level = 0, $docTag = 'phparray', $spaceInd = 0, array $options = [], array $stackData = [])
1298
    {
1299
        // 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
1300
        $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);
1301
        // Set indenting mode:
1302
        $indentChar = $spaceInd ? ' ' : "\t";
1303
        $indentN = $spaceInd > 0 ? $spaceInd : 1;
1304
        $nl = $spaceInd >= 0 ? LF : '';
1305
        // Init output variable:
1306
        $output = '';
1307
        // Traverse the input array
1308
        foreach ($array as $k => $v) {
1309
            $attr = '';
1310
            $tagName = $k;
1311
            // Construct the tag name.
1312
            // Use tag based on grand-parent + parent tag name
1313
            if (isset($stackData['grandParentTagName'], $stackData['parentTagName'], $options['grandParentTagMap'][$stackData['grandParentTagName'] . '/' . $stackData['parentTagName']])) {
1314
                $attr .= ' index="' . htmlspecialchars($tagName) . '"';
1315
                $tagName = (string)$options['grandParentTagMap'][$stackData['grandParentTagName'] . '/' . $stackData['parentTagName']];
1316
            } elseif (isset($stackData['parentTagName'], $options['parentTagMap'][$stackData['parentTagName'] . ':_IS_NUM']) && MathUtility::canBeInterpretedAsInteger($tagName)) {
1317
                // Use tag based on parent tag name + if current tag is numeric
1318
                $attr .= ' index="' . htmlspecialchars($tagName) . '"';
1319
                $tagName = (string)$options['parentTagMap'][$stackData['parentTagName'] . ':_IS_NUM'];
1320
            } elseif (isset($stackData['parentTagName'], $options['parentTagMap'][$stackData['parentTagName'] . ':' . $tagName])) {
1321
                // Use tag based on parent tag name + current tag
1322
                $attr .= ' index="' . htmlspecialchars($tagName) . '"';
1323
                $tagName = (string)$options['parentTagMap'][$stackData['parentTagName'] . ':' . $tagName];
1324
            } elseif (isset($stackData['parentTagName'], $options['parentTagMap'][$stackData['parentTagName']])) {
1325
                // Use tag based on parent tag name:
1326
                $attr .= ' index="' . htmlspecialchars($tagName) . '"';
1327
                $tagName = (string)$options['parentTagMap'][$stackData['parentTagName']];
1328
            } elseif (MathUtility::canBeInterpretedAsInteger($tagName)) {
1329
                // If integer...;
1330
                if ($options['useNindex'] ?? false) {
1331
                    // If numeric key, prefix "n"
1332
                    $tagName = 'n' . $tagName;
1333
                } else {
1334
                    // Use special tag for num. keys:
1335
                    $attr .= ' index="' . $tagName . '"';
1336
                    $tagName = ($options['useIndexTagForNum'] ?? false) ?: 'numIndex';
1337
                }
1338
            } elseif (!empty($options['useIndexTagForAssoc'])) {
1339
                // Use tag for all associative keys:
1340
                $attr .= ' index="' . htmlspecialchars($tagName) . '"';
1341
                $tagName = $options['useIndexTagForAssoc'];
1342
            }
1343
            // The tag name is cleaned up so only alphanumeric chars (plus - and _) are in there and not longer than 100 chars either.
1344
            $tagName = substr(preg_replace('/[^[:alnum:]_-]/', '', $tagName), 0, 100);
1345
            // If the value is an array then we will call this function recursively:
1346
            if (is_array($v)) {
1347
                // Sub elements:
1348
                if (isset($options['alt_options']) && ($options['alt_options'][($stackData['path'] ?? '') . '/' . $tagName] ?? false)) {
1349
                    $subOptions = $options['alt_options'][($stackData['path'] ?? '') . '/' . $tagName];
1350
                    $clearStackPath = (bool)($subOptions['clearStackPath'] ?? false);
1351
                } else {
1352
                    $subOptions = $options;
1353
                    $clearStackPath = false;
1354
                }
1355
                if (empty($v)) {
1356
                    $content = '';
1357
                } else {
1358
                    $content = $nl . self::array2xml($v, $NSprefix, $level + 1, '', $spaceInd, $subOptions, [
1359
                            'parentTagName' => $tagName,
1360
                            'grandParentTagName' => $stackData['parentTagName'] ?? '',
1361
                            'path' => $clearStackPath ? '' : ($stackData['path'] ?? '') . '/' . $tagName,
1362
                        ]) . ($spaceInd >= 0 ? str_pad('', ($level + 1) * $indentN, $indentChar) : '');
1363
                }
1364
                // Do not set "type = array". Makes prettier XML but means that empty arrays are not restored with xml2array
1365
                if (!isset($options['disableTypeAttrib']) || (int)$options['disableTypeAttrib'] != 2) {
1366
                    $attr .= ' type="array"';
1367
                }
1368
            } else {
1369
                // Just a value:
1370
                // Look for binary chars:
1371
                $vLen = strlen((string)$v);
1372
                // Go for base64 encoding if the initial segment NOT matching any binary char has the same length as the whole string!
1373
                if ($vLen && strcspn($v, $binaryChars) != $vLen) {
1374
                    // If the value contained binary chars then we base64-encode it and set an attribute to notify this situation:
1375
                    $content = $nl . chunk_split(base64_encode($v));
1376
                    $attr .= ' base64="1"';
1377
                } else {
1378
                    // Otherwise, just htmlspecialchar the stuff:
1379
                    $content = htmlspecialchars((string)$v);
1380
                    $dType = gettype($v);
1381
                    if ($dType === 'string') {
1382
                        if (isset($options['useCDATA']) && $options['useCDATA'] && $content != $v) {
1383
                            $content = '<![CDATA[' . $v . ']]>';
1384
                        }
1385
                    } elseif (!($options['disableTypeAttrib'] ?? false)) {
1386
                        $attr .= ' type="' . $dType . '"';
1387
                    }
1388
                }
1389
            }
1390
            if ((string)$tagName !== '') {
1391
                // Add the element to the output string:
1392
                $output .= ($spaceInd >= 0 ? str_pad('', ($level + 1) * $indentN, $indentChar) : '')
1393
                    . '<' . $NSprefix . $tagName . $attr . '>' . $content . '</' . $NSprefix . $tagName . '>' . $nl;
1394
            }
1395
        }
1396
        // If we are at the outer-most level, then we finally wrap it all in the document tags and return that as the value:
1397
        if (!$level) {
1398
            $output = '<' . $docTag . '>' . $nl . $output . '</' . $docTag . '>';
1399
        }
1400
        return $output;
1401
    }
1402
1403
    /**
1404
     * Converts an XML string to a PHP array.
1405
     * This is the reverse function of array2xml()
1406
     * This is a wrapper for xml2arrayProcess that adds a two-level cache
1407
     *
1408
     * @param string $string XML content to convert into an array
1409
     * @param string $NSprefix The tag-prefix resolve, eg. a namespace like "T3:"
1410
     * @param bool $reportDocTag If set, the document tag will be set in the key "_DOCUMENT_TAG" of the output array
1411
     * @return mixed If the parsing had errors, a string with the error message is returned. Otherwise an array with the content.
1412
     * @see array2xml()
1413
     * @see xml2arrayProcess()
1414
     */
1415
    public static function xml2array($string, $NSprefix = '', $reportDocTag = false)
1416
    {
1417
        $runtimeCache = static::makeInstance(CacheManager::class)->getCache('runtime');
1418
        $firstLevelCache = $runtimeCache->get('generalUtilityXml2Array') ?: [];
1419
        $identifier = md5($string . $NSprefix . ($reportDocTag ? '1' : '0'));
1420
        // Look up in first level cache
1421
        if (empty($firstLevelCache[$identifier])) {
1422
            $firstLevelCache[$identifier] = self::xml2arrayProcess($string, $NSprefix, $reportDocTag);
1423
            $runtimeCache->set('generalUtilityXml2Array', $firstLevelCache);
1424
        }
1425
        return $firstLevelCache[$identifier];
1426
    }
1427
1428
    /**
1429
     * Converts an XML string to a PHP array.
1430
     * This is the reverse function of array2xml()
1431
     *
1432
     * @param string $string XML content to convert into an array
1433
     * @param string $NSprefix The tag-prefix resolve, eg. a namespace like "T3:"
1434
     * @param bool $reportDocTag If set, the document tag will be set in the key "_DOCUMENT_TAG" of the output array
1435
     * @return mixed If the parsing had errors, a string with the error message is returned. Otherwise an array with the content.
1436
     * @see array2xml()
1437
     */
1438
    public static function xml2arrayProcess($string, $NSprefix = '', $reportDocTag = false)
1439
    {
1440
        $string = trim((string)$string);
1441
        // Disables the functionality to allow external entities to be loaded when parsing the XML, must be kept
1442
        $previousValueOfEntityLoader = null;
1443
        if (PHP_MAJOR_VERSION < 8) {
1444
            $previousValueOfEntityLoader = libxml_disable_entity_loader(true);
1445
        }
1446
        // Create parser:
1447
        $parser = xml_parser_create();
1448
        $vals = [];
1449
        $index = [];
1450
        xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0);
1451
        xml_parser_set_option($parser, XML_OPTION_SKIP_WHITE, 0);
1452
        // Default output charset is UTF-8, only ASCII, ISO-8859-1 and UTF-8 are supported!!!
1453
        $match = [];
1454
        preg_match('/^[[:space:]]*<\\?xml[^>]*encoding[[:space:]]*=[[:space:]]*"([^"]*)"/', substr($string, 0, 200), $match);
1455
        $theCharset = $match[1] ?? 'utf-8';
1456
        // us-ascii / utf-8 / iso-8859-1
1457
        xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, $theCharset);
1458
        // Parse content:
1459
        xml_parse_into_struct($parser, $string, $vals, $index);
1460
        if (PHP_MAJOR_VERSION < 8) {
1461
            libxml_disable_entity_loader($previousValueOfEntityLoader);
0 ignored issues
show
Bug introduced by
It seems like $previousValueOfEntityLoader can also be of type null; however, parameter $disable of libxml_disable_entity_loader() does only seem to accept boolean, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1461
            libxml_disable_entity_loader(/** @scrutinizer ignore-type */ $previousValueOfEntityLoader);
Loading history...
1462
        }
1463
        // If error, return error message:
1464
        if (xml_get_error_code($parser)) {
1465
            return 'Line ' . xml_get_current_line_number($parser) . ': ' . xml_error_string(xml_get_error_code($parser));
1466
        }
1467
        xml_parser_free($parser);
1468
        // Init vars:
1469
        $stack = [[]];
1470
        $stacktop = 0;
1471
        $current = [];
1472
        $tagName = '';
1473
        $documentTag = '';
1474
        // Traverse the parsed XML structure:
1475
        foreach ($vals as $key => $val) {
1476
            // First, process the tag-name (which is used in both cases, whether "complete" or "close")
1477
            $tagName = $val['tag'];
1478
            if (!$documentTag) {
1479
                $documentTag = $tagName;
1480
            }
1481
            // Test for name space:
1482
            $tagName = $NSprefix && strpos($tagName, $NSprefix) === 0 ? substr($tagName, strlen($NSprefix)) : $tagName;
1483
            // Test for numeric tag, encoded on the form "nXXX":
1484
            $testNtag = substr($tagName, 1);
1485
            // Closing tag.
1486
            $tagName = $tagName[0] === 'n' && MathUtility::canBeInterpretedAsInteger($testNtag) ? (int)$testNtag : $tagName;
1487
            // Test for alternative index value:
1488
            if ((string)($val['attributes']['index'] ?? '') !== '') {
1489
                $tagName = $val['attributes']['index'];
1490
            }
1491
            // Setting tag-values, manage stack:
1492
            switch ($val['type']) {
1493
                case 'open':
1494
                    // If open tag it means there is an array stored in sub-elements. Therefore increase the stackpointer and reset the accumulation array:
1495
                    // Setting blank place holder
1496
                    $current[$tagName] = [];
1497
                    $stack[$stacktop++] = $current;
1498
                    $current = [];
1499
                    break;
1500
                case 'close':
1501
                    // If the tag is "close" then it is an array which is closing and we decrease the stack pointer.
1502
                    $oldCurrent = $current;
1503
                    $current = $stack[--$stacktop];
1504
                    // Going to the end of array to get placeholder key, key($current), and fill in array next:
1505
                    end($current);
1506
                    $current[key($current)] = $oldCurrent;
1507
                    unset($oldCurrent);
1508
                    break;
1509
                case 'complete':
1510
                    // If "complete", then it's a value. If the attribute "base64" is set, then decode the value, otherwise just set it.
1511
                    if (!empty($val['attributes']['base64'])) {
1512
                        $current[$tagName] = base64_decode($val['value']);
1513
                    } else {
1514
                        // Had to cast it as a string - otherwise it would be evaluate FALSE if tested with isset()!!
1515
                        $current[$tagName] = (string)($val['value'] ?? '');
1516
                        // Cast type:
1517
                        switch ((string)($val['attributes']['type'] ?? '')) {
1518
                            case 'integer':
1519
                                $current[$tagName] = (int)$current[$tagName];
1520
                                break;
1521
                            case 'double':
1522
                                $current[$tagName] = (double)$current[$tagName];
1523
                                break;
1524
                            case 'boolean':
1525
                                $current[$tagName] = (bool)$current[$tagName];
1526
                                break;
1527
                            case 'NULL':
1528
                                $current[$tagName] = null;
1529
                                break;
1530
                            case 'array':
1531
                                // 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...
1532
                                $current[$tagName] = [];
1533
                                break;
1534
                        }
1535
                    }
1536
                    break;
1537
            }
1538
        }
1539
        if ($reportDocTag) {
1540
            $current[$tagName]['_DOCUMENT_TAG'] = $documentTag;
1541
        }
1542
        // Finally return the content of the document tag.
1543
        return $current[$tagName];
1544
    }
1545
1546
    /**
1547
     * This implodes an array of XML parts (made with xml_parse_into_struct()) into XML again.
1548
     *
1549
     * @param array<int, array<string, mixed>> $vals An array of XML parts, see xml2tree
1550
     * @return string Re-compiled XML data.
1551
     */
1552
    public static function xmlRecompileFromStructValArray(array $vals)
1553
    {
1554
        $XMLcontent = '';
1555
        foreach ($vals as $val) {
1556
            $type = $val['type'];
1557
            // Open tag:
1558
            if ($type === 'open' || $type === 'complete') {
1559
                $XMLcontent .= '<' . $val['tag'];
1560
                if (isset($val['attributes'])) {
1561
                    foreach ($val['attributes'] as $k => $v) {
1562
                        $XMLcontent .= ' ' . $k . '="' . htmlspecialchars($v) . '"';
1563
                    }
1564
                }
1565
                if ($type === 'complete') {
1566
                    if (isset($val['value'])) {
1567
                        $XMLcontent .= '>' . htmlspecialchars($val['value']) . '</' . $val['tag'] . '>';
1568
                    } else {
1569
                        $XMLcontent .= '/>';
1570
                    }
1571
                } else {
1572
                    $XMLcontent .= '>';
1573
                }
1574
                if ($type === 'open' && isset($val['value'])) {
1575
                    $XMLcontent .= htmlspecialchars($val['value']);
1576
                }
1577
            }
1578
            // Finish tag:
1579
            if ($type === 'close') {
1580
                $XMLcontent .= '</' . $val['tag'] . '>';
1581
            }
1582
            // Cdata
1583
            if ($type === 'cdata') {
1584
                $XMLcontent .= htmlspecialchars($val['value']);
1585
            }
1586
        }
1587
        return $XMLcontent;
1588
    }
1589
1590
    /**
1591
     * Minifies JavaScript
1592
     *
1593
     * @param string $script Script to minify
1594
     * @param string $error Error message (if any)
1595
     * @return string Minified script or source string if error happened
1596
     * @deprecated will be removed in TYPO3 v12.0. Use ResourceCompressor->compressJavaScriptSource() instead.
1597
     */
1598
    public static function minifyJavaScript($script, &$error = '')
1599
    {
1600
        trigger_error('Calling GeneralUtility::minifyJavaScript directly will be removed in TYPO3 v12.0. Use ResourceCompressor->compressJavaScriptSource() instead.', E_USER_DEPRECATED);
1601
        $fakeThis = null;
1602
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_div.php']['minifyJavaScript'] ?? [] as $hookMethod) {
1603
            try {
1604
                $parameters = ['script' => $script];
1605
                $script = static::callUserFunction($hookMethod, $parameters, $fakeThis);
1606
            } catch (\Exception $e) {
1607
                $error .= 'Error minifying Javascript: ' . $e->getMessage();
1608
                static::getLogger()->warning('Error minifying Javascript: {file}, hook: {hook}', [
1609
                    'file' => $script,
1610
                    'hook' => $hookMethod,
1611
                    'exception' => $e,
1612
                ]);
1613
            }
1614
        }
1615
        return $script;
1616
    }
1617
1618
    /*************************
1619
     *
1620
     * FILES FUNCTIONS
1621
     *
1622
     *************************/
1623
    /**
1624
     * Reads the file or url $url and returns the content
1625
     * If you are having trouble with proxies when reading URLs you can configure your way out of that with settings within $GLOBALS['TYPO3_CONF_VARS']['HTTP'].
1626
     *
1627
     * @param string $url File/URL to read
1628
     * @return mixed The content from the resource given as input. FALSE if an error has occurred.
1629
     */
1630
    public static function getUrl($url)
1631
    {
1632
        // Looks like it's an external file, use Guzzle by default
1633
        if (preg_match('/^(?:http|ftp)s?|s(?:ftp|cp):/', $url)) {
1634
            $requestFactory = static::makeInstance(RequestFactory::class);
1635
            try {
1636
                $response = $requestFactory->request($url);
1637
            } catch (RequestException $exception) {
1638
                return false;
1639
            }
1640
            $content = $response->getBody()->getContents();
1641
        } else {
1642
            $content = @file_get_contents($url);
1643
        }
1644
        return $content;
1645
    }
1646
1647
    /**
1648
     * Writes $content to the file $file
1649
     *
1650
     * @param string $file Filepath to write to
1651
     * @param string $content Content to write
1652
     * @param bool $changePermissions If TRUE, permissions are forced to be set
1653
     * @return bool TRUE if the file was successfully opened and written to.
1654
     */
1655
    public static function writeFile($file, $content, $changePermissions = false)
1656
    {
1657
        if (!@is_file($file)) {
1658
            $changePermissions = true;
1659
        }
1660
        if ($fd = fopen($file, 'wb')) {
1661
            $res = fwrite($fd, $content);
1662
            fclose($fd);
1663
            if ($res === false) {
1664
                return false;
1665
            }
1666
            // Change the permissions only if the file has just been created
1667
            if ($changePermissions) {
1668
                static::fixPermissions($file);
1669
            }
1670
            return true;
1671
        }
1672
        return false;
1673
    }
1674
1675
    /**
1676
     * Sets the file system mode and group ownership of a file or a folder.
1677
     *
1678
     * @param string $path Path of file or folder, must not be escaped. Path can be absolute or relative
1679
     * @param bool $recursive If set, also fixes permissions of files and folders in the folder (if $path is a folder)
1680
     * @return mixed TRUE on success, FALSE on error, always TRUE on Windows OS
1681
     */
1682
    public static function fixPermissions($path, $recursive = false)
1683
    {
1684
        $targetPermissions = null;
1685
        if (Environment::isWindows()) {
1686
            return true;
1687
        }
1688
        $result = false;
1689
        // Make path absolute
1690
        if (!PathUtility::isAbsolutePath($path)) {
1691
            $path = static::getFileAbsFileName($path);
1692
        }
1693
        if (static::isAllowedAbsPath($path)) {
1694
            if (@is_file($path)) {
1695
                $targetPermissions = (string)($GLOBALS['TYPO3_CONF_VARS']['SYS']['fileCreateMask'] ?? '0644');
1696
            } elseif (@is_dir($path)) {
1697
                $targetPermissions = (string)($GLOBALS['TYPO3_CONF_VARS']['SYS']['folderCreateMask'] ?? '0755');
1698
            }
1699
            if (!empty($targetPermissions)) {
1700
                // make sure it's always 4 digits
1701
                $targetPermissions = str_pad($targetPermissions, 4, '0', STR_PAD_LEFT);
1702
                $targetPermissions = octdec($targetPermissions);
1703
                // "@" is there because file is not necessarily OWNED by the user
1704
                $result = @chmod($path, (int)$targetPermissions);
1705
            }
1706
            // Set createGroup if not empty
1707
            if (
1708
                isset($GLOBALS['TYPO3_CONF_VARS']['SYS']['createGroup'])
1709
                && $GLOBALS['TYPO3_CONF_VARS']['SYS']['createGroup'] !== ''
1710
            ) {
1711
                // "@" is there because file is not necessarily OWNED by the user
1712
                $changeGroupResult = @chgrp($path, $GLOBALS['TYPO3_CONF_VARS']['SYS']['createGroup']);
1713
                $result = $changeGroupResult ? $result : false;
1714
            }
1715
            // Call recursive if recursive flag if set and $path is directory
1716
            if ($recursive && @is_dir($path)) {
1717
                $handle = opendir($path);
1718
                if (is_resource($handle)) {
1719
                    while (($file = readdir($handle)) !== false) {
1720
                        $recursionResult = null;
1721
                        if ($file !== '.' && $file !== '..') {
1722
                            if (@is_file($path . '/' . $file)) {
1723
                                $recursionResult = static::fixPermissions($path . '/' . $file);
1724
                            } elseif (@is_dir($path . '/' . $file)) {
1725
                                $recursionResult = static::fixPermissions($path . '/' . $file, true);
1726
                            }
1727
                            if (isset($recursionResult) && !$recursionResult) {
1728
                                $result = false;
1729
                            }
1730
                        }
1731
                    }
1732
                    closedir($handle);
1733
                }
1734
            }
1735
        }
1736
        return $result;
1737
    }
1738
1739
    /**
1740
     * Writes $content to a filename in the typo3temp/ folder (and possibly one or two subfolders...)
1741
     * Accepts an additional subdirectory in the file path!
1742
     *
1743
     * @param string $filepath Absolute file path to write within the typo3temp/ or Environment::getVarPath() folder - the file path must be prefixed with this path
1744
     * @param string $content Content string to write
1745
     * @return string Returns NULL on success, otherwise an error string telling about the problem.
1746
     */
1747
    public static function writeFileToTypo3tempDir($filepath, $content)
1748
    {
1749
        // Parse filepath into directory and basename:
1750
        $fI = pathinfo($filepath);
1751
        $fI['dirname'] .= '/';
1752
        // Check parts:
1753
        if (!static::validPathStr($filepath) || !$fI['basename'] || strlen($fI['basename']) >= 60) {
1754
            return 'Input filepath "' . $filepath . '" was generally invalid!';
1755
        }
1756
1757
        // Setting main temporary directory name (standard)
1758
        $allowedPathPrefixes = [
1759
            Environment::getPublicPath() . '/typo3temp' => 'Environment::getPublicPath() + "/typo3temp/"',
1760
        ];
1761
        // Also allow project-path + /var/
1762
        if (Environment::getVarPath() !== Environment::getPublicPath() . '/typo3temp/var') {
1763
            $relPath = substr(Environment::getVarPath(), strlen(Environment::getProjectPath()) + 1);
1764
            $allowedPathPrefixes[Environment::getVarPath()] = 'ProjectPath + ' . $relPath;
1765
        }
1766
1767
        $errorMessage = null;
1768
        foreach ($allowedPathPrefixes as $pathPrefix => $prefixLabel) {
1769
            $dirName = $pathPrefix . '/';
1770
            // Invalid file path, let's check for the other path, if it exists
1771
            if (!str_starts_with($fI['dirname'], $dirName)) {
1772
                if ($errorMessage === null) {
1773
                    $errorMessage = '"' . $fI['dirname'] . '" was not within directory ' . $prefixLabel;
1774
                }
1775
                continue;
1776
            }
1777
            // This resets previous error messages from the first path
1778
            $errorMessage = null;
1779
1780
            if (!@is_dir($dirName)) {
1781
                $errorMessage = $prefixLabel . ' was not a directory!';
1782
                // continue and see if the next iteration resets the errorMessage above
1783
                continue;
1784
            }
1785
            // Checking if the "subdir" is found
1786
            $subdir = substr($fI['dirname'], strlen($dirName));
1787
            if ($subdir) {
1788
                if (preg_match('#^(?:[[:alnum:]_]+/)+$#', $subdir)) {
1789
                    $dirName .= $subdir;
1790
                    if (!@is_dir($dirName)) {
1791
                        static::mkdir_deep($pathPrefix . '/' . $subdir);
1792
                    }
1793
                } else {
1794
                    $errorMessage = 'Subdir, "' . $subdir . '", was NOT on the form "[[:alnum:]_]/+"';
1795
                    break;
1796
                }
1797
            }
1798
            // Checking dir-name again (sub-dir might have been created)
1799
            if (@is_dir($dirName)) {
1800
                if ($filepath === $dirName . $fI['basename']) {
1801
                    static::writeFile($filepath, $content);
1802
                    if (!@is_file($filepath)) {
1803
                        $errorMessage = 'The file was not written to the disk. Please, check that you have write permissions to the ' . $prefixLabel . ' directory.';
1804
                    }
1805
                    break;
1806
                }
1807
                $errorMessage = 'Calculated file location didn\'t match input "' . $filepath . '".';
1808
                break;
1809
            }
1810
            $errorMessage = '"' . $dirName . '" is not a directory!';
1811
            break;
1812
        }
1813
        return $errorMessage;
1814
    }
1815
1816
    /**
1817
     * Wrapper function for mkdir.
1818
     * Sets folder permissions according to $GLOBALS['TYPO3_CONF_VARS']['SYS']['folderCreateMask']
1819
     * and group ownership according to $GLOBALS['TYPO3_CONF_VARS']['SYS']['createGroup']
1820
     *
1821
     * @param string $newFolder Absolute path to folder, see PHP mkdir() function. Removes trailing slash internally.
1822
     * @return bool TRUE if operation was successful
1823
     */
1824
    public static function mkdir($newFolder)
1825
    {
1826
        $result = @mkdir($newFolder, (int)octdec($GLOBALS['TYPO3_CONF_VARS']['SYS']['folderCreateMask']));
1827
        if ($result) {
1828
            static::fixPermissions($newFolder);
1829
        }
1830
        return $result;
1831
    }
1832
1833
    /**
1834
     * Creates a directory - including parent directories if necessary and
1835
     * sets permissions on newly created directories.
1836
     *
1837
     * @param string $directory Target directory to create
1838
     * @throws \InvalidArgumentException If $directory is not a string
1839
     * @throws \RuntimeException If directory could not be created
1840
     */
1841
    public static function mkdir_deep($directory)
1842
    {
1843
        if (!is_string($directory)) {
0 ignored issues
show
introduced by
The condition is_string($directory) is always true.
Loading history...
1844
            throw new \InvalidArgumentException('The specified directory is of type "' . gettype($directory) . '" but a string is expected.', 1303662955);
1845
        }
1846
        // Ensure there is only one slash
1847
        $fullPath = rtrim($directory, '/') . '/';
1848
        if ($fullPath !== '/' && !is_dir($fullPath)) {
1849
            $firstCreatedPath = static::createDirectoryPath($fullPath);
1850
            if ($firstCreatedPath !== '') {
1851
                static::fixPermissions($firstCreatedPath, true);
1852
            }
1853
        }
1854
    }
1855
1856
    /**
1857
     * Creates directories for the specified paths if they do not exist. This
1858
     * functions sets proper permission mask but does not set proper user and
1859
     * group.
1860
     *
1861
     * @static
1862
     * @param string $fullDirectoryPath
1863
     * @return string Path to the the first created directory in the hierarchy
1864
     * @see \TYPO3\CMS\Core\Utility\GeneralUtility::mkdir_deep
1865
     * @throws \RuntimeException If directory could not be created
1866
     */
1867
    protected static function createDirectoryPath($fullDirectoryPath)
1868
    {
1869
        $currentPath = $fullDirectoryPath;
1870
        $firstCreatedPath = '';
1871
        $permissionMask = (int)octdec($GLOBALS['TYPO3_CONF_VARS']['SYS']['folderCreateMask'] ?? 0);
1872
        if (!@is_dir($currentPath)) {
1873
            do {
1874
                $firstCreatedPath = $currentPath;
1875
                $separatorPosition = (int)strrpos($currentPath, DIRECTORY_SEPARATOR);
1876
                $currentPath = substr($currentPath, 0, $separatorPosition);
1877
            } while (!is_dir($currentPath) && $separatorPosition > 0);
1878
            $result = @mkdir($fullDirectoryPath, $permissionMask, true);
1879
            // Check existence of directory again to avoid race condition. Directory could have get created by another process between previous is_dir() and mkdir()
1880
            if (!$result && !@is_dir($fullDirectoryPath)) {
1881
                throw new \RuntimeException('Could not create directory "' . $fullDirectoryPath . '"!', 1170251401);
1882
            }
1883
        }
1884
        return $firstCreatedPath;
1885
    }
1886
1887
    /**
1888
     * Wrapper function for rmdir, allowing recursive deletion of folders and files
1889
     *
1890
     * @param string $path Absolute path to folder, see PHP rmdir() function. Removes trailing slash internally.
1891
     * @param bool $removeNonEmpty Allow deletion of non-empty directories
1892
     * @return bool TRUE if operation was successful
1893
     */
1894
    public static function rmdir($path, $removeNonEmpty = false)
1895
    {
1896
        $OK = false;
1897
        // Remove trailing slash
1898
        $path = preg_replace('|/$|', '', $path) ?? '';
1899
        $isWindows = DIRECTORY_SEPARATOR === '\\';
1900
        if (file_exists($path)) {
1901
            $OK = true;
1902
            if (!is_link($path) && is_dir($path)) {
1903
                if ($removeNonEmpty === true && ($handle = @opendir($path))) {
1904
                    $entries = [];
1905
1906
                    while (false !== ($file = readdir($handle))) {
1907
                        if ($file === '.' || $file === '..') {
1908
                            continue;
1909
                        }
1910
1911
                        $entries[] = $path . '/' . $file;
1912
                    }
1913
1914
                    closedir($handle);
1915
1916
                    foreach ($entries as $entry) {
1917
                        if (!static::rmdir($entry, $removeNonEmpty)) {
1918
                            $OK = false;
1919
                        }
1920
                    }
1921
                }
1922
                if ($OK) {
1923
                    $OK = @rmdir($path);
1924
                }
1925
            } elseif (is_link($path) && is_dir($path) && $isWindows) {
1926
                $OK = @rmdir($path);
1927
            } else {
1928
                // If $path is a file, simply remove it
1929
                $OK = @unlink($path);
1930
            }
1931
            clearstatcache();
1932
        } elseif (is_link($path)) {
1933
            $OK = @unlink($path);
1934
            if (!$OK && $isWindows) {
1935
                // Try to delete dead folder links on Windows systems
1936
                $OK = @rmdir($path);
1937
            }
1938
            clearstatcache();
1939
        }
1940
        return $OK;
1941
    }
1942
1943
    /**
1944
     * Returns an array with the names of folders in a specific path
1945
     * Will return 'error' (string) if there were an error with reading directory content.
1946
     * Will return null if provided path is false.
1947
     *
1948
     * @param string $path Path to list directories from
1949
     * @return string[]|string|null Returns an array with the directory entries as values. If no path is provided, the return value will be null.
1950
     */
1951
    public static function get_dirs($path)
1952
    {
1953
        $dirs = null;
1954
        if ($path) {
1955
            if (is_dir($path)) {
1956
                $dir = scandir($path);
1957
                $dirs = [];
1958
                foreach ($dir as $entry) {
1959
                    if (is_dir($path . '/' . $entry) && $entry !== '..' && $entry !== '.') {
1960
                        $dirs[] = $entry;
1961
                    }
1962
                }
1963
            } else {
1964
                $dirs = 'error';
1965
            }
1966
        }
1967
        return $dirs;
1968
    }
1969
1970
    /**
1971
     * Finds all files in a given path and returns them as an array. Each
1972
     * array key is a md5 hash of the full path to the file. This is done because
1973
     * 'some' extensions like the import/export extension depend on this.
1974
     *
1975
     * @param string $path The path to retrieve the files from.
1976
     * @param string $extensionList A comma-separated list of file extensions. Only files of the specified types will be retrieved. When left blank, files of any type will be retrieved.
1977
     * @param bool $prependPath If TRUE, the full path to the file is returned. If FALSE only the file name is returned.
1978
     * @param string $order The sorting order. The default sorting order is alphabetical. Setting $order to 'mtime' will sort the files by modification time.
1979
     * @param string $excludePattern A regular expression pattern of file names to exclude. For example: 'clear.gif' or '(clear.gif|.htaccess)'. The pattern will be wrapped with: '/^' and '$/'.
1980
     * @return array<string, string>|string Array of the files found, or an error message in case the path could not be opened.
1981
     */
1982
    public static function getFilesInDir($path, $extensionList = '', $prependPath = false, $order = '', $excludePattern = '')
1983
    {
1984
        $excludePattern = (string)$excludePattern;
1985
        $path = rtrim($path, '/');
1986
        if (!@is_dir($path)) {
1987
            return [];
1988
        }
1989
1990
        $rawFileList = scandir($path);
1991
        if ($rawFileList === false) {
1992
            return 'error opening path: "' . $path . '"';
1993
        }
1994
1995
        $pathPrefix = $path . '/';
1996
        $allowedFileExtensionArray = self::trimExplode(',', $extensionList);
1997
        $extensionList = ',' . str_replace(' ', '', $extensionList) . ',';
1998
        $files = [];
1999
        foreach ($rawFileList as $entry) {
2000
            $completePathToEntry = $pathPrefix . $entry;
2001
            if (!@is_file($completePathToEntry)) {
2002
                continue;
2003
            }
2004
2005
            foreach ($allowedFileExtensionArray as $allowedFileExtension) {
2006
                if (
2007
                    ($extensionList === ',,' || stripos($extensionList, ',' . substr($entry, strlen($allowedFileExtension) * -1, strlen($allowedFileExtension)) . ',') !== false)
2008
                    && ($excludePattern === '' || !preg_match('/^' . $excludePattern . '$/', $entry))
2009
                ) {
2010
                    if ($order !== 'mtime') {
2011
                        $files[] = $entry;
2012
                    } else {
2013
                        // Store the value in the key so we can do a fast asort later.
2014
                        $files[$entry] = filemtime($completePathToEntry);
2015
                    }
2016
                }
2017
            }
2018
        }
2019
2020
        $valueName = 'value';
2021
        if ($order === 'mtime') {
2022
            asort($files);
2023
            $valueName = 'key';
2024
        }
2025
2026
        $valuePathPrefix = $prependPath ? $pathPrefix : '';
2027
        $foundFiles = [];
2028
        foreach ($files as $key => $value) {
2029
            // Don't change this ever - extensions may depend on the fact that the hash is an md5 of the path! (import/export extension)
2030
            $foundFiles[md5($pathPrefix . ${$valueName})] = $valuePathPrefix . ${$valueName};
2031
        }
2032
2033
        return $foundFiles;
2034
    }
2035
2036
    /**
2037
     * Recursively gather all files and folders of a path.
2038
     *
2039
     * @param string[] $fileArr Empty input array (will have files added to it)
2040
     * @param string $path The path to read recursively from (absolute) (include trailing slash!)
2041
     * @param string $extList Comma list of file extensions: Only files with extensions in this list (if applicable) will be selected.
2042
     * @param bool $regDirs If set, directories are also included in output.
2043
     * @param int $recursivityLevels The number of levels to dig down...
2044
     * @param string $excludePattern regex pattern of files/directories to exclude
2045
     * @return array<string, string> An array with the found files/directories.
2046
     */
2047
    public static function getAllFilesAndFoldersInPath(array $fileArr, $path, $extList = '', $regDirs = false, $recursivityLevels = 99, $excludePattern = '')
2048
    {
2049
        if ($regDirs) {
2050
            $fileArr[md5($path)] = $path;
2051
        }
2052
        $fileArr = array_merge($fileArr, (array)self::getFilesInDir($path, $extList, true, '', $excludePattern));
2053
        $dirs = self::get_dirs($path);
2054
        if ($recursivityLevels > 0 && is_array($dirs)) {
0 ignored issues
show
introduced by
The condition is_array($dirs) is always false.
Loading history...
2055
            foreach ($dirs as $subdirs) {
2056
                if ((string)$subdirs !== '' && ($excludePattern === '' || !preg_match('/^' . $excludePattern . '$/', $subdirs))) {
2057
                    $fileArr = self::getAllFilesAndFoldersInPath($fileArr, $path . $subdirs . '/', $extList, $regDirs, $recursivityLevels - 1, $excludePattern);
2058
                }
2059
            }
2060
        }
2061
        return $fileArr;
2062
    }
2063
2064
    /**
2065
     * Removes the absolute part of all files/folders in fileArr
2066
     *
2067
     * @param string[] $fileArr The file array to remove the prefix from
2068
     * @param string $prefixToRemove The prefix path to remove (if found as first part of string!)
2069
     * @return string[]|string The input $fileArr processed, or a string with an error message, when an error occurred.
2070
     */
2071
    public static function removePrefixPathFromList(array $fileArr, string $prefixToRemove)
2072
    {
2073
        foreach ($fileArr as &$absFileRef) {
2074
            if (str_starts_with($absFileRef, $prefixToRemove)) {
2075
                $absFileRef = substr($absFileRef, strlen($prefixToRemove));
2076
            } else {
2077
                return 'ERROR: One or more of the files was NOT prefixed with the prefix-path!';
2078
            }
2079
        }
2080
        unset($absFileRef);
2081
        return $fileArr;
2082
    }
2083
2084
    /**
2085
     * Fixes a path for windows-backslashes and reduces double-slashes to single slashes
2086
     *
2087
     * @param string $theFile File path to process
2088
     * @return string
2089
     */
2090
    public static function fixWindowsFilePath($theFile)
2091
    {
2092
        return str_replace(['\\', '//'], '/', $theFile);
2093
    }
2094
2095
    /**
2096
     * Resolves "../" sections in the input path string.
2097
     * For example "fileadmin/directory/../other_directory/" will be resolved to "fileadmin/other_directory/"
2098
     *
2099
     * @param string $pathStr File path in which "/../" is resolved
2100
     * @return string
2101
     */
2102
    public static function resolveBackPath($pathStr)
2103
    {
2104
        if (!str_contains($pathStr, '..')) {
2105
            return $pathStr;
2106
        }
2107
        $parts = explode('/', $pathStr);
2108
        $output = [];
2109
        $c = 0;
2110
        foreach ($parts as $part) {
2111
            if ($part === '..') {
2112
                if ($c) {
2113
                    array_pop($output);
2114
                    --$c;
2115
                } else {
2116
                    $output[] = $part;
2117
                }
2118
            } else {
2119
                ++$c;
2120
                $output[] = $part;
2121
            }
2122
        }
2123
        return implode('/', $output);
2124
    }
2125
2126
    /**
2127
     * Prefixes a URL used with 'header-location' with 'http://...' depending on whether it has it already.
2128
     * - If already having a scheme, nothing is prepended
2129
     * - If having REQUEST_URI slash '/', then prefixing 'http://[host]' (relative to host)
2130
     * - Otherwise prefixed with TYPO3_REQUEST_DIR (relative to current dir / TYPO3_REQUEST_DIR)
2131
     *
2132
     * @param string $path URL / path to prepend full URL addressing to.
2133
     * @return string
2134
     */
2135
    public static function locationHeaderUrl($path)
2136
    {
2137
        if (strpos($path, '//') === 0) {
2138
            return $path;
2139
        }
2140
2141
        // relative to HOST
2142
        if (strpos($path, '/') === 0) {
2143
            return self::getIndpEnv('TYPO3_REQUEST_HOST') . $path;
2144
        }
2145
2146
        $urlComponents = parse_url($path);
2147
        if (!($urlComponents['scheme'] ?? false)) {
2148
            // No scheme either
2149
            return self::getIndpEnv('TYPO3_REQUEST_DIR') . $path;
2150
        }
2151
2152
        return $path;
2153
    }
2154
2155
    /**
2156
     * Returns the maximum upload size for a file that is allowed. Measured in KB.
2157
     * This might be handy to find out the real upload limit that is possible for this
2158
     * TYPO3 installation.
2159
     *
2160
     * @return int The maximum size of uploads that are allowed (measured in kilobytes)
2161
     */
2162
    public static function getMaxUploadFileSize()
2163
    {
2164
        $uploadMaxFilesize = (string)ini_get('upload_max_filesize');
2165
        $postMaxSize = (string)ini_get('post_max_size');
2166
        // Check for PHP restrictions of the maximum size of one of the $_FILES
2167
        $phpUploadLimit = self::getBytesFromSizeMeasurement($uploadMaxFilesize);
2168
        // Check for PHP restrictions of the maximum $_POST size
2169
        $phpPostLimit = self::getBytesFromSizeMeasurement($postMaxSize);
2170
        // If the total amount of post data is smaller (!) than the upload_max_filesize directive,
2171
        // then this is the real limit in PHP
2172
        $phpUploadLimit = $phpPostLimit > 0 && $phpPostLimit < $phpUploadLimit ? $phpPostLimit : $phpUploadLimit;
2173
        return floor($phpUploadLimit) / 1024;
2174
    }
2175
2176
    /**
2177
     * Gets the bytes value from a measurement string like "100k".
2178
     *
2179
     * @param string $measurement The measurement (e.g. "100k")
2180
     * @return int The bytes value (e.g. 102400)
2181
     */
2182
    public static function getBytesFromSizeMeasurement($measurement)
2183
    {
2184
        $bytes = (float)$measurement;
2185
        if (stripos($measurement, 'G')) {
2186
            $bytes *= 1024 * 1024 * 1024;
2187
        } elseif (stripos($measurement, 'M')) {
2188
            $bytes *= 1024 * 1024;
2189
        } elseif (stripos($measurement, 'K')) {
2190
            $bytes *= 1024;
2191
        }
2192
        return (int)$bytes;
2193
    }
2194
2195
    /**
2196
     * Function for static version numbers on files, based on the filemtime
2197
     *
2198
     * This will make the filename automatically change when a file is
2199
     * changed, and by that re-cached by the browser. If the file does not
2200
     * exist physically the original file passed to the function is
2201
     * returned without the timestamp.
2202
     *
2203
     * Behaviour is influenced by the setting
2204
     * TYPO3_CONF_VARS['BE' and 'FE'][versionNumberInFilename]
2205
     * = TRUE (BE) / "embed" (FE) : modify filename
2206
     * = FALSE (BE) / "querystring" (FE) : add timestamp as parameter
2207
     *
2208
     * @param string $file Relative path to file including all potential query parameters (not htmlspecialchared yet)
2209
     * @return string Relative path with version filename including the timestamp
2210
     */
2211
    public static function createVersionNumberedFilename($file)
2212
    {
2213
        $lookupFile = explode('?', $file);
2214
        $path = self::resolveBackPath(self::dirname(Environment::getCurrentScript()) . '/' . $lookupFile[0]);
2215
2216
        $doNothing = false;
2217
2218
        if (($GLOBALS['TYPO3_REQUEST'] ?? null) instanceof ServerRequestInterface
2219
            && ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isFrontend()
2220
        ) {
2221
            $mode = strtolower($GLOBALS['TYPO3_CONF_VARS']['FE']['versionNumberInFilename']);
2222
            if ($mode === 'embed') {
2223
                $mode = true;
2224
            } else {
2225
                if ($mode === 'querystring') {
2226
                    $mode = false;
2227
                } else {
2228
                    $doNothing = true;
2229
                }
2230
            }
2231
        } else {
2232
            $mode = $GLOBALS['TYPO3_CONF_VARS']['BE']['versionNumberInFilename'];
2233
        }
2234
        if ($doNothing || !file_exists($path)) {
2235
            // File not found, return filename unaltered
2236
            $fullName = $file;
2237
        } else {
2238
            if (!$mode) {
2239
                // If use of .htaccess rule is not configured,
2240
                // we use the default query-string method
2241
                if (!empty($lookupFile[1])) {
2242
                    $separator = '&';
2243
                } else {
2244
                    $separator = '?';
2245
                }
2246
                $fullName = $file . $separator . filemtime($path);
2247
            } else {
2248
                // Change the filename
2249
                $name = explode('.', $lookupFile[0]);
2250
                $extension = array_pop($name);
2251
                array_push($name, filemtime($path), $extension);
2252
                $fullName = implode('.', $name);
2253
                // Append potential query string
2254
                $fullName .= !empty($lookupFile[1]) ? '?' . $lookupFile[1] : '';
2255
            }
2256
        }
2257
        return $fullName;
2258
    }
2259
2260
    /**
2261
     * Writes string to a temporary file named after the md5-hash of the string
2262
     * Quite useful for extensions adding their custom built JavaScript during runtime.
2263
     *
2264
     * @param string $content JavaScript to write to file.
2265
     * @return string filename to include in the <script> tag
2266
     */
2267
    public static function writeJavaScriptContentToTemporaryFile(string $content)
2268
    {
2269
        $script = 'typo3temp/assets/js/' . md5($content) . '.js';
2270
        if (!@is_file(Environment::getPublicPath() . '/' . $script)) {
2271
            self::writeFileToTypo3tempDir(Environment::getPublicPath() . '/' . $script, $content);
2272
        }
2273
        return $script;
2274
    }
2275
2276
    /**
2277
     * Writes string to a temporary file named after the md5-hash of the string
2278
     * Quite useful for extensions adding their custom built StyleSheet during runtime.
2279
     *
2280
     * @param string $content CSS styles to write to file.
2281
     * @return string filename to include in the <link> tag
2282
     */
2283
    public static function writeStyleSheetContentToTemporaryFile(string $content)
2284
    {
2285
        $script = 'typo3temp/assets/css/' . md5($content) . '.css';
2286
        if (!@is_file(Environment::getPublicPath() . '/' . $script)) {
2287
            self::writeFileToTypo3tempDir(Environment::getPublicPath() . '/' . $script, $content);
2288
        }
2289
        return $script;
2290
    }
2291
2292
    /*************************
2293
     *
2294
     * SYSTEM INFORMATION
2295
     *
2296
     *************************/
2297
2298
    /**
2299
     * Returns the link-url to the current script.
2300
     * In $getParams you can set associative keys corresponding to the GET-vars you wish to add to the URL. If you set them empty, they will remove existing GET-vars from the current URL.
2301
     * REMEMBER to always use htmlspecialchars() for content in href-properties to get ampersands converted to entities (XHTML requirement and XSS precaution)
2302
     *
2303
     * @param array $getParams Array of GET parameters to include
2304
     * @return string
2305
     */
2306
    public static function linkThisScript(array $getParams = [])
2307
    {
2308
        $parts = self::getIndpEnv('SCRIPT_NAME');
2309
        $params = self::_GET();
2310
        foreach ($getParams as $key => $value) {
2311
            if ($value !== '') {
2312
                $params[$key] = $value;
2313
            } else {
2314
                unset($params[$key]);
2315
            }
2316
        }
2317
        $pString = self::implodeArrayForUrl('', $params);
2318
        return $pString ? $parts . '?' . ltrim($pString, '&') : $parts;
2319
    }
2320
2321
    /**
2322
     * This method is only for testing and should never be used outside tests-
2323
     *
2324
     * @param string $envName
2325
     * @param mixed $value
2326
     * @internal
2327
     */
2328
    public static function setIndpEnv($envName, $value)
2329
    {
2330
        self::$indpEnvCache[$envName] = $value;
2331
    }
2332
2333
    /**
2334
     * Abstraction method which returns System Environment Variables regardless of server OS, CGI/MODULE version etc. Basically this is SERVER variables for most of them.
2335
     * This should be used instead of getEnv() and $_SERVER/ENV_VARS to get reliable values for all situations.
2336
     *
2337
     * @param string $getEnvName Name of the "environment variable"/"server variable" you wish to use. Valid values are SCRIPT_NAME, SCRIPT_FILENAME, REQUEST_URI, PATH_INFO, REMOTE_ADDR, REMOTE_HOST, HTTP_REFERER, HTTP_HOST, HTTP_USER_AGENT, HTTP_ACCEPT_LANGUAGE, QUERY_STRING, TYPO3_DOCUMENT_ROOT, TYPO3_HOST_ONLY, TYPO3_HOST_ONLY, TYPO3_REQUEST_HOST, TYPO3_REQUEST_URL, TYPO3_REQUEST_SCRIPT, TYPO3_REQUEST_DIR, TYPO3_SITE_URL, _ARRAY
2338
     * @return string Value based on the input key, independent of server/os environment.
2339
     * @throws \UnexpectedValueException
2340
     */
2341
    public static function getIndpEnv($getEnvName)
2342
    {
2343
        if (array_key_exists($getEnvName, self::$indpEnvCache)) {
2344
            return self::$indpEnvCache[$getEnvName];
2345
        }
2346
2347
        /*
2348
        Conventions:
2349
        output from parse_url():
2350
        URL:	http://username:[email protected]:8080/typo3/32/temp/phpcheck/index.php/arg1/arg2/arg3/?arg1,arg2,arg3&p1=parameter1&p2[key]=value#link1
2351
        [scheme] => 'http'
2352
        [user] => 'username'
2353
        [pass] => 'password'
2354
        [host] => '192.168.1.4'
2355
        [port] => '8080'
2356
        [path] => '/typo3/32/temp/phpcheck/index.php/arg1/arg2/arg3/'
2357
        [query] => 'arg1,arg2,arg3&p1=parameter1&p2[key]=value'
2358
        [fragment] => 'link1'Further definition: [path_script] = '/typo3/32/temp/phpcheck/index.php'
2359
        [path_dir] = '/typo3/32/temp/phpcheck/'
2360
        [path_info] = '/arg1/arg2/arg3/'
2361
        [path] = [path_script/path_dir][path_info]Keys supported:URI______:
2362
        REQUEST_URI		=	[path]?[query]		= /typo3/32/temp/phpcheck/index.php/arg1/arg2/arg3/?arg1,arg2,arg3&p1=parameter1&p2[key]=value
2363
        HTTP_HOST		=	[host][:[port]]		= 192.168.1.4:8080
2364
        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')!
2365
        PATH_INFO		=	[path_info]			= /arg1/arg2/arg3/
2366
        QUERY_STRING	=	[query]				= arg1,arg2,arg3&p1=parameter1&p2[key]=value
2367
        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
2368
        (Notice: NO username/password + NO fragment)CLIENT____:
2369
        REMOTE_ADDR		=	(client IP)
2370
        REMOTE_HOST		=	(client host)
2371
        HTTP_USER_AGENT	=	(client user agent)
2372
        HTTP_ACCEPT_LANGUAGE	= (client accept language)SERVER____:
2373
        SCRIPT_FILENAME	=	Absolute filename of script		(Differs between windows/unix). On windows 'C:\\some\\path\\' will be converted to 'C:/some/path/'Special extras:
2374
        TYPO3_HOST_ONLY =		[host] = 192.168.1.4
2375
        TYPO3_PORT =			[port] = 8080 (blank if 80, taken from host value)
2376
        TYPO3_REQUEST_HOST = 		[scheme]://[host][:[port]]
2377
        TYPO3_REQUEST_URL =		[scheme]://[host][:[port]][path]?[query] (scheme will by default be "http" until we can detect something different)
2378
        TYPO3_REQUEST_SCRIPT =  	[scheme]://[host][:[port]][path_script]
2379
        TYPO3_REQUEST_DIR =		[scheme]://[host][:[port]][path_dir]
2380
        TYPO3_SITE_URL = 		[scheme]://[host][:[port]][path_dir] of the TYPO3 website frontend
2381
        TYPO3_SITE_PATH = 		[path_dir] of the TYPO3 website frontend
2382
        TYPO3_SITE_SCRIPT = 		[script / Speaking URL] of the TYPO3 website
2383
        TYPO3_DOCUMENT_ROOT =		Absolute path of root of documents: TYPO3_DOCUMENT_ROOT.SCRIPT_NAME = SCRIPT_FILENAME (typically)
2384
        TYPO3_SSL = 			Returns TRUE if this session uses SSL/TLS (https)
2385
        TYPO3_PROXY = 			Returns TRUE if this session runs over a well known proxyNotice: [fragment] is apparently NEVER available to the script!Testing suggestions:
2386
        - Output all the values.
2387
        - 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
2388
        - ALSO TRY the script from the ROOT of a site (like 'http://www.mytest.com/' and not 'http://www.mytest.com/test/' !!)
2389
         */
2390
        $retVal = '';
2391
        switch ((string)$getEnvName) {
2392
            case 'SCRIPT_NAME':
2393
                $retVal = Environment::isRunningOnCgiServer()
2394
                    && (($_SERVER['ORIG_PATH_INFO'] ?? false) ?: ($_SERVER['PATH_INFO'] ?? false))
2395
                        ? (($_SERVER['ORIG_PATH_INFO'] ?? '') ?: ($_SERVER['PATH_INFO'] ?? ''))
2396
                        : (($_SERVER['ORIG_SCRIPT_NAME'] ?? '') ?: ($_SERVER['SCRIPT_NAME'] ?? ''));
2397
                // Add a prefix if TYPO3 is behind a proxy: ext-domain.com => int-server.com/prefix
2398
                if (self::cmpIP($_SERVER['REMOTE_ADDR'] ?? '', $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP'] ?? '')) {
2399
                    if (self::getIndpEnv('TYPO3_SSL') && $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefixSSL']) {
2400
                        $retVal = $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefixSSL'] . $retVal;
2401
                    } elseif ($GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefix']) {
2402
                        $retVal = $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefix'] . $retVal;
2403
                    }
2404
                }
2405
                break;
2406
            case 'SCRIPT_FILENAME':
2407
                $retVal = Environment::getCurrentScript();
2408
                break;
2409
            case 'REQUEST_URI':
2410
                // Typical application of REQUEST_URI is return urls, forms submitting to itself etc. Example: returnUrl='.rawurlencode(\TYPO3\CMS\Core\Utility\GeneralUtility::getIndpEnv('REQUEST_URI'))
2411
                if (!empty($GLOBALS['TYPO3_CONF_VARS']['SYS']['requestURIvar'])) {
2412
                    // This is for URL rewriters that store the original URI in a server variable (eg ISAPI_Rewriter for IIS: HTTP_X_REWRITE_URL)
2413
                    [$v, $n] = explode('|', $GLOBALS['TYPO3_CONF_VARS']['SYS']['requestURIvar']);
2414
                    $retVal = $GLOBALS[$v][$n];
2415
                } elseif (empty($_SERVER['REQUEST_URI'])) {
2416
                    // This is for ISS/CGI which does not have the REQUEST_URI available.
2417
                    $retVal = '/' . ltrim(self::getIndpEnv('SCRIPT_NAME'), '/') . (!empty($_SERVER['QUERY_STRING']) ? '?' . $_SERVER['QUERY_STRING'] : '');
2418
                } else {
2419
                    $retVal = '/' . ltrim($_SERVER['REQUEST_URI'], '/');
2420
                }
2421
                // Add a prefix if TYPO3 is behind a proxy: ext-domain.com => int-server.com/prefix
2422
                if (isset($_SERVER['REMOTE_ADDR'], $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP'])
2423
                    && self::cmpIP($_SERVER['REMOTE_ADDR'], $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP'])
2424
                ) {
2425
                    if (self::getIndpEnv('TYPO3_SSL') && $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefixSSL']) {
2426
                        $retVal = $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefixSSL'] . $retVal;
2427
                    } elseif ($GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefix']) {
2428
                        $retVal = $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefix'] . $retVal;
2429
                    }
2430
                }
2431
                break;
2432
            case 'PATH_INFO':
2433
                // $_SERVER['PATH_INFO'] != $_SERVER['SCRIPT_NAME'] is necessary because some servers (Windows/CGI)
2434
                // are seen to set PATH_INFO equal to script_name
2435
                // Further, there must be at least one '/' in the path - else the PATH_INFO value does not make sense.
2436
                // IF 'PATH_INFO' never works for our purpose in TYPO3 with CGI-servers,
2437
                // then 'PHP_SAPI=='cgi'' might be a better check.
2438
                // Right now strcmp($_SERVER['PATH_INFO'], GeneralUtility::getIndpEnv('SCRIPT_NAME')) will always
2439
                // return FALSE for CGI-versions, but that is only as long as SCRIPT_NAME is set equal to PATH_INFO
2440
                // because of PHP_SAPI=='cgi' (see above)
2441
                if (!Environment::isRunningOnCgiServer()) {
2442
                    $retVal = $_SERVER['PATH_INFO'];
2443
                }
2444
                break;
2445
            case 'TYPO3_REV_PROXY':
2446
                $retVal = self::cmpIP($_SERVER['REMOTE_ADDR'] ?? '', $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP']);
2447
                break;
2448
            case 'REMOTE_ADDR':
2449
                $retVal = $_SERVER['REMOTE_ADDR'] ?? '';
2450
                if (self::cmpIP($retVal, $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP'] ?? '')) {
2451
                    $ip = self::trimExplode(',', $_SERVER['HTTP_X_FORWARDED_FOR'] ?? '');
2452
                    // Choose which IP in list to use
2453
                    if (!empty($ip)) {
2454
                        switch ($GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyHeaderMultiValue']) {
2455
                            case 'last':
2456
                                $ip = array_pop($ip);
2457
                                break;
2458
                            case 'first':
2459
                                $ip = array_shift($ip);
2460
                                break;
2461
                            case 'none':
2462
2463
                            default:
2464
                                $ip = '';
2465
                        }
2466
                    }
2467
                    if (self::validIP((string)$ip)) {
2468
                        $retVal = $ip;
2469
                    }
2470
                }
2471
                break;
2472
            case 'HTTP_HOST':
2473
                // if it is not set we're most likely on the cli
2474
                $retVal = $_SERVER['HTTP_HOST'] ?? '';
2475
                if (isset($_SERVER['REMOTE_ADDR']) && static::cmpIP($_SERVER['REMOTE_ADDR'], $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP'])) {
2476
                    $host = self::trimExplode(',', $_SERVER['HTTP_X_FORWARDED_HOST'] ?? '');
2477
                    // Choose which host in list to use
2478
                    if (!empty($host)) {
2479
                        switch ($GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyHeaderMultiValue']) {
2480
                            case 'last':
2481
                                $host = array_pop($host);
2482
                                break;
2483
                            case 'first':
2484
                                $host = array_shift($host);
2485
                                break;
2486
                            case 'none':
2487
2488
                            default:
2489
                                $host = '';
2490
                        }
2491
                    }
2492
                    if ($host) {
2493
                        $retVal = $host;
2494
                    }
2495
                }
2496
                break;
2497
            case 'HTTP_REFERER':
2498
2499
            case 'HTTP_USER_AGENT':
2500
2501
            case 'HTTP_ACCEPT_ENCODING':
2502
2503
            case 'HTTP_ACCEPT_LANGUAGE':
2504
2505
            case 'REMOTE_HOST':
2506
2507
            case 'QUERY_STRING':
2508
                $retVal = $_SERVER[$getEnvName] ?? '';
2509
                break;
2510
            case 'TYPO3_DOCUMENT_ROOT':
2511
                // Get the web root (it is not the root of the TYPO3 installation)
2512
                // The absolute path of the script can be calculated with TYPO3_DOCUMENT_ROOT + SCRIPT_FILENAME
2513
                // 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.
2514
                // Therefore the DOCUMENT_ROOT is now always calculated as the SCRIPT_FILENAME minus the end part shared with SCRIPT_NAME.
2515
                $SFN = self::getIndpEnv('SCRIPT_FILENAME');
2516
                $SN_A = explode('/', strrev(self::getIndpEnv('SCRIPT_NAME')));
2517
                $SFN_A = explode('/', strrev($SFN));
2518
                $acc = [];
2519
                foreach ($SN_A as $kk => $vv) {
2520
                    if ((string)$SFN_A[$kk] === (string)$vv) {
2521
                        $acc[] = $vv;
2522
                    } else {
2523
                        break;
2524
                    }
2525
                }
2526
                $commonEnd = strrev(implode('/', $acc));
2527
                if ((string)$commonEnd !== '') {
2528
                    $retVal = substr($SFN, 0, -(strlen($commonEnd) + 1));
2529
                }
2530
                break;
2531
            case 'TYPO3_HOST_ONLY':
2532
                $httpHost = self::getIndpEnv('HTTP_HOST');
2533
                $httpHostBracketPosition = strpos($httpHost, ']');
2534
                $httpHostParts = explode(':', $httpHost);
2535
                $retVal = $httpHostBracketPosition !== false ? substr($httpHost, 0, $httpHostBracketPosition + 1) : array_shift($httpHostParts);
2536
                break;
2537
            case 'TYPO3_PORT':
2538
                $httpHost = self::getIndpEnv('HTTP_HOST');
2539
                $httpHostOnly = self::getIndpEnv('TYPO3_HOST_ONLY');
2540
                $retVal = strlen($httpHost) > strlen($httpHostOnly) ? substr($httpHost, strlen($httpHostOnly) + 1) : '';
2541
                break;
2542
            case 'TYPO3_REQUEST_HOST':
2543
                $retVal = (self::getIndpEnv('TYPO3_SSL') ? 'https://' : 'http://') . self::getIndpEnv('HTTP_HOST');
2544
                break;
2545
            case 'TYPO3_REQUEST_URL':
2546
                $retVal = self::getIndpEnv('TYPO3_REQUEST_HOST') . self::getIndpEnv('REQUEST_URI');
2547
                break;
2548
            case 'TYPO3_REQUEST_SCRIPT':
2549
                $retVal = self::getIndpEnv('TYPO3_REQUEST_HOST') . self::getIndpEnv('SCRIPT_NAME');
2550
                break;
2551
            case 'TYPO3_REQUEST_DIR':
2552
                $retVal = self::getIndpEnv('TYPO3_REQUEST_HOST') . self::dirname(self::getIndpEnv('SCRIPT_NAME')) . '/';
2553
                break;
2554
            case 'TYPO3_SITE_URL':
2555
                if (Environment::getCurrentScript()) {
2556
                    $lPath = PathUtility::stripPathSitePrefix(PathUtility::dirnameDuringBootstrap(Environment::getCurrentScript())) . '/';
2557
                    $url = self::getIndpEnv('TYPO3_REQUEST_DIR');
2558
                    $siteUrl = substr($url, 0, -strlen($lPath));
2559
                    if (substr($siteUrl, -1) !== '/') {
2560
                        $siteUrl .= '/';
2561
                    }
2562
                    $retVal = $siteUrl;
2563
                }
2564
                break;
2565
            case 'TYPO3_SITE_PATH':
2566
                $retVal = substr(self::getIndpEnv('TYPO3_SITE_URL'), strlen(self::getIndpEnv('TYPO3_REQUEST_HOST')));
2567
                break;
2568
            case 'TYPO3_SITE_SCRIPT':
2569
                $retVal = substr(self::getIndpEnv('TYPO3_REQUEST_URL'), strlen(self::getIndpEnv('TYPO3_SITE_URL')));
2570
                break;
2571
            case 'TYPO3_SSL':
2572
                $proxySSL = trim($GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxySSL'] ?? '');
2573
                if ($proxySSL === '*') {
2574
                    $proxySSL = $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP'];
2575
                }
2576
                if (self::cmpIP($_SERVER['REMOTE_ADDR'] ?? '', $proxySSL)) {
2577
                    $retVal = true;
2578
                } else {
2579
                    $retVal = self::webserverUsesHttps();
2580
                }
2581
                break;
2582
            case '_ARRAY':
2583
                $out = [];
2584
                // Here, list ALL possible keys to this function for debug display.
2585
                $envTestVars = [
2586
                    'HTTP_HOST',
2587
                    'TYPO3_HOST_ONLY',
2588
                    'TYPO3_PORT',
2589
                    'PATH_INFO',
2590
                    'QUERY_STRING',
2591
                    'REQUEST_URI',
2592
                    'HTTP_REFERER',
2593
                    'TYPO3_REQUEST_HOST',
2594
                    'TYPO3_REQUEST_URL',
2595
                    'TYPO3_REQUEST_SCRIPT',
2596
                    'TYPO3_REQUEST_DIR',
2597
                    'TYPO3_SITE_URL',
2598
                    'TYPO3_SITE_SCRIPT',
2599
                    'TYPO3_SSL',
2600
                    'TYPO3_REV_PROXY',
2601
                    'SCRIPT_NAME',
2602
                    'TYPO3_DOCUMENT_ROOT',
2603
                    'SCRIPT_FILENAME',
2604
                    'REMOTE_ADDR',
2605
                    'REMOTE_HOST',
2606
                    'HTTP_USER_AGENT',
2607
                    'HTTP_ACCEPT_LANGUAGE',
2608
                ];
2609
                foreach ($envTestVars as $v) {
2610
                    $out[$v] = self::getIndpEnv($v);
2611
                }
2612
                reset($out);
2613
                $retVal = $out;
2614
                break;
2615
        }
2616
        self::$indpEnvCache[$getEnvName] = $retVal;
2617
        return $retVal;
2618
    }
2619
2620
    /**
2621
     * Checks if the provided host header value matches the trusted hosts pattern.
2622
     *
2623
     * @param string $hostHeaderValue HTTP_HOST header value as sent during the request (may include port)
2624
     * @return bool
2625
     * @deprecated will be removed in TYPO3 v12.0.
2626
     */
2627
    public static function isAllowedHostHeaderValue($hostHeaderValue)
2628
    {
2629
        trigger_error('GeneralUtility::isAllowedHostHeaderValue() will be removed in TYPO3 v12.0. Host header is verified by frontend and backend middlewares.', E_USER_DEPRECATED);
2630
2631
        $verifyHostHeader = new VerifyHostHeader($GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern'] ?? '');
2632
        return $verifyHostHeader->isAllowedHostHeaderValue($hostHeaderValue, $_SERVER);
2633
    }
2634
2635
    /**
2636
     * Determine if the webserver uses HTTPS.
2637
     *
2638
     * HEADS UP: This does not check if the client performed a
2639
     * HTTPS request, as possible proxies are not taken into
2640
     * account. It provides raw information about the current
2641
     * webservers configuration only.
2642
     *
2643
     * @return bool
2644
     */
2645
    protected static function webserverUsesHttps()
2646
    {
2647
        if (!empty($_SERVER['SSL_SESSION_ID'])) {
2648
            return true;
2649
        }
2650
2651
        // https://secure.php.net/manual/en/reserved.variables.server.php
2652
        // "Set to a non-empty value if the script was queried through the HTTPS protocol."
2653
        return !empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) !== 'off';
2654
    }
2655
2656
    /*************************
2657
     *
2658
     * TYPO3 SPECIFIC FUNCTIONS
2659
     *
2660
     *************************/
2661
    /**
2662
     * Returns the absolute filename of a relative reference, resolves the "EXT:" prefix
2663
     * (way of referring to files inside extensions) and checks that the file is inside
2664
     * the TYPO3's base folder and implies a check with
2665
     * \TYPO3\CMS\Core\Utility\GeneralUtility::validPathStr().
2666
     *
2667
     * @param string $filename The input filename/filepath to evaluate
2668
     * @return string Returns the absolute filename of $filename if valid, otherwise blank string.
2669
     */
2670
    public static function getFileAbsFileName($filename)
2671
    {
2672
        if ((string)$filename === '') {
2673
            return '';
2674
        }
2675
        // Extension
2676
        if (PathUtility::isExtensionPath($filename)) {
2677
            try {
2678
                $filename = ExtensionManagementUtility::resolvePackagePath($filename);
2679
            } catch (PackageException $e) {
2680
                $filename = '';
2681
            }
2682
        } elseif (!PathUtility::isAbsolutePath($filename)) {
2683
            // is relative. Prepended with the public web folder
2684
            $filename = Environment::getPublicPath() . '/' . $filename;
2685
        } elseif (!(
2686
            str_starts_with($filename, Environment::getProjectPath())
2687
                  || str_starts_with($filename, Environment::getPublicPath())
2688
        )) {
2689
            // absolute, but set to blank if not allowed
2690
            $filename = '';
2691
        }
2692
        if ((string)$filename !== '' && static::validPathStr($filename)) {
2693
            // checks backpath.
2694
            return $filename;
2695
        }
2696
        return '';
2697
    }
2698
2699
    /**
2700
     * Checks for malicious file paths.
2701
     *
2702
     * Returns TRUE if no '//', '..', '\' or control characters are found in the $theFile.
2703
     * This should make sure that the path is not pointing 'backwards' and further doesn't contain double/back slashes.
2704
     * So it's compatible with the UNIX style path strings valid for TYPO3 internally.
2705
     *
2706
     * @param string $theFile File path to evaluate
2707
     * @return bool TRUE, $theFile is allowed path string, FALSE otherwise
2708
     * @see https://php.net/manual/en/security.filesystem.nullbytes.php
2709
     */
2710
    public static function validPathStr($theFile)
2711
    {
2712
        return !str_contains($theFile, '//') && !str_contains($theFile, '\\')
2713
            && preg_match('#(?:^\\.\\.|/\\.\\./|[[:cntrl:]])#u', $theFile) === 0;
2714
    }
2715
2716
    /**
2717
     * Checks if the $path is absolute or relative (detecting either '/' or 'x:/' as first part of string) and returns TRUE if so.
2718
     *
2719
     * @param string $path File path to evaluate
2720
     * @return bool
2721
     * @deprecated will be removed in TYPO3 v12.0. Use PathUtility::isAbsolutePath() instead.
2722
     */
2723
    public static function isAbsPath($path)
2724
    {
2725
        trigger_error('GeneralUtility::isAbsPath() will be removed in TYPO3 v12.0. Use PathUtility::isAbsolutePath() instead.', E_USER_DEPRECATED);
2726
        if (substr($path, 0, 6) === 'vfs://') {
2727
            return true;
2728
        }
2729
        return
2730
            (isset($path[0]) && $path[0] === '/')
2731
            || (Environment::isWindows() && (strpos($path, ':/') === 1))
2732
            || strpos($path, ':\\') === 1;
2733
    }
2734
2735
    /**
2736
     * Returns TRUE if the path is absolute, without backpath '..' and within TYPO3s project or public folder OR within the lockRootPath
2737
     *
2738
     * @param string $path File path to evaluate
2739
     * @return bool
2740
     */
2741
    public static function isAllowedAbsPath($path)
2742
    {
2743
        if (substr($path, 0, 6) === 'vfs://') {
2744
            return true;
2745
        }
2746
        $lockRootPath = $GLOBALS['TYPO3_CONF_VARS']['BE']['lockRootPath'] ?? '';
2747
        return PathUtility::isAbsolutePath($path) && static::validPathStr($path)
2748
            && (
2749
                str_starts_with($path, Environment::getProjectPath())
2750
                || str_starts_with($path, Environment::getPublicPath())
2751
                || ($lockRootPath && str_starts_with($path, $lockRootPath))
2752
            );
2753
    }
2754
2755
    /**
2756
     * Low level utility function to copy directories and content recursive
2757
     *
2758
     * @param string $source Path to source directory, relative to document root or absolute
2759
     * @param string $destination Path to destination directory, relative to document root or absolute
2760
     */
2761
    public static function copyDirectory($source, $destination)
2762
    {
2763
        if (!str_contains($source, Environment::getProjectPath() . '/')) {
2764
            $source = Environment::getPublicPath() . '/' . $source;
2765
        }
2766
        if (!str_contains($destination, Environment::getProjectPath() . '/')) {
2767
            $destination = Environment::getPublicPath() . '/' . $destination;
2768
        }
2769
        if (static::isAllowedAbsPath($source) && static::isAllowedAbsPath($destination)) {
2770
            static::mkdir_deep($destination);
2771
            $iterator = new \RecursiveIteratorIterator(
2772
                new \RecursiveDirectoryIterator($source, \RecursiveDirectoryIterator::SKIP_DOTS),
2773
                \RecursiveIteratorIterator::SELF_FIRST
2774
            );
2775
            /** @var \SplFileInfo $item */
2776
            foreach ($iterator as $item) {
2777
                $target = $destination . '/' . static::fixWindowsFilePath($iterator->getSubPathName());
0 ignored issues
show
Bug introduced by
The method getSubPathName() does not exist on RecursiveIteratorIterator. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

2777
                $target = $destination . '/' . static::fixWindowsFilePath($iterator->/** @scrutinizer ignore-call */ getSubPathName());

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
2778
                if ($item->isDir()) {
2779
                    static::mkdir($target);
2780
                } else {
2781
                    static::upload_copy_move(static::fixWindowsFilePath($item->getPathname()), $target);
2782
                }
2783
            }
2784
        }
2785
    }
2786
2787
    /**
2788
     * Checks if a given string is a valid frame URL to be loaded in the
2789
     * backend.
2790
     *
2791
     * If the given url is empty or considered to be harmless, it is returned
2792
     * as is, else the event is logged and an empty string is returned.
2793
     *
2794
     * @param string $url potential URL to check
2795
     * @return string $url or empty string
2796
     */
2797
    public static function sanitizeLocalUrl($url = '')
2798
    {
2799
        $sanitizedUrl = '';
2800
        if (!empty($url)) {
2801
            $decodedUrl = rawurldecode($url);
2802
            $parsedUrl = parse_url($decodedUrl);
2803
            $testAbsoluteUrl = self::resolveBackPath($decodedUrl);
2804
            $testRelativeUrl = self::resolveBackPath(self::dirname(self::getIndpEnv('SCRIPT_NAME')) . '/' . $decodedUrl);
2805
            // Pass if URL is on the current host:
2806
            if (self::isValidUrl($decodedUrl)) {
2807
                if (self::isOnCurrentHost($decodedUrl) && strpos($decodedUrl, self::getIndpEnv('TYPO3_SITE_URL')) === 0) {
2808
                    $sanitizedUrl = $url;
2809
                }
2810
            } elseif (PathUtility::isAbsolutePath($decodedUrl) && self::isAllowedAbsPath($decodedUrl)) {
2811
                $sanitizedUrl = $url;
2812
            } elseif (strpos($testAbsoluteUrl, self::getIndpEnv('TYPO3_SITE_PATH')) === 0 && $decodedUrl[0] === '/' &&
2813
                substr($decodedUrl, 0, 2) !== '//'
2814
            ) {
2815
                $sanitizedUrl = $url;
2816
            } elseif (empty($parsedUrl['scheme']) && strpos($testRelativeUrl, self::getIndpEnv('TYPO3_SITE_PATH')) === 0
2817
                && $decodedUrl[0] !== '/' && strpbrk($decodedUrl, '*:|"<>') === false && !str_contains($decodedUrl, '\\\\')
2818
            ) {
2819
                $sanitizedUrl = $url;
2820
            }
2821
        }
2822
        if (!empty($url) && empty($sanitizedUrl)) {
2823
            static::getLogger()->notice('The URL "{url}" is not considered to be local and was denied.', ['url' => $url]);
2824
        }
2825
        return $sanitizedUrl;
2826
    }
2827
2828
    /**
2829
     * Moves $source file to $destination if uploaded, otherwise try to make a copy
2830
     *
2831
     * @param string $source Source file, absolute path
2832
     * @param string $destination Destination file, absolute path
2833
     * @return bool Returns TRUE if the file was moved.
2834
     * @see upload_to_tempfile()
2835
     */
2836
    public static function upload_copy_move($source, $destination)
2837
    {
2838
        if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][\TYPO3\CMS\Core\Utility\GeneralUtility::class]['moveUploadedFile'] ?? null)) {
2839
            $params = ['source' => $source, 'destination' => $destination, 'method' => 'upload_copy_move'];
2840
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][\TYPO3\CMS\Core\Utility\GeneralUtility::class]['moveUploadedFile'] as $hookMethod) {
2841
                $fakeThis = null;
2842
                self::callUserFunction($hookMethod, $params, $fakeThis);
2843
            }
2844
        }
2845
2846
        $result = false;
2847
        if (is_uploaded_file($source)) {
2848
            // Return the value of move_uploaded_file, and if FALSE the temporary $source is still
2849
            // around so the user can use unlink to delete it:
2850
            $result = move_uploaded_file($source, $destination);
2851
        } else {
2852
            @copy($source, $destination);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for copy(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

2852
            /** @scrutinizer ignore-unhandled */ @copy($source, $destination);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
2853
        }
2854
        // Change the permissions of the file
2855
        self::fixPermissions($destination);
2856
        // If here the file is copied and the temporary $source is still around,
2857
        // so when returning FALSE the user can try unlink to delete the $source
2858
        return $result;
2859
    }
2860
2861
    /**
2862
     * Will move an uploaded file (normally in "/tmp/xxxxx") to a temporary filename in Environment::getProjectPath() . "var/" from where TYPO3 can use it.
2863
     * Use this function to move uploaded files to where you can work on them.
2864
     * REMEMBER to use \TYPO3\CMS\Core\Utility\GeneralUtility::unlink_tempfile() afterwards - otherwise temp-files will build up! They are NOT automatically deleted in the temporary folder!
2865
     *
2866
     * @param string $uploadedFileName The temporary uploaded filename, eg. $_FILES['[upload field name here]']['tmp_name']
2867
     * @return string If a new file was successfully created, return its filename, otherwise blank string.
2868
     * @see unlink_tempfile()
2869
     * @see upload_copy_move()
2870
     */
2871
    public static function upload_to_tempfile($uploadedFileName)
2872
    {
2873
        if (is_uploaded_file($uploadedFileName)) {
2874
            $tempFile = self::tempnam('upload_temp_');
2875
            if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][\TYPO3\CMS\Core\Utility\GeneralUtility::class]['moveUploadedFile'] ?? null)) {
2876
                $params = ['source' => $uploadedFileName, 'destination' => $tempFile, 'method' => 'upload_to_tempfile'];
2877
                foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][\TYPO3\CMS\Core\Utility\GeneralUtility::class]['moveUploadedFile'] as $hookMethod) {
2878
                    $fakeThis = null;
2879
                    self::callUserFunction($hookMethod, $params, $fakeThis);
2880
                }
2881
            }
2882
2883
            move_uploaded_file($uploadedFileName, $tempFile);
2884
            return @is_file($tempFile) ? $tempFile : '';
2885
        }
2886
2887
        return '';
2888
    }
2889
2890
    /**
2891
     * Deletes (unlink) a temporary filename in the var/ or typo3temp folder given as input.
2892
     * The function will check that the file exists, is within TYPO3's var/ or typo3temp/ folder and does not contain back-spaces ("../") so it should be pretty safe.
2893
     * Use this after upload_to_tempfile() or tempnam() from this class!
2894
     *
2895
     * @param string $uploadedTempFileName absolute file path - must reside within var/ or typo3temp/ folder.
2896
     * @return bool|null Returns TRUE if the file was unlink()'ed
2897
     * @see upload_to_tempfile()
2898
     * @see tempnam()
2899
     */
2900
    public static function unlink_tempfile($uploadedTempFileName)
2901
    {
2902
        if ($uploadedTempFileName) {
2903
            $uploadedTempFileName = self::fixWindowsFilePath($uploadedTempFileName);
2904
            if (
2905
                self::validPathStr($uploadedTempFileName)
2906
                && (
2907
                    str_starts_with($uploadedTempFileName, Environment::getPublicPath() . '/typo3temp/')
2908
                    || str_starts_with($uploadedTempFileName, Environment::getVarPath() . '/')
2909
                )
2910
                && @is_file($uploadedTempFileName)
2911
            ) {
2912
                if (unlink($uploadedTempFileName)) {
2913
                    return true;
2914
                }
2915
            }
2916
        }
2917
2918
        return null;
2919
    }
2920
2921
    /**
2922
     * Create temporary filename (Create file with unique file name)
2923
     * This function should be used for getting temporary file names - will make your applications safe for open_basedir = on
2924
     * REMEMBER to delete the temporary files after use! This is done by \TYPO3\CMS\Core\Utility\GeneralUtility::unlink_tempfile()
2925
     *
2926
     * @param string $filePrefix Prefix for temporary file
2927
     * @param string $fileSuffix Suffix for temporary file, for example a special file extension
2928
     * @return string result from PHP function tempnam() with the temp/var folder prefixed.
2929
     * @see unlink_tempfile()
2930
     * @see upload_to_tempfile()
2931
     */
2932
    public static function tempnam($filePrefix, $fileSuffix = '')
2933
    {
2934
        $temporaryPath = Environment::getVarPath() . '/transient/';
2935
        if (!is_dir($temporaryPath)) {
2936
            self::mkdir_deep($temporaryPath);
2937
        }
2938
        if ($fileSuffix === '') {
2939
            $path = (string)tempnam($temporaryPath, $filePrefix);
2940
            $tempFileName = $temporaryPath . PathUtility::basename($path);
2941
        } else {
2942
            do {
2943
                $tempFileName = $temporaryPath . $filePrefix . random_int(1, PHP_INT_MAX) . $fileSuffix;
2944
            } while (file_exists($tempFileName));
2945
            touch($tempFileName);
2946
            clearstatcache(false, $tempFileName);
2947
        }
2948
        return $tempFileName;
2949
    }
2950
2951
    /**
2952
     * Responds on input localization setting value whether the page it comes from should be hidden if no translation exists or not.
2953
     *
2954
     * @param int $l18n_cfg_fieldValue Value from "l18n_cfg" field of a page record
2955
     * @return bool TRUE if the page should be hidden
2956
     * @deprecated since TYPO3 v11, will be removed in TYPO3 v12. Use PageTranslationVisibility BitSet instead.
2957
     */
2958
    public static function hideIfNotTranslated($l18n_cfg_fieldValue)
2959
    {
2960
        trigger_error('GeneralUtility::hideIfNotTranslated() will be removed in TYPO3 v12, use the PageTranslationVisibility BitSet API instead.', E_USER_DEPRECATED);
2961
        return $GLOBALS['TYPO3_CONF_VARS']['FE']['hidePagesIfNotTranslatedByDefault'] xor ($l18n_cfg_fieldValue & 2);
2962
    }
2963
2964
    /**
2965
     * Returns true if the "l18n_cfg" field value is not set to hide
2966
     * pages in the default language
2967
     *
2968
     * @param int $localizationConfiguration
2969
     * @return bool
2970
     * @deprecated since TYPO3 v11, will be removed in TYPO3 v12. Use PageTranslationVisibility BitSet instead.
2971
     */
2972
    public static function hideIfDefaultLanguage($localizationConfiguration)
2973
    {
2974
        trigger_error('GeneralUtility::hideIfDefaultLanguage() will be removed in TYPO3 v12, use the PageTranslationVisibility BitSet API instead.', E_USER_DEPRECATED);
2975
        return (bool)($localizationConfiguration & 1);
2976
    }
2977
2978
    /**
2979
     * Calls a user-defined function/method in class
2980
     * Such a function/method should look like this: "function proc(&$params, &$ref) {...}"
2981
     *
2982
     * @param string $funcName Function/Method reference or Closure.
2983
     * @param mixed $params Parameters to be pass along (typically an array) (REFERENCE!)
2984
     * @param object|null $ref Reference to be passed along (typically "$this" - being a reference to the calling object)
2985
     * @return mixed Content from method/function call
2986
     * @throws \InvalidArgumentException
2987
     */
2988
    public static function callUserFunction($funcName, &$params, ?object $ref = null)
2989
    {
2990
        // Check if we're using a closure and invoke it directly.
2991
        if (is_object($funcName) && is_a($funcName, \Closure::class)) {
0 ignored issues
show
introduced by
The condition is_object($funcName) is always false.
Loading history...
2992
            return call_user_func_array($funcName, [&$params, &$ref]);
2993
        }
2994
        $funcName = trim($funcName);
2995
        $parts = explode('->', $funcName);
2996
        // Call function or method
2997
        if (count($parts) === 2) {
2998
            // It's a class/method
2999
            // Check if class/method exists:
3000
            if (class_exists($parts[0])) {
3001
                // Create object
3002
                $classObj = self::makeInstance($parts[0]);
3003
                $methodName = (string)$parts[1];
3004
                $callable = [$classObj, $methodName];
3005
                if (is_callable($callable)) {
3006
                    // Call method:
3007
                    $content = call_user_func_array($callable, [&$params, &$ref]);
3008
                } else {
3009
                    throw new \InvalidArgumentException('No method name \'' . $parts[1] . '\' in class ' . $parts[0], 1294585865);
3010
                }
3011
            } else {
3012
                throw new \InvalidArgumentException('No class named ' . $parts[0], 1294585866);
3013
            }
3014
        } elseif (function_exists($funcName) && is_callable($funcName)) {
3015
            // It's a function
3016
            $content = call_user_func_array($funcName, [&$params, &$ref]);
3017
        } else {
3018
            throw new \InvalidArgumentException('No function named: ' . $funcName, 1294585867);
3019
        }
3020
        return $content;
3021
    }
3022
3023
    /**
3024
     * @param ContainerInterface $container
3025
     * @internal
3026
     */
3027
    public static function setContainer(ContainerInterface $container): void
3028
    {
3029
        self::$container = $container;
3030
    }
3031
3032
    /**
3033
     * @return ContainerInterface
3034
     * @internal
3035
     */
3036
    public static function getContainer(): ContainerInterface
3037
    {
3038
        if (self::$container === null) {
3039
            throw new \LogicException('PSR-11 Container is not available', 1549404144);
3040
        }
3041
        return self::$container;
3042
    }
3043
3044
    /**
3045
     * Creates an instance of a class taking into account the class-extensions
3046
     * API of TYPO3. USE THIS method instead of the PHP "new" keyword.
3047
     * Eg. "$obj = new myclass;" should be "$obj = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance("myclass")" instead!
3048
     *
3049
     * You can also pass arguments for a constructor:
3050
     * \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\myClass::class, $arg1, $arg2, ..., $argN)
3051
     *
3052
     * @template T
3053
     * @param string|class-string<T> $className name of the class to instantiate, must not be empty and not start with a backslash
0 ignored issues
show
Documentation Bug introduced by
The doc comment string|class-string<T> at position 2 could not be parsed: Unknown type name 'class-string' at position 2 in string|class-string<T>.
Loading history...
3054
     * @param array<int, mixed> $constructorArguments Arguments for the constructor
3055
     * @return object&T the created instance
3056
     * @throws \InvalidArgumentException if $className is empty or starts with a backslash
3057
     */
3058
    public static function makeInstance($className, ...$constructorArguments)
3059
    {
3060
        if (!is_string($className) || empty($className)) {
3061
            throw new \InvalidArgumentException('$className must be a non empty string.', 1288965219);
3062
        }
3063
        // Never instantiate with a beginning backslash, otherwise things like singletons won't work.
3064
        if ($className[0] === '\\') {
3065
            throw new \InvalidArgumentException(
3066
                '$className "' . $className . '" must not start with a backslash.',
3067
                1420281366
3068
            );
3069
        }
3070
        if (isset(static::$finalClassNameCache[$className])) {
3071
            $finalClassName = static::$finalClassNameCache[$className];
3072
        } else {
3073
            $finalClassName = self::getClassName($className);
3074
            static::$finalClassNameCache[$className] = $finalClassName;
3075
        }
3076
        // Return singleton instance if it is already registered
3077
        if (isset(self::$singletonInstances[$finalClassName])) {
3078
            return self::$singletonInstances[$finalClassName];
3079
        }
3080
        // Return instance if it has been injected by addInstance()
3081
        if (
3082
            isset(self::$nonSingletonInstances[$finalClassName])
3083
            && !empty(self::$nonSingletonInstances[$finalClassName])
3084
        ) {
3085
            return array_shift(self::$nonSingletonInstances[$finalClassName]);
3086
        }
3087
3088
        // Read service and prototypes from the DI container, this is required to
3089
        // support classes that require dependency injection.
3090
        // We operate on the original class name on purpose, as class overrides
3091
        // are resolved inside the container
3092
        if (self::$container !== null && $constructorArguments === [] && self::$container->has($className)) {
3093
            return self::$container->get($className);
3094
        }
3095
3096
        // Create new instance and call constructor with parameters
3097
        $instance = new $finalClassName(...$constructorArguments);
3098
        // Register new singleton instance, but only if it is not a known PSR-11 container service
3099
        if ($instance instanceof SingletonInterface && !(self::$container !== null && self::$container->has($className))) {
3100
            self::$singletonInstances[$finalClassName] = $instance;
3101
        }
3102
        if ($instance instanceof LoggerAwareInterface) {
3103
            $instance->setLogger(static::makeInstance(LogManager::class)->getLogger($className));
3104
        }
3105
        return $instance;
3106
    }
3107
3108
    /**
3109
     * Creates a class taking implementation settings and class aliases into account.
3110
     *
3111
     * Intended to be used to create objects by the dependency injection
3112
     * container.
3113
     *
3114
     * @template T
3115
     * @param string|class-string<T> $className name of the class to instantiate
0 ignored issues
show
Documentation Bug introduced by
The doc comment string|class-string<T> at position 2 could not be parsed: Unknown type name 'class-string' at position 2 in string|class-string<T>.
Loading history...
3116
     * @param array<int, mixed> $constructorArguments Arguments for the constructor
3117
     * @return object&T the created instance
3118
     * @internal
3119
     */
3120
    public static function makeInstanceForDi(string $className, ...$constructorArguments): object
3121
    {
3122
        $finalClassName = static::$finalClassNameCache[$className] ?? static::$finalClassNameCache[$className] = self::getClassName($className);
3123
3124
        // Return singleton instance if it is already registered (currently required for unit and functional tests)
3125
        if (isset(self::$singletonInstances[$finalClassName])) {
3126
            return self::$singletonInstances[$finalClassName];
3127
        }
3128
        // Create new instance and call constructor with parameters
3129
        return new $finalClassName(...$constructorArguments);
3130
    }
3131
3132
    /**
3133
     * Returns the class name for a new instance, taking into account
3134
     * registered implementations for this class
3135
     *
3136
     * @param string $className Base class name to evaluate
3137
     * @return class-string Final class name to instantiate with "new [classname]
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
3138
     */
3139
    protected static function getClassName($className)
3140
    {
3141
        if (class_exists($className)) {
3142
            while (static::classHasImplementation($className)) {
3143
                $className = static::getImplementationForClass($className);
3144
            }
3145
        }
3146
        return ClassLoadingInformation::getClassNameForAlias($className);
3147
    }
3148
3149
    /**
3150
     * Returns the configured implementation of the class
3151
     *
3152
     * @param string $className
3153
     * @return string
3154
     */
3155
    protected static function getImplementationForClass($className)
3156
    {
3157
        return $GLOBALS['TYPO3_CONF_VARS']['SYS']['Objects'][$className]['className'];
3158
    }
3159
3160
    /**
3161
     * Checks if a class has a configured implementation
3162
     *
3163
     * @param string $className
3164
     * @return bool
3165
     */
3166
    protected static function classHasImplementation($className)
3167
    {
3168
        return !empty($GLOBALS['TYPO3_CONF_VARS']['SYS']['Objects'][$className]['className']);
3169
    }
3170
3171
    /**
3172
     * Sets the instance of a singleton class to be returned by makeInstance.
3173
     *
3174
     * If this function is called multiple times for the same $className,
3175
     * makeInstance will return the last set instance.
3176
     *
3177
     * Warning:
3178
     * This is NOT a public API method and must not be used in own extensions!
3179
     * This methods exists mostly for unit tests to inject a mock of a singleton class.
3180
     * If you use this, make sure to always combine this with getSingletonInstances()
3181
     * and resetSingletonInstances() in setUp() and tearDown() of the test class.
3182
     *
3183
     * @see makeInstance
3184
     * @param string $className
3185
     * @param \TYPO3\CMS\Core\SingletonInterface $instance
3186
     * @internal
3187
     */
3188
    public static function setSingletonInstance($className, SingletonInterface $instance)
3189
    {
3190
        self::checkInstanceClassName($className, $instance);
3191
        // Check for XCLASS registration (same is done in makeInstance() in order to store the singleton of the final class name)
3192
        $finalClassName = self::getClassName($className);
3193
        self::$singletonInstances[$finalClassName] = $instance;
3194
    }
3195
3196
    /**
3197
     * Removes the instance of a singleton class to be returned by makeInstance.
3198
     *
3199
     * Warning:
3200
     * This is NOT a public API method and must not be used in own extensions!
3201
     * This methods exists mostly for unit tests to inject a mock of a singleton class.
3202
     * If you use this, make sure to always combine this with getSingletonInstances()
3203
     * and resetSingletonInstances() in setUp() and tearDown() of the test class.
3204
     *
3205
     * @see makeInstance
3206
     * @throws \InvalidArgumentException
3207
     * @param string $className
3208
     * @param \TYPO3\CMS\Core\SingletonInterface $instance
3209
     * @internal
3210
     */
3211
    public static function removeSingletonInstance($className, SingletonInterface $instance)
3212
    {
3213
        self::checkInstanceClassName($className, $instance);
3214
        if (!isset(self::$singletonInstances[$className])) {
3215
            throw new \InvalidArgumentException('No Instance registered for ' . $className . '.', 1394099179);
3216
        }
3217
        if ($instance !== self::$singletonInstances[$className]) {
3218
            throw new \InvalidArgumentException('The instance you are trying to remove has not been registered before.', 1394099256);
3219
        }
3220
        unset(self::$singletonInstances[$className]);
3221
    }
3222
3223
    /**
3224
     * Set a group of singleton instances. Similar to setSingletonInstance(),
3225
     * but multiple instances can be set.
3226
     *
3227
     * Warning:
3228
     * This is NOT a public API method and must not be used in own extensions!
3229
     * This method is usually only used in tests to restore the list of singletons in
3230
     * tearDown(), that was backed up with getSingletonInstances() in setUp() and
3231
     * manipulated in tests with setSingletonInstance()
3232
     *
3233
     * @internal
3234
     * @param array<string, SingletonInterface> $newSingletonInstances
3235
     */
3236
    public static function resetSingletonInstances(array $newSingletonInstances)
3237
    {
3238
        static::$singletonInstances = [];
3239
        foreach ($newSingletonInstances as $className => $instance) {
3240
            static::setSingletonInstance($className, $instance);
3241
        }
3242
    }
3243
3244
    /**
3245
     * Get all currently registered singletons
3246
     *
3247
     * Warning:
3248
     * This is NOT a public API method and must not be used in own extensions!
3249
     * This method is usually only used in tests in setUp() to fetch the list of
3250
     * currently registered singletons, if this list is manipulated with
3251
     * setSingletonInstance() in tests.
3252
     *
3253
     * @internal
3254
     * @return array<string, SingletonInterface>
3255
     */
3256
    public static function getSingletonInstances()
3257
    {
3258
        return static::$singletonInstances;
3259
    }
3260
3261
    /**
3262
     * Get all currently registered non singleton instances
3263
     *
3264
     * Warning:
3265
     * This is NOT a public API method and must not be used in own extensions!
3266
     * This method is only used in UnitTestCase base test tearDown() to verify tests
3267
     * have no left over instances that were previously added using addInstance().
3268
     *
3269
     * @internal
3270
     * @return array<string, array<object>>
3271
     */
3272
    public static function getInstances()
3273
    {
3274
        return static::$nonSingletonInstances;
3275
    }
3276
3277
    /**
3278
     * Sets the instance of a non-singleton class to be returned by makeInstance.
3279
     *
3280
     * If this function is called multiple times for the same $className,
3281
     * makeInstance will return the instances in the order in which they have
3282
     * been added (FIFO).
3283
     *
3284
     * Warning: This is a helper method for unit tests. Do not call this directly in production code!
3285
     *
3286
     * @see makeInstance
3287
     * @throws \InvalidArgumentException if class extends \TYPO3\CMS\Core\SingletonInterface
3288
     * @param string $className
3289
     * @param object $instance
3290
     */
3291
    public static function addInstance($className, $instance)
3292
    {
3293
        self::checkInstanceClassName($className, $instance);
3294
        if ($instance instanceof SingletonInterface) {
3295
            throw new \InvalidArgumentException('$instance must not be an instance of TYPO3\\CMS\\Core\\SingletonInterface. For setting singletons, please use setSingletonInstance.', 1288969325);
3296
        }
3297
        if (!isset(self::$nonSingletonInstances[$className])) {
3298
            self::$nonSingletonInstances[$className] = [];
3299
        }
3300
        self::$nonSingletonInstances[$className][] = $instance;
3301
    }
3302
3303
    /**
3304
     * Checks that $className is non-empty and that $instance is an instance of
3305
     * $className.
3306
     *
3307
     * @throws \InvalidArgumentException if $className is empty or if $instance is no instance of $className
3308
     * @param string $className a class name
3309
     * @param object $instance an object
3310
     */
3311
    protected static function checkInstanceClassName($className, $instance)
3312
    {
3313
        if ($className === '') {
3314
            throw new \InvalidArgumentException('$className must not be empty.', 1288967479);
3315
        }
3316
        if (!$instance instanceof $className) {
3317
            throw new \InvalidArgumentException('$instance must be an instance of ' . $className . ', but actually is an instance of ' . get_class($instance) . '.', 1288967686);
3318
        }
3319
    }
3320
3321
    /**
3322
     * Purge all instances returned by makeInstance.
3323
     *
3324
     * This function is most useful when called from tearDown in a test case
3325
     * to drop any instances that have been created by the tests.
3326
     *
3327
     * Warning: This is a helper method for unit tests. Do not call this directly in production code!
3328
     *
3329
     * @see makeInstance
3330
     */
3331
    public static function purgeInstances()
3332
    {
3333
        self::$container = null;
3334
        self::$singletonInstances = [];
3335
        self::$nonSingletonInstances = [];
3336
    }
3337
3338
    /**
3339
     * Flush internal runtime caches
3340
     *
3341
     * Used in unit tests only.
3342
     *
3343
     * @internal
3344
     */
3345
    public static function flushInternalRuntimeCaches()
3346
    {
3347
        self::$indpEnvCache = [];
3348
    }
3349
3350
    /**
3351
     * Find the best service and check if it works.
3352
     * Returns object of the service class.
3353
     *
3354
     * @param string $serviceType Type of service (service key).
3355
     * @param string $serviceSubType Sub type like file extensions or similar. Defined by the service.
3356
     * @param array $excludeServiceKeys List of service keys which should be excluded in the search for a service
3357
     * @throws \RuntimeException
3358
     * @return object|string[] The service object or an array with error infos.
3359
     */
3360
    public static function makeInstanceService($serviceType, $serviceSubType = '', array $excludeServiceKeys = [])
3361
    {
3362
        $error = false;
3363
        $requestInfo = [
3364
            'requestedServiceType' => $serviceType,
3365
            'requestedServiceSubType' => $serviceSubType,
3366
            'requestedExcludeServiceKeys' => $excludeServiceKeys,
3367
        ];
3368
        while ($info = ExtensionManagementUtility::findService($serviceType, $serviceSubType, $excludeServiceKeys)) {
3369
            // provide information about requested service to service object
3370
            $info = array_merge($info, $requestInfo);
3371
            $obj = self::makeInstance($info['className']);
3372
            if (is_object($obj)) {
3373
                if (!is_callable([$obj, 'init'])) {
3374
                    self::getLogger()->error('Requested service {class} has no init() method.', [
3375
                        'class' => $info['className'],
3376
                        'service' => $info,
3377
                    ]);
3378
                    throw new \RuntimeException('Broken service: ' . $info['className'], 1568119209);
3379
                }
3380
                $obj->info = $info;
3381
                // service available?
3382
                if ($obj->init()) {
3383
                    return $obj;
3384
                }
3385
                $error = $obj->getLastErrorArray();
3386
                unset($obj);
3387
            }
3388
3389
            // deactivate the service
3390
            ExtensionManagementUtility::deactivateService($info['serviceType'], $info['serviceKey']);
3391
        }
3392
        return $error;
3393
    }
3394
3395
    /**
3396
     * Quotes a string for usage as JS parameter.
3397
     *
3398
     * @param string $value the string to encode, may be empty
3399
     * @return string the encoded value already quoted (with single quotes),
3400
     */
3401
    public static function quoteJSvalue($value)
3402
    {
3403
        $json = (string)json_encode(
3404
            (string)$value,
3405
            JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_TAG
3406
        );
3407
3408
        return strtr(
3409
            $json,
3410
            [
3411
                '"' => '\'',
3412
                '\\\\' => '\\u005C',
3413
                ' ' => '\\u0020',
3414
                '!' => '\\u0021',
3415
                '\\t' => '\\u0009',
3416
                '\\n' => '\\u000A',
3417
                '\\r' => '\\u000D',
3418
            ]
3419
        );
3420
    }
3421
3422
    /**
3423
     * Serializes data to JSON, to be used in HTML attribute, e.g.
3424
     *
3425
     * `<div data-value="[[JSON]]">...</div>`
3426
     * (`[[JSON]]` represents return value of this function)
3427
     *
3428
     * @param mixed $value
3429
     * @param bool $useHtmlEntities
3430
     * @return string
3431
     */
3432
    public static function jsonEncodeForHtmlAttribute($value, bool $useHtmlEntities = true): string
3433
    {
3434
        $json = (string)json_encode($value, JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_TAG);
3435
        return $useHtmlEntities ? htmlspecialchars($json) : $json;
3436
    }
3437
3438
    /**
3439
     * Serializes data to JSON, to be used in JavaScript instructions, e.g.
3440
     *
3441
     * `<script>const value = JSON.parse('[[JSON]]');</script>`
3442
     * (`[[JSON]]` represents return value of this function)
3443
     *
3444
     * @param mixed $value
3445
     * @return string
3446
     */
3447
    public static function jsonEncodeForJavaScript($value): string
3448
    {
3449
        $json = (string)json_encode($value, JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_TAG);
3450
        return strtr(
3451
            $json,
3452
            [
3453
                // comments below refer to JSON-encoded data
3454
                '\\\\' => '\\\\u005C', // `"\\Vendor\\Package"` -> `"\\u005CVendor\\u005CPackage"`
3455
                '\\t' => '\\u0009', // `"\t"` -> `"\u0009"`
3456
                '\\n' => '\\u000A', // `"\n"` -> `"\u000A"`
3457
                '\\r' => '\\u000D', // `"\r"` -> `"\u000D"`
3458
            ]
3459
        );
3460
    }
3461
3462
    /**
3463
     * @return LoggerInterface
3464
     */
3465
    protected static function getLogger()
3466
    {
3467
        return static::makeInstance(LogManager::class)->getLogger(__CLASS__);
3468
    }
3469
}
3470