Completed
Push — master ( 8e305d...1d0f8f )
by
unknown
30:04
created

GeneralUtility::isAllowedAbsPath()   B

Complexity

Conditions 7
Paths 12

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

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

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

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

2252
        $fileArr = array_merge($fileArr, /** @scrutinizer ignore-type */ self::getFilesInDir($path, $extList, 1, 1, $excludePattern));
Loading history...
2253
        $dirs = self::get_dirs($path);
2254
        if ($recursivityLevels > 0 && is_array($dirs)) {
0 ignored issues
show
introduced by
The condition is_array($dirs) is always false.
Loading history...
2255
            foreach ($dirs as $subdirs) {
2256
                if ((string)$subdirs !== '' && ($excludePattern === '' || !preg_match('/^' . $excludePattern . '$/', $subdirs))) {
2257
                    $fileArr = self::getAllFilesAndFoldersInPath($fileArr, $path . $subdirs . '/', $extList, $regDirs, $recursivityLevels - 1, $excludePattern);
2258
                }
2259
            }
2260
        }
2261
        return $fileArr;
2262
    }
2263
2264
    /**
2265
     * Removes the absolute part of all files/folders in fileArr
2266
     *
2267
     * @param string[] $fileArr The file array to remove the prefix from
2268
     * @param string $prefixToRemove The prefix path to remove (if found as first part of string!)
2269
     * @return string[]|string The input $fileArr processed, or a string with an error message, when an error occurred.
2270
     */
2271
    public static function removePrefixPathFromList(array $fileArr, $prefixToRemove)
2272
    {
2273
        foreach ($fileArr as $k => &$absFileRef) {
2274
            if (self::isFirstPartOfStr($absFileRef, $prefixToRemove)) {
2275
                $absFileRef = substr($absFileRef, strlen($prefixToRemove));
2276
            } else {
2277
                return 'ERROR: One or more of the files was NOT prefixed with the prefix-path!';
2278
            }
2279
        }
2280
        unset($absFileRef);
2281
        return $fileArr;
2282
    }
2283
2284
    /**
2285
     * Fixes a path for windows-backslashes and reduces double-slashes to single slashes
2286
     *
2287
     * @param string $theFile File path to process
2288
     * @return string
2289
     */
2290
    public static function fixWindowsFilePath($theFile)
2291
    {
2292
        return str_replace(['\\', '//'], '/', $theFile);
2293
    }
2294
2295
    /**
2296
     * Resolves "../" sections in the input path string.
2297
     * For example "fileadmin/directory/../other_directory/" will be resolved to "fileadmin/other_directory/"
2298
     *
2299
     * @param string $pathStr File path in which "/../" is resolved
2300
     * @return string
2301
     */
2302
    public static function resolveBackPath($pathStr)
2303
    {
2304
        if (strpos($pathStr, '..') === false) {
2305
            return $pathStr;
2306
        }
2307
        $parts = explode('/', $pathStr);
2308
        $output = [];
2309
        $c = 0;
2310
        foreach ($parts as $part) {
2311
            if ($part === '..') {
2312
                if ($c) {
2313
                    array_pop($output);
2314
                    --$c;
2315
                } else {
2316
                    $output[] = $part;
2317
                }
2318
            } else {
2319
                ++$c;
2320
                $output[] = $part;
2321
            }
2322
        }
2323
        return implode('/', $output);
2324
    }
2325
2326
    /**
2327
     * Prefixes a URL used with 'header-location' with 'http://...' depending on whether it has it already.
2328
     * - If already having a scheme, nothing is prepended
2329
     * - If having REQUEST_URI slash '/', then prefixing 'http://[host]' (relative to host)
2330
     * - Otherwise prefixed with TYPO3_REQUEST_DIR (relative to current dir / TYPO3_REQUEST_DIR)
2331
     *
2332
     * @param string $path URL / path to prepend full URL addressing to.
2333
     * @return string
2334
     */
2335
    public static function locationHeaderUrl($path)
2336
    {
2337
        if (strpos($path, '//') === 0) {
2338
            return $path;
2339
        }
2340
2341
        // relative to HOST
2342
        if (strpos($path, '/') === 0) {
2343
            return self::getIndpEnv('TYPO3_REQUEST_HOST') . $path;
2344
        }
2345
2346
        $urlComponents = parse_url($path);
2347
        if (!($urlComponents['scheme'] ?? false)) {
2348
            // No scheme either
2349
            return self::getIndpEnv('TYPO3_REQUEST_DIR') . $path;
2350
        }
2351
2352
        return $path;
2353
    }
2354
2355
    /**
2356
     * Returns the maximum upload size for a file that is allowed. Measured in KB.
2357
     * This might be handy to find out the real upload limit that is possible for this
2358
     * TYPO3 installation.
2359
     *
2360
     * @return int The maximum size of uploads that are allowed (measured in kilobytes)
2361
     */
2362
    public static function getMaxUploadFileSize()
2363
    {
2364
        // Check for PHP restrictions of the maximum size of one of the $_FILES
2365
        $phpUploadLimit = self::getBytesFromSizeMeasurement(ini_get('upload_max_filesize'));
2366
        // Check for PHP restrictions of the maximum $_POST size
2367
        $phpPostLimit = self::getBytesFromSizeMeasurement(ini_get('post_max_size'));
2368
        // If the total amount of post data is smaller (!) than the upload_max_filesize directive,
2369
        // then this is the real limit in PHP
2370
        $phpUploadLimit = $phpPostLimit > 0 && $phpPostLimit < $phpUploadLimit ? $phpPostLimit : $phpUploadLimit;
2371
        return floor($phpUploadLimit) / 1024;
2372
    }
2373
2374
    /**
2375
     * Gets the bytes value from a measurement string like "100k".
2376
     *
2377
     * @param string $measurement The measurement (e.g. "100k")
2378
     * @return int The bytes value (e.g. 102400)
2379
     */
2380
    public static function getBytesFromSizeMeasurement($measurement)
2381
    {
2382
        $bytes = (float)$measurement;
2383
        if (stripos($measurement, 'G')) {
2384
            $bytes *= 1024 * 1024 * 1024;
2385
        } elseif (stripos($measurement, 'M')) {
2386
            $bytes *= 1024 * 1024;
2387
        } elseif (stripos($measurement, 'K')) {
2388
            $bytes *= 1024;
2389
        }
2390
        return $bytes;
2391
    }
2392
2393
    /**
2394
     * Function for static version numbers on files, based on the filemtime
2395
     *
2396
     * This will make the filename automatically change when a file is
2397
     * changed, and by that re-cached by the browser. If the file does not
2398
     * exist physically the original file passed to the function is
2399
     * returned without the timestamp.
2400
     *
2401
     * Behaviour is influenced by the setting
2402
     * TYPO3_CONF_VARS[TYPO3_MODE][versionNumberInFilename]
2403
     * = TRUE (BE) / "embed" (FE) : modify filename
2404
     * = FALSE (BE) / "querystring" (FE) : add timestamp as parameter
2405
     *
2406
     * @param string $file Relative path to file including all potential query parameters (not htmlspecialchared yet)
2407
     * @return string Relative path with version filename including the timestamp
2408
     */
2409
    public static function createVersionNumberedFilename($file)
2410
    {
2411
        $lookupFile = explode('?', $file);
2412
        $path = self::resolveBackPath(self::dirname(Environment::getCurrentScript()) . '/' . $lookupFile[0]);
2413
2414
        $doNothing = false;
2415
        if (TYPO3_MODE === 'FE') {
0 ignored issues
show
introduced by
The condition TYPO3\CMS\Core\Utility\TYPO3_MODE === 'FE' is always false.
Loading history...
2416
            $mode = strtolower($GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['versionNumberInFilename']);
2417
            if ($mode === 'embed') {
2418
                $mode = true;
2419
            } else {
2420
                if ($mode === 'querystring') {
2421
                    $mode = false;
2422
                } else {
2423
                    $doNothing = true;
2424
                }
2425
            }
2426
        } else {
2427
            $mode = $GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['versionNumberInFilename'];
2428
        }
2429
        if ($doNothing || !file_exists($path)) {
2430
            // File not found, return filename unaltered
2431
            $fullName = $file;
2432
        } else {
2433
            if (!$mode) {
2434
                // If use of .htaccess rule is not configured,
2435
                // we use the default query-string method
2436
                if (!empty($lookupFile[1])) {
2437
                    $separator = '&';
2438
                } else {
2439
                    $separator = '?';
2440
                }
2441
                $fullName = $file . $separator . filemtime($path);
2442
            } else {
2443
                // Change the filename
2444
                $name = explode('.', $lookupFile[0]);
2445
                $extension = array_pop($name);
2446
                array_push($name, filemtime($path), $extension);
2447
                $fullName = implode('.', $name);
2448
                // Append potential query string
2449
                $fullName .= $lookupFile[1] ? '?' . $lookupFile[1] : '';
2450
            }
2451
        }
2452
        return $fullName;
2453
    }
2454
2455
    /**
2456
     * Writes string to a temporary file named after the md5-hash of the string
2457
     * Quite useful for extensions adding their custom built JavaScript during runtime.
2458
     *
2459
     * @param string $content JavaScript to write to file.
2460
     * @return string filename to include in the <script> tag
2461
     */
2462
    public static function writeJavaScriptContentToTemporaryFile(string $content)
2463
    {
2464
        $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...
2465
        if (!@is_file(Environment::getPublicPath() . '/' . $script)) {
2466
            self::writeFileToTypo3tempDir(Environment::getPublicPath() . '/' . $script, $content);
2467
        }
2468
        return $script;
2469
    }
2470
2471
    /**
2472
     * Writes string to a temporary file named after the md5-hash of the string
2473
     * Quite useful for extensions adding their custom built StyleSheet during runtime.
2474
     *
2475
     * @param string $content CSS styles to write to file.
2476
     * @return string filename to include in the <link> tag
2477
     */
2478
    public static function writeStyleSheetContentToTemporaryFile(string $content)
2479
    {
2480
        $script = 'typo3temp/assets/css/' . self::shortMD5($content) . '.css';
2481
        if (!@is_file(Environment::getPublicPath() . '/' . $script)) {
2482
            self::writeFileToTypo3tempDir(Environment::getPublicPath() . '/' . $script, $content);
2483
        }
2484
        return $script;
2485
    }
2486
2487
    /*************************
2488
     *
2489
     * SYSTEM INFORMATION
2490
     *
2491
     *************************/
2492
2493
    /**
2494
     * Returns the link-url to the current script.
2495
     * 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.
2496
     * REMEMBER to always use htmlspecialchars() for content in href-properties to get ampersands converted to entities (XHTML requirement and XSS precaution)
2497
     *
2498
     * @param array $getParams Array of GET parameters to include
2499
     * @return string
2500
     */
2501
    public static function linkThisScript(array $getParams = [])
2502
    {
2503
        $parts = self::getIndpEnv('SCRIPT_NAME');
2504
        $params = self::_GET();
2505
        foreach ($getParams as $key => $value) {
2506
            if ($value !== '') {
2507
                $params[$key] = $value;
2508
            } else {
2509
                unset($params[$key]);
2510
            }
2511
        }
2512
        $pString = self::implodeArrayForUrl('', $params);
2513
        return $pString ? $parts . '?' . ltrim($pString, '&') : $parts;
2514
    }
2515
2516
    /**
2517
     * 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.
2518
     * So basically it adds the parameters in $getParams to an existing URL, $url
2519
     *
2520
     * @param string $url URL string
2521
     * @param array $getParams Array of key/value pairs for get parameters to add/overrule with. Can be multidimensional.
2522
     * @return string Output URL with added getParams.
2523
     * @deprecated will be removed in TYPO3 v11.0. Use PSR-7 URI objects instead.
2524
     */
2525
    public static function linkThisUrl($url, array $getParams = [])
2526
    {
2527
        trigger_error('GeneralUtility::linkThisUrl() will be removed in TYPO3 v11.0. Use PSR-7 URI objects instead.', E_USER_DEPRECATED);
2528
        $parts = parse_url($url);
2529
        $getP = [];
2530
        if ($parts['query']) {
2531
            parse_str($parts['query'], $getP);
2532
        }
2533
        ArrayUtility::mergeRecursiveWithOverrule($getP, $getParams);
2534
        $uP = explode('?', $url);
2535
        $params = self::implodeArrayForUrl('', $getP);
2536
        $outurl = $uP[0] . ($params ? '?' . substr($params, 1) : '');
2537
        return $outurl;
2538
    }
2539
2540
    /**
2541
     * This method is only for testing and should never be used outside tests-
2542
     *
2543
     * @param string $envName
2544
     * @param mixed $value
2545
     * @internal
2546
     */
2547
    public static function setIndpEnv($envName, $value)
2548
    {
2549
        self::$indpEnvCache[$envName] = $value;
2550
    }
2551
2552
    /**
2553
     * Abstraction method which returns System Environment Variables regardless of server OS, CGI/MODULE version etc. Basically this is SERVER variables for most of them.
2554
     * This should be used instead of getEnv() and $_SERVER/ENV_VARS to get reliable values for all situations.
2555
     *
2556
     * @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
2557
     * @return string Value based on the input key, independent of server/os environment.
2558
     * @throws \UnexpectedValueException
2559
     */
2560
    public static function getIndpEnv($getEnvName)
2561
    {
2562
        if (array_key_exists($getEnvName, self::$indpEnvCache)) {
2563
            return self::$indpEnvCache[$getEnvName];
2564
        }
2565
2566
        /*
2567
        Conventions:
2568
        output from parse_url():
2569
        URL:	http://username:[email protected]:8080/typo3/32/temp/phpcheck/index.php/arg1/arg2/arg3/?arg1,arg2,arg3&p1=parameter1&p2[key]=value#link1
2570
        [scheme] => 'http'
2571
        [user] => 'username'
2572
        [pass] => 'password'
2573
        [host] => '192.168.1.4'
2574
        [port] => '8080'
2575
        [path] => '/typo3/32/temp/phpcheck/index.php/arg1/arg2/arg3/'
2576
        [query] => 'arg1,arg2,arg3&p1=parameter1&p2[key]=value'
2577
        [fragment] => 'link1'Further definition: [path_script] = '/typo3/32/temp/phpcheck/index.php'
2578
        [path_dir] = '/typo3/32/temp/phpcheck/'
2579
        [path_info] = '/arg1/arg2/arg3/'
2580
        [path] = [path_script/path_dir][path_info]Keys supported:URI______:
2581
        REQUEST_URI		=	[path]?[query]		= /typo3/32/temp/phpcheck/index.php/arg1/arg2/arg3/?arg1,arg2,arg3&p1=parameter1&p2[key]=value
2582
        HTTP_HOST		=	[host][:[port]]		= 192.168.1.4:8080
2583
        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')!
2584
        PATH_INFO		=	[path_info]			= /arg1/arg2/arg3/
2585
        QUERY_STRING	=	[query]				= arg1,arg2,arg3&p1=parameter1&p2[key]=value
2586
        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
2587
        (Notice: NO username/password + NO fragment)CLIENT____:
2588
        REMOTE_ADDR		=	(client IP)
2589
        REMOTE_HOST		=	(client host)
2590
        HTTP_USER_AGENT	=	(client user agent)
2591
        HTTP_ACCEPT_LANGUAGE	= (client accept language)SERVER____:
2592
        SCRIPT_FILENAME	=	Absolute filename of script		(Differs between windows/unix). On windows 'C:\\some\\path\\' will be converted to 'C:/some/path/'Special extras:
2593
        TYPO3_HOST_ONLY =		[host] = 192.168.1.4
2594
        TYPO3_PORT =			[port] = 8080 (blank if 80, taken from host value)
2595
        TYPO3_REQUEST_HOST = 		[scheme]://[host][:[port]]
2596
        TYPO3_REQUEST_URL =		[scheme]://[host][:[port]][path]?[query] (scheme will by default be "http" until we can detect something different)
2597
        TYPO3_REQUEST_SCRIPT =  	[scheme]://[host][:[port]][path_script]
2598
        TYPO3_REQUEST_DIR =		[scheme]://[host][:[port]][path_dir]
2599
        TYPO3_SITE_URL = 		[scheme]://[host][:[port]][path_dir] of the TYPO3 website frontend
2600
        TYPO3_SITE_PATH = 		[path_dir] of the TYPO3 website frontend
2601
        TYPO3_SITE_SCRIPT = 		[script / Speaking URL] of the TYPO3 website
2602
        TYPO3_DOCUMENT_ROOT =		Absolute path of root of documents: TYPO3_DOCUMENT_ROOT.SCRIPT_NAME = SCRIPT_FILENAME (typically)
2603
        TYPO3_SSL = 			Returns TRUE if this session uses SSL/TLS (https)
2604
        TYPO3_PROXY = 			Returns TRUE if this session runs over a well known proxyNotice: [fragment] is apparently NEVER available to the script!Testing suggestions:
2605
        - Output all the values.
2606
        - 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
2607
        - ALSO TRY the script from the ROOT of a site (like 'http://www.mytest.com/' and not 'http://www.mytest.com/test/' !!)
2608
         */
2609
        $retVal = '';
2610
        switch ((string)$getEnvName) {
2611
            case 'SCRIPT_NAME':
2612
                $retVal = Environment::isRunningOnCgiServer()
2613
                    && (($_SERVER['ORIG_PATH_INFO'] ?? false) ?: ($_SERVER['PATH_INFO'] ?? false))
2614
                        ? (($_SERVER['ORIG_PATH_INFO'] ?? '') ?: ($_SERVER['PATH_INFO'] ?? ''))
0 ignored issues
show
Coding Style introduced by
Expected 1 space before "?"; newline found
Loading history...
2615
                        : (($_SERVER['ORIG_SCRIPT_NAME'] ?? '') ?: ($_SERVER['SCRIPT_NAME'] ?? ''));
0 ignored issues
show
Coding Style introduced by
Expected 1 space before ":"; newline found
Loading history...
2616
                // Add a prefix if TYPO3 is behind a proxy: ext-domain.com => int-server.com/prefix
2617
                if (self::cmpIP($_SERVER['REMOTE_ADDR'] ?? '', $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP'] ?? '')) {
2618
                    if (self::getIndpEnv('TYPO3_SSL') && $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefixSSL']) {
2619
                        $retVal = $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefixSSL'] . $retVal;
2620
                    } elseif ($GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefix']) {
2621
                        $retVal = $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefix'] . $retVal;
2622
                    }
2623
                }
2624
                break;
2625
            case 'SCRIPT_FILENAME':
2626
                $retVal = Environment::getCurrentScript();
2627
                break;
2628
            case 'REQUEST_URI':
2629
                // Typical application of REQUEST_URI is return urls, forms submitting to itself etc. Example: returnUrl='.rawurlencode(\TYPO3\CMS\Core\Utility\GeneralUtility::getIndpEnv('REQUEST_URI'))
2630
                if (!empty($GLOBALS['TYPO3_CONF_VARS']['SYS']['requestURIvar'])) {
2631
                    // This is for URL rewriters that store the original URI in a server variable (eg ISAPI_Rewriter for IIS: HTTP_X_REWRITE_URL)
2632
                    [$v, $n] = explode('|', $GLOBALS['TYPO3_CONF_VARS']['SYS']['requestURIvar']);
2633
                    $retVal = $GLOBALS[$v][$n];
2634
                } elseif (empty($_SERVER['REQUEST_URI'])) {
2635
                    // This is for ISS/CGI which does not have the REQUEST_URI available.
2636
                    $retVal = '/' . ltrim(self::getIndpEnv('SCRIPT_NAME'), '/') . (!empty($_SERVER['QUERY_STRING']) ? '?' . $_SERVER['QUERY_STRING'] : '');
2637
                } else {
2638
                    $retVal = '/' . ltrim($_SERVER['REQUEST_URI'], '/');
2639
                }
2640
                // Add a prefix if TYPO3 is behind a proxy: ext-domain.com => int-server.com/prefix
2641
                if (isset($_SERVER['REMOTE_ADDR'], $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP'])
2642
                    && self::cmpIP($_SERVER['REMOTE_ADDR'], $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP'])
2643
                ) {
2644
                    if (self::getIndpEnv('TYPO3_SSL') && $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefixSSL']) {
2645
                        $retVal = $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefixSSL'] . $retVal;
2646
                    } elseif ($GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefix']) {
2647
                        $retVal = $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefix'] . $retVal;
2648
                    }
2649
                }
2650
                break;
2651
            case 'PATH_INFO':
2652
                // $_SERVER['PATH_INFO'] != $_SERVER['SCRIPT_NAME'] is necessary because some servers (Windows/CGI)
2653
                // are seen to set PATH_INFO equal to script_name
2654
                // Further, there must be at least one '/' in the path - else the PATH_INFO value does not make sense.
2655
                // IF 'PATH_INFO' never works for our purpose in TYPO3 with CGI-servers,
2656
                // then 'PHP_SAPI=='cgi'' might be a better check.
2657
                // Right now strcmp($_SERVER['PATH_INFO'], GeneralUtility::getIndpEnv('SCRIPT_NAME')) will always
2658
                // return FALSE for CGI-versions, but that is only as long as SCRIPT_NAME is set equal to PATH_INFO
2659
                // because of PHP_SAPI=='cgi' (see above)
2660
                if (!Environment::isRunningOnCgiServer()) {
2661
                    $retVal = $_SERVER['PATH_INFO'];
2662
                }
2663
                break;
2664
            case 'TYPO3_REV_PROXY':
2665
                $retVal = self::cmpIP($_SERVER['REMOTE_ADDR'], $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP']);
2666
                break;
2667
            case 'REMOTE_ADDR':
2668
                $retVal = $_SERVER['REMOTE_ADDR'] ?? null;
2669
                if (self::cmpIP($retVal, $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP'] ?? '')) {
2670
                    $ip = self::trimExplode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
2671
                    // Choose which IP in list to use
2672
                    if (!empty($ip)) {
2673
                        switch ($GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyHeaderMultiValue']) {
2674
                            case 'last':
2675
                                $ip = array_pop($ip);
2676
                                break;
2677
                            case 'first':
2678
                                $ip = array_shift($ip);
2679
                                break;
2680
                            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...
2681
2682
                            default:
2683
                                $ip = '';
2684
                        }
2685
                    }
2686
                    if (self::validIP($ip)) {
2687
                        $retVal = $ip;
2688
                    }
2689
                }
2690
                break;
2691
            case 'HTTP_HOST':
2692
                // if it is not set we're most likely on the cli
2693
                $retVal = $_SERVER['HTTP_HOST'] ?? null;
2694
                if (isset($_SERVER['REMOTE_ADDR']) && static::cmpIP($_SERVER['REMOTE_ADDR'], $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP'])) {
2695
                    $host = self::trimExplode(',', $_SERVER['HTTP_X_FORWARDED_HOST']);
2696
                    // Choose which host in list to use
2697
                    if (!empty($host)) {
2698
                        switch ($GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyHeaderMultiValue']) {
2699
                            case 'last':
2700
                                $host = array_pop($host);
2701
                                break;
2702
                            case 'first':
2703
                                $host = array_shift($host);
2704
                                break;
2705
                            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...
2706
2707
                            default:
2708
                                $host = '';
2709
                        }
2710
                    }
2711
                    if ($host) {
2712
                        $retVal = $host;
2713
                    }
2714
                }
2715
                if (!static::isAllowedHostHeaderValue($retVal)) {
2716
                    throw new \UnexpectedValueException(
2717
                        '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.',
2718
                        1396795884
2719
                    );
2720
                }
2721
                break;
2722
            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...
2723
2724
            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...
2725
2726
            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...
2727
2728
            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...
2729
2730
            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...
2731
2732
            case 'QUERY_STRING':
2733
                $retVal = $_SERVER[$getEnvName] ?? '';
2734
                break;
2735
            case 'TYPO3_DOCUMENT_ROOT':
2736
                // Get the web root (it is not the root of the TYPO3 installation)
2737
                // The absolute path of the script can be calculated with TYPO3_DOCUMENT_ROOT + SCRIPT_FILENAME
2738
                // 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.
2739
                // Therefore the DOCUMENT_ROOT is now always calculated as the SCRIPT_FILENAME minus the end part shared with SCRIPT_NAME.
2740
                $SFN = self::getIndpEnv('SCRIPT_FILENAME');
2741
                $SN_A = explode('/', strrev(self::getIndpEnv('SCRIPT_NAME')));
2742
                $SFN_A = explode('/', strrev($SFN));
2743
                $acc = [];
2744
                foreach ($SN_A as $kk => $vv) {
2745
                    if ((string)$SFN_A[$kk] === (string)$vv) {
2746
                        $acc[] = $vv;
2747
                    } else {
2748
                        break;
2749
                    }
2750
                }
2751
                $commonEnd = strrev(implode('/', $acc));
2752
                if ((string)$commonEnd !== '') {
2753
                    $retVal = substr($SFN, 0, -(strlen($commonEnd) + 1));
2754
                }
2755
                break;
2756
            case 'TYPO3_HOST_ONLY':
2757
                $httpHost = self::getIndpEnv('HTTP_HOST');
2758
                $httpHostBracketPosition = strpos($httpHost, ']');
2759
                $httpHostParts = explode(':', $httpHost);
2760
                $retVal = $httpHostBracketPosition !== false ? substr($httpHost, 0, $httpHostBracketPosition + 1) : array_shift($httpHostParts);
2761
                break;
2762
            case 'TYPO3_PORT':
2763
                $httpHost = self::getIndpEnv('HTTP_HOST');
2764
                $httpHostOnly = self::getIndpEnv('TYPO3_HOST_ONLY');
2765
                $retVal = strlen($httpHost) > strlen($httpHostOnly) ? substr($httpHost, strlen($httpHostOnly) + 1) : '';
2766
                break;
2767
            case 'TYPO3_REQUEST_HOST':
2768
                $retVal = (self::getIndpEnv('TYPO3_SSL') ? 'https://' : 'http://') . self::getIndpEnv('HTTP_HOST');
2769
                break;
2770
            case 'TYPO3_REQUEST_URL':
2771
                $retVal = self::getIndpEnv('TYPO3_REQUEST_HOST') . self::getIndpEnv('REQUEST_URI');
2772
                break;
2773
            case 'TYPO3_REQUEST_SCRIPT':
2774
                $retVal = self::getIndpEnv('TYPO3_REQUEST_HOST') . self::getIndpEnv('SCRIPT_NAME');
2775
                break;
2776
            case 'TYPO3_REQUEST_DIR':
2777
                $retVal = self::getIndpEnv('TYPO3_REQUEST_HOST') . self::dirname(self::getIndpEnv('SCRIPT_NAME')) . '/';
2778
                break;
2779
            case 'TYPO3_SITE_URL':
2780
                $url = self::getIndpEnv('TYPO3_REQUEST_DIR');
2781
                // This can only be set by external entry scripts
2782
                if (defined('TYPO3_PATH_WEB')) {
2783
                    $retVal = $url;
2784
                } elseif (Environment::getCurrentScript()) {
2785
                    $lPath = PathUtility::stripPathSitePrefix(PathUtility::dirnameDuringBootstrap(Environment::getCurrentScript())) . '/';
2786
                    $siteUrl = substr($url, 0, -strlen($lPath));
2787
                    if (substr($siteUrl, -1) !== '/') {
2788
                        $siteUrl .= '/';
2789
                    }
2790
                    $retVal = $siteUrl;
2791
                }
2792
                break;
2793
            case 'TYPO3_SITE_PATH':
2794
                $retVal = substr(self::getIndpEnv('TYPO3_SITE_URL'), strlen(self::getIndpEnv('TYPO3_REQUEST_HOST')));
2795
                break;
2796
            case 'TYPO3_SITE_SCRIPT':
2797
                $retVal = substr(self::getIndpEnv('TYPO3_REQUEST_URL'), strlen(self::getIndpEnv('TYPO3_SITE_URL')));
2798
                break;
2799
            case 'TYPO3_SSL':
2800
                $proxySSL = trim($GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxySSL'] ?? null);
2801
                if ($proxySSL === '*') {
2802
                    $proxySSL = $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP'];
2803
                }
2804
                if (self::cmpIP($_SERVER['REMOTE_ADDR'] ?? '', $proxySSL)) {
2805
                    $retVal = true;
2806
                } else {
2807
                    // https://secure.php.net/manual/en/reserved.variables.server.php
2808
                    // "Set to a non-empty value if the script was queried through the HTTPS protocol."
2809
                    $retVal = !empty($_SERVER['SSL_SESSION_ID'])
2810
                        || (!empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) !== 'off');
2811
                }
2812
                break;
2813
            case '_ARRAY':
2814
                $out = [];
2815
                // Here, list ALL possible keys to this function for debug display.
2816
                $envTestVars = [
2817
                    'HTTP_HOST',
2818
                    'TYPO3_HOST_ONLY',
2819
                    'TYPO3_PORT',
2820
                    'PATH_INFO',
2821
                    'QUERY_STRING',
2822
                    'REQUEST_URI',
2823
                    'HTTP_REFERER',
2824
                    'TYPO3_REQUEST_HOST',
2825
                    'TYPO3_REQUEST_URL',
2826
                    'TYPO3_REQUEST_SCRIPT',
2827
                    'TYPO3_REQUEST_DIR',
2828
                    'TYPO3_SITE_URL',
2829
                    'TYPO3_SITE_SCRIPT',
2830
                    'TYPO3_SSL',
2831
                    'TYPO3_REV_PROXY',
2832
                    'SCRIPT_NAME',
2833
                    'TYPO3_DOCUMENT_ROOT',
2834
                    'SCRIPT_FILENAME',
2835
                    'REMOTE_ADDR',
2836
                    'REMOTE_HOST',
2837
                    'HTTP_USER_AGENT',
2838
                    'HTTP_ACCEPT_LANGUAGE'
2839
                ];
2840
                foreach ($envTestVars as $v) {
2841
                    $out[$v] = self::getIndpEnv($v);
2842
                }
2843
                reset($out);
2844
                $retVal = $out;
2845
                break;
2846
        }
2847
        self::$indpEnvCache[$getEnvName] = $retVal;
2848
        return $retVal;
2849
    }
2850
2851
    /**
2852
     * Checks if the provided host header value matches the trusted hosts pattern.
2853
     * If the pattern is not defined (which only can happen early in the bootstrap), deny any value.
2854
     * The result is saved, so the check needs to be executed only once.
2855
     *
2856
     * @param string $hostHeaderValue HTTP_HOST header value as sent during the request (may include port)
2857
     * @return bool
2858
     */
2859
    public static function isAllowedHostHeaderValue($hostHeaderValue)
2860
    {
2861
        if (static::$allowHostHeaderValue === true) {
2862
            return true;
2863
        }
2864
2865
        if (static::isInternalRequestType()) {
2866
            return static::$allowHostHeaderValue = true;
2867
        }
2868
2869
        // Deny the value if trusted host patterns is empty, which means we are early in the bootstrap
2870
        if (empty($GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern'])) {
2871
            return false;
2872
        }
2873
2874
        if ($GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern'] === self::ENV_TRUSTED_HOSTS_PATTERN_ALLOW_ALL) {
2875
            static::$allowHostHeaderValue = true;
2876
        } else {
2877
            static::$allowHostHeaderValue = static::hostHeaderValueMatchesTrustedHostsPattern($hostHeaderValue);
2878
        }
2879
2880
        return static::$allowHostHeaderValue;
2881
    }
2882
2883
    /**
2884
     * Checks if the provided host header value matches the trusted hosts pattern without any preprocessing.
2885
     *
2886
     * @param string $hostHeaderValue
2887
     * @return bool
2888
     * @internal
2889
     */
2890
    public static function hostHeaderValueMatchesTrustedHostsPattern($hostHeaderValue)
2891
    {
2892
        if ($GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern'] === self::ENV_TRUSTED_HOSTS_PATTERN_SERVER_NAME) {
2893
            // Allow values that equal the server name
2894
            // Note that this is only secure if name base virtual host are configured correctly in the webserver
2895
            $defaultPort = self::getIndpEnv('TYPO3_SSL') ? '443' : '80';
2896
            $parsedHostValue = parse_url('http://' . $hostHeaderValue);
2897
            if (isset($parsedHostValue['port'])) {
2898
                $hostMatch = (strtolower($parsedHostValue['host']) === strtolower($_SERVER['SERVER_NAME']) && (string)$parsedHostValue['port'] === $_SERVER['SERVER_PORT']);
2899
            } else {
2900
                $hostMatch = (strtolower($hostHeaderValue) === strtolower($_SERVER['SERVER_NAME']) && $defaultPort === $_SERVER['SERVER_PORT']);
2901
            }
2902
        } else {
2903
            // In case name based virtual hosts are not possible, we allow setting a trusted host pattern
2904
            // See https://typo3.org/teams/security/security-bulletins/typo3-core/typo3-core-sa-2014-001/ for further details
2905
            $hostMatch = (bool)preg_match('/^' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern'] . '$/i', $hostHeaderValue);
2906
        }
2907
2908
        return $hostMatch;
2909
    }
2910
2911
    /**
2912
     * Allows internal requests to the install tool and from the command line.
2913
     * We accept this risk to have the install tool always available.
2914
     * Also CLI needs to be allowed as unfortunately AbstractUserAuthentication::getAuthInfoArray()
2915
     * accesses HTTP_HOST without reason on CLI
2916
     * Additionally, allows requests when no REQUESTTYPE is set, which can happen quite early in the
2917
     * Bootstrap. See Application.php in EXT:backend/Classes/Http/.
2918
     *
2919
     * @return bool
2920
     */
2921
    protected static function isInternalRequestType()
2922
    {
2923
        return Environment::isCli() || !defined('TYPO3_REQUESTTYPE') || (defined('TYPO3_REQUESTTYPE') && TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_INSTALL);
2924
    }
2925
2926
    /**
2927
     * Gets the unixtime as milliseconds.
2928
     *
2929
     * @return int The unixtime as milliseconds
2930
     * @deprecated will be removed in TYPO3 v11.0. Use the native PHP functions round(microtime(true) * 1000) instead.
2931
     */
2932
    public static function milliseconds()
2933
    {
2934
        trigger_error('GeneralUtility::milliseconds() will be removed in TYPO3 v11.0. Use the native PHP functions round(microtime(true) * 1000) instead.', E_USER_DEPRECATED);
2935
        return round(microtime(true) * 1000);
2936
    }
2937
2938
    /*************************
2939
     *
2940
     * TYPO3 SPECIFIC FUNCTIONS
2941
     *
2942
     *************************/
2943
    /**
2944
     * Returns the absolute filename of a relative reference, resolves the "EXT:" prefix
2945
     * (way of referring to files inside extensions) and checks that the file is inside
2946
     * the TYPO3's base folder and implies a check with
2947
     * \TYPO3\CMS\Core\Utility\GeneralUtility::validPathStr().
2948
     *
2949
     * @param string $filename The input filename/filepath to evaluate
2950
     * @return string Returns the absolute filename of $filename if valid, otherwise blank string.
2951
     */
2952
    public static function getFileAbsFileName($filename)
2953
    {
2954
        if ((string)$filename === '') {
2955
            return '';
2956
        }
2957
        // Extension
2958
        if (strpos($filename, 'EXT:') === 0) {
2959
            [$extKey, $local] = explode('/', substr($filename, 4), 2);
2960
            $filename = '';
2961
            if ((string)$extKey !== '' && ExtensionManagementUtility::isLoaded($extKey) && (string)$local !== '') {
2962
                $filename = ExtensionManagementUtility::extPath($extKey) . $local;
2963
            }
2964
        } elseif (!static::isAbsPath($filename)) {
2965
            // is relative. Prepended with the public web folder
2966
            $filename = Environment::getPublicPath() . '/' . $filename;
2967
        } elseif (!(
2968
            static::isFirstPartOfStr($filename, Environment::getProjectPath())
2969
                  || static::isFirstPartOfStr($filename, Environment::getPublicPath())
2970
        )) {
2971
            // absolute, but set to blank if not allowed
2972
            $filename = '';
2973
        }
2974
        if ((string)$filename !== '' && static::validPathStr($filename)) {
2975
            // checks backpath.
2976
            return $filename;
2977
        }
2978
        return '';
2979
    }
2980
2981
    /**
2982
     * Checks for malicious file paths.
2983
     *
2984
     * Returns TRUE if no '//', '..', '\' or control characters are found in the $theFile.
2985
     * This should make sure that the path is not pointing 'backwards' and further doesn't contain double/back slashes.
2986
     * So it's compatible with the UNIX style path strings valid for TYPO3 internally.
2987
     *
2988
     * @param string $theFile File path to evaluate
2989
     * @return bool TRUE, $theFile is allowed path string, FALSE otherwise
2990
     * @see http://php.net/manual/en/security.filesystem.nullbytes.php
2991
     */
2992
    public static function validPathStr($theFile)
2993
    {
2994
        return strpos($theFile, '//') === false && strpos($theFile, '\\') === false
2995
            && preg_match('#(?:^\\.\\.|/\\.\\./|[[:cntrl:]])#u', $theFile) === 0;
2996
    }
2997
2998
    /**
2999
     * Checks if the $path is absolute or relative (detecting either '/' or 'x:/' as first part of string) and returns TRUE if so.
3000
     *
3001
     * @param string $path File path to evaluate
3002
     * @return bool
3003
     */
3004
    public static function isAbsPath($path)
3005
    {
3006
        if (substr($path, 0, 6) === 'vfs://') {
3007
            return true;
3008
        }
3009
        return isset($path[0]) && $path[0] === '/' || Environment::isWindows() && (strpos($path, ':/') === 1 || strpos($path, ':\\') === 1);
3010
    }
3011
3012
    /**
3013
     * Returns TRUE if the path is absolute, without backpath '..' and within TYPO3s project or public folder OR within the lockRootPath
3014
     *
3015
     * @param string $path File path to evaluate
3016
     * @return bool
3017
     */
3018
    public static function isAllowedAbsPath($path)
3019
    {
3020
        if (substr($path, 0, 6) === 'vfs://') {
3021
            return true;
3022
        }
3023
        $lockRootPath = $GLOBALS['TYPO3_CONF_VARS']['BE']['lockRootPath'];
3024
        return static::isAbsPath($path) && static::validPathStr($path)
3025
            && (
3026
                static::isFirstPartOfStr($path, Environment::getProjectPath())
3027
                || static::isFirstPartOfStr($path, Environment::getPublicPath())
3028
                || $lockRootPath && static::isFirstPartOfStr($path, $lockRootPath)
3029
            );
3030
    }
3031
3032
    /**
3033
     * Verifies the input filename against the 'fileDenyPattern'. Returns TRUE if OK.
3034
     *
3035
     * Filenames are not allowed to contain control characters. Therefore we
3036
     * always filter on [[:cntrl:]].
3037
     *
3038
     * @param string $filename File path to evaluate
3039
     * @return bool
3040
     * @deprecated will be removed in TYPO3 v11.0. Use the new FileNameValidator API instead.
3041
     */
3042
    public static function verifyFilenameAgainstDenyPattern($filename)
3043
    {
3044
        trigger_error('GeneralUtility::verifyFilenameAgainstDenyPattern() will be removed in TYPO3 v11.0. Use FileNameValidator->isValid($filename) instead.', E_USER_DEPRECATED);
3045
        return self::makeInstance(FileNameValidator::class)->isValid((string)$filename);
3046
    }
3047
3048
    /**
3049
     * Low level utility function to copy directories and content recursive
3050
     *
3051
     * @param string $source Path to source directory, relative to document root or absolute
3052
     * @param string $destination Path to destination directory, relative to document root or absolute
3053
     */
3054
    public static function copyDirectory($source, $destination)
3055
    {
3056
        if (strpos($source, Environment::getProjectPath() . '/') === false) {
3057
            $source = Environment::getPublicPath() . '/' . $source;
3058
        }
3059
        if (strpos($destination, Environment::getProjectPath() . '/') === false) {
3060
            $destination = Environment::getPublicPath() . '/' . $destination;
3061
        }
3062
        if (static::isAllowedAbsPath($source) && static::isAllowedAbsPath($destination)) {
3063
            static::mkdir_deep($destination);
3064
            $iterator = new \RecursiveIteratorIterator(
3065
                new \RecursiveDirectoryIterator($source, \RecursiveDirectoryIterator::SKIP_DOTS),
3066
                \RecursiveIteratorIterator::SELF_FIRST
3067
            );
3068
            /** @var \SplFileInfo $item */
3069
            foreach ($iterator as $item) {
3070
                $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

3070
                $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...
3071
                if ($item->isDir()) {
3072
                    static::mkdir($target);
3073
                } else {
3074
                    static::upload_copy_move(static::fixWindowsFilePath($item->getPathname()), $target);
3075
                }
3076
            }
3077
        }
3078
    }
3079
3080
    /**
3081
     * Checks if a given string is a valid frame URL to be loaded in the
3082
     * backend.
3083
     *
3084
     * If the given url is empty or considered to be harmless, it is returned
3085
     * as is, else the event is logged and an empty string is returned.
3086
     *
3087
     * @param string $url potential URL to check
3088
     * @return string $url or empty string
3089
     */
3090
    public static function sanitizeLocalUrl($url = '')
3091
    {
3092
        $sanitizedUrl = '';
3093
        if (!empty($url)) {
3094
            $decodedUrl = rawurldecode($url);
3095
            $parsedUrl = parse_url($decodedUrl);
3096
            $testAbsoluteUrl = self::resolveBackPath($decodedUrl);
3097
            $testRelativeUrl = self::resolveBackPath(self::dirname(self::getIndpEnv('SCRIPT_NAME')) . '/' . $decodedUrl);
3098
            // Pass if URL is on the current host:
3099
            if (self::isValidUrl($decodedUrl)) {
3100
                if (self::isOnCurrentHost($decodedUrl) && strpos($decodedUrl, self::getIndpEnv('TYPO3_SITE_URL')) === 0) {
3101
                    $sanitizedUrl = $url;
3102
                }
3103
            } elseif (self::isAbsPath($decodedUrl) && self::isAllowedAbsPath($decodedUrl)) {
3104
                $sanitizedUrl = $url;
3105
            } elseif (strpos($testAbsoluteUrl, self::getIndpEnv('TYPO3_SITE_PATH')) === 0 && $decodedUrl[0] === '/') {
3106
                $sanitizedUrl = $url;
3107
            } elseif (empty($parsedUrl['scheme']) && strpos($testRelativeUrl, self::getIndpEnv('TYPO3_SITE_PATH')) === 0
3108
                && $decodedUrl[0] !== '/' && strpbrk($decodedUrl, '*:|"<>') === false && strpos($decodedUrl, '\\\\') === false
3109
            ) {
3110
                $sanitizedUrl = $url;
3111
            }
3112
        }
3113
        if (!empty($url) && empty($sanitizedUrl)) {
3114
            static::getLogger()->notice('The URL "' . $url . '" is not considered to be local and was denied.');
3115
        }
3116
        return $sanitizedUrl;
3117
    }
3118
3119
    /**
3120
     * Moves $source file to $destination if uploaded, otherwise try to make a copy
3121
     *
3122
     * @param string $source Source file, absolute path
3123
     * @param string $destination Destination file, absolute path
3124
     * @return bool Returns TRUE if the file was moved.
3125
     * @see upload_to_tempfile()
3126
     */
3127
    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...
3128
    {
3129
        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...
3130
            $params = ['source' => $source, 'destination' => $destination, 'method' => 'upload_copy_move'];
3131
            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...
3132
                $fakeThis = false;
3133
                self::callUserFunction($hookMethod, $params, $fakeThis);
3134
            }
3135
        }
3136
3137
        $result = false;
3138
        if (is_uploaded_file($source)) {
3139
            // Return the value of move_uploaded_file, and if FALSE the temporary $source is still
3140
            // around so the user can use unlink to delete it:
3141
            $result = move_uploaded_file($source, $destination);
3142
        } else {
3143
            @copy($source, $destination);
3144
        }
3145
        // Change the permissions of the file
3146
        self::fixPermissions($destination);
3147
        // If here the file is copied and the temporary $source is still around,
3148
        // so when returning FALSE the user can try unlink to delete the $source
3149
        return $result;
3150
    }
3151
3152
    /**
3153
     * Will move an uploaded file (normally in "/tmp/xxxxx") to a temporary filename in Environment::getProjectPath() . "var/" from where TYPO3 can use it.
3154
     * Use this function to move uploaded files to where you can work on them.
3155
     * 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!
3156
     *
3157
     * @param string $uploadedFileName The temporary uploaded filename, eg. $_FILES['[upload field name here]']['tmp_name']
3158
     * @return string If a new file was successfully created, return its filename, otherwise blank string.
3159
     * @see unlink_tempfile()
3160
     * @see upload_copy_move()
3161
     */
3162
    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...
3163
    {
3164
        if (is_uploaded_file($uploadedFileName)) {
3165
            $tempFile = self::tempnam('upload_temp_');
3166
            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...
3167
                $params = ['source' => $uploadedFileName, 'destination' => $tempFile, 'method' => 'upload_to_tempfile'];
3168
                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...
3169
                    $fakeThis = false;
3170
                    self::callUserFunction($hookMethod, $params, $fakeThis);
3171
                }
3172
            }
3173
3174
            move_uploaded_file($uploadedFileName, $tempFile);
3175
            return @is_file($tempFile) ? $tempFile : '';
3176
        }
3177
3178
        return '';
3179
    }
3180
3181
    /**
3182
     * Deletes (unlink) a temporary filename in the var/ or typo3temp folder given as input.
3183
     * 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.
3184
     * Use this after upload_to_tempfile() or tempnam() from this class!
3185
     *
3186
     * @param string $uploadedTempFileName absolute file path - must reside within var/ or typo3temp/ folder.
3187
     * @return bool|null Returns TRUE if the file was unlink()'ed
3188
     * @see upload_to_tempfile()
3189
     * @see tempnam()
3190
     */
3191
    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...
3192
    {
3193
        if ($uploadedTempFileName) {
3194
            $uploadedTempFileName = self::fixWindowsFilePath($uploadedTempFileName);
3195
            if (
3196
                self::validPathStr($uploadedTempFileName)
3197
                && (
3198
                    self::isFirstPartOfStr($uploadedTempFileName, Environment::getPublicPath() . '/typo3temp/')
3199
                    || self::isFirstPartOfStr($uploadedTempFileName, Environment::getVarPath() . '/')
3200
                )
3201
                && @is_file($uploadedTempFileName)
3202
            ) {
3203
                if (unlink($uploadedTempFileName)) {
3204
                    return true;
3205
                }
3206
            }
3207
        }
3208
3209
        return null;
3210
    }
3211
3212
    /**
3213
     * Create temporary filename (Create file with unique file name)
3214
     * This function should be used for getting temporary file names - will make your applications safe for open_basedir = on
3215
     * REMEMBER to delete the temporary files after use! This is done by \TYPO3\CMS\Core\Utility\GeneralUtility::unlink_tempfile()
3216
     *
3217
     * @param string $filePrefix Prefix for temporary file
3218
     * @param string $fileSuffix Suffix for temporary file, for example a special file extension
3219
     * @return string result from PHP function tempnam() with the temp/var folder prefixed.
3220
     * @see unlink_tempfile()
3221
     * @see upload_to_tempfile()
3222
     */
3223
    public static function tempnam($filePrefix, $fileSuffix = '')
3224
    {
3225
        $temporaryPath = Environment::getVarPath() . '/transient/';
3226
        if (!is_dir($temporaryPath)) {
3227
            self::mkdir_deep($temporaryPath);
3228
        }
3229
        if ($fileSuffix === '') {
3230
            $tempFileName = $temporaryPath . PathUtility::basename(tempnam($temporaryPath, $filePrefix));
3231
        } else {
3232
            do {
3233
                $tempFileName = $temporaryPath . $filePrefix . random_int(1, PHP_INT_MAX) . $fileSuffix;
3234
            } while (file_exists($tempFileName));
3235
            touch($tempFileName);
3236
            clearstatcache(null, $tempFileName);
3237
        }
3238
        return $tempFileName;
3239
    }
3240
3241
    /**
3242
     * Standard authentication code (used in Direct Mail, checkJumpUrl and setfixed links computations)
3243
     *
3244
     * @param mixed $uid_or_record Uid (int) or record (array)
3245
     * @param string $fields List of fields from the record if that is given.
3246
     * @param int $codeLength Length of returned authentication code.
3247
     * @return string MD5 hash of 8 chars.
3248
     */
3249
    public static function stdAuthCode($uid_or_record, $fields = '', $codeLength = 8)
3250
    {
3251
        if (is_array($uid_or_record)) {
3252
            $recCopy_temp = [];
3253
            if ($fields) {
3254
                $fieldArr = self::trimExplode(',', $fields, true);
3255
                foreach ($fieldArr as $k => $v) {
3256
                    $recCopy_temp[$k] = $uid_or_record[$v];
3257
                }
3258
            } else {
3259
                $recCopy_temp = $uid_or_record;
3260
            }
3261
            $preKey = implode('|', $recCopy_temp);
3262
        } else {
3263
            $preKey = $uid_or_record;
3264
        }
3265
        $authCode = $preKey . '||' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'];
3266
        $authCode = substr(md5($authCode), 0, $codeLength);
3267
        return $authCode;
3268
    }
3269
3270
    /**
3271
     * Responds on input localization setting value whether the page it comes from should be hidden if no translation exists or not.
3272
     *
3273
     * @param int $l18n_cfg_fieldValue Value from "l18n_cfg" field of a page record
3274
     * @return bool TRUE if the page should be hidden
3275
     */
3276
    public static function hideIfNotTranslated($l18n_cfg_fieldValue)
3277
    {
3278
        return $GLOBALS['TYPO3_CONF_VARS']['FE']['hidePagesIfNotTranslatedByDefault'] xor ($l18n_cfg_fieldValue & 2);
3279
    }
3280
3281
    /**
3282
     * Returns true if the "l18n_cfg" field value is not set to hide
3283
     * pages in the default language
3284
     *
3285
     * @param int $localizationConfiguration
3286
     * @return bool
3287
     */
3288
    public static function hideIfDefaultLanguage($localizationConfiguration)
3289
    {
3290
        return (bool)($localizationConfiguration & 1);
3291
    }
3292
3293
    /**
3294
     * Calls a user-defined function/method in class
3295
     * Such a function/method should look like this: "function proc(&$params, &$ref) {...}"
3296
     *
3297
     * @param string $funcName Function/Method reference or Closure.
3298
     * @param mixed $params Parameters to be pass along (typically an array) (REFERENCE!)
3299
     * @param mixed $ref Reference to be passed along (typically "$this" - being a reference to the calling object) (REFERENCE!)
3300
     * @return mixed Content from method/function call
3301
     * @throws \InvalidArgumentException
3302
     */
3303
    public static function callUserFunction($funcName, &$params, &$ref = null)
3304
    {
3305
        if (!($ref === null || is_object($ref))) {
3306
            trigger_error(
3307
                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)),
3308
                E_USER_DEPRECATED
3309
            );
3310
        }
3311
3312
        // Check if we're using a closure and invoke it directly.
3313
        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...
3314
            return call_user_func_array($funcName, [&$params, &$ref]);
3315
        }
3316
        $funcName = trim($funcName);
3317
        $parts = explode('->', $funcName);
3318
        // Call function or method
3319
        if (count($parts) === 2) {
3320
            // It's a class/method
3321
            // Check if class/method exists:
3322
            if (class_exists($parts[0])) {
3323
                // Create object
3324
                $classObj = self::makeInstance($parts[0]);
3325
                if (method_exists($classObj, $parts[1])) {
3326
                    // Call method:
3327
                    $content = call_user_func_array([&$classObj, $parts[1]], [&$params, &$ref]);
3328
                } else {
3329
                    $errorMsg = 'No method name \'' . $parts[1] . '\' in class ' . $parts[0];
3330
                    throw new \InvalidArgumentException($errorMsg, 1294585865);
3331
                }
3332
            } else {
3333
                $errorMsg = 'No class named ' . $parts[0];
3334
                throw new \InvalidArgumentException($errorMsg, 1294585866);
3335
            }
3336
        } elseif (function_exists($funcName)) {
3337
            // It's a function
3338
            $content = call_user_func_array($funcName, [&$params, &$ref]);
3339
        } else {
3340
            $errorMsg = 'No function named: ' . $funcName;
3341
            throw new \InvalidArgumentException($errorMsg, 1294585867);
3342
        }
3343
        return $content;
3344
    }
3345
3346
    /**
3347
     * @param ContainerInterface $container
3348
     * @internal
3349
     */
3350
    public static function setContainer(ContainerInterface $container): void
3351
    {
3352
        self::$container = $container;
3353
    }
3354
3355
    /**
3356
     * @return ContainerInterface
3357
     * @internal
3358
     */
3359
    public static function getContainer(): ContainerInterface
3360
    {
3361
        if (self::$container === null) {
3362
            throw new \LogicException('PSR-11 Container is not available', 1549404144);
3363
        }
3364
        return self::$container;
3365
    }
3366
3367
    /**
3368
     * Creates an instance of a class taking into account the class-extensions
3369
     * API of TYPO3. USE THIS method instead of the PHP "new" keyword.
3370
     * Eg. "$obj = new myclass;" should be "$obj = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance("myclass")" instead!
3371
     *
3372
     * You can also pass arguments for a constructor:
3373
     * \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\myClass::class, $arg1, $arg2, ..., $argN)
3374
     *
3375
     * You may want to use \TYPO3\CMS\Extbase\Object\ObjectManager::get() if you
3376
     * want TYPO3 to take care about injecting dependencies of the class to be
3377
     * created. Therefore create an instance of ObjectManager via
3378
     * GeneralUtility::makeInstance() first and call its get() method to get
3379
     * the instance of a specific class.
3380
     *
3381
     * @param string $className name of the class to instantiate, must not be empty and not start with a backslash
3382
     * @param array<int, mixed> $constructorArguments Arguments for the constructor
3383
     * @return object the created instance
3384
     * @throws \InvalidArgumentException if $className is empty or starts with a backslash
3385
     */
3386
    public static function makeInstance($className, ...$constructorArguments)
3387
    {
3388
        if (!is_string($className) || empty($className)) {
0 ignored issues
show
introduced by
The condition is_string($className) is always true.
Loading history...
3389
            throw new \InvalidArgumentException('$className must be a non empty string.', 1288965219);
3390
        }
3391
        // Never instantiate with a beginning backslash, otherwise things like singletons won't work.
3392
        if ($className[0] === '\\') {
3393
            throw new \InvalidArgumentException(
3394
                '$className "' . $className . '" must not start with a backslash.',
3395
                1420281366
3396
            );
3397
        }
3398
        if (isset(static::$finalClassNameCache[$className])) {
3399
            $finalClassName = static::$finalClassNameCache[$className];
3400
        } else {
3401
            $finalClassName = self::getClassName($className);
3402
            static::$finalClassNameCache[$className] = $finalClassName;
3403
        }
3404
        // Return singleton instance if it is already registered
3405
        if (isset(self::$singletonInstances[$finalClassName])) {
3406
            return self::$singletonInstances[$finalClassName];
3407
        }
3408
        // Return instance if it has been injected by addInstance()
3409
        if (
3410
            isset(self::$nonSingletonInstances[$finalClassName])
3411
            && !empty(self::$nonSingletonInstances[$finalClassName])
3412
        ) {
3413
            return array_shift(self::$nonSingletonInstances[$finalClassName]);
3414
        }
3415
3416
        // Read service and prototypes from the DI container, this is required to
3417
        // support classes that require dependency injection.
3418
        // We operate on the original class name on purpose, as class overrides
3419
        // are resolved inside the container
3420
        if (self::$container !== null && $constructorArguments === [] && self::$container->has($className)) {
3421
            return self::$container->get($className);
3422
        }
3423
3424
        // Create new instance and call constructor with parameters
3425
        $instance = new $finalClassName(...$constructorArguments);
3426
        // Register new singleton instance, but only if it is not a known PSR-11 container service
3427
        if ($instance instanceof SingletonInterface && !(self::$container !== null && self::$container->has($className))) {
3428
            self::$singletonInstances[$finalClassName] = $instance;
3429
        }
3430
        if ($instance instanceof LoggerAwareInterface) {
3431
            $instance->setLogger(static::makeInstance(LogManager::class)->getLogger($className));
3432
        }
3433
        return $instance;
3434
    }
3435
3436
    /**
3437
     * Creates a class taking implementation settings and class aliases into account.
3438
     *
3439
     * Intended to be used to create objects by the dependency injection
3440
     * container.
3441
     *
3442
     * @param string $className name of the class to instantiate
3443
     * @param array<int, mixed> $constructorArguments Arguments for the constructor
3444
     * @return object the created instance
3445
     * @internal
3446
     */
3447
    public static function makeInstanceForDi(string $className, ...$constructorArguments): object
3448
    {
3449
        $finalClassName = static::$finalClassNameCache[$className] ?? static::$finalClassNameCache[$className] = self::getClassName($className);
3450
3451
        // Return singleton instance if it is already registered (currently required for unit and functional tests)
3452
        if (isset(self::$singletonInstances[$finalClassName])) {
3453
            return self::$singletonInstances[$finalClassName];
3454
        }
3455
        // Create new instance and call constructor with parameters
3456
        return new $finalClassName(...$constructorArguments);
3457
    }
3458
3459
    /**
3460
     * Returns the class name for a new instance, taking into account
3461
     * registered implementations for this class
3462
     *
3463
     * @param string $className Base class name to evaluate
3464
     * @return string Final class name to instantiate with "new [classname]
3465
     */
3466
    protected static function getClassName($className)
3467
    {
3468
        if (class_exists($className)) {
3469
            while (static::classHasImplementation($className)) {
3470
                $className = static::getImplementationForClass($className);
3471
            }
3472
        }
3473
        return ClassLoadingInformation::getClassNameForAlias($className);
3474
    }
3475
3476
    /**
3477
     * Returns the configured implementation of the class
3478
     *
3479
     * @param string $className
3480
     * @return string
3481
     */
3482
    protected static function getImplementationForClass($className)
3483
    {
3484
        return $GLOBALS['TYPO3_CONF_VARS']['SYS']['Objects'][$className]['className'];
3485
    }
3486
3487
    /**
3488
     * Checks if a class has a configured implementation
3489
     *
3490
     * @param string $className
3491
     * @return bool
3492
     */
3493
    protected static function classHasImplementation($className)
3494
    {
3495
        return !empty($GLOBALS['TYPO3_CONF_VARS']['SYS']['Objects'][$className]['className']);
3496
    }
3497
3498
    /**
3499
     * Sets the instance of a singleton class to be returned by makeInstance.
3500
     *
3501
     * If this function is called multiple times for the same $className,
3502
     * makeInstance will return the last set instance.
3503
     *
3504
     * Warning:
3505
     * This is NOT a public API method and must not be used in own extensions!
3506
     * This methods exists mostly for unit tests to inject a mock of a singleton class.
3507
     * If you use this, make sure to always combine this with getSingletonInstances()
3508
     * and resetSingletonInstances() in setUp() and tearDown() of the test class.
3509
     *
3510
     * @see makeInstance
3511
     * @param string $className
3512
     * @param \TYPO3\CMS\Core\SingletonInterface $instance
3513
     * @internal
3514
     */
3515
    public static function setSingletonInstance($className, SingletonInterface $instance)
3516
    {
3517
        self::checkInstanceClassName($className, $instance);
3518
        // Check for XCLASS registration (same is done in makeInstance() in order to store the singleton of the final class name)
3519
        $finalClassName = self::getClassName($className);
3520
        self::$singletonInstances[$finalClassName] = $instance;
3521
    }
3522
3523
    /**
3524
     * Removes the instance of a singleton class to be returned by makeInstance.
3525
     *
3526
     * Warning:
3527
     * This is NOT a public API method and must not be used in own extensions!
3528
     * This methods exists mostly for unit tests to inject a mock of a singleton class.
3529
     * If you use this, make sure to always combine this with getSingletonInstances()
3530
     * and resetSingletonInstances() in setUp() and tearDown() of the test class.
3531
     *
3532
     * @see makeInstance
3533
     * @throws \InvalidArgumentException
3534
     * @param string $className
3535
     * @param \TYPO3\CMS\Core\SingletonInterface $instance
3536
     * @internal
3537
     */
3538
    public static function removeSingletonInstance($className, SingletonInterface $instance)
3539
    {
3540
        self::checkInstanceClassName($className, $instance);
3541
        if (!isset(self::$singletonInstances[$className])) {
3542
            throw new \InvalidArgumentException('No Instance registered for ' . $className . '.', 1394099179);
3543
        }
3544
        if ($instance !== self::$singletonInstances[$className]) {
3545
            throw new \InvalidArgumentException('The instance you are trying to remove has not been registered before.', 1394099256);
3546
        }
3547
        unset(self::$singletonInstances[$className]);
3548
    }
3549
3550
    /**
3551
     * Set a group of singleton instances. Similar to setSingletonInstance(),
3552
     * but multiple instances can be set.
3553
     *
3554
     * Warning:
3555
     * This is NOT a public API method and must not be used in own extensions!
3556
     * This method is usually only used in tests to restore the list of singletons in
3557
     * tearDown(), that was backed up with getSingletonInstances() in setUp() and
3558
     * manipulated in tests with setSingletonInstance()
3559
     *
3560
     * @internal
3561
     * @param array<string, SingletonInterface> $newSingletonInstances
3562
     */
3563
    public static function resetSingletonInstances(array $newSingletonInstances)
3564
    {
3565
        static::$singletonInstances = [];
3566
        foreach ($newSingletonInstances as $className => $instance) {
3567
            static::setSingletonInstance($className, $instance);
3568
        }
3569
    }
3570
3571
    /**
3572
     * Get all currently registered singletons
3573
     *
3574
     * Warning:
3575
     * This is NOT a public API method and must not be used in own extensions!
3576
     * This method is usually only used in tests in setUp() to fetch the list of
3577
     * currently registered singletons, if this list is manipulated with
3578
     * setSingletonInstance() in tests.
3579
     *
3580
     * @internal
3581
     * @return array<string, SingletonInterface>
3582
     */
3583
    public static function getSingletonInstances()
3584
    {
3585
        return static::$singletonInstances;
3586
    }
3587
3588
    /**
3589
     * Get all currently registered non singleton instances
3590
     *
3591
     * Warning:
3592
     * This is NOT a public API method and must not be used in own extensions!
3593
     * This method is only used in UnitTestCase base test tearDown() to verify tests
3594
     * have no left over instances that were previously added using addInstance().
3595
     *
3596
     * @internal
3597
     * @return array<string, array<object>>
3598
     */
3599
    public static function getInstances()
3600
    {
3601
        return static::$nonSingletonInstances;
3602
    }
3603
3604
    /**
3605
     * Sets the instance of a non-singleton class to be returned by makeInstance.
3606
     *
3607
     * If this function is called multiple times for the same $className,
3608
     * makeInstance will return the instances in the order in which they have
3609
     * been added (FIFO).
3610
     *
3611
     * Warning: This is a helper method for unit tests. Do not call this directly in production code!
3612
     *
3613
     * @see makeInstance
3614
     * @throws \InvalidArgumentException if class extends \TYPO3\CMS\Core\SingletonInterface
3615
     * @param string $className
3616
     * @param object $instance
3617
     */
3618
    public static function addInstance($className, $instance)
3619
    {
3620
        self::checkInstanceClassName($className, $instance);
3621
        if ($instance instanceof SingletonInterface) {
3622
            throw new \InvalidArgumentException('$instance must not be an instance of TYPO3\\CMS\\Core\\SingletonInterface. For setting singletons, please use setSingletonInstance.', 1288969325);
3623
        }
3624
        if (!isset(self::$nonSingletonInstances[$className])) {
3625
            self::$nonSingletonInstances[$className] = [];
3626
        }
3627
        self::$nonSingletonInstances[$className][] = $instance;
3628
    }
3629
3630
    /**
3631
     * Checks that $className is non-empty and that $instance is an instance of
3632
     * $className.
3633
     *
3634
     * @throws \InvalidArgumentException if $className is empty or if $instance is no instance of $className
3635
     * @param string $className a class name
3636
     * @param object $instance an object
3637
     */
3638
    protected static function checkInstanceClassName($className, $instance)
3639
    {
3640
        if ($className === '') {
3641
            throw new \InvalidArgumentException('$className must not be empty.', 1288967479);
3642
        }
3643
        if (!$instance instanceof $className) {
3644
            throw new \InvalidArgumentException('$instance must be an instance of ' . $className . ', but actually is an instance of ' . get_class($instance) . '.', 1288967686);
3645
        }
3646
    }
3647
3648
    /**
3649
     * Purge all instances returned by makeInstance.
3650
     *
3651
     * This function is most useful when called from tearDown in a test case
3652
     * to drop any instances that have been created by the tests.
3653
     *
3654
     * Warning: This is a helper method for unit tests. Do not call this directly in production code!
3655
     *
3656
     * @see makeInstance
3657
     */
3658
    public static function purgeInstances()
3659
    {
3660
        self::$container = null;
3661
        self::$singletonInstances = [];
3662
        self::$nonSingletonInstances = [];
3663
    }
3664
3665
    /**
3666
     * Flush internal runtime caches
3667
     *
3668
     * Used in unit tests only.
3669
     *
3670
     * @internal
3671
     */
3672
    public static function flushInternalRuntimeCaches()
3673
    {
3674
        self::$indpEnvCache = [];
3675
    }
3676
3677
    /**
3678
     * Find the best service and check if it works.
3679
     * Returns object of the service class.
3680
     *
3681
     * @param string $serviceType Type of service (service key).
3682
     * @param string $serviceSubType Sub type like file extensions or similar. Defined by the service.
3683
     * @param mixed $excludeServiceKeys List of service keys which should be excluded in the search for a service. Array or comma list.
3684
     * @throws \RuntimeException
3685
     * @return object|string[] The service object or an array with error infos.
3686
     */
3687
    public static function makeInstanceService($serviceType, $serviceSubType = '', $excludeServiceKeys = [])
3688
    {
3689
        $error = false;
3690
        if (!is_array($excludeServiceKeys)) {
3691
            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);
3692
            $excludeServiceKeys = self::trimExplode(',', $excludeServiceKeys, true);
3693
        }
3694
        $requestInfo = [
3695
            'requestedServiceType' => $serviceType,
3696
            'requestedServiceSubType' => $serviceSubType,
3697
            'requestedExcludeServiceKeys' => $excludeServiceKeys
3698
        ];
3699
        while ($info = ExtensionManagementUtility::findService($serviceType, $serviceSubType, $excludeServiceKeys)) {
3700
            // provide information about requested service to service object
3701
            $info = array_merge($info, $requestInfo);
3702
            $obj = self::makeInstance($info['className']);
3703
            if (is_object($obj)) {
3704
                if (!@is_callable([$obj, 'init'])) {
3705
                    self::getLogger()->error('Requested service ' . $info['className'] . ' has no init() method.', ['service' => $info]);
3706
                    throw new \RuntimeException('Broken service: ' . $info['className'], 1568119209);
3707
                }
3708
                $obj->info = $info;
3709
                // service available?
3710
                if ($obj->init()) {
3711
                    return $obj;
3712
                }
3713
                $error = $obj->getLastErrorArray();
3714
                unset($obj);
3715
            }
3716
3717
            // deactivate the service
3718
            ExtensionManagementUtility::deactivateService($info['serviceType'], $info['serviceKey']);
3719
        }
3720
        return $error;
3721
    }
3722
3723
    /**
3724
     * Quotes a string for usage as JS parameter.
3725
     *
3726
     * @param string $value the string to encode, may be empty
3727
     * @return string the encoded value already quoted (with single quotes),
3728
     */
3729
    public static function quoteJSvalue($value)
3730
    {
3731
        return strtr(
3732
            json_encode((string)$value, JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_TAG),
3733
            [
3734
                '"' => '\'',
3735
                '\\\\' => '\\u005C',
3736
                ' ' => '\\u0020',
3737
                '!' => '\\u0021',
3738
                '\\t' => '\\u0009',
3739
                '\\n' => '\\u000A',
3740
                '\\r' => '\\u000D'
3741
            ]
3742
        );
3743
    }
3744
3745
    /**
3746
     * @param mixed $value
3747
     * @param bool $useHtmlEntities
3748
     * @return string
3749
     */
3750
    public static function jsonEncodeForHtmlAttribute($value, bool $useHtmlEntities = true): string
3751
    {
3752
        $json = json_encode($value, JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_TAG);
3753
        return $useHtmlEntities ? htmlspecialchars($json) : $json;
3754
    }
3755
3756
    /**
3757
     * Set the ApplicationContext
3758
     *
3759
     * This function is used by the Bootstrap to hand over the application context. It must not be used anywhere else,
3760
     * because the context shall never be changed on runtime!
3761
     *
3762
     * @param \TYPO3\CMS\Core\Core\ApplicationContext $applicationContext
3763
     * @throws \RuntimeException if applicationContext is overridden
3764
     * @internal This is not a public API method, do not use in own extensions, will probably be removed in TYPO3 v11.
3765
     */
3766
    public static function presetApplicationContext(ApplicationContext $applicationContext)
3767
    {
3768
        if (static::$applicationContext === null) {
3769
            static::$applicationContext = $applicationContext;
3770
        } else {
3771
            throw new \RuntimeException('Trying to override applicationContext which has already been defined!', 1376084316);
3772
        }
3773
    }
3774
3775
    /**
3776
     * For testing purposes only!
3777
     * The functional test framework uses this to reset the internal $application context
3778
     * variable in between multiple tests before it is re-initialized using presetApplicationContext()
3779
     * which otherwise throws an exception if the internal variable is already set.
3780
     *
3781
     * @internal May be changed or removed any time, will probably be removed in TYPO3 v11.
3782
     */
3783
    public static function resetApplicationContext(): void
3784
    {
3785
        static::$applicationContext = null;
3786
    }
3787
3788
    /**
3789
     * Get the ApplicationContext
3790
     *
3791
     * @return \TYPO3\CMS\Core\Core\ApplicationContext
3792
     * @deprecated since TYPO3 v10.2, will be removed in TYPO3 v11, use Environment::getContext() instead.
3793
     */
3794
    public static function getApplicationContext()
3795
    {
3796
        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);
3797
        // Implicitly setting the application context here, but only if it is used, otherwise this does not
3798
        // need to be populated.
3799
        if (static::$applicationContext === null) {
3800
            static::$applicationContext = Environment::getContext();
3801
        }
3802
        return static::$applicationContext;
3803
    }
3804
3805
    /**
3806
     * Check if the current request is running on a CGI server API
3807
     * @return bool
3808
     * @deprecated will be removed in TYPO3 v11.0. Use Environment::isRunningOnCgiServer() instead.
3809
     */
3810
    public static function isRunningOnCgiServerApi()
3811
    {
3812
        trigger_error('GeneralUtility::isRunningOnCgiServerApi() will be removed in TYPO3 v11.0. Use "Environment::isRunningOnCgiServer()" instead.', E_USER_DEPRECATED);
3813
        return Environment::isRunningOnCgiServer();
3814
    }
3815
3816
    /**
3817
     * @return LoggerInterface
3818
     */
3819
    protected static function getLogger()
3820
    {
3821
        return static::makeInstance(LogManager::class)->getLogger(__CLASS__);
3822
    }
3823
}
3824