diff --git a/Neos.Cache/Classes/Backend/PdoBackend.php b/Neos.Cache/Classes/Backend/PdoBackend.php index 99ef0e8dac..839d098ba2 100644 --- a/Neos.Cache/Classes/Backend/PdoBackend.php +++ b/Neos.Cache/Classes/Backend/PdoBackend.php @@ -526,7 +526,7 @@ protected function createCacheTables(): void { $this->connect(); try { - PdoHelper::importSql($this->databaseHandle, $this->pdoDriver, __DIR__ . '/../../Resources/Private/DDL.sql'); + PdoHelper::importSql($this->databaseHandle, $this->pdoDriver, __DIR__ . '/../../Resources/Private/DDL.sql', ['###CACHE_TABLE_NAME###' => $this->cacheTableName, '###TAGS_TABLE_NAME###' => $this->tagsTableName]); } catch (\PDOException $exception) { throw new Exception('Could not create cache tables with DSN "' . $this->dataSourceName . '". PDO error: ' . $exception->getMessage(), 1259576985); } @@ -648,6 +648,7 @@ public function setup(): Result $this->connect(); } catch (Exception $exception) { $result->addError(new Error($exception->getMessage(), (int)$exception->getCode(), [], 'Failed')); + return $result; } if ($this->pdoDriver === 'sqlite') { $result->addNotice(new Notice('SQLite database tables are created automatically and don\'t need to be set up')); diff --git a/Neos.Cache/Classes/Backend/TaggableMultiBackend.php b/Neos.Cache/Classes/Backend/TaggableMultiBackend.php index 5fdaf6200b..bc34f509ea 100644 --- a/Neos.Cache/Classes/Backend/TaggableMultiBackend.php +++ b/Neos.Cache/Classes/Backend/TaggableMultiBackend.php @@ -55,16 +55,15 @@ protected function buildSubBackend(string $backendClassName, array $backendOptio public function flushByTag(string $tag): int { $this->prepareBackends(); - $count = 0; + $flushed = 0; foreach ($this->backends as $backend) { try { - $count |= $backend->flushByTag($tag); + $flushed += $backend->flushByTag($tag); } catch (\Throwable $t) { $this->handleError($t); } } - - return $count; + return $flushed; } /** diff --git a/Neos.Cache/Classes/Psr/Cache/CachePool.php b/Neos.Cache/Classes/Psr/Cache/CachePool.php index 16a989319b..998b0f4653 100644 --- a/Neos.Cache/Classes/Psr/Cache/CachePool.php +++ b/Neos.Cache/Classes/Psr/Cache/CachePool.php @@ -27,7 +27,7 @@ class CachePool implements CacheItemPoolInterface /** * Pattern an entry identifier must match. */ - const PATTERN_ENTRYIDENTIFIER = '/^[a-zA-Z0-9_%\-&]{1,250}$/'; + const PATTERN_ENTRYIDENTIFIER = '/^[a-zA-Z0-9_%\-&\.]{1,250}$/'; /** * @var BackendInterface diff --git a/Neos.Cache/Resources/Private/DDL.sql b/Neos.Cache/Resources/Private/DDL.sql index c19211d534..5e8967256d 100644 --- a/Neos.Cache/Resources/Private/DDL.sql +++ b/Neos.Cache/Resources/Private/DDL.sql @@ -1,6 +1,6 @@ BEGIN; -CREATE TABLE "cache" ( +CREATE TABLE "###CACHE_TABLE_NAME###" ( "identifier" VARCHAR(250) NOT NULL, "cache" VARCHAR(250) NOT NULL, "context" VARCHAR(150) NOT NULL, @@ -10,13 +10,13 @@ CREATE TABLE "cache" ( PRIMARY KEY ("identifier", "cache", "context") ); -CREATE TABLE "tags" ( +CREATE TABLE "###TAGS_TABLE_NAME###" ( "identifier" VARCHAR(250) NOT NULL, "cache" VARCHAR(250) NOT NULL, "context" VARCHAR(150) NOT NULL, "tag" VARCHAR(250) NOT NULL ); -CREATE INDEX "identifier" ON "tags" ("identifier", "cache", "context"); -CREATE INDEX "tag" ON "tags" ("tag"); +CREATE INDEX "identifier" ON "###TAGS_TABLE_NAME###" ("identifier", "cache", "context"); +CREATE INDEX "tag" ON "###TAGS_TABLE_NAME###" ("tag"); COMMIT; diff --git a/Neos.Cache/Resources/Private/mysql.DDL.sql b/Neos.Cache/Resources/Private/mysql.DDL.sql index 89576116cd..6be26bf5e8 100644 --- a/Neos.Cache/Resources/Private/mysql.DDL.sql +++ b/Neos.Cache/Resources/Private/mysql.DDL.sql @@ -1,6 +1,6 @@ BEGIN; -CREATE TABLE "cache" ( +CREATE TABLE "###CACHE_TABLE_NAME###" ( "identifier" VARCHAR(250) NOT NULL, "cache" VARCHAR(250) NOT NULL, "context" VARCHAR(150) NOT NULL, @@ -10,7 +10,7 @@ CREATE TABLE "cache" ( PRIMARY KEY ("identifier", "cache", "context") ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -CREATE TABLE "tags" ( +CREATE TABLE "###TAGS_TABLE_NAME###" ( "pk" INT NOT NULL AUTO_INCREMENT, "identifier" VARCHAR(250) NOT NULL, "cache" VARCHAR(250) NOT NULL, @@ -18,7 +18,7 @@ CREATE TABLE "tags" ( "tag" VARCHAR(250) NOT NULL, PRIMARY KEY ("pk") ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -CREATE INDEX "identifier" ON tags ("identifier", "cache", "context"); -CREATE INDEX "tag" ON "tags" ("tag"); +CREATE INDEX "identifier" ON ###TAGS_TABLE_NAME### ("identifier", "cache", "context"); +CREATE INDEX "tag" ON "###TAGS_TABLE_NAME###" ("tag"); COMMIT; diff --git a/Neos.Cache/Resources/Private/pgsql.DDL.sql b/Neos.Cache/Resources/Private/pgsql.DDL.sql index aa5be82ca5..96e310cac9 100644 --- a/Neos.Cache/Resources/Private/pgsql.DDL.sql +++ b/Neos.Cache/Resources/Private/pgsql.DDL.sql @@ -1,6 +1,6 @@ BEGIN; -CREATE TABLE "cache" ( +CREATE TABLE "###CACHE_TABLE_NAME###" ( "identifier" VARCHAR(250) NOT NULL, "cache" VARCHAR(250) NOT NULL, "context" VARCHAR(150) NOT NULL, @@ -10,13 +10,13 @@ CREATE TABLE "cache" ( PRIMARY KEY ("identifier", "cache", "context") ); -CREATE TABLE "tags" ( +CREATE TABLE "###TAGS_TABLE_NAME###" ( "identifier" VARCHAR(250) NOT NULL, "cache" VARCHAR(250) NOT NULL, "context" VARCHAR(150) NOT NULL, "tag" VARCHAR(250) NOT NULL ); -CREATE INDEX "identifier" ON "tags" ("identifier", "cache", "context"); -CREATE INDEX "tag" ON "tags" ("tag"); +CREATE INDEX "identifier" ON "###TAGS_TABLE_NAME###" ("identifier", "cache", "context"); +CREATE INDEX "tag" ON "###TAGS_TABLE_NAME###" ("tag"); COMMIT; diff --git a/Neos.Cache/Tests/Unit/Backend/TaggableMultiBackendTest.php b/Neos.Cache/Tests/Unit/Backend/TaggableMultiBackendTest.php new file mode 100644 index 0000000000..f99d99d010 --- /dev/null +++ b/Neos.Cache/Tests/Unit/Backend/TaggableMultiBackendTest.php @@ -0,0 +1,48 @@ +getMockBuilder(NullBackend::class); + $firstNullBackendMock = $mockBuilder->getMock(); + $secondNullBackendMock = $mockBuilder->getMock(); + $thirdNullBackendMock = $mockBuilder->getMock(); + + $firstNullBackendMock->expects(self::once())->method('flushByTag')->with('foo')->willReturn(2); + $secondNullBackendMock->expects(self::once())->method('flushByTag')->with('foo')->willThrowException(new \RuntimeException()); + $thirdNullBackendMock->expects(self::once())->method('flushByTag')->with('foo')->willReturn(3); + + $multiBackend = new TaggableMultiBackend($this->getEnvironmentConfiguration(), []); + $this->inject($multiBackend, 'backends', [$firstNullBackendMock, $secondNullBackendMock, $thirdNullBackendMock]); + $this->inject($multiBackend, 'initialized', true); + + $result = $multiBackend->flushByTag('foo'); + self::assertSame(5, $result); + } + + /** + * @return EnvironmentConfiguration + */ + public function getEnvironmentConfiguration(): EnvironmentConfiguration + { + return new EnvironmentConfiguration( + __DIR__ . '~Testing', + 'vfs://Foo/', + 255 + ); + } +} diff --git a/Neos.Cache/Tests/Unit/Psr/Cache/CachePoolTest.php b/Neos.Cache/Tests/Unit/Psr/Cache/CachePoolTest.php index 578a2d395f..5ca532204c 100644 --- a/Neos.Cache/Tests/Unit/Psr/Cache/CachePoolTest.php +++ b/Neos.Cache/Tests/Unit/Psr/Cache/CachePoolTest.php @@ -12,6 +12,7 @@ */ use Neos\Cache\Backend\AbstractBackend; +use Neos\Cache\Backend\BackendInterface; use Neos\Cache\Psr\Cache\CachePool; use Neos\Cache\Psr\Cache\CacheItem; use Neos\Cache\Psr\InvalidArgumentException; @@ -23,6 +24,57 @@ */ class CachePoolTest extends BaseTestCase { + public function validIdentifiersDataProvider(): array + { + return [ + ['short'], + ['SomeValidIdentifier'], + ['withNumbers0123456789'], + ['withUnder_score'], + ['with.dot'], + + // The following tests exceed the minimum requirements of the PSR-6 keys (@see https://www.php-fig.org/psr/psr-6/#definitions) + ['dashes-are-allowed'], + ['percent%sign'], + ['amper&sand'], + ['a-string-that-exceeds-the-psr-minimum-maxlength-of-sixtyfour-but-is-shorter-than-twohundredandfifty-characters'], + ]; + } + + /** + * @test + * @dataProvider validIdentifiersDataProvider + */ + public function validIdentifiers(string $identifier): void + { + $mockBackend = $this->getMockBuilder(BackendInterface::class)->getMock(); + $cachePool = new CachePool($identifier, $mockBackend); + self::assertInstanceOf(CachePool::class, $cachePool); + } + + public function invalidIdentifiersDataProvider(): array + { + return [ + [''], + ['späcialcharacters'], + ['a-string-that-exceeds-the-maximum-allowed-length-of-twohundredandfifty-characters-which-is-pretty-large-as-it-turns-out-so-i-repeat-a-string-that-exceeds-the-maximum-allowed-length-of-twohundredandfifty-characters-still-not-there-wow-crazy-flow-rocks-though'], + ]; + } + + /** + * @test + * @dataProvider invalidIdentifiersDataProvider + */ + public function invalidIdentifiers(string $identifier): void + { + $mockBackend = $this->getMockBuilder(BackendInterface::class)->getMock(); + + $this->expectException(\InvalidArgumentException::class); + new CachePool($identifier, $mockBackend); + } + + + /** * @test */ diff --git a/Neos.Flow/Classes/Core/Booting/Scripts.php b/Neos.Flow/Classes/Core/Booting/Scripts.php index 3c8214eda3..9b352de3c6 100644 --- a/Neos.Flow/Classes/Core/Booting/Scripts.php +++ b/Neos.Flow/Classes/Core/Booting/Scripts.php @@ -60,6 +60,9 @@ */ class Scripts { + /** @var string */ + protected static $builtPhpCommand = null; + /** * Initializes the Class Loader * @@ -773,6 +776,10 @@ protected static function buildSubprocessCommand(string $commandIdentifier, arra */ public static function buildPhpCommand(array $settings): string { + if (isset(static::$builtPhpCommand)) { + return static::$builtPhpCommand; + } + $subRequestEnvironmentVariables = [ 'FLOW_ROOTPATH' => FLOW_PATH_ROOT, 'FLOW_PATH_TEMPORARY_BASE' => FLOW_PATH_TEMPORARY_BASE, @@ -810,7 +817,7 @@ public static function buildPhpCommand(array $settings): string static::ensureWebSubrequestsUseCurrentlyRunningPhpVersion($command); - return $command; + return static::$builtPhpCommand = $command; } /** @@ -867,8 +874,13 @@ protected static function ensureCLISubrequestsUseCurrentlyRunningPhpBinary($phpB ); } - exec(PHP_BINARY . ' -r "echo realpath(PHP_BINARY);"', $output); - $realPhpBinary = $output[0]; + // stfu to avoid possible open_basedir restriction https://github.com/neos/flow-development-collection/pull/2491 + $realPhpBinary = @realpath(PHP_BINARY); + if ($realPhpBinary === false) { + // bypass with exec open_basedir restriction + exec(PHP_BINARY . ' -r "echo realpath(PHP_BINARY);"', $output); + $realPhpBinary = $output[0]; + } if (strcmp($realPhpBinary, $configuredPhpBinaryPathAndFilename) !== 0) { throw new Exception\SubProcessException(sprintf( 'You are running the Flow CLI with a PHP binary different from the one Flow is configured to use internally. ' . diff --git a/Neos.Flow/Classes/Mvc/Dispatcher.php b/Neos.Flow/Classes/Mvc/Dispatcher.php index ebd9552960..b3ba774fbd 100644 --- a/Neos.Flow/Classes/Mvc/Dispatcher.php +++ b/Neos.Flow/Classes/Mvc/Dispatcher.php @@ -27,6 +27,7 @@ use Neos\Flow\Security\Exception\AccessDeniedException; use Neos\Flow\Security\Exception\AuthenticationRequiredException; use Neos\Flow\Security\Exception\MissingConfigurationException; +use Psr\Log\LoggerInterface; /** * Dispatches requests to the controller which was specified by the request and @@ -105,7 +106,7 @@ public function dispatch(ActionRequest $request, ActionResponse $response) // Rethrow as the SecurityEntryPoint middleware will take care of the rest throw $exception->attachInterceptedRequest($request); } catch (AccessDeniedException $exception) { - /** @var PsrLoggerFactoryInterface $securityLogger */ + /** @var LoggerInterface $securityLogger */ $securityLogger = $this->objectManager->get(PsrLoggerFactoryInterface::class)->get('securityLogger'); $securityLogger->warning('Access denied', LogEnvironment::fromMethodName(__METHOD__)); throw $exception; diff --git a/Neos.Flow/Classes/Mvc/Routing/Dto/RouteTags.php b/Neos.Flow/Classes/Mvc/Routing/Dto/RouteTags.php index 6853915aca..44a1833079 100644 --- a/Neos.Flow/Classes/Mvc/Routing/Dto/RouteTags.php +++ b/Neos.Flow/Classes/Mvc/Routing/Dto/RouteTags.php @@ -71,7 +71,9 @@ public static function createFromTag(string $tag): self */ public static function createFromArray(array $tags): self { - array_walk($tags, 'static::validateTag'); + foreach ($tags as $tag) { + self::validateTag($tag); + } return new static($tags); } diff --git a/Neos.Flow/Classes/ObjectManagement/DependencyInjection/ProxyClassBuilder.php b/Neos.Flow/Classes/ObjectManagement/DependencyInjection/ProxyClassBuilder.php index 848fca8471..4e5dec36e2 100644 --- a/Neos.Flow/Classes/ObjectManagement/DependencyInjection/ProxyClassBuilder.php +++ b/Neos.Flow/Classes/ObjectManagement/DependencyInjection/ProxyClassBuilder.php @@ -291,7 +291,7 @@ protected function buildConstructorInjectionCode(Configuration $objectConfigurat } else { if (strpos($argumentValue, '.') !== false) { $settingPath = explode('.', $argumentValue); - $settings = Arrays::getValueByPath($this->configurationManager->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_SETTINGS), array_shift($settingPath)); + $settings = $this->configurationManager->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_SETTINGS, array_shift($settingPath)); $argumentValue = Arrays::getValueByPath($settings, $settingPath); } if (!isset($this->objectConfigurations[$argumentValue])) { @@ -629,7 +629,7 @@ protected function buildMethodParametersCode(array $argumentConfigurations) } else { if (strpos($argumentValue, '.') !== false) { $settingPath = explode('.', $argumentValue); - $settings = Arrays::getValueByPath($this->configurationManager->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_SETTINGS), array_shift($settingPath)); + $settings = $this->configurationManager->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_SETTINGS, array_shift($settingPath)); $argumentValue = Arrays::getValueByPath($settings, $settingPath); } $preparedArguments[] = '\Neos\Flow\Core\Bootstrap::$staticObjectManager->get(\'' . $argumentValue . '\')'; diff --git a/Neos.Flow/Classes/Persistence/Doctrine/ObjectValidationAndDeDuplicationListener.php b/Neos.Flow/Classes/Persistence/Doctrine/ObjectValidationAndDeDuplicationListener.php index 3e9f7a746c..9b6eb1c186 100644 --- a/Neos.Flow/Classes/Persistence/Doctrine/ObjectValidationAndDeDuplicationListener.php +++ b/Neos.Flow/Classes/Persistence/Doctrine/ObjectValidationAndDeDuplicationListener.php @@ -1,4 +1,5 @@ getObjectManager(); + $unitOfWork = $entityManager->getUnitOfWork(); + $objectToPersist = $eventArgs->getObject(); + + $classMetadata = $entityManager->getClassMetadata(get_class($objectToPersist)); + $className = $classMetadata->rootEntityName; + + $classSchema = $this->reflectionService->getClassSchema($className); + $identityMapOfClassName = $unitOfWork->getIdentityMap()[$className] ?? []; + + if ($classSchema !== null && $classSchema->getModelType() === ClassSchema::MODELTYPE_VALUEOBJECT) { + foreach ($identityMapOfClassName as $objectInIdentityMap) { + if ($this->persistenceManager->getIdentifierByObject($objectInIdentityMap) === $this->persistenceManager->getIdentifierByObject($objectToPersist)) { + $unitOfWork->removeFromIdentityMap($objectInIdentityMap); + } + } + } + } + /** * An onFlush event listener used to act upon persistence. * diff --git a/Neos.Flow/Configuration/Settings.Persistence.yaml b/Neos.Flow/Configuration/Settings.Persistence.yaml index 2b3766c60c..4bf261f976 100644 --- a/Neos.Flow/Configuration/Settings.Persistence.yaml +++ b/Neos.Flow/Configuration/Settings.Persistence.yaml @@ -44,7 +44,7 @@ Neos: events: ['onFlush'] listener: 'Neos\Flow\Persistence\Doctrine\AllowedObjectsListener' 'Neos\Flow\Persistence\Doctrine\ObjectValidationAndDeDuplicationListener': - events: ['onFlush'] + events: ['onFlush', 'prePersist'] listener: 'Neos\Flow\Persistence\Doctrine\ObjectValidationAndDeDuplicationListener' # Doctrine ORM Second Level Cache configuration. diff --git a/Neos.Flow/Tests/Unit/Core/Booting/ScriptsTest.php b/Neos.Flow/Tests/Unit/Core/Booting/ScriptsTest.php index 2db985361e..3088f3fe4a 100644 --- a/Neos.Flow/Tests/Unit/Core/Booting/ScriptsTest.php +++ b/Neos.Flow/Tests/Unit/Core/Booting/ScriptsTest.php @@ -37,6 +37,8 @@ protected static function ensureWebSubrequestsUseCurrentlyRunningPhpVersion($php public static function buildSubprocessCommand(string $commandIdentifier, array $settings, array $commandArguments = []): string { + // clear cache for testing + static::$builtPhpCommand = null; return parent::buildSubprocessCommand($commandIdentifier, $settings, $commandArguments); } } diff --git a/Neos.Utility.Arrays/Classes/Arrays.php b/Neos.Utility.Arrays/Classes/Arrays.php index d09777dba4..984c5a4b3a 100644 --- a/Neos.Utility.Arrays/Classes/Arrays.php +++ b/Neos.Utility.Arrays/Classes/Arrays.php @@ -194,12 +194,12 @@ public static function array_reduce(array $array, string $function, $initial = n /** * Returns the value of a nested array by following the specifed path. * - * @param array &$array The array to traverse as a reference + * @param array $array The array to traverse * @param array|string $path The path to follow. Either a simple array of keys or a string in the format 'foo.bar.baz' * @return mixed The value found, NULL if the path didn't exist (note there is no way to distinguish between a found NULL value and "path not found") * @throws \InvalidArgumentException */ - public static function getValueByPath(array &$array, $path) + public static function getValueByPath(array $array, $path) { if (is_string($path)) { $path = explode('.', $path); diff --git a/Neos.Utility.Pdo/Classes/PdoHelper.php b/Neos.Utility.Pdo/Classes/PdoHelper.php index 4836bb8854..873ef34621 100644 --- a/Neos.Utility.Pdo/Classes/PdoHelper.php +++ b/Neos.Utility.Pdo/Classes/PdoHelper.php @@ -33,9 +33,10 @@ abstract class PdoHelper * @param \PDO $databaseHandle * @param string $pdoDriver * @param string $pathAndFilename + * @param array $replacePairs every key in this array will be replaced with the corresponding value in the loaded SQL (example: ['###CACHE_TABLE_NAME###' => 'caches', '###TAGS_TABLE_NAME###' => 'tags']) * @return void */ - public static function importSql(\PDO $databaseHandle, string $pdoDriver, string $pathAndFilename) + public static function importSql(\PDO $databaseHandle, string $pdoDriver, string $pathAndFilename, array $replacePairs = []) { $path = dirname($pathAndFilename); $filename = basename($pathAndFilename); @@ -52,7 +53,7 @@ public static function importSql(\PDO $databaseHandle, string $pdoDriver, string $statement = ''; foreach ($sql as $line) { - $statement .= ' ' . trim($line); + $statement .= ' ' . trim(strtr($line, $replacePairs)); if (substr($statement, -1) === ';') { $databaseHandle->exec($statement); $statement = ''; diff --git a/composer.json b/composer.json index 6967905ac3..d372ceeaee 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ "psr/http-server-handler": "^1.0", "psr/http-client": "^1.0", "ramsey/uuid": "^3.0 || ^4.0", - "doctrine/orm": "^2.9.3 <2.16.0", + "doctrine/orm": "^2.9.3", "doctrine/migrations": "^3.0", "doctrine/dbal": "^2.13", "doctrine/common": "^3.0.3",