Skip to content

Commit

Permalink
Merge pull request #177 from boesing/feature/psr-6-maximum-cache-key-…
Browse files Browse the repository at this point in the history
…length-validation

PSR-6 maximum cache key length validation
  • Loading branch information
boesing authored Nov 13, 2021
2 parents acd7f65 + 6a367b5 commit a9055df
Show file tree
Hide file tree
Showing 10 changed files with 241 additions and 150 deletions.
5 changes: 5 additions & 0 deletions docs/book/v3/reference/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down
9 changes: 0 additions & 9 deletions psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -214,12 +214,6 @@
<code>is_object($ttl)</code>
<code>null === $ttl</code>
</DocblockTypeContradiction>
<InvalidReturnStatement occurrences="1">
<code>new $exceptionClass($throwable-&gt;getMessage(), $throwable-&gt;getCode(), $throwable)</code>
</InvalidReturnStatement>
<InvalidReturnType occurrences="1">
<code>SimpleCacheException</code>
</InvalidReturnType>
<MixedArgument occurrences="3">
<code>$key</code>
<code>$key</code>
Expand Down Expand Up @@ -849,9 +843,6 @@
<code>$key</code>
<code>$key</code>
</MixedArgument>
<MixedArgumentTypeCoercion occurrences="1">
<code>$capabilities ?? $this-&gt;defaultCapabilities</code>
</MixedArgumentTypeCoercion>
<MixedAssignment occurrences="3">
<code>$item</code>
<code>$item</code>
Expand Down
8 changes: 8 additions & 0 deletions src/Psr/CacheItemPool/CacheItemPoolDecorator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -37,6 +38,7 @@
*/
class CacheItemPoolDecorator implements CacheItemPoolInterface
{
use MaximumKeyLengthTrait;
use SerializationTrait;

/** @var StorageInterface */
Expand All @@ -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;
}

Expand Down Expand Up @@ -361,6 +365,10 @@ private function validateKey($key)
is_string($key) ? $key : gettype($key)
));
}

if ($this->exceedsMaximumKeyLength($key)) {
throw InvalidArgumentException::maximumKeyLengthExceeded($key, $this->maximumKeyLength);
}
}

/**
Expand Down
10 changes: 10 additions & 0 deletions src/Psr/CacheItemPool/InvalidArgumentException.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
));
}
}
70 changes: 70 additions & 0 deletions src/Psr/MaximumKeyLengthTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

declare(strict_types=1);

namespace Laminas\Cache\Psr;

use Laminas\Cache\Psr\SimpleCache\SimpleCacheInvalidArgumentException;
use Laminas\Cache\Storage\Capabilities;
use Laminas\Cache\Storage\StorageInterface;

use function get_class;
use function min;
use function preg_match;
use function sprintf;

/**
* Provides memoizing of maximum key length for a storage adapter.
*
* @internal
*/
trait MaximumKeyLengthTrait
{
/**
* PCRE runs into a compilation error if the quantifier exceeds this limit
*
* @internal
*
* @readonly
* @var positive-int
*/
public static $pcreMaximumQuantifierLength = 65535;

/**
* @var int
* @psalm-var 0|positive-int
*/
private $maximumKeyLength;

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-6/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::$pcreMaximumQuantifierLength - 1);
}

private function exceedsMaximumKeyLength(string $key): bool
{
return $this->maximumKeyLength !== Capabilities::UNLIMITED_KEY_LENGTH
&& preg_match('/^.{' . ($this->maximumKeyLength + 1) . ',}/u', $key);
}
}
2 changes: 2 additions & 0 deletions src/Psr/SerializationTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
/**
* Provides common functionality surrounding value de/serialization as required
* by both PSR-6 and PSR-16
*
* @internal
*/
trait SerializationTrait
{
Expand Down
53 changes: 4 additions & 49 deletions src/Psr/SimpleCache/SimpleCacheDecorator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -34,20 +34,14 @@
*/
class SimpleCacheDecorator implements SimpleCacheInterface
{
use MaximumKeyLengthTrait;
use SerializationTrait;

/**
* Characters reserved by PSR-16 that are not valid in cache keys.
*/
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;

Expand All @@ -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)) {
Expand Down Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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<int|string,mixed>
Expand Down
10 changes: 10 additions & 0 deletions src/Psr/SimpleCache/SimpleCacheInvalidArgumentException.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
));
}
}
Loading

0 comments on commit a9055df

Please sign in to comment.