diff --git a/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php b/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php index 40eb4978b1b3c..c2cc91714cabd 100644 --- a/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php @@ -37,7 +37,7 @@ abstract class AbstractAdapter implements AdapterInterface, CacheInterface, Logg protected function __construct(string $namespace = '', int $defaultLifetime = 0) { - $this->namespace = '' === $namespace ? '' : CacheItem::validateKey($namespace).static::NS_SEPARATOR; + $this->namespace = '' === $namespace ? '' : CacheItem::validateKey($namespace, static::NS_SEPARATOR).static::NS_SEPARATOR; $this->defaultLifetime = $defaultLifetime; if (null !== $this->maxIdLength && \strlen($namespace) > $this->maxIdLength - 24) { throw new InvalidArgumentException(sprintf('Namespace must be %d chars max, %d given ("%s").', $this->maxIdLength - 24, \strlen($namespace), $namespace)); diff --git a/src/Symfony/Component/Cache/Adapter/AbstractTagAwareAdapter.php b/src/Symfony/Component/Cache/Adapter/AbstractTagAwareAdapter.php index ef62b4fb21c7f..34e646cb1b4ab 100644 --- a/src/Symfony/Component/Cache/Adapter/AbstractTagAwareAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/AbstractTagAwareAdapter.php @@ -37,9 +37,14 @@ abstract class AbstractTagAwareAdapter implements TagAwareAdapterInterface, TagA private const TAGS_PREFIX = "\1tags\1"; + /** + * @internal + */ + protected const NS_SEPARATOR = ':'; + protected function __construct(string $namespace = '', int $defaultLifetime = 0) { - $this->namespace = '' === $namespace ? '' : CacheItem::validateKey($namespace).':'; + $this->namespace = '' === $namespace ? '' : CacheItem::validateKey($namespace, static::NS_SEPARATOR).static::NS_SEPARATOR; $this->defaultLifetime = $defaultLifetime; if (null !== $this->maxIdLength && \strlen($namespace) > $this->maxIdLength - 24) { throw new InvalidArgumentException(sprintf('Namespace must be %d chars max, %d given ("%s").', $this->maxIdLength - 24, \strlen($namespace), $namespace)); diff --git a/src/Symfony/Component/Cache/Adapter/ArrayAdapter.php b/src/Symfony/Component/Cache/Adapter/ArrayAdapter.php index 319dc0487b3e9..54c12aac6d755 100644 --- a/src/Symfony/Component/Cache/Adapter/ArrayAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/ArrayAdapter.php @@ -30,6 +30,11 @@ class ArrayAdapter implements AdapterInterface, CacheInterface, LoggerAwareInter { use LoggerAwareTrait; + /** + * @internal + */ + protected const NS_SEPARATOR = ':'; + private bool $storeSerialized; private array $values = []; private array $tags = []; @@ -108,7 +113,7 @@ public function hasItem(mixed $key): bool return true; } - \assert('' !== CacheItem::validateKey($key)); + \assert('' !== CacheItem::validateKey($key, static::NS_SEPARATOR)); return isset($this->expiries[$key]) && !$this->deleteItem($key); } @@ -138,7 +143,7 @@ public function getItems(array $keys = []): iterable public function deleteItem(mixed $key): bool { - \assert('' !== CacheItem::validateKey($key)); + \assert('' !== CacheItem::validateKey($key, static::NS_SEPARATOR)); unset($this->values[$key], $this->tags[$key], $this->expiries[$key]); return true; @@ -357,7 +362,7 @@ private function validateKeys(array $keys): bool { foreach ($keys as $key) { if (!\is_string($key) || !isset($this->expiries[$key])) { - CacheItem::validateKey($key); + CacheItem::validateKey($key, static::NS_SEPARATOR); } } diff --git a/src/Symfony/Component/Cache/CacheItem.php b/src/Symfony/Component/Cache/CacheItem.php index 1a81706da9c07..5e8ff74453452 100644 --- a/src/Symfony/Component/Cache/CacheItem.php +++ b/src/Symfony/Component/Cache/CacheItem.php @@ -127,7 +127,7 @@ public function getMetadata(): array * * @throws InvalidArgumentException When $key is not valid */ - public static function validateKey($key): string + public static function validateKey($key, string $allowChars = null): string { if (!\is_string($key)) { throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', get_debug_type($key))); @@ -135,8 +135,9 @@ public static function validateKey($key): string if ('' === $key) { throw new InvalidArgumentException('Cache key length must be greater than zero.'); } - if (false !== strpbrk($key, self::RESERVED_CHARACTERS)) { - throw new InvalidArgumentException(sprintf('Cache key "%s" contains reserved characters "%s".', $key, self::RESERVED_CHARACTERS)); + $reservedChars = null === $allowChars ? self::RESERVED_CHARACTERS : str_replace(str_split($allowChars), '', self::RESERVED_CHARACTERS); + if ('' !== $reservedChars && false !== strpbrk($key, $reservedChars)) { + throw new InvalidArgumentException(sprintf('Cache key "%s" contains reserved characters "%s".', $key, $reservedChars)); } return $key; diff --git a/src/Symfony/Component/Cache/Tests/Adapter/AdapterTestCase.php b/src/Symfony/Component/Cache/Tests/Adapter/AdapterTestCase.php index fa02c7708d3a9..7bd3ac033dcc5 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/AdapterTestCase.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/AdapterTestCase.php @@ -21,6 +21,8 @@ abstract class AdapterTestCase extends CachePoolTest { + protected static ?string $allowPsr6Keys = ':'; + protected function setUp(): void { parent::setUp(); @@ -40,6 +42,16 @@ protected function setUp(): void } } + public static function invalidKeys(): array + { + $keys = parent::invalidKeys(); + if (null !== static::$allowPsr6Keys) { + $keys = array_filter($keys, fn ($key) => !\is_string($key[0] ?? null) || false === strpbrk($key[0], static::$allowPsr6Keys)); + } + + return $keys; + } + public function testGet() { if (isset($this->skippedTests[__FUNCTION__])) { diff --git a/src/Symfony/Component/Cache/Tests/Adapter/Psr16AdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/Psr16AdapterTest.php index bdd5d04c564b6..1f646515e9011 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/Psr16AdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/Psr16AdapterTest.php @@ -22,6 +22,8 @@ */ class Psr16AdapterTest extends AdapterTestCase { + protected static ?string $allowPsr6Keys = null; + protected $skippedTests = [ 'testPrune' => 'Psr16adapter just proxies', 'testClearPrefix' => 'SimpleCache cannot clear by prefix', diff --git a/src/Symfony/Component/Cache/Tests/Psr16CacheTest.php b/src/Symfony/Component/Cache/Tests/Psr16CacheTest.php index 264679aecc1b6..9d30fa0630fd0 100644 --- a/src/Symfony/Component/Cache/Tests/Psr16CacheTest.php +++ b/src/Symfony/Component/Cache/Tests/Psr16CacheTest.php @@ -57,7 +57,7 @@ protected function setUp(): void public function createSimpleCache(int $defaultLifetime = 0): CacheInterface { - return new Psr16Cache(new FilesystemAdapter('', $defaultLifetime)); + return new Psr16Cache(new FilesystemTestAdapter('', $defaultLifetime)); } public static function validKeys(): array @@ -181,3 +181,8 @@ public function __wakeup() throw new \Exception(__CLASS__); } } + +class FilesystemTestAdapter extends FilesystemAdapter +{ + protected const NS_SEPARATOR = '_'; +} diff --git a/src/Symfony/Component/Cache/Traits/AbstractAdapterTrait.php b/src/Symfony/Component/Cache/Traits/AbstractAdapterTrait.php index 4ab2537db94ca..3b5870a909dfb 100644 --- a/src/Symfony/Component/Cache/Traits/AbstractAdapterTrait.php +++ b/src/Symfony/Component/Cache/Traits/AbstractAdapterTrait.php @@ -346,7 +346,7 @@ protected function getId(mixed $key): string if (\is_string($key) && isset($this->ids[$key])) { return $this->namespace.$this->namespaceVersion.$this->ids[$key]; } - \assert('' !== CacheItem::validateKey($key)); + \assert('' !== CacheItem::validateKey($key, static::NS_SEPARATOR)); $this->ids[$key] = $key; if (\count($this->ids) > 1000) {