Commit efca6e7c authored by Benni Mack's avatar Benni Mack Committed by Christian Kuhn
Browse files

[TASK] Use native str_starts_with() PHP method

One of our main utility methods "GeneralUtility::isFirstPartOfStr"
can now replaced by PHP's native "str_starts_with()" function
(see https://www.php.net/manual/en/function.str-starts-with.php)
which is also available for PHP 7.4 thanks to Symfony's polyfill
package.

This way, we can
a) slim down our own code base in favor of native PHP calls
b) add a bit of performance due to native PHP calls
c) move towards type-safety to ensure that we hand over strings
to these methods, as our own method was a bit more "lax" on things

Resolves: #95257
Releases: master
Change-Id: I70617ab4419849353a72a10dfed31a2d96f58072
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/70444

Tested-by: core-ci's avatarcore-ci <typo3@b13.com>
Tested-by: Georg Ringer's avatarGeorg Ringer <georg.ringer@gmail.com>
Tested-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
Reviewed-by: Georg Ringer's avatarGeorg Ringer <georg.ringer@gmail.com>
Reviewed-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
parent 2602225d
......@@ -123,12 +123,12 @@ class TableManualRepository
}
foreach ($cshKeys as $cshKey => $value) {
// Extensions
if (GeneralUtility::isFirstPartOfStr($cshKey, 'xEXT_') && !isset($GLOBALS['TCA'][$cshKey])) {
if (str_starts_with($cshKey, 'xEXT_') && !isset($GLOBALS['TCA'][$cshKey])) {
$lang->loadSingleTableDescription($cshKey);
$this->renderTableOfContentItem($mode, $cshKey, 'extensions', $outputSections, $tocArray, $cshKeys);
}
// Other
if (!GeneralUtility::isFirstPartOfStr($cshKey, '_MOD_') && !isset($GLOBALS['TCA'][$cshKey])) {
if (!str_starts_with($cshKey, '_MOD_') && !isset($GLOBALS['TCA'][$cshKey])) {
$lang->loadSingleTableDescription($cshKey);
$this->renderTableOfContentItem($mode, $cshKey, 'other', $outputSections, $tocArray, $cshKeys);
}
......
......@@ -38,7 +38,7 @@ class AddRecord extends AbstractNode
{
$options = $this->data['renderData']['fieldControlOptions'];
$parameterArray = $this->data['parameterArray'];
$itemName = $parameterArray['itemFormElName'];
$itemName = (string)$parameterArray['itemFormElName'];
// Handle options and fallback
$title = $options['title'] ?? 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.createNew';
......@@ -70,7 +70,7 @@ class AddRecord extends AbstractNode
$pid = $this->resolvePid($options, $table);
$prefixOfFormElName = 'data[' . $this->data['tableName'] . '][' . $this->data['databaseRow']['uid'] . '][' . $this->data['fieldName'] . ']';
$flexFormPath = '';
if (GeneralUtility::isFirstPartOfStr($itemName, $prefixOfFormElName)) {
if (str_starts_with($itemName, $prefixOfFormElName)) {
$flexFormPath = str_replace('][', '/', substr($itemName, strlen($prefixOfFormElName) + 1, -1));
}
......
......@@ -3065,7 +3065,7 @@ class BackendUtility
// Look up the path:
if ($table === '_FILE') {
if (!GeneralUtility::isFirstPartOfStr($ref, Environment::getPublicPath())) {
if (!str_starts_with($ref, Environment::getPublicPath())) {
return '';
}
......
......@@ -507,7 +507,7 @@ class BackendLayoutView implements SingletonInterface
{
$columnName = $column['name'];
if (GeneralUtility::isFirstPartOfStr($columnName, 'LLL:')) {
if (str_starts_with($columnName, 'LLL:')) {
$columnName = $this->getLanguageService()->sL($columnName);
}
......
......@@ -144,7 +144,7 @@ class SimpleFileBackend extends AbstractBackend implements PhpCapableBackendInte
if ($basedir[strlen($basedir) - 1] !== '/') {
$basedir .= '/';
}
if (GeneralUtility::isFirstPartOfStr($cacheDirectory, $basedir)) {
if (str_starts_with($cacheDirectory, $basedir)) {
$documentRoot = $basedir;
$cacheDirectory = str_replace($basedir, '', $cacheDirectory);
$cacheDirectoryInBaseDir = true;
......
......@@ -441,7 +441,7 @@ class ConfigurationManager
{
// Early return for white listed paths
foreach ($this->whiteListedLocalConfigurationPaths as $whiteListedPath) {
if (GeneralUtility::isFirstPartOfStr($path, $whiteListedPath)) {
if (str_starts_with($path, $whiteListedPath)) {
return true;
}
}
......
......@@ -8749,7 +8749,7 @@ class DataHandler implements LoggerAwareInterface
}
}
// flush cache by tag
if (GeneralUtility::isFirstPartOfStr(strtolower($cacheCmd), 'cachetag:')) {
if (str_starts_with(strtolower($cacheCmd), 'cachetag:')) {
$cacheTag = substr($cacheCmd, 9);
$tagsToFlush[] = $cacheTag;
}
......
......@@ -58,6 +58,6 @@ class PharStreamWrapperInterceptor implements Assertable
}
$baseName = $invocation->getBaseName();
return GeneralUtility::validPathStr($baseName)
&& GeneralUtility::isFirstPartOfStr($baseName, Environment::getExtensionsPath());
&& str_starts_with($baseName, Environment::getExtensionsPath());
}
}
......@@ -107,11 +107,11 @@ abstract class AbstractXmlParser implements LocalizationParserInterface
* @param bool $sameLocation If TRUE, then locallang localization file name will be returned with same directory as $fileRef
* @return string Absolute path to the language file
*/
protected function getLocalizedFileName($fileRef, $language, $sameLocation = false)
protected function getLocalizedFileName(string $fileRef, string $language, bool $sameLocation = false)
{
// If $fileRef is already prefixed with "[language key]" then we should return it as is
$fileName = PathUtility::basename($fileRef);
if (GeneralUtility::isFirstPartOfStr($fileName, $language . '.')) {
if (str_starts_with($fileName, $language . '.')) {
return GeneralUtility::getFileAbsFileName($fileRef);
}
......@@ -120,13 +120,13 @@ abstract class AbstractXmlParser implements LocalizationParserInterface
}
// Analyze file reference
if (GeneralUtility::isFirstPartOfStr($fileRef, Environment::getFrameworkBasePath() . '/')) {
if (str_starts_with($fileRef, Environment::getFrameworkBasePath() . '/')) {
// Is system
$validatedPrefix = Environment::getFrameworkBasePath() . '/';
} elseif (GeneralUtility::isFirstPartOfStr($fileRef, Environment::getBackendPath() . '/ext/')) {
} elseif (str_starts_with($fileRef, Environment::getBackendPath() . '/ext/')) {
// Is global
$validatedPrefix = Environment::getBackendPath() . '/ext/';
} elseif (GeneralUtility::isFirstPartOfStr($fileRef, Environment::getExtensionsPath() . '/')) {
} elseif (str_starts_with($fileRef, Environment::getExtensionsPath() . '/')) {
// Is local
$validatedPrefix = Environment::getExtensionsPath() . '/';
} else {
......
......@@ -114,7 +114,7 @@ class SimpleLockStrategy implements LockingStrategyInterface
$success = true;
if (
GeneralUtility::isAllowedAbsPath($this->filePath)
&& GeneralUtility::isFirstPartOfStr($this->filePath, Environment::getVarPath() . '/' . self::FILE_LOCK_FOLDER)
&& str_starts_with($this->filePath, Environment::getVarPath() . '/' . self::FILE_LOCK_FOLDER)
) {
if (@unlink($this->filePath) === false) {
$success = false;
......
......@@ -133,7 +133,7 @@ class LocalDriver extends AbstractHierarchicalFilesystemDriver implements Stream
if ($this->hasCapability(ResourceStorage::CAPABILITY_PUBLIC)) {
if (!empty($this->configuration['baseUri'])) {
$this->baseUri = rtrim($this->configuration['baseUri'], '/') . '/';
} elseif (GeneralUtility::isFirstPartOfStr($this->absoluteBasePath, Environment::getPublicPath())) {
} elseif (str_starts_with($this->absoluteBasePath, Environment::getPublicPath())) {
// use site-relative URLs
$temporaryBaseUri = rtrim(PathUtility::stripPathSitePrefix($this->absoluteBasePath), '/');
if ($temporaryBaseUri !== '') {
......@@ -782,7 +782,7 @@ class LocalDriver extends AbstractHierarchicalFilesystemDriver implements Stream
// as for the "virtual storage" for backwards-compatibility, this check always fails, as the file probably lies under public web path
// thus, it is not checked here
// @todo is check in storage
if (GeneralUtility::isFirstPartOfStr($localFilePath, $this->absoluteBasePath) && $this->storageUid > 0) {
if (str_starts_with($localFilePath, $this->absoluteBasePath) && $this->storageUid > 0) {
throw new \InvalidArgumentException('Cannot add a file that is already part of this storage.', 1314778269);
}
$newFileName = $this->sanitizeFileName($newFileName !== '' ? $newFileName : PathUtility::basename($localFilePath));
......@@ -1310,7 +1310,7 @@ class LocalDriver extends AbstractHierarchicalFilesystemDriver implements Stream
if ($folderIdentifier !== '/') {
$folderIdentifier .= '/';
}
return GeneralUtility::isFirstPartOfStr($entryIdentifier, $folderIdentifier);
return str_starts_with($entryIdentifier, $folderIdentifier);
}
/**
......
......@@ -256,7 +256,7 @@ class ResourceCompressor
if (GeneralUtility::isValidUrl($filename)) {
// check if it is possibly a local file with fully qualified URL
if (GeneralUtility::isOnCurrentHost($filename) &&
GeneralUtility::isFirstPartOfStr(
str_starts_with(
$filename,
$GLOBALS['TYPO3_REQUEST']->getAttribute('normalizedParams')->getSiteUrl()
)
......@@ -295,7 +295,7 @@ class ResourceCompressor
$contents = substr($contents, 3);
}
// only fix paths if files aren't already in typo3temp (already processed)
if ($type === 'css' && !GeneralUtility::isFirstPartOfStr($filename, $this->targetDirectory)) {
if ($type === 'css' && !str_starts_with($filename, $this->targetDirectory)) {
$contents = $this->cssFixRelativeUrlPaths($contents, $filename);
}
$concatenated .= LF . $contents;
......@@ -494,7 +494,7 @@ class ResourceCompressor
{
foreach ($baseDirectories as $baseDirectory) {
// check, if $filename starts with base directory
if (GeneralUtility::isFirstPartOfStr($filename, $baseDirectory)) {
if (str_starts_with($filename, $baseDirectory)) {
return true;
}
}
......
......@@ -297,7 +297,7 @@ class ResourceFactory implements SingletonInterface
// This is done in all considered sub functions anyway
$input = str_replace(Environment::getPublicPath() . '/', '', $input);
if (GeneralUtility::isFirstPartOfStr($input, 'file:')) {
if (str_starts_with($input, 'file:')) {
$input = substr($input, 5);
return $this->retrieveFileOrFolderObject($input);
}
......@@ -354,7 +354,7 @@ class ResourceFactory implements SingletonInterface
// auto-detecting the best-matching storage to use
$folderIdentifier = $parts[0];
// make sure to not use an absolute path, and remove Environment::getPublicPath if it is prepended
if (GeneralUtility::isFirstPartOfStr($folderIdentifier, Environment::getPublicPath() . '/')) {
if (str_starts_with($folderIdentifier, Environment::getPublicPath() . '/')) {
$folderIdentifier = PathUtility::stripPathSitePrefix($parts[0]);
}
}
......
......@@ -110,9 +110,9 @@ class ConstantConfigurationParser
protected function buildConfigurationArray(array $configurationOption): array
{
$hierarchicConfiguration = [];
if (GeneralUtility::isFirstPartOfStr($configurationOption['type'], 'user')) {
if (str_starts_with((string)$configurationOption['type'], 'user')) {
$configurationOption = $this->extractInformationForConfigFieldsOfTypeUser($configurationOption);
} elseif (GeneralUtility::isFirstPartOfStr($configurationOption['type'], 'options')) {
} elseif (str_starts_with((string)$configurationOption['type'], 'options')) {
$configurationOption = $this->extractInformationForConfigFieldsOfTypeOptions($configurationOption);
}
$languageService = $this->getLanguageService();
......
......@@ -724,9 +724,11 @@ class GeneralUtility
* @param string $str Full string to check
* @param string $partStr Reference string which must be found as the "first part" of the full string
* @return bool TRUE if $partStr was found to be equal to the first part of $str
* @deprecated will be removed in TYPO3 v12.0. Use native PHP str_starts_with() with proper casting instead.
*/
public static function isFirstPartOfStr($str, $partStr)
{
trigger_error('GeneralUtility::isFirstPartOfStr() will be removed in TYPO3 v12.0. Use PHPs str_starts_with() method instead', E_USER_DEPRECATED);
$str = is_array($str) ? '' : (string)$str;
$partStr = is_array($partStr) ? '' : (string)$partStr;
return $partStr !== '' && strpos($str, $partStr, 0) === 0;
......@@ -893,7 +895,7 @@ class GeneralUtility
// our original $url might only contain <scheme>: (e.g. mail:)
// so we convert that to the double-slashed version to ensure
// our check against the $recomposedUrl is proper
if (!self::isFirstPartOfStr($url, $parsedUrl['scheme'] . '://')) {
if (!str_starts_with($url, $parsedUrl['scheme'] . '://')) {
$url = str_replace($parsedUrl['scheme'] . ':', $parsedUrl['scheme'] . '://', $url);
}
$recomposedUrl = HttpUtility::buildUrl($parsedUrl);
......@@ -1832,7 +1834,7 @@ class GeneralUtility
foreach ($allowedPathPrefixes as $pathPrefix => $prefixLabel) {
$dirName = $pathPrefix . '/';
// Invalid file path, let's check for the other path, if it exists
if (!static::isFirstPartOfStr($fI['dirname'], $dirName)) {
if (!str_starts_with($fI['dirname'], $dirName)) {
if ($errorMessage === null) {
$errorMessage = '"' . $fI['dirname'] . '" was not within directory ' . $prefixLabel;
}
......@@ -2132,10 +2134,10 @@ class GeneralUtility
* @param string $prefixToRemove The prefix path to remove (if found as first part of string!)
* @return string[]|string The input $fileArr processed, or a string with an error message, when an error occurred.
*/
public static function removePrefixPathFromList(array $fileArr, $prefixToRemove)
public static function removePrefixPathFromList(array $fileArr, string $prefixToRemove)
{
foreach ($fileArr as $k => &$absFileRef) {
if (self::isFirstPartOfStr($absFileRef, $prefixToRemove)) {
foreach ($fileArr as &$absFileRef) {
if (str_starts_with($absFileRef, $prefixToRemove)) {
$absFileRef = substr($absFileRef, strlen($prefixToRemove));
} else {
return 'ERROR: One or more of the files was NOT prefixed with the prefix-path!';
......@@ -2822,8 +2824,8 @@ class GeneralUtility
// is relative. Prepended with the public web folder
$filename = Environment::getPublicPath() . '/' . $filename;
} elseif (!(
static::isFirstPartOfStr($filename, Environment::getProjectPath())
|| static::isFirstPartOfStr($filename, Environment::getPublicPath())
str_starts_with($filename, Environment::getProjectPath())
|| str_starts_with($filename, Environment::getPublicPath())
)) {
// absolute, but set to blank if not allowed
$filename = '';
......@@ -2883,9 +2885,9 @@ class GeneralUtility
$lockRootPath = $GLOBALS['TYPO3_CONF_VARS']['BE']['lockRootPath'] ?? '';
return static::isAbsPath($path) && static::validPathStr($path)
&& (
static::isFirstPartOfStr($path, Environment::getProjectPath())
|| static::isFirstPartOfStr($path, Environment::getPublicPath())
|| ($lockRootPath && static::isFirstPartOfStr($path, $lockRootPath))
str_starts_with($path, Environment::getProjectPath())
|| str_starts_with($path, Environment::getPublicPath())
|| ($lockRootPath && str_starts_with($path, $lockRootPath))
);
}
......@@ -3041,8 +3043,8 @@ class GeneralUtility
if (
self::validPathStr($uploadedTempFileName)
&& (
self::isFirstPartOfStr($uploadedTempFileName, Environment::getPublicPath() . '/typo3temp/')
|| self::isFirstPartOfStr($uploadedTempFileName, Environment::getVarPath() . '/')
str_starts_with($uploadedTempFileName, Environment::getPublicPath() . '/typo3temp/')
|| str_starts_with($uploadedTempFileName, Environment::getVarPath() . '/')
)
&& @is_file($uploadedTempFileName)
) {
......
.. include:: ../../Includes.txt
========================================================
Deprecation: #95257 - GeneralUtility::isFirstPartOfStr()
========================================================
See :issue:`95257`
Description
===========
The helper method :php:`GeneralUtility::isFirstPartOfStr()` has
been marked as deprecated, as the newly available PHP-built in
function :php:`str_starts_with()` can be used instead, which
supports proper typing and is faster on PHP 8.0.
For PHP 7.4 installations, the dependency `symfony/polyfill-php80`
adds the PHP function in lower PHP environments, which TYPO3
Core ships as dependency.
Impact
======
Calling `GeneralUtility::isFirstPartOfStr()` will trigger a
PHP deprecation notice.
Affected Installations
======================
TYPO3 installations using this TYPO3 API function - either via
extensions or in their own site-specific code. An analysis
via TYPO3's extension scanner will show any matches.
Migration
=========
Replace all calls of `GeneralUtility::isFirstPartOfStr()` with
`str_starts_with()` to avoid deprecation warnings and to keep
your code up-to-date.
See https://www.php.net/manual/en/function.str-starts-with.php for further syntax.
.. index:: PHP-API, FullyScanned, ext:core
\ No newline at end of file
......@@ -32,7 +32,7 @@ class GeneralUtilityFilesystemFixture extends GeneralUtility
*/
public static function isAbsPath($path): bool
{
return self::isFirstPartOfStr($path, 'vfs://') || parent::isAbsPath($path);
return str_starts_with($path, 'vfs://') || parent::isAbsPath($path);
}
/**
......@@ -43,7 +43,7 @@ class GeneralUtilityFilesystemFixture extends GeneralUtility
*/
public static function isAllowedAbsPath($path): bool
{
return self::isFirstPartOfStr($path, 'vfs://') || parent::isAllowedAbsPath($path);
return str_starts_with($path, 'vfs://') || parent::isAllowedAbsPath($path);
}
/**
......@@ -54,7 +54,7 @@ class GeneralUtilityFilesystemFixture extends GeneralUtility
*/
public static function validPathStr($theFile): bool
{
return self::isFirstPartOfStr($theFile, 'vfs://') || parent::validPathStr($theFile);
return str_starts_with($theFile, 'vfs://') || parent::validPathStr($theFile);
}
/**
......
......@@ -584,70 +584,6 @@ class GeneralUtilityTest extends UnitTestCase
self::assertCount(1000, explode(',', $list));
}
///////////////////////////////
// Tests concerning isFirstPartOfStr
///////////////////////////////
/**
* Data provider for isFirstPartOfStrReturnsTrueForMatchingFirstParts
*
* @return array
*/
public function isFirstPartOfStrReturnsTrueForMatchingFirstPartDataProvider(): array
{
return [
'match first part of string' => ['hello world', 'hello'],
'match whole string' => ['hello', 'hello'],
'integer is part of string with same number' => ['24', 24],
'string is part of integer with same number' => [24, '24'],
'integer is part of string starting with same number' => ['24 beer please', 24]
];
}
/**
* @test
* @dataProvider isFirstPartOfStrReturnsTrueForMatchingFirstPartDataProvider
*/
public function isFirstPartOfStrReturnsTrueForMatchingFirstPart($string, $part): void
{
self::assertTrue(GeneralUtility::isFirstPartOfStr($string, $part));
}
/**
* Data provider for checkIsFirstPartOfStrReturnsFalseForNotMatchingFirstParts
*
* @return array
*/
public function isFirstPartOfStrReturnsFalseForNotMatchingFirstPartDataProvider(): array
{
return [
'no string match' => ['hello', 'bye'],
'no case sensitive string match' => ['hello world', 'Hello'],
'array is not part of string' => ['string', []],
'string is not part of array' => [[], 'string'],
'NULL is not part of string' => ['string', null],
'string is not part of NULL' => [null, 'string'],
'NULL is not part of array' => [[], null],
'array is not part of NULL' => [null, []],
'empty string is not part of empty string' => ['', ''],
'NULL is not part of empty string' => ['', null],
'false is not part of empty string' => ['', false],
'empty string is not part of NULL' => [null, ''],
'empty string is not part of false' => [false, ''],
'empty string is not part of zero integer' => [0, ''],
'zero integer is not part of NULL' => [null, 0],
'zero integer is not part of empty string' => ['', 0]
];
}
/**
* @test
* @dataProvider isFirstPartOfStrReturnsFalseForNotMatchingFirstPartDataProvider
*/
public function isFirstPartOfStrReturnsFalseForNotMatchingFirstPart($string, $part): void
{
self::assertFalse(GeneralUtility::isFirstPartOfStr($string, $part));
}
///////////////////////////////
// Tests concerning formatSize
///////////////////////////////
......
......@@ -86,4 +86,68 @@ class GeneralUtilityTest extends UnitTestCase
'List contains removeme multiple times nothing else 5x' => ['removeme,removeme,removeme,removeme,removeme', ''],
];
}
///////////////////////////////
// Tests concerning isFirstPartOfStr
///////////////////////////////
/**
* Data provider for isFirstPartOfStrReturnsTrueForMatchingFirstParts
*
* @return array
*/
public function isFirstPartOfStrReturnsTrueForMatchingFirstPartDataProvider(): array
{
return [
'match first part of string' => ['hello world', 'hello'],
'match whole string' => ['hello', 'hello'],
'integer is part of string with same number' => ['24', 24],
'string is part of integer with same number' => [24, '24'],
'integer is part of string starting with same number' => ['24 beer please', 24]
];
}
/**
* @test
* @dataProvider isFirstPartOfStrReturnsTrueForMatchingFirstPartDataProvider
*/
public function isFirstPartOfStrReturnsTrueForMatchingFirstPart($string, $part): void
{
self::assertTrue(GeneralUtility::isFirstPartOfStr($string, $part));
}
/**
* Data provider for checkIsFirstPartOfStrReturnsFalseForNotMatchingFirstParts
*
* @return array
*/
public function isFirstPartOfStrReturnsFalseForNotMatchingFirstPartDataProvider(): array
{
return [
'no string match' => ['hello', 'bye'],
'no case sensitive string match' => ['hello world', 'Hello'],
'array is not part of string' => ['string', []],
'string is not part of array' => [[], 'string'],
'NULL is not part of string' => ['string', null],
'string is not part of NULL' => [null, 'string'],
'NULL is not part of array' => [[], null],
'array is not part of NULL' => [null, []],
'empty string is not part of empty string' => ['', ''],
'NULL is not part of empty string' => ['', null],
'false is not part of empty string' => ['', false],
'empty string is not part of NULL' => [null, ''],
'empty string is not part of false' => [false, ''],
'empty string is not part of zero integer' => [0, ''],
'zero integer is not part of NULL' => [null, 0],
'zero integer is not part of empty string' => ['', 0]
];
}
/**
* @test
* @dataProvider isFirstPartOfStrReturnsFalseForNotMatchingFirstPartDataProvider
*/
public function isFirstPartOfStrReturnsFalseForNotMatchingFirstPart($string, $part): void
{
self::assertFalse(GeneralUtility::isFirstPartOfStr($string, $part));
}
}
......@@ -76,7 +76,7 @@ class LocalizationUtility
return null;
}
$value = null;
if (GeneralUtility::isFirstPartOfStr($key, 'LLL:')) {
if (str_starts_with($key, 'LLL:')) {
$keyParts = explode(':', $key);
unset($keyParts[0]);
$key = array_pop($keyParts);
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment