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