TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,433 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Utility;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
use TYPO3\CMS\Core\Core\Environment;
|
||||
use TYPO3\CMS\Core\Http\NormalizedParams;
|
||||
use TYPO3\CMS\Core\SystemResource\Exception\CanNotResolvePublicResourceException;
|
||||
use TYPO3\CMS\Core\SystemResource\Exception\CanNotResolveSystemResourceException;
|
||||
use TYPO3\CMS\Core\SystemResource\Publishing\SystemResourcePublisherInterface;
|
||||
use TYPO3\CMS\Core\SystemResource\Publishing\UriGenerationOptions;
|
||||
use TYPO3\CMS\Core\SystemResource\SystemResourceFactory;
|
||||
|
||||
/**
|
||||
* Class with helper functions for file paths.
|
||||
*/
|
||||
readonly class PathUtility
|
||||
{
|
||||
/**
|
||||
* Creates an absolute URL out of really any input path, removes '../' parts for the targetPath
|
||||
*
|
||||
* @todo: And this exactly is a big issue as it mixes file system paths with (relative) URLs.
|
||||
* Additionally, it depends on the current request and can not do its job on CLI.
|
||||
* Deprecate entirely and replace with stricter API.
|
||||
*
|
||||
* @param string $targetPath can be "../typo3conf/ext/myext/myfile.js" or "/myfile.js"
|
||||
* @param bool $prefixWithSitePath Don't use this argument. It is only used by TYPO3 in one place, which are subject to removal.
|
||||
* @return string something like "/mysite/typo3conf/ext/myext/myfile.js"
|
||||
*/
|
||||
public static function getAbsoluteWebPath(string $targetPath, bool $prefixWithSitePath = true): string
|
||||
{
|
||||
if (static::hasProtocolAndScheme($targetPath)) {
|
||||
return $targetPath;
|
||||
}
|
||||
|
||||
$prefixWithSitePath = $prefixWithSitePath && !Environment::isCli();
|
||||
if (self::isAbsolutePath($targetPath)) {
|
||||
if (str_starts_with($targetPath, Environment::getPublicPath())) {
|
||||
// It is an absolute file system path with file/folder inside document root,
|
||||
// therefore we can strip the full file system path to the document root to obtain the URI
|
||||
$targetPath = self::stripPathSitePrefix($targetPath);
|
||||
} elseif (Environment::isComposerMode() && str_contains($targetPath, 'Resources/Public') && str_starts_with($targetPath, Environment::getProjectPath())) {
|
||||
// TYPO3 is in managed by Composer and it is an absolute file system path inside composer root path,
|
||||
// and a public resource is referenced, therefore we can calculate the path to the published assets
|
||||
// This is true for all Composer packages that are installed in vendor folder by Composer, but still recognized by TYPO3
|
||||
$relativePath = substr($targetPath, strlen(Environment::getProjectPath()));
|
||||
// The $relativePath might contain multiple occurrences of 'Resources/Public', so only search for first one
|
||||
[$relativePrefix, $relativeAssetPath] = explode('Resources/Public', $relativePath, 2);
|
||||
$targetPath = '_assets/' . md5($relativePrefix) . $relativeAssetPath;
|
||||
} else {
|
||||
// At this point it can be ANY path, even an invalid or non existent and it is totally unclear,
|
||||
// whether this is a mistake or accidentally working as intended.
|
||||
// The only conclusion here is, that this API has to be deprecated altogether an be replaced with API
|
||||
// that clearly distinguishes between creating a URL from a static resource and ensuring an URL is absolute and not relative to current script.
|
||||
$prefixWithSitePath = false;
|
||||
}
|
||||
} else {
|
||||
// Make an absolute path out of it
|
||||
$targetPath = self::dirname(Environment::getCurrentScript()) . '/' . $targetPath;
|
||||
$targetPath = self::stripPathSitePrefix($targetPath);
|
||||
}
|
||||
|
||||
if ($prefixWithSitePath) {
|
||||
// @todo: Another reason this method must fall.
|
||||
$targetPath = NormalizedParams::createFromServerParams($_SERVER)->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<string> $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.
|
||||
* <pre>
|
||||
* base: abc/one.txt
|
||||
* include: ../../two.txt
|
||||
* result: ../two.txt
|
||||
* </pre>
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user