Completed
Push — master ( c2ed6d...2771a9 )
by
unknown
19:03
created

GeneralUtility::getLogger()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
namespace TYPO3\CMS\Core\Utility;
3
4
/*
5
 * This file is part of the TYPO3 CMS project.
6
 *
7
 * It is free software; you can redistribute it and/or modify it under
8
 * the terms of the GNU General Public License, either version 2
9
 * of the License, or any later version.
10
 *
11
 * For the full copyright and license information, please read the
12
 * LICENSE.txt file that was distributed with this source code.
13
 *
14
 * The TYPO3 project - inspiring people to share!
15
 */
16
17
use Egulias\EmailValidator\EmailValidator;
18
use Egulias\EmailValidator\Validation\RFCValidation;
19
use GuzzleHttp\Exception\RequestException;
20
use Psr\Container\ContainerInterface;
21
use Psr\Log\LoggerAwareInterface;
22
use Psr\Log\LoggerInterface;
23
use TYPO3\CMS\Core\Cache\CacheManager;
24
use TYPO3\CMS\Core\Core\ApplicationContext;
25
use TYPO3\CMS\Core\Core\ClassLoadingInformation;
26
use TYPO3\CMS\Core\Core\Environment;
27
use TYPO3\CMS\Core\Http\RequestFactory;
28
use TYPO3\CMS\Core\Log\LogManager;
29
use TYPO3\CMS\Core\Resource\Security\FileNameValidator;
30
use TYPO3\CMS\Core\Service\OpcodeCacheService;
31
use TYPO3\CMS\Core\SingletonInterface;
32
33
/**
34
 * The legendary "t3lib_div" class - Miscellaneous functions for general purpose.
35
 * Most of the functions do not relate specifically to TYPO3
36
 * However a section of functions requires certain TYPO3 features available
37
 * See comments in the source.
38
 * You are encouraged to use this library in your own scripts!
39
 *
40
 * USE:
41
 * The class is intended to be used without creating an instance of it.
42
 * So: Don't instantiate - call functions with "\TYPO3\CMS\Core\Utility\GeneralUtility::" prefixed the function name.
43
 * So use \TYPO3\CMS\Core\Utility\GeneralUtility::[method-name] to refer to the functions, eg. '\TYPO3\CMS\Core\Utility\GeneralUtility::milliseconds()'
44
 */
45
class GeneralUtility
46
{
47
    const ENV_TRUSTED_HOSTS_PATTERN_ALLOW_ALL = '.*';
48
    const ENV_TRUSTED_HOSTS_PATTERN_SERVER_NAME = 'SERVER_NAME';
49
50
    /**
51
     * State of host header value security check
52
     * in order to avoid unnecessary multiple checks during one request
53
     *
54
     * @var bool
55
     */
56
    protected static $allowHostHeaderValue = false;
57
58
    /**
59
     * @var ContainerInterface|null
60
     */
61
    protected static $container;
62
63
    /**
64
     * Singleton instances returned by makeInstance, using the class names as
65
     * array keys
66
     *
67
     * @var array<\TYPO3\CMS\Core\SingletonInterface>
68
     */
69
    protected static $singletonInstances = [];
70
71
    /**
72
     * Instances returned by makeInstance, using the class names as array keys
73
     *
74
     * @var array<object>
75
     */
76
    protected static $nonSingletonInstances = [];
77
78
    /**
79
     * Cache for makeInstance with given class name and final class names to reduce number of self::getClassName() calls
80
     *
81
     * @var array Given class name => final class name
82
     */
83
    protected static $finalClassNameCache = [];
84
85
    /**
86
     * The application context
87
     *
88
     * @var \TYPO3\CMS\Core\Core\ApplicationContext
89
     * @deprecated will be removed in TYPO3 v11.
90
     */
91
    protected static $applicationContext;
92
93
    /**
94
     * @var array
95
     */
96
    protected static $indpEnvCache = [];
97
98
    /*************************
99
     *
100
     * GET/POST Variables
101
     *
102
     * Background:
103
     * Input GET/POST variables in PHP may have their quotes escaped with "\" or not depending on configuration.
104
     * TYPO3 has always converted quotes to BE escaped if the configuration told that they would not be so.
105
     * But the clean solution is that quotes are never escaped and that is what the functions below offers.
106
     * Eventually TYPO3 should provide this in the global space as well.
107
     * In the transitional phase (or forever..?) we need to encourage EVERY to read and write GET/POST vars through the API functions below.
108
     * This functionality was previously needed to normalize between magic quotes logic, which was removed from PHP 5.4,
109
     * so these methods are still in use, but not tackle the slash problem anymore.
110
     *
111
     *************************/
112
    /**
113
     * Returns the 'GLOBAL' value of incoming data from POST or GET, with priority to POST, which is equivalent to 'GP' order
114
     * In case you already know by which method your data is arriving consider using GeneralUtility::_GET or GeneralUtility::_POST.
115
     *
116
     * @param string $var GET/POST var to return
117
     * @return mixed POST var named $var, if not set, the GET var of the same name and if also not set, NULL.
118
     */
119
    public static function _GP($var)
120
    {
121
        if (empty($var)) {
122
            return;
123
        }
124
        if (isset($_POST[$var])) {
125
            $value = $_POST[$var];
126
        } elseif (isset($_GET[$var])) {
127
            $value = $_GET[$var];
128
        } else {
129
            $value = null;
130
        }
131
        // This is there for backwards-compatibility, in order to avoid NULL
132
        if (isset($value) && !is_array($value)) {
133
            $value = (string)$value;
134
        }
135
        return $value;
136
    }
137
138
    /**
139
     * Returns the global arrays $_GET and $_POST merged with $_POST taking precedence.
140
     *
141
     * @param string $parameter Key (variable name) from GET or POST vars
142
     * @return array Returns the GET vars merged recursively onto the POST vars.
143
     */
144
    public static function _GPmerged($parameter)
145
    {
146
        $postParameter = isset($_POST[$parameter]) && is_array($_POST[$parameter]) ? $_POST[$parameter] : [];
147
        $getParameter = isset($_GET[$parameter]) && is_array($_GET[$parameter]) ? $_GET[$parameter] : [];
148
        $mergedParameters = $getParameter;
149
        ArrayUtility::mergeRecursiveWithOverrule($mergedParameters, $postParameter);
150
        return $mergedParameters;
151
    }
152
153
    /**
154
     * Returns the global $_GET array (or value from) normalized to contain un-escaped values.
155
     * This function was previously used to normalize between magic quotes logic, which was removed from PHP 5.5
156
     *
157
     * @param string $var Optional pointer to value in GET array (basically name of GET var)
158
     * @return mixed If $var is set it returns the value of $_GET[$var]. If $var is NULL (default), returns $_GET itself.
159
     * @see _POST()
160
     * @see _GP()
161
     */
162
    public static function _GET($var = null)
163
    {
164
        $value = $var === null
165
            ? $_GET
0 ignored issues
show
Coding Style introduced by
Expected 1 space before "?"; newline found
Loading history...
166
            : (empty($var) ? null : ($_GET[$var] ?? null));
0 ignored issues
show
Coding Style introduced by
Expected 1 space before ":"; newline found
Loading history...
167
        // This is there for backwards-compatibility, in order to avoid NULL
168
        if (isset($value) && !is_array($value)) {
169
            $value = (string)$value;
170
        }
171
        return $value;
172
    }
173
174
    /**
175
     * Returns the global $_POST array (or value from) normalized to contain un-escaped values.
176
     *
177
     * @param string $var Optional pointer to value in POST array (basically name of POST var)
178
     * @return mixed If $var is set it returns the value of $_POST[$var]. If $var is NULL (default), returns $_POST itself.
179
     * @see _GET()
180
     * @see _GP()
181
     */
182
    public static function _POST($var = null)
183
    {
184
        $value = $var === null ? $_POST : (empty($var) || !isset($_POST[$var]) ? null : $_POST[$var]);
185
        // This is there for backwards-compatibility, in order to avoid NULL
186
        if (isset($value) && !is_array($value)) {
187
            $value = (string)$value;
188
        }
189
        return $value;
190
    }
191
192
    /*************************
193
     *
194
     * STRING FUNCTIONS
195
     *
196
     *************************/
197
    /**
198
     * Truncates a string with appended/prepended "..." and takes current character set into consideration.
199
     *
200
     * @param string $string String to truncate
201
     * @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.
202
     * @param string $appendString Appendix to the truncated string
203
     * @return string Cropped string
204
     */
205
    public static function fixed_lgd_cs($string, $chars, $appendString = '...')
0 ignored issues
show
Coding Style introduced by
Method name "GeneralUtility::fixed_lgd_cs" is not in camel caps format
Loading history...
206
    {
207
        if ((int)$chars === 0 || mb_strlen($string, 'utf-8') <= abs($chars)) {
208
            return $string;
209
        }
210
        if ($chars > 0) {
211
            $string = mb_substr($string, 0, $chars, 'utf-8') . $appendString;
212
        } else {
213
            $string = $appendString . mb_substr($string, $chars, mb_strlen($string, 'utf-8'), 'utf-8');
214
        }
215
        return $string;
216
    }
217
218
    /**
219
     * Match IP number with list of numbers with wildcard
220
     * Dispatcher method for switching into specialised IPv4 and IPv6 methods.
221
     *
222
     * @param string $baseIP Is the current remote IP address for instance, typ. REMOTE_ADDR
223
     * @param string $list Is a comma-list of IP-addresses to match with. *-wildcard allowed instead of number, plus leaving out parts in the IP number is accepted as wildcard (eg. 192.168.*.* equals 192.168). If list is "*" no check is done and the function returns TRUE immediately. An empty list always returns FALSE.
224
     * @return bool TRUE if an IP-mask from $list matches $baseIP
225
     */
226
    public static function cmpIP($baseIP, $list)
227
    {
228
        $list = trim($list);
229
        if ($list === '') {
230
            return false;
231
        }
232
        if ($list === '*') {
233
            return true;
234
        }
235
        if (strpos($baseIP, ':') !== false && self::validIPv6($baseIP)) {
236
            return self::cmpIPv6($baseIP, $list);
237
        }
238
        return self::cmpIPv4($baseIP, $list);
239
    }
240
241
    /**
242
     * Match IPv4 number with list of numbers with wildcard
243
     *
244
     * @param string $baseIP Is the current remote IP address for instance, typ. REMOTE_ADDR
245
     * @param string $list Is a comma-list of IP-addresses to match with. *-wildcard allowed instead of number, plus leaving out parts in the IP number is accepted as wildcard (eg. 192.168.*.* equals 192.168), could also contain IPv6 addresses
246
     * @return bool TRUE if an IP-mask from $list matches $baseIP
247
     */
248
    public static function cmpIPv4($baseIP, $list)
249
    {
250
        $IPpartsReq = explode('.', $baseIP);
251
        if (count($IPpartsReq) === 4) {
252
            $values = self::trimExplode(',', $list, true);
253
            foreach ($values as $test) {
254
                $testList = explode('/', $test);
255
                if (count($testList) === 2) {
256
                    [$test, $mask] = $testList;
257
                } else {
258
                    $mask = false;
259
                }
260
                if ((int)$mask) {
261
                    // "192.168.3.0/24"
262
                    $lnet = ip2long($test);
263
                    $lip = ip2long($baseIP);
264
                    $binnet = str_pad(decbin($lnet), 32, '0', STR_PAD_LEFT);
265
                    $firstpart = substr($binnet, 0, $mask);
0 ignored issues
show
Bug introduced by
$mask of type false|string is incompatible with the type integer expected by parameter $length of substr(). ( Ignorable by Annotation )

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

265
                    $firstpart = substr($binnet, 0, /** @scrutinizer ignore-type */ $mask);
Loading history...
266
                    $binip = str_pad(decbin($lip), 32, '0', STR_PAD_LEFT);
267
                    $firstip = substr($binip, 0, $mask);
268
                    $yes = $firstpart === $firstip;
269
                } else {
270
                    // "192.168.*.*"
271
                    $IPparts = explode('.', $test);
272
                    $yes = 1;
273
                    foreach ($IPparts as $index => $val) {
274
                        $val = trim($val);
275
                        if ($val !== '*' && $IPpartsReq[$index] !== $val) {
276
                            $yes = 0;
277
                        }
278
                    }
279
                }
280
                if ($yes) {
281
                    return true;
282
                }
283
            }
284
        }
285
        return false;
286
    }
287
288
    /**
289
     * Match IPv6 address with a list of IPv6 prefixes
290
     *
291
     * @param string $baseIP Is the current remote IP address for instance
292
     * @param string $list Is a comma-list of IPv6 prefixes, could also contain IPv4 addresses
293
     * @return bool TRUE If a baseIP matches any prefix
294
     */
295
    public static function cmpIPv6($baseIP, $list)
296
    {
297
        // Policy default: Deny connection
298
        $success = false;
299
        $baseIP = self::normalizeIPv6($baseIP);
300
        $values = self::trimExplode(',', $list, true);
301
        foreach ($values as $test) {
302
            $testList = explode('/', $test);
303
            if (count($testList) === 2) {
304
                [$test, $mask] = $testList;
305
            } else {
306
                $mask = false;
307
            }
308
            if (self::validIPv6($test)) {
309
                $test = self::normalizeIPv6($test);
310
                $maskInt = (int)$mask ?: 128;
311
                // Special case; /0 is an allowed mask - equals a wildcard
312
                if ($mask === '0') {
313
                    $success = true;
314
                } elseif ($maskInt == 128) {
315
                    $success = $test === $baseIP;
316
                } else {
317
                    $testBin = self::IPv6Hex2Bin($test);
318
                    $baseIPBin = self::IPv6Hex2Bin($baseIP);
319
                    $success = true;
320
                    // Modulo is 0 if this is a 8-bit-boundary
321
                    $maskIntModulo = $maskInt % 8;
322
                    $numFullCharactersUntilBoundary = (int)($maskInt / 8);
323
                    if (strpos($testBin, substr($baseIPBin, 0, $numFullCharactersUntilBoundary)) !== 0) {
324
                        $success = false;
325
                    } elseif ($maskIntModulo > 0) {
326
                        // If not an 8-bit-boundary, check bits of last character
327
                        $testLastBits = str_pad(decbin(ord(substr($testBin, $numFullCharactersUntilBoundary, 1))), 8, '0', STR_PAD_LEFT);
328
                        $baseIPLastBits = str_pad(decbin(ord(substr($baseIPBin, $numFullCharactersUntilBoundary, 1))), 8, '0', STR_PAD_LEFT);
329
                        if (strncmp($testLastBits, $baseIPLastBits, $maskIntModulo) != 0) {
330
                            $success = false;
331
                        }
332
                    }
333
                }
334
            }
335
            if ($success) {
336
                return true;
337
            }
338
        }
339
        return false;
340
    }
341
342
    /**
343
     * Transform a regular IPv6 address from hex-representation into binary
344
     *
345
     * @param string $hex IPv6 address in hex-presentation
346
     * @return string Binary representation (16 characters, 128 characters)
347
     * @see IPv6Bin2Hex()
348
     */
349
    public static function IPv6Hex2Bin($hex)
350
    {
351
        return inet_pton($hex);
352
    }
353
354
    /**
355
     * Transform an IPv6 address from binary to hex-representation
356
     *
357
     * @param string $bin IPv6 address in hex-presentation
358
     * @return string Binary representation (16 characters, 128 characters)
359
     * @see IPv6Hex2Bin()
360
     */
361
    public static function IPv6Bin2Hex($bin)
362
    {
363
        return inet_ntop($bin);
364
    }
365
366
    /**
367
     * Normalize an IPv6 address to full length
368
     *
369
     * @param string $address Given IPv6 address
370
     * @return string Normalized address
371
     * @see compressIPv6()
372
     */
373
    public static function normalizeIPv6($address)
374
    {
375
        $normalizedAddress = '';
376
        // According to RFC lowercase-representation is recommended
377
        $address = strtolower($address);
378
        // Normalized representation has 39 characters (0000:0000:0000:0000:0000:0000:0000:0000)
379
        if (strlen($address) === 39) {
380
            // Already in full expanded form
381
            return $address;
382
        }
383
        // Count 2 if if address has hidden zero blocks
384
        $chunks = explode('::', $address);
385
        if (count($chunks) === 2) {
386
            $chunksLeft = explode(':', $chunks[0]);
387
            $chunksRight = explode(':', $chunks[1]);
388
            $left = count($chunksLeft);
389
            $right = count($chunksRight);
390
            // Special case: leading zero-only blocks count to 1, should be 0
391
            if ($left === 1 && strlen($chunksLeft[0]) === 0) {
392
                $left = 0;
393
            }
394
            $hiddenBlocks = 8 - ($left + $right);
395
            $hiddenPart = '';
396
            $h = 0;
397
            while ($h < $hiddenBlocks) {
398
                $hiddenPart .= '0000:';
399
                $h++;
400
            }
401
            if ($left === 0) {
402
                $stageOneAddress = $hiddenPart . $chunks[1];
403
            } else {
404
                $stageOneAddress = $chunks[0] . ':' . $hiddenPart . $chunks[1];
405
            }
406
        } else {
407
            $stageOneAddress = $address;
408
        }
409
        // Normalize the blocks:
410
        $blocks = explode(':', $stageOneAddress);
411
        $divCounter = 0;
412
        foreach ($blocks as $block) {
413
            $tmpBlock = '';
414
            $i = 0;
415
            $hiddenZeros = 4 - strlen($block);
416
            while ($i < $hiddenZeros) {
417
                $tmpBlock .= '0';
418
                $i++;
419
            }
420
            $normalizedAddress .= $tmpBlock . $block;
421
            if ($divCounter < 7) {
422
                $normalizedAddress .= ':';
423
                $divCounter++;
424
            }
425
        }
426
        return $normalizedAddress;
427
    }
428
429
    /**
430
     * Compress an IPv6 address to the shortest notation
431
     *
432
     * @param string $address Given IPv6 address
433
     * @return string Compressed address
434
     * @see normalizeIPv6()
435
     */
436
    public static function compressIPv6($address)
437
    {
438
        return inet_ntop(inet_pton($address));
439
    }
440
441
    /**
442
     * Validate a given IP address.
443
     *
444
     * Possible format are IPv4 and IPv6.
445
     *
446
     * @param string $ip IP address to be tested
447
     * @return bool TRUE if $ip is either of IPv4 or IPv6 format.
448
     */
449
    public static function validIP($ip)
450
    {
451
        return filter_var($ip, FILTER_VALIDATE_IP) !== false;
452
    }
453
454
    /**
455
     * Validate a given IP address to the IPv4 address format.
456
     *
457
     * Example for possible format: 10.0.45.99
458
     *
459
     * @param string $ip IP address to be tested
460
     * @return bool TRUE if $ip is of IPv4 format.
461
     */
462
    public static function validIPv4($ip)
463
    {
464
        return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false;
465
    }
466
467
    /**
468
     * Validate a given IP address to the IPv6 address format.
469
     *
470
     * Example for possible format: 43FB::BB3F:A0A0:0 | ::1
471
     *
472
     * @param string $ip IP address to be tested
473
     * @return bool TRUE if $ip is of IPv6 format.
474
     */
475
    public static function validIPv6($ip)
476
    {
477
        return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false;
478
    }
479
480
    /**
481
     * Match fully qualified domain name with list of strings with wildcard
482
     *
483
     * @param string $baseHost A hostname or an IPv4/IPv6-address (will by reverse-resolved; typically REMOTE_ADDR)
484
     * @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)
485
     * @return bool TRUE if a domain name mask from $list matches $baseIP
486
     */
487
    public static function cmpFQDN($baseHost, $list)
488
    {
489
        $baseHost = trim($baseHost);
490
        if (empty($baseHost)) {
491
            return false;
492
        }
493
        if (self::validIPv4($baseHost) || self::validIPv6($baseHost)) {
494
            // Resolve hostname
495
            // Note: this is reverse-lookup and can be randomly set as soon as somebody is able to set
496
            // the reverse-DNS for his IP (security when for example used with REMOTE_ADDR)
497
            $baseHostName = gethostbyaddr($baseHost);
498
            if ($baseHostName === $baseHost) {
499
                // Unable to resolve hostname
500
                return false;
501
            }
502
        } else {
503
            $baseHostName = $baseHost;
504
        }
505
        $baseHostNameParts = explode('.', $baseHostName);
506
        $values = self::trimExplode(',', $list, true);
507
        foreach ($values as $test) {
508
            $hostNameParts = explode('.', $test);
509
            // To match hostNameParts can only be shorter (in case of wildcards) or equal
510
            $hostNamePartsCount = count($hostNameParts);
511
            $baseHostNamePartsCount = count($baseHostNameParts);
512
            if ($hostNamePartsCount > $baseHostNamePartsCount) {
513
                continue;
514
            }
515
            $yes = true;
516
            foreach ($hostNameParts as $index => $val) {
517
                $val = trim($val);
518
                if ($val === '*') {
519
                    // Wildcard valid for one or more hostname-parts
520
                    $wildcardStart = $index + 1;
521
                    // Wildcard as last/only part always matches, otherwise perform recursive checks
522
                    if ($wildcardStart < $hostNamePartsCount) {
523
                        $wildcardMatched = false;
524
                        $tempHostName = implode('.', array_slice($hostNameParts, $index + 1));
525
                        while ($wildcardStart < $baseHostNamePartsCount && !$wildcardMatched) {
526
                            $tempBaseHostName = implode('.', array_slice($baseHostNameParts, $wildcardStart));
527
                            $wildcardMatched = self::cmpFQDN($tempBaseHostName, $tempHostName);
528
                            $wildcardStart++;
529
                        }
530
                        if ($wildcardMatched) {
531
                            // Match found by recursive compare
532
                            return true;
533
                        }
534
                        $yes = false;
535
                    }
536
                } elseif ($baseHostNameParts[$index] !== $val) {
537
                    // In case of no match
538
                    $yes = false;
539
                }
540
            }
541
            if ($yes) {
542
                return true;
543
            }
544
        }
545
        return false;
546
    }
547
548
    /**
549
     * Checks if a given URL matches the host that currently handles this HTTP request.
550
     * Scheme, hostname and (optional) port of the given URL are compared.
551
     *
552
     * @param string $url URL to compare with the TYPO3 request host
553
     * @return bool Whether the URL matches the TYPO3 request host
554
     */
555
    public static function isOnCurrentHost($url)
556
    {
557
        return stripos($url . '/', self::getIndpEnv('TYPO3_REQUEST_HOST') . '/') === 0;
558
    }
559
560
    /**
561
     * Check for item in list
562
     * Check if an item exists in a comma-separated list of items.
563
     *
564
     * @param string $list Comma-separated list of items (string)
565
     * @param string $item Item to check for
566
     * @return bool TRUE if $item is in $list
567
     */
568
    public static function inList($list, $item)
569
    {
570
        return strpos(',' . $list . ',', ',' . $item . ',') !== false;
571
    }
572
573
    /**
574
     * Removes an item from a comma-separated list of items.
575
     *
576
     * If $element contains a comma, the behaviour of this method is undefined.
577
     * Empty elements in the list are preserved.
578
     *
579
     * @param string $element Element to remove
580
     * @param string $list Comma-separated list of items (string)
581
     * @return string New comma-separated list of items
582
     */
583
    public static function rmFromList($element, $list)
584
    {
585
        $items = explode(',', $list);
586
        foreach ($items as $k => $v) {
587
            if ($v == $element) {
588
                unset($items[$k]);
589
            }
590
        }
591
        return implode(',', $items);
592
    }
593
594
    /**
595
     * Expand a comma-separated list of integers with ranges (eg 1,3-5,7 becomes 1,3,4,5,7).
596
     * Ranges are limited to 1000 values per range.
597
     *
598
     * @param string $list Comma-separated list of integers with ranges (string)
599
     * @return string New comma-separated list of items
600
     */
601
    public static function expandList($list)
602
    {
603
        $items = explode(',', $list);
604
        $list = [];
605
        foreach ($items as $item) {
606
            $range = explode('-', $item);
607
            if (isset($range[1])) {
608
                $runAwayBrake = 1000;
609
                for ($n = $range[0]; $n <= $range[1]; $n++) {
610
                    $list[] = $n;
611
                    $runAwayBrake--;
612
                    if ($runAwayBrake <= 0) {
613
                        break;
614
                    }
615
                }
616
            } else {
617
                $list[] = $item;
618
            }
619
        }
620
        return implode(',', $list);
621
    }
622
623
    /**
624
     * Makes a positive integer hash out of the first 7 chars from the md5 hash of the input
625
     *
626
     * @param string $str String to md5-hash
627
     * @return int Returns 28bit integer-hash
628
     */
629
    public static function md5int($str)
630
    {
631
        return hexdec(substr(md5($str), 0, 7));
632
    }
633
634
    /**
635
     * Returns the first 10 positions of the MD5-hash		(changed from 6 to 10 recently)
636
     *
637
     * @param string $input Input string to be md5-hashed
638
     * @param int $len The string-length of the output
639
     * @return string Substring of the resulting md5-hash, being $len chars long (from beginning)
640
     */
641
    public static function shortMD5($input, $len = 10)
642
    {
643
        return substr(md5($input), 0, $len);
644
    }
645
646
    /**
647
     * Returns a proper HMAC on a given input string and secret TYPO3 encryption key.
648
     *
649
     * @param string $input Input string to create HMAC from
650
     * @param string $additionalSecret additionalSecret to prevent hmac being used in a different context
651
     * @return string resulting (hexadecimal) HMAC currently with a length of 40 (HMAC-SHA-1)
652
     */
653
    public static function hmac($input, $additionalSecret = '')
654
    {
655
        $hashAlgorithm = 'sha1';
656
        $hashBlocksize = 64;
657
        $secret = $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'] . $additionalSecret;
658
        if (extension_loaded('hash') && function_exists('hash_hmac') && function_exists('hash_algos') && in_array($hashAlgorithm, hash_algos())) {
659
            $hmac = hash_hmac($hashAlgorithm, $input, $secret);
660
        } else {
661
            // Outer padding
662
            $opad = str_repeat(chr(92), $hashBlocksize);
663
            // Inner padding
664
            $ipad = str_repeat(chr(54), $hashBlocksize);
665
            if (strlen($secret) > $hashBlocksize) {
666
                // Keys longer than block size are shorten
667
                $key = str_pad(pack('H*', call_user_func($hashAlgorithm, $secret)), $hashBlocksize, "\0");
668
            } else {
669
                // Keys shorter than block size are zero-padded
670
                $key = str_pad($secret, $hashBlocksize, "\0");
671
            }
672
            $hmac = call_user_func($hashAlgorithm, ($key ^ $opad) . pack('H*', call_user_func(
673
                $hashAlgorithm,
674
                ($key ^ $ipad) . $input
675
            )));
676
        }
677
        return $hmac;
678
    }
679
680
    /**
681
     * Takes comma-separated lists and arrays and removes all duplicates
682
     * If a value in the list is trim(empty), the value is ignored.
683
     *
684
     * @param string $in_list Accept multiple parameters which can be comma-separated lists of values and arrays.
685
     * @param mixed $secondParameter Dummy field, which if set will show a warning!
686
     * @return string Returns the list without any duplicates of values, space around values are trimmed
687
     */
688
    public static function uniqueList($in_list, $secondParameter = null)
689
    {
690
        if (is_array($in_list)) {
0 ignored issues
show
introduced by
The condition is_array($in_list) is always false.
Loading history...
691
            throw new \InvalidArgumentException('TYPO3 Fatal Error: TYPO3\\CMS\\Core\\Utility\\GeneralUtility::uniqueList() does NOT support array arguments anymore! Only string comma lists!', 1270853885);
692
        }
693
        if (isset($secondParameter)) {
694
            throw new \InvalidArgumentException('TYPO3 Fatal Error: TYPO3\\CMS\\Core\\Utility\\GeneralUtility::uniqueList() does NOT support more than a single argument value anymore. You have specified more than one!', 1270853886);
695
        }
696
        return implode(',', array_unique(self::trimExplode(',', $in_list, true)));
697
    }
698
699
    /**
700
     * Splits a reference to a file in 5 parts
701
     *
702
     * @param string $fileNameWithPath File name with path to be analyzed (must exist if open_basedir is set)
703
     * @return array Contains keys [path], [file], [filebody], [fileext], [realFileext]
704
     */
705
    public static function split_fileref($fileNameWithPath)
0 ignored issues
show
Coding Style introduced by
Method name "GeneralUtility::split_fileref" is not in camel caps format
Loading history...
706
    {
707
        $info = [];
708
        $reg = [];
709
        if (preg_match('/(.*\\/)(.*)$/', $fileNameWithPath, $reg)) {
710
            $info['path'] = $reg[1];
711
            $info['file'] = $reg[2];
712
        } else {
713
            $info['path'] = '';
714
            $info['file'] = $fileNameWithPath;
715
        }
716
        $reg = '';
717
        // If open_basedir is set and the fileName was supplied without a path the is_dir check fails
718
        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 array|null 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

718
        if (!is_dir($fileNameWithPath) && preg_match('/(.*)\\.([^\\.]*$)/', $info['file'], /** @scrutinizer ignore-type */ $reg)) {
Loading history...
719
            $info['filebody'] = $reg[1];
720
            $info['fileext'] = strtolower($reg[2]);
721
            $info['realFileext'] = $reg[2];
722
        } else {
723
            $info['filebody'] = $info['file'];
724
            $info['fileext'] = '';
725
        }
726
        reset($info);
727
        return $info;
728
    }
729
730
    /**
731
     * Returns the directory part of a path without trailing slash
732
     * If there is no dir-part, then an empty string is returned.
733
     * Behaviour:
734
     *
735
     * '/dir1/dir2/script.php' => '/dir1/dir2'
736
     * '/dir1/' => '/dir1'
737
     * 'dir1/script.php' => 'dir1'
738
     * 'd/script.php' => 'd'
739
     * '/script.php' => ''
740
     * '' => ''
741
     *
742
     * @param string $path Directory name / path
743
     * @return string Processed input value. See function description.
744
     */
745
    public static function dirname($path)
746
    {
747
        $p = self::revExplode('/', $path, 2);
748
        return count($p) === 2 ? $p[0] : '';
749
    }
750
751
    /**
752
     * Returns TRUE if the first part of $str matches the string $partStr
753
     *
754
     * @param string $str Full string to check
755
     * @param string $partStr Reference string which must be found as the "first part" of the full string
756
     * @return bool TRUE if $partStr was found to be equal to the first part of $str
757
     */
758
    public static function isFirstPartOfStr($str, $partStr)
759
    {
760
        $str = is_array($str) ? '' : (string)$str;
0 ignored issues
show
introduced by
The condition is_array($str) is always false.
Loading history...
761
        $partStr = is_array($partStr) ? '' : (string)$partStr;
0 ignored issues
show
introduced by
The condition is_array($partStr) is always false.
Loading history...
762
        return $partStr !== '' && strpos($str, $partStr, 0) === 0;
763
    }
764
765
    /**
766
     * Formats the input integer $sizeInBytes as bytes/kilobytes/megabytes (-/K/M)
767
     *
768
     * @param int $sizeInBytes Number of bytes to format.
769
     * @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".
770
     * @param int $base The unit base if not using a unit name. Defaults to 1024.
771
     * @return string Formatted representation of the byte number, for output.
772
     */
773
    public static function formatSize($sizeInBytes, $labels = '', $base = 0)
774
    {
775
        $defaultFormats = [
776
            'iec' => ['base' => 1024, 'labels' => [' ', ' Ki', ' Mi', ' Gi', ' Ti', ' Pi', ' Ei', ' Zi', ' Yi']],
777
            'si' => ['base' => 1000, 'labels' => [' ', ' k', ' M', ' G', ' T', ' P', ' E', ' Z', ' Y']],
778
        ];
779
        // Set labels and base:
780
        if (empty($labels)) {
781
            $labels = 'iec';
782
        }
783
        if (isset($defaultFormats[$labels])) {
784
            $base = $defaultFormats[$labels]['base'];
785
            $labelArr = $defaultFormats[$labels]['labels'];
786
        } else {
787
            $base = (int)$base;
788
            if ($base !== 1000 && $base !== 1024) {
789
                $base = 1024;
790
            }
791
            $labelArr = explode('|', str_replace('"', '', $labels));
792
        }
793
        // @todo find out which locale is used for current BE user to cover the BE case as well
794
        $oldLocale = setlocale(LC_NUMERIC, 0);
795
        $newLocale = $GLOBALS['TSFE']->config['config']['locale_all'] ?? '';
796
        if ($newLocale) {
797
            setlocale(LC_NUMERIC, $newLocale);
798
        }
799
        $localeInfo = localeconv();
800
        if ($newLocale) {
801
            setlocale(LC_NUMERIC, $oldLocale);
802
        }
803
        $sizeInBytes = max($sizeInBytes, 0);
804
        $multiplier = floor(($sizeInBytes ? log($sizeInBytes) : 0) / log($base));
805
        $sizeInUnits = $sizeInBytes / $base ** $multiplier;
806
        if ($sizeInUnits > ($base * .9)) {
807
            $multiplier++;
808
        }
809
        $multiplier = min($multiplier, count($labelArr) - 1);
810
        $sizeInUnits = $sizeInBytes / $base ** $multiplier;
811
        return number_format($sizeInUnits, (($multiplier > 0) && ($sizeInUnits < 20)) ? 2 : 0, $localeInfo['decimal_point'], '') . $labelArr[$multiplier];
812
    }
813
814
    /**
815
     * This splits a string by the chars in $operators (typical /+-*) and returns an array with them in
816
     *
817
     * @param string $string Input string, eg "123 + 456 / 789 - 4
818
     * @param string $operators Operators to split by, typically "/+-*
819
     * @return array Array with operators and operands separated.
820
     * @see \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::calc()
821
     * @see \TYPO3\CMS\Frontend\Imaging\GifBuilder::calcOffset()
822
     */
823
    public static function splitCalc($string, $operators)
824
    {
825
        $res = [];
826
        $sign = '+';
827
        while ($string) {
828
            $valueLen = strcspn($string, $operators);
829
            $value = substr($string, 0, $valueLen);
830
            $res[] = [$sign, trim($value)];
831
            $sign = substr($string, $valueLen, 1);
832
            $string = substr($string, $valueLen + 1);
833
        }
834
        reset($res);
835
        return $res;
836
    }
837
838
    /**
839
     * Checking syntax of input email address
840
     *
841
     * @param string $email Input string to evaluate
842
     * @return bool Returns TRUE if the $email address (input string) is valid
843
     */
844
    public static function validEmail($email)
845
    {
846
        // Early return in case input is not a string
847
        if (!is_string($email)) {
0 ignored issues
show
introduced by
The condition is_string($email) is always true.
Loading history...
848
            return false;
849
        }
850
        if (trim($email) !== $email) {
851
            return false;
852
        }
853
        $validator = new EmailValidator();
854
        return $validator->isValid($email, new RFCValidation());
855
    }
856
857
    /**
858
     * Returns an ASCII string (punicode) representation of $value
859
     *
860
     * @param string $value
861
     * @return string An ASCII encoded (punicode) string
862
     * @deprecated since TYPO3 v10.0, will be removed in TYPO3 v11.0, use PHP's native idn_to_ascii($domain, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46) function directly.
863
     */
864
    public static function idnaEncode($value)
865
    {
866
        trigger_error(__METHOD__ . ' will be removed in TYPO3 v11.0. Use PHPs native "idn_to_ascii($domain, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46)" function directly instead.', E_USER_DEPRECATED);
867
        // Early return in case input is not a string or empty
868
        if (!is_string($value) || empty($value)) {
0 ignored issues
show
introduced by
The condition is_string($value) is always true.
Loading history...
869
            return (string)$value;
870
        }
871
        // Split on the last "@" since addresses like "foo@bar"@example.org are valid where the only focus
872
        // is an email address
873
        $atPosition = strrpos($value, '@');
874
        if ($atPosition !== false) {
875
            $domain = substr($value, $atPosition + 1);
876
            $local = substr($value, 0, $atPosition);
877
            $domain = (string)HttpUtility::idn_to_ascii($domain);
878
            // Return if no @ found or it is placed at the very beginning or end of the email
879
            return $local . '@' . $domain;
880
        }
881
        return (string)HttpUtility::idn_to_ascii($value);
882
    }
883
884
    /**
885
     * Returns a given string with underscores as UpperCamelCase.
886
     * Example: Converts blog_example to BlogExample
887
     *
888
     * @param string $string String to be converted to camel case
889
     * @return string UpperCamelCasedWord
890
     */
891
    public static function underscoredToUpperCamelCase($string)
892
    {
893
        return str_replace(' ', '', ucwords(str_replace('_', ' ', strtolower($string))));
894
    }
895
896
    /**
897
     * Returns a given string with underscores as lowerCamelCase.
898
     * Example: Converts minimal_value to minimalValue
899
     *
900
     * @param string $string String to be converted to camel case
901
     * @return string lowerCamelCasedWord
902
     */
903
    public static function underscoredToLowerCamelCase($string)
904
    {
905
        return lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', strtolower($string)))));
906
    }
907
908
    /**
909
     * Returns a given CamelCasedString as a lowercase string with underscores.
910
     * Example: Converts BlogExample to blog_example, and minimalValue to minimal_value
911
     *
912
     * @param string $string String to be converted to lowercase underscore
913
     * @return string lowercase_and_underscored_string
914
     */
915
    public static function camelCaseToLowerCaseUnderscored($string)
916
    {
917
        $value = preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $string);
918
        return mb_strtolower($value, 'utf-8');
919
    }
920
921
    /**
922
     * Checks if a given string is a Uniform Resource Locator (URL).
923
     *
924
     * On seriously malformed URLs, parse_url may return FALSE and emit an
925
     * E_WARNING.
926
     *
927
     * filter_var() requires a scheme to be present.
928
     *
929
     * http://www.faqs.org/rfcs/rfc2396.html
930
     * Scheme names consist of a sequence of characters beginning with a
931
     * lower case letter and followed by any combination of lower case letters,
932
     * digits, plus ("+"), period ("."), or hyphen ("-").  For resiliency,
933
     * programs interpreting URI should treat upper case letters as equivalent to
934
     * lower case in scheme names (e.g., allow "HTTP" as well as "http").
935
     * scheme = alpha *( alpha | digit | "+" | "-" | "." )
936
     *
937
     * Convert the domain part to punicode if it does not look like a regular
938
     * domain name. Only the domain part because RFC3986 specifies the the rest of
939
     * the url may not contain special characters:
940
     * http://tools.ietf.org/html/rfc3986#appendix-A
941
     *
942
     * @param string $url The URL to be validated
943
     * @return bool Whether the given URL is valid
944
     */
945
    public static function isValidUrl($url)
946
    {
947
        $parsedUrl = parse_url($url);
948
        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...
949
            return false;
950
        }
951
        // HttpUtility::buildUrl() will always build urls with <scheme>://
952
        // our original $url might only contain <scheme>: (e.g. mail:)
953
        // so we convert that to the double-slashed version to ensure
954
        // our check against the $recomposedUrl is proper
955
        if (!self::isFirstPartOfStr($url, $parsedUrl['scheme'] . '://')) {
956
            $url = str_replace($parsedUrl['scheme'] . ':', $parsedUrl['scheme'] . '://', $url);
957
        }
958
        $recomposedUrl = HttpUtility::buildUrl($parsedUrl);
959
        if ($recomposedUrl !== $url) {
960
            // The parse_url() had to modify characters, so the URL is invalid
961
            return false;
962
        }
963
        if (isset($parsedUrl['host']) && !preg_match('/^[a-z0-9.\\-]*$/i', $parsedUrl['host'])) {
964
            $host = HttpUtility::idn_to_ascii($parsedUrl['host']);
965
            if ($host === false) {
966
                return false;
967
            }
968
            $parsedUrl['host'] = $host;
969
        }
970
        return filter_var(HttpUtility::buildUrl($parsedUrl), FILTER_VALIDATE_URL) !== false;
971
    }
972
973
    /*************************
974
     *
975
     * ARRAY FUNCTIONS
976
     *
977
     *************************/
978
979
    /**
980
     * Explodes a $string delimited by $delimiter and casts each item in the array to (int).
981
     * Corresponds to \TYPO3\CMS\Core\Utility\GeneralUtility::trimExplode(), but with conversion to integers for all values.
982
     *
983
     * @param string $delimiter Delimiter string to explode with
984
     * @param string $string The string to explode
985
     * @param bool $removeEmptyValues If set, all empty values (='') will NOT be set in output
986
     * @param int $limit If positive, the result will contain a maximum of limit elements,
987
     * @return array Exploded values, all converted to integers
988
     */
989
    public static function intExplode($delimiter, $string, $removeEmptyValues = false, $limit = 0)
990
    {
991
        $result = explode($delimiter, $string);
992
        foreach ($result as $key => &$value) {
993
            if ($removeEmptyValues && ($value === '' || trim($value) === '')) {
994
                unset($result[$key]);
995
            } else {
996
                $value = (int)$value;
997
            }
998
        }
999
        unset($value);
1000
        if ($limit !== 0) {
1001
            if ($limit < 0) {
1002
                $result = array_slice($result, 0, $limit);
1003
            } elseif (count($result) > $limit) {
1004
                $lastElements = array_slice($result, $limit - 1);
1005
                $result = array_slice($result, 0, $limit - 1);
1006
                $result[] = implode($delimiter, $lastElements);
1007
            }
1008
        }
1009
        return $result;
1010
    }
1011
1012
    /**
1013
     * Reverse explode which explodes the string counting from behind.
1014
     *
1015
     * Note: The delimiter has to given in the reverse order as
1016
     *       it is occurring within the string.
1017
     *
1018
     * GeneralUtility::revExplode('[]', '[my][words][here]', 2)
1019
     *   ==> array('[my][words', 'here]')
1020
     *
1021
     * @param string $delimiter Delimiter string to explode with
1022
     * @param string $string The string to explode
1023
     * @param int $count Number of array entries
1024
     * @return array Exploded values
1025
     */
1026
    public static function revExplode($delimiter, $string, $count = 0)
1027
    {
1028
        // 2 is the (currently, as of 2014-02) most-used value for $count in the core, therefore we check it first
1029
        if ($count === 2) {
1030
            $position = strrpos($string, strrev($delimiter));
1031
            if ($position !== false) {
1032
                return [substr($string, 0, $position), substr($string, $position + strlen($delimiter))];
1033
            }
1034
            return [$string];
1035
        }
1036
        if ($count <= 1) {
1037
            return [$string];
1038
        }
1039
        $explodedValues = explode($delimiter, strrev($string), $count);
1040
        $explodedValues = array_map('strrev', $explodedValues);
1041
        return array_reverse($explodedValues);
1042
    }
1043
1044
    /**
1045
     * Explodes a string and trims all values for whitespace in the end.
1046
     * If $onlyNonEmptyValues is set, then all blank ('') values are removed.
1047
     *
1048
     * @param string $delim Delimiter string to explode with
1049
     * @param string $string The string to explode
1050
     * @param bool $removeEmptyValues If set, all empty values will be removed in output
1051
     * @param int $limit If limit is set and positive, the returned array will contain a maximum of limit elements with
1052
     *                   the last element containing the rest of string. If the limit parameter is negative, all components
1053
     *                   except the last -limit are returned.
1054
     * @return array Exploded values
1055
     */
1056
    public static function trimExplode($delim, $string, $removeEmptyValues = false, $limit = 0)
1057
    {
1058
        $result = explode($delim, $string);
1059
        if ($removeEmptyValues) {
1060
            $temp = [];
1061
            foreach ($result as $value) {
1062
                if (trim($value) !== '') {
1063
                    $temp[] = $value;
1064
                }
1065
            }
1066
            $result = $temp;
1067
        }
1068
        if ($limit > 0 && count($result) > $limit) {
1069
            $lastElements = array_splice($result, $limit - 1);
1070
            $result[] = implode($delim, $lastElements);
1071
        } elseif ($limit < 0) {
1072
            $result = array_slice($result, 0, $limit);
1073
        }
1074
        $result = array_map('trim', $result);
1075
        return $result;
1076
    }
1077
1078
    /**
1079
     * Implodes a multidim-array into GET-parameters (eg. &param[key][key2]=value2&param[key][key3]=value3)
1080
     *
1081
     * @param string $name Name prefix for entries. Set to blank if you wish none.
1082
     * @param array $theArray The (multidimensional) array to implode
1083
     * @param string $str (keep blank)
1084
     * @param bool $skipBlank If set, parameters which were blank strings would be removed.
1085
     * @param bool $rawurlencodeParamName If set, the param name itself (for example "param[key][key2]") would be rawurlencoded as well.
1086
     * @return string Imploded result, fx. &param[key][key2]=value2&param[key][key3]=value3
1087
     * @see explodeUrl2Array()
1088
     */
1089
    public static function implodeArrayForUrl($name, array $theArray, $str = '', $skipBlank = false, $rawurlencodeParamName = false)
1090
    {
1091
        foreach ($theArray as $Akey => $AVal) {
1092
            $thisKeyName = $name ? $name . '[' . $Akey . ']' : $Akey;
1093
            if (is_array($AVal)) {
1094
                $str = self::implodeArrayForUrl($thisKeyName, $AVal, $str, $skipBlank, $rawurlencodeParamName);
1095
            } else {
1096
                if (!$skipBlank || (string)$AVal !== '') {
1097
                    $str .= '&' . ($rawurlencodeParamName ? rawurlencode($thisKeyName) : $thisKeyName) . '=' . rawurlencode($AVal);
1098
                }
1099
            }
1100
        }
1101
        return $str;
1102
    }
1103
1104
    /**
1105
     * Explodes a string with GETvars (eg. "&id=1&type=2&ext[mykey]=3") into an array.
1106
     *
1107
     * Note! If you want to use a multi-dimensional string, consider this plain simple PHP code instead:
1108
     *
1109
     * $result = [];
1110
     * parse_str($queryParametersAsString, $result);
1111
     *
1112
     * However, if you do magic with a flat structure (e.g. keeping "ext[mykey]" as flat key in a one-dimensional array)
1113
     * then this method is for you.
1114
     *
1115
     * @param string $string GETvars string
1116
     * @return array Array of values. All values AND keys are rawurldecoded() as they properly should be. But this means that any implosion of the array again must rawurlencode it!
1117
     * @see implodeArrayForUrl()
1118
     */
1119
    public static function explodeUrl2Array($string)
1120
    {
1121
        $output = [];
1122
        $p = explode('&', $string);
1123
        foreach ($p as $v) {
1124
            if ($v !== '') {
1125
                [$pK, $pV] = explode('=', $v, 2);
1126
                $output[rawurldecode($pK)] = rawurldecode($pV);
1127
            }
1128
        }
1129
        return $output;
1130
    }
1131
1132
    /**
1133
     * Returns an array with selected keys from incoming data.
1134
     * (Better read source code if you want to find out...)
1135
     *
1136
     * @param string $varList List of variable/key names
1137
     * @param array $getArray Array from where to get values based on the keys in $varList
1138
     * @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
1139
     * @return array Output array with selected variables.
1140
     */
1141
    public static function compileSelectedGetVarsFromArray($varList, array $getArray, $GPvarAlt = true)
1142
    {
1143
        $keys = self::trimExplode(',', $varList, true);
1144
        $outArr = [];
1145
        foreach ($keys as $v) {
1146
            if (isset($getArray[$v])) {
1147
                $outArr[$v] = $getArray[$v];
1148
            } elseif ($GPvarAlt) {
1149
                $outArr[$v] = self::_GP($v);
1150
            }
1151
        }
1152
        return $outArr;
1153
    }
1154
1155
    /**
1156
     * Removes dots "." from end of a key identifier of TypoScript styled array.
1157
     * array('key.' => array('property.' => 'value')) --> array('key' => array('property' => 'value'))
1158
     *
1159
     * @param array $ts TypoScript configuration array
1160
     * @return array TypoScript configuration array without dots at the end of all keys
1161
     */
1162
    public static function removeDotsFromTS(array $ts)
1163
    {
1164
        $out = [];
1165
        foreach ($ts as $key => $value) {
1166
            if (is_array($value)) {
1167
                $key = rtrim($key, '.');
1168
                $out[$key] = self::removeDotsFromTS($value);
1169
            } else {
1170
                $out[$key] = $value;
1171
            }
1172
        }
1173
        return $out;
1174
    }
1175
1176
    /*************************
1177
     *
1178
     * HTML/XML PROCESSING
1179
     *
1180
     *************************/
1181
    /**
1182
     * Returns an array with all attributes of the input HTML tag as key/value pairs. Attributes are only lowercase a-z
1183
     * $tag is either a whole tag (eg '<TAG OPTION ATTRIB=VALUE>') or the parameter list (ex ' OPTION ATTRIB=VALUE>')
1184
     * If an attribute is empty, then the value for the key is empty. You can check if it existed with isset()
1185
     *
1186
     * @param string $tag HTML-tag string (or attributes only)
1187
     * @return array Array with the attribute values.
1188
     */
1189
    public static function get_tag_attributes($tag)
0 ignored issues
show
Coding Style introduced by
Method name "GeneralUtility::get_tag_attributes" is not in camel caps format
Loading history...
1190
    {
1191
        $components = self::split_tag_attributes($tag);
1192
        // Attribute name is stored here
1193
        $name = '';
1194
        $valuemode = false;
1195
        $attributes = [];
1196
        foreach ($components as $key => $val) {
1197
            // 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
1198
            if ($val !== '=') {
1199
                if ($valuemode) {
1200
                    if ($name) {
1201
                        $attributes[$name] = htmlspecialchars_decode($val, ENT_NOQUOTES);
1202
                        $name = '';
1203
                    }
1204
                } else {
1205
                    if ($key = strtolower(preg_replace('/[^[:alnum:]_\\:\\-]/', '', $val))) {
1206
                        $attributes[$key] = '';
1207
                        $name = $key;
1208
                    }
1209
                }
1210
                $valuemode = false;
1211
            } else {
1212
                $valuemode = true;
1213
            }
1214
        }
1215
        return $attributes;
1216
    }
1217
1218
    /**
1219
     * Returns an array with the 'components' from an attribute list from an HTML tag. The result is normally analyzed by get_tag_attributes
1220
     * Removes tag-name if found
1221
     *
1222
     * @param string $tag HTML-tag string (or attributes only)
1223
     * @return array Array with the attribute values.
1224
     */
1225
    public static function split_tag_attributes($tag)
0 ignored issues
show
Coding Style introduced by
Method name "GeneralUtility::split_tag_attributes" is not in camel caps format
Loading history...
1226
    {
1227
        $tag_tmp = trim(preg_replace('/^<[^[:space:]]*/', '', trim($tag)));
1228
        // Removes any > in the end of the string
1229
        $tag_tmp = trim(rtrim($tag_tmp, '>'));
1230
        $value = [];
1231
        // Compared with empty string instead , 030102
1232
        while ($tag_tmp !== '') {
1233
            $firstChar = $tag_tmp[0];
1234
            if ($firstChar === '"' || $firstChar === '\'') {
1235
                $reg = explode($firstChar, $tag_tmp, 3);
1236
                $value[] = $reg[1];
1237
                $tag_tmp = trim($reg[2]);
1238
            } elseif ($firstChar === '=') {
1239
                $value[] = '=';
1240
                // Removes = chars.
1241
                $tag_tmp = trim(substr($tag_tmp, 1));
1242
            } else {
1243
                // There are '' around the value. We look for the next ' ' or '>'
1244
                $reg = preg_split('/[[:space:]=]/', $tag_tmp, 2);
1245
                $value[] = trim($reg[0]);
1246
                $tag_tmp = trim(substr($tag_tmp, strlen($reg[0]), 1) . ($reg[1] ?? ''));
1247
            }
1248
        }
1249
        reset($value);
1250
        return $value;
1251
    }
1252
1253
    /**
1254
     * Implodes attributes in the array $arr for an attribute list in eg. and HTML tag (with quotes)
1255
     *
1256
     * @param array $arr Array with attribute key/value pairs, eg. "bgcolor"=>"red", "border"=>0
1257
     * @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!
1258
     * @param bool $dontOmitBlankAttribs If TRUE, don't check if values are blank. Default is to omit attributes with blank values.
1259
     * @return string Imploded attributes, eg. 'bgcolor="red" border="0"'
1260
     */
1261
    public static function implodeAttributes(array $arr, $xhtmlSafe = false, $dontOmitBlankAttribs = false)
1262
    {
1263
        if ($xhtmlSafe) {
1264
            $newArr = [];
1265
            foreach ($arr as $p => $v) {
1266
                if (!isset($newArr[strtolower($p)])) {
1267
                    $newArr[strtolower($p)] = htmlspecialchars($v);
1268
                }
1269
            }
1270
            $arr = $newArr;
1271
        }
1272
        $list = [];
1273
        foreach ($arr as $p => $v) {
1274
            if ((string)$v !== '' || $dontOmitBlankAttribs) {
1275
                $list[] = $p . '="' . $v . '"';
1276
            }
1277
        }
1278
        return implode(' ', $list);
1279
    }
1280
1281
    /**
1282
     * Wraps JavaScript code XHTML ready with <script>-tags
1283
     * Automatic re-indenting of the JS code is done by using the first line as indent reference.
1284
     * This is nice for indenting JS code with PHP code on the same level.
1285
     *
1286
     * @param string $string JavaScript code
1287
     * @return string The wrapped JS code, ready to put into a XHTML page
1288
     */
1289
    public static function wrapJS($string)
1290
    {
1291
        if (trim($string)) {
1292
            // remove nl from the beginning
1293
            $string = ltrim($string, LF);
1294
            // re-ident to one tab using the first line as reference
1295
            $match = [];
1296
            if (preg_match('/^(\\t+)/', $string, $match)) {
1297
                $string = str_replace($match[1], "\t", $string);
1298
            }
1299
            return '<script>
1300
/*<![CDATA[*/
1301
' . $string . '
1302
/*]]>*/
1303
</script>';
1304
        }
1305
        return '';
1306
    }
1307
1308
    /**
1309
     * Parses XML input into a PHP array with associative keys
1310
     *
1311
     * @param string $string XML data input
1312
     * @param int $depth Number of element levels to resolve the XML into an array. Any further structure will be set as XML.
1313
     * @param array $parserOptions Options that will be passed to PHP's xml_parser_set_option()
1314
     * @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.
1315
     */
1316
    public static function xml2tree($string, $depth = 999, $parserOptions = [])
1317
    {
1318
        // Disables the functionality to allow external entities to be loaded when parsing the XML, must be kept
1319
        $previousValueOfEntityLoader = libxml_disable_entity_loader(true);
1320
        $parser = xml_parser_create();
1321
        $vals = [];
1322
        $index = [];
1323
        xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0);
1324
        xml_parser_set_option($parser, XML_OPTION_SKIP_WHITE, 0);
1325
        foreach ($parserOptions as $option => $value) {
1326
            xml_parser_set_option($parser, $option, $value);
1327
        }
1328
        xml_parse_into_struct($parser, $string, $vals, $index);
1329
        libxml_disable_entity_loader($previousValueOfEntityLoader);
1330
        if (xml_get_error_code($parser)) {
1331
            return 'Line ' . xml_get_current_line_number($parser) . ': ' . xml_error_string(xml_get_error_code($parser));
1332
        }
1333
        xml_parser_free($parser);
1334
        $stack = [[]];
1335
        $stacktop = 0;
1336
        $startPoint = 0;
1337
        $tagi = [];
1338
        foreach ($vals as $key => $val) {
1339
            $type = $val['type'];
1340
            // open tag:
1341
            if ($type === 'open' || $type === 'complete') {
1342
                $stack[$stacktop++] = $tagi;
1343
                if ($depth == $stacktop) {
1344
                    $startPoint = $key;
1345
                }
1346
                $tagi = ['tag' => $val['tag']];
1347
                if (isset($val['attributes'])) {
1348
                    $tagi['attrs'] = $val['attributes'];
1349
                }
1350
                if (isset($val['value'])) {
1351
                    $tagi['values'][] = $val['value'];
1352
                }
1353
            }
1354
            // finish tag:
1355
            if ($type === 'complete' || $type === 'close') {
1356
                $oldtagi = $tagi;
1357
                $tagi = $stack[--$stacktop];
1358
                $oldtag = $oldtagi['tag'];
1359
                unset($oldtagi['tag']);
1360
                if ($depth == $stacktop + 1) {
1361
                    if ($key - $startPoint > 0) {
1362
                        $partArray = array_slice($vals, $startPoint + 1, $key - $startPoint - 1);
1363
                        $oldtagi['XMLvalue'] = self::xmlRecompileFromStructValArray($partArray);
1364
                    } else {
1365
                        $oldtagi['XMLvalue'] = $oldtagi['values'][0];
1366
                    }
1367
                }
1368
                $tagi['ch'][$oldtag][] = $oldtagi;
1369
                unset($oldtagi);
1370
            }
1371
            // cdata
1372
            if ($type === 'cdata') {
1373
                $tagi['values'][] = $val['value'];
1374
            }
1375
        }
1376
        return $tagi['ch'];
1377
    }
1378
1379
    /**
1380
     * Converts a PHP array into an XML string.
1381
     * The XML output is optimized for readability since associative keys are used as tag names.
1382
     * 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.
1383
     * Numeric keys are stored with the default tag name "numIndex" but can be overridden to other formats)
1384
     * 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
1385
     * 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.
1386
     * 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!
1387
     * 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...
1388
     *
1389
     * @param array $array The input PHP array with any kind of data; text, binary, integers. Not objects though.
1390
     * @param string $NSprefix tag-prefix, eg. a namespace prefix like "T3:"
1391
     * @param int $level Current recursion level. Don't change, stay at zero!
1392
     * @param string $docTag Alternative document tag. Default is "phparray".
1393
     * @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
1394
     * @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')
1395
     * @param array $stackData Stack data. Don't touch.
1396
     * @return string An XML string made from the input content in the array.
1397
     * @see xml2array()
1398
     */
1399
    public static function array2xml(array $array, $NSprefix = '', $level = 0, $docTag = 'phparray', $spaceInd = 0, array $options = [], array $stackData = [])
1400
    {
1401
        // 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
1402
        $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);
1403
        // Set indenting mode:
1404
        $indentChar = $spaceInd ? ' ' : "\t";
1405
        $indentN = $spaceInd > 0 ? $spaceInd : 1;
1406
        $nl = $spaceInd >= 0 ? LF : '';
1407
        // Init output variable:
1408
        $output = '';
1409
        // Traverse the input array
1410
        foreach ($array as $k => $v) {
1411
            $attr = '';
1412
            $tagName = $k;
1413
            // Construct the tag name.
1414
            // Use tag based on grand-parent + parent tag name
1415
            if (isset($stackData['grandParentTagName'], $stackData['parentTagName'], $options['grandParentTagMap'][$stackData['grandParentTagName'] . '/' . $stackData['parentTagName']])) {
1416
                $attr .= ' index="' . htmlspecialchars($tagName) . '"';
1417
                $tagName = (string)$options['grandParentTagMap'][$stackData['grandParentTagName'] . '/' . $stackData['parentTagName']];
1418
            } elseif (isset($stackData['parentTagName'], $options['parentTagMap'][$stackData['parentTagName'] . ':_IS_NUM']) && MathUtility::canBeInterpretedAsInteger($tagName)) {
1419
                // Use tag based on parent tag name + if current tag is numeric
1420
                $attr .= ' index="' . htmlspecialchars($tagName) . '"';
1421
                $tagName = (string)$options['parentTagMap'][$stackData['parentTagName'] . ':_IS_NUM'];
1422
            } elseif (isset($stackData['parentTagName'], $options['parentTagMap'][$stackData['parentTagName'] . ':' . $tagName])) {
1423
                // Use tag based on parent tag name + current tag
1424
                $attr .= ' index="' . htmlspecialchars($tagName) . '"';
1425
                $tagName = (string)$options['parentTagMap'][$stackData['parentTagName'] . ':' . $tagName];
1426
            } elseif (isset($stackData['parentTagName'], $options['parentTagMap'][$stackData['parentTagName']])) {
1427
                // Use tag based on parent tag name:
1428
                $attr .= ' index="' . htmlspecialchars($tagName) . '"';
1429
                $tagName = (string)$options['parentTagMap'][$stackData['parentTagName']];
1430
            } elseif (MathUtility::canBeInterpretedAsInteger($tagName)) {
1431
                // If integer...;
1432
                if ($options['useNindex']) {
1433
                    // If numeric key, prefix "n"
1434
                    $tagName = 'n' . $tagName;
1435
                } else {
1436
                    // Use special tag for num. keys:
1437
                    $attr .= ' index="' . $tagName . '"';
1438
                    $tagName = $options['useIndexTagForNum'] ?: 'numIndex';
1439
                }
1440
            } elseif (!empty($options['useIndexTagForAssoc'])) {
1441
                // Use tag for all associative keys:
1442
                $attr .= ' index="' . htmlspecialchars($tagName) . '"';
1443
                $tagName = $options['useIndexTagForAssoc'];
1444
            }
1445
            // The tag name is cleaned up so only alphanumeric chars (plus - and _) are in there and not longer than 100 chars either.
1446
            $tagName = substr(preg_replace('/[^[:alnum:]_-]/', '', $tagName), 0, 100);
1447
            // If the value is an array then we will call this function recursively:
1448
            if (is_array($v)) {
1449
                // Sub elements:
1450
                if (isset($options['alt_options']) && $options['alt_options'][($stackData['path'] ?? '') . '/' . $tagName]) {
1451
                    $subOptions = $options['alt_options'][$stackData['path'] . '/' . $tagName];
1452
                    $clearStackPath = $subOptions['clearStackPath'];
1453
                } else {
1454
                    $subOptions = $options;
1455
                    $clearStackPath = false;
1456
                }
1457
                if (empty($v)) {
1458
                    $content = '';
1459
                } else {
1460
                    $content = $nl . self::array2xml($v, $NSprefix, $level + 1, '', $spaceInd, $subOptions, [
1461
                            'parentTagName' => $tagName,
1462
                            'grandParentTagName' => $stackData['parentTagName'] ?? '',
1463
                            'path' => $clearStackPath ? '' : ($stackData['path'] ?? '') . '/' . $tagName
1464
                        ]) . ($spaceInd >= 0 ? str_pad('', ($level + 1) * $indentN, $indentChar) : '');
1465
                }
1466
                // Do not set "type = array". Makes prettier XML but means that empty arrays are not restored with xml2array
1467
                if (!isset($options['disableTypeAttrib']) || (int)$options['disableTypeAttrib'] != 2) {
1468
                    $attr .= ' type="array"';
1469
                }
1470
            } else {
1471
                // Just a value:
1472
                // Look for binary chars:
1473
                $vLen = strlen($v);
1474
                // Go for base64 encoding if the initial segment NOT matching any binary char has the same length as the whole string!
1475
                if ($vLen && strcspn($v, $binaryChars) != $vLen) {
1476
                    // If the value contained binary chars then we base64-encode it and set an attribute to notify this situation:
1477
                    $content = $nl . chunk_split(base64_encode($v));
1478
                    $attr .= ' base64="1"';
1479
                } else {
1480
                    // Otherwise, just htmlspecialchar the stuff:
1481
                    $content = htmlspecialchars($v);
1482
                    $dType = gettype($v);
1483
                    if ($dType === 'string') {
1484
                        if (isset($options['useCDATA']) && $options['useCDATA'] && $content != $v) {
1485
                            $content = '<![CDATA[' . $v . ']]>';
1486
                        }
1487
                    } elseif (!$options['disableTypeAttrib']) {
1488
                        $attr .= ' type="' . $dType . '"';
1489
                    }
1490
                }
1491
            }
1492
            if ((string)$tagName !== '') {
1493
                // Add the element to the output string:
1494
                $output .= ($spaceInd >= 0 ? str_pad('', ($level + 1) * $indentN, $indentChar) : '')
1495
                    . '<' . $NSprefix . $tagName . $attr . '>' . $content . '</' . $NSprefix . $tagName . '>' . $nl;
1496
            }
1497
        }
1498
        // If we are at the outer-most level, then we finally wrap it all in the document tags and return that as the value:
1499
        if (!$level) {
1500
            $output = '<' . $docTag . '>' . $nl . $output . '</' . $docTag . '>';
1501
        }
1502
        return $output;
1503
    }
1504
1505
    /**
1506
     * Converts an XML string to a PHP array.
1507
     * This is the reverse function of array2xml()
1508
     * This is a wrapper for xml2arrayProcess that adds a two-level cache
1509
     *
1510
     * @param string $string XML content to convert into an array
1511
     * @param string $NSprefix The tag-prefix resolve, eg. a namespace like "T3:"
1512
     * @param bool $reportDocTag If set, the document tag will be set in the key "_DOCUMENT_TAG" of the output array
1513
     * @return mixed If the parsing had errors, a string with the error message is returned. Otherwise an array with the content.
1514
     * @see array2xml()
1515
     * @see xml2arrayProcess()
1516
     */
1517
    public static function xml2array($string, $NSprefix = '', $reportDocTag = false)
1518
    {
1519
        $runtimeCache = static::makeInstance(CacheManager::class)->getCache('runtime');
1520
        $firstLevelCache = $runtimeCache->get('generalUtilityXml2Array') ?: [];
1521
        $identifier = md5($string . $NSprefix . ($reportDocTag ? '1' : '0'));
1522
        // Look up in first level cache
1523
        if (empty($firstLevelCache[$identifier])) {
1524
            $firstLevelCache[$identifier] = self::xml2arrayProcess(trim($string), $NSprefix, $reportDocTag);
1525
            $runtimeCache->set('generalUtilityXml2Array', $firstLevelCache);
1526
        }
1527
        return $firstLevelCache[$identifier];
1528
    }
1529
1530
    /**
1531
     * Converts an XML string to a PHP array.
1532
     * This is the reverse function of array2xml()
1533
     *
1534
     * @param string $string XML content to convert into an array
1535
     * @param string $NSprefix The tag-prefix resolve, eg. a namespace like "T3:"
1536
     * @param bool $reportDocTag If set, the document tag will be set in the key "_DOCUMENT_TAG" of the output array
1537
     * @return mixed If the parsing had errors, a string with the error message is returned. Otherwise an array with the content.
1538
     * @see array2xml()
1539
     */
1540
    protected static function xml2arrayProcess($string, $NSprefix = '', $reportDocTag = false)
1541
    {
1542
        // Disables the functionality to allow external entities to be loaded when parsing the XML, must be kept
1543
        $previousValueOfEntityLoader = libxml_disable_entity_loader(true);
1544
        // Create parser:
1545
        $parser = xml_parser_create();
1546
        $vals = [];
1547
        $index = [];
1548
        xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0);
1549
        xml_parser_set_option($parser, XML_OPTION_SKIP_WHITE, 0);
1550
        // Default output charset is UTF-8, only ASCII, ISO-8859-1 and UTF-8 are supported!!!
1551
        $match = [];
1552
        preg_match('/^[[:space:]]*<\\?xml[^>]*encoding[[:space:]]*=[[:space:]]*"([^"]*)"/', substr($string, 0, 200), $match);
1553
        $theCharset = $match[1] ?? 'utf-8';
1554
        // us-ascii / utf-8 / iso-8859-1
1555
        xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, $theCharset);
1556
        // Parse content:
1557
        xml_parse_into_struct($parser, $string, $vals, $index);
1558
        libxml_disable_entity_loader($previousValueOfEntityLoader);
1559
        // If error, return error message:
1560
        if (xml_get_error_code($parser)) {
1561
            return 'Line ' . xml_get_current_line_number($parser) . ': ' . xml_error_string(xml_get_error_code($parser));
1562
        }
1563
        xml_parser_free($parser);
1564
        // Init vars:
1565
        $stack = [[]];
1566
        $stacktop = 0;
1567
        $current = [];
1568
        $tagName = '';
1569
        $documentTag = '';
1570
        // Traverse the parsed XML structure:
1571
        foreach ($vals as $key => $val) {
1572
            // First, process the tag-name (which is used in both cases, whether "complete" or "close")
1573
            $tagName = $val['tag'];
1574
            if (!$documentTag) {
1575
                $documentTag = $tagName;
1576
            }
1577
            // Test for name space:
1578
            $tagName = $NSprefix && strpos($tagName, $NSprefix) === 0 ? substr($tagName, strlen($NSprefix)) : $tagName;
1579
            // Test for numeric tag, encoded on the form "nXXX":
1580
            $testNtag = substr($tagName, 1);
1581
            // Closing tag.
1582
            $tagName = $tagName[0] === 'n' && MathUtility::canBeInterpretedAsInteger($testNtag) ? (int)$testNtag : $tagName;
1583
            // Test for alternative index value:
1584
            if ((string)($val['attributes']['index'] ?? '') !== '') {
1585
                $tagName = $val['attributes']['index'];
1586
            }
1587
            // Setting tag-values, manage stack:
1588
            switch ($val['type']) {
1589
                case 'open':
1590
                    // If open tag it means there is an array stored in sub-elements. Therefore increase the stackpointer and reset the accumulation array:
1591
                    // Setting blank place holder
1592
                    $current[$tagName] = [];
1593
                    $stack[$stacktop++] = $current;
1594
                    $current = [];
1595
                    break;
1596
                case 'close':
1597
                    // If the tag is "close" then it is an array which is closing and we decrease the stack pointer.
1598
                    $oldCurrent = $current;
1599
                    $current = $stack[--$stacktop];
1600
                    // Going to the end of array to get placeholder key, key($current), and fill in array next:
1601
                    end($current);
1602
                    $current[key($current)] = $oldCurrent;
1603
                    unset($oldCurrent);
1604
                    break;
1605
                case 'complete':
1606
                    // If "complete", then it's a value. If the attribute "base64" is set, then decode the value, otherwise just set it.
1607
                    if (!empty($val['attributes']['base64'])) {
1608
                        $current[$tagName] = base64_decode($val['value']);
1609
                    } else {
1610
                        // Had to cast it as a string - otherwise it would be evaluate FALSE if tested with isset()!!
1611
                        $current[$tagName] = (string)($val['value'] ?? '');
1612
                        // Cast type:
1613
                        switch ((string)($val['attributes']['type'] ?? '')) {
1614
                            case 'integer':
1615
                                $current[$tagName] = (int)$current[$tagName];
1616
                                break;
1617
                            case 'double':
1618
                                $current[$tagName] = (double)$current[$tagName];
1619
                                break;
1620
                            case 'boolean':
1621
                                $current[$tagName] = (bool)$current[$tagName];
1622
                                break;
1623
                            case 'NULL':
1624
                                $current[$tagName] = null;
1625
                                break;
1626
                            case 'array':
1627
                                // 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...
1628
                                $current[$tagName] = [];
1629
                                break;
1630
                        }
1631
                    }
1632
                    break;
1633
            }
1634
        }
1635
        if ($reportDocTag) {
1636
            $current[$tagName]['_DOCUMENT_TAG'] = $documentTag;
1637
        }
1638
        // Finally return the content of the document tag.
1639
        return $current[$tagName];
1640
    }
1641
1642
    /**
1643
     * This implodes an array of XML parts (made with xml_parse_into_struct()) into XML again.
1644
     *
1645
     * @param array $vals An array of XML parts, see xml2tree
1646
     * @return string Re-compiled XML data.
1647
     */
1648
    public static function xmlRecompileFromStructValArray(array $vals)
1649
    {
1650
        $XMLcontent = '';
1651
        foreach ($vals as $val) {
1652
            $type = $val['type'];
1653
            // Open tag:
1654
            if ($type === 'open' || $type === 'complete') {
1655
                $XMLcontent .= '<' . $val['tag'];
1656
                if (isset($val['attributes'])) {
1657
                    foreach ($val['attributes'] as $k => $v) {
1658
                        $XMLcontent .= ' ' . $k . '="' . htmlspecialchars($v) . '"';
1659
                    }
1660
                }
1661
                if ($type === 'complete') {
1662
                    if (isset($val['value'])) {
1663
                        $XMLcontent .= '>' . htmlspecialchars($val['value']) . '</' . $val['tag'] . '>';
1664
                    } else {
1665
                        $XMLcontent .= '/>';
1666
                    }
1667
                } else {
1668
                    $XMLcontent .= '>';
1669
                }
1670
                if ($type === 'open' && isset($val['value'])) {
1671
                    $XMLcontent .= htmlspecialchars($val['value']);
1672
                }
1673
            }
1674
            // Finish tag:
1675
            if ($type === 'close') {
1676
                $XMLcontent .= '</' . $val['tag'] . '>';
1677
            }
1678
            // Cdata
1679
            if ($type === 'cdata') {
1680
                $XMLcontent .= htmlspecialchars($val['value']);
1681
            }
1682
        }
1683
        return $XMLcontent;
1684
    }
1685
1686
    /**
1687
     * Minifies JavaScript
1688
     *
1689
     * @param string $script Script to minify
1690
     * @param string $error Error message (if any)
1691
     * @return string Minified script or source string if error happened
1692
     */
1693
    public static function minifyJavaScript($script, &$error = '')
1694
    {
1695
        $fakeThis = false;
1696
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_div.php']['minifyJavaScript'] ?? [] as $hookMethod) {
1697
            try {
1698
                $parameters = ['script' => $script];
1699
                $script = static::callUserFunction($hookMethod, $parameters, $fakeThis);
1700
            } catch (\Exception $e) {
1701
                $errorMessage = 'Error minifying java script: ' . $e->getMessage();
1702
                $error .= $errorMessage;
1703
                static::getLogger()->warning($errorMessage, [
1704
                    'JavaScript' => $script,
1705
                    'hook' => $hookMethod,
1706
                    'exception' => $e,
1707
                ]);
1708
            }
1709
        }
1710
        return $script;
1711
    }
1712
1713
    /*************************
1714
     *
1715
     * FILES FUNCTIONS
1716
     *
1717
     *************************/
1718
    /**
1719
     * Reads the file or url $url and returns the content
1720
     * 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'].
1721
     *
1722
     * @param string $url File/URL to read
1723
     * @param int $includeHeader Whether the HTTP header should be fetched or not. 0=disable, 1=fetch header+content, 2=fetch header only
1724
     * @param array $requestHeaders HTTP headers to be used in the request
1725
     * @param array $report Error code/message and, if $includeHeader is 1, response meta data (HTTP status and content type)
1726
     * @return mixed The content from the resource given as input. FALSE if an error has occurred.
1727
     */
1728
    public static function getUrl($url, $includeHeader = 0, $requestHeaders = null, &$report = null)
1729
    {
1730
        if (isset($report)) {
1731
            $report['error'] = 0;
1732
            $report['message'] = '';
1733
        }
1734
        // Looks like it's an external file, use Guzzle by default
1735
        if (preg_match('/^(?:http|ftp)s?|s(?:ftp|cp):/', $url)) {
1736
            $requestFactory = static::makeInstance(RequestFactory::class);
1737
            if (is_array($requestHeaders)) {
1738
                $configuration = ['headers' => $requestHeaders];
1739
            } else {
1740
                $configuration = [];
1741
            }
1742
            $includeHeader = (int)$includeHeader;
1743
            $method = $includeHeader === 2 ? 'HEAD' : 'GET';
1744
            try {
1745
                if (isset($report)) {
1746
                    $report['lib'] = 'GuzzleHttp';
1747
                }
1748
                $response = $requestFactory->request($url, $method, $configuration);
1749
            } catch (RequestException $exception) {
1750
                if (isset($report)) {
1751
                    $report['error'] = $exception->getCode() ?: 1518707554;
1752
                    $report['message'] = $exception->getMessage();
1753
                    $report['exception'] = $exception;
1754
                }
1755
                return false;
1756
            }
1757
            $content = '';
1758
            // Add the headers to the output
1759
            if ($includeHeader) {
1760
                $parsedURL = parse_url($url);
1761
                $content = $method . ' ' . ($parsedURL['path'] ?? '/')
1762
                    . (!empty($parsedURL['query']) ? '?' . $parsedURL['query'] : '') . ' HTTP/1.0' . CRLF
1763
                    . 'Host: ' . $parsedURL['host'] . CRLF
1764
                    . 'Connection: close' . CRLF;
1765
                if (is_array($requestHeaders)) {
1766
                    $content .= implode(CRLF, $requestHeaders) . CRLF;
1767
                }
1768
                foreach ($response->getHeaders() as $headerName => $headerValues) {
1769
                    $content .= $headerName . ': ' . implode(', ', $headerValues) . CRLF;
1770
                }
1771
                // Headers are separated from the body with two CRLFs
1772
                $content .= CRLF;
1773
            }
1774
1775
            $content .= $response->getBody()->getContents();
1776
1777
            if (isset($report)) {
1778
                if ($response->getStatusCode() >= 300 && $response->getStatusCode() < 400) {
1779
                    $report['http_code'] = $response->getStatusCode();
1780
                    $report['content_type'] = $response->getHeaderLine('Content-Type');
1781
                    $report['error'] = $response->getStatusCode();
1782
                    $report['message'] = $response->getReasonPhrase();
1783
                } elseif (empty($content)) {
1784
                    $report['error'] = $response->getStatusCode();
1785
                    $report['message'] = $response->getReasonPhrase();
1786
                } elseif ($includeHeader) {
1787
                    // Set only for $includeHeader to work exactly like PHP variant
1788
                    $report['http_code'] = $response->getStatusCode();
1789
                    $report['content_type'] = $response->getHeaderLine('Content-Type');
1790
                }
1791
            }
1792
        } else {
1793
            if (isset($report)) {
1794
                $report['lib'] = 'file';
1795
            }
1796
            $content = @file_get_contents($url);
1797
            if ($content === false && isset($report)) {
1798
                $report['error'] = -1;
1799
                $report['message'] = 'Couldn\'t get URL: ' . $url;
1800
            }
1801
        }
1802
        return $content;
1803
    }
1804
1805
    /**
1806
     * Writes $content to the file $file
1807
     *
1808
     * @param string $file Filepath to write to
1809
     * @param string $content Content to write
1810
     * @param bool $changePermissions If TRUE, permissions are forced to be set
1811
     * @return bool TRUE if the file was successfully opened and written to.
1812
     */
1813
    public static function writeFile($file, $content, $changePermissions = false)
1814
    {
1815
        if (!@is_file($file)) {
1816
            $changePermissions = true;
1817
        }
1818
        if ($fd = fopen($file, 'wb')) {
1819
            $res = fwrite($fd, $content);
1820
            fclose($fd);
1821
            if ($res === false) {
1822
                return false;
1823
            }
1824
            // Change the permissions only if the file has just been created
1825
            if ($changePermissions) {
1826
                static::fixPermissions($file);
1827
            }
1828
            return true;
1829
        }
1830
        return false;
1831
    }
1832
1833
    /**
1834
     * Sets the file system mode and group ownership of a file or a folder.
1835
     *
1836
     * @param string $path Path of file or folder, must not be escaped. Path can be absolute or relative
1837
     * @param bool $recursive If set, also fixes permissions of files and folders in the folder (if $path is a folder)
1838
     * @return mixed TRUE on success, FALSE on error, always TRUE on Windows OS
1839
     */
1840
    public static function fixPermissions($path, $recursive = false)
1841
    {
1842
        $targetPermissions = null;
1843
        if (Environment::isWindows()) {
1844
            return true;
1845
        }
1846
        $result = false;
1847
        // Make path absolute
1848
        if (!static::isAbsPath($path)) {
1849
            $path = static::getFileAbsFileName($path);
1850
        }
1851
        if (static::isAllowedAbsPath($path)) {
1852
            if (@is_file($path)) {
1853
                $targetPermissions = $GLOBALS['TYPO3_CONF_VARS']['SYS']['fileCreateMask'] ?? '0644';
1854
            } elseif (@is_dir($path)) {
1855
                $targetPermissions = $GLOBALS['TYPO3_CONF_VARS']['SYS']['folderCreateMask'] ?? '0755';
1856
            }
1857
            if (!empty($targetPermissions)) {
1858
                // make sure it's always 4 digits
1859
                $targetPermissions = str_pad($targetPermissions, 4, 0, STR_PAD_LEFT);
1860
                $targetPermissions = octdec($targetPermissions);
1861
                // "@" is there because file is not necessarily OWNED by the user
1862
                $result = @chmod($path, $targetPermissions);
0 ignored issues
show
Bug introduced by
It seems like $targetPermissions can also be of type double; however, parameter $mode of chmod() does only seem to accept integer, 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

1862
                $result = @chmod($path, /** @scrutinizer ignore-type */ $targetPermissions);
Loading history...
1863
            }
1864
            // Set createGroup if not empty
1865
            if (
1866
                isset($GLOBALS['TYPO3_CONF_VARS']['SYS']['createGroup'])
1867
                && $GLOBALS['TYPO3_CONF_VARS']['SYS']['createGroup'] !== ''
1868
            ) {
1869
                // "@" is there because file is not necessarily OWNED by the user
1870
                $changeGroupResult = @chgrp($path, $GLOBALS['TYPO3_CONF_VARS']['SYS']['createGroup']);
1871
                $result = $changeGroupResult ? $result : false;
1872
            }
1873
            // Call recursive if recursive flag if set and $path is directory
1874
            if ($recursive && @is_dir($path)) {
1875
                $handle = opendir($path);
1876
                if (is_resource($handle)) {
1877
                    while (($file = readdir($handle)) !== false) {
1878
                        $recursionResult = null;
1879
                        if ($file !== '.' && $file !== '..') {
1880
                            if (@is_file($path . '/' . $file)) {
1881
                                $recursionResult = static::fixPermissions($path . '/' . $file);
1882
                            } elseif (@is_dir($path . '/' . $file)) {
1883
                                $recursionResult = static::fixPermissions($path . '/' . $file, true);
1884
                            }
1885
                            if (isset($recursionResult) && !$recursionResult) {
1886
                                $result = false;
1887
                            }
1888
                        }
1889
                    }
1890
                    closedir($handle);
1891
                }
1892
            }
1893
        }
1894
        return $result;
1895
    }
1896
1897
    /**
1898
     * Writes $content to a filename in the typo3temp/ folder (and possibly one or two subfolders...)
1899
     * Accepts an additional subdirectory in the file path!
1900
     *
1901
     * @param string $filepath Absolute file path to write within the typo3temp/ or Environment::getVarPath() folder - the file path must be prefixed with this path
1902
     * @param string $content Content string to write
1903
     * @return string Returns NULL on success, otherwise an error string telling about the problem.
1904
     */
1905
    public static function writeFileToTypo3tempDir($filepath, $content)
1906
    {
1907
        // Parse filepath into directory and basename:
1908
        $fI = pathinfo($filepath);
1909
        $fI['dirname'] .= '/';
1910
        // Check parts:
1911
        if (!static::validPathStr($filepath) || !$fI['basename'] || strlen($fI['basename']) >= 60) {
1912
            return 'Input filepath "' . $filepath . '" was generally invalid!';
1913
        }
1914
1915
        // Setting main temporary directory name (standard)
1916
        $allowedPathPrefixes = [
1917
            Environment::getPublicPath() . '/typo3temp' => 'Environment::getPublicPath() + "/typo3temp/"'
1918
        ];
1919
        // Also allow project-path + /var/
1920
        if (Environment::getVarPath() !== Environment::getPublicPath() . '/typo3temp/var') {
1921
            $relPath = substr(Environment::getVarPath(), strlen(Environment::getProjectPath()) + 1);
1922
            $allowedPathPrefixes[Environment::getVarPath()] = 'ProjectPath + ' . $relPath;
1923
        }
1924
1925
        $errorMessage = null;
1926
        foreach ($allowedPathPrefixes as $pathPrefix => $prefixLabel) {
1927
            $dirName = $pathPrefix . '/';
1928
            // Invalid file path, let's check for the other path, if it exists
1929
            if (!static::isFirstPartOfStr($fI['dirname'], $dirName)) {
1930
                if ($errorMessage === null) {
1931
                    $errorMessage = '"' . $fI['dirname'] . '" was not within directory ' . $prefixLabel;
1932
                }
1933
                continue;
1934
            }
1935
            // This resets previous error messages from the first path
1936
            $errorMessage = null;
1937
1938
            if (!@is_dir($dirName)) {
1939
                $errorMessage = $prefixLabel . ' was not a directory!';
1940
                // continue and see if the next iteration resets the errorMessage above
1941
                continue;
1942
            }
1943
            // Checking if the "subdir" is found
1944
            $subdir = substr($fI['dirname'], strlen($dirName));
1945
            if ($subdir) {
1946
                if (preg_match('#^(?:[[:alnum:]_]+/)+$#', $subdir)) {
1947
                    $dirName .= $subdir;
1948
                    if (!@is_dir($dirName)) {
1949
                        static::mkdir_deep($pathPrefix . '/' . $subdir);
1950
                    }
1951
                } else {
1952
                    $errorMessage = 'Subdir, "' . $subdir . '", was NOT on the form "[[:alnum:]_]/+"';
1953
                    break;
1954
                }
1955
            }
1956
            // Checking dir-name again (sub-dir might have been created)
1957
            if (@is_dir($dirName)) {
1958
                if ($filepath === $dirName . $fI['basename']) {
1959
                    static::writeFile($filepath, $content);
1960
                    if (!@is_file($filepath)) {
1961
                        $errorMessage = 'The file was not written to the disk. Please, check that you have write permissions to the ' . $prefixLabel . ' directory.';
1962
                        break;
1963
                    }
1964
                } else {
1965
                    $errorMessage = 'Calculated file location didn\'t match input "' . $filepath . '".';
1966
                    break;
1967
                }
1968
            } else {
1969
                $errorMessage = '"' . $dirName . '" is not a directory!';
1970
                break;
1971
            }
1972
        }
1973
        return $errorMessage;
1974
    }
1975
1976
    /**
1977
     * Wrapper function for mkdir.
1978
     * Sets folder permissions according to $GLOBALS['TYPO3_CONF_VARS']['SYS']['folderCreateMask']
1979
     * and group ownership according to $GLOBALS['TYPO3_CONF_VARS']['SYS']['createGroup']
1980
     *
1981
     * @param string $newFolder Absolute path to folder, see PHP mkdir() function. Removes trailing slash internally.
1982
     * @return bool TRUE if operation was successful
1983
     */
1984
    public static function mkdir($newFolder)
1985
    {
1986
        $result = @mkdir($newFolder, octdec($GLOBALS['TYPO3_CONF_VARS']['SYS']['folderCreateMask']));
0 ignored issues
show
Bug introduced by
It seems like octdec($GLOBALS['TYPO3_C...']['folderCreateMask']) can also be of type double; however, parameter $mode of mkdir() does only seem to accept integer, 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

1986
        $result = @mkdir($newFolder, /** @scrutinizer ignore-type */ octdec($GLOBALS['TYPO3_CONF_VARS']['SYS']['folderCreateMask']));
Loading history...
1987
        if ($result) {
1988
            static::fixPermissions($newFolder);
1989
        }
1990
        return $result;
1991
    }
1992
1993
    /**
1994
     * Creates a directory - including parent directories if necessary and
1995
     * sets permissions on newly created directories.
1996
     *
1997
     * @param string $directory Target directory to create. Must a have trailing slash
1998
     * @throws \InvalidArgumentException If $directory or $deepDirectory are not strings
1999
     * @throws \RuntimeException If directory could not be created
2000
     */
2001
    public static function mkdir_deep($directory)
0 ignored issues
show
Coding Style introduced by
Method name "GeneralUtility::mkdir_deep" is not in camel caps format
Loading history...
2002
    {
2003
        if (!is_string($directory)) {
0 ignored issues
show
introduced by
The condition is_string($directory) is always true.
Loading history...
2004
            throw new \InvalidArgumentException('The specified directory is of type "' . gettype($directory) . '" but a string is expected.', 1303662955);
2005
        }
2006
        // Ensure there is only one slash
2007
        $fullPath = rtrim($directory, '/') . '/';
2008
        if ($fullPath !== '/' && !is_dir($fullPath)) {
2009
            $firstCreatedPath = static::createDirectoryPath($fullPath);
2010
            if ($firstCreatedPath !== '') {
2011
                static::fixPermissions($firstCreatedPath, true);
2012
            }
2013
        }
2014
    }
2015
2016
    /**
2017
     * Creates directories for the specified paths if they do not exist. This
2018
     * functions sets proper permission mask but does not set proper user and
2019
     * group.
2020
     *
2021
     * @static
2022
     * @param string $fullDirectoryPath
2023
     * @return string Path to the the first created directory in the hierarchy
2024
     * @see \TYPO3\CMS\Core\Utility\GeneralUtility::mkdir_deep
2025
     * @throws \RuntimeException If directory could not be created
2026
     */
2027
    protected static function createDirectoryPath($fullDirectoryPath)
2028
    {
2029
        $currentPath = $fullDirectoryPath;
2030
        $firstCreatedPath = '';
2031
        $permissionMask = octdec($GLOBALS['TYPO3_CONF_VARS']['SYS']['folderCreateMask']);
2032
        if (!@is_dir($currentPath)) {
2033
            do {
2034
                $firstCreatedPath = $currentPath;
2035
                $separatorPosition = strrpos($currentPath, DIRECTORY_SEPARATOR);
2036
                $currentPath = substr($currentPath, 0, $separatorPosition);
2037
            } while (!is_dir($currentPath) && $separatorPosition !== false);
2038
            $result = @mkdir($fullDirectoryPath, $permissionMask, true);
0 ignored issues
show
Bug introduced by
It seems like $permissionMask can also be of type double; however, parameter $mode of mkdir() does only seem to accept integer, 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

2038
            $result = @mkdir($fullDirectoryPath, /** @scrutinizer ignore-type */ $permissionMask, true);
Loading history...
2039
            // Check existence of directory again to avoid race condition. Directory could have get created by another process between previous is_dir() and mkdir()
2040
            if (!$result && !@is_dir($fullDirectoryPath)) {
2041
                throw new \RuntimeException('Could not create directory "' . $fullDirectoryPath . '"!', 1170251401);
2042
            }
2043
        }
2044
        return $firstCreatedPath;
2045
    }
2046
2047
    /**
2048
     * Wrapper function for rmdir, allowing recursive deletion of folders and files
2049
     *
2050
     * @param string $path Absolute path to folder, see PHP rmdir() function. Removes trailing slash internally.
2051
     * @param bool $removeNonEmpty Allow deletion of non-empty directories
2052
     * @return bool TRUE if operation was successful
2053
     */
2054
    public static function rmdir($path, $removeNonEmpty = false)
2055
    {
2056
        $OK = false;
2057
        // Remove trailing slash
2058
        $path = preg_replace('|/$|', '', $path);
2059
        $isWindows = DIRECTORY_SEPARATOR === '\\';
2060
        if (file_exists($path)) {
2061
            $OK = true;
2062
            if (!is_link($path) && is_dir($path)) {
2063
                if ($removeNonEmpty === true && ($handle = @opendir($path))) {
2064
                    $entries = [];
2065
2066
                    while (false !== ($file = readdir($handle))) {
2067
                        if ($file === '.' || $file === '..') {
2068
                            continue;
2069
                        }
2070
2071
                        $entries[] = $path . '/' . $file;
2072
                    }
2073
2074
                    closedir($handle);
2075
2076
                    foreach ($entries as $entry) {
2077
                        if (!static::rmdir($entry, $removeNonEmpty)) {
2078
                            $OK = false;
2079
                        }
2080
                    }
2081
                }
2082
                if ($OK) {
2083
                    $OK = @rmdir($path);
2084
                }
2085
            } elseif (is_link($path) && is_dir($path) && $isWindows) {
2086
                $OK = @rmdir($path);
2087
            } else {
2088
                // If $path is a file, simply remove it
2089
                $OK = @unlink($path);
2090
            }
2091
            clearstatcache();
2092
        } elseif (is_link($path)) {
2093
            $OK = @unlink($path);
2094
            if (!$OK && $isWindows) {
2095
                // Try to delete dead folder links on Windows systems
2096
                $OK = @rmdir($path);
2097
            }
2098
            clearstatcache();
2099
        }
2100
        return $OK;
2101
    }
2102
2103
    /**
2104
     * Flushes a directory by first moving to a temporary resource, and then
2105
     * triggering the remove process. This way directories can be flushed faster
2106
     * to prevent race conditions on concurrent processes accessing the same directory.
2107
     *
2108
     * @param string $directory The directory to be renamed and flushed
2109
     * @param bool $keepOriginalDirectory Whether to only empty the directory and not remove it
2110
     * @param bool $flushOpcodeCache Also flush the opcode cache right after renaming the directory.
2111
     * @return bool Whether the action was successful
2112
     */
2113
    public static function flushDirectory($directory, $keepOriginalDirectory = false, $flushOpcodeCache = false)
2114
    {
2115
        $result = false;
2116
2117
        if (is_link($directory)) {
2118
            // Avoid attempting to rename the symlink see #87367
2119
            $directory = realpath($directory);
2120
        }
2121
2122
        if (is_dir($directory)) {
2123
            $temporaryDirectory = rtrim($directory, '/') . '.' . StringUtility::getUniqueId('remove');
2124
            if (rename($directory, $temporaryDirectory)) {
2125
                if ($flushOpcodeCache) {
2126
                    self::makeInstance(OpcodeCacheService::class)->clearAllActive($directory);
2127
                }
2128
                if ($keepOriginalDirectory) {
2129
                    static::mkdir($directory);
2130
                }
2131
                clearstatcache();
2132
                $result = static::rmdir($temporaryDirectory, true);
2133
            }
2134
        }
2135
2136
        return $result;
2137
    }
2138
2139
    /**
2140
     * Returns an array with the names of folders in a specific path
2141
     * Will return 'error' (string) if there were an error with reading directory content.
2142
     * Will return null if provided path is false.
2143
     *
2144
     * @param string $path Path to list directories from
2145
     * @return array|string|null Returns an array with the directory entries as values. If no path is provided, the return value will be null.
2146
     */
2147
    public static function get_dirs($path)
0 ignored issues
show
Coding Style introduced by
Method name "GeneralUtility::get_dirs" is not in camel caps format
Loading history...
2148
    {
2149
        $dirs = null;
2150
        if ($path) {
2151
            if (is_dir($path)) {
2152
                $dir = scandir($path);
2153
                $dirs = [];
2154
                foreach ($dir as $entry) {
2155
                    if (is_dir($path . '/' . $entry) && $entry !== '..' && $entry !== '.') {
2156
                        $dirs[] = $entry;
2157
                    }
2158
                }
2159
            } else {
2160
                $dirs = 'error';
2161
            }
2162
        }
2163
        return $dirs;
2164
    }
2165
2166
    /**
2167
     * Finds all files in a given path and returns them as an array. Each
2168
     * array key is a md5 hash of the full path to the file. This is done because
2169
     * 'some' extensions like the import/export extension depend on this.
2170
     *
2171
     * @param string $path The path to retrieve the files from.
2172
     * @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.
2173
     * @param bool $prependPath If TRUE, the full path to the file is returned. If FALSE only the file name is returned.
2174
     * @param string $order The sorting order. The default sorting order is alphabetical. Setting $order to 'mtime' will sort the files by modification time.
2175
     * @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 '$/'.
2176
     * @return array|string Array of the files found, or an error message in case the path could not be opened.
2177
     */
2178
    public static function getFilesInDir($path, $extensionList = '', $prependPath = false, $order = '', $excludePattern = '')
2179
    {
2180
        $excludePattern = (string)$excludePattern;
2181
        $path = rtrim($path, '/');
2182
        if (!@is_dir($path)) {
2183
            return [];
2184
        }
2185
2186
        $rawFileList = scandir($path);
2187
        if ($rawFileList === false) {
2188
            return 'error opening path: "' . $path . '"';
2189
        }
2190
2191
        $pathPrefix = $path . '/';
2192
        $allowedFileExtensionArray = self::trimExplode(',', $extensionList);
2193
        $extensionList = ',' . str_replace(' ', '', $extensionList) . ',';
2194
        $files = [];
2195
        foreach ($rawFileList as $entry) {
2196
            $completePathToEntry = $pathPrefix . $entry;
2197
            if (!@is_file($completePathToEntry)) {
2198
                continue;
2199
            }
2200
2201
            foreach ($allowedFileExtensionArray as $allowedFileExtension) {
2202
                if (
2203
                    ($extensionList === ',,' || stripos($extensionList, ',' . substr($entry, strlen($allowedFileExtension) * -1, strlen($allowedFileExtension)) . ',') !== false)
2204
                    && ($excludePattern === '' || !preg_match('/^' . $excludePattern . '$/', $entry))
2205
                ) {
2206
                    if ($order !== 'mtime') {
2207
                        $files[] = $entry;
2208
                    } else {
2209
                        // Store the value in the key so we can do a fast asort later.
2210
                        $files[$entry] = filemtime($completePathToEntry);
2211
                    }
2212
                }
2213
            }
2214
        }
2215
2216
        $valueName = 'value';
2217
        if ($order === 'mtime') {
2218
            asort($files);
2219
            $valueName = 'key';
2220
        }
2221
2222
        $valuePathPrefix = $prependPath ? $pathPrefix : '';
2223
        $foundFiles = [];
2224
        foreach ($files as $key => $value) {
2225
            // Don't change this ever - extensions may depend on the fact that the hash is an md5 of the path! (import/export extension)
2226
            $foundFiles[md5($pathPrefix . ${$valueName})] = $valuePathPrefix . ${$valueName};
2227
        }
2228
2229
        return $foundFiles;
2230
    }
2231
2232
    /**
2233
     * Recursively gather all files and folders of a path.
2234
     *
2235
     * @param array $fileArr Empty input array (will have files added to it)
2236
     * @param string $path The path to read recursively from (absolute) (include trailing slash!)
2237
     * @param string $extList Comma list of file extensions: Only files with extensions in this list (if applicable) will be selected.
2238
     * @param bool $regDirs If set, directories are also included in output.
2239
     * @param int $recursivityLevels The number of levels to dig down...
2240
     * @param string $excludePattern regex pattern of files/directories to exclude
2241
     * @return array An array with the found files/directories.
2242
     */
2243
    public static function getAllFilesAndFoldersInPath(array $fileArr, $path, $extList = '', $regDirs = false, $recursivityLevels = 99, $excludePattern = '')
2244
    {
2245
        if ($regDirs) {
2246
            $fileArr[md5($path)] = $path;
2247
        }
2248
        $fileArr = array_merge($fileArr, self::getFilesInDir($path, $extList, 1, 1, $excludePattern));
0 ignored issues
show
Bug introduced by
It seems like self::getFilesInDir($pat... 1, 1, $excludePattern) can also be of type string; however, parameter $array2 of array_merge() does only seem to accept array|null, 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

2248
        $fileArr = array_merge($fileArr, /** @scrutinizer ignore-type */ self::getFilesInDir($path, $extList, 1, 1, $excludePattern));
Loading history...
2249
        $dirs = self::get_dirs($path);
2250
        if ($recursivityLevels > 0 && is_array($dirs)) {
2251
            foreach ($dirs as $subdirs) {
2252
                if ((string)$subdirs !== '' && ($excludePattern === '' || !preg_match('/^' . $excludePattern . '$/', $subdirs))) {
2253
                    $fileArr = self::getAllFilesAndFoldersInPath($fileArr, $path . $subdirs . '/', $extList, $regDirs, $recursivityLevels - 1, $excludePattern);
2254
                }
2255
            }
2256
        }
2257
        return $fileArr;
2258
    }
2259
2260
    /**
2261
     * Removes the absolute part of all files/folders in fileArr
2262
     *
2263
     * @param array $fileArr The file array to remove the prefix from
2264
     * @param string $prefixToRemove The prefix path to remove (if found as first part of string!)
2265
     * @return array|string The input $fileArr processed, or a string with an error message, when an error occurred.
2266
     */
2267
    public static function removePrefixPathFromList(array $fileArr, $prefixToRemove)
2268
    {
2269
        foreach ($fileArr as $k => &$absFileRef) {
2270
            if (self::isFirstPartOfStr($absFileRef, $prefixToRemove)) {
2271
                $absFileRef = substr($absFileRef, strlen($prefixToRemove));
2272
            } else {
2273
                return 'ERROR: One or more of the files was NOT prefixed with the prefix-path!';
2274
            }
2275
        }
2276
        unset($absFileRef);
2277
        return $fileArr;
2278
    }
2279
2280
    /**
2281
     * Fixes a path for windows-backslashes and reduces double-slashes to single slashes
2282
     *
2283
     * @param string $theFile File path to process
2284
     * @return string
2285
     */
2286
    public static function fixWindowsFilePath($theFile)
2287
    {
2288
        return str_replace(['\\', '//'], '/', $theFile);
2289
    }
2290
2291
    /**
2292
     * Resolves "../" sections in the input path string.
2293
     * For example "fileadmin/directory/../other_directory/" will be resolved to "fileadmin/other_directory/"
2294
     *
2295
     * @param string $pathStr File path in which "/../" is resolved
2296
     * @return string
2297
     */
2298
    public static function resolveBackPath($pathStr)
2299
    {
2300
        if (strpos($pathStr, '..') === false) {
2301
            return $pathStr;
2302
        }
2303
        $parts = explode('/', $pathStr);
2304
        $output = [];
2305
        $c = 0;
2306
        foreach ($parts as $part) {
2307
            if ($part === '..') {
2308
                if ($c) {
2309
                    array_pop($output);
2310
                    --$c;
2311
                } else {
2312
                    $output[] = $part;
2313
                }
2314
            } else {
2315
                ++$c;
2316
                $output[] = $part;
2317
            }
2318
        }
2319
        return implode('/', $output);
2320
    }
2321
2322
    /**
2323
     * Prefixes a URL used with 'header-location' with 'http://...' depending on whether it has it already.
2324
     * - If already having a scheme, nothing is prepended
2325
     * - If having REQUEST_URI slash '/', then prefixing 'http://[host]' (relative to host)
2326
     * - Otherwise prefixed with TYPO3_REQUEST_DIR (relative to current dir / TYPO3_REQUEST_DIR)
2327
     *
2328
     * @param string $path URL / path to prepend full URL addressing to.
2329
     * @return string
2330
     */
2331
    public static function locationHeaderUrl($path)
2332
    {
2333
        if (strpos($path, '//') === 0) {
2334
            return $path;
2335
        }
2336
2337
        // relative to HOST
2338
        if (strpos($path, '/') === 0) {
2339
            return self::getIndpEnv('TYPO3_REQUEST_HOST') . $path;
2340
        }
2341
2342
        $urlComponents = parse_url($path);
2343
        if (!($urlComponents['scheme'] ?? false)) {
2344
            // No scheme either
2345
            return self::getIndpEnv('TYPO3_REQUEST_DIR') . $path;
2346
        }
2347
2348
        return $path;
2349
    }
2350
2351
    /**
2352
     * Returns the maximum upload size for a file that is allowed. Measured in KB.
2353
     * This might be handy to find out the real upload limit that is possible for this
2354
     * TYPO3 installation.
2355
     *
2356
     * @return int The maximum size of uploads that are allowed (measured in kilobytes)
2357
     */
2358
    public static function getMaxUploadFileSize()
2359
    {
2360
        // Check for PHP restrictions of the maximum size of one of the $_FILES
2361
        $phpUploadLimit = self::getBytesFromSizeMeasurement(ini_get('upload_max_filesize'));
2362
        // Check for PHP restrictions of the maximum $_POST size
2363
        $phpPostLimit = self::getBytesFromSizeMeasurement(ini_get('post_max_size'));
2364
        // If the total amount of post data is smaller (!) than the upload_max_filesize directive,
2365
        // then this is the real limit in PHP
2366
        $phpUploadLimit = $phpPostLimit > 0 && $phpPostLimit < $phpUploadLimit ? $phpPostLimit : $phpUploadLimit;
2367
        return floor($phpUploadLimit) / 1024;
2368
    }
2369
2370
    /**
2371
     * Gets the bytes value from a measurement string like "100k".
2372
     *
2373
     * @param string $measurement The measurement (e.g. "100k")
2374
     * @return int The bytes value (e.g. 102400)
2375
     */
2376
    public static function getBytesFromSizeMeasurement($measurement)
2377
    {
2378
        $bytes = (float)$measurement;
2379
        if (stripos($measurement, 'G')) {
2380
            $bytes *= 1024 * 1024 * 1024;
2381
        } elseif (stripos($measurement, 'M')) {
2382
            $bytes *= 1024 * 1024;
2383
        } elseif (stripos($measurement, 'K')) {
2384
            $bytes *= 1024;
2385
        }
2386
        return $bytes;
2387
    }
2388
2389
    /**
2390
     * Function for static version numbers on files, based on the filemtime
2391
     *
2392
     * This will make the filename automatically change when a file is
2393
     * changed, and by that re-cached by the browser. If the file does not
2394
     * exist physically the original file passed to the function is
2395
     * returned without the timestamp.
2396
     *
2397
     * Behaviour is influenced by the setting
2398
     * TYPO3_CONF_VARS[TYPO3_MODE][versionNumberInFilename]
2399
     * = TRUE (BE) / "embed" (FE) : modify filename
2400
     * = FALSE (BE) / "querystring" (FE) : add timestamp as parameter
2401
     *
2402
     * @param string $file Relative path to file including all potential query parameters (not htmlspecialchared yet)
2403
     * @return string Relative path with version filename including the timestamp
2404
     */
2405
    public static function createVersionNumberedFilename($file)
2406
    {
2407
        $lookupFile = explode('?', $file);
2408
        $path = self::resolveBackPath(self::dirname(Environment::getCurrentScript()) . '/' . $lookupFile[0]);
2409
2410
        $doNothing = false;
2411
        if (TYPO3_MODE === 'FE') {
0 ignored issues
show
introduced by
The condition TYPO3\CMS\Core\Utility\TYPO3_MODE === 'FE' is always false.
Loading history...
2412
            $mode = strtolower($GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['versionNumberInFilename']);
2413
            if ($mode === 'embed') {
2414
                $mode = true;
2415
            } else {
2416
                if ($mode === 'querystring') {
2417
                    $mode = false;
2418
                } else {
2419
                    $doNothing = true;
2420
                }
2421
            }
2422
        } else {
2423
            $mode = $GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['versionNumberInFilename'];
2424
        }
2425
        if ($doNothing || !file_exists($path)) {
2426
            // File not found, return filename unaltered
2427
            $fullName = $file;
2428
        } else {
2429
            if (!$mode) {
2430
                // If use of .htaccess rule is not configured,
2431
                // we use the default query-string method
2432
                if (!empty($lookupFile[1])) {
2433
                    $separator = '&';
2434
                } else {
2435
                    $separator = '?';
2436
                }
2437
                $fullName = $file . $separator . filemtime($path);
2438
            } else {
2439
                // Change the filename
2440
                $name = explode('.', $lookupFile[0]);
2441
                $extension = array_pop($name);
2442
                array_push($name, filemtime($path), $extension);
2443
                $fullName = implode('.', $name);
2444
                // Append potential query string
2445
                $fullName .= $lookupFile[1] ? '?' . $lookupFile[1] : '';
2446
            }
2447
        }
2448
        return $fullName;
2449
    }
2450
2451
    /**
2452
     * Writes string to a temporary file named after the md5-hash of the string
2453
     * Quite useful for extensions adding their custom built JavaScript during runtime.
2454
     *
2455
     * @param string $content JavaScript to write to file.
2456
     * @return string filename to include in the <script> tag
2457
     */
2458
    public static function writeJavaScriptContentToTemporaryFile(string $content)
2459
    {
2460
        $script = 'typo3temp/assets/js/' . GeneralUtility::shortMD5($content) . '.js';
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
2461
        if (!@is_file(Environment::getPublicPath() . '/' . $script)) {
2462
            self::writeFileToTypo3tempDir(Environment::getPublicPath() . '/' . $script, $content);
2463
        }
2464
        return $script;
2465
    }
2466
2467
    /**
2468
     * Writes string to a temporary file named after the md5-hash of the string
2469
     * Quite useful for extensions adding their custom built StyleSheet during runtime.
2470
     *
2471
     * @param string $content CSS styles to write to file.
2472
     * @return string filename to include in the <link> tag
2473
     */
2474
    public static function writeStyleSheetContentToTemporaryFile(string $content)
2475
    {
2476
        $script = 'typo3temp/assets/css/' . self::shortMD5($content) . '.css';
2477
        if (!@is_file(Environment::getPublicPath() . '/' . $script)) {
2478
            self::writeFileToTypo3tempDir(Environment::getPublicPath() . '/' . $script, $content);
2479
        }
2480
        return $script;
2481
    }
2482
2483
    /*************************
2484
     *
2485
     * SYSTEM INFORMATION
2486
     *
2487
     *************************/
2488
2489
    /**
2490
     * Returns the link-url to the current script.
2491
     * 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.
2492
     * REMEMBER to always use htmlspecialchars() for content in href-properties to get ampersands converted to entities (XHTML requirement and XSS precaution)
2493
     *
2494
     * @param array $getParams Array of GET parameters to include
2495
     * @return string
2496
     */
2497
    public static function linkThisScript(array $getParams = [])
2498
    {
2499
        $parts = self::getIndpEnv('SCRIPT_NAME');
2500
        $params = self::_GET();
2501
        foreach ($getParams as $key => $value) {
2502
            if ($value !== '') {
2503
                $params[$key] = $value;
2504
            } else {
2505
                unset($params[$key]);
2506
            }
2507
        }
2508
        $pString = self::implodeArrayForUrl('', $params);
2509
        return $pString ? $parts . '?' . ltrim($pString, '&') : $parts;
2510
    }
2511
2512
    /**
2513
     * Takes a full URL, $url, possibly with a querystring and overlays the $getParams arrays values onto the querystring, packs it all together and returns the URL again.
2514
     * So basically it adds the parameters in $getParams to an existing URL, $url
2515
     *
2516
     * @param string $url URL string
2517
     * @param array $getParams Array of key/value pairs for get parameters to add/overrule with. Can be multidimensional.
2518
     * @return string Output URL with added getParams.
2519
     */
2520
    public static function linkThisUrl($url, array $getParams = [])
2521
    {
2522
        $parts = parse_url($url);
2523
        $getP = [];
2524
        if ($parts['query']) {
2525
            parse_str($parts['query'], $getP);
2526
        }
2527
        ArrayUtility::mergeRecursiveWithOverrule($getP, $getParams);
2528
        $uP = explode('?', $url);
2529
        $params = self::implodeArrayForUrl('', $getP);
2530
        $outurl = $uP[0] . ($params ? '?' . substr($params, 1) : '');
2531
        return $outurl;
2532
    }
2533
2534
    /**
2535
     * This method is only for testing and should never be used outside tests-
2536
     *
2537
     * @param string $envName
2538
     * @param mixed $value
2539
     * @internal
2540
     */
2541
    public static function setIndpEnv($envName, $value)
2542
    {
2543
        self::$indpEnvCache[$envName] = $value;
2544
    }
2545
2546
    /**
2547
     * Abstraction method which returns System Environment Variables regardless of server OS, CGI/MODULE version etc. Basically this is SERVER variables for most of them.
2548
     * This should be used instead of getEnv() and $_SERVER/ENV_VARS to get reliable values for all situations.
2549
     *
2550
     * @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
2551
     * @return string Value based on the input key, independent of server/os environment.
2552
     * @throws \UnexpectedValueException
2553
     */
2554
    public static function getIndpEnv($getEnvName)
2555
    {
2556
        if (array_key_exists($getEnvName, self::$indpEnvCache)) {
2557
            return self::$indpEnvCache[$getEnvName];
2558
        }
2559
2560
        /*
2561
        Conventions:
2562
        output from parse_url():
2563
        URL:	http://username:[email protected]:8080/typo3/32/temp/phpcheck/index.php/arg1/arg2/arg3/?arg1,arg2,arg3&p1=parameter1&p2[key]=value#link1
2564
        [scheme] => 'http'
2565
        [user] => 'username'
2566
        [pass] => 'password'
2567
        [host] => '192.168.1.4'
2568
        [port] => '8080'
2569
        [path] => '/typo3/32/temp/phpcheck/index.php/arg1/arg2/arg3/'
2570
        [query] => 'arg1,arg2,arg3&p1=parameter1&p2[key]=value'
2571
        [fragment] => 'link1'Further definition: [path_script] = '/typo3/32/temp/phpcheck/index.php'
2572
        [path_dir] = '/typo3/32/temp/phpcheck/'
2573
        [path_info] = '/arg1/arg2/arg3/'
2574
        [path] = [path_script/path_dir][path_info]Keys supported:URI______:
2575
        REQUEST_URI		=	[path]?[query]		= /typo3/32/temp/phpcheck/index.php/arg1/arg2/arg3/?arg1,arg2,arg3&p1=parameter1&p2[key]=value
2576
        HTTP_HOST		=	[host][:[port]]		= 192.168.1.4:8080
2577
        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')!
2578
        PATH_INFO		=	[path_info]			= /arg1/arg2/arg3/
2579
        QUERY_STRING	=	[query]				= arg1,arg2,arg3&p1=parameter1&p2[key]=value
2580
        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
2581
        (Notice: NO username/password + NO fragment)CLIENT____:
2582
        REMOTE_ADDR		=	(client IP)
2583
        REMOTE_HOST		=	(client host)
2584
        HTTP_USER_AGENT	=	(client user agent)
2585
        HTTP_ACCEPT_LANGUAGE	= (client accept language)SERVER____:
2586
        SCRIPT_FILENAME	=	Absolute filename of script		(Differs between windows/unix). On windows 'C:\\some\\path\\' will be converted to 'C:/some/path/'Special extras:
2587
        TYPO3_HOST_ONLY =		[host] = 192.168.1.4
2588
        TYPO3_PORT =			[port] = 8080 (blank if 80, taken from host value)
2589
        TYPO3_REQUEST_HOST = 		[scheme]://[host][:[port]]
2590
        TYPO3_REQUEST_URL =		[scheme]://[host][:[port]][path]?[query] (scheme will by default be "http" until we can detect something different)
2591
        TYPO3_REQUEST_SCRIPT =  	[scheme]://[host][:[port]][path_script]
2592
        TYPO3_REQUEST_DIR =		[scheme]://[host][:[port]][path_dir]
2593
        TYPO3_SITE_URL = 		[scheme]://[host][:[port]][path_dir] of the TYPO3 website frontend
2594
        TYPO3_SITE_PATH = 		[path_dir] of the TYPO3 website frontend
2595
        TYPO3_SITE_SCRIPT = 		[script / Speaking URL] of the TYPO3 website
2596
        TYPO3_DOCUMENT_ROOT =		Absolute path of root of documents: TYPO3_DOCUMENT_ROOT.SCRIPT_NAME = SCRIPT_FILENAME (typically)
2597
        TYPO3_SSL = 			Returns TRUE if this session uses SSL/TLS (https)
2598
        TYPO3_PROXY = 			Returns TRUE if this session runs over a well known proxyNotice: [fragment] is apparently NEVER available to the script!Testing suggestions:
2599
        - Output all the values.
2600
        - 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
2601
        - ALSO TRY the script from the ROOT of a site (like 'http://www.mytest.com/' and not 'http://www.mytest.com/test/' !!)
2602
         */
2603
        $retVal = '';
2604
        switch ((string)$getEnvName) {
2605
            case 'SCRIPT_NAME':
2606
                $retVal = Environment::isRunningOnCgiServer()
2607
                    && (($_SERVER['ORIG_PATH_INFO'] ?? false) ?: ($_SERVER['PATH_INFO'] ?? false))
2608
                        ? (($_SERVER['ORIG_PATH_INFO'] ?? '') ?: ($_SERVER['PATH_INFO'] ?? ''))
0 ignored issues
show
Coding Style introduced by
Expected 1 space before "?"; newline found
Loading history...
2609
                        : (($_SERVER['ORIG_SCRIPT_NAME'] ?? '') ?: ($_SERVER['SCRIPT_NAME'] ?? ''));
0 ignored issues
show
Coding Style introduced by
Expected 1 space before ":"; newline found
Loading history...
2610
                // Add a prefix if TYPO3 is behind a proxy: ext-domain.com => int-server.com/prefix
2611
                if (self::cmpIP($_SERVER['REMOTE_ADDR'] ?? '', $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP'] ?? '')) {
2612
                    if (self::getIndpEnv('TYPO3_SSL') && $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefixSSL']) {
2613
                        $retVal = $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefixSSL'] . $retVal;
2614
                    } elseif ($GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefix']) {
2615
                        $retVal = $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefix'] . $retVal;
2616
                    }
2617
                }
2618
                break;
2619
            case 'SCRIPT_FILENAME':
2620
                $retVal = Environment::getCurrentScript();
2621
                break;
2622
            case 'REQUEST_URI':
2623
                // Typical application of REQUEST_URI is return urls, forms submitting to itself etc. Example: returnUrl='.rawurlencode(\TYPO3\CMS\Core\Utility\GeneralUtility::getIndpEnv('REQUEST_URI'))
2624
                if (!empty($GLOBALS['TYPO3_CONF_VARS']['SYS']['requestURIvar'])) {
2625
                    // This is for URL rewriters that store the original URI in a server variable (eg ISAPI_Rewriter for IIS: HTTP_X_REWRITE_URL)
2626
                    [$v, $n] = explode('|', $GLOBALS['TYPO3_CONF_VARS']['SYS']['requestURIvar']);
2627
                    $retVal = $GLOBALS[$v][$n];
2628
                } elseif (empty($_SERVER['REQUEST_URI'])) {
2629
                    // This is for ISS/CGI which does not have the REQUEST_URI available.
2630
                    $retVal = '/' . ltrim(self::getIndpEnv('SCRIPT_NAME'), '/') . (!empty($_SERVER['QUERY_STRING']) ? '?' . $_SERVER['QUERY_STRING'] : '');
2631
                } else {
2632
                    $retVal = '/' . ltrim($_SERVER['REQUEST_URI'], '/');
2633
                }
2634
                // Add a prefix if TYPO3 is behind a proxy: ext-domain.com => int-server.com/prefix
2635
                if (isset($_SERVER['REMOTE_ADDR'], $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP'])
2636
                    && self::cmpIP($_SERVER['REMOTE_ADDR'], $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP'])
2637
                ) {
2638
                    if (self::getIndpEnv('TYPO3_SSL') && $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefixSSL']) {
2639
                        $retVal = $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefixSSL'] . $retVal;
2640
                    } elseif ($GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefix']) {
2641
                        $retVal = $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefix'] . $retVal;
2642
                    }
2643
                }
2644
                break;
2645
            case 'PATH_INFO':
2646
                // $_SERVER['PATH_INFO'] != $_SERVER['SCRIPT_NAME'] is necessary because some servers (Windows/CGI)
2647
                // are seen to set PATH_INFO equal to script_name
2648
                // Further, there must be at least one '/' in the path - else the PATH_INFO value does not make sense.
2649
                // IF 'PATH_INFO' never works for our purpose in TYPO3 with CGI-servers,
2650
                // then 'PHP_SAPI=='cgi'' might be a better check.
2651
                // Right now strcmp($_SERVER['PATH_INFO'], GeneralUtility::getIndpEnv('SCRIPT_NAME')) will always
2652
                // return FALSE for CGI-versions, but that is only as long as SCRIPT_NAME is set equal to PATH_INFO
2653
                // because of PHP_SAPI=='cgi' (see above)
2654
                if (!Environment::isRunningOnCgiServer()) {
2655
                    $retVal = $_SERVER['PATH_INFO'];
2656
                }
2657
                break;
2658
            case 'TYPO3_REV_PROXY':
2659
                $retVal = self::cmpIP($_SERVER['REMOTE_ADDR'], $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP']);
2660
                break;
2661
            case 'REMOTE_ADDR':
2662
                $retVal = $_SERVER['REMOTE_ADDR'] ?? null;
2663
                if (self::cmpIP($retVal, $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP'] ?? '')) {
2664
                    $ip = self::trimExplode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
2665
                    // Choose which IP in list to use
2666
                    if (!empty($ip)) {
2667
                        switch ($GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyHeaderMultiValue']) {
2668
                            case 'last':
2669
                                $ip = array_pop($ip);
2670
                                break;
2671
                            case 'first':
2672
                                $ip = array_shift($ip);
2673
                                break;
2674
                            case 'none':
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
2675
2676
                            default:
2677
                                $ip = '';
2678
                        }
2679
                    }
2680
                    if (self::validIP($ip)) {
2681
                        $retVal = $ip;
2682
                    }
2683
                }
2684
                break;
2685
            case 'HTTP_HOST':
2686
                // if it is not set we're most likely on the cli
2687
                $retVal = $_SERVER['HTTP_HOST'] ?? null;
2688
                if (isset($_SERVER['REMOTE_ADDR']) && static::cmpIP($_SERVER['REMOTE_ADDR'], $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP'])) {
2689
                    $host = self::trimExplode(',', $_SERVER['HTTP_X_FORWARDED_HOST']);
2690
                    // Choose which host in list to use
2691
                    if (!empty($host)) {
2692
                        switch ($GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyHeaderMultiValue']) {
2693
                            case 'last':
2694
                                $host = array_pop($host);
2695
                                break;
2696
                            case 'first':
2697
                                $host = array_shift($host);
2698
                                break;
2699
                            case 'none':
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
2700
2701
                            default:
2702
                                $host = '';
2703
                        }
2704
                    }
2705
                    if ($host) {
2706
                        $retVal = $host;
2707
                    }
2708
                }
2709
                if (!static::isAllowedHostHeaderValue($retVal)) {
2710
                    throw new \UnexpectedValueException(
2711
                        'The current host header value does not match the configured trusted hosts pattern! Check the pattern defined in $GLOBALS[\'TYPO3_CONF_VARS\'][\'SYS\'][\'trustedHostsPattern\'] and adapt it, if you want to allow the current host header \'' . $retVal . '\' for your installation.',
2712
                        1396795884
2713
                    );
2714
                }
2715
                break;
2716
            case 'HTTP_REFERER':
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
2717
2718
            case 'HTTP_USER_AGENT':
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
2719
2720
            case 'HTTP_ACCEPT_ENCODING':
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
2721
2722
            case 'HTTP_ACCEPT_LANGUAGE':
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
2723
2724
            case 'REMOTE_HOST':
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
2725
2726
            case 'QUERY_STRING':
2727
                $retVal = $_SERVER[$getEnvName] ?? '';
2728
                break;
2729
            case 'TYPO3_DOCUMENT_ROOT':
2730
                // Get the web root (it is not the root of the TYPO3 installation)
2731
                // The absolute path of the script can be calculated with TYPO3_DOCUMENT_ROOT + SCRIPT_FILENAME
2732
                // 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.
2733
                // Therefore the DOCUMENT_ROOT is now always calculated as the SCRIPT_FILENAME minus the end part shared with SCRIPT_NAME.
2734
                $SFN = self::getIndpEnv('SCRIPT_FILENAME');
2735
                $SN_A = explode('/', strrev(self::getIndpEnv('SCRIPT_NAME')));
2736
                $SFN_A = explode('/', strrev($SFN));
2737
                $acc = [];
2738
                foreach ($SN_A as $kk => $vv) {
2739
                    if ((string)$SFN_A[$kk] === (string)$vv) {
2740
                        $acc[] = $vv;
2741
                    } else {
2742
                        break;
2743
                    }
2744
                }
2745
                $commonEnd = strrev(implode('/', $acc));
2746
                if ((string)$commonEnd !== '') {
2747
                    $retVal = substr($SFN, 0, -(strlen($commonEnd) + 1));
2748
                }
2749
                break;
2750
            case 'TYPO3_HOST_ONLY':
2751
                $httpHost = self::getIndpEnv('HTTP_HOST');
2752
                $httpHostBracketPosition = strpos($httpHost, ']');
2753
                $httpHostParts = explode(':', $httpHost);
2754
                $retVal = $httpHostBracketPosition !== false ? substr($httpHost, 0, $httpHostBracketPosition + 1) : array_shift($httpHostParts);
2755
                break;
2756
            case 'TYPO3_PORT':
2757
                $httpHost = self::getIndpEnv('HTTP_HOST');
2758
                $httpHostOnly = self::getIndpEnv('TYPO3_HOST_ONLY');
2759
                $retVal = strlen($httpHost) > strlen($httpHostOnly) ? substr($httpHost, strlen($httpHostOnly) + 1) : '';
2760
                break;
2761
            case 'TYPO3_REQUEST_HOST':
2762
                $retVal = (self::getIndpEnv('TYPO3_SSL') ? 'https://' : 'http://') . self::getIndpEnv('HTTP_HOST');
2763
                break;
2764
            case 'TYPO3_REQUEST_URL':
2765
                $retVal = self::getIndpEnv('TYPO3_REQUEST_HOST') . self::getIndpEnv('REQUEST_URI');
2766
                break;
2767
            case 'TYPO3_REQUEST_SCRIPT':
2768
                $retVal = self::getIndpEnv('TYPO3_REQUEST_HOST') . self::getIndpEnv('SCRIPT_NAME');
2769
                break;
2770
            case 'TYPO3_REQUEST_DIR':
2771
                $retVal = self::getIndpEnv('TYPO3_REQUEST_HOST') . self::dirname(self::getIndpEnv('SCRIPT_NAME')) . '/';
2772
                break;
2773
            case 'TYPO3_SITE_URL':
2774
                $url = self::getIndpEnv('TYPO3_REQUEST_DIR');
2775
                // This can only be set by external entry scripts
2776
                if (defined('TYPO3_PATH_WEB')) {
2777
                    $retVal = $url;
2778
                } elseif (Environment::getCurrentScript()) {
2779
                    $lPath = PathUtility::stripPathSitePrefix(PathUtility::dirnameDuringBootstrap(Environment::getCurrentScript())) . '/';
2780
                    $siteUrl = substr($url, 0, -strlen($lPath));
2781
                    if (substr($siteUrl, -1) !== '/') {
2782
                        $siteUrl .= '/';
2783
                    }
2784
                    $retVal = $siteUrl;
2785
                }
2786
                break;
2787
            case 'TYPO3_SITE_PATH':
2788
                $retVal = substr(self::getIndpEnv('TYPO3_SITE_URL'), strlen(self::getIndpEnv('TYPO3_REQUEST_HOST')));
2789
                break;
2790
            case 'TYPO3_SITE_SCRIPT':
2791
                $retVal = substr(self::getIndpEnv('TYPO3_REQUEST_URL'), strlen(self::getIndpEnv('TYPO3_SITE_URL')));
2792
                break;
2793
            case 'TYPO3_SSL':
2794
                $proxySSL = trim($GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxySSL'] ?? null);
2795
                if ($proxySSL === '*') {
2796
                    $proxySSL = $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP'];
2797
                }
2798
                if (self::cmpIP($_SERVER['REMOTE_ADDR'] ?? '', $proxySSL)) {
2799
                    $retVal = true;
2800
                } else {
2801
                    // https://secure.php.net/manual/en/reserved.variables.server.php
2802
                    // "Set to a non-empty value if the script was queried through the HTTPS protocol."
2803
                    $retVal = !empty($_SERVER['SSL_SESSION_ID'])
2804
                        || (!empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) !== 'off');
2805
                }
2806
                break;
2807
            case '_ARRAY':
2808
                $out = [];
2809
                // Here, list ALL possible keys to this function for debug display.
2810
                $envTestVars = [
2811
                    'HTTP_HOST',
2812
                    'TYPO3_HOST_ONLY',
2813
                    'TYPO3_PORT',
2814
                    'PATH_INFO',
2815
                    'QUERY_STRING',
2816
                    'REQUEST_URI',
2817
                    'HTTP_REFERER',
2818
                    'TYPO3_REQUEST_HOST',
2819
                    'TYPO3_REQUEST_URL',
2820
                    'TYPO3_REQUEST_SCRIPT',
2821
                    'TYPO3_REQUEST_DIR',
2822
                    'TYPO3_SITE_URL',
2823
                    'TYPO3_SITE_SCRIPT',
2824
                    'TYPO3_SSL',
2825
                    'TYPO3_REV_PROXY',
2826
                    'SCRIPT_NAME',
2827
                    'TYPO3_DOCUMENT_ROOT',
2828
                    'SCRIPT_FILENAME',
2829
                    'REMOTE_ADDR',
2830
                    'REMOTE_HOST',
2831
                    'HTTP_USER_AGENT',
2832
                    'HTTP_ACCEPT_LANGUAGE'
2833
                ];
2834
                foreach ($envTestVars as $v) {
2835
                    $out[$v] = self::getIndpEnv($v);
2836
                }
2837
                reset($out);
2838
                $retVal = $out;
2839
                break;
2840
        }
2841
        self::$indpEnvCache[$getEnvName] = $retVal;
2842
        return $retVal;
2843
    }
2844
2845
    /**
2846
     * Checks if the provided host header value matches the trusted hosts pattern.
2847
     * If the pattern is not defined (which only can happen early in the bootstrap), deny any value.
2848
     * The result is saved, so the check needs to be executed only once.
2849
     *
2850
     * @param string $hostHeaderValue HTTP_HOST header value as sent during the request (may include port)
2851
     * @return bool
2852
     */
2853
    public static function isAllowedHostHeaderValue($hostHeaderValue)
2854
    {
2855
        if (static::$allowHostHeaderValue === true) {
2856
            return true;
2857
        }
2858
2859
        if (static::isInternalRequestType()) {
2860
            return static::$allowHostHeaderValue = true;
2861
        }
2862
2863
        // Deny the value if trusted host patterns is empty, which means we are early in the bootstrap
2864
        if (empty($GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern'])) {
2865
            return false;
2866
        }
2867
2868
        if ($GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern'] === self::ENV_TRUSTED_HOSTS_PATTERN_ALLOW_ALL) {
2869
            static::$allowHostHeaderValue = true;
2870
        } else {
2871
            static::$allowHostHeaderValue = static::hostHeaderValueMatchesTrustedHostsPattern($hostHeaderValue);
2872
        }
2873
2874
        return static::$allowHostHeaderValue;
2875
    }
2876
2877
    /**
2878
     * Checks if the provided host header value matches the trusted hosts pattern without any preprocessing.
2879
     *
2880
     * @param string $hostHeaderValue
2881
     * @return bool
2882
     * @internal
2883
     */
2884
    public static function hostHeaderValueMatchesTrustedHostsPattern($hostHeaderValue)
2885
    {
2886
        if ($GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern'] === self::ENV_TRUSTED_HOSTS_PATTERN_SERVER_NAME) {
2887
            // Allow values that equal the server name
2888
            // Note that this is only secure if name base virtual host are configured correctly in the webserver
2889
            $defaultPort = self::getIndpEnv('TYPO3_SSL') ? '443' : '80';
2890
            $parsedHostValue = parse_url('http://' . $hostHeaderValue);
2891
            if (isset($parsedHostValue['port'])) {
2892
                $hostMatch = (strtolower($parsedHostValue['host']) === strtolower($_SERVER['SERVER_NAME']) && (string)$parsedHostValue['port'] === $_SERVER['SERVER_PORT']);
2893
            } else {
2894
                $hostMatch = (strtolower($hostHeaderValue) === strtolower($_SERVER['SERVER_NAME']) && $defaultPort === $_SERVER['SERVER_PORT']);
2895
            }
2896
        } else {
2897
            // In case name based virtual hosts are not possible, we allow setting a trusted host pattern
2898
            // See https://typo3.org/teams/security/security-bulletins/typo3-core/typo3-core-sa-2014-001/ for further details
2899
            $hostMatch = (bool)preg_match('/^' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern'] . '$/i', $hostHeaderValue);
2900
        }
2901
2902
        return $hostMatch;
2903
    }
2904
2905
    /**
2906
     * Allows internal requests to the install tool and from the command line.
2907
     * We accept this risk to have the install tool always available.
2908
     * Also CLI needs to be allowed as unfortunately AbstractUserAuthentication::getAuthInfoArray()
2909
     * accesses HTTP_HOST without reason on CLI
2910
     * Additionally, allows requests when no REQUESTTYPE is set, which can happen quite early in the
2911
     * Bootstrap. See Application.php in EXT:backend/Classes/Http/.
2912
     *
2913
     * @return bool
2914
     */
2915
    protected static function isInternalRequestType()
2916
    {
2917
        return Environment::isCli() || !defined('TYPO3_REQUESTTYPE') || (defined('TYPO3_REQUESTTYPE') && TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_INSTALL);
2918
    }
2919
2920
    /**
2921
     * Gets the unixtime as milliseconds.
2922
     *
2923
     * @return int The unixtime as milliseconds
2924
     */
2925
    public static function milliseconds()
2926
    {
2927
        return round(microtime(true) * 1000);
2928
    }
2929
2930
    /*************************
2931
     *
2932
     * TYPO3 SPECIFIC FUNCTIONS
2933
     *
2934
     *************************/
2935
    /**
2936
     * Returns the absolute filename of a relative reference, resolves the "EXT:" prefix
2937
     * (way of referring to files inside extensions) and checks that the file is inside
2938
     * the TYPO3's base folder and implies a check with
2939
     * \TYPO3\CMS\Core\Utility\GeneralUtility::validPathStr().
2940
     *
2941
     * @param string $filename The input filename/filepath to evaluate
2942
     * @return string Returns the absolute filename of $filename if valid, otherwise blank string.
2943
     */
2944
    public static function getFileAbsFileName($filename)
2945
    {
2946
        if ((string)$filename === '') {
2947
            return '';
2948
        }
2949
        // Extension
2950
        if (strpos($filename, 'EXT:') === 0) {
2951
            [$extKey, $local] = explode('/', substr($filename, 4), 2);
2952
            $filename = '';
2953
            if ((string)$extKey !== '' && ExtensionManagementUtility::isLoaded($extKey) && (string)$local !== '') {
2954
                $filename = ExtensionManagementUtility::extPath($extKey) . $local;
2955
            }
2956
        } elseif (!static::isAbsPath($filename)) {
2957
            // is relative. Prepended with the public web folder
2958
            $filename = Environment::getPublicPath() . '/' . $filename;
2959
        } elseif (!(
2960
            static::isFirstPartOfStr($filename, Environment::getProjectPath())
2961
                  || static::isFirstPartOfStr($filename, Environment::getPublicPath())
2962
        )) {
2963
            // absolute, but set to blank if not allowed
2964
            $filename = '';
2965
        }
2966
        if ((string)$filename !== '' && static::validPathStr($filename)) {
2967
            // checks backpath.
2968
            return $filename;
2969
        }
2970
        return '';
2971
    }
2972
2973
    /**
2974
     * Checks for malicious file paths.
2975
     *
2976
     * Returns TRUE if no '//', '..', '\' or control characters are found in the $theFile.
2977
     * This should make sure that the path is not pointing 'backwards' and further doesn't contain double/back slashes.
2978
     * So it's compatible with the UNIX style path strings valid for TYPO3 internally.
2979
     *
2980
     * @param string $theFile File path to evaluate
2981
     * @return bool TRUE, $theFile is allowed path string, FALSE otherwise
2982
     * @see http://php.net/manual/en/security.filesystem.nullbytes.php
2983
     */
2984
    public static function validPathStr($theFile)
2985
    {
2986
        return strpos($theFile, '//') === false && strpos($theFile, '\\') === false
2987
            && preg_match('#(?:^\\.\\.|/\\.\\./|[[:cntrl:]])#u', $theFile) === 0;
2988
    }
2989
2990
    /**
2991
     * Checks if the $path is absolute or relative (detecting either '/' or 'x:/' as first part of string) and returns TRUE if so.
2992
     *
2993
     * @param string $path File path to evaluate
2994
     * @return bool
2995
     */
2996
    public static function isAbsPath($path)
2997
    {
2998
        if (substr($path, 0, 6) === 'vfs://') {
2999
            return true;
3000
        }
3001
        return isset($path[0]) && $path[0] === '/' || Environment::isWindows() && (strpos($path, ':/') === 1 || strpos($path, ':\\') === 1);
3002
    }
3003
3004
    /**
3005
     * Returns TRUE if the path is absolute, without backpath '..' and within TYPO3s project or public folder OR within the lockRootPath
3006
     *
3007
     * @param string $path File path to evaluate
3008
     * @return bool
3009
     */
3010
    public static function isAllowedAbsPath($path)
3011
    {
3012
        if (substr($path, 0, 6) === 'vfs://') {
3013
            return true;
3014
        }
3015
        $lockRootPath = $GLOBALS['TYPO3_CONF_VARS']['BE']['lockRootPath'];
3016
        return static::isAbsPath($path) && static::validPathStr($path)
3017
            && (
3018
                static::isFirstPartOfStr($path, Environment::getProjectPath())
3019
                || static::isFirstPartOfStr($path, Environment::getPublicPath())
3020
                || $lockRootPath && static::isFirstPartOfStr($path, $lockRootPath)
3021
            );
3022
    }
3023
3024
    /**
3025
     * Verifies the input filename against the 'fileDenyPattern'. Returns TRUE if OK.
3026
     *
3027
     * Filenames are not allowed to contain control characters. Therefore we
3028
     * always filter on [[:cntrl:]].
3029
     *
3030
     * @param string $filename File path to evaluate
3031
     * @return bool
3032
     * @deprecated will be removed in TYPO3 v11.0. Use the new FileNameValidator API instead.
3033
     */
3034
    public static function verifyFilenameAgainstDenyPattern($filename)
3035
    {
3036
        trigger_error('GeneralUtility::verifyFilenameAgainstDenyPattern() will be removed in TYPO3 v11.0. Use FileNameValidator->isValid($filename) instead.', E_USER_DEPRECATED);
3037
        return self::makeInstance(FileNameValidator::class)->isValid((string)$filename);
3038
    }
3039
3040
    /**
3041
     * Low level utility function to copy directories and content recursive
3042
     *
3043
     * @param string $source Path to source directory, relative to document root or absolute
3044
     * @param string $destination Path to destination directory, relative to document root or absolute
3045
     */
3046
    public static function copyDirectory($source, $destination)
3047
    {
3048
        if (strpos($source, Environment::getProjectPath() . '/') === false) {
3049
            $source = Environment::getPublicPath() . '/' . $source;
3050
        }
3051
        if (strpos($destination, Environment::getProjectPath() . '/') === false) {
3052
            $destination = Environment::getPublicPath() . '/' . $destination;
3053
        }
3054
        if (static::isAllowedAbsPath($source) && static::isAllowedAbsPath($destination)) {
3055
            static::mkdir_deep($destination);
3056
            $iterator = new \RecursiveIteratorIterator(
3057
                new \RecursiveDirectoryIterator($source, \RecursiveDirectoryIterator::SKIP_DOTS),
3058
                \RecursiveIteratorIterator::SELF_FIRST
3059
            );
3060
            /** @var \SplFileInfo $item */
3061
            foreach ($iterator as $item) {
3062
                $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

3062
                $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...
3063
                if ($item->isDir()) {
3064
                    static::mkdir($target);
3065
                } else {
3066
                    static::upload_copy_move(static::fixWindowsFilePath($item->getPathname()), $target);
3067
                }
3068
            }
3069
        }
3070
    }
3071
3072
    /**
3073
     * Checks if a given string is a valid frame URL to be loaded in the
3074
     * backend.
3075
     *
3076
     * If the given url is empty or considered to be harmless, it is returned
3077
     * as is, else the event is logged and an empty string is returned.
3078
     *
3079
     * @param string $url potential URL to check
3080
     * @return string $url or empty string
3081
     */
3082
    public static function sanitizeLocalUrl($url = '')
3083
    {
3084
        $sanitizedUrl = '';
3085
        if (!empty($url)) {
3086
            $decodedUrl = rawurldecode($url);
3087
            $parsedUrl = parse_url($decodedUrl);
3088
            $testAbsoluteUrl = self::resolveBackPath($decodedUrl);
3089
            $testRelativeUrl = self::resolveBackPath(self::dirname(self::getIndpEnv('SCRIPT_NAME')) . '/' . $decodedUrl);
3090
            // Pass if URL is on the current host:
3091
            if (self::isValidUrl($decodedUrl)) {
3092
                if (self::isOnCurrentHost($decodedUrl) && strpos($decodedUrl, self::getIndpEnv('TYPO3_SITE_URL')) === 0) {
3093
                    $sanitizedUrl = $url;
3094
                }
3095
            } elseif (self::isAbsPath($decodedUrl) && self::isAllowedAbsPath($decodedUrl)) {
3096
                $sanitizedUrl = $url;
3097
            } elseif (strpos($testAbsoluteUrl, self::getIndpEnv('TYPO3_SITE_PATH')) === 0 && $decodedUrl[0] === '/') {
3098
                $sanitizedUrl = $url;
3099
            } elseif (empty($parsedUrl['scheme']) && strpos($testRelativeUrl, self::getIndpEnv('TYPO3_SITE_PATH')) === 0
3100
                && $decodedUrl[0] !== '/' && strpbrk($decodedUrl, '*:|"<>') === false && strpos($decodedUrl, '\\\\') === false
3101
            ) {
3102
                $sanitizedUrl = $url;
3103
            }
3104
        }
3105
        if (!empty($url) && empty($sanitizedUrl)) {
3106
            static::getLogger()->notice('The URL "' . $url . '" is not considered to be local and was denied.');
3107
        }
3108
        return $sanitizedUrl;
3109
    }
3110
3111
    /**
3112
     * Moves $source file to $destination if uploaded, otherwise try to make a copy
3113
     *
3114
     * @param string $source Source file, absolute path
3115
     * @param string $destination Destination file, absolute path
3116
     * @return bool Returns TRUE if the file was moved.
3117
     * @see upload_to_tempfile()
3118
     */
3119
    public static function upload_copy_move($source, $destination)
0 ignored issues
show
Coding Style introduced by
Method name "GeneralUtility::upload_copy_move" is not in camel caps format
Loading history...
3120
    {
3121
        if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][\TYPO3\CMS\Core\Utility\GeneralUtility::class]['moveUploadedFile'] ?? null)) {
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
3122
            $params = ['source' => $source, 'destination' => $destination, 'method' => 'upload_copy_move'];
3123
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][\TYPO3\CMS\Core\Utility\GeneralUtility::class]['moveUploadedFile'] as $hookMethod) {
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
3124
                $fakeThis = false;
3125
                self::callUserFunction($hookMethod, $params, $fakeThis);
3126
            }
3127
        }
3128
3129
        $result = false;
3130
        if (is_uploaded_file($source)) {
3131
            // Return the value of move_uploaded_file, and if FALSE the temporary $source is still
3132
            // around so the user can use unlink to delete it:
3133
            $result = move_uploaded_file($source, $destination);
3134
        } else {
3135
            @copy($source, $destination);
3136
        }
3137
        // Change the permissions of the file
3138
        self::fixPermissions($destination);
3139
        // If here the file is copied and the temporary $source is still around,
3140
        // so when returning FALSE the user can try unlink to delete the $source
3141
        return $result;
3142
    }
3143
3144
    /**
3145
     * Will move an uploaded file (normally in "/tmp/xxxxx") to a temporary filename in Environment::getProjectPath() . "var/" from where TYPO3 can use it.
3146
     * Use this function to move uploaded files to where you can work on them.
3147
     * 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!
3148
     *
3149
     * @param string $uploadedFileName The temporary uploaded filename, eg. $_FILES['[upload field name here]']['tmp_name']
3150
     * @return string If a new file was successfully created, return its filename, otherwise blank string.
3151
     * @see unlink_tempfile()
3152
     * @see upload_copy_move()
3153
     */
3154
    public static function upload_to_tempfile($uploadedFileName)
0 ignored issues
show
Coding Style introduced by
Method name "GeneralUtility::upload_to_tempfile" is not in camel caps format
Loading history...
3155
    {
3156
        if (is_uploaded_file($uploadedFileName)) {
3157
            $tempFile = self::tempnam('upload_temp_');
3158
            if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][\TYPO3\CMS\Core\Utility\GeneralUtility::class]['moveUploadedFile'] ?? null)) {
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
3159
                $params = ['source' => $uploadedFileName, 'destination' => $tempFile, 'method' => 'upload_to_tempfile'];
3160
                foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][\TYPO3\CMS\Core\Utility\GeneralUtility::class]['moveUploadedFile'] as $hookMethod) {
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
3161
                    $fakeThis = false;
3162
                    self::callUserFunction($hookMethod, $params, $fakeThis);
3163
                }
3164
            }
3165
3166
            move_uploaded_file($uploadedFileName, $tempFile);
3167
            return @is_file($tempFile) ? $tempFile : '';
3168
        }
3169
3170
        return '';
3171
    }
3172
3173
    /**
3174
     * Deletes (unlink) a temporary filename in the var/ or typo3temp folder given as input.
3175
     * 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.
3176
     * Use this after upload_to_tempfile() or tempnam() from this class!
3177
     *
3178
     * @param string $uploadedTempFileName absolute file path - must reside within var/ or typo3temp/ folder.
3179
     * @return bool|null Returns TRUE if the file was unlink()'ed
3180
     * @see upload_to_tempfile()
3181
     * @see tempnam()
3182
     */
3183
    public static function unlink_tempfile($uploadedTempFileName)
0 ignored issues
show
Coding Style introduced by
Method name "GeneralUtility::unlink_tempfile" is not in camel caps format
Loading history...
3184
    {
3185
        if ($uploadedTempFileName) {
3186
            $uploadedTempFileName = self::fixWindowsFilePath($uploadedTempFileName);
3187
            if (
3188
                self::validPathStr($uploadedTempFileName)
3189
                && (
3190
                    self::isFirstPartOfStr($uploadedTempFileName, Environment::getPublicPath() . '/typo3temp/')
3191
                    || self::isFirstPartOfStr($uploadedTempFileName, Environment::getVarPath() . '/')
3192
                )
3193
                && @is_file($uploadedTempFileName)
3194
            ) {
3195
                if (unlink($uploadedTempFileName)) {
3196
                    return true;
3197
                }
3198
            }
3199
        }
3200
3201
        return null;
3202
    }
3203
3204
    /**
3205
     * Create temporary filename (Create file with unique file name)
3206
     * This function should be used for getting temporary file names - will make your applications safe for open_basedir = on
3207
     * REMEMBER to delete the temporary files after use! This is done by \TYPO3\CMS\Core\Utility\GeneralUtility::unlink_tempfile()
3208
     *
3209
     * @param string $filePrefix Prefix for temporary file
3210
     * @param string $fileSuffix Suffix for temporary file, for example a special file extension
3211
     * @return string result from PHP function tempnam() with the temp/var folder prefixed.
3212
     * @see unlink_tempfile()
3213
     * @see upload_to_tempfile()
3214
     */
3215
    public static function tempnam($filePrefix, $fileSuffix = '')
3216
    {
3217
        $temporaryPath = Environment::getVarPath() . '/transient/';
3218
        if (!is_dir($temporaryPath)) {
3219
            self::mkdir_deep($temporaryPath);
3220
        }
3221
        if ($fileSuffix === '') {
3222
            $tempFileName = $temporaryPath . PathUtility::basename(tempnam($temporaryPath, $filePrefix));
3223
        } else {
3224
            do {
3225
                $tempFileName = $temporaryPath . $filePrefix . random_int(1, PHP_INT_MAX) . $fileSuffix;
3226
            } while (file_exists($tempFileName));
3227
            touch($tempFileName);
3228
            clearstatcache(null, $tempFileName);
3229
        }
3230
        return $tempFileName;
3231
    }
3232
3233
    /**
3234
     * Standard authentication code (used in Direct Mail, checkJumpUrl and setfixed links computations)
3235
     *
3236
     * @param mixed $uid_or_record Uid (int) or record (array)
3237
     * @param string $fields List of fields from the record if that is given.
3238
     * @param int $codeLength Length of returned authentication code.
3239
     * @return string MD5 hash of 8 chars.
3240
     */
3241
    public static function stdAuthCode($uid_or_record, $fields = '', $codeLength = 8)
3242
    {
3243
        if (is_array($uid_or_record)) {
3244
            $recCopy_temp = [];
3245
            if ($fields) {
3246
                $fieldArr = self::trimExplode(',', $fields, true);
3247
                foreach ($fieldArr as $k => $v) {
3248
                    $recCopy_temp[$k] = $uid_or_record[$v];
3249
                }
3250
            } else {
3251
                $recCopy_temp = $uid_or_record;
3252
            }
3253
            $preKey = implode('|', $recCopy_temp);
3254
        } else {
3255
            $preKey = $uid_or_record;
3256
        }
3257
        $authCode = $preKey . '||' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'];
3258
        $authCode = substr(md5($authCode), 0, $codeLength);
3259
        return $authCode;
3260
    }
3261
3262
    /**
3263
     * Responds on input localization setting value whether the page it comes from should be hidden if no translation exists or not.
3264
     *
3265
     * @param int $l18n_cfg_fieldValue Value from "l18n_cfg" field of a page record
3266
     * @return bool TRUE if the page should be hidden
3267
     */
3268
    public static function hideIfNotTranslated($l18n_cfg_fieldValue)
3269
    {
3270
        return $GLOBALS['TYPO3_CONF_VARS']['FE']['hidePagesIfNotTranslatedByDefault'] xor ($l18n_cfg_fieldValue & 2);
3271
    }
3272
3273
    /**
3274
     * Returns true if the "l18n_cfg" field value is not set to hide
3275
     * pages in the default language
3276
     *
3277
     * @param int $localizationConfiguration
3278
     * @return bool
3279
     */
3280
    public static function hideIfDefaultLanguage($localizationConfiguration)
3281
    {
3282
        return (bool)($localizationConfiguration & 1);
3283
    }
3284
3285
    /**
3286
     * Calls a user-defined function/method in class
3287
     * Such a function/method should look like this: "function proc(&$params, &$ref) {...}"
3288
     *
3289
     * @param string $funcName Function/Method reference or Closure.
3290
     * @param mixed $params Parameters to be pass along (typically an array) (REFERENCE!)
3291
     * @param mixed $ref Reference to be passed along (typically "$this" - being a reference to the calling object) (REFERENCE!)
3292
     * @return mixed Content from method/function call
3293
     * @throws \InvalidArgumentException
3294
     */
3295
    public static function callUserFunction($funcName, &$params, &$ref = null)
3296
    {
3297
        if (!($ref === null || is_object($ref))) {
3298
            trigger_error(
3299
                sprintf('Argument "$ref" is of type "%s" which is deprecated since TYPO3 10.3. "$ref" must be of type "object" or null as of version 11.0', gettype($ref)),
3300
                E_USER_DEPRECATED
3301
            );
3302
        }
3303
3304
        // Check if we're using a closure and invoke it directly.
3305
        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...
3306
            return call_user_func_array($funcName, [&$params, &$ref]);
3307
        }
3308
        $funcName = trim($funcName);
3309
        $parts = explode('->', $funcName);
3310
        // Call function or method
3311
        if (count($parts) === 2) {
3312
            // It's a class/method
3313
            // Check if class/method exists:
3314
            if (class_exists($parts[0])) {
3315
                // Create object
3316
                $classObj = self::makeInstance($parts[0]);
3317
                if (method_exists($classObj, $parts[1])) {
3318
                    // Call method:
3319
                    $content = call_user_func_array([&$classObj, $parts[1]], [&$params, &$ref]);
3320
                } else {
3321
                    $errorMsg = 'No method name \'' . $parts[1] . '\' in class ' . $parts[0];
3322
                    throw new \InvalidArgumentException($errorMsg, 1294585865);
3323
                }
3324
            } else {
3325
                $errorMsg = 'No class named ' . $parts[0];
3326
                throw new \InvalidArgumentException($errorMsg, 1294585866);
3327
            }
3328
        } elseif (function_exists($funcName)) {
3329
            // It's a function
3330
            $content = call_user_func_array($funcName, [&$params, &$ref]);
3331
        } else {
3332
            $errorMsg = 'No function named: ' . $funcName;
3333
            throw new \InvalidArgumentException($errorMsg, 1294585867);
3334
        }
3335
        return $content;
3336
    }
3337
3338
    /**
3339
     * @param ContainerInterface $container
3340
     * @internal
3341
     */
3342
    public static function setContainer(ContainerInterface $container): void
3343
    {
3344
        self::$container = $container;
3345
    }
3346
3347
    /**
3348
     * @return ContainerInterface
3349
     * @internal
3350
     */
3351
    public static function getContainer(): ContainerInterface
3352
    {
3353
        if (self::$container === null) {
3354
            throw new \LogicException('PSR-11 Container is not available', 1549404144);
3355
        }
3356
        return self::$container;
3357
    }
3358
3359
    /**
3360
     * Creates an instance of a class taking into account the class-extensions
3361
     * API of TYPO3. USE THIS method instead of the PHP "new" keyword.
3362
     * Eg. "$obj = new myclass;" should be "$obj = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance("myclass")" instead!
3363
     *
3364
     * You can also pass arguments for a constructor:
3365
     * \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\myClass::class, $arg1, $arg2, ..., $argN)
3366
     *
3367
     * You may want to use \TYPO3\CMS\Extbase\Object\ObjectManager::get() if you
3368
     * want TYPO3 to take care about injecting dependencies of the class to be
3369
     * created. Therefore create an instance of ObjectManager via
3370
     * GeneralUtility::makeInstance() first and call its get() method to get
3371
     * the instance of a specific class.
3372
     *
3373
     * @param string $className name of the class to instantiate, must not be empty and not start with a backslash
3374
     * @param array<int,mixed> $constructorArguments Arguments for the constructor
3375
     * @return object the created instance
3376
     * @throws \InvalidArgumentException if $className is empty or starts with a backslash
3377
     */
3378
    public static function makeInstance($className, ...$constructorArguments)
3379
    {
3380
        if (!is_string($className) || empty($className)) {
0 ignored issues
show
introduced by
The condition is_string($className) is always true.
Loading history...
3381
            throw new \InvalidArgumentException('$className must be a non empty string.', 1288965219);
3382
        }
3383
        // Never instantiate with a beginning backslash, otherwise things like singletons won't work.
3384
        if ($className[0] === '\\') {
3385
            throw new \InvalidArgumentException(
3386
                '$className "' . $className . '" must not start with a backslash.',
3387
                1420281366
3388
            );
3389
        }
3390
        if (isset(static::$finalClassNameCache[$className])) {
3391
            $finalClassName = static::$finalClassNameCache[$className];
3392
        } else {
3393
            $finalClassName = self::getClassName($className);
3394
            static::$finalClassNameCache[$className] = $finalClassName;
3395
        }
3396
        // Return singleton instance if it is already registered
3397
        if (isset(self::$singletonInstances[$finalClassName])) {
3398
            return self::$singletonInstances[$finalClassName];
3399
        }
3400
        // Return instance if it has been injected by addInstance()
3401
        if (
3402
            isset(self::$nonSingletonInstances[$finalClassName])
3403
            && !empty(self::$nonSingletonInstances[$finalClassName])
3404
        ) {
3405
            return array_shift(self::$nonSingletonInstances[$finalClassName]);
0 ignored issues
show
Bug introduced by
self::nonSingletonInstances[$finalClassName] of type object is incompatible with the type array expected by parameter $array of array_shift(). ( Ignorable by Annotation )

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

3405
            return array_shift(/** @scrutinizer ignore-type */ self::$nonSingletonInstances[$finalClassName]);
Loading history...
3406
        }
3407
3408
        // Read service and prototypes from the DI container, this is required to
3409
        // support classes that require dependency injection.
3410
        // We operate on the original class name on purpose, as class overrides
3411
        // are resolved inside the container
3412
        if (self::$container !== null && $constructorArguments === [] && self::$container->has($className)) {
3413
            return self::$container->get($className);
3414
        }
3415
3416
        // Create new instance and call constructor with parameters
3417
        $instance = new $finalClassName(...$constructorArguments);
3418
        // Register new singleton instance, but only if it is not a known PSR-11 container service
3419
        if ($instance instanceof SingletonInterface && !(self::$container !== null && self::$container->has($className))) {
3420
            self::$singletonInstances[$finalClassName] = $instance;
3421
        }
3422
        if ($instance instanceof LoggerAwareInterface) {
3423
            $instance->setLogger(static::makeInstance(LogManager::class)->getLogger($className));
3424
        }
3425
        return $instance;
3426
    }
3427
3428
    /**
3429
     * Creates a class taking implementation settings and class aliases into account.
3430
     *
3431
     * Intended to be used to create objects by the dependency injection
3432
     * container.
3433
     *
3434
     * @param string $className name of the class to instantiate
3435
     * @param array<int,mixed> $constructorArguments Arguments for the constructor
3436
     * @return object the created instance
3437
     * @internal
3438
     */
3439
    public static function makeInstanceForDi(string $className, ...$constructorArguments): object
3440
    {
3441
        $finalClassName = static::$finalClassNameCache[$className] ?? static::$finalClassNameCache[$className] = self::getClassName($className);
3442
3443
        // Return singleton instance if it is already registered (currently required for unit and functional tests)
3444
        if (isset(self::$singletonInstances[$finalClassName])) {
3445
            return self::$singletonInstances[$finalClassName];
3446
        }
3447
        // Create new instance and call constructor with parameters
3448
        return new $finalClassName(...$constructorArguments);
3449
    }
3450
3451
    /**
3452
     * Returns the class name for a new instance, taking into account
3453
     * registered implementations for this class
3454
     *
3455
     * @param string $className Base class name to evaluate
3456
     * @return string Final class name to instantiate with "new [classname]
3457
     */
3458
    protected static function getClassName($className)
3459
    {
3460
        if (class_exists($className)) {
3461
            while (static::classHasImplementation($className)) {
3462
                $className = static::getImplementationForClass($className);
3463
            }
3464
        }
3465
        return ClassLoadingInformation::getClassNameForAlias($className);
3466
    }
3467
3468
    /**
3469
     * Returns the configured implementation of the class
3470
     *
3471
     * @param string $className
3472
     * @return string
3473
     */
3474
    protected static function getImplementationForClass($className)
3475
    {
3476
        return $GLOBALS['TYPO3_CONF_VARS']['SYS']['Objects'][$className]['className'];
3477
    }
3478
3479
    /**
3480
     * Checks if a class has a configured implementation
3481
     *
3482
     * @param string $className
3483
     * @return bool
3484
     */
3485
    protected static function classHasImplementation($className)
3486
    {
3487
        return !empty($GLOBALS['TYPO3_CONF_VARS']['SYS']['Objects'][$className]['className']);
3488
    }
3489
3490
    /**
3491
     * Sets the instance of a singleton class to be returned by makeInstance.
3492
     *
3493
     * If this function is called multiple times for the same $className,
3494
     * makeInstance will return the last set instance.
3495
     *
3496
     * Warning:
3497
     * This is NOT a public API method and must not be used in own extensions!
3498
     * This methods exists mostly for unit tests to inject a mock of a singleton class.
3499
     * If you use this, make sure to always combine this with getSingletonInstances()
3500
     * and resetSingletonInstances() in setUp() and tearDown() of the test class.
3501
     *
3502
     * @see makeInstance
3503
     * @param string $className
3504
     * @param \TYPO3\CMS\Core\SingletonInterface $instance
3505
     * @internal
3506
     */
3507
    public static function setSingletonInstance($className, SingletonInterface $instance)
3508
    {
3509
        self::checkInstanceClassName($className, $instance);
3510
        // Check for XCLASS registration (same is done in makeInstance() in order to store the singleton of the final class name)
3511
        $finalClassName = self::getClassName($className);
3512
        self::$singletonInstances[$finalClassName] = $instance;
3513
    }
3514
3515
    /**
3516
     * Removes the instance of a singleton class to be returned by makeInstance.
3517
     *
3518
     * Warning:
3519
     * This is NOT a public API method and must not be used in own extensions!
3520
     * This methods exists mostly for unit tests to inject a mock of a singleton class.
3521
     * If you use this, make sure to always combine this with getSingletonInstances()
3522
     * and resetSingletonInstances() in setUp() and tearDown() of the test class.
3523
     *
3524
     * @see makeInstance
3525
     * @throws \InvalidArgumentException
3526
     * @param string $className
3527
     * @param \TYPO3\CMS\Core\SingletonInterface $instance
3528
     * @internal
3529
     */
3530
    public static function removeSingletonInstance($className, SingletonInterface $instance)
3531
    {
3532
        self::checkInstanceClassName($className, $instance);
3533
        if (!isset(self::$singletonInstances[$className])) {
3534
            throw new \InvalidArgumentException('No Instance registered for ' . $className . '.', 1394099179);
3535
        }
3536
        if ($instance !== self::$singletonInstances[$className]) {
3537
            throw new \InvalidArgumentException('The instance you are trying to remove has not been registered before.', 1394099256);
3538
        }
3539
        unset(self::$singletonInstances[$className]);
3540
    }
3541
3542
    /**
3543
     * Set a group of singleton instances. Similar to setSingletonInstance(),
3544
     * but multiple instances can be set.
3545
     *
3546
     * Warning:
3547
     * This is NOT a public API method and must not be used in own extensions!
3548
     * This method is usually only used in tests to restore the list of singletons in
3549
     * tearDown(), that was backed up with getSingletonInstances() in setUp() and
3550
     * manipulated in tests with setSingletonInstance()
3551
     *
3552
     * @internal
3553
     * @param array $newSingletonInstances $className => $object
3554
     */
3555
    public static function resetSingletonInstances(array $newSingletonInstances)
3556
    {
3557
        static::$singletonInstances = [];
3558
        foreach ($newSingletonInstances as $className => $instance) {
3559
            static::setSingletonInstance($className, $instance);
3560
        }
3561
    }
3562
3563
    /**
3564
     * Get all currently registered singletons
3565
     *
3566
     * Warning:
3567
     * This is NOT a public API method and must not be used in own extensions!
3568
     * This method is usually only used in tests in setUp() to fetch the list of
3569
     * currently registered singletons, if this list is manipulated with
3570
     * setSingletonInstance() in tests.
3571
     *
3572
     * @internal
3573
     * @return array $className => $object
3574
     */
3575
    public static function getSingletonInstances()
3576
    {
3577
        return static::$singletonInstances;
3578
    }
3579
3580
    /**
3581
     * Get all currently registered non singleton instances
3582
     *
3583
     * Warning:
3584
     * This is NOT a public API method and must not be used in own extensions!
3585
     * This method is only used in UnitTestCase base test tearDown() to verify tests
3586
     * have no left over instances that were previously added using addInstance().
3587
     *
3588
     * @internal
3589
     * @return array $className => $objects[]
3590
     */
3591
    public static function getInstances()
3592
    {
3593
        return static::$nonSingletonInstances;
3594
    }
3595
3596
    /**
3597
     * Sets the instance of a non-singleton class to be returned by makeInstance.
3598
     *
3599
     * If this function is called multiple times for the same $className,
3600
     * makeInstance will return the instances in the order in which they have
3601
     * been added (FIFO).
3602
     *
3603
     * Warning: This is a helper method for unit tests. Do not call this directly in production code!
3604
     *
3605
     * @see makeInstance
3606
     * @throws \InvalidArgumentException if class extends \TYPO3\CMS\Core\SingletonInterface
3607
     * @param string $className
3608
     * @param object $instance
3609
     */
3610
    public static function addInstance($className, $instance)
3611
    {
3612
        self::checkInstanceClassName($className, $instance);
3613
        if ($instance instanceof SingletonInterface) {
3614
            throw new \InvalidArgumentException('$instance must not be an instance of TYPO3\\CMS\\Core\\SingletonInterface. For setting singletons, please use setSingletonInstance.', 1288969325);
3615
        }
3616
        if (!isset(self::$nonSingletonInstances[$className])) {
3617
            self::$nonSingletonInstances[$className] = [];
3618
        }
3619
        self::$nonSingletonInstances[$className][] = $instance;
3620
    }
3621
3622
    /**
3623
     * Checks that $className is non-empty and that $instance is an instance of
3624
     * $className.
3625
     *
3626
     * @throws \InvalidArgumentException if $className is empty or if $instance is no instance of $className
3627
     * @param string $className a class name
3628
     * @param object $instance an object
3629
     */
3630
    protected static function checkInstanceClassName($className, $instance)
3631
    {
3632
        if ($className === '') {
3633
            throw new \InvalidArgumentException('$className must not be empty.', 1288967479);
3634
        }
3635
        if (!$instance instanceof $className) {
3636
            throw new \InvalidArgumentException('$instance must be an instance of ' . $className . ', but actually is an instance of ' . get_class($instance) . '.', 1288967686);
3637
        }
3638
    }
3639
3640
    /**
3641
     * Purge all instances returned by makeInstance.
3642
     *
3643
     * This function is most useful when called from tearDown in a test case
3644
     * to drop any instances that have been created by the tests.
3645
     *
3646
     * Warning: This is a helper method for unit tests. Do not call this directly in production code!
3647
     *
3648
     * @see makeInstance
3649
     */
3650
    public static function purgeInstances()
3651
    {
3652
        self::$container = null;
3653
        self::$singletonInstances = [];
3654
        self::$nonSingletonInstances = [];
3655
    }
3656
3657
    /**
3658
     * Flush internal runtime caches
3659
     *
3660
     * Used in unit tests only.
3661
     *
3662
     * @internal
3663
     */
3664
    public static function flushInternalRuntimeCaches()
3665
    {
3666
        self::$indpEnvCache = [];
3667
    }
3668
3669
    /**
3670
     * Find the best service and check if it works.
3671
     * Returns object of the service class.
3672
     *
3673
     * @param string $serviceType Type of service (service key).
3674
     * @param string $serviceSubType Sub type like file extensions or similar. Defined by the service.
3675
     * @param mixed $excludeServiceKeys List of service keys which should be excluded in the search for a service. Array or comma list.
3676
     * @throws \RuntimeException
3677
     * @return object|string[] The service object or an array with error infos.
3678
     */
3679
    public static function makeInstanceService($serviceType, $serviceSubType = '', $excludeServiceKeys = [])
3680
    {
3681
        $error = false;
3682
        if (!is_array($excludeServiceKeys)) {
3683
            trigger_error('GeneralUtility::makeInstanceService expects the third method argument to be an array instead of a comma-separated string. TYPO3 v11.0 will only support arrays as third argument for $excludeServiceKeys', E_USER_DEPRECATED);
3684
            $excludeServiceKeys = self::trimExplode(',', $excludeServiceKeys, true);
3685
        }
3686
        $requestInfo = [
3687
            'requestedServiceType' => $serviceType,
3688
            'requestedServiceSubType' => $serviceSubType,
3689
            'requestedExcludeServiceKeys' => $excludeServiceKeys
3690
        ];
3691
        while ($info = ExtensionManagementUtility::findService($serviceType, $serviceSubType, $excludeServiceKeys)) {
3692
            // provide information about requested service to service object
3693
            $info = array_merge($info, $requestInfo);
3694
            $obj = self::makeInstance($info['className']);
3695
            if (is_object($obj)) {
3696
                if (!@is_callable([$obj, 'init'])) {
3697
                    self::getLogger()->error('Requested service ' . $info['className'] . ' has no init() method.', ['service' => $info]);
3698
                    throw new \RuntimeException('Broken service: ' . $info['className'], 1568119209);
3699
                }
3700
                $obj->info = $info;
3701
                // service available?
3702
                if ($obj->init()) {
3703
                    return $obj;
3704
                }
3705
                $error = $obj->getLastErrorArray();
3706
                unset($obj);
3707
            }
3708
3709
            // deactivate the service
3710
            ExtensionManagementUtility::deactivateService($info['serviceType'], $info['serviceKey']);
3711
        }
3712
        return $error;
3713
    }
3714
3715
    /**
3716
     * Quotes a string for usage as JS parameter.
3717
     *
3718
     * @param string $value the string to encode, may be empty
3719
     * @return string the encoded value already quoted (with single quotes),
3720
     */
3721
    public static function quoteJSvalue($value)
3722
    {
3723
        return strtr(
3724
            json_encode((string)$value, JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_TAG),
3725
            [
3726
                '"' => '\'',
3727
                '\\\\' => '\\u005C',
3728
                ' ' => '\\u0020',
3729
                '!' => '\\u0021',
3730
                '\\t' => '\\u0009',
3731
                '\\n' => '\\u000A',
3732
                '\\r' => '\\u000D'
3733
            ]
3734
        );
3735
    }
3736
3737
    /**
3738
     * Set the ApplicationContext
3739
     *
3740
     * This function is used by the Bootstrap to hand over the application context. It must not be used anywhere else,
3741
     * because the context shall never be changed on runtime!
3742
     *
3743
     * @param \TYPO3\CMS\Core\Core\ApplicationContext $applicationContext
3744
     * @throws \RuntimeException if applicationContext is overridden
3745
     * @internal This is not a public API method, do not use in own extensions, will probably be removed in TYPO3 v11.
3746
     */
3747
    public static function presetApplicationContext(ApplicationContext $applicationContext)
3748
    {
3749
        if (static::$applicationContext === null) {
3750
            static::$applicationContext = $applicationContext;
3751
        } else {
3752
            throw new \RuntimeException('Trying to override applicationContext which has already been defined!', 1376084316);
3753
        }
3754
    }
3755
3756
    /**
3757
     * For testing purposes only!
3758
     * The functional test framework uses this to reset the internal $application context
3759
     * variable in between multiple tests before it is re-initialized using presetApplicationContext()
3760
     * which otherwise throws an exception if the internal variable is already set.
3761
     *
3762
     * @internal May be changed or removed any time, will probably be removed in TYPO3 v11.
3763
     */
3764
    public static function resetApplicationContext(): void
3765
    {
3766
        static::$applicationContext = null;
3767
    }
3768
3769
    /**
3770
     * Get the ApplicationContext
3771
     *
3772
     * @return \TYPO3\CMS\Core\Core\ApplicationContext
3773
     * @deprecated since TYPO3 v10.2, will be removed in TYPO3 v11, use Environment::getContext() instead.
3774
     */
3775
    public static function getApplicationContext()
3776
    {
3777
        trigger_error('GeneralUtility::getApplicationContext() has been superseded by Environment API. This method will be removed in TYPO3 v11. Use Environment::getContext() instead.', E_USER_DEPRECATED);
3778
        // Implicitly setting the application context here, but only if it is used, otherwise this does not
3779
        // need to be populated.
3780
        if (static::$applicationContext === null) {
3781
            static::$applicationContext = Environment::getContext();
3782
        }
3783
        return static::$applicationContext;
3784
    }
3785
3786
    /**
3787
     * Check if the current request is running on a CGI server API
3788
     * @return bool
3789
     * @deprecated will be removed in TYPO3 v11.0. Use Environment::isRunningOnCgiServer() instead.
3790
     */
3791
    public static function isRunningOnCgiServerApi()
3792
    {
3793
        trigger_error('GeneralUtility::isRunningOnCgiServerApi() will be removed in TYPO3 v11.0. Use "Environment::isRunningOnCgiServer()" instead.', E_USER_DEPRECATED);
3794
        return Environment::isRunningOnCgiServer();
3795
    }
3796
3797
    /**
3798
     * @return LoggerInterface
3799
     */
3800
    protected static function getLogger()
3801
    {
3802
        return static::makeInstance(LogManager::class)->getLogger(__CLASS__);
3803
    }
3804
}
3805