From efca6e7c20643c48f23a027081b8931fea71a6a4 Mon Sep 17 00:00:00 2001
From: Benni Mack <benni@typo3.org>
Date: Wed, 11 Aug 2021 10:19:23 +0200
Subject: [PATCH] [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 <typo3@b13.com>
Tested-by: Georg Ringer <georg.ringer@gmail.com>
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
Reviewed-by: Georg Ringer <georg.ringer@gmail.com>
Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch>
---
 .../Repository/TableManualRepository.php      |  4 +-
 .../Classes/Form/FieldControl/AddRecord.php   |  4 +-
 .../Classes/Utility/BackendUtility.php        |  2 +-
 .../Classes/View/BackendLayoutView.php        |  2 +-
 .../Cache/Backend/SimpleFileBackend.php       |  2 +-
 .../Configuration/ConfigurationManager.php    |  2 +-
 .../core/Classes/DataHandling/DataHandler.php |  2 +-
 .../IO/PharStreamWrapperInterceptor.php       |  2 +-
 .../Localization/Parser/AbstractXmlParser.php | 10 +--
 .../Classes/Locking/SimpleLockStrategy.php    |  2 +-
 .../Classes/Resource/Driver/LocalDriver.php   |  6 +-
 .../Classes/Resource/ResourceCompressor.php   |  6 +-
 .../core/Classes/Resource/ResourceFactory.php |  4 +-
 .../Parser/ConstantConfigurationParser.php    |  4 +-
 .../core/Classes/Utility/GeneralUtility.php   | 26 ++++----
 ...n-95257-GeneralUtilityisFirstPartOfStr.rst | 46 +++++++++++++
 .../GeneralUtilityFilesystemFixture.php       |  6 +-
 .../Tests/Unit/Utility/GeneralUtilityTest.php | 64 -------------------
 .../Utility/GeneralUtilityTest.php            | 64 +++++++++++++++++++
 .../Classes/Utility/LocalizationUtility.php   |  2 +-
 .../Classes/Utility/InstallUtility.php        |  2 +-
 .../Classes/Utility/ListUtility.php           |  3 +-
 .../Validation/RedirectUrlValidator.php       |  2 +-
 .../Classes/Service/TranslationService.php    |  2 +-
 typo3/sysext/impexp/Classes/Export.php        | 10 +--
 typo3/sysext/impexp/Classes/Import.php        |  4 +-
 .../Php/MethodCallStaticMatcher.php           |  7 ++
 .../Classes/Command/LostFilesCommand.php      |  4 +-
 28 files changed, 174 insertions(+), 120 deletions(-)
 create mode 100644 typo3/sysext/core/Documentation/Changelog/master/Deprecation-95257-GeneralUtilityisFirstPartOfStr.rst

diff --git a/typo3/sysext/backend/Classes/Domain/Repository/TableManualRepository.php b/typo3/sysext/backend/Classes/Domain/Repository/TableManualRepository.php
index c44dc51d670f..a8209085a0ed 100644
--- a/typo3/sysext/backend/Classes/Domain/Repository/TableManualRepository.php
+++ b/typo3/sysext/backend/Classes/Domain/Repository/TableManualRepository.php
@@ -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);
             }
diff --git a/typo3/sysext/backend/Classes/Form/FieldControl/AddRecord.php b/typo3/sysext/backend/Classes/Form/FieldControl/AddRecord.php
index 29aa1fe94e94..dbcac271d4e2 100644
--- a/typo3/sysext/backend/Classes/Form/FieldControl/AddRecord.php
+++ b/typo3/sysext/backend/Classes/Form/FieldControl/AddRecord.php
@@ -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));
         }
 
diff --git a/typo3/sysext/backend/Classes/Utility/BackendUtility.php b/typo3/sysext/backend/Classes/Utility/BackendUtility.php
index cbb4386d277d..b9d10b5cbc25 100644
--- a/typo3/sysext/backend/Classes/Utility/BackendUtility.php
+++ b/typo3/sysext/backend/Classes/Utility/BackendUtility.php
@@ -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 '';
                 }
 
diff --git a/typo3/sysext/backend/Classes/View/BackendLayoutView.php b/typo3/sysext/backend/Classes/View/BackendLayoutView.php
index 9c86c9a50076..539b9b419d97 100644
--- a/typo3/sysext/backend/Classes/View/BackendLayoutView.php
+++ b/typo3/sysext/backend/Classes/View/BackendLayoutView.php
@@ -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);
         }
 
diff --git a/typo3/sysext/core/Classes/Cache/Backend/SimpleFileBackend.php b/typo3/sysext/core/Classes/Cache/Backend/SimpleFileBackend.php
index 87a26ebc7336..e7540b0d76b1 100644
--- a/typo3/sysext/core/Classes/Cache/Backend/SimpleFileBackend.php
+++ b/typo3/sysext/core/Classes/Cache/Backend/SimpleFileBackend.php
@@ -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;
diff --git a/typo3/sysext/core/Classes/Configuration/ConfigurationManager.php b/typo3/sysext/core/Classes/Configuration/ConfigurationManager.php
index 5ab57fb66a3a..5ffc5402213a 100644
--- a/typo3/sysext/core/Classes/Configuration/ConfigurationManager.php
+++ b/typo3/sysext/core/Classes/Configuration/ConfigurationManager.php
@@ -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;
             }
         }
diff --git a/typo3/sysext/core/Classes/DataHandling/DataHandler.php b/typo3/sysext/core/Classes/DataHandling/DataHandler.php
index 7a533329c596..b5f9cffcf83f 100644
--- a/typo3/sysext/core/Classes/DataHandling/DataHandler.php
+++ b/typo3/sysext/core/Classes/DataHandling/DataHandler.php
@@ -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;
         }
diff --git a/typo3/sysext/core/Classes/IO/PharStreamWrapperInterceptor.php b/typo3/sysext/core/Classes/IO/PharStreamWrapperInterceptor.php
index d57c39f4f223..f4cf0e34b44d 100644
--- a/typo3/sysext/core/Classes/IO/PharStreamWrapperInterceptor.php
+++ b/typo3/sysext/core/Classes/IO/PharStreamWrapperInterceptor.php
@@ -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());
     }
 }
diff --git a/typo3/sysext/core/Classes/Localization/Parser/AbstractXmlParser.php b/typo3/sysext/core/Classes/Localization/Parser/AbstractXmlParser.php
index bfb8accd17cc..4fa802dca0bd 100644
--- a/typo3/sysext/core/Classes/Localization/Parser/AbstractXmlParser.php
+++ b/typo3/sysext/core/Classes/Localization/Parser/AbstractXmlParser.php
@@ -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 {
diff --git a/typo3/sysext/core/Classes/Locking/SimpleLockStrategy.php b/typo3/sysext/core/Classes/Locking/SimpleLockStrategy.php
index 374a60722553..2d0bf4249132 100644
--- a/typo3/sysext/core/Classes/Locking/SimpleLockStrategy.php
+++ b/typo3/sysext/core/Classes/Locking/SimpleLockStrategy.php
@@ -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;
diff --git a/typo3/sysext/core/Classes/Resource/Driver/LocalDriver.php b/typo3/sysext/core/Classes/Resource/Driver/LocalDriver.php
index 76672ec83a75..ec5f97d7e81b 100644
--- a/typo3/sysext/core/Classes/Resource/Driver/LocalDriver.php
+++ b/typo3/sysext/core/Classes/Resource/Driver/LocalDriver.php
@@ -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);
     }
 
     /**
diff --git a/typo3/sysext/core/Classes/Resource/ResourceCompressor.php b/typo3/sysext/core/Classes/Resource/ResourceCompressor.php
index 43b739d35791..8250ea6f95b6 100644
--- a/typo3/sysext/core/Classes/Resource/ResourceCompressor.php
+++ b/typo3/sysext/core/Classes/Resource/ResourceCompressor.php
@@ -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;
             }
         }
diff --git a/typo3/sysext/core/Classes/Resource/ResourceFactory.php b/typo3/sysext/core/Classes/Resource/ResourceFactory.php
index 2a4dc9f29b30..7b0d1286f29d 100644
--- a/typo3/sysext/core/Classes/Resource/ResourceFactory.php
+++ b/typo3/sysext/core/Classes/Resource/ResourceFactory.php
@@ -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]);
             }
         }
diff --git a/typo3/sysext/core/Classes/TypoScript/Parser/ConstantConfigurationParser.php b/typo3/sysext/core/Classes/TypoScript/Parser/ConstantConfigurationParser.php
index 8f80cdf2d2a4..4dc089d29275 100644
--- a/typo3/sysext/core/Classes/TypoScript/Parser/ConstantConfigurationParser.php
+++ b/typo3/sysext/core/Classes/TypoScript/Parser/ConstantConfigurationParser.php
@@ -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();
diff --git a/typo3/sysext/core/Classes/Utility/GeneralUtility.php b/typo3/sysext/core/Classes/Utility/GeneralUtility.php
index 73e1aaf474a4..dd3e13f9e2c3 100644
--- a/typo3/sysext/core/Classes/Utility/GeneralUtility.php
+++ b/typo3/sysext/core/Classes/Utility/GeneralUtility.php
@@ -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)
             ) {
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Deprecation-95257-GeneralUtilityisFirstPartOfStr.rst b/typo3/sysext/core/Documentation/Changelog/master/Deprecation-95257-GeneralUtilityisFirstPartOfStr.rst
new file mode 100644
index 000000000000..b0f3dcc3338c
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/master/Deprecation-95257-GeneralUtilityisFirstPartOfStr.rst
@@ -0,0 +1,46 @@
+.. 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
diff --git a/typo3/sysext/core/Tests/Unit/Utility/Fixtures/GeneralUtilityFilesystemFixture.php b/typo3/sysext/core/Tests/Unit/Utility/Fixtures/GeneralUtilityFilesystemFixture.php
index 0d586b1123ad..e1d9031bd7d4 100644
--- a/typo3/sysext/core/Tests/Unit/Utility/Fixtures/GeneralUtilityFilesystemFixture.php
+++ b/typo3/sysext/core/Tests/Unit/Utility/Fixtures/GeneralUtilityFilesystemFixture.php
@@ -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);
     }
 
     /**
diff --git a/typo3/sysext/core/Tests/Unit/Utility/GeneralUtilityTest.php b/typo3/sysext/core/Tests/Unit/Utility/GeneralUtilityTest.php
index 6a1b4ca37cf6..fb1d7d26d5c8 100644
--- a/typo3/sysext/core/Tests/Unit/Utility/GeneralUtilityTest.php
+++ b/typo3/sysext/core/Tests/Unit/Utility/GeneralUtilityTest.php
@@ -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
     ///////////////////////////////
diff --git a/typo3/sysext/core/Tests/UnitDeprecated/Utility/GeneralUtilityTest.php b/typo3/sysext/core/Tests/UnitDeprecated/Utility/GeneralUtilityTest.php
index 8f662ed0493b..8a3be6e716ef 100644
--- a/typo3/sysext/core/Tests/UnitDeprecated/Utility/GeneralUtilityTest.php
+++ b/typo3/sysext/core/Tests/UnitDeprecated/Utility/GeneralUtilityTest.php
@@ -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));
+    }
 }
diff --git a/typo3/sysext/extbase/Classes/Utility/LocalizationUtility.php b/typo3/sysext/extbase/Classes/Utility/LocalizationUtility.php
index b10e183c628a..b7007fa5609f 100644
--- a/typo3/sysext/extbase/Classes/Utility/LocalizationUtility.php
+++ b/typo3/sysext/extbase/Classes/Utility/LocalizationUtility.php
@@ -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);
diff --git a/typo3/sysext/extensionmanager/Classes/Utility/InstallUtility.php b/typo3/sysext/extensionmanager/Classes/Utility/InstallUtility.php
index 606f4dedfc18..2d069e5cc868 100644
--- a/typo3/sysext/extensionmanager/Classes/Utility/InstallUtility.php
+++ b/typo3/sysext/extensionmanager/Classes/Utility/InstallUtility.php
@@ -608,7 +608,7 @@ class InstallUtility implements SingletonInterface, LoggerAwareInterface
     {
         $allowedPaths = Extension::returnAllowedInstallPaths();
         foreach ($allowedPaths as $allowedPath) {
-            if (GeneralUtility::isFirstPartOfStr($path, $allowedPath)) {
+            if (str_starts_with($path, $allowedPath)) {
                 return true;
             }
         }
diff --git a/typo3/sysext/extensionmanager/Classes/Utility/ListUtility.php b/typo3/sysext/extensionmanager/Classes/Utility/ListUtility.php
index 18c8aa4c7bf4..a7c4a7ecd60e 100644
--- a/typo3/sysext/extensionmanager/Classes/Utility/ListUtility.php
+++ b/typo3/sysext/extensionmanager/Classes/Utility/ListUtility.php
@@ -21,7 +21,6 @@ use TYPO3\CMS\Core\Package\PackageInterface;
 use TYPO3\CMS\Core\Package\PackageManager;
 use TYPO3\CMS\Core\SingletonInterface;
 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
-use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\PathUtility;
 use TYPO3\CMS\Core\Utility\VersionNumberUtility;
 use TYPO3\CMS\Extensionmanager\Domain\Model\Extension;
@@ -162,7 +161,7 @@ class ListUtility implements SingletonInterface
     protected function getInstallTypeForPackage(PackageInterface $package)
     {
         foreach (Extension::returnInstallPaths() as $installType => $installPath) {
-            if (GeneralUtility::isFirstPartOfStr($package->getPackagePath(), $installPath)) {
+            if (str_starts_with($package->getPackagePath(), $installPath)) {
                 return $installType;
             }
         }
diff --git a/typo3/sysext/felogin/Classes/Validation/RedirectUrlValidator.php b/typo3/sysext/felogin/Classes/Validation/RedirectUrlValidator.php
index a1f0f7e5dd44..4b98ceaf5044 100644
--- a/typo3/sysext/felogin/Classes/Validation/RedirectUrlValidator.php
+++ b/typo3/sysext/felogin/Classes/Validation/RedirectUrlValidator.php
@@ -120,7 +120,7 @@ class RedirectUrlValidator implements LoggerAwareInterface
             $parsedUrl = @parse_url($url);
             if ($parsedUrl !== false && !isset($parsedUrl['scheme']) && !isset($parsedUrl['host'])) {
                 // If the relative URL starts with a slash, we need to check if it's within the current site path
-                return $parsedUrl['path'][0] !== '/' || GeneralUtility::isFirstPartOfStr($parsedUrl['path'], GeneralUtility::getIndpEnv('TYPO3_SITE_PATH'));
+                return $parsedUrl['path'][0] !== '/' || str_starts_with($parsedUrl['path'], GeneralUtility::getIndpEnv('TYPO3_SITE_PATH'));
             }
         }
         return false;
diff --git a/typo3/sysext/form/Classes/Service/TranslationService.php b/typo3/sysext/form/Classes/Service/TranslationService.php
index cb0f530938f5..f121d6116b93 100644
--- a/typo3/sysext/form/Classes/Service/TranslationService.php
+++ b/typo3/sysext/form/Classes/Service/TranslationService.php
@@ -121,7 +121,7 @@ class TranslationService implements SingletonInterface
         }
 
         $keyParts = explode(':', $key);
-        if (GeneralUtility::isFirstPartOfStr($key, 'LLL:')) {
+        if (str_starts_with($key, 'LLL:')) {
             $locallangPathAndFilename = $keyParts[1] . ':' . $keyParts[2];
             $key = $keyParts[3];
         } elseif (PathUtility::isExtensionPath($key)) {
diff --git a/typo3/sysext/impexp/Classes/Export.php b/typo3/sysext/impexp/Classes/Export.php
index d5e1289433d7..7011a74daf79 100644
--- a/typo3/sysext/impexp/Classes/Export.php
+++ b/typo3/sysext/impexp/Classes/Export.php
@@ -591,8 +591,8 @@ class Export extends ImportExport
         foreach ($relations as &$relation) {
             if (isset($relation['type']) && $relation['type'] === 'file') {
                 foreach ($relation['newValueFiles'] as &$fileRelationData) {
-                    $absoluteFilePath = $fileRelationData['ID_absFile'];
-                    if (GeneralUtility::isFirstPartOfStr($absoluteFilePath, Environment::getPublicPath())) {
+                    $absoluteFilePath = (string)$fileRelationData['ID_absFile'];
+                    if (str_starts_with($absoluteFilePath, Environment::getPublicPath())) {
                         $relatedFilePath = PathUtility::stripPathSitePrefix($absoluteFilePath);
                         $fileRelationData['ID'] = md5($relatedFilePath);
                     }
@@ -603,8 +603,8 @@ class Export extends ImportExport
                 if (is_array($relation['flexFormRels']['file'] ?? null)) {
                     foreach ($relation['flexFormRels']['file'] as &$subList) {
                         foreach ($subList as &$fileRelationData) {
-                            $absoluteFilePath = $fileRelationData['ID_absFile'];
-                            if (GeneralUtility::isFirstPartOfStr($absoluteFilePath, Environment::getPublicPath())) {
+                            $absoluteFilePath = (string)$fileRelationData['ID_absFile'];
+                            if (str_starts_with($absoluteFilePath, Environment::getPublicPath())) {
                                 $relatedFilePath = PathUtility::stripPathSitePrefix($absoluteFilePath);
                                 $fileRelationData['ID'] = md5($relatedFilePath);
                             }
@@ -1062,7 +1062,7 @@ class Export extends ImportExport
                         $resAbsolutePath = GeneralUtility::resolveBackPath(PathUtility::dirname($fileData['ID_absFile']) . '/' . $resRelativePath);
                         $resAbsolutePath = GeneralUtility::getFileAbsFileName($resAbsolutePath);
                         if ($resAbsolutePath !== ''
-                            && GeneralUtility::isFirstPartOfStr($resAbsolutePath, Environment::getPublicPath() . '/' . $this->getFileadminFolderName() . '/')
+                            && str_starts_with($resAbsolutePath, Environment::getPublicPath() . '/' . $this->getFileadminFolderName() . '/')
                             && @is_file($resAbsolutePath)
                         ) {
                             $resourceCaptured = true;
diff --git a/typo3/sysext/impexp/Classes/Import.php b/typo3/sysext/impexp/Classes/Import.php
index 5ae6daaea3a5..abb074f81e7f 100644
--- a/typo3/sysext/impexp/Classes/Import.php
+++ b/typo3/sysext/impexp/Classes/Import.php
@@ -1588,7 +1588,7 @@ class Import extends ImportExport
         if ($this->dat['header']['files'][$softref['file_ID']]) {
             // Initialize; Get directory prefix for file and find possible RTE filename
             $dirPrefix = PathUtility::dirname($relFileName) . '/';
-            if (GeneralUtility::isFirstPartOfStr($dirPrefix, $this->getFileadminFolderName() . '/')) {
+            if (str_starts_with($dirPrefix, $this->getFileadminFolderName() . '/')) {
                 // File in fileadmin/ folder:
                 // Create file (and possible resources)
                 $newFileName = $this->processSoftReferencesSaveFileCreateRelFile($dirPrefix, PathUtility::basename($relFileName), $softref['file_ID'], $table, $uid) ?: '';
@@ -1653,7 +1653,7 @@ class Import extends ImportExport
                                 $relResourceFileName = $this->dat['files'][$res_fileID]['parentRelFileName'];
                                 $absResourceFileName = GeneralUtility::resolveBackPath(Environment::getPublicPath() . '/' . $origDirPrefix . $relResourceFileName);
                                 $absResourceFileName = GeneralUtility::getFileAbsFileName($absResourceFileName);
-                                if ($absResourceFileName && GeneralUtility::isFirstPartOfStr($absResourceFileName, Environment::getPublicPath() . '/' . $this->getFileadminFolderName() . '/')) {
+                                if ($absResourceFileName && str_starts_with($absResourceFileName, Environment::getPublicPath() . '/' . $this->getFileadminFolderName() . '/')) {
                                     $destDir = PathUtility::stripPathSitePrefix(PathUtility::dirname($absResourceFileName) . '/');
                                     if ($this->resolveStoragePath($destDir, false) !== null && $this->checkOrCreateDir($destDir)) {
                                         $this->writeFileVerify($absResourceFileName, $res_fileID);
diff --git a/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallStaticMatcher.php b/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallStaticMatcher.php
index 96b3f36903c6..0a7fba636a9e 100644
--- a/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallStaticMatcher.php
+++ b/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallStaticMatcher.php
@@ -1170,4 +1170,11 @@ return [
             'Deprecation-94791-GeneralUtilityminifyJavaScript.rst'
         ],
     ],
+    'TYPO3\CMS\Core\Utility\GeneralUtility::isFirstPartOfStr' => [
+        'numberOfMandatoryArguments' => 2,
+        'maximumNumberOfArguments' => 2,
+        'restFiles' => [
+            'Deprecation-95257-GeneralUtilityisFirstPartOfStr.rst'
+        ],
+    ],
 ];
diff --git a/typo3/sysext/lowlevel/Classes/Command/LostFilesCommand.php b/typo3/sysext/lowlevel/Classes/Command/LostFilesCommand.php
index 92460689055c..fdc9f90c33bf 100644
--- a/typo3/sysext/lowlevel/Classes/Command/LostFilesCommand.php
+++ b/typo3/sysext/lowlevel/Classes/Command/LostFilesCommand.php
@@ -200,7 +200,7 @@ If you want to get more detailed information, use the --verbose option.')
             $customPaths = GeneralUtility::trimExplode(',', $customPaths, true);
             foreach ($customPaths as $customPath) {
                 if (false === realpath(Environment::getPublicPath() . '/' . $customPath)
-                    || !GeneralUtility::isFirstPartOfStr((string)realpath(Environment::getPublicPath() . '/' . $customPath), (string)realpath(Environment::getPublicPath()))) {
+                    || !str_starts_with((string)realpath(Environment::getPublicPath() . '/' . $customPath), (string)realpath(Environment::getPublicPath()))) {
                     throw new \Exception('The path: "' . $customPath . '" is invalid', 1450086736);
                 }
                 $files = GeneralUtility::getAllFilesAndFoldersInPath($files, Environment::getPublicPath() . '/' . $customPath);
@@ -224,7 +224,7 @@ If you want to get more detailed information, use the --verbose option.')
 
             $fileIsInExcludedPath = false;
             foreach ($excludedPaths as $exclPath) {
-                if (GeneralUtility::isFirstPartOfStr($value, $exclPath)) {
+                if (str_starts_with($value, $exclPath)) {
                     $fileIsInExcludedPath = true;
                     break;
                 }
-- 
GitLab