Passed
Push — master ( 8e4875...07bfbc )
by
unknown
12:19
created

PathUtility::getAbsoluteWebPath()   B

Complexity

Conditions 8
Paths 8

Size

Total Lines 24
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 16
dl 0
loc 24
rs 8.4444
c 0
b 0
f 0
cc 8
nc 8
nop 1
1
<?php
2
3
/*
4
 * This file is part of the TYPO3 CMS project.
5
 *
6
 * It is free software; you can redistribute it and/or modify it under
7
 * the terms of the GNU General Public License, either version 2
8
 * of the License, or any later version.
9
 *
10
 * For the full copyright and license information, please read the
11
 * LICENSE.txt file that was distributed with this source code.
12
 *
13
 * The TYPO3 project - inspiring people to share!
14
 */
15
16
namespace TYPO3\CMS\Core\Utility;
17
18
use 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
        $currentLocale = (string)setlocale(LC_CTYPE, '0');
172
        setlocale(LC_CTYPE, $GLOBALS['TYPO3_CONF_VARS']['SYS']['systemLocale']);
173
        $basename = basename($path);
174
        setlocale(LC_CTYPE, $currentLocale);
175
        return $basename;
176
    }
177
178
    /**
179
     * Returns parent directory's path
180
     * Since dirname() is locale dependent we need to access
181
     * the filesystem with the same locale of the system, not
182
     * the rendering context.
183
     * @see http://www.php.net/manual/en/function.dirname.php
184
     *
185
     *
186
     * @param string $path
187
     *
188
     * @return string
189
     */
190
    public static function dirname($path)
191
    {
192
        $currentLocale = (string)setlocale(LC_CTYPE, '0');
193
        setlocale(LC_CTYPE, $GLOBALS['TYPO3_CONF_VARS']['SYS']['systemLocale']);
194
        $dirname = dirname($path);
195
        setlocale(LC_CTYPE, $currentLocale);
196
        return $dirname;
197
    }
198
199
    /**
200
     * Returns parent directory's path
201
     * Since dirname() is locale dependent we need to access
202
     * the filesystem with the same locale of the system, not
203
     * the rendering context.
204
     * @see http://www.php.net/manual/en/function.dirname.php
205
     *
206
     *
207
     * @param string $path
208
     * @param int $options
209
     *
210
     * @return string|string[]
211
     */
212
    public static function pathinfo($path, $options = null)
213
    {
214
        $currentLocale = (string)setlocale(LC_CTYPE, '0');
215
        setlocale(LC_CTYPE, $GLOBALS['TYPO3_CONF_VARS']['SYS']['systemLocale']);
216
        $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...
217
        setlocale(LC_CTYPE, $currentLocale);
218
        return $pathinfo;
219
    }
220
221
    /**
222
     * Checks if the $path is absolute or relative (detecting either '/' or 'x:/' as first part of string) and returns TRUE if so.
223
     *
224
     * @param string $path File path to evaluate
225
     * @return bool
226
     */
227
    public static function isAbsolutePath($path)
228
    {
229
        // On Windows also a path starting with a drive letter is absolute: X:/
230
        if (Environment::isWindows() && (substr($path, 1, 2) === ':/' || substr($path, 1, 2) === ':\\')) {
231
            return true;
232
        }
233
        // Path starting with a / is always absolute, on every system
234
        return $path[0] === '/';
235
    }
236
237
    /**
238
     * Gets the (absolute) path of an include file based on the (absolute) path of a base file
239
     *
240
     * Does NOT do any sanity checks. This is a task for the calling function, e.g.
241
     * call GeneralUtility::getFileAbsFileName() on the result.
242
     * @see \TYPO3\CMS\Core\Utility\GeneralUtility::getFileAbsFileName()
243
     *
244
     * Resolves all dots and slashes between that paths of both files.
245
     * Whether the result is absolute or not, depends of the base file name.
246
     *
247
     * If the include file goes higher than a relative base file, then the result
248
     * will contain dots as a relative part.
249
     * <pre>
250
     *   base:    abc/one.txt
251
     *   include: ../../two.txt
252
     *   result:  ../two.txt
253
     * </pre>
254
     * The exact behavior, refer to getCanonicalPath().
255
     *
256
     * @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
257
     * @param string $includeFileName The name of the file that is included in the file
258
     * @return string The (absolute) path of the include file
259
     */
260
    public static function getAbsolutePathOfRelativeReferencedFileOrPath($baseFilenameOrPath, $includeFileName)
261
    {
262
        $fileName = static::basename($includeFileName);
263
        $basePath = substr($baseFilenameOrPath, -1) === '/' ? $baseFilenameOrPath : static::dirname($baseFilenameOrPath);
264
        $newDir = static::getCanonicalPath($basePath . '/' . static::dirname($includeFileName));
265
        // Avoid double slash on empty path
266
        $result = (($newDir !== '/') ? $newDir : '') . '/' . $fileName;
267
        return $result;
268
    }
269
270
    /**
271
     * Returns parent directory's path
272
     * Early during bootstrap there is no TYPO3_CONF_VARS yet so the setting for the system locale
273
     * is also unavailable. The path of the parent directory is determined with a regular expression
274
     * to avoid issues with locales.
275
     *
276
     * @param string $path
277
     *
278
     * @return string Path without trailing slash
279
     */
280
    public static function dirnameDuringBootstrap($path): string
281
    {
282
        return preg_replace('#(.*)(/|\\\\)([^\\\\/]+)$#', '$1', $path);
283
    }
284
285
    /**
286
     * Returns filename part of a path
287
     * Early during bootstrap there is no TYPO3_CONF_VARS yet so the setting for the system locale
288
     * is also unavailable. The filename part is determined with a regular expression to avoid issues
289
     * with locales.
290
     *
291
     * @param string $path
292
     *
293
     * @return string
294
     */
295
    public static function basenameDuringBootstrap($path): string
296
    {
297
        return preg_replace('#.*[/\\\\]([^\\\\/]+)$#', '$1', $path);
298
    }
299
300
    /*********************
301
     *
302
     * Cleaning methods
303
     *
304
     *********************/
305
    /**
306
     * Resolves all dots, slashes and removes spaces after or before a path...
307
     *
308
     * @param string $path Input string
309
     * @return string Canonical path, always without trailing slash
310
     */
311
    public static function getCanonicalPath($path)
312
    {
313
        // Replace backslashes with slashes to work with Windows paths if given
314
        $path = trim(str_replace('\\', '/', $path));
315
316
        // @todo do we really need this? Probably only in testing context for vfs?
317
        $protocol = '';
318
        if (strpos($path, '://') !== false) {
319
            [$protocol, $path] = explode('://', $path);
320
            $protocol .= '://';
321
        }
322
323
        $absolutePathPrefix = '';
324
        if (static::isAbsolutePath($path)) {
325
            if (Environment::isWindows() && substr($path, 1, 2) === ':/') {
326
                $absolutePathPrefix = substr($path, 0, 3);
327
                $path = substr($path, 3);
328
            } else {
329
                $path = ltrim($path, '/');
330
                $absolutePathPrefix = '/';
331
            }
332
        }
333
334
        $theDirParts = explode('/', $path);
335
        $theDirPartsCount = count($theDirParts);
336
        for ($partCount = 0; $partCount < $theDirPartsCount; $partCount++) {
337
            // double-slashes in path: remove element
338
            if ($theDirParts[$partCount] === '') {
339
                array_splice($theDirParts, $partCount, 1);
340
                $partCount--;
341
                $theDirPartsCount--;
342
            }
343
            // "." in path: remove element
344
            if (($theDirParts[$partCount] ?? '') === '.') {
345
                array_splice($theDirParts, $partCount, 1);
346
                $partCount--;
347
                $theDirPartsCount--;
348
            }
349
            // ".." in path:
350
            if (($theDirParts[$partCount] ?? '') === '..') {
351
                if ($partCount >= 1) {
352
                    // Remove this and previous element
353
                    array_splice($theDirParts, $partCount - 1, 2);
354
                    $partCount -= 2;
355
                    $theDirPartsCount -= 2;
356
                } elseif ($absolutePathPrefix) {
357
                    // can't go higher than root dir
358
                    // simply remove this part and continue
359
                    array_splice($theDirParts, $partCount, 1);
360
                    $partCount--;
361
                    $theDirPartsCount--;
362
                }
363
            }
364
        }
365
366
        return $protocol . $absolutePathPrefix . implode('/', $theDirParts);
367
    }
368
369
    /**
370
     * Strip first part of a path, equal to the length of public web path including trailing slash
371
     *
372
     * @param string $path
373
     * @return string
374
     * @internal
375
     */
376
    public static function stripPathSitePrefix($path)
377
    {
378
        return substr($path, strlen(Environment::getPublicPath() . '/'));
379
    }
380
}
381