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