bug #28414 [VarExporter] fix exporting final serializable classes extending internal ones (nicolas-grekas)
This PR was merged into the 4.2-dev branch.
Discussion
----------
[VarExporter] fix exporting final serializable classes extending internal ones
| Q | A
| ------------- | ---
| Branch? | master
| Bug fix? | yes
| New feature? | no
| BC breaks? | no
| Deprecations? | no
| Tests pass? | yes
| Fixed tickets | -
| License | MIT
| Doc PR | -
Another edge case, discovered while reading https://github.com/doctrine/instantiator/issues/39
Commits
-------
a5bf9b0445
[VarExporter] fix exporting final serializable classes extending internal ones
This commit is contained in:
commit
3b931fe6c9
@ -76,7 +76,7 @@ class Exporter
|
||||
$sleep = null;
|
||||
$arrayValue = (array) $value;
|
||||
|
||||
if (!isset(Registry::$prototypes[$class])) {
|
||||
if (!isset(Registry::$reflectors[$class])) {
|
||||
// Might throw Exception("Serialization of '...' is not allowed")
|
||||
Registry::getClassReflector($class);
|
||||
serialize(Registry::$prototypes[$class]);
|
||||
@ -87,14 +87,16 @@ class Exporter
|
||||
$reflector = Registry::$reflectors[$class];
|
||||
$proto = Registry::$prototypes[$class];
|
||||
|
||||
if ($value instanceof \ArrayIterator || $value instanceof \ArrayObject) {
|
||||
if (($value instanceof \ArrayIterator || $value instanceof \ArrayObject) && null !== $proto) {
|
||||
// ArrayIterator and ArrayObject need special care because their "flags"
|
||||
// option changes the behavior of the (array) casting operator.
|
||||
$proto = Registry::$cloneable[$class] ? clone Registry::$prototypes[$class] : $reflector->newInstanceWithoutConstructor();
|
||||
$properties = self::getArrayObjectProperties($value, $arrayValue, $proto);
|
||||
} elseif ($value instanceof \SplObjectStorage) {
|
||||
// By implementing Serializable, SplObjectStorage breaks internal references,
|
||||
// let's deal with it on our own.
|
||||
|
||||
// populates Registry::$prototypes[$class] with a new instance
|
||||
Registry::getClassReflector($class, Registry::$instantiableWithoutConstructor[$class], Registry::$cloneable[$class]);
|
||||
} elseif ($value instanceof \SplObjectStorage && Registry::$cloneable[$class] && null !== $proto) {
|
||||
// By implementing Serializable, SplObjectStorage breaks
|
||||
// internal references; let's deal with it on our own.
|
||||
foreach (clone $value as $v) {
|
||||
$properties[] = $v;
|
||||
$properties[] = $value[$v];
|
||||
@ -284,12 +286,15 @@ class Exporter
|
||||
continue;
|
||||
}
|
||||
if (!Registry::$instantiableWithoutConstructor[$class]) {
|
||||
if (is_subclass_of($class, 'Throwable')) {
|
||||
$eol = is_subclass_of($class, 'Error') ? "\0Error\0" : "\0Exception\0";
|
||||
$serializables[$k] = 'O:'.\strlen($class).':"'.$class.'":1:{s:'.(5 + \strlen($eol)).':"'.$eol.'trace";a:0:{}}';
|
||||
if (is_subclass_of($class, 'Serializable')) {
|
||||
$serializables[$k] = 'C:'.\strlen($class).':"'.$class.'":0:{}';
|
||||
} else {
|
||||
$serializables[$k] = 'O:'.\strlen($class).':"'.$class.'":0:{}';
|
||||
}
|
||||
if (is_subclass_of($class, 'Throwable')) {
|
||||
$eol = is_subclass_of($class, 'Error') ? "\0Error\0" : "\0Exception\0";
|
||||
$serializables[$k] = substr_replace($serializables[$k], '1:{s:'.(5 + \strlen($eol)).':"'.$eol.'trace";a:0:{}}', -4);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
$code .= $subIndent.(1 !== $k - $j ? $k.' => ' : '');
|
||||
|
@ -37,7 +37,9 @@ class Registry
|
||||
|
||||
try {
|
||||
foreach ($serializables as $k => $v) {
|
||||
$objects[$k] = unserialize($v);
|
||||
if (false === $objects[$k] = unserialize($v)) {
|
||||
throw new \Exception(error_get_last()['message'] ?? 'unserialize(): unknown error');
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
ini_set('unserialize_callback_func', $unserializeCallback);
|
||||
@ -67,18 +69,16 @@ class Registry
|
||||
if (self::$instantiableWithoutConstructor[$class] = $instantiableWithoutConstructor || !$reflector->isFinal()) {
|
||||
$proto = $reflector->newInstanceWithoutConstructor();
|
||||
} else {
|
||||
$r = $reflector;
|
||||
do {
|
||||
if ($r->isInternal()) {
|
||||
if (false === $proto = @unserialize('O:'.\strlen($class).':"'.$class.'":0:{}')) {
|
||||
throw new \Exception(sprintf("Serialization of '%s' is not allowed", $class));
|
||||
}
|
||||
break;
|
||||
}
|
||||
} while ($r = $r->getParentClass());
|
||||
|
||||
if (self::$instantiableWithoutConstructor[$class] = !$r) {
|
||||
try {
|
||||
$proto = $reflector->newInstanceWithoutConstructor();
|
||||
self::$instantiableWithoutConstructor[$class] = true;
|
||||
} catch (\ReflectionException $e) {
|
||||
$proto = $reflector->implementsInterface('Serializable') ? 'C:' : 'O:';
|
||||
if ('C:' === $proto && !$reflector->getMethod('unserialize')->isInternal()) {
|
||||
$proto = null;
|
||||
} elseif (false === $proto = @unserialize($proto.\strlen($class).':"'.$class.'":0:{}')) {
|
||||
throw new \Exception(sprintf("Serialization of '%s' is not allowed", $class));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
return \Symfony\Component\VarExporter\Internal\Hydrator::hydrate(
|
||||
$o = \Symfony\Component\VarExporter\Internal\Registry::unserialize([], [
|
||||
'C:54:"Symfony\\Component\\VarExporter\\Tests\\FinalArrayIterator":49:{a:2:{i:0;i:123;i:1;s:21:"x:i:0;a:0:{};m:a:0:{}";}}',
|
||||
]),
|
||||
null,
|
||||
[],
|
||||
$o[0],
|
||||
[]
|
||||
);
|
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
return \Symfony\Component\VarExporter\Internal\Hydrator::hydrate(
|
||||
$o = [
|
||||
(\Symfony\Component\VarExporter\Internal\Registry::$factories[\Symfony\Component\VarExporter\Tests\FinalStdClass::class] ?? \Symfony\Component\VarExporter\Internal\Registry::f(\Symfony\Component\VarExporter\Tests\FinalStdClass::class))(),
|
||||
],
|
||||
null,
|
||||
[],
|
||||
$o[0],
|
||||
[]
|
||||
);
|
@ -68,9 +68,9 @@ class VarExporterTest extends TestCase
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideMarshall
|
||||
* @dataProvider provideExport
|
||||
*/
|
||||
public function testMarshall(string $testName, $value, bool $staticValueExpected = false)
|
||||
public function testExport(string $testName, $value, bool $staticValueExpected = false)
|
||||
{
|
||||
$dumpedValue = $this->getDump($value);
|
||||
$isStaticValue = true;
|
||||
@ -98,7 +98,7 @@ class VarExporterTest extends TestCase
|
||||
}
|
||||
}
|
||||
|
||||
public function provideMarshall()
|
||||
public function provideExport()
|
||||
{
|
||||
yield array('multiline-string', array("\0\0\r\nA" => "B\rC\n\n"), true);
|
||||
|
||||
@ -175,6 +175,10 @@ class VarExporterTest extends TestCase
|
||||
$rl->setValue($value, 123);
|
||||
|
||||
yield array('final-error', $value);
|
||||
|
||||
yield array('final-array-iterator', new FinalArrayIterator());
|
||||
|
||||
yield array('final-stdclass', new FinalStdClass());
|
||||
}
|
||||
}
|
||||
|
||||
@ -278,3 +282,28 @@ final class FinalError extends \Error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class FinalArrayIterator extends \ArrayIterator
|
||||
{
|
||||
public function serialize()
|
||||
{
|
||||
return serialize(array(123, parent::serialize()));
|
||||
}
|
||||
|
||||
public function unserialize($data)
|
||||
{
|
||||
if ('' === $data) {
|
||||
throw new \InvalidArgumentException('Serialized data is empty.');
|
||||
}
|
||||
list(, $data) = unserialize($data);
|
||||
parent::unserialize($data);
|
||||
}
|
||||
}
|
||||
|
||||
final class FinalStdClass extends \stdClass
|
||||
{
|
||||
public function __clone()
|
||||
{
|
||||
throw new \BadMethodCallException('Should not be called.');
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
"name": "symfony/var-exporter",
|
||||
"type": "library",
|
||||
"description": "A blend of var_export() + serialize() to turn any serializable data structure to plain PHP code",
|
||||
"keywords": ["export", "serialize"],
|
||||
"keywords": ["export", "serialize", "instantiate", "hydrate", "construct", "clone"],
|
||||
"homepage": "https://symfony.com",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
|
Reference in New Issue
Block a user