Passed
Push — master ( 748870...d66510 )
by
unknown
16:05
created

PathUtility   C

Complexity

Total Complexity 54

Size/Duplication

Total Lines 368
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 54
eloc 126
dl 0
loc 368
rs 6.4799
c 0
b 0
f 0

14 Methods

Rating   Name   Duplication   Size   Complexity  
A getRelativePathTo() 0 3 1
B getAbsoluteWebPath() 0 24 8
A sanitizeTrailingSeparator() 0 3 1
B getRelativePath() 0 25 7
B getCommonPrefix() 0 28 8
B getCanonicalPath() 0 56 11
A dirnameDuringBootstrap() 0 3 1
A dirname() 0 11 2
A getAbsolutePathOfRelativeReferencedFileOrPath() 0 8 3
A pathinfo() 0 11 4
A stripPathSitePrefix() 0 3 1
A isAbsolutePath() 0 8 4
A basename() 0 11 2
A basenameDuringBootstrap() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like PathUtility often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use PathUtility, and based on these observations, apply Extract Interface, too.

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 TYPO3\CMS\Core\Core\Environment;
19
20
/**
21
 * Class with helper functions for file paths.
22
 */
23
class PathUtility
24
{
25
    /**
26
     * Gets the relative path from the current used script to a given directory.
27
     * The allowed TYPO3 path is checked as well, thus it's not possible to go to upper levels.
28
     *
29
     * @param string $targetPath Absolute target path
30
     * @return string|null
31
     */
32
    public static function getRelativePathTo($targetPath)
33
    {
34
        return self::getRelativePath(self::dirname(Environment::getCurrentScript()), $targetPath);
35
    }
36
37
    /**
38
     * Creates an absolute URL out of really any input path, removes '../' parts for the targetPath
39
     *
40
     * @param string $targetPath can be "../typo3conf/ext/myext/myfile.js" or "/myfile.js"
41
     * @return string something like "/mysite/typo3conf/ext/myext/myfile.js"
42
     */
43
    public static function getAbsoluteWebPath($targetPath)
44
    {
45
        if (self::isAbsolutePath($targetPath)) {
46
            if (strpos($targetPath, Environment::getPublicPath()) === 0) {
47
                $targetPath = self::stripPathSitePrefix($targetPath);
48
                if (!Environment::isCli()) {
49
                    $targetPath = GeneralUtility::getIndpEnv('TYPO3_SITE_PATH') . $targetPath;
50
                }
51
            }
52
        } elseif (strpos($targetPath, '://') !== false) {
53
            return $targetPath;
54
        } elseif (file_exists(Environment::getPublicPath() . '/' . $targetPath)) {
55
            if (!Environment::isCli()) {
56
                $targetPath = GeneralUtility::getIndpEnv('TYPO3_SITE_PATH') . $targetPath;
57
            }
58
        } else {
59
            // Make an absolute path out of it
60
            $targetPath = GeneralUtility::resolveBackPath(self::dirname(Environment::getCurrentScript()) . '/' . $targetPath);
61
            $targetPath = self::stripPathSitePrefix($targetPath);
62
            if (!Environment::isCli()) {
63
                $targetPath = GeneralUtility::getIndpEnv('TYPO3_SITE_PATH') . $targetPath;
64
            }
65
        }
66
        return $targetPath;
67
    }
68
69
    /**
70
     * Gets the relative path from a source directory to a target directory.
71
     * The allowed TYPO3 path is checked as well, thus it's not possible to go to upper levels.
72
     *
73
     * @param string $sourcePath Absolute source path
74
     * @param string $targetPath Absolute target path
75
     * @return string|null
76
     */
77
    public static function getRelativePath($sourcePath, $targetPath)
78
    {
79
        $relativePath = null;
80
        $sourcePath = rtrim(GeneralUtility::fixWindowsFilePath($sourcePath), '/');
81
        $targetPath = rtrim(GeneralUtility::fixWindowsFilePath($targetPath), '/');
82
        if ($sourcePath !== $targetPath) {
83
            $commonPrefix = self::getCommonPrefix([$sourcePath, $targetPath]);
84
            if ($commonPrefix !== null && GeneralUtility::isAllowedAbsPath($commonPrefix)) {
85
                $commonPrefixLength = strlen($commonPrefix);
86
                $resolvedSourcePath = '';
87
                $resolvedTargetPath = '';
88
                $sourcePathSteps = 0;
89
                if (strlen($sourcePath) > $commonPrefixLength) {
90
                    $resolvedSourcePath = (string)substr($sourcePath, $commonPrefixLength);
91
                }
92
                if (strlen($targetPath) > $commonPrefixLength) {
93
                    $resolvedTargetPath = (string)substr($targetPath, $commonPrefixLength);
94
                }
95
                if ($resolvedSourcePath !== '') {
96
                    $sourcePathSteps = count(explode('/', $resolvedSourcePath));
97
                }
98
                $relativePath = self::sanitizeTrailingSeparator(str_repeat('../', $sourcePathSteps) . $resolvedTargetPath);
99
            }
100
        }
101
        return $relativePath;
102
    }
103
104
    /**
105
     * Gets the common path prefix out of many paths.
106
     * + /var/www/domain.com/typo3/sysext/frontend/
107
     * + /var/www/domain.com/typo3/sysext/em/
108
     * + /var/www/domain.com/typo3/sysext/file/
109
     * = /var/www/domain.com/typo3/sysext/
110
     *
111
     * @param array $paths Paths to be processed
112
     * @return string|null
113
     */
114
    public static function getCommonPrefix(array $paths)
115
    {
116
        $paths = array_map([GeneralUtility::class, 'fixWindowsFilePath'], $paths);
117
        $commonPath = null;
118
        if (count($paths) === 1) {
119
            $commonPath = array_shift($paths);
120
        } elseif (count($paths) > 1) {
121
            $parts = explode('/', (string)array_shift($paths));
122
            $comparePath = '';
123
            $break = false;
124
            foreach ($parts as $part) {
125
                $comparePath .= $part . '/';
126
                foreach ($paths as $path) {
127
                    if (strpos($path . '/', $comparePath) !== 0) {
128
                        $break = true;
129
                        break;
130
                    }
131
                }
132
                if ($break) {
133
                    break;
134
                }
135
                $commonPath = $comparePath;
136
            }
137
        }
138
        if ($commonPath !== null) {
139
            $commonPath = self::sanitizeTrailingSeparator($commonPath, '/');
140
        }
141
        return $commonPath;
142
    }
143
144
    /**
145
     * Sanitizes a trailing separator.
146
     * (e.g. 'some/path' -> 'some/path/')
147
     *
148
     * @param string $path The path to be sanitized
149
     * @param string $separator The separator to be used
150
     * @return string
151
     */
152
    public static function sanitizeTrailingSeparator($path, $separator = '/')
153
    {
154
        return rtrim($path, $separator) . $separator;
155
    }
156
157
    /**
158
     * Returns trailing name component of path
159
     * Since basename() is locale dependent we need to access
160
     * the filesystem with the same locale of the system, not
161
     * the rendering context.
162
     * @see http://www.php.net/manual/en/function.basename.php
163
     *
164
     *
165
     * @param string $path
166
     *
167
     * @return string
168
     */
169
    public static function basename($path)
170
    {
171
        $targetLocale = $GLOBALS['TYPO3_CONF_VARS']['SYS']['systemLocale'] ?? '';
172
        if (empty($targetLocale)) {
173
            return basename($path);
174
        }
175
        $currentLocale = (string)setlocale(LC_CTYPE, '0');
176
        setlocale(LC_CTYPE, $targetLocale);
177
        $basename = basename($path);
178
        setlocale(LC_CTYPE, $currentLocale);
179
        return $basename;
180
    }
181
182
    /**
183
     * Returns parent directory's path
184
     * Since dirname() is locale dependent we need to access
185
     * the filesystem with the same locale of the system, not
186
     * the rendering context.
187
     * @see http://www.php.net/manual/en/function.dirname.php
188
     *
189
     *
190
     * @param string $path
191
     *
192
     * @return string
193
     */
194
    public static function dirname($path)
195
    {
196
        $targetLocale = $GLOBALS['TYPO3_CONF_VARS']['SYS']['systemLocale'] ?? '';
197
        if (empty($targetLocale)) {
198
            return dirname($path);
199
        }
200
        $currentLocale = (string)setlocale(LC_CTYPE, '0');
201
        setlocale(LC_CTYPE, $targetLocale);
202
        $dirname = dirname($path);
203
        setlocale(LC_CTYPE, $currentLocale);
204
        return $dirname;
205
    }
206
207
    /**
208
     * Returns parent directory's path
209
     * Since dirname() is locale dependent we need to access
210
     * the filesystem with the same locale of the system, not
211
     * the rendering context.
212
     * @see http://www.php.net/manual/en/function.dirname.php
213
     *
214
     *
215
     * @param string $path
216
     * @param int $options
217
     *
218
     * @return string|string[]
219
     */
220
    public static function pathinfo($path, $options = null)
221
    {
222
        $targetLocale = $GLOBALS['TYPO3_CONF_VARS']['SYS']['systemLocale'] ?? '';
223
        if (empty($targetLocale)) {
224
            return $options === null ? pathinfo($path) : pathinfo($path, $options);
225
        }
226
        $currentLocale = (string)setlocale(LC_CTYPE, '0');
227
        setlocale(LC_CTYPE, $targetLocale);
228
        $pathinfo = $options == null ? pathinfo($path) : pathinfo($path, $options);
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $options of type integer|null against null; this is ambiguous if the integer can be zero. Consider using a strict comparison === instead.
Loading history...
229
        setlocale(LC_CTYPE, $currentLocale);
230
        return $pathinfo;
231
    }
232
233
    /**
234
     * Checks if the $path is absolute or relative (detecting either '/' or 'x:/' as first part of string) and returns TRUE if so.
235
     *
236
     * @param string $path File path to evaluate
237
     * @return bool
238
     */
239
    public static function isAbsolutePath($path)
240
    {
241
        // On Windows also a path starting with a drive letter is absolute: X:/
242
        if (Environment::isWindows() && (substr($path, 1, 2) === ':/' || substr($path, 1, 2) === ':\\')) {
243
            return true;
244
        }
245
        // Path starting with a / is always absolute, on every system
246
        return $path[0] === '/';
247
    }
248
249
    /**
250
     * Gets the (absolute) path of an include file based on the (absolute) path of a base file
251
     *
252
     * Does NOT do any sanity checks. This is a task for the calling function, e.g.
253
     * call GeneralUtility::getFileAbsFileName() on the result.
254
     * @see \TYPO3\CMS\Core\Utility\GeneralUtility::getFileAbsFileName()
255
     *
256
     * Resolves all dots and slashes between that paths of both files.
257
     * Whether the result is absolute or not, depends of the base file name.
258
     *
259
     * If the include file goes higher than a relative base file, then the result
260
     * will contain dots as a relative part.
261
     * <pre>
262
     *   base:    abc/one.txt
263
     *   include: ../../two.txt
264
     *   result:  ../two.txt
265
     * </pre>
266
     * The exact behavior, refer to getCanonicalPath().
267
     *
268
     * @param string $baseFilenameOrPath The name of the file or a path that serves as a base; a path will need to have a '/' at the end
269
     * @param string $includeFileName The name of the file that is included in the file
270
     * @return string The (absolute) path of the include file
271
     */
272
    public static function getAbsolutePathOfRelativeReferencedFileOrPath($baseFilenameOrPath, $includeFileName)
273
    {
274
        $fileName = static::basename($includeFileName);
275
        $basePath = substr($baseFilenameOrPath, -1) === '/' ? $baseFilenameOrPath : static::dirname($baseFilenameOrPath);
276
        $newDir = static::getCanonicalPath($basePath . '/' . static::dirname($includeFileName));
277
        // Avoid double slash on empty path
278
        $result = (($newDir !== '/') ? $newDir : '') . '/' . $fileName;
279
        return $result;
280
    }
281
282
    /**
283
     * Returns parent directory's path
284
     * Early during bootstrap there is no TYPO3_CONF_VARS yet so the setting for the system locale
285
     * is also unavailable. The path of the parent directory is determined with a regular expression
286
     * to avoid issues with locales.
287
     *
288
     * @param string $path
289
     *
290
     * @return string Path without trailing slash
291
     */
292
    public static function dirnameDuringBootstrap($path): string
293
    {
294
        return preg_replace('#(.*)(/|\\\\)([^\\\\/]+)$#', '$1', $path);
295
    }
296
297
    /**
298
     * Returns filename part of a path
299
     * Early during bootstrap there is no TYPO3_CONF_VARS yet so the setting for the system locale
300
     * is also unavailable. The filename part is determined with a regular expression to avoid issues
301
     * with locales.
302
     *
303
     * @param string $path
304
     *
305
     * @return string
306
     */
307
    public static function basenameDuringBootstrap($path): string
308
    {
309
        return preg_replace('#.*[/\\\\]([^\\\\/]+)$#', '$1', $path);
310
    }
311
312
    /*********************
313
     *
314
     * Cleaning methods
315
     *
316
     *********************/
317
    /**
318
     * Resolves all dots, slashes and removes spaces after or before a path...
319
     *
320
     * @param string $path Input string
321
     * @return string Canonical path, always without trailing slash
322
     */
323
    public static function getCanonicalPath($path)
324
    {
325
        // Replace backslashes with slashes to work with Windows paths if given
326
        $path = trim(str_replace('\\', '/', $path));
327
328
        // @todo do we really need this? Probably only in testing context for vfs?
329
        $protocol = '';
330
        if (strpos($path, '://') !== false) {
331
            [$protocol, $path] = explode('://', $path);
332
            $protocol .= '://';
333
        }
334
335
        $absolutePathPrefix = '';
336
        if (static::isAbsolutePath($path)) {
337
            if (Environment::isWindows() && substr($path, 1, 2) === ':/') {
338
                $absolutePathPrefix = substr($path, 0, 3);
339
                $path = substr($path, 3);
340
            } else {
341
                $path = ltrim($path, '/');
342
                $absolutePathPrefix = '/';
343
            }
344
        }
345
346
        $theDirParts = explode('/', $path);
347
        $theDirPartsCount = count($theDirParts);
348
        for ($partCount = 0; $partCount < $theDirPartsCount; $partCount++) {
349
            // double-slashes in path: remove element
350
            if ($theDirParts[$partCount] === '') {
351
                array_splice($theDirParts, $partCount, 1);
352
                $partCount--;
353
                $theDirPartsCount--;
354
            }
355
            // "." in path: remove element
356
            if (($theDirParts[$partCount] ?? '') === '.') {
357
                array_splice($theDirParts, $partCount, 1);
358
                $partCount--;
359
                $theDirPartsCount--;
360
            }
361
            // ".." in path:
362
            if (($theDirParts[$partCount] ?? '') === '..') {
363
                if ($partCount >= 1) {
364
                    // Remove this and previous element
365
                    array_splice($theDirParts, $partCount - 1, 2);
366
                    $partCount -= 2;
367
                    $theDirPartsCount -= 2;
368
                } elseif ($absolutePathPrefix) {
369
                    // can't go higher than root dir
370
                    // simply remove this part and continue
371
                    array_splice($theDirParts, $partCount, 1);
372
                    $partCount--;
373
                    $theDirPartsCount--;
374
                }
375
            }
376
        }
377
378
        return $protocol . $absolutePathPrefix . implode('/', $theDirParts);
379
    }
380
381
    /**
382
     * Strip first part of a path, equal to the length of public web path including trailing slash
383
     *
384
     * @param string $path
385
     * @return string
386
     * @internal
387
     */
388
    public static function stripPathSitePrefix($path)
389
    {
390
        return substr($path, strlen(Environment::getPublicPath() . '/'));
391
    }
392
}
393