diff --git a/src/Adapter/Apc/ApcCachePool.php b/src/Adapter/Apc/ApcCachePool.php index 36ad5188..34996427 100644 --- a/src/Adapter/Apc/ApcCachePool.php +++ b/src/Adapter/Apc/ApcCachePool.php @@ -44,12 +44,18 @@ protected function fetchObjectFromCache($key) return [false, null, [], null]; } - $success = false; - $cacheData = apc_fetch($key, $success); + $success = false; + + try { + $cacheData = apc_fetch($key, $success); + } catch (\Error $e) { + throw new \ErrorException($e->getMessage(), $e->getCode(), E_ERROR, $e->getFile(), $e->getLine()); + } + if (!$success) { return [false, null, [], null]; } - list($data, $tags, $timestamp) = unserialize($cacheData); + list($data, $tags, $timestamp) = parent::unserialize($cacheData); return [$success, $data, $tags, $timestamp]; } diff --git a/src/Adapter/Apc/Tests/IntegrationPoolTest.php b/src/Adapter/Apc/Tests/IntegrationPoolTest.php index 274108f3..93708738 100644 --- a/src/Adapter/Apc/Tests/IntegrationPoolTest.php +++ b/src/Adapter/Apc/Tests/IntegrationPoolTest.php @@ -12,10 +12,13 @@ namespace Cache\Adapter\Apc\Tests; use Cache\Adapter\Apc\ApcCachePool; +use Cache\Adapter\Common\Tests\Traits\TestNotUnserializableTrait; use Cache\IntegrationTests\CachePoolTest as BaseTest; class IntegrationPoolTest extends BaseTest { + use TestNotUnserializableTrait; + protected $skippedTests = [ 'testExpiration' => 'The cache expire at the next request.', 'testSaveExpired' => 'The cache expire at the next request.', diff --git a/src/Adapter/Apcu/ApcuCachePool.php b/src/Adapter/Apcu/ApcuCachePool.php index de11f580..424be42d 100644 --- a/src/Adapter/Apcu/ApcuCachePool.php +++ b/src/Adapter/Apcu/ApcuCachePool.php @@ -44,12 +44,18 @@ protected function fetchObjectFromCache($key) return [false, null, [], null]; } - $success = false; - $cacheData = apcu_fetch($key, $success); + $success = false; + + try { + $cacheData = apcu_fetch($key, $success); + } catch (\Error $e) { + throw new \ErrorException($e->getMessage(), $e->getCode(), E_ERROR, $e->getFile(), $e->getLine()); + } + if (!$success) { return [false, null, [], null]; } - list($data, $tags, $timestamp) = unserialize($cacheData); + list($data, $tags, $timestamp) = parent::unserialize($cacheData); return [$success, $data, $tags, $timestamp]; } diff --git a/src/Adapter/Apcu/Tests/IntegrationPoolTest.php b/src/Adapter/Apcu/Tests/IntegrationPoolTest.php index ded34409..1ee4e39e 100644 --- a/src/Adapter/Apcu/Tests/IntegrationPoolTest.php +++ b/src/Adapter/Apcu/Tests/IntegrationPoolTest.php @@ -12,10 +12,13 @@ namespace Cache\Adapter\Apcu\Tests; use Cache\Adapter\Apcu\ApcuCachePool; +use Cache\Adapter\Common\Tests\Traits\TestNotUnserializableTrait; use Cache\IntegrationTests\CachePoolTest as BaseTest; class IntegrationPoolTest extends BaseTest { + use TestNotUnserializableTrait; + protected $skippedTests = [ 'testExpiration' => 'The cache expire at the next request.', 'testSaveExpired' => 'The cache expire at the next request.', diff --git a/src/Adapter/Common/AbstractCachePool.php b/src/Adapter/Common/AbstractCachePool.php index e8494af3..36841dca 100644 --- a/src/Adapter/Common/AbstractCachePool.php +++ b/src/Adapter/Common/AbstractCachePool.php @@ -333,6 +333,7 @@ private function handleException(\Exception $e, $function) } $this->log($level, $e->getMessage(), ['exception' => $e]); + if (!$e instanceof CacheException) { $e = new CachePoolException(sprintf('Exception thrown when executing "%s". ', $function), 0, $e); } @@ -555,4 +556,46 @@ public function has($key) { return $this->hasItem($key); } + + /** + * Like the native unserialize() function but throws an exception if anything goes wrong. + * + * @param string $value + * + * @throws \Exception + * + * @return mixed + */ + protected static function unserialize($value) + { + if ('b:0;' === $value) { + return false; + } + + $unserializeCallbackHandler = \ini_set('unserialize_callback_func', __CLASS__.'::handleUnserializeCallback'); + + try { + if (false !== $value = \unserialize($value)) { + return $value; + } + + throw new \DomainException('Failed to unserialize cached value'); + } catch (\Error $e) { + throw new \ErrorException($e->getMessage(), $e->getCode(), E_ERROR, $e->getFile(), $e->getLine()); + } finally { + \ini_set('unserialize_callback_func', $unserializeCallbackHandler); + } + } + + /** + * @internal + * + * @param string $class + * + * @throws \DomainException + */ + public static function handleUnserializeCallback($class) + { + throw new \DomainException('Class not found: '.$class); + } } diff --git a/src/Adapter/Common/CacheItem.php b/src/Adapter/Common/CacheItem.php index 47e68c35..3f19b923 100644 --- a/src/Adapter/Common/CacheItem.php +++ b/src/Adapter/Common/CacheItem.php @@ -62,6 +62,7 @@ class CacheItem implements PhpCacheItem /** * @param string $key * @param \Closure|bool $callable or boolean hasValue + * @param mixed $value */ public function __construct($key, $callable = null, $value = null) { @@ -241,8 +242,14 @@ private function initialize() { if ($this->callable !== null) { // $func will be $adapter->fetchObjectFromCache(); - $func = $this->callable; - $result = $func(); + $func = $this->callable; + + try { + $result = $func(); + } catch (\DomainException $exception) { + return; + } + $this->hasValue = $result[0]; $this->value = $result[1]; $this->prevTags = isset($result[2]) ? $result[2] : []; diff --git a/src/Adapter/Common/Tests/Fixture/NotUnserializable.php b/src/Adapter/Common/Tests/Fixture/NotUnserializable.php new file mode 100644 index 00000000..3d3dd593 --- /dev/null +++ b/src/Adapter/Common/Tests/Fixture/NotUnserializable.php @@ -0,0 +1,25 @@ +, Tobias Nyholm + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Cache\Adapter\Common\Tests\Fixture; + +class NotUnserializable implements \Serializable +{ + public function serialize() + { + return serialize(123); + } + + public function unserialize($ser) + { + throw new \Exception(__CLASS__); + } +} diff --git a/src/Adapter/Common/Tests/Traits/TestNotUnserializableTrait.php b/src/Adapter/Common/Tests/Traits/TestNotUnserializableTrait.php new file mode 100644 index 00000000..ad2a1cf9 --- /dev/null +++ b/src/Adapter/Common/Tests/Traits/TestNotUnserializableTrait.php @@ -0,0 +1,50 @@ +, Tobias Nyholm + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Cache\Adapter\Common\Tests\Traits; + +use Cache\Adapter\Common\Tests\Fixture\NotUnserializable; +use Psr\Cache\CacheItemPoolInterface; + +trait TestNotUnserializableTrait +{ + public function testNotUnserializable() + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + + return; + } + + $cache = $this->createCachePool(); + + $item = $cache->getItem('foo'); + $cache->save($item->set(new NotUnserializable())); + + $item = $cache->getItem('foo'); + $this->assertFalse($item->isHit()); + + foreach ($cache->getItems(['foo']) as $item) { + } + + $cache->save($item->set(new NotUnserializable())); + + foreach ($cache->getItems(['foo']) as $item) { + } + + $this->assertFalse($item->isHit()); + } + + /** + * @return CacheItemPoolInterface that is used in the tests + */ + abstract public function createCachePool(); +} diff --git a/src/Adapter/Doctrine/DoctrineCachePool.php b/src/Adapter/Doctrine/DoctrineCachePool.php index 3fd333d2..9989091c 100644 --- a/src/Adapter/Doctrine/DoctrineCachePool.php +++ b/src/Adapter/Doctrine/DoctrineCachePool.php @@ -42,11 +42,27 @@ public function __construct(Cache $cache) */ protected function fetchObjectFromCache($key) { - if (false === $data = $this->cache->fetch($key)) { - return [false, null, [], null]; - } + $unserializeCallbackHandler = ini_set('unserialize_callback_func', parent::class.'::handleUnserializeCallback'); + + try { + if (false === $data = $this->cache->fetch($key)) { + return [false, null, [], null]; + } + + return parent::unserialize($data); + } catch (\Error $e) { + $trace = $e->getTrace(); - return unserialize($data); + if (isset($trace[0]['function']) && !isset($trace[0]['class'])) { + if ($trace[0]['function'] === 'unserialize' || $trace[0]['function'] === 'apcu_fetch' || $trace[0]['function'] === 'apc_fetch') { + throw new \ErrorException($e->getMessage(), $e->getCode(), E_ERROR, $e->getFile(), $e->getLine()); + } + } + + throw $e; + } finally { + ini_set('unserialize_callback_func', $unserializeCallbackHandler); + } } /** diff --git a/src/Adapter/Doctrine/Tests/IntegrationPoolTest.php b/src/Adapter/Doctrine/Tests/IntegrationPoolTest.php index ad5d5d38..5d00785e 100644 --- a/src/Adapter/Doctrine/Tests/IntegrationPoolTest.php +++ b/src/Adapter/Doctrine/Tests/IntegrationPoolTest.php @@ -11,9 +11,11 @@ namespace Cache\Adapter\Doctrine\Tests; +use Cache\Adapter\Common\Tests\Traits\TestNotUnserializableTrait; use Cache\IntegrationTests\CachePoolTest; class IntegrationPoolTest extends CachePoolTest { use CreatePoolTrait; + use TestNotUnserializableTrait; } diff --git a/src/Adapter/Filesystem/FilesystemCachePool.php b/src/Adapter/Filesystem/FilesystemCachePool.php index 065519d1..0bdc3cf1 100644 --- a/src/Adapter/Filesystem/FilesystemCachePool.php +++ b/src/Adapter/Filesystem/FilesystemCachePool.php @@ -64,7 +64,7 @@ protected function fetchObjectFromCache($key) $file = $this->getFilePath($key); try { - $data = @unserialize($this->filesystem->read($file)); + $data = parent::unserialize($this->filesystem->read($file)); if ($data === false) { return $empty; } @@ -159,7 +159,7 @@ protected function getList($name) $this->filesystem->write($file, serialize([])); } - return unserialize($this->filesystem->read($file)); + return parent::unserialize($this->filesystem->read($file)); } /** diff --git a/src/Adapter/Filesystem/Tests/FilesystemCachePoolTest.php b/src/Adapter/Filesystem/Tests/FilesystemCachePoolTest.php index fe4365db..0cc5967e 100644 --- a/src/Adapter/Filesystem/Tests/FilesystemCachePoolTest.php +++ b/src/Adapter/Filesystem/Tests/FilesystemCachePoolTest.php @@ -11,6 +11,8 @@ namespace Cache\Adapter\Filesystem\Tests; +use Cache\Adapter\Common\Tests\Traits\TestNotUnserializableTrait; + use PHPUnit\Framework\TestCase; /** @@ -19,6 +21,7 @@ class FilesystemCachePoolTest extends TestCase { use CreatePoolTrait; + use TestNotUnserializableTrait; /** * @expectedException \Psr\Cache\InvalidArgumentException diff --git a/src/Adapter/Illuminate/IlluminateCachePool.php b/src/Adapter/Illuminate/IlluminateCachePool.php index 90209ae1..aed55120 100644 --- a/src/Adapter/Illuminate/IlluminateCachePool.php +++ b/src/Adapter/Illuminate/IlluminateCachePool.php @@ -58,11 +58,27 @@ protected function storeItemInCache(PhpCacheItem $item, $ttl) */ protected function fetchObjectFromCache($key) { - if (null === $data = $this->store->get($this->getHierarchyKey($key))) { - return [false, null, [], null]; - } + $unserializeCallbackHandler = ini_set('unserialize_callback_func', parent::class.'::handleUnserializeCallback'); + + try { + if (null === $data = $this->store->get($this->getHierarchyKey($key))) { + return [false, null, [], null]; + } + + return parent::unserialize($data); + } catch (\Error $e) { + $trace = $e->getTrace(); + + if (isset($trace[0]['function']) && !isset($trace[0]['class'])) { + if ($trace[0]['function'] === 'apcu_fetch' || $trace[0]['function'] === 'apc_fetch') { + throw new \ErrorException($e->getMessage(), $e->getCode(), E_ERROR, $e->getFile(), $e->getLine()); + } + } - return unserialize($data); + throw $e; + } finally { + ini_set('unserialize_callback_func', $unserializeCallbackHandler); + } } /** @@ -80,12 +96,15 @@ protected function clearOneObjectFromCache($key) { $path = null; $keyString = $this->getHierarchyKey($key, $path); + if ($path) { if ($this->store->get($path) === null) { $this->store->put($path, 0, 0); } + $this->store->increment($path); } + $this->clearHierarchyKeyCache(); return $this->store->forget($keyString); diff --git a/src/Adapter/Illuminate/Tests/IntegrationPoolTest.php b/src/Adapter/Illuminate/Tests/IntegrationPoolTest.php index f24075c7..9566814e 100644 --- a/src/Adapter/Illuminate/Tests/IntegrationPoolTest.php +++ b/src/Adapter/Illuminate/Tests/IntegrationPoolTest.php @@ -11,9 +11,11 @@ namespace Cache\Adapter\Illuminate\Tests; +use Cache\Adapter\Common\Tests\Traits\TestNotUnserializableTrait; use Cache\IntegrationTests\CachePoolTest; class IntegrationPoolTest extends CachePoolTest { use CreatePoolTrait; + use TestNotUnserializableTrait; } diff --git a/src/Adapter/Memcache/MemcacheCachePool.php b/src/Adapter/Memcache/MemcacheCachePool.php index 8efedd9d..8858ca19 100644 --- a/src/Adapter/Memcache/MemcacheCachePool.php +++ b/src/Adapter/Memcache/MemcacheCachePool.php @@ -38,7 +38,7 @@ public function __construct(Memcache $cache) */ protected function fetchObjectFromCache($key) { - if (false === $result = unserialize($this->cache->get($key))) { + if (false === $result = parent::unserialize($this->cache->get($key))) { return [false, null, [], null]; } diff --git a/src/Adapter/Memcache/Tests/IntegrationPoolTest.php b/src/Adapter/Memcache/Tests/IntegrationPoolTest.php index ea5a49f5..3b488c18 100644 --- a/src/Adapter/Memcache/Tests/IntegrationPoolTest.php +++ b/src/Adapter/Memcache/Tests/IntegrationPoolTest.php @@ -11,12 +11,15 @@ namespace Cache\Adapter\Memcache\Tests; +use Cache\Adapter\Common\Tests\Traits\TestNotUnserializableTrait; use Cache\Adapter\Memcache\MemcacheCachePool; use Cache\IntegrationTests\CachePoolTest; use Memcache; class IntegrationPoolTest extends CachePoolTest { + use TestNotUnserializableTrait; + private $client; public function createCachePool() diff --git a/src/Adapter/Memcached/MemcachedCachePool.php b/src/Adapter/Memcached/MemcachedCachePool.php index ad3eca1f..b712868a 100644 --- a/src/Adapter/Memcached/MemcachedCachePool.php +++ b/src/Adapter/Memcached/MemcachedCachePool.php @@ -45,7 +45,7 @@ public function __construct(\Memcached $cache) */ protected function fetchObjectFromCache($key) { - if (false === $result = unserialize($this->cache->get($this->getHierarchyKey($key)))) { + if (false === $result = parent::unserialize($this->cache->get($this->getHierarchyKey($key)))) { return [false, null, [], null]; } diff --git a/src/Adapter/Memcached/Tests/IntegrationPoolTest.php b/src/Adapter/Memcached/Tests/IntegrationPoolTest.php index 1442323d..6ccf2e1f 100644 --- a/src/Adapter/Memcached/Tests/IntegrationPoolTest.php +++ b/src/Adapter/Memcached/Tests/IntegrationPoolTest.php @@ -11,9 +11,11 @@ namespace Cache\Adapter\Memcached\Tests; +use Cache\Adapter\Common\Tests\Traits\TestNotUnserializableTrait; use Cache\IntegrationTests\CachePoolTest as BaseTest; class IntegrationPoolTest extends BaseTest { use CreatePoolTrait; + use TestNotUnserializableTrait; } diff --git a/src/Adapter/MongoDB/MongoDBCachePool.php b/src/Adapter/MongoDB/MongoDBCachePool.php index 12912f9b..02855109 100644 --- a/src/Adapter/MongoDB/MongoDBCachePool.php +++ b/src/Adapter/MongoDB/MongoDBCachePool.php @@ -149,11 +149,11 @@ public function setDirectValue($name, $value) private function freezeValue($value) { - return static::jsonArmor(serialize($value)); + return static::jsonArmor(parent::serialize($value)); } private function thawValue($value) { - return unserialize(static::jsonDeArmor($value)); + return parent::unserialize(static::jsonDeArmor($value)); } } diff --git a/src/Adapter/MongoDB/Tests/IntegrationPoolTest.php b/src/Adapter/MongoDB/Tests/IntegrationPoolTest.php index 0be1dffd..a21ee8ad 100644 --- a/src/Adapter/MongoDB/Tests/IntegrationPoolTest.php +++ b/src/Adapter/MongoDB/Tests/IntegrationPoolTest.php @@ -11,12 +11,14 @@ namespace Cache\Adapter\MongoDB\Tests; +use Cache\Adapter\Common\Tests\Traits\TestNotUnserializableTrait; use Cache\Adapter\MongoDB\MongoDBCachePool; use Cache\IntegrationTests\CachePoolTest; class IntegrationPoolTest extends CachePoolTest { use CreateServerTrait; + use TestNotUnserializableTrait; public function createCachePool() { diff --git a/src/Adapter/Predis/PredisCachePool.php b/src/Adapter/Predis/PredisCachePool.php index f1ec6eee..010ff4da 100644 --- a/src/Adapter/Predis/PredisCachePool.php +++ b/src/Adapter/Predis/PredisCachePool.php @@ -42,7 +42,7 @@ public function __construct(Client $cache) */ protected function fetchObjectFromCache($key) { - if (false === $result = unserialize($this->cache->get($this->getHierarchyKey($key)))) { + if (false === $result = parent::unserialize($this->cache->get($this->getHierarchyKey($key)))) { return [false, null, [], null]; } diff --git a/src/Adapter/Predis/Tests/IntegrationPoolTest.php b/src/Adapter/Predis/Tests/IntegrationPoolTest.php index b713551e..78ba2bcd 100644 --- a/src/Adapter/Predis/Tests/IntegrationPoolTest.php +++ b/src/Adapter/Predis/Tests/IntegrationPoolTest.php @@ -11,9 +11,11 @@ namespace Cache\Adapter\Predis\Tests; +use Cache\Adapter\Common\Tests\Traits\TestNotUnserializableTrait; use Cache\IntegrationTests\CachePoolTest; class IntegrationPoolTest extends CachePoolTest { use CreatePoolTrait; + use TestNotUnserializableTrait; } diff --git a/src/Adapter/Redis/RedisCachePool.php b/src/Adapter/Redis/RedisCachePool.php index 1ca05f2c..b5de65b9 100644 --- a/src/Adapter/Redis/RedisCachePool.php +++ b/src/Adapter/Redis/RedisCachePool.php @@ -51,7 +51,7 @@ public function __construct($cache) */ protected function fetchObjectFromCache($key) { - if (false === $result = unserialize($this->cache->get($this->getHierarchyKey($key)))) { + if (false === $result = parent::unserialize($this->cache->get($this->getHierarchyKey($key)))) { return [false, null, [], null]; } diff --git a/src/Adapter/Redis/Tests/IntegrationPoolTest.php b/src/Adapter/Redis/Tests/IntegrationPoolTest.php index db6be513..6a9831ec 100644 --- a/src/Adapter/Redis/Tests/IntegrationPoolTest.php +++ b/src/Adapter/Redis/Tests/IntegrationPoolTest.php @@ -11,9 +11,11 @@ namespace Cache\Adapter\Redis\Tests; +use Cache\Adapter\Common\Tests\Traits\TestNotUnserializableTrait; use Cache\IntegrationTests\CachePoolTest as BaseTest; class IntegrationPoolTest extends BaseTest { use CreateRedisPoolTrait; + use TestNotUnserializableTrait; }