TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:09 +02:00
commit af8cc155b5
6818 changed files with 642608 additions and 0 deletions
File diff suppressed because it is too large Load Diff
+81
View File
@@ -0,0 +1,81 @@
<?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 TYPO3\CMS\Extbase\Persistence\RepositoryInterface;
/**
* Several functions related to naming and conversions of names
* such as translation between Repository and Model names or
* exploding an objectControllerName into pieces
*/
class ClassNamingUtility
{
/**
* Translates a model name to an appropriate repository name
* e.g. Tx_Extbase_Domain_Model_Foo to Tx_Extbase_Domain_Repository_FooRepository
* or \TYPO3\CMS\Extbase\Domain\Model\Foo to \TYPO3\CMS\Extbase\Domain\Repository\FooRepository
*/
public static function translateModelNameToRepositoryName(string $modelName): string
{
return str_replace(
'\\Domain\\Model',
'\\Domain\\Repository',
$modelName
) . 'Repository';
}
/**
* Translates a repository name to an appropriate model name
* e.g. Tx_Extbase_Domain_Repository_FooRepository to Tx_Extbase_Domain_Model_Foo
* or \TYPO3\CMS\Extbase\Domain\Repository\FooRepository to \TYPO3\CMS\Extbase\Domain\Model\Foo
*
* @param class-string<RepositoryInterface> $repositoryName
*
* @return class-string
*/
public static function translateRepositoryNameToModelName(string $repositoryName): string
{
return preg_replace(
['/\\\\Domain\\\\Repository/', '/Repository$/'],
['\\Domain\\Model', ''],
$repositoryName
);
}
/**
* Explodes a controllerObjectName like \Vendor\Ext\Controller\FooController
* into several pieces like vendorName, extensionName, subpackageKey and controllerName
*
* @param string $controllerObjectName The controller name to be exploded
* @return array<string> An array of controllerObjectName pieces
*/
public static function explodeObjectControllerName(string $controllerObjectName): array
{
$matches = [];
$extensionName = str_starts_with($controllerObjectName, 'TYPO3\\CMS')
? '^(?P<vendorName>[^\\\\]+\\\[^\\\\]+)\\\(?P<extensionName>[^\\\\]+)'
: '^(?P<vendorName>[^\\\\]+)\\\\(?P<extensionName>[^\\\\]+)';
preg_match(
'/' . $extensionName . '\\\\(Controller|Command|(?P<subpackageKey>.+)\\\\Controller)\\\\(?P<controllerName>[a-z\\\\]+)Controller$/ix',
$controllerObjectName,
$matches
);
return array_filter($matches, is_string(...), ARRAY_FILTER_USE_KEY);
}
}
+549
View File
@@ -0,0 +1,549 @@
<?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\Log\LoggerInterface;
use Symfony\Component\Process\Exception\RuntimeException;
use Symfony\Component\Process\Process;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Log\LogManager;
/**
* Class to handle system commands.
* finds executables (programs) on Unix and Windows without knowing where they are
*
* returns exec command for a program
* or FALSE
*
* This class is meant to be used without instance:
*
* ```
* $cmd = CommandUtility::getCommand ('awstats','perl');
* ```
*
* The data of this class is cached.
* That means if a program is found once it don't have to be searched again.
*
* user functions:
*
* addPaths() could be used to extend the search paths
* getCommand() get a command string
* checkCommand() returns TRUE if a command is available
*
* Search paths that are included:
* $TYPO3_CONF_VARS['GFX']['processor_path']
* $TYPO3_CONF_VARS['SYS']['binPath']
* $GLOBALS['_SERVER']['PATH']
* '/usr/bin/,/usr/local/bin/' on Unix
*
* binaries can be preconfigured with
* $TYPO3_CONF_VARS['SYS']['binSetup']
*/
class CommandUtility
{
/**
* Tells if object is already initialized
*/
protected static bool $initialized = false;
/**
* Contains application list. This is an array with the following structure:
* - app => file name to the application (like 'tar' or 'bzip2')
* - path => full path to the application without application name (like '/usr/bin/' for '/usr/bin/tar')
* - valid => TRUE or FALSE
* Array key is identical to 'app'.
*
* @var array<string, array{app: string, path: string, valid: bool}>
*/
protected static array $applications = [];
/**
* Paths where to search for applications
*
* The key is a path. The value is either the same path, or false if the path is not valid.
*
* @var array<string, string|false>|null
*/
protected static ?array $paths = null;
/**
* Execute a shell command.
*
* Needs to be central to have better control and possible fix for issues. Is a wrapper for Symfony's Process
* component.
*
* @see Process
*/
public static function exec(string|array $command, ?array &$output = null, int &$returnValue = 0, ?float $timeout = 60): string|false
{
if (is_string($command)) {
$process = Process::fromShellCommandline($command, null, null, null, $timeout);
} else {
$process = new Process($command, null, null, null, $timeout);
}
try {
$returnValue = $process->run();
} catch (RuntimeException $runtimeException) {
self::getLogger()->warning('Executing command "{command}" failed.', [
'command' => $command,
'exception' => $runtimeException,
]);
return false;
}
$processOutput = $process->getOutput();
if (str_ends_with($processOutput, PHP_EOL)) {
// Last \n is ignored by PHP exec(): https://github.com/php/php-src/blob/b675db4c56dd0de4ea1f5195d587ed90f0096ed8/ext/standard/exec.c#L148
$processOutput = substr($processOutput, 0, -1);
}
$output = explode(PHP_EOL, $processOutput);
return rtrim(strrchr($processOutput, PHP_EOL) ?: $processOutput);
}
/**
* Compile the command for running ImageMagick/GraphicsMagick.
*
* @param string $command Command to be run: identify, convert or combine/composite
* @param string $parameters The parameters string
* @param string $path Override the default path (e.g. used by the install tool)
* @return string Compiled command that deals with ImageMagick & GraphicsMagick
*/
public static function imageMagickCommand(string $command, string $parameters, string $path = ''): string
{
$gfxConf = $GLOBALS['TYPO3_CONF_VARS']['GFX'];
$isExt = Environment::isWindows() ? '.exe' : '';
if (!$path) {
$path = (string)($gfxConf['processor_path'] ?? '');
}
$path = GeneralUtility::fixWindowsFilePath($path);
// This is only used internally, has no effect outside
if ($command === 'combine') {
$command = 'composite';
}
// Compile the path & command
if ($gfxConf['processor'] === 'GraphicsMagick') {
$path = self::escapeShellArgument($path . 'gm' . $isExt) . ' ' . self::escapeShellArgument($command);
} else {
if (Environment::isWindows() && !@is_file($path . $command . $isExt)) {
$path = self::escapeShellArgument($path . 'magick' . $isExt) . ' ' . self::escapeShellArgument($command);
} else {
$path = self::escapeShellArgument($path . $command . $isExt);
}
}
// strip profile information for thumbnails and reduce their size
if ($parameters && $command !== 'identify') {
// Use legacy processor_stripColorProfileCommand setting if defined, otherwise
// use the preferred configuration option processor_stripColorProfileParameters
$stripColorProfileCommand = $gfxConf['processor_stripColorProfileCommand']
?? implode(' ', array_map(CommandUtility::escapeShellArgument(...), $gfxConf['processor_stripColorProfileParameters'] ?? []));
// Determine whether the strip profile action has be disabled by TypoScript:
if ($gfxConf['processor_stripColorProfileByDefault']
&& $stripColorProfileCommand !== ''
&& $parameters !== '-version'
&& !str_contains($parameters, $stripColorProfileCommand)
&& !str_contains($parameters, '###SkipStripProfile###')
) {
$parameters = $stripColorProfileCommand . ' ' . $parameters;
} else {
$parameters = str_replace('###SkipStripProfile###', '', $parameters);
}
// When converting images that have background transparency, this needs to be not filled,
// but preserved, so that e.g. conversion from SVG into PNG/JPG contains transparency info.
// Without this option, the default background color for conversions is white (https://imagemagick.org/script/command-line-options.php#background)
$parameters = '-background none ' . $parameters;
}
// Add -auto-orient on convert so IM/GM respects the image orient
if ($parameters && $command === 'convert') {
$parameters = '-auto-orient ' . $parameters;
}
// set interlace parameter for convert command
if ($command !== 'identify' && $gfxConf['processor_interlace']) {
$parameters = '-interlace ' . CommandUtility::escapeShellArgument($gfxConf['processor_interlace']) . ' ' . $parameters;
}
$cmdLine = $path . ' ' . $parameters;
// It is needed to change the parameters order when a mask image has been specified
if ($command === 'composite') {
$paramsArr = self::unQuoteFilenames($parameters);
$paramsArrCount = count($paramsArr);
if ($paramsArrCount > 5) {
$tmp = $paramsArr[$paramsArrCount - 3];
$paramsArr[$paramsArrCount - 3] = $paramsArr[$paramsArrCount - 4];
$paramsArr[$paramsArrCount - 4] = $tmp;
}
$cmdLine = $path . ' ' . implode(' ', $paramsArr);
}
return $cmdLine;
}
/**
* Checks if a command is valid or not, updates global variables
*
* @param string $cmd The command that should be executed. eg: "convert"
* @param string $handler Executor for the command. eg: "perl"
* @return bool|int True if the command is valid; False if cmd is not found; -1 if the handler is not found
*/
public static function checkCommand(string $cmd, string $handler = ''): bool|int
{
if (!self::init()) {
return false;
}
if ($handler !== '' && !self::checkCommand($handler)) {
return -1;
}
// Already checked and valid
if (self::$applications[$cmd]['valid'] ?? false) {
return true;
}
// Is set but was (above) not TRUE
if (isset(self::$applications[$cmd]['valid'])) {
return false;
}
foreach (self::$paths as $path => $validPath) {
// Ignore invalid (FALSE) paths
if ($validPath) {
if (Environment::isWindows()) {
// Windows OS
// @todo Why is_executable() is not called here?
if (@is_file($path . $cmd)) {
self::$applications[$cmd]['app'] = $cmd;
self::$applications[$cmd]['path'] = $path;
self::$applications[$cmd]['valid'] = true;
return true;
}
if (@is_file($path . $cmd . '.exe')) {
self::$applications[$cmd]['app'] = $cmd . '.exe';
self::$applications[$cmd]['path'] = $path;
self::$applications[$cmd]['valid'] = true;
return true;
}
} else {
// Unix-like OS
$filePath = realpath($path . $cmd);
if ($filePath && @is_executable($filePath)) {
self::$applications[$cmd]['app'] = $cmd;
self::$applications[$cmd]['path'] = $path;
self::$applications[$cmd]['valid'] = true;
return true;
}
}
}
}
// Try to get the executable with the command 'which'.
// It does the same like already done, but maybe on other paths
if (!Environment::isWindows()) {
$output = null;
$returnValue = 0;
$cmd = @self::exec('which ' . self::escapeShellArgument($cmd), $output, $returnValue);
if ($returnValue === 0) {
self::$applications[$cmd]['app'] = $cmd;
self::$applications[$cmd]['path'] = PathUtility::dirname($cmd) . '/';
self::$applications[$cmd]['valid'] = true;
return true;
}
}
return false;
}
/**
* Returns a command string for exec(), system()
*
* @param string $cmd The command that should be executed. eg: "convert"
* @param string $handler Handler (executor) for the command. eg: "perl"
* @param string $handlerOpt Options for the handler, like '-w' for "perl"
* @return string|bool|int Returns command string, or FALSE if cmd is not found, or -1 if the handler is not found
*/
public static function getCommand(string $cmd, string $handler = '', string $handlerOpt = ''): string|bool|int
{
if (!self::init()) {
return false;
}
// Handler
if ($handler) {
$handler = self::getCommand($handler);
if (!$handler) {
return -1;
}
$handler .= ' ' . escapeshellcmd($handlerOpt) . ' ';
}
// Command
if (!self::checkCommand($cmd)) {
return false;
}
$cmd = self::$applications[$cmd]['path'] . self::$applications[$cmd]['app'] . ' ';
return trim($handler . $cmd);
}
/**
* Extend the preset paths. This way an extension can install an executable and provide the path to \TYPO3\CMS\Core\Utility\CommandUtility
*
* @param string $paths Comma separated list of extra paths where a command should be searched. Relative paths (without leading "/") are prepend with public web path
*/
public static function addPaths(string $paths): void
{
self::initPaths($paths);
}
/**
* Returns an array of search paths
*
* @param bool $addInvalid If set the array contains invalid path too. Then the key is the path and the value is empty
* @return array<string, string|false> Array of search paths (empty if exec is disabled)
*/
public static function getPaths(bool $addInvalid = false): array
{
if (!self::init()) {
return [];
}
return $addInvalid
? self::$paths
: array_filter(self::$paths);
}
/**
* Initializes this class
*/
protected static function init(): bool
{
if ($GLOBALS['TYPO3_CONF_VARS']['BE']['disable_exec_function']) {
return false;
}
if (!self::$initialized) {
self::initPaths();
self::$applications = self::getConfiguredApps();
self::$initialized = true;
}
return true;
}
/**
* Initializes and extends the preset paths with own
*
* @param string $paths Comma separated list of extra paths where a command should be searched. Relative paths (without leading "/") are prepend with public web path
*/
protected static function initPaths(string $paths = ''): void
{
$doCheck = false;
// Init global paths array if not already done
if (!is_array(self::$paths)) {
self::$paths = self::getPathsInternal();
$doCheck = true;
}
// Merge the submitted paths array to the global
if ($paths) {
$paths = GeneralUtility::trimExplode(',', $paths, true);
foreach ($paths as $path) {
// Make absolute path of relative
if (!str_starts_with($path, '/')) {
$path = Environment::getProjectPath() . '/' . $path;
}
if (!isset(self::$paths[$path])) {
if (@is_dir($path)) {
self::$paths[$path] = $path;
} else {
self::$paths[$path] = false;
}
}
}
}
// Check if new paths are invalid
if ($doCheck) {
foreach (self::$paths as $path => $valid) {
// Ignore invalid (FALSE) paths
if ($valid && !@is_dir($path)) {
self::$paths[$path] = false;
}
}
}
}
/**
* Processes and returns the paths from $GLOBALS['TYPO3_CONF_VARS']['SYS']['binSetup']
*
* @return array<string, array{app: string, path: string, valid: bool}> Array of commands and path
*/
protected static function getConfiguredApps(): array
{
$cmdArr = [];
if ($GLOBALS['TYPO3_CONF_VARS']['SYS']['binSetup']) {
$binSetup = str_replace(['\'.chr(10).\'', '\' . LF . \''], LF, $GLOBALS['TYPO3_CONF_VARS']['SYS']['binSetup']);
$pathSetup = preg_split('/[\n,]+/', $binSetup);
foreach ($pathSetup as $val) {
if (trim($val) === '') {
continue;
}
[$cmd, $cmdPath] = GeneralUtility::trimExplode('=', $val, true, 2);
$cmdArr[$cmd]['app'] = PathUtility::basename($cmdPath);
$cmdArr[$cmd]['path'] = PathUtility::dirname($cmdPath) . '/';
$cmdArr[$cmd]['valid'] = true;
}
}
return $cmdArr;
}
/**
* Sets the search paths from different sources, internal
*
* @return array<string, string> Array of absolute paths (keys and values are equal)
*/
protected static function getPathsInternal(): array
{
$pathsArr = [];
$sysPathArr = [];
// Image magick paths first
if ($imPath = $GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_path']) {
$imPath = self::fixPath($imPath);
$pathsArr[$imPath] = $imPath;
}
// Add configured paths
if ($GLOBALS['TYPO3_CONF_VARS']['SYS']['binPath']) {
$sysPath = GeneralUtility::trimExplode(',', $GLOBALS['TYPO3_CONF_VARS']['SYS']['binPath'], true);
foreach ($sysPath as $val) {
$val = self::fixPath($val);
$sysPathArr[$val] = $val;
}
}
// Add path from environment
if (!empty($GLOBALS['_SERVER']['PATH']) || !empty($GLOBALS['_SERVER']['Path'])) {
$sep = Environment::isWindows() ? ';' : ':';
$serverPath = $GLOBALS['_SERVER']['PATH'] ?? $GLOBALS['_SERVER']['Path'];
$envPath = GeneralUtility::trimExplode($sep, $serverPath, true);
foreach ($envPath as $val) {
$val = self::fixPath($val);
$sysPathArr[$val] = $val;
}
}
// Set common paths for Unix (only)
if (!Environment::isWindows()) {
$sysPathArr = array_merge($sysPathArr, [
'/usr/bin/' => '/usr/bin/',
'/usr/local/bin/' => '/usr/local/bin/',
]);
}
return array_merge($pathsArr, $sysPathArr);
}
/**
* Set a path to the right format
*
* @param string $path Input path
* @return string Output path
*/
protected static function fixPath(string $path): string
{
return str_replace('//', '/', $path . '/');
}
/**
* Escape shell arguments (for example filenames) to be used on the local system.
*
* The setting UTF8filesystem will be taken into account.
*
* @param string[] $input Input arguments to be escaped
* @return string[] Escaped shell arguments
*/
public static function escapeShellArguments(array $input): array
{
$isUTF8Filesystem = !empty($GLOBALS['TYPO3_CONF_VARS']['SYS']['UTF8filesystem']);
$currentLocale = false;
if ($isUTF8Filesystem) {
if ($GLOBALS['TYPO3_CONF_VARS']['SYS']['systemLocale'] ?? false) {
$currentLocale = setlocale(LC_CTYPE, '0');
setlocale(LC_CTYPE, $GLOBALS['TYPO3_CONF_VARS']['SYS']['systemLocale']);
}
}
$output = array_map('escapeshellarg', $input);
if ($isUTF8Filesystem && $currentLocale !== false) {
setlocale(LC_CTYPE, $currentLocale);
}
return $output;
}
/**
* Explode a string (normally a list of filenames) with whitespaces by considering quotes in that string.
*
* @param string $parameters The whole parameters string
* @return array Exploded parameters
*/
protected static function unQuoteFilenames(string $parameters): array
{
$paramsArr = explode(' ', trim($parameters));
// Whenever a quote character (") is found, $quoteActive is set to the element number inside of $params.
// A value of -1 means that there are not open quotes at the current position.
$quoteActive = -1;
foreach ($paramsArr as $k => $v) {
if ($quoteActive > -1) {
$paramsArr[$quoteActive] .= ' ' . $v;
unset($paramsArr[$k]);
if (substr($v, -1) === $paramsArr[$quoteActive][0]) {
$quoteActive = -1;
}
} elseif (!trim($v)) {
// Remove empty elements
unset($paramsArr[$k]);
} elseif (preg_match('/^(["\'])/', $v) && substr($v, -1) !== $v[0]) {
$quoteActive = $k;
}
}
// Return re-indexed array
return array_values($paramsArr);
}
/**
* Escape a shell argument (for example a filename) to be used on the local system.
*
* The setting UTF8filesystem will be taken into account.
*
* @param string $input Input-argument to be escaped
* @return string Escaped shell argument
*/
public static function escapeShellArgument(string $input): string
{
return self::escapeShellArguments([$input])[0];
}
protected static function getLogger(): LoggerInterface
{
return GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__);
}
}
+175
View File
@@ -0,0 +1,175 @@
<?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 TYPO3\CMS\Core\IO\CsvStreamFilter;
/**
* Class with helper functions for CSV handling
*/
class CsvUtility
{
/**
* whether to passthrough data as is, without any modification
*/
public const TYPE_PASSTHROUGH = 0;
/**
* whether to remove control characters like `=`, `+`, ...
*/
public const TYPE_REMOVE_CONTROLS = 1;
/**
* whether to prefix control characters like `=`, `+`, ...
* to become `'=`, `'+`, ...
*/
public const TYPE_PREFIX_CONTROLS = 2;
/**
* Convert a string, formatted as CSV, into a multidimensional array
*
* This cannot be done by str_getcsv, since it's impossible to handle enclosed cells with a line feed in it
*
* @param string $input The CSV input
* @param string $fieldDelimiter The field delimiter
* @param string $fieldEnclosure The field enclosure
* @param int $maximumColumns The maximum amount of columns
*/
public static function csvToArray(string $input, string $fieldDelimiter = ',', string $fieldEnclosure = '"', int $maximumColumns = 0): array
{
$multiArray = [];
$maximumCellCount = 0;
if (($handle = fopen('php://memory', 'r+')) !== false) {
fwrite($handle, $input);
rewind($handle);
while (($cells = fgetcsv($handle, 0, $fieldDelimiter, $fieldEnclosure, '\\')) !== false) {
$maximumCellCount = max(count($cells), $maximumCellCount);
$multiArray[] = preg_replace('|<br */?>|i', LF, $cells);
}
fclose($handle);
}
if ($maximumColumns > $maximumCellCount) {
$maximumCellCount = $maximumColumns;
}
foreach ($multiArray as &$row) {
for ($key = 0; $key < $maximumCellCount; $key++) {
if (
$maximumColumns > 0
&& $maximumColumns < $maximumCellCount
&& $key >= $maximumColumns
) {
if (isset($row[$key])) {
unset($row[$key]);
}
} elseif (!isset($row[$key])) {
$row[$key] = '';
}
}
}
return $multiArray;
}
/**
* Takes a row and returns a CSV string of the values with $delim (default is ,) and $quote (default is ") as separator chars.
*
* @param string[] $row Input array of values
* @param string $delim Delimited, default is comma
* @param string $quote Quote-character to wrap around the values.
* @param int $type Output behaviour concerning potentially harmful control literals
* @return string A single line of CSV
*/
public static function csvValues(array $row, string $delim = ',', string $quote = '"', int $type = self::TYPE_REMOVE_CONTROLS): string
{
$resource = fopen('php://temp', 'w');
if (!is_resource($resource)) {
throw new \RuntimeException('Cannot open temporary data stream for writing', 1625556521);
}
$modifier = CsvStreamFilter::applyStreamFilter($resource, false);
array_map(self::assertCellValueType(...), $row);
if ($type === self::TYPE_REMOVE_CONTROLS) {
$row = array_map(self::removeControlLiterals(...), $row);
} elseif ($type === self::TYPE_PREFIX_CONTROLS) {
$row = array_map(self::prefixControlLiterals(...), $row);
}
fputcsv($resource, $modifier($row), $delim, $quote, '\\');
fseek($resource, 0);
return stream_get_contents($resource);
}
/**
* Prefixes control literals at the beginning of a cell value with a single quote
*
* (e.g. `=+value` --> `'=+value`)
*/
protected static function prefixControlLiterals(bool|int|float|string|null $cellValue): bool|int|float|string|null
{
if (!self::shallFilterValue($cellValue)) {
return $cellValue;
}
$cellValue = (string)$cellValue;
return preg_replace('#^([\t\v=+*%/@-])#', '\'${1}', $cellValue);
}
/**
* Removes control literals from the beginning of a cell value
*
* (e.g. `=+value` --> `value`)
*/
protected static function removeControlLiterals(bool|int|float|string|null $cellValue): bool|int|float|string|null
{
if (!self::shallFilterValue($cellValue)) {
return $cellValue;
}
$cellValue = (string)$cellValue;
return preg_replace('#^([\t\v=+*%/@-]+)+#', '', $cellValue);
}
/**
* Asserts scalar or null types for given cell value.
*/
protected static function assertCellValueType(mixed $cellValue): void
{
// int, float, string, bool, null
if ($cellValue === null || is_scalar($cellValue)) {
return;
}
throw new \RuntimeException(
sprintf('Unexpected type %s for cell value', gettype($cellValue)),
1625562833
);
}
/**
* Whether cell value shall be filtered.
*
* This applies to everything that is not or cannot be represented
* as boolean, integer or float.
*/
protected static function shallFilterValue(bool|int|float|string|null $cellValue): bool
{
return $cellValue !== null
&& !is_bool($cellValue)
&& !is_numeric($cellValue)
&& !MathUtility::canBeInterpretedAsInteger($cellValue)
&& !MathUtility::canBeInterpretedAsFloat($cellValue);
}
}
+158
View File
@@ -0,0 +1,158 @@
<?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 TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Extbase\Utility\DebuggerUtility;
/**
* Class to handle debug
*/
class DebugUtility
{
protected static bool $plainTextOutput = true;
protected static bool $ansiColorUsage = true;
/**
* Debug
*
* Directly echos out debug information as HTML (or plain in CLI context)
*/
public static function debug(mixed $var = '', string $header = 'Debug'): void
{
// buffer the output of debug if no buffering started before
if (ob_get_level() === 0) {
ob_start();
}
echo self::renderDump($var, $header);
}
/**
* Converts a variable to a string
*
* @return string Plain, not HTML encoded string
*/
public static function convertVariableToString(mixed $variable): string
{
$string = self::renderDump($variable, '', true, false);
return $string === '' ? '| debug |' : $string;
}
/**
* Displays the "path" of the function call stack in a string, using debug_backtrace
*
* @param bool $prependFileNames If set to true file names are added to the output
* @return string Plain, not HTML encoded string
*/
public static function debugTrail(bool $prependFileNames = false): string
{
$trail = debug_backtrace(0);
$trail = array_reverse($trail);
array_pop($trail);
$path = [];
foreach ($trail as $dat) {
$fileInformation = $prependFileNames && !empty($dat['file']) ? $dat['file'] . ':' : '';
$pathFragment = $fileInformation . ($dat['class'] ?? '') . ($dat['type'] ?? '') . $dat['function'];
// add the path of the included file
if (in_array($dat['function'], ['require', 'include', 'require_once', 'include_once'])) {
$pathFragment .= '(' . PathUtility::stripPathSitePrefix($dat['args'][0]) . '),' . PathUtility::stripPathSitePrefix($dat['file']);
}
if (array_key_exists('line', $dat)) {
$path[] = $pathFragment . '#' . $dat['line'];
} else {
$path[] = $pathFragment;
}
}
return implode(' // ', $path);
}
/**
* Returns a string with a list of ascii-values for the first $characters characters in $string
*
* @param string $string String to show ASCII value for
* @param int $characters Number of characters to show
* @return string The string with ASCII values in separated by a space char.
*/
public static function ordinalValue(string $string, int $characters = 100): string
{
if (strlen($string) < $characters) {
$characters = strlen($string);
}
$valuestring = '';
for ($i = 0; $i < $characters; $i++) {
$valuestring .= ' ' . ord($string[$i]);
}
return trim($valuestring);
}
/**
* Returns HTML-code, which is a visual representation of a multidimensional array
* use \TYPO3\CMS\Core\Utility\GeneralUtility::print_array() in order to print an array
* Returns FALSE if $array_in is not an array
*
* @param mixed $array_in Array to view
* @return string HTML output
*/
public static function viewArray(mixed $array_in): string
{
return self::renderDump($array_in);
}
/**
* Renders the dump according to the context, either for command line or as HTML output
*
* @param bool|null $plainText Omit or pass null to use the current default.
* @param bool|null $ansiColors Omit or pass null to use the current default.
*/
protected static function renderDump(mixed $variable, string $title = '', ?bool $plainText = null, ?bool $ansiColors = null): string
{
$plainText = $plainText ?? Environment::isCli() && self::$plainTextOutput;
$ansiColors = $ansiColors ?? Environment::isCli() && self::$ansiColorUsage;
return trim(DebuggerUtility::var_dump($variable, $title, 8, $plainText, $ansiColors, true));
}
/**
* Preset plaintext output
*
* Warning:
* This is NOT a public API method and must not be used in own extensions!
* This method is usually only used in tests to preset the output behaviour
*
* @internal
*/
public static function usePlainTextOutput(bool $plainTextOutput): void
{
static::$plainTextOutput = $plainTextOutput;
}
/**
* Preset ansi color usage
*
* Warning:
* This is NOT a public API method and must not be used in own extensions!
* This method is usually only used in tests to preset the ansi color usage
*
* @internal
*/
public static function useAnsiColor(bool $ansiColorUsage): void
{
static::$ansiColorUsage = $ansiColorUsage;
}
}
+26
View File
@@ -0,0 +1,26 @@
<?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;
enum DiffGranularity
{
// Show the diff "by word"
case WORD;
// Show any diff (like a patch) based on a single character
case CHARACTER;
}
+34
View File
@@ -0,0 +1,34 @@
<?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 cogpowered\FineDiff\Diff;
use cogpowered\FineDiff\Granularity\Character;
use cogpowered\FineDiff\Granularity\Word;
/**
* Helper service to create a diff HTML of two strings.
* It is currently a facade for lolli42/finediff.
*/
readonly class DiffUtility
{
public function diff(string $from, string $to, DiffGranularity $granularity = DiffGranularity::WORD): string
{
return (new Diff($granularity === DiffGranularity::WORD ? new Word() : new Character()))->render($from, $to);
}
}
@@ -0,0 +1,27 @@
<?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\Exception;
/**
* Exception thrown if ArrayUtility::getValueByPath() and
* ArrayUtility::removeByPath() don't find the target path in given array.
*
* Note this extends from \RuntimeException to be backwards compatible with the
* formerly thrown \RuntimeException in the method.
*/
class MissingArrayPathException extends \RuntimeException {}
@@ -0,0 +1,26 @@
<?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\Exception;
/**
* Exception thrown if a method is not implemented yet.
*
* Note this extends from \RuntimeException to be backwards compatible with the
* formerly thrown \RuntimeException in the methods.
*/
class NotImplementedMethodException extends \RuntimeException {}
File diff suppressed because it is too large Load Diff
+150
View File
@@ -0,0 +1,150 @@
<?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\File;
use TYPO3\CMS\Core\Charset\CharsetConverter;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\PathUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* Contains class with basic file management functions
*
* Contains functions for management, validation etc of files in TYPO3.
*
* @internal All methods in this class should not be used anymore since TYPO3 6.0, this class is therefore marked
* as internal.
* Please use corresponding \TYPO3\CMS\Core\Resource\ResourceStorage
* (fetched via BE_USERS->getFileStorages()), as all functions should be
* found there (in a cleaner manner).
*/
class BasicFileUtility
{
/**
* @var string
*/
public const UNSAFE_FILENAME_CHARACTER_EXPRESSION = '\\x00-\\x2C\\/\\x3A-\\x3F\\x5B-\\x60\\x7B-\\xBF';
/**
* This number decides the highest allowed appended number used on a filename before we use naming with unique strings
*
* @var int
*/
public $maxNumber = 99;
/**
* This number decides how many characters out of a unique MD5-hash that is appended to a filename if getUniqueName is asked to find an available filename.
*
* @var int
*/
public $uniquePrecision = 6;
/**
* Cleans $theDir for slashes in the end of the string and returns the new path, if it exists on the server.
*
* @param string $theDir Directory path to check
* @return bool|string Returns the cleaned up directory name if OK, otherwise FALSE.
* @todo: should go into the LocalDriver in a protected way (not important to the outside world)
*/
protected function sanitizeFolderPath($theDir)
{
if (!GeneralUtility::validPathStr($theDir)) {
return false;
}
$theDir = PathUtility::getCanonicalPath($theDir);
if (@is_dir($theDir)) {
return $theDir;
}
return false;
}
/**
* Returns the destination path/filename of a unique filename/foldername in that path.
* If $theFile exists in $theDest (directory) the file have numbers appended up to $this->maxNumber. Hereafter a unique string will be appended.
* This function is used by fx. DataHandler when files are attached to records and needs to be uniquely named in the uploads/* folders
*
* @param string $theFile The input filename to check
* @param string $theDest The directory for which to return a unique filename for $theFile. $theDest MUST be a valid directory. Should be absolute.
* @param bool $dontCheckForUnique If set the filename is returned with the path prepended without checking whether it already existed!
* @return string|null The destination absolute filepath (not just the name!) of a unique filename/foldername in that path.
* @internal May be removed without further notice. Method has been marked as deprecated for various versions but is still used in core.
* @todo: should go into the LocalDriver in a protected way (not important to the outside world)
*/
public function getUniqueName($theFile, $theDest, $dontCheckForUnique = false)
{
// $theDest is cleaned up
$theDest = $this->sanitizeFolderPath($theDest);
if ($theDest) {
// Fetches info about path, name, extension of $theFile
$origFileInfo = GeneralUtility::split_fileref($theFile);
// Check if the file exists and if not - return the filename...
$fileInfo = $origFileInfo;
$theDestFile = $theDest . '/' . $fileInfo['file'];
// The destinations file
if (!file_exists($theDestFile) || $dontCheckForUnique) {
// If the file does NOT exist we return this filename
return $theDestFile;
}
// Well the filename in its pure form existed. Now we try to append numbers / unique-strings and see if we can find an available filename...
$theTempFileBody = preg_replace('/_[0-9][0-9]$/', '', $origFileInfo['filebody']);
// This removes _xx if appended to the file
$theOrigExt = $origFileInfo['realFileext'] ? '.' . $origFileInfo['realFileext'] : '';
for ($a = 1; $a <= $this->maxNumber + 1; $a++) {
if ($a <= $this->maxNumber) {
// First we try to append numbers
$insert = '_' . sprintf('%02d', $a);
} else {
// .. then we try unique-strings...
$insert = '_' . substr(md5(StringUtility::getUniqueId()), 0, $this->uniquePrecision);
}
$theTestFile = $theTempFileBody . $insert . $theOrigExt;
$theDestFile = $theDest . '/' . $theTestFile;
// The destinations file
if (!file_exists($theDestFile)) {
// If the file does NOT exist we return this filename
return $theDestFile;
}
}
}
return null;
}
/**
* Returns a string where any character not matching [.a-zA-Z0-9_-] is substituted by '_'
* Trailing dots are removed
*
* @param string $fileName Input string, typically the body of a filename
* @return string Output string with any characters not matching [.a-zA-Z0-9_-] is substituted by '_' and trailing dots removed
* @internal May be removed without further notice. Method has been marked as deprecated for various versions but is still used in core.
*/
public function cleanFileName($fileName)
{
// Handle UTF-8 characters
if ($GLOBALS['TYPO3_CONF_VARS']['SYS']['UTF8filesystem']) {
// allow ".", "-", 0-9, a-z, A-Z and everything beyond U+C0 (latin capital letter a with grave)
$cleanFileName = preg_replace('/[' . self::UNSAFE_FILENAME_CHARACTER_EXPRESSION . ']/u', '_', trim($fileName)) ?? '';
} else {
$fileName = GeneralUtility::makeInstance(CharsetConverter::class)->utf8_char_mapping($fileName);
// Replace unwanted characters by underscores
$cleanFileName = preg_replace('/[' . self::UNSAFE_FILENAME_CHARACTER_EXPRESSION . '\\xC0-\\xFF]/', '_', trim($fileName)) ?? '';
}
// Strip trailing dots and return
return rtrim($cleanFileName, '.');
}
}
File diff suppressed because it is too large Load Diff
+220
View File
@@ -0,0 +1,220 @@
<?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\File;
use Symfony\Component\Filesystem\Exception\IOException;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Utility\CommandUtility;
use TYPO3\CMS\Core\Utility\PathUtility;
/**
* Most of this code is thankfully taken from \Composer\Util\Filesystem
*
* @internal Only to be used in TYPO3\CMS\Core\SystemResource namespace
*/
readonly class FileSystem
{
/**
* Returns the shortest path from $from to $to
*
* @param bool $directories If true, the source/target are considered to be directories
* @throws \InvalidArgumentException
*/
public function findShortestPath(string $from, string $to, bool $directories = false): string
{
if (!PathUtility::isAbsolutePath($from) || !PathUtility::isAbsolutePath($to)) {
throw new \InvalidArgumentException(sprintf('$from (%s) and $to (%s) must be absolute paths.', $from, $to), 1765283155);
}
$from = PathUtility::getCanonicalPath($from);
$to = PathUtility::getCanonicalPath($to);
if ($directories) {
$from = rtrim($from, '/') . '/dummy_file';
}
if (dirname($from) === dirname($to)) {
return './' . basename($to);
}
$commonPath = $to;
while (!str_starts_with($from . '/', $commonPath . '/') && $commonPath !== '/' && preg_match('{^[A-Z]:/?$}i', $commonPath) === 0) {
$commonPath = str_replace('\\', '/', dirname($commonPath));
}
// no commonality at all
if (!str_starts_with($from, $commonPath)) {
return $to;
}
$commonPath = rtrim($commonPath, '/') . '/';
$sourcePathDepth = substr_count((string)substr($from, strlen($commonPath)), '/');
$commonPathCode = str_repeat('../', $sourcePathDepth);
$result = $commonPathCode . substr($to, strlen($commonPath));
if ($result === '') {
return './';
}
return $result;
}
/**
* Creates a relative symlink from $link to $target
*
* @param string $target The path of the binary file to be symlinked
* @param string $link The path where the symlink should be created
*/
public function relativeSymlink(string $target, string $link): bool
{
if (!function_exists('symlink')) {
return false;
}
$cwd = $this->getCwd();
$relativePath = $this->findShortestPath($link, $target);
chdir(dirname($link));
$result = @symlink($relativePath, $link);
chdir($cwd);
return $result;
}
/**
* Return true if that directory is a symlink.
*/
public function isSymlinkedDirectory(string $directory): bool
{
if (!is_dir($directory)) {
return false;
}
$resolved = $this->resolveSymlinkedDirectorySymlink($directory);
return is_link($resolved);
}
/**
* Return true if that file is a symlink.
*/
public function isSymlinkedFile(string $file): bool
{
if (!is_file($file)) {
return false;
}
return is_link($file);
}
/**
* Creates an NTFS junction.
*/
public function junction(string $target, string $junction): void
{
if (!Environment::isWindows()) {
throw new \LogicException(sprintf('Function %s is not available on non-Windows platform', __CLASS__), 1765283168);
}
if (!is_dir($target)) {
throw new IOException(sprintf('Cannot junction to "%s" as it is not a directory.', $target), 1765283131, null, $target);
}
// Removing any previously junction to ensure clean execution.
if (!is_dir($junction) || $this->isJunction($junction)) {
@rmdir($junction);
}
$commandLine = [
'mklink',
'/J',
];
$commandLine[] = str_replace('/', DIRECTORY_SEPARATOR, $junction);
$commandLine[] = realpath($target);
CommandUtility::exec($commandLine);
if (CommandUtility::exec($commandLine) === false) {
throw new IOException(sprintf('Failed to create junction to "%s" at "%s".', $target, $junction), 1763664408, null, $target);
}
clearstatcache(true, $junction);
}
/**
* Returns whether the target directory is a Windows NTFS Junction.
*
* We test if the path is a directory and not an ordinary link, then check
* that the mode value returned from lstat (which gives the status of the
* link itself) is not a directory, by replicating the POSIX S_ISDIR test.
*
* @param string $junction Path to check.
*/
public function isJunction(string $junction): bool
{
if (!Environment::isWindows()) {
return false;
}
// Important to clear all caches first
clearstatcache(true, $junction);
if (!is_dir($junction) || is_link($junction)) {
return false;
}
$stat = lstat($junction);
// S_ISDIR test (S_IFDIR is 0x4000, S_IFMT is 0xF000 bitmask)
return is_array($stat) && ($stat['mode'] & 0xF000) !== 0x4000;
}
/**
* Resolve pathname to symbolic link of a directory
*
* @param string $pathname Directory path to resolve
*/
private function resolveSymlinkedDirectorySymlink(string $pathname): string
{
if (!is_dir($pathname)) {
return $pathname;
}
$resolved = rtrim($pathname, '/');
if ($resolved === '') {
return $pathname;
}
return $resolved;
}
/**
* getcwd() equivalent which always returns a string
*
* @throws \RuntimeException
*/
private function getCwd(): string
{
$cwd = getcwd();
// fallback to realpath('') just in case this works but odds are it would break as well if we are in a case where getcwd fails
if ($cwd === false) {
$cwd = realpath('');
}
// crappy state, assume '' and hopefully relative paths allow things to continue
if ($cwd === false) {
throw new \RuntimeException('Could not determine the current working directory', 1765283181);
}
return $cwd;
}
}
File diff suppressed because it is too large Load Diff
+145
View File
@@ -0,0 +1,145 @@
<?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;
/**
* HTTP Utility class
*/
class HttpUtility
{
// HTTP Headers, see https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
// INFORMATIONAL CODES
public const HTTP_STATUS_100 = 'HTTP/1.1 100 Continue';
public const HTTP_STATUS_101 = 'HTTP/1.1 101 Switching Protocols';
public const HTTP_STATUS_102 = 'HTTP/1.1 102 Processing';
public const HTTP_STATUS_103 = 'HTTP/1.1 103 Early Hints';
// SUCCESS CODES
public const HTTP_STATUS_200 = 'HTTP/1.1 200 OK';
public const HTTP_STATUS_201 = 'HTTP/1.1 201 Created';
public const HTTP_STATUS_202 = 'HTTP/1.1 202 Accepted';
public const HTTP_STATUS_203 = 'HTTP/1.1 203 Non-Authoritative Information';
public const HTTP_STATUS_204 = 'HTTP/1.1 204 No Content';
public const HTTP_STATUS_205 = 'HTTP/1.1 205 Reset Content';
public const HTTP_STATUS_206 = 'HTTP/1.1 206 Partial Content';
public const HTTP_STATUS_207 = 'HTTP/1.1 207 Multi-status';
public const HTTP_STATUS_208 = 'HTTP/1.1 208 Already Reported';
public const HTTP_STATUS_226 = 'HTTP/1.1 226 IM Used';
// REDIRECTION CODES
public const HTTP_STATUS_300 = 'HTTP/1.1 300 Multiple Choices';
public const HTTP_STATUS_301 = 'HTTP/1.1 301 Moved Permanently';
public const HTTP_STATUS_302 = 'HTTP/1.1 302 Found';
public const HTTP_STATUS_303 = 'HTTP/1.1 303 See Other';
public const HTTP_STATUS_304 = 'HTTP/1.1 304 Not Modified';
public const HTTP_STATUS_305 = 'HTTP/1.1 305 Use Proxy';
public const HTTP_STATUS_306 = 'HTTP/1.1 306 Switch Proxy'; // Deprecated
public const HTTP_STATUS_307 = 'HTTP/1.1 307 Temporary Redirect';
public const HTTP_STATUS_308 = 'HTTP/1.1 308 Permanent Redirect';
// CLIENT ERROR
public const HTTP_STATUS_400 = 'HTTP/1.1 400 Bad Request';
public const HTTP_STATUS_401 = 'HTTP/1.1 401 Unauthorized';
public const HTTP_STATUS_402 = 'HTTP/1.1 402 Payment Required';
public const HTTP_STATUS_403 = 'HTTP/1.1 403 Forbidden';
public const HTTP_STATUS_404 = 'HTTP/1.1 404 Not Found';
public const HTTP_STATUS_405 = 'HTTP/1.1 405 Method Not Allowed';
public const HTTP_STATUS_406 = 'HTTP/1.1 406 Not Acceptable';
public const HTTP_STATUS_407 = 'HTTP/1.1 407 Proxy Authentication Required';
public const HTTP_STATUS_408 = 'HTTP/1.1 408 Request Timeout';
public const HTTP_STATUS_409 = 'HTTP/1.1 409 Conflict';
public const HTTP_STATUS_410 = 'HTTP/1.1 410 Gone';
public const HTTP_STATUS_411 = 'HTTP/1.1 411 Length Required';
public const HTTP_STATUS_412 = 'HTTP/1.1 412 Precondition Failed';
public const HTTP_STATUS_413 = 'HTTP/1.1 413 Request Entity Too Large';
public const HTTP_STATUS_414 = 'HTTP/1.1 414 URI Too Long';
public const HTTP_STATUS_415 = 'HTTP/1.1 415 Unsupported Media Type';
public const HTTP_STATUS_416 = 'HTTP/1.1 416 Requested range not satisfiable';
public const HTTP_STATUS_417 = 'HTTP/1.1 417 Expectation Failed';
public const HTTP_STATUS_418 = 'HTTP/1.1 418 I\'m a teapot';
public const HTTP_STATUS_422 = 'HTTP/1.1 422 Unprocessable Entity';
public const HTTP_STATUS_423 = 'HTTP/1.1 423 Locked';
public const HTTP_STATUS_424 = 'HTTP/1.1 424 Failed Dependency';
public const HTTP_STATUS_425 = 'HTTP/1.1 425 Unordered Collection';
public const HTTP_STATUS_426 = 'HTTP/1.1 426 Upgrade Required';
public const HTTP_STATUS_428 = 'HTTP/1.1 428 Precondition Required';
public const HTTP_STATUS_429 = 'HTTP/1.1 429 Too Many Requests';
public const HTTP_STATUS_431 = 'HTTP/1.1 431 Request Header Fields Too Large';
public const HTTP_STATUS_451 = 'HTTP/1.1 451 Unavailable For Legal Reasons';
// SERVER ERROR
public const HTTP_STATUS_500 = 'HTTP/1.1 500 Internal Server Error';
public const HTTP_STATUS_501 = 'HTTP/1.1 501 Not Implemented';
public const HTTP_STATUS_502 = 'HTTP/1.1 502 Bad Gateway';
public const HTTP_STATUS_503 = 'HTTP/1.1 503 Service Unavailable';
public const HTTP_STATUS_504 = 'HTTP/1.1 504 Gateway Time-out';
public const HTTP_STATUS_505 = 'HTTP/1.1 505 Version not Supported';
public const HTTP_STATUS_506 = 'HTTP/1.1 506 Variant Also Negotiates';
public const HTTP_STATUS_507 = 'HTTP/1.1 507 Insufficient Storage';
public const HTTP_STATUS_508 = 'HTTP/1.1 508 Loop Detected';
public const HTTP_STATUS_509 = 'HTTP/1.1 509 Bandwidth Limit Exceeded';
public const HTTP_STATUS_511 = 'HTTP/1.1 511 Network Authentication Required';
// URL Schemes
public const SCHEME_HTTP = 1;
public const SCHEME_HTTPS = 2;
/**
* Builds a URL string from an array with the URL parts, as e.g. output by parse_url().
*
* @see http://www.php.net/parse_url
*/
public static function buildUrl(array $urlParts): string
{
return (isset($urlParts['scheme']) ? $urlParts['scheme'] . '://' : '')
. (isset($urlParts['user']) ? $urlParts['user']
. (isset($urlParts['pass']) ? ':' . $urlParts['pass'] : '') . '@' : '')
. ($urlParts['host'] ?? '')
. (isset($urlParts['port']) ? ':' . $urlParts['port'] : '')
. ($urlParts['path'] ?? '')
. (isset($urlParts['query']) ? '?' . $urlParts['query'] : '')
. (isset($urlParts['fragment']) ? '#' . $urlParts['fragment'] : '');
}
/**
* Implodes a multidimensional array of query parameters to a string of GET parameters (eg. param[key][key2]=value2&param[key][key3]=value3)
* and properly encodes parameter names as well as values. Spaces are encoded as %20
*
* @param array $parameters The (multidimensional) array of query parameters with values
* @param string $prependCharacter If the created query string is not empty, prepend this character "?" or "&" else no prepend
* @param bool $skipEmptyParameters If true, empty parameters (blank string, empty array, null) are removed.
* @return string Imploded result, for example param[key][key2]=value2&param[key][key3]=value3
* @see explodeUrl2Array()
*/
public static function buildQueryString(array $parameters, string $prependCharacter = '', bool $skipEmptyParameters = false): string
{
if (empty($parameters)) {
return '';
}
if ($skipEmptyParameters) {
// This callback filters empty strings, array and null but keeps zero integers
$parameters = ArrayUtility::filterRecursive(
$parameters,
static function ($item) {
return $item !== '' && $item !== [] && $item !== null;
}
);
}
$queryString = http_build_query($parameters, '', '&', PHP_QUERY_RFC3986);
$prependCharacter = $prependCharacter === '?' || $prependCharacter === '&' ? $prependCharacter : '';
return $queryString && $prependCharacter ? $prependCharacter . $queryString : $queryString;
}
}
@@ -0,0 +1,94 @@
<?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;
/**
* Anonymize a given IP
*
* Inspired by https://github.com/geertw/php-ip-anonymizer
*/
class IpAnonymizationUtility
{
/**
* IPv4 netmask used to anonymize IPv4 address.
*
* 1) Mask host
* 2) Mask host and subnet
*
* @var array<int, string>
*/
public const MASKV4 = [
1 => '255.255.255.0',
2 => '255.255.0.0',
];
/**
* IPv6 netmask used to anonymize IPv6 address.
*
* 1) Mask Interface ID
* 2) Mask Interface ID and SLA ID
*
* @var array<int, string>
*/
public const MASKV6 = [
1 => 'ffff:ffff:ffff:ffff:0000:0000:0000:0000',
2 => 'ffff:ffff:ffff:0000:0000:0000:0000:0000',
];
/**
* Anonymize given IP
*
* @param string $address IP address
* @param int $mask Allowed values are 0 (masking disabled), 1 (mask host), 2 (mask host and subnet)
* @throws \UnexpectedValueException
*/
public static function anonymizeIp(string $address, ?int $mask = null): string
{
if ($mask === null) {
$mask = (int)$GLOBALS['TYPO3_CONF_VARS']['SYS']['ipAnonymization'];
}
if ($mask < 0 || $mask > 2) {
throw new \UnexpectedValueException(sprintf('The provided value "%d" is not an allowed value for the IP mask.', $mask), 1519739203);
}
if ($mask === 0) {
return $address;
}
if (empty($address)) {
return '';
}
$packedAddress = @inet_pton($address);
if ($packedAddress === false) {
return '';
}
$length = strlen($packedAddress);
if ($length === 4) {
$bitMask = self::MASKV4[$mask];
} elseif ($length === 16) {
$bitMask = self::MASKV6[$mask];
} else {
return '';
}
$packedBitMask = inet_pton($bitMask);
if ($packedBitMask === false) {
return '';
}
return inet_ntop($packedAddress & $packedBitMask);
}
}
+359
View File
@@ -0,0 +1,359 @@
<?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 Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Exception\ExceptionInterface;
/**
* Class to handle mail specific functionality
*/
class MailUtility
{
/**
* Gets a valid "from" for mail messages (email and name).
*
* Ready to be passed to $mail->setFrom()
*
* This method can return three different variants:
* 1. An assoc. array: key => Valid email address which can be used as sender; value => Valid name which can be used as a sender
* 2. A numeric array with one entry: Valid email address which can be used as sender
* 3. Null, if no address is configured
*
* @return array<string|int, string>|null
*/
public static function getSystemFrom(): ?array
{
$address = self::getSystemFromAddress();
$name = self::getSystemFromName();
if (!$address) {
return null;
}
if ($name) {
return [$address => $name];
}
return [$address];
}
/**
* Creates a valid "from" name for mail messages.
*
* As configured in Install Tool.
*
* @return string|null The name (unquoted, unformatted). NULL if none is set or an invalid non-string value.
*/
public static function getSystemFromName(): ?string
{
$name = $GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailFromName'] ?? null;
return (!empty($name) && is_string($name)) ? $name : null;
}
/**
* Creates a valid email address for the sender of mail messages.
*
* Uses a fallback chain:
* $TYPO3_CONF_VARS['MAIL']['defaultMailFromAddress'] ->
* no-reply@FirstDomainRecordFound ->
* no-reply@php_uname('n') ->
* no-reply@example.com
*
* Ready to be passed to $mail->setFrom()
*
* @return string An email address
*/
public static function getSystemFromAddress(): string
{
$address = $GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailFromAddress'] ?? null;
if (!is_string($address) || !GeneralUtility::validEmail($address)) {
// still nothing, get host name from server
$address = 'no-reply@' . php_uname('n');
if (!GeneralUtility::validEmail($address)) {
// if everything fails use a dummy address
$address = 'no-reply@example.com';
}
}
return $address;
}
/**
* Gets a default "reply-to" for mail messages (email and name).
*
* Ready to be passed to $mail->setReplyTo()
*
* This method returns a list of email addresses, but depending on the existence of "defaultMailReplyToName"
* the array can have a different shape:
*
* 1. An assoc. array: key => a valid reply-to address which can be used as sender; value => a valid reply-to name which can be used as a sender
* 2. A numeric array with one entry: a valid reply-to address which can be used as sender
*
* @return array<string|int, string>
*/
public static function getSystemReplyTo(): array
{
$mailConfiguration = $GLOBALS['TYPO3_CONF_VARS']['MAIL'] ?? [];
$replyToAddress = $mailConfiguration['defaultMailReplyToAddress'] ?? null;
if (empty($replyToAddress) || !GeneralUtility::validEmail($replyToAddress)) {
return [];
}
if (!empty($mailConfiguration['defaultMailReplyToName'])) {
$replyTo = [$replyToAddress => $mailConfiguration['defaultMailReplyToName']];
} else {
$replyTo = [$replyToAddress];
}
return $replyTo;
}
/**
* Breaks up a single line of text for emails
* Words - longer than $lineWidth - will not be split into parts
*
* @param string $str The string to break up
* @param string $newlineChar The string to implode the broken lines with (default/typically \n)
* @param int $lineWidth The line width
* @return string Reformatted text
*/
public static function breakLinesForEmail(string $str, string $newlineChar = LF, int $lineWidth = 76): string
{
$lines = [];
$substrStart = 0;
while (strlen($str) > $substrStart) {
$substr = substr($str, $substrStart, $lineWidth);
// has line exceeded (reached) the maximum width?
if (strlen($substr) === $lineWidth) {
// find last space-char
$spacePos = strrpos(rtrim($substr), ' ');
// space-char found?
if ($spacePos !== false) {
// take everything up to last space-char
$theLine = substr($substr, 0, $spacePos);
$substrStart++;
} else {
// search for space-char in remaining text
// makes this line longer than $lineWidth!
$afterParts = explode(' ', substr($str, $lineWidth + $substrStart), 2);
$theLine = $substr . $afterParts[0];
}
if ($theLine === '') {
// prevent endless loop because of empty line
break;
}
} else {
$theLine = $substr;
}
$lines[] = trim($theLine);
$substrStart += strlen($theLine);
if (trim(substr($str, $substrStart, $lineWidth)) === '') {
// no more text
break;
}
}
return implode($newlineChar, $lines);
}
/**
* Parses mailbox headers and turns them into an array.
*
* Mailbox headers are a comma separated list of 'name <email@example.org>' combinations
* or plain email addresses (or a mix of these).
* The resulting array has key-value pairs where the key is either a number
* (no display name in the mailbox header) and the value is the email address,
* or the key is the email address and the value is the display name.
*
* Groups (RFC 5322 section 3.4) are flattened to their members and their display
* name is discarded, comments are removed, and invalid addresses are silently skipped.
*
* @param string $rawAddresses Comma separated list of email addresses (optionally with display name)
* @return array Parsed list of addresses.
*/
public static function parseAddresses(string $rawAddresses): array
{
$addressList = [];
foreach (self::splitAddressList($rawAddresses) as $rawMailbox) {
$address = self::parseMailbox($rawMailbox);
if ($address === null) {
continue;
}
if ($address->getName() !== '') {
// item with name found ( name <email@example.org> )
$addressList[$address->getAddress()] = $address->getName();
} else {
// item without name found ( email@example.org )
$addressList[] = $address->getAddress();
}
}
return $addressList;
}
/**
* Splits a raw address-list header value into its individual mailboxes, while
* honoring quoted strings ( "last, first" <email@example.org> ), comments
* (which are removed), domain literals ( user@[IPv6:2001:db8::1] ) and
* angle-addr parts. Group members are flattened into the list, the display
* name of a group is discarded.
*
* @return string[]
*/
private static function splitAddressList(string $rawAddresses): array
{
$mailboxes = [];
$buffer = '';
$inQuotes = false;
$inAngleAddr = false;
$inDomainLiteral = false;
$commentDepth = 0;
$length = strlen($rawAddresses);
for ($i = 0; $i < $length; $i++) {
$char = $rawAddresses[$i];
if ($commentDepth > 0) {
if ($char === '\\') {
$i++;
} elseif ($char === '(') {
$commentDepth++;
} elseif ($char === ')') {
$commentDepth--;
if ($commentDepth === 0) {
// a comment is equivalent to folding white space (RFC 5322, section 3.2.2)
$buffer .= ' ';
}
}
continue;
}
if ($inQuotes) {
if ($char === '\\' && $i + 1 < $length) {
$buffer .= $char . $rawAddresses[++$i];
continue;
}
if ($char === '"') {
$inQuotes = false;
}
$buffer .= $char;
continue;
}
switch ($char) {
case '"':
$inQuotes = true;
$buffer .= $char;
break;
case '(':
$commentDepth++;
break;
case '[':
case ']':
$inDomainLiteral = $char === '[';
$buffer .= $char;
break;
case '<':
case '>':
$inAngleAddr = $char === '<';
$buffer .= $char;
break;
case ',':
case ';':
if ($inAngleAddr || $inDomainLiteral) {
$buffer .= $char;
break;
}
$mailboxes[] = $buffer;
$buffer = '';
break;
case ':':
if ($inAngleAddr || $inDomainLiteral) {
$buffer .= $char;
break;
}
// a colon ends the display name of a group ( groupname: member@example.org; )
$buffer = '';
break;
default:
$buffer .= $char;
}
}
$mailboxes[] = $buffer;
return $mailboxes;
}
private static function parseMailbox(string $rawMailbox): ?Address
{
$rawMailbox = trim($rawMailbox);
// NUL is invalid anywhere, even in the obsolete syntax (RFC 5322, section 4.1)
if ($rawMailbox === '' || str_contains($rawMailbox, "\0")) {
return null;
}
$displayName = '';
$addrSpec = $rawMailbox;
$angleStart = self::findAngleAddrStart($rawMailbox);
if ($angleStart !== null) {
$angleEnd = strrpos($rawMailbox, '>');
if ($angleEnd === false || $angleEnd < $angleStart) {
return null;
}
$displayName = self::normalizeDisplayName(substr($rawMailbox, 0, $angleStart));
$addrSpec = substr($rawMailbox, $angleStart + 1, $angleEnd - $angleStart - 1);
if (str_starts_with($addrSpec, '@')) {
// an obsolete route ( <@relay.example.org:user@example.org> ) is ignored (RFC 5322, section 4.4)
$routeEnd = strpos($addrSpec, ':');
if ($routeEnd !== false) {
$addrSpec = substr($addrSpec, $routeEnd + 1);
}
}
}
try {
return new Address($addrSpec, $displayName);
} catch (ExceptionInterface) {
return null;
}
}
/**
* Finds the position of the '<' starting an angle-addr, ignoring any '<'
* inside a quoted display name ( "Contact <va@example.org>" <real@example.org> ).
*/
private static function findAngleAddrStart(string $rawMailbox): ?int
{
$inQuotes = false;
$length = strlen($rawMailbox);
for ($i = 0; $i < $length; $i++) {
$char = $rawMailbox[$i];
if ($inQuotes && $char === '\\') {
$i++;
} elseif ($char === '"') {
$inQuotes = !$inQuotes;
} elseif ($char === '<' && !$inQuotes) {
return $i;
}
}
return null;
}
/**
* Resolves a quoted display name ( "last, first" ) and contained
* quoted-pairs ( \" ) to the plain text it represents.
*/
private static function normalizeDisplayName(string $displayName): string
{
$displayName = trim($displayName);
if (strlen($displayName) > 1 && str_starts_with($displayName, '"') && str_ends_with($displayName, '"')) {
$displayName = stripslashes(substr($displayName, 1, -1));
}
return $displayName;
}
}
+217
View File
@@ -0,0 +1,217 @@
<?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;
/**
* Class with helper functions for mathematical calculations
*/
class MathUtility
{
/**
* Forces the integer $theInt into the boundaries of $min and $max. If the $theInt is FALSE then the $defaultValue is applied.
*
* @param mixed $theInt Input value - will be cast to int if non-integer value is passed.
* @param int $min Lower limit
* @param int $max Higher limit
* @param int $defaultValue Default value if input is FALSE.
* @return int The input value forced into the boundaries of $min and $max
*/
public static function forceIntegerInRange(mixed $theInt, int $min, int $max = 2000000000, int $defaultValue = 0): int
{
// Returns $theInt as an integer in the integerspace from $min to $max
$theInt = (int)$theInt;
// If the input value is zero after being converted to integer,
// defaultValue may set another default value for it.
if ($defaultValue && !$theInt) {
$theInt = $defaultValue;
}
if ($theInt < $min) {
$theInt = $min;
}
if ($theInt > $max) {
$theInt = $max;
}
return $theInt;
}
/**
* Tests if the input can be interpreted as integer.
*
* Note: "0" will return true while any other number with a leading 0 (including multiple zeroes) will be false.
*
* Note: Integer casting from objects or arrays is considered undefined and thus will return false.
*
* @see https://php.net/manual/en/language.types.integer.php#language.types.integer.casting.from-other
* @param mixed $var Any input variable to test
* @return bool Returns TRUE if string is an integer
*/
public static function canBeInterpretedAsInteger(mixed $var): bool
{
return match (gettype($var)) {
'integer' => true,
// Due to historical reasons `TRUE` is correctly interpreted as integer
// but `FALSE` not even if a (int) cast would return `0` and keeping it
// we can simply return the boolean value to have the same behaviour and
// still avoiding type casting chain.
'boolean' => $var,
// We use a type casting chain here to ensure that value is the same after
// casting and eliminated invalid stuff from it. The `@` silence operator
// can look weird here but is required to avoid enforced casting issues
// with PHP 8.5.0 and newer.
'string' => (string)@(int)$var === $var,
// We use a type casting chain here to ensure that value is the same after
// casting and eliminated invalid stuff from it. The `@` silence operator
// can look weird here but is required to avoid enforced casting issues
// with PHP 8.5.0 and newer.
// gettype() returns `double` for `float values`
'double' => !is_nan($var) && (string)@(int)$var === (string)$var,
// non-scalar like array, object, resource, NULL or unknown_type
default => false,
};
}
/**
* Tests if the input can be interpreted as float.
*
* Note: Float casting from objects or arrays is considered undefined and thus will return false.
*
* @see http://www.php.net/manual/en/language.types.float.php, section "Formally" for the notation
* @param mixed $var Any input variable to test
* @return bool Returns TRUE if string is a float
*/
public static function canBeInterpretedAsFloat(mixed $var): bool
{
$pattern_lnum = '[0-9]+';
$pattern_dnum = '([0-9]*[\.]' . $pattern_lnum . ')|(' . $pattern_lnum . '[\.][0-9]*)';
$pattern_exp_dnum = '[+-]?((' . $pattern_lnum . '|' . $pattern_dnum . ')([eE][+-]?' . $pattern_lnum . ')?)';
if ($var === '' || is_object($var) || is_array($var)) {
return false;
}
$matches = preg_match('/^' . $pattern_exp_dnum . '$/', (string)$var);
return $matches === 1;
}
/**
* Calculates the input by +,-,*,/,%,^ with priority to + and -
*
* @param string $string Input string, eg "123 + 456 / 789 - 4
* @return float|string Calculated value. Or error string.
* @see \TYPO3\CMS\Core\Utility\MathUtility::calculateWithParentheses()
*/
public static function calculateWithPriorityToAdditionAndSubtraction(string $string): float|string
{
// Removing all whitespace
$string = preg_replace('/[[:space:]]*/', '', $string);
// Ensuring an operator for the first entrance
$string = '+' . $string;
$qm = '\\*\\/\\+-^%';
$regex = '([' . $qm . '])([' . $qm . ']?[0-9\\.]*)';
// Split the expression here:
$reg = [];
preg_match_all('/' . $regex . '/', $string, $reg);
reset($reg[2]);
$number = 0;
$Msign = '+';
$err = '';
$buffer = (float)current($reg[2]);
// Advance pointer
$regSliced = array_slice($reg[2], 1, null, true);
foreach ($regSliced as $k => $v) {
$v = (float)$v;
$sign = $reg[1][$k];
if ($sign === '+' || $sign === '-') {
$Msign === '-' ? ($number -= $buffer) : ($number += $buffer);
$Msign = $sign;
$buffer = $v;
} else {
if ($sign === '/') {
if ($v) {
$buffer /= $v;
} else {
$err = 'dividing by zero';
}
}
if ($sign === '%') {
if ($v) {
$buffer %= $v;
} else {
$err = 'dividing by zero';
}
}
if ($sign === '*') {
$buffer *= $v;
}
if ($sign === '^') {
$buffer = $buffer ** $v;
}
}
}
$number = $Msign === '-' ? ($number - $buffer) : ($number + $buffer);
return $err ? 'ERROR: ' . $err : $number;
}
/**
* Calculates the input with parenthesis levels
*
* @param string $string Input string, eg "(123 + 456) / 789 - 4
* @return string Calculated value. Or error string.
* @see calculateWithPriorityToAdditionAndSubtraction()
* @see \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::stdWrap()
*/
public static function calculateWithParentheses(string $string): string
{
$securC = 100;
do {
$valueLenO = strcspn($string, '(');
$valueLenC = strcspn($string, ')');
if ($valueLenC == strlen($string) || $valueLenC < $valueLenO) {
$value = self::calculateWithPriorityToAdditionAndSubtraction(substr($string, 0, $valueLenC));
$string = $value . substr($string, $valueLenC + 1);
return $string;
}
$string = substr($string, 0, $valueLenO) . self::calculateWithParentheses(substr($string, $valueLenO + 1));
// Security:
$securC--;
if ($securC <= 0) {
break;
}
} while ($valueLenO < strlen($string));
return $string;
}
/**
* Checks whether the given number $value is an integer in the range [$minimum;$maximum]
*
* @param mixed $value Integer value to check. If not an integer this method always returns false.
* @param int $minimum Lower boundary of the range
* @param int $maximum Upper boundary of the range
*/
public static function isIntegerInRange(mixed $value, int $minimum, int $maximum): bool
{
$value = filter_var($value, FILTER_VALIDATE_INT, [
'options' => [
'min_range' => $minimum,
'max_range' => $maximum,
],
]);
return is_int($value);
}
}
+433
View File
@@ -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;
}
}
+102
View File
@@ -0,0 +1,102 @@
<?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;
/**
* Class with helper functions for permuting items.
*/
class PermutationUtility
{
/**
* Combines string items of multiple arrays as cross-product into flat items.
*
* Example:
* + meltStringItems([['a', 'b'], ['c', 'd'], ['e', 'f']])
* + results into ['ace', 'acf', 'ade', 'adf', 'bce', 'bcf', 'bde', 'bdf']
*
* @param array[] $payload Distinct array that should be melted
* @param string $previousResult Previous item results
* @return string[]
*/
public static function meltStringItems(array $payload, string $previousResult = ''): array
{
$results = [];
$items = static::nextItems($payload);
foreach ($items as $item) {
if (!(is_string($item) || $item instanceof \Stringable)) {
throw new \LogicException(
sprintf('Expected string, got %s', gettype($item)),
1578164102
);
}
$resultItem = $previousResult . $item;
if (!empty($payload)) {
$results = array_merge(
$results,
static::meltStringItems($payload, $resultItem)
);
continue;
}
$results[] = $resultItem;
}
return $results;
}
/**
* Combines arbitrary items of multiple arrays as cross-product into flat items.
*
* Example:
* + meltArrayItems(['a','b'], ['c','e'], ['f','g'])
* + results into ['a', 'c', 'e'], ['a', 'c', 'f'], ['a', 'd', 'e'], ['a', 'd', 'f'],
* ['b', 'c', 'e'], ['b', 'c', 'f'], ['b', 'd', 'e'], ['b', 'd', 'f'],
*
* @param array[] $payload Distinct items that should be melted
* @param array $previousResult Previous item results
* @return array[]
*/
public static function meltArrayItems(array $payload, array $previousResult = []): array
{
$results = [];
$items = static::nextItems($payload);
foreach ($items as $item) {
$resultItems = $previousResult;
$resultItems[] = $item;
if (!empty($payload)) {
$results = array_merge(
$results,
static::meltArrayItems($payload, $resultItems)
);
continue;
}
$results[] = $resultItems;
}
return $results;
}
protected static function nextItems(array &$payload): iterable
{
$items = array_shift($payload);
if (is_iterable($items)) {
return $items;
}
throw new \LogicException(
sprintf('Expected iterable, got %s', gettype($items)),
1578164101
);
}
}
File diff suppressed because it is too large Load Diff
+53
View File
@@ -0,0 +1,53 @@
<?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\String;
/**
* @internal
*/
class StringFragment implements \Stringable
{
public readonly int $length;
public readonly string $ident;
public static function raw(string $value): self
{
return new self($value, StringFragmentSplitter::TYPE_RAW);
}
public static function expression(string $value): self
{
return new self($value, StringFragmentSplitter::TYPE_EXPRESSION);
}
public function __construct(
public readonly string $value,
public readonly string $type
) {
if ($this->value === '') {
throw new \LogicException('Value must not be empty', 1651671582);
}
$this->length = strlen($this->value);
$this->ident = md5($this->type . '::' . ($this->value));
}
public function __toString(): string
{
return $this->value;
}
}
@@ -0,0 +1,120 @@
<?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\String;
/**
* @internal
*/
class StringFragmentCollection implements \Stringable, \Countable
{
/**
* @var list<StringFragment>
*/
protected array $fragments;
/**
* Length of all fragment strings
*/
protected int $length = 0;
public function __construct(StringFragment ...$fragments)
{
$lengths = array_map(static fn(StringFragment $fragment) => $fragment->length, $fragments);
$this->length = array_sum($lengths);
$this->fragments = $fragments;
}
public function __toString(): string
{
return implode('', array_map('strval', $this->fragments));
}
public function count(): int
{
return count($this->fragments);
}
public function with(StringFragment ...$fragments): self
{
$target = clone $this;
foreach ($fragments as $fragment) {
$target->length += $fragment->length;
$target->fragments[] = $fragment;
}
return $target;
}
public function withOnlyType(string $type): self
{
$fragments = array_filter(
$this->fragments,
static fn(StringFragment $item) => $item->type === $type
);
return new self(...$fragments);
}
public function withoutType(string $type): self
{
$fragments = array_filter(
$this->fragments,
static fn(StringFragment $item) => $item->type !== $type
);
return new self(...$fragments);
}
/**
* @return list<StringFragment>
*/
public function getFragments(): array
{
return $this->fragments;
}
public function getLength(): int
{
return $this->length;
}
public function diff(self $other): self
{
$otherFragmentIdents = $other->getFragmentIdents();
$differentFragments = array_filter(
$this->fragments,
static fn(StringFragment $item) => !in_array($item->ident, $otherFragmentIdents, true)
);
return new self(...$differentFragments);
}
public function intersect(self $other): self
{
$otherFragmentIdents = $other->getFragmentIdents();
$sameFragments = array_filter(
$this->fragments,
static fn(StringFragment $item) => in_array($item->ident, $otherFragmentIdents, true)
);
return new self(...$sameFragments);
}
/**
* @return list<string>
*/
protected function getFragmentIdents(): array
{
return array_map(static fn(StringFragment $item) => $item->ident, $this->fragments);
}
}
@@ -0,0 +1,41 @@
<?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\String;
/**
* @internal
*/
readonly class StringFragmentPattern
{
public function __construct(
public string $type,
public string $pattern
) {}
/**
* Compiles this string fragment pattern to a scoped PCRE pattern string.
*/
public function compilePattern(): string
{
return sprintf(
'(?P<%s>%s)',
$this->type . '_' . bin2hex(random_bytes(5)),
$this->pattern
);
}
}
@@ -0,0 +1,110 @@
<?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\String;
/**
* Splits a string into RAW and EXPRESSION fragments.
* EXPRESSION fragments are resolved by provided arbitrary regex pattern.
*
* @internal
*/
class StringFragmentSplitter
{
/**
* Raw string literals
*/
public const TYPE_RAW = 'raw';
/**
* Literals used as expression
*/
public const TYPE_EXPRESSION = 'expression';
/**
* Returns `null` in case there have not been any pattern matches,
* if omitted an array containing only `raw` fragments is returned
*/
public const FLAG_UNMATCHED_AS_NULL = 1;
/**
* @var list<StringFragmentPattern>
*/
protected readonly array $patterns;
public function __construct(StringFragmentPattern ...$patterns)
{
$this->patterns = $patterns;
}
/**
* @param string $value to be split into `raw` and `expression` fragments
* @param int $flags (optional) `FLAG_UNMATCHED_AS_NULL`
*/
public function split(string $value, int $flags = 0): ?StringFragmentCollection
{
$pattern = chr(1) . implode('|', $this->preparePatterns()) . chr(1);
$options = PREG_UNMATCHED_AS_NULL | PREG_OFFSET_CAPTURE | PREG_SET_ORDER;
if (!preg_match_all($pattern, $value, $matches, $options)) {
if (($flags & self::FLAG_UNMATCHED_AS_NULL) === self::FLAG_UNMATCHED_AS_NULL) {
return null;
}
return new StringFragmentCollection(StringFragment::raw($value));
}
$collection = new StringFragmentCollection();
foreach ($matches as $match) {
// filters string keys (e.g. `expression_a1b2c3d4e5`) from matches, skips numeric indexes
$types = array_filter(
array_keys($match),
static fn(int|string $type) => is_string($type) && $type !== ''
);
foreach ($types as $type) {
$matchOffset = $match[$type][1];
if ($matchOffset < 0) {
continue;
}
$matchValue = $match[$type][0];
// matches contain only pattern matches, but no raw string literals - by comparing the
// position of the collection with the current offset, missing raw literals are synchronized
if ($collection->getLength() < $matchOffset) {
$gapValue = substr($value, $collection->getLength(), $matchOffset - $collection->getLength());
$collection = $collection->with(StringFragment::raw($gapValue));
}
$collection = $collection->with(StringFragment::expression($matchValue));
}
}
// synchronize missing raw string literals
// (at the end of the given value after previous expression)
if ($collection->getLength() < strlen($value)) {
$gapValue = substr($value, $collection->getLength());
$collection = $collection->with(StringFragment::raw($gapValue));
}
return $collection;
}
/**
* @return list<string>
*/
protected function preparePatterns(): array
{
return array_map(
static fn(StringFragmentPattern $pattern) => $pattern->compilePattern(),
$this->patterns
);
}
}
+207
View File
@@ -0,0 +1,207 @@
<?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;
/**
* Class with helper functions for string handling
*/
class StringUtility
{
/**
* Casts applicable types (string, bool, finite numeric) to string.
*
* Any other type will be replaced by the `$default` value.
*/
public static function cast(mixed $value, ?string $default = null): ?string
{
if (is_string($value)) {
return $value;
}
if (is_bool($value) || (is_numeric($value) && is_finite($value))) {
return (string)$value;
}
return $default;
}
/**
* Keeps only string types (filters out non-strings).
*
* Any other non-string type will be replaced by the `$default` value.
*/
public static function filter(mixed $value, ?string $default = null): ?string
{
return is_string($value) ? $value : $default;
}
/**
* This function generates a unique id by using the more entropy parameter.
* Furthermore, the dots are removed so the id can be used inside HTML attributes e.g. id.
*
* @return non-empty-string
*/
public static function getUniqueId(string $prefix = ''): string
{
$uniqueId = uniqid($prefix, true);
return str_replace('.', '', $uniqueId);
}
/**
* Escape a CSS selector to be used for DOM queries
*
* This method takes care to escape any CSS selector meta character.
* The result may be used to query the DOM like $('#' + escapedSelector)
*/
public static function escapeCssSelector(string $selector): string
{
return preg_replace('/([#:.\\[\\],=@])/', '\\\\$1', $selector);
}
/**
* Removes the Byte Order Mark (BOM) from the input string.
*
* This method supports UTF-8 encoded strings only!
*/
public static function removeByteOrderMark(string $input): string
{
if (str_starts_with($input, "\xef\xbb\xbf")) {
$input = substr($input, 3);
}
return $input;
}
/**
* Matching two strings against each other, supporting a "*" wildcard (match many) or a "?" wildcard (match one= or (if wrapped in "/") PCRE regular expressions
*
* @param string $haystack The string in which to find $needle.
* @param string $needle The string to find in $haystack
* @return bool Returns TRUE if $needle matches or is found in (according to wildcards) $haystack. E.g. if $haystack is "Netscape 6.5" and $needle is "Net*" or "Net*ape" then it returns TRUE.
*/
public static function searchStringWildcard(string $haystack, string $needle): bool
{
$result = false;
if ($haystack === $needle) {
$result = true;
} elseif ($needle) {
if (preg_match('/^\\/.+\\/$/', $needle)) {
// Regular expression, only "//" is allowed as delimiter
$regex = $needle;
} else {
$needle = str_replace(['*', '?'], ['%%%MANY%%%', '%%%ONE%%%'], $needle);
$regex = '/^' . preg_quote($needle, '/') . '$/';
// Replace the marker with .* to match anything (wildcard)
$regex = str_replace(['%%%MANY%%%', '%%%ONE%%%'], ['.*', '.'], $regex);
}
$result = (bool)preg_match($regex, $haystack);
}
return $result;
}
/**
* Takes a comma-separated list and removes all duplicates.
* If a value in the list is trim(empty), the value is ignored.
*
* @param string $list A comma-separated list of values.
* @return string Returns the list without any duplicates of values, space around values are trimmed.
*/
public static function uniqueList(string $list): string
{
return implode(',', array_unique(GeneralUtility::trimExplode(',', $list, true)));
}
/**
* Works the same as str_pad() except that it correctly handles strings with multibyte characters
* and takes an additional optional argument $encoding.
*
* @deprecated since TYPO3 v15.0, will be removed in TYPO3 v16.0. Use the native PHP function mb_str_pad() instead.
*/
public static function multibyteStringPad(string $string, int $length, string $pad_string = ' ', int $pad_type = STR_PAD_RIGHT, string $encoding = 'UTF-8'): string
{
trigger_error(
'StringUtility::multibyteStringPad() will be removed in TYPO3 v16.0. Use the native PHP function mb_str_pad() instead.',
E_USER_DEPRECATED
);
// An empty pad string returns the input unchanged instead of throwing a ValueError like mb_str_pad() does.
if ($pad_string === '') {
return $string;
}
return mb_str_pad($string, $length, $pad_string, $pad_type, $encoding);
}
/**
* Returns base64 encoded value with a URL and filename safe alphabet
* according to https://tools.ietf.org/html/rfc4648#section-5
*
* The difference to classic base64 is, that the result
* alphabet is adjusted like shown below, padding (`=`)
* is stripped completely:
* + position #62: `+` -> `-` (minus)
* + position #63: `/` -> `_` (underscore)
*
* @param string $value raw value
* @return string base64url encoded string
*/
public static function base64urlEncode(string $value): string
{
return strtr(base64_encode($value), ['+' => '-', '/' => '_', '=' => '']);
}
/**
* Returns base64 decoded value with a URL and filename safe alphabet
* according to https://tools.ietf.org/html/rfc4648#section-5
*
* The difference to classic base64 is, that the result
* alphabet is adjusted like shown below, padding (`=`)
* is stripped completely:
* + position #62: `-` (minus) -> `+`
* + position #63: `_` (underscore) -> `/`
*
* @param string $value base64url decoded string
* @param bool $strict enforces to only allow characters contained in the base64(url) alphabet
* @return string|false raw value, or `false` if non-base64(url) characters were given in strict mode
*/
public static function base64urlDecode(string $value, bool $strict = false): string|false
{
return base64_decode(strtr($value, ['-' => '+', '_' => '/']), $strict);
}
/**
* Explodes a string while respecting escape characters
*
* e.g.: delimiter: '.'; escapeCharacter: '\'; subject: 'new\.site.child'
* result: [new.site, child]
* @param string $delimiter
* @param string $subject
* @param string $escapeCharacter
*/
public static function explodeEscaped(string $delimiter, string $subject, string $escapeCharacter = '\\'): array
{
if ($delimiter !== '') {
$placeholder = '\\0\\0\\0_esc';
$subjectEscaped = str_replace($escapeCharacter . $delimiter, $placeholder, $subject);
$escapeParts = explode($delimiter, $subjectEscaped);
foreach ($escapeParts as &$part) {
$part = str_replace($placeholder, $delimiter, $part);
}
return $escapeParts;
}
return [$subject];
}
}
+117
View File
@@ -0,0 +1,117 @@
<?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 TYPO3\CMS\Core\Information\Typo3Version;
/**
* Class with helper functions for version number handling
*/
class VersionNumberUtility
{
/**
* Returns an integer from a three part version number, eg '4.12.3' -> 4012003
*
* @param string $versionNumber Version number on format x.x.x
* @return int Integer version of version number (where each part can count to 999)
*/
public static function convertVersionNumberToInteger(string $versionNumber): int
{
$versionParts = explode('.', $versionNumber);
$version = $versionParts[0];
for ($i = 1; $i < 3; $i++) {
if (!empty($versionParts[$i])) {
$version .= str_pad((string)(int)$versionParts[$i], 3, '0', STR_PAD_LEFT);
} else {
$version .= '000';
}
}
return (int)$version;
}
/**
* Removes -dev -alpha -beta -RC states (also without '-' prefix) from a version number
* and replaces them by .0 and normalizes to a three part version number
*/
public static function getNumericTypo3Version(): string
{
$t3version = static::getCurrentTypo3Version();
$t3version = preg_replace('/-?(dev|alpha|beta|RC).*$/', '', $t3version);
$parts = GeneralUtility::intExplode('.', $t3version . '..');
$t3version = MathUtility::forceIntegerInRange($parts[0], 0, 999) . '.'
. MathUtility::forceIntegerInRange($parts[1], 0, 999) . '.'
. MathUtility::forceIntegerInRange($parts[2], 0, 999);
return $t3version;
}
/**
* Wrapper function for the static TYPO3 version to
* make functions using the constant unit testable.
*/
public static function getCurrentTypo3Version(): string
{
return (string)GeneralUtility::makeInstance(Typo3Version::class);
}
/**
* This function converts version range strings (like '4.2.0-4.4.99') to an array
* (like array('4.2.0', '4.4.99'). It also forces each version part to be between
* 0 and 999
*
* @param string $versionsString A string in the form 'x.x.x-y.y.y'
* @return string[]
*/
public static function convertVersionsStringToVersionNumbers(string $versionsString): array
{
$versions = GeneralUtility::trimExplode('-', $versionsString);
foreach ($versions as $i => $version) {
$cleanedVersion = GeneralUtility::trimExplode('.', $version);
foreach ($cleanedVersion as $j => $cleaned) {
$cleanedVersion[$j] = MathUtility::forceIntegerInRange((int)$cleaned, 0, 999);
}
$cleanedVersionString = implode('.', $cleanedVersion);
if (static::convertVersionNumberToInteger($cleanedVersionString) === 0) {
$cleanedVersionString = '';
}
$versions[$i] = $cleanedVersionString;
}
return $versions;
}
/**
* Parses the version number x.x.x and returns an array with the various parts.
* It also forces each … 0 to 999
*
* @param string $version Version string, in the format x.x.x
* @return array<string, int|string>
*/
public static function convertVersionStringToArray(string $version): array
{
$parts = GeneralUtility::intExplode('.', $version . '..');
$parts[0] = MathUtility::forceIntegerInRange($parts[0], 0, 999);
$parts[1] = MathUtility::forceIntegerInRange($parts[1], 0, 999);
$parts[2] = MathUtility::forceIntegerInRange($parts[2], 0, 999);
$result = [];
$result['version'] = $parts[0] . '.' . $parts[1] . '.' . $parts[2];
$result['version_int'] = (int)($parts[0] * 1000000 + $parts[1] * 1000 + $parts[2]);
$result['version_main'] = $parts[0];
$result['version_sub'] = $parts[1];
$result['version_dev'] = $parts[2];
return $result;
}
}