getSitePath() . $targetPath; } return $targetPath; } /** * @internal Will be removed (or made private) before v14 LTS release * * @throws CanNotResolvePublicResourceException * @throws CanNotResolveSystemResourceException */ public static function getSystemResourceUri(string $resourceIdentifier, ?ServerRequestInterface $request = null, ?UriGenerationOptions $options = null): UriInterface { $resourceFactory = GeneralUtility::makeInstance(SystemResourceFactory::class); $resource = $resourceFactory->createPublicResource($resourceIdentifier); $resourcePublisher = GeneralUtility::makeInstance(SystemResourcePublisherInterface::class); return $resourcePublisher->generateUri($resource, $request, $options); } /** * Checks whether the given path is an extension resource */ public static function isExtensionPath(string $path, bool $includePackagePaths = false): bool { return str_starts_with($path, 'EXT:') || ($includePackagePaths && str_starts_with($path, 'PKG:')); } /** * Gets the common path prefix out of many paths. * + /var/www/domain.com/typo3/sysext/frontend/ * + /var/www/domain.com/typo3/sysext/em/ * + /var/www/domain.com/typo3/sysext/file/ * = /var/www/domain.com/typo3/sysext/ * * @param array $paths Paths to be processed */ public static function getCommonPrefix(array $paths): ?string { $paths = array_map(GeneralUtility::fixWindowsFilePath(...), $paths); $commonPath = null; if (count($paths) === 1) { $commonPath = array_shift($paths); } elseif (count($paths) > 1) { $parts = explode('/', (string)array_shift($paths)); $comparePath = ''; $break = false; foreach ($parts as $part) { $comparePath .= $part . '/'; foreach ($paths as $path) { if (!str_starts_with($path . '/', $comparePath)) { $break = true; break; } } if ($break) { break; } $commonPath = $comparePath; } } if ($commonPath !== null) { $commonPath = self::sanitizeTrailingSeparator($commonPath, '/'); } return $commonPath; } /** * Normalizes a trailing separator. * * (e.g. 'some/path' -> 'some/path/') * * @param string $path The path to be sanitized * @param string $separator The separator to be used */ public static function sanitizeTrailingSeparator(string $path, string $separator = '/'): string { return rtrim($path, $separator) . $separator; } /** * Returns trailing name component of path * * Since basename() is locale dependent we need to access * the filesystem with the same locale of the system, not * the rendering context. * * @see http://www.php.net/manual/en/function.basename.php * * @param string $path */ public static function basename(string $path): string { $targetLocale = $GLOBALS['TYPO3_CONF_VARS']['SYS']['systemLocale'] ?? ''; if (empty($targetLocale)) { return basename($path); } $currentLocale = (string)setlocale(LC_CTYPE, '0'); setlocale(LC_CTYPE, $targetLocale); $basename = basename($path); setlocale(LC_CTYPE, $currentLocale); return $basename; } /** * Returns parent directory's path * * Since dirname() is locale dependent we need to access * the filesystem with the same locale of the system, not * the rendering context. * * @see http://www.php.net/manual/en/function.dirname.php * * @param string $path */ public static function dirname(string $path): string { $targetLocale = $GLOBALS['TYPO3_CONF_VARS']['SYS']['systemLocale'] ?? ''; if (empty($targetLocale)) { return dirname($path); } $currentLocale = (string)setlocale(LC_CTYPE, '0'); setlocale(LC_CTYPE, $targetLocale); $dirname = dirname($path); setlocale(LC_CTYPE, $currentLocale); return $dirname; } /** * Returns parent directory's path * * Since pathinfo() is locale dependent we need to access * the filesystem with the same locale of the system, not * the rendering context. * * The valid flags for $options are the same as for the built-in * phpinfo() function. * * @see http://www.php.net/manual/en/function.pathinfo.php * * @return ($options is PATHINFO_ALL ? array{dirname?: string, basename?: string, extension?: string, filename?: string} : string) */ public static function pathinfo(string $path, int $options = PATHINFO_ALL): string|array { $targetLocale = $GLOBALS['TYPO3_CONF_VARS']['SYS']['systemLocale'] ?? ''; if (empty($targetLocale)) { return pathinfo($path, $options); } $currentLocale = (string)setlocale(LC_CTYPE, '0'); setlocale(LC_CTYPE, $targetLocale); $pathinfo = pathinfo($path, $options); setlocale(LC_CTYPE, $currentLocale); return $pathinfo; } /** * Checks if the $path is absolute or relative (detecting either '/' or 'x:/' as first part of string) and returns TRUE if so. */ public static function isAbsolutePath(string $path): bool { // On Windows also a path starting with a drive letter is absolute: X:/ if (Environment::isWindows() && (substr($path, 1, 2) === ':/' || substr($path, 1, 2) === ':\\')) { return true; } // Path starting with a / is always absolute, on every system return str_starts_with($path, '/'); } /** * Gets the (absolute) path of an include file based on the (absolute) path of a base file * * Does NOT do any sanity checks. This is a task for the calling function, e.g. * call GeneralUtility::getFileAbsFileName() on the result. * @see \TYPO3\CMS\Core\Utility\GeneralUtility::getFileAbsFileName() * * Resolves all dots and slashes between that paths of both files. * Whether the result is absolute or not, depends on the base file name. * * If the include file goes higher than a relative base file, then the result * will contain dots as a relative part. *
     *   base:    abc/one.txt
     *   include: ../../two.txt
     *   result:  ../two.txt
     * 
* The exact behavior, refer to getCanonicalPath(). * * @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 * @param string $includeFileName The name of the file that is included in the file * @return string The (absolute) path of the include file */ public static function getAbsolutePathOfRelativeReferencedFileOrPath(string $baseFilenameOrPath, string $includeFileName): string { $fileName = static::basename($includeFileName); $basePath = str_ends_with($baseFilenameOrPath, '/') ? $baseFilenameOrPath : static::dirname($baseFilenameOrPath); $newDir = static::getCanonicalPath($basePath . '/' . static::dirname($includeFileName)); // Avoid double slash on empty path return (($newDir !== '/') ? $newDir : '') . '/' . $fileName; } /** * Returns parent directory's path * Early during bootstrap there is no TYPO3_CONF_VARS yet so the setting for the system locale * is also unavailable. The path of the parent directory is determined with a regular expression * to avoid issues with locales. * * * @return string Path without trailing slash */ public static function dirnameDuringBootstrap(string $path): string { return preg_replace('#(.*)(/|\\\\)([^\\\\/]+)$#', '$1', $path); } /** * Returns filename part of a path * Early during bootstrap there is no TYPO3_CONF_VARS yet so the setting for the system locale * is also unavailable. The filename part is determined with a regular expression to avoid issues * with locales. */ public static function basenameDuringBootstrap(string $path): string { return preg_replace('#.*[/\\\\]([^\\\\/]+)$#', '$1', $path); } /********************* * * Cleaning methods * *********************/ /** * Resolves all dots, slashes and removes spaces after or before a path... * * @param string $path Input string * @return string Canonical path, always without trailing slash */ public static function getCanonicalPath(string $path): string { // Replace backslashes with slashes to work with Windows paths if given $path = trim(str_replace('\\', '/', $path)); // @todo do we really need this? Probably only in testing context for vfs? $protocol = ''; if (str_contains($path, '://')) { [$protocol, $path] = explode('://', $path); $protocol .= '://'; } $absolutePathPrefix = ''; if (static::isAbsolutePath($path)) { if (Environment::isWindows() && substr($path, 1, 2) === ':/') { $absolutePathPrefix = substr($path, 0, 3); $path = substr($path, 3); } else { $path = ltrim($path, '/'); $absolutePathPrefix = '/'; } } $theDirParts = explode('/', $path); $theDirPartsCount = count($theDirParts); // This cannot use a foreach() as some steps skip ahead multiple elements. for ($partCount = 0; $partCount < $theDirPartsCount; $partCount++) { // double-slashes in path: remove element if ($theDirParts[$partCount] === '') { array_splice($theDirParts, $partCount, 1); $partCount--; $theDirPartsCount--; } // "." in path: remove element if (($theDirParts[$partCount] ?? '') === '.') { array_splice($theDirParts, $partCount, 1); $partCount--; $theDirPartsCount--; } // ".." in path: if (($theDirParts[$partCount] ?? '') === '..') { if ($partCount >= 1) { // Remove this and previous element array_splice($theDirParts, $partCount - 1, 2); $partCount -= 2; $theDirPartsCount -= 2; } elseif ($absolutePathPrefix) { // can't go higher than root dir // simply remove this part and continue array_splice($theDirParts, $partCount, 1); $partCount--; $theDirPartsCount--; } } } return $protocol . $absolutePathPrefix . implode('/', $theDirParts); } /** * Strip first part of a path, equal to the length of public web path including trailing slash * * @internal */ public static function stripPathSitePrefix(string $path): string { return substr($path, strlen(Environment::getPublicPath() . '/')); } /** * Tries to guess whether a given URL hast protocol and (optional) scheme. * Scheme relative URLs match as well. * Current implementation is two simple string operations. * * This is just a guess. For a more detailed validation and parsing, * use \TYPO3\CMS\Core\Utility\GeneralUtility::isValidUrl() * * @param string $path * * @internal */ public static function hasProtocolAndScheme(string $path): bool { return str_starts_with($path, '//') || strpos($path, '://') > 0; } /** * Evaluates a given path against the optional settings in `$GLOBALS['TYPO3_CONF_VARS']['BE']['lockRootPath']`. * Albeit the name `BE/lockRootPath` is misleading, this setting was and is used in general and is not limited * to the backend-scope. The setting actually allows defining additional paths, besides the project root path. * * @param string $path Absolute path to a file or directory */ public static function isAllowedAdditionalPath(string $path): bool { // ensure the submitted path ends with a string, even for a file $path = self::sanitizeTrailingSeparator($path); $allowedPaths = $GLOBALS['TYPO3_CONF_VARS']['BE']['lockRootPath'] ?? []; if (is_string($allowedPaths)) { // The setting was a string before and is now an array // For compatibility reasons, we cast a string to an array here for now $allowedPaths = [$allowedPaths]; } if (!is_array($allowedPaths)) { throw new \RuntimeException('$GLOBALS[\'TYPO3_CONF_VARS\'][\'BE\'][\'lockRootPath\'] is expected to be an array.', 1707408379); } foreach ($allowedPaths as $allowedPath) { $allowedPath = trim($allowedPath); if ($allowedPath !== '' && str_starts_with($path, self::sanitizeTrailingSeparator($allowedPath))) { return true; } } return false; } }