diff --git a/.travis.yml b/.travis.yml
index d4c5156a6d..b76383fb1d 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -151,6 +151,7 @@ before_install:
tfold ext.libsodium tpecl libsodium sodium.so $INI
tfold ext.mongodb tpecl mongodb-1.5.0 mongodb.so $INI
tfold ext.amqp tpecl amqp-1.9.3 amqp.so $INI
+ tfold ext.igbinary tpecl igbinary-2.0.6 igbinary.so $INI
fi
- |
diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
index b3908ace88..de7b13e9b0 100644
--- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
@@ -24,6 +24,7 @@ use Symfony\Component\Cache\Adapter\AbstractAdapter;
use Symfony\Component\Cache\Adapter\AdapterInterface;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\Cache\Adapter\TagAwareAdapter;
+use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
use Symfony\Component\Cache\ResettableInterface;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\Config\Loader\LoaderInterface;
@@ -1539,6 +1540,10 @@ class FrameworkExtension extends Extension
private function registerCacheConfiguration(array $config, ContainerBuilder $container)
{
+ if (!class_exists(DefaultMarshaller::class)) {
+ $container->removeDefinition('cache.default_marshaller');
+ }
+
$version = new Parameter('container.build_id');
$container->getDefinition('cache.adapter.apcu')->replaceArgument(2, $version);
$container->getDefinition('cache.adapter.filesystem')->replaceArgument(2, $config['directory']);
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml
index d31be8db3f..3d31361cf1 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml
@@ -75,6 +75,7 @@
+ */ +class DefaultMarshaller implements MarshallerInterface +{ + private $useIgbinarySerialize = true; + + public function __construct(bool $useIgbinarySerialize = null) + { + if (null === $useIgbinarySerialize) { + $useIgbinarySerialize = \extension_loaded('igbinary'); + } elseif ($useIgbinarySerialize && !\extension_loaded('igbinary')) { + throw new CacheException('The "igbinary" PHP extension is not loaded.'); + } + $this->useIgbinarySerialize = $useIgbinarySerialize; + } + + /** + * {@inheritdoc} + */ + public function marshall(array $values, ?array &$failed): array + { + $serialized = $failed = array(); + + foreach ($values as $id => $value) { + try { + if ($this->useIgbinarySerialize) { + $serialized[$id] = igbinary_serialize($value); + } else { + $serialized[$id] = serialize($value); + } + } catch (\Exception $e) { + $failed[] = $id; + } + } + + return $serialized; + } + + /** + * {@inheritdoc} + */ + public function unmarshall(string $value) + { + if ('b:0;' === $value) { + return false; + } + if ('N;' === $value) { + return null; + } + static $igbinaryNull; + if ($value === ($igbinaryNull ?? $igbinaryNull = \extension_loaded('igbinary') ? igbinary_serialize(null) : false)) { + return null; + } + $unserializeCallbackHandler = ini_set('unserialize_callback_func', __CLASS__.'::handleUnserializeCallback'); + try { + if (':' === ($value[1] ?? ':')) { + if (false !== $value = unserialize($value)) { + return $value; + } + } elseif (false === $igbinaryNull) { + throw new \RuntimeException('Failed to unserialize cached value, did you forget to install the "igbinary" extension?'); + } elseif (null !== $value = igbinary_unserialize($value)) { + return $value; + } + + throw new \DomainException(error_get_last() ? error_get_last()['message'] : '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 + */ + public static function handleUnserializeCallback($class) + { + throw new \DomainException('Class not found: '.$class); + } +} diff --git a/src/Symfony/Component/Cache/Marshaller/MarshallerInterface.php b/src/Symfony/Component/Cache/Marshaller/MarshallerInterface.php new file mode 100644 index 0000000000..4d757e38c4 --- /dev/null +++ b/src/Symfony/Component/Cache/Marshaller/MarshallerInterface.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Marshaller; + +/** + * Serializes/unserializes PHP values. + * + * Implementations of this interface MUST deal with errors carefuly. They MUST + * also deal with forward and backward compatibility at the storage format level. + * + * @author Nicolas Grekas
+ */
+interface MarshallerInterface
+{
+ /**
+ * Serializes a list of values.
+ *
+ * When serialization fails for a specific value, no exception should be
+ * thrown. Instead, its key should be listed in $failed.
+ */
+ public function marshall(array $values, ?array &$failed): array;
+
+ /**
+ * Unserializes a single value and throws an exception if anything goes wrong.
+ *
+ * @return mixed
+ *
+ * @throws \Exception Whenever unserialization fails
+ */
+ public function unmarshall(string $value);
+}
diff --git a/src/Symfony/Component/Cache/Simple/FilesystemCache.php b/src/Symfony/Component/Cache/Simple/FilesystemCache.php
index 37b3d3fa61..8e04d53355 100644
--- a/src/Symfony/Component/Cache/Simple/FilesystemCache.php
+++ b/src/Symfony/Component/Cache/Simple/FilesystemCache.php
@@ -11,6 +11,8 @@
namespace Symfony\Component\Cache\Simple;
+use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
+use Symfony\Component\Cache\Marshaller\MarshallerInterface;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\Cache\Traits\FilesystemTrait;
@@ -18,8 +20,9 @@ class FilesystemCache extends AbstractCache implements PruneableInterface
{
use FilesystemTrait;
- public function __construct(string $namespace = '', int $defaultLifetime = 0, string $directory = null)
+ public function __construct(string $namespace = '', int $defaultLifetime = 0, string $directory = null, MarshallerInterface $marshaller = null)
{
+ $this->marshaller = $marshaller ?? new DefaultMarshaller();
parent::__construct('', $defaultLifetime);
$this->init($namespace, $directory);
}
diff --git a/src/Symfony/Component/Cache/Simple/MemcachedCache.php b/src/Symfony/Component/Cache/Simple/MemcachedCache.php
index 0ff521b9c3..8e418b071e 100644
--- a/src/Symfony/Component/Cache/Simple/MemcachedCache.php
+++ b/src/Symfony/Component/Cache/Simple/MemcachedCache.php
@@ -11,6 +11,7 @@
namespace Symfony\Component\Cache\Simple;
+use Symfony\Component\Cache\Marshaller\MarshallerInterface;
use Symfony\Component\Cache\Traits\MemcachedTrait;
class MemcachedCache extends AbstractCache
@@ -19,8 +20,8 @@ class MemcachedCache extends AbstractCache
protected $maxIdLength = 250;
- public function __construct(\Memcached $client, string $namespace = '', int $defaultLifetime = 0)
+ public function __construct(\Memcached $client, string $namespace = '', int $defaultLifetime = 0, MarshallerInterface $marshaller = null)
{
- $this->init($client, $namespace, $defaultLifetime);
+ $this->init($client, $namespace, $defaultLifetime, $marshaller);
}
}
diff --git a/src/Symfony/Component/Cache/Simple/PdoCache.php b/src/Symfony/Component/Cache/Simple/PdoCache.php
index 65b9879cd3..083198cb6e 100644
--- a/src/Symfony/Component/Cache/Simple/PdoCache.php
+++ b/src/Symfony/Component/Cache/Simple/PdoCache.php
@@ -11,6 +11,7 @@
namespace Symfony\Component\Cache\Simple;
+use Symfony\Component\Cache\Marshaller\MarshallerInterface;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\Cache\Traits\PdoTrait;
@@ -41,8 +42,8 @@ class PdoCache extends AbstractCache implements PruneableInterface
* @throws InvalidArgumentException When PDO error mode is not PDO::ERRMODE_EXCEPTION
* @throws InvalidArgumentException When namespace contains invalid characters
*/
- public function __construct($connOrDsn, string $namespace = '', int $defaultLifetime = 0, array $options = array())
+ public function __construct($connOrDsn, string $namespace = '', int $defaultLifetime = 0, array $options = array(), MarshallerInterface $marshaller = null)
{
- $this->init($connOrDsn, $namespace, $defaultLifetime, $options);
+ $this->init($connOrDsn, $namespace, $defaultLifetime, $options, $marshaller);
}
}
diff --git a/src/Symfony/Component/Cache/Simple/PhpArrayCache.php b/src/Symfony/Component/Cache/Simple/PhpArrayCache.php
index bb3321b27c..aeef4668ae 100644
--- a/src/Symfony/Component/Cache/Simple/PhpArrayCache.php
+++ b/src/Symfony/Component/Cache/Simple/PhpArrayCache.php
@@ -13,6 +13,7 @@ namespace Symfony\Component\Cache\Simple;
use Psr\SimpleCache\CacheInterface;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
+use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
use Symfony\Component\Cache\Traits\PhpArrayTrait;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\Cache\ResettableInterface;
@@ -28,6 +29,8 @@ class PhpArrayCache implements CacheInterface, PruneableInterface, ResettableInt
{
use PhpArrayTrait;
+ private $marshaller;
+
/**
* @param string $file The PHP file were values are cached
* @param CacheInterface $fallbackPool A pool to fallback on when an item is not hit
@@ -83,7 +86,7 @@ class PhpArrayCache implements CacheInterface, PruneableInterface, ResettableInt
}
if (\is_string($value) && isset($value[2]) && ':' === $value[1]) {
try {
- return unserialize($value);
+ return ($this->marshaller ?? $this->marshaller = new DefaultMarshaller())->unmarshall($value);
} catch (\Throwable $e) {
return $default;
}
diff --git a/src/Symfony/Component/Cache/Simple/RedisCache.php b/src/Symfony/Component/Cache/Simple/RedisCache.php
index 45bb5ff799..df2a96e86b 100644
--- a/src/Symfony/Component/Cache/Simple/RedisCache.php
+++ b/src/Symfony/Component/Cache/Simple/RedisCache.php
@@ -11,6 +11,7 @@
namespace Symfony\Component\Cache\Simple;
+use Symfony\Component\Cache\Marshaller\MarshallerInterface;
use Symfony\Component\Cache\Traits\RedisTrait;
class RedisCache extends AbstractCache
@@ -22,8 +23,8 @@ class RedisCache extends AbstractCache
* @param string $namespace
* @param int $defaultLifetime
*/
- public function __construct($redisClient, string $namespace = '', int $defaultLifetime = 0)
+ public function __construct($redisClient, string $namespace = '', int $defaultLifetime = 0, MarshallerInterface $marshaller = null)
{
- $this->init($redisClient, $namespace, $defaultLifetime);
+ $this->init($redisClient, $namespace, $defaultLifetime, $marshaller);
}
}
diff --git a/src/Symfony/Component/Cache/Tests/Adapter/MaxIdLengthAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/MaxIdLengthAdapterTest.php
index 04781b8fbd..f4822e9dfc 100644
--- a/src/Symfony/Component/Cache/Tests/Adapter/MaxIdLengthAdapterTest.php
+++ b/src/Symfony/Component/Cache/Tests/Adapter/MaxIdLengthAdapterTest.php
@@ -56,7 +56,7 @@ class MaxIdLengthAdapterTest extends TestCase
$reflectionProperty->setValue($cache, true);
// Versioning enabled
- $this->assertEquals('--------------------------:1:------------', $reflectionMethod->invokeArgs($cache, array(str_repeat('-', 12))));
+ $this->assertEquals('--------------------------:1/------------', $reflectionMethod->invokeArgs($cache, array(str_repeat('-', 12))));
$this->assertLessThanOrEqual(50, strlen($reflectionMethod->invokeArgs($cache, array(str_repeat('-', 12)))));
$this->assertLessThanOrEqual(50, strlen($reflectionMethod->invokeArgs($cache, array(str_repeat('-', 23)))));
$this->assertLessThanOrEqual(50, strlen($reflectionMethod->invokeArgs($cache, array(str_repeat('-', 40)))));
diff --git a/src/Symfony/Component/Cache/Tests/Marshaller/DefaultMarshallerTest.php b/src/Symfony/Component/Cache/Tests/Marshaller/DefaultMarshallerTest.php
new file mode 100644
index 0000000000..fc625d12fc
--- /dev/null
+++ b/src/Symfony/Component/Cache/Tests/Marshaller/DefaultMarshallerTest.php
@@ -0,0 +1,104 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Cache\Tests\Marshaller;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
+
+class DefaultMarshallerTest extends TestCase
+{
+ public function testSerialize()
+ {
+ $marshaller = new DefaultMarshaller();
+ $values = array(
+ 'a' => 123,
+ 'b' => function () {},
+ );
+
+ $expected = array('a' => \extension_loaded('igbinary') ? igbinary_serialize(123) : serialize(123));
+ $this->assertSame($expected, $marshaller->marshall($values, $failed));
+ $this->assertSame(array('b'), $failed);
+ }
+
+ public function testNativeUnserialize()
+ {
+ $marshaller = new DefaultMarshaller();
+ $this->assertNull($marshaller->unmarshall(serialize(null)));
+ $this->assertFalse($marshaller->unmarshall(serialize(false)));
+ $this->assertSame('', $marshaller->unmarshall(serialize('')));
+ $this->assertSame(0, $marshaller->unmarshall(serialize(0)));
+ }
+
+ /**
+ * @requires extension igbinary
+ */
+ public function testIgbinaryUnserialize()
+ {
+ $marshaller = new DefaultMarshaller();
+ $this->assertNull($marshaller->unmarshall(igbinary_serialize(null)));
+ $this->assertFalse($marshaller->unmarshall(igbinary_serialize(false)));
+ $this->assertSame('', $marshaller->unmarshall(igbinary_serialize('')));
+ $this->assertSame(0, $marshaller->unmarshall(igbinary_serialize(0)));
+ }
+
+ /**
+ * @expectedException \DomainException
+ * @expectedExceptionMessage Class not found: NotExistingClass
+ */
+ public function testNativeUnserializeNotFoundClass()
+ {
+ $marshaller = new DefaultMarshaller();
+ $marshaller->unmarshall('O:16:"NotExistingClass":0:{}');
+ }
+
+ /**
+ * @requires extension igbinary
+ * @expectedException \DomainException
+ * @expectedExceptionMessage Class not found: NotExistingClass
+ */
+ public function testIgbinaryUnserializeNotFoundClass()
+ {
+ $marshaller = new DefaultMarshaller();
+ $marshaller->unmarshall(rawurldecode('%00%00%00%02%17%10NotExistingClass%14%00'));
+ }
+
+ /**
+ * @expectedException \DomainException
+ * @expectedExceptionMessage unserialize(): Error at offset 0 of 3 bytes
+ */
+ public function testNativeUnserializeInvalid()
+ {
+ $marshaller = new DefaultMarshaller();
+ set_error_handler(function () { return false; });
+ try {
+ @$marshaller->unmarshall(':::');
+ } finally {
+ restore_error_handler();
+ }
+ }
+
+ /**
+ * @requires extension igbinary
+ * @expectedException \DomainException
+ * @expectedExceptionMessage igbinary_unserialize_zval: unknown type '61', position 5
+ */
+ public function testIgbinaryUnserializeInvalid()
+ {
+ $marshaller = new DefaultMarshaller();
+ set_error_handler(function () { return false; });
+ try {
+ @$marshaller->unmarshall(rawurldecode('%00%00%00%02abc'));
+ } finally {
+ restore_error_handler();
+ }
+ }
+}
diff --git a/src/Symfony/Component/Cache/Traits/AbstractTrait.php b/src/Symfony/Component/Cache/Traits/AbstractTrait.php
index 361ec5782a..68634b0947 100644
--- a/src/Symfony/Component/Cache/Traits/AbstractTrait.php
+++ b/src/Symfony/Component/Cache/Traits/AbstractTrait.php
@@ -109,14 +109,14 @@ trait AbstractTrait
if ($cleared = $this->versioningIsEnabled) {
$namespaceVersion = 2;
try {
- foreach ($this->doFetch(array('@'.$this->namespace)) as $v) {
+ foreach ($this->doFetch(array('/'.$this->namespace)) as $v) {
$namespaceVersion = 1 + (int) $v;
}
} catch (\Exception $e) {
}
- $namespaceVersion .= ':';
+ $namespaceVersion .= '/';
try {
- $cleared = $this->doSave(array('@'.$this->namespace => $namespaceVersion), 0);
+ $cleared = $this->doSave(array('/'.$this->namespace => $namespaceVersion), 0);
} catch (\Exception $e) {
$cleared = false;
}
@@ -222,9 +222,13 @@ trait AbstractTrait
* @return mixed
*
* @throws \Exception
+ *
+ * @deprecated since Symfony 4.2, use DefaultMarshaller instead.
*/
protected static function unserialize($value)
{
+ @trigger_error(sprintf('The "%s::unserialize()" method is deprecated since Symfony 4.2, use DefaultMarshaller instead.', __CLASS__), E_USER_DEPRECATED);
+
if ('b:0;' === $value) {
return false;
}
@@ -245,9 +249,9 @@ trait AbstractTrait
{
if ($this->versioningIsEnabled && '' === $this->namespaceVersion) {
$this->ids = array();
- $this->namespaceVersion = '1:';
+ $this->namespaceVersion = '1/';
try {
- foreach ($this->doFetch(array('@'.$this->namespace)) as $v) {
+ foreach ($this->doFetch(array('/'.$this->namespace)) as $v) {
$this->namespaceVersion = $v;
}
} catch (\Exception $e) {
diff --git a/src/Symfony/Component/Cache/Traits/ApcuTrait.php b/src/Symfony/Component/Cache/Traits/ApcuTrait.php
index 4bbb48bc4e..02ca023b08 100644
--- a/src/Symfony/Component/Cache/Traits/ApcuTrait.php
+++ b/src/Symfony/Component/Cache/Traits/ApcuTrait.php
@@ -51,6 +51,7 @@ trait ApcuTrait
*/
protected function doFetch(array $ids)
{
+ $unserializeCallbackHandler = ini_set('unserialize_callback_func', __CLASS__.'::handleUnserializeCallback');
try {
$values = array();
foreach (apcu_fetch($ids, $ok) ?: array() as $k => $v) {
@@ -62,6 +63,8 @@ trait ApcuTrait
return $values;
} catch (\Error $e) {
throw new \ErrorException($e->getMessage(), $e->getCode(), E_ERROR, $e->getFile(), $e->getLine());
+ } finally {
+ ini_set('unserialize_callback_func', $unserializeCallbackHandler);
}
}
diff --git a/src/Symfony/Component/Cache/Traits/FilesystemTrait.php b/src/Symfony/Component/Cache/Traits/FilesystemTrait.php
index 23974b3bc5..52dcf985dd 100644
--- a/src/Symfony/Component/Cache/Traits/FilesystemTrait.php
+++ b/src/Symfony/Component/Cache/Traits/FilesystemTrait.php
@@ -23,6 +23,8 @@ trait FilesystemTrait
{
use FilesystemCommonTrait;
+ private $marshaller;
+
/**
* @return bool
*/
@@ -68,7 +70,7 @@ trait FilesystemTrait
$value = stream_get_contents($h);
fclose($h);
if ($i === $id) {
- $values[$id] = parent::unserialize($value);
+ $values[$id] = $this->marshaller->unmarshall($value);
}
}
}
@@ -91,17 +93,19 @@ trait FilesystemTrait
*/
protected function doSave(array $values, $lifetime)
{
- $ok = true;
$expiresAt = $lifetime ? (time() + $lifetime) : 0;
+ $values = $this->marshaller->marshall($values, $failed);
foreach ($values as $id => $value) {
- $ok = $this->write($this->getFile($id, true), $expiresAt."\n".rawurlencode($id)."\n".serialize($value), $expiresAt) && $ok;
+ if (!$this->write($this->getFile($id, true), $expiresAt."\n".rawurlencode($id)."\n".$value, $expiresAt)) {
+ $failed[] = $id;
+ }
}
- if (!$ok && !is_writable($this->directory)) {
+ if ($failed && !is_writable($this->directory)) {
throw new CacheException(sprintf('Cache directory is not writable (%s)', $this->directory));
}
- return $ok;
+ return $failed;
}
}
diff --git a/src/Symfony/Component/Cache/Traits/MemcachedTrait.php b/src/Symfony/Component/Cache/Traits/MemcachedTrait.php
index c0d43c2e61..f312898355 100644
--- a/src/Symfony/Component/Cache/Traits/MemcachedTrait.php
+++ b/src/Symfony/Component/Cache/Traits/MemcachedTrait.php
@@ -13,6 +13,8 @@ namespace Symfony\Component\Cache\Traits;
use Symfony\Component\Cache\Exception\CacheException;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
+use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
+use Symfony\Component\Cache\Marshaller\MarshallerInterface;
/**
* @author Rob Frawley 2nd