From 9da751822701d4beeac96bf26d9f205224dc8eda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Sat, 13 Nov 2021 14:51:09 +0100 Subject: [PATCH 1/7] refactor: mark `SerializationTrait` as `internal` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- src/Psr/SerializationTrait.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Psr/SerializationTrait.php b/src/Psr/SerializationTrait.php index fbdb3189..52652eef 100644 --- a/src/Psr/SerializationTrait.php +++ b/src/Psr/SerializationTrait.php @@ -9,6 +9,8 @@ /** * Provides common functionality surrounding value de/serialization as required * by both PSR-6 and PSR-16 + * + * @internal */ trait SerializationTrait { From a7f162092c2f130345cde559b0e6fcb085e05c55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Sat, 13 Nov 2021 14:51:40 +0100 Subject: [PATCH 2/7] refactor: extract maximum key length functionality into dedicated trait MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- src/Psr/MaximumKeyLengthTrait.php | 69 +++++++++++++++++++ src/Psr/SimpleCache/SimpleCacheDecorator.php | 53 ++------------ .../SimpleCacheInvalidArgumentException.php | 10 +++ .../SimpleCache/SimpleCacheDecoratorTest.php | 8 +-- 4 files changed, 87 insertions(+), 53 deletions(-) create mode 100644 src/Psr/MaximumKeyLengthTrait.php diff --git a/src/Psr/MaximumKeyLengthTrait.php b/src/Psr/MaximumKeyLengthTrait.php new file mode 100644 index 00000000..52c2b2f5 --- /dev/null +++ b/src/Psr/MaximumKeyLengthTrait.php @@ -0,0 +1,69 @@ +getMaxKeyLength(); + + if ($maximumKeyLength === Capabilities::UNLIMITED_KEY_LENGTH) { + $this->maximumKeyLength = Capabilities::UNLIMITED_KEY_LENGTH; + return; + } + + if ($maximumKeyLength === Capabilities::UNKNOWN_KEY_LENGTH) { + // For backward compatibility, assume adapters which do not provide a maximum key length do support 64 chars + $maximumKeyLength = 64; + } + + if ($maximumKeyLength < 64) { + throw new SimpleCacheInvalidArgumentException(sprintf( + 'The storage adapter "%s" does not fulfill the minimum requirements for PSR-16:' + . ' The maximum key length capability must allow at least 64 characters.', + get_class($storage) + )); + } + + /** @psalm-suppress PropertyTypeCoercion The result of this will always be > 0 */ + $this->maximumKeyLength = min($maximumKeyLength, self::$PCRE_MAXIMUM_QUANTIFIER_LENGTH - 1); + } + + private function exceedsMaximumKeyLength(string $key): bool + { + return $this->maximumKeyLength !== Capabilities::UNLIMITED_KEY_LENGTH + && preg_match('/^.{' . ($this->maximumKeyLength + 1) . ',}/u', $key); + } +} diff --git a/src/Psr/SimpleCache/SimpleCacheDecorator.php b/src/Psr/SimpleCache/SimpleCacheDecorator.php index 786cce8a..a73cb2a1 100644 --- a/src/Psr/SimpleCache/SimpleCacheDecorator.php +++ b/src/Psr/SimpleCache/SimpleCacheDecorator.php @@ -6,6 +6,7 @@ use DateTimeImmutable; use DateTimeZone; use Laminas\Cache\Exception\InvalidArgumentException as LaminasCacheInvalidArgumentException; +use Laminas\Cache\Psr\MaximumKeyLengthTrait; use Laminas\Cache\Psr\SerializationTrait; use Laminas\Cache\Storage\Capabilities; use Laminas\Cache\Storage\ClearByNamespaceInterface; @@ -23,7 +24,6 @@ use function is_int; use function is_object; use function is_string; -use function min; use function preg_match; use function preg_quote; use function sprintf; @@ -34,6 +34,7 @@ */ class SimpleCacheDecorator implements SimpleCacheInterface { + use MaximumKeyLengthTrait; use SerializationTrait; /** @@ -41,13 +42,6 @@ class SimpleCacheDecorator implements SimpleCacheInterface */ public const INVALID_KEY_CHARS = ':@{}()/\\'; - /** - * PCRE runs into a compilation error if the quantifier exceeds this limit - * - * @internal - */ - public const PCRE_MAXIMUM_QUANTIFIER_LENGTH = 65535; - /** @var bool */ private $providesPerItemTtl = true; @@ -65,12 +59,6 @@ class SimpleCacheDecorator implements SimpleCacheInterface /** @var DateTimeZone */ private $utc; - /** - * @var int - * @psalm-var 0|positive-int - */ - private $maximumKeyLength; - public function __construct(StorageInterface $storage) { if ($this->isSerializationRequired($storage)) { @@ -374,15 +362,8 @@ private function validateKey($key): void )); } - if ( - $this->maximumKeyLength !== Capabilities::UNLIMITED_KEY_LENGTH - && preg_match('/^.{' . ($this->maximumKeyLength + 1) . ',}/u', $key) - ) { - throw new SimpleCacheInvalidArgumentException(sprintf( - 'Invalid key "%s" provided; key is too long. Must be no more than %d characters', - $key, - $this->maximumKeyLength - )); + if ($this->exceedsMaximumKeyLength($key)) { + throw SimpleCacheInvalidArgumentException::maximumKeyLengthExceeded($key, $this->maximumKeyLength); } } @@ -459,32 +440,6 @@ private function convertIterableKeysToList(iterable $keys): array return $array; } - private function memoizeMaximumKeyLengthCapability(StorageInterface $storage, Capabilities $capabilities): void - { - $maximumKeyLength = $capabilities->getMaxKeyLength(); - - if ($maximumKeyLength === Capabilities::UNLIMITED_KEY_LENGTH) { - $this->maximumKeyLength = Capabilities::UNLIMITED_KEY_LENGTH; - return; - } - - if ($maximumKeyLength === Capabilities::UNKNOWN_KEY_LENGTH) { - // For backward compatibility, assume adapters which do not provide a maximum key length do support 64 chars - $maximumKeyLength = 64; - } - - if ($maximumKeyLength < 64) { - throw new SimpleCacheInvalidArgumentException(sprintf( - 'The storage adapter "%s" does not fulfill the minimum requirements for PSR-16:' - . ' The maximum key length capability must allow at least 64 characters.', - get_class($storage) - )); - } - - /** @psalm-suppress PropertyTypeCoercion The result of this will always be > 0 */ - $this->maximumKeyLength = min($maximumKeyLength, self::PCRE_MAXIMUM_QUANTIFIER_LENGTH - 1); - } - /** * @param iterable $values * @psalm-return array diff --git a/src/Psr/SimpleCache/SimpleCacheInvalidArgumentException.php b/src/Psr/SimpleCache/SimpleCacheInvalidArgumentException.php index 75b4f4c2..9ca1fd87 100644 --- a/src/Psr/SimpleCache/SimpleCacheInvalidArgumentException.php +++ b/src/Psr/SimpleCache/SimpleCacheInvalidArgumentException.php @@ -5,6 +5,16 @@ use InvalidArgumentException; use Psr\SimpleCache\InvalidArgumentException as PsrInvalidArgumentException; +use function sprintf; + class SimpleCacheInvalidArgumentException extends InvalidArgumentException implements PsrInvalidArgumentException { + public static function maximumKeyLengthExceeded(string $key, int $maximumKeyLength): self + { + return new self(sprintf( + 'Invalid key "%s" provided; key is too long. Must be no more than %d characters', + $key, + $maximumKeyLength + )); + } } diff --git a/test/Psr/SimpleCache/SimpleCacheDecoratorTest.php b/test/Psr/SimpleCache/SimpleCacheDecoratorTest.php index 31b09f93..0ac602cf 100644 --- a/test/Psr/SimpleCache/SimpleCacheDecoratorTest.php +++ b/test/Psr/SimpleCache/SimpleCacheDecoratorTest.php @@ -1139,7 +1139,7 @@ public function testWillUsePcreMaximumQuantifierLengthIfAdapterAllowsMoreThanTha null, true, 60, - SimpleCacheDecorator::PCRE_MAXIMUM_QUANTIFIER_LENGTH + SimpleCacheDecorator::$PCRE_MAXIMUM_QUANTIFIER_LENGTH ); $storage @@ -1147,11 +1147,11 @@ public function testWillUsePcreMaximumQuantifierLengthIfAdapterAllowsMoreThanTha ->willReturn($capabilities); $decorator = new SimpleCacheDecorator($storage); - $key = str_repeat('a', SimpleCacheDecorator::PCRE_MAXIMUM_QUANTIFIER_LENGTH); + $key = str_repeat('a', SimpleCacheDecorator::$PCRE_MAXIMUM_QUANTIFIER_LENGTH); $this->expectException(SimpleCacheInvalidArgumentException::class); $this->expectExceptionMessage(sprintf( 'key is too long. Must be no more than %d characters', - SimpleCacheDecorator::PCRE_MAXIMUM_QUANTIFIER_LENGTH - 1 + SimpleCacheDecorator::$PCRE_MAXIMUM_QUANTIFIER_LENGTH - 1 )); $decorator->has($key); } @@ -1163,7 +1163,7 @@ public function testPcreMaximumQuantifierLengthWontResultInCompilationError(): v preg_match( sprintf( '/^.{%d,}$/', - SimpleCacheDecorator::PCRE_MAXIMUM_QUANTIFIER_LENGTH + SimpleCacheDecorator::$PCRE_MAXIMUM_QUANTIFIER_LENGTH ), '' ) From baf90d8bfee55ce6c7b4cbfcd66bf7973aa872d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Sat, 13 Nov 2021 15:21:15 +0100 Subject: [PATCH 3/7] feature: introduce maximum key length verification for `PSR-6` cache item pool decorator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- psalm-baseline.xml | 9 - .../CacheItemPool/CacheItemPoolDecorator.php | 8 + .../InvalidArgumentException.php | 10 + src/Psr/MaximumKeyLengthTrait.php | 1 + .../CacheItemPoolDecoratorTest.php | 213 +++++++++++------- 5 files changed, 148 insertions(+), 93 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 1e925e34..d2471f2a 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -214,12 +214,6 @@ is_object($ttl) null === $ttl - - new $exceptionClass($throwable->getMessage(), $throwable->getCode(), $throwable) - - - SimpleCacheException - $key $key @@ -849,9 +843,6 @@ $key $key - - $capabilities ?? $this->defaultCapabilities - $item $item diff --git a/src/Psr/CacheItemPool/CacheItemPoolDecorator.php b/src/Psr/CacheItemPool/CacheItemPoolDecorator.php index 698da51f..639a4b0a 100644 --- a/src/Psr/CacheItemPool/CacheItemPoolDecorator.php +++ b/src/Psr/CacheItemPool/CacheItemPoolDecorator.php @@ -3,6 +3,7 @@ namespace Laminas\Cache\Psr\CacheItemPool; use Laminas\Cache\Exception; +use Laminas\Cache\Psr\MaximumKeyLengthTrait; use Laminas\Cache\Psr\SerializationTrait; use Laminas\Cache\Storage\ClearByNamespaceInterface; use Laminas\Cache\Storage\FlushableInterface; @@ -37,6 +38,7 @@ */ class CacheItemPoolDecorator implements CacheItemPoolInterface { + use MaximumKeyLengthTrait; use SerializationTrait; /** @var StorageInterface */ @@ -55,6 +57,8 @@ class CacheItemPoolDecorator implements CacheItemPoolInterface public function __construct(StorageInterface $storage) { $this->validateStorage($storage); + $capabilities = $storage->getCapabilities(); + $this->memoizeMaximumKeyLengthCapability($storage, $capabilities); $this->storage = $storage; } @@ -361,6 +365,10 @@ private function validateKey($key) is_string($key) ? $key : gettype($key) )); } + + if ($this->exceedsMaximumKeyLength($key)) { + throw InvalidArgumentException::maximumKeyLengthExceeded($key, $this->maximumKeyLength); + } } /** diff --git a/src/Psr/CacheItemPool/InvalidArgumentException.php b/src/Psr/CacheItemPool/InvalidArgumentException.php index 8711f4ef..2f958a0a 100644 --- a/src/Psr/CacheItemPool/InvalidArgumentException.php +++ b/src/Psr/CacheItemPool/InvalidArgumentException.php @@ -4,6 +4,16 @@ use Psr\Cache\InvalidArgumentException as InvalidArgumentExceptionInterface; +use function sprintf; + class InvalidArgumentException extends \InvalidArgumentException implements InvalidArgumentExceptionInterface { + public static function maximumKeyLengthExceeded(string $key, int $maximumKeyLength): self + { + return new self(sprintf( + 'Invalid key "%s" provided; key is too long. Must be no more than %d characters', + $key, + $maximumKeyLength + )); + } } diff --git a/src/Psr/MaximumKeyLengthTrait.php b/src/Psr/MaximumKeyLengthTrait.php index 52c2b2f5..ef5f161d 100644 --- a/src/Psr/MaximumKeyLengthTrait.php +++ b/src/Psr/MaximumKeyLengthTrait.php @@ -26,6 +26,7 @@ trait MaximumKeyLengthTrait * @internal * * @readonly + * @var positive-int */ public static $PCRE_MAXIMUM_QUANTIFIER_LENGTH = 65535; diff --git a/test/Psr/CacheItemPool/CacheItemPoolDecoratorTest.php b/test/Psr/CacheItemPool/CacheItemPoolDecoratorTest.php index b1c8135b..ad4d200d 100644 --- a/test/Psr/CacheItemPool/CacheItemPoolDecoratorTest.php +++ b/test/Psr/CacheItemPool/CacheItemPoolDecoratorTest.php @@ -8,6 +8,7 @@ use Laminas\Cache\Psr\CacheItemPool\CacheItem; use Laminas\Cache\Psr\CacheItemPool\CacheItemPoolDecorator; use Laminas\Cache\Psr\CacheItemPool\InvalidArgumentException; +use Laminas\Cache\Psr\SimpleCache\SimpleCacheDecorator; use Laminas\Cache\Storage\Adapter\AdapterOptions; use Laminas\Cache\Storage\Capabilities; use Laminas\Cache\Storage\ClearByNamespaceInterface; @@ -24,49 +25,53 @@ use function array_keys; use function array_map; -use function assert; +use function preg_match; +use function sprintf; +use function str_repeat; use function time; final class CacheItemPoolDecoratorTest extends TestCase { - /** @var array */ - protected $defaultCapabilities = [ - 'staticTtl' => true, - 'minTtl' => 1, - 'supportedDatatypes' => [ - 'NULL' => true, - 'boolean' => true, - 'integer' => true, - 'double' => true, - 'string' => true, - 'array' => true, - 'object' => 'object', - 'resource' => false, - ], - ]; - /** @var StorageInterface&FlushableInterface&MockObject */ private $storage; /** @var CacheItemPoolDecorator */ private $adapter; + /** @var array */ + private $requiredTypes = [ + 'NULL' => true, + 'boolean' => true, + 'integer' => true, + 'double' => true, + 'string' => true, + 'array' => true, + 'object' => 'object', + 'resource' => false, + ]; + + /** @var AdapterOptions&MockObject */ + private $options; + protected function setUp(): void { parent::setUp(); + $this->options = $this->createMock(AdapterOptions::class); $this->storage = $this->createMockedStorage(); $this->adapter = $this->getAdapter($this->storage); } - /** @var (AdapterOptions&MockObject)|null */ - private $optionsMock; - /** * @return StorageInterface&FlushableInterface&ClearByNamespaceInterface&MockObject */ private function createMockedStorage( - ?array $capabilities = null, - ?array $options = null + ?AdapterOptions $options = null, + ?array $supportedDataTypes = null, + bool $staticTtl = true, + int $minTtl = 1, + int $maxKeyLength = -1, + bool $useRequestTime = false, + bool $lockOnExpire = false ): StorageInterface { $storage = $this->createMock(FlushableNamespaceStorageInterface::class); @@ -74,9 +79,14 @@ private function createMockedStorage( ->method('getEventManager') ->willReturn(new EventManager()); - $capabilities = $this->createCapabilitiesMock( + $capabilities = $this->createCapabilities( $storage, - $capabilities ?? $this->defaultCapabilities + $supportedDataTypes, + $staticTtl, + $minTtl, + $maxKeyLength, + $useRequestTime, + $lockOnExpire ); $storage @@ -85,7 +95,7 @@ private function createMockedStorage( $storage ->method('getOptions') - ->willReturn($this->createOptionsMock($options)); + ->willReturn($options ?? $this->options); return $storage; } @@ -95,7 +105,7 @@ public function testStorageNotFlushableThrowsException(): void $this->expectException(CacheException::class); $storage = $this->createMock(StorageInterface::class); - $capabilities = new Capabilities($storage, new stdClass(), $this->defaultCapabilities); + $capabilities = $this->createCapabilities($storage); $storage ->expects(self::once()) @@ -110,21 +120,16 @@ public function testStorageNeedsSerializerWillThrowException(): void $this->expectException(CacheException::class); $storage = $this->createMock(StorageInterface::class); - $dataTypes = [ - 'staticTtl' => true, - 'minTtl' => 1, - 'supportedDatatypes' => [ - 'NULL' => true, - 'boolean' => true, - 'integer' => true, - 'double' => false, - 'string' => true, - 'array' => true, - 'object' => 'object', - 'resource' => false, - ], - ]; - $capabilities = new Capabilities($storage, new stdClass(), $dataTypes); + $capabilities = $this->createCapabilities($storage, [ + 'NULL' => true, + 'boolean' => true, + 'integer' => true, + 'double' => false, + 'string' => true, + 'array' => true, + 'object' => 'object', + 'resource' => false, + ]); $storage ->expects(self::once()) @@ -137,14 +142,14 @@ public function testStorageNeedsSerializerWillThrowException(): void public function testStorageFalseStaticTtlThrowsException(): void { $this->expectException(CacheException::class); - $storage = $this->createMockedStorage(['staticTtl' => false]); + $storage = $this->createMockedStorage(null, null, false); $this->getAdapter($storage); } public function testStorageZeroMinTtlThrowsException(): void { $this->expectException(CacheException::class); - $storage = $this->createMockedStorage(['staticTtl' => true, 'minTtl' => 0]); + $storage = $this->createMockedStorage(null, null, true, 0); $this->getAdapter($storage); } @@ -342,8 +347,7 @@ public function testSaveItemWithExpiration(): void ->with('foo') ->willReturn(null); - assert($this->optionsMock instanceof MockObject); - $this->optionsMock + $this->options ->expects(self::exactly(2)) ->method('setTtl') ->withConsecutive([3600], [0]) @@ -390,8 +394,7 @@ public function testExpiredItemNotSaved(): void ->with(['foo' => 'bar']) ->willReturn(['foo' => true]); - assert($this->optionsMock instanceof MockObject); - $this->optionsMock + $this->options ->expects(self::exactly(2)) ->method('setTtl') ->with(0) @@ -556,6 +559,11 @@ public function testClearReturnsTrue(): void ->with(['foo' => 'bar']) ->willReturn(['foo' => true]); + $this->options + ->expects(self::once()) + ->method('getNamespace') + ->willReturn('laminascache'); + $adapter = $this->getAdapter($storage); $storage ->expects(self::once()) @@ -571,7 +579,7 @@ public function testClearReturnsTrue(): void public function testClearWithoutNamespaceReturnsTrue(): void { - $storage = $this->createMockedStorage(null, ['namespace' => '']); + $storage = $this->createMockedStorage(new AdapterOptions(['namespace' => ''])); $adapter = $this->getAdapter($storage); $storage ->expects(self::once()) @@ -592,7 +600,7 @@ public function testClearWithoutNamespaceReturnsTrue(): void public function testClearEmptyReturnsTrue(): void { - $storage = $this->createMockedStorage(null, ['namespace' => '']); + $storage = $this->createMockedStorage(new AdapterOptions(['namespace' => ''])); $storage ->expects(self::once()) ->method('flush') @@ -611,11 +619,17 @@ public function testClearDeferred(): void ->method('hasItem') ->willReturn(false); + $this->options + ->expects(self::once()) + ->method('getNamespace') + ->willReturn('bar'); + $item = $adapter->getItem('foo'); $adapter->saveDeferred($item); $this->storage ->expects(self::once()) ->method('clearByNamespace') + ->with('bar') ->willReturn(true); $adapter->clear(); @@ -624,7 +638,7 @@ public function testClearDeferred(): void public function testClearRuntimeExceptionReturnsFalse(): void { - $storage = $this->createMockedStorage(null, ['namespace' => '']); + $storage = $this->createMockedStorage(new AdapterOptions(['namespace' => ''])); $storage ->expects(self::once()) ->method('flush') @@ -634,7 +648,7 @@ public function testClearRuntimeExceptionReturnsFalse(): void public function testClearByNamespaceReturnsTrue(): void { - $storage = $this->createMockedStorage(null, ['namespace' => 'laminascache']); + $storage = $this->createMockedStorage(new AdapterOptions(['namespace' => 'laminascache'])); $storage ->expects(self::once()) ->method('clearByNamespace') @@ -646,7 +660,7 @@ public function testClearByNamespaceReturnsTrue(): void public function testClearByEmptyNamespaceCallsFlush(): void { - $storage = $this->createMockedStorage(null, ['namespace' => '']); + $storage = $this->createMockedStorage(new AdapterOptions(['namespace' => ''])); $storage ->expects(self::once()) ->method('flush') @@ -657,7 +671,7 @@ public function testClearByEmptyNamespaceCallsFlush(): void public function testClearByNamespaceRuntimeExceptionReturnsFalse(): void { - $storage = $this->createMockedStorage(null, ['namespace' => 'laminascache']); + $storage = $this->createMockedStorage(new AdapterOptions(['namespace' => 'laminascache'])); $storage ->expects(self::once()) ->method('clearByNamespace') @@ -870,36 +884,9 @@ private function getAdapter(StorageInterface $storage): CacheItemPoolDecorator return new CacheItemPoolDecorator($storage); } - /** - * @param array $capabilities - * @return Capabilities&MockObject - */ - private function createCapabilitiesMock(StorageInterface $storage, array $capabilities): Capabilities - { - return $this - ->getMockBuilder(Capabilities::class) - ->enableProxyingToOriginalMethods() - ->enableOriginalConstructor() - ->setConstructorArgs([ - $storage, - new stdClass(), - $capabilities, - ])->getMock(); - } - - private function createOptionsMock(?array $options): AdapterOptions + private function createAdapterOptions(?array $options = null): AdapterOptions { - $mock = $this->optionsMock = $this - ->getMockBuilder(AdapterOptions::class) - ->enableProxyingToOriginalMethods() - ->enableOriginalConstructor() - ->getMock(); - - if ($options) { - $mock->setFromArray($options); - } - - return $mock; + return new AdapterOptions($options); } protected function tearDown(): void @@ -956,9 +943,9 @@ public function testWontSaveAlreadyExpiredCacheItemAsDeferredItem(): void { $adapter = $this->createMock(FlushableStorageAdapterInterface::class); $adapter - ->expects(self::exactly(2)) + ->expects(self::atLeast(3)) ->method('getCapabilities') - ->willReturn(new Capabilities($adapter, new stdClass(), $this->defaultCapabilities)); + ->willReturn($this->createCapabilities($adapter)); $adapter ->expects(self::never()) @@ -993,4 +980,62 @@ public function deletionVerificationProvider(): array 'deletion successful due to hasItems states the key does not exist' => [false, true], ]; } + + public function testWillUsePcreMaximumQuantifierLengthIfAdapterAllowsMoreThanThat(): void + { + $storage = $this->createMock(FlushableStorageAdapterInterface::class); + $capabilities = $this->createCapabilities( + $storage, + null, + true, + 60, + SimpleCacheDecorator::$PCRE_MAXIMUM_QUANTIFIER_LENGTH + ); + + $storage + ->method('getCapabilities') + ->willReturn($capabilities); + + $decorator = new CacheItemPoolDecorator($storage); + $key = str_repeat('a', CacheItemPoolDecorator::$PCRE_MAXIMUM_QUANTIFIER_LENGTH); + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage(sprintf( + 'key is too long. Must be no more than %d characters', + CacheItemPoolDecorator::$PCRE_MAXIMUM_QUANTIFIER_LENGTH - 1 + )); + $decorator->getItem($key); + } + + public function testPcreMaximumQuantifierLengthWontResultInCompilationError(): void + { + self::assertEquals( + 0, + preg_match( + sprintf( + '/^.{%d,}$/', + CacheItemPoolDecorator::$PCRE_MAXIMUM_QUANTIFIER_LENGTH + ), + '' + ) + ); + } + + private function createCapabilities( + StorageInterface $storage, + ?array $supportedDataTypes = null, + bool $staticTtl = true, + int $minTtl = 1, + int $maxKeyLength = -1, + bool $useRequestTime = false, + bool $lockOnExpire = false + ): Capabilities { + return new Capabilities($storage, new stdClass(), [ + 'supportedDatatypes' => $supportedDataTypes ?? $this->requiredTypes, + 'staticTtl' => $staticTtl, + 'minTtl' => $minTtl, + 'maxKeyLength' => $maxKeyLength, + 'useRequestTime' => $useRequestTime, + 'lockOnExpire' => $lockOnExpire, + ]); + } } From 3237b5dc7b1b51aeacec013de6e3be1ca8d8d854 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Sat, 13 Nov 2021 15:22:40 +0100 Subject: [PATCH 4/7] qa: provide proper error message so it can be used with both PSR-6 and PSR-16 decorators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- src/Psr/MaximumKeyLengthTrait.php | 2 +- test/Psr/SimpleCache/SimpleCacheDecoratorTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Psr/MaximumKeyLengthTrait.php b/src/Psr/MaximumKeyLengthTrait.php index ef5f161d..21e95298 100644 --- a/src/Psr/MaximumKeyLengthTrait.php +++ b/src/Psr/MaximumKeyLengthTrait.php @@ -52,7 +52,7 @@ private function memoizeMaximumKeyLengthCapability(StorageInterface $storage, Ca if ($maximumKeyLength < 64) { throw new SimpleCacheInvalidArgumentException(sprintf( - 'The storage adapter "%s" does not fulfill the minimum requirements for PSR-16:' + 'The storage adapter "%s" does not fulfill the minimum requirements for PSR-6/PSR-16:' . ' The maximum key length capability must allow at least 64 characters.', get_class($storage) )); diff --git a/test/Psr/SimpleCache/SimpleCacheDecoratorTest.php b/test/Psr/SimpleCache/SimpleCacheDecoratorTest.php index 0ac602cf..cbf43590 100644 --- a/test/Psr/SimpleCache/SimpleCacheDecoratorTest.php +++ b/test/Psr/SimpleCache/SimpleCacheDecoratorTest.php @@ -1127,7 +1127,7 @@ public function testWillThrowExceptionWhenStorageDoesNotFulfillMinimumRequiremen ->willReturn($capabilities); $this->expectException(SimpleCacheInvalidArgumentException::class); - $this->expectExceptionMessage('does not fulfill the minimum requirements for PSR-16'); + $this->expectExceptionMessage('does not fulfill the minimum requirements'); new SimpleCacheDecorator($storage); } From 354d2b856d46a92a976a4b178858af307e600ae7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Sat, 13 Nov 2021 15:31:39 +0100 Subject: [PATCH 5/7] docs: add new `maximum key length` feature to the migration documentation of v3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This also provides breaking changes information which belong to internal code which should not affect upstream projects. Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- docs/book/v3/reference/migration.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/book/v3/reference/migration.md b/docs/book/v3/reference/migration.md index 970f4300..d15ef507 100644 --- a/docs/book/v3/reference/migration.md +++ b/docs/book/v3/reference/migration.md @@ -16,11 +16,13 @@ In the past, most of the adapters Laminas does officially support were not prope 4. Cache adapters which are used within the project needs to be required in at least `^2.0`; in case you don't know which adapters are in use, either check your project configuration or search for the `Laminas\Cache\Storage\Adapter` namespace in your projects source code. Every adapter has to be listed in either your `module.config.php` (laminas-mvc) or `config.php` (mezzio) configuration. 5. Project does not use any of the [removed classes and traits](#removed-classes-and-traits) 6. Storage adapters are not extended in any way as [all adapters are `final`](#breaking-changes) starting with v2.0 of the individual adapter component +7. PSR-6 `CacheItemPoolDecorator` does now validate the maximum key length the same way as PSR-6 `SimpleCacheDecorator` and therefore fulfills the requirements by the underlying PSR. ## New Features - Each cache adapter has its [own package](#satellite-packages). - Support for PHP 8.1 +- PSR-6 `CacheItemPoolDecorator` validates the maximum key length. ## Removed Classes and Traits @@ -39,6 +41,9 @@ With `laminas-cache` v3, some classes/traits were removed as well: **Please note that it is not possible to inject the pattern configuration as an array anymore** - Storage configurations must be in a specific shape. For more details, head to the release notes of [2.12.0](https://github.com/laminas/laminas-cache/releases/tag/2.12.0) - All cache adapters are now marked as `final` and are not extensible anymore. In case that you are extending one of the cache adapters, please switch change your code as `composition` should be preferred over inheritance. For an example, please check out the [composition over inheritance](#composition-over-inheritance) section. +- Due to the enhancement of `CacheItemPoolDecorator`, the maximum key length for the underlying cache adapter is validated before it is passed to the adapter. +- The `SerializationTrait` which was meant to be used by both `PSR-6` and `PSR-16` decorators is now marked as `internal`. +- The `PCRE_MAXIMUM_QUANTIFIER_LENGTH` constant of the `SimpleCacheDecorator` (which was marked as `internal`) has now been moved to the new (also `internal`) `MaximumKeyLengthTrait` and thus had to become a public static property (as traits do not support constants). ## Satellite Packages From 44a34ceda0f7b7eebd70b15f153edeb64da1c546 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Sat, 13 Nov 2021 15:37:33 +0100 Subject: [PATCH 6/7] qa: remove unused code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- test/Psr/CacheItemPool/CacheItemPoolDecoratorTest.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/test/Psr/CacheItemPool/CacheItemPoolDecoratorTest.php b/test/Psr/CacheItemPool/CacheItemPoolDecoratorTest.php index ad4d200d..521fa572 100644 --- a/test/Psr/CacheItemPool/CacheItemPoolDecoratorTest.php +++ b/test/Psr/CacheItemPool/CacheItemPoolDecoratorTest.php @@ -884,11 +884,6 @@ private function getAdapter(StorageInterface $storage): CacheItemPoolDecorator return new CacheItemPoolDecorator($storage); } - private function createAdapterOptions(?array $options = null): AdapterOptions - { - return new AdapterOptions($options); - } - protected function tearDown(): void { try { From 6a367b589e48f76ba063baf776af4dfb0d0853fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Sat, 13 Nov 2021 15:41:30 +0100 Subject: [PATCH 7/7] refactor: convert `PCRE_MAXIMUM_QUANTIFIER_LENGTH` public property to a camel cased property according to codestyle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- src/Psr/MaximumKeyLengthTrait.php | 4 ++-- test/Psr/CacheItemPool/CacheItemPoolDecoratorTest.php | 8 ++++---- test/Psr/SimpleCache/SimpleCacheDecoratorTest.php | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Psr/MaximumKeyLengthTrait.php b/src/Psr/MaximumKeyLengthTrait.php index 21e95298..5ec2be1e 100644 --- a/src/Psr/MaximumKeyLengthTrait.php +++ b/src/Psr/MaximumKeyLengthTrait.php @@ -28,7 +28,7 @@ trait MaximumKeyLengthTrait * @readonly * @var positive-int */ - public static $PCRE_MAXIMUM_QUANTIFIER_LENGTH = 65535; + public static $pcreMaximumQuantifierLength = 65535; /** * @var int @@ -59,7 +59,7 @@ private function memoizeMaximumKeyLengthCapability(StorageInterface $storage, Ca } /** @psalm-suppress PropertyTypeCoercion The result of this will always be > 0 */ - $this->maximumKeyLength = min($maximumKeyLength, self::$PCRE_MAXIMUM_QUANTIFIER_LENGTH - 1); + $this->maximumKeyLength = min($maximumKeyLength, self::$pcreMaximumQuantifierLength - 1); } private function exceedsMaximumKeyLength(string $key): bool diff --git a/test/Psr/CacheItemPool/CacheItemPoolDecoratorTest.php b/test/Psr/CacheItemPool/CacheItemPoolDecoratorTest.php index 521fa572..322bf783 100644 --- a/test/Psr/CacheItemPool/CacheItemPoolDecoratorTest.php +++ b/test/Psr/CacheItemPool/CacheItemPoolDecoratorTest.php @@ -984,7 +984,7 @@ public function testWillUsePcreMaximumQuantifierLengthIfAdapterAllowsMoreThanTha null, true, 60, - SimpleCacheDecorator::$PCRE_MAXIMUM_QUANTIFIER_LENGTH + SimpleCacheDecorator::$pcreMaximumQuantifierLength ); $storage @@ -992,11 +992,11 @@ public function testWillUsePcreMaximumQuantifierLengthIfAdapterAllowsMoreThanTha ->willReturn($capabilities); $decorator = new CacheItemPoolDecorator($storage); - $key = str_repeat('a', CacheItemPoolDecorator::$PCRE_MAXIMUM_QUANTIFIER_LENGTH); + $key = str_repeat('a', CacheItemPoolDecorator::$pcreMaximumQuantifierLength); $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage(sprintf( 'key is too long. Must be no more than %d characters', - CacheItemPoolDecorator::$PCRE_MAXIMUM_QUANTIFIER_LENGTH - 1 + CacheItemPoolDecorator::$pcreMaximumQuantifierLength - 1 )); $decorator->getItem($key); } @@ -1008,7 +1008,7 @@ public function testPcreMaximumQuantifierLengthWontResultInCompilationError(): v preg_match( sprintf( '/^.{%d,}$/', - CacheItemPoolDecorator::$PCRE_MAXIMUM_QUANTIFIER_LENGTH + CacheItemPoolDecorator::$pcreMaximumQuantifierLength ), '' ) diff --git a/test/Psr/SimpleCache/SimpleCacheDecoratorTest.php b/test/Psr/SimpleCache/SimpleCacheDecoratorTest.php index cbf43590..0ccc4e8e 100644 --- a/test/Psr/SimpleCache/SimpleCacheDecoratorTest.php +++ b/test/Psr/SimpleCache/SimpleCacheDecoratorTest.php @@ -1139,7 +1139,7 @@ public function testWillUsePcreMaximumQuantifierLengthIfAdapterAllowsMoreThanTha null, true, 60, - SimpleCacheDecorator::$PCRE_MAXIMUM_QUANTIFIER_LENGTH + SimpleCacheDecorator::$pcreMaximumQuantifierLength ); $storage @@ -1147,11 +1147,11 @@ public function testWillUsePcreMaximumQuantifierLengthIfAdapterAllowsMoreThanTha ->willReturn($capabilities); $decorator = new SimpleCacheDecorator($storage); - $key = str_repeat('a', SimpleCacheDecorator::$PCRE_MAXIMUM_QUANTIFIER_LENGTH); + $key = str_repeat('a', SimpleCacheDecorator::$pcreMaximumQuantifierLength); $this->expectException(SimpleCacheInvalidArgumentException::class); $this->expectExceptionMessage(sprintf( 'key is too long. Must be no more than %d characters', - SimpleCacheDecorator::$PCRE_MAXIMUM_QUANTIFIER_LENGTH - 1 + SimpleCacheDecorator::$pcreMaximumQuantifierLength - 1 )); $decorator->has($key); } @@ -1163,7 +1163,7 @@ public function testPcreMaximumQuantifierLengthWontResultInCompilationError(): v preg_match( sprintf( '/^.{%d,}$/', - SimpleCacheDecorator::$PCRE_MAXIMUM_QUANTIFIER_LENGTH + SimpleCacheDecorator::$pcreMaximumQuantifierLength ), '' )