From 9ee802a2570342a5143feeea96c5e62780bec22f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Kucha=C5=99?= <honza.kuchar@grifart.cz> Date: Wed, 30 Mar 2022 21:20:23 +0200 Subject: [PATCH] Introducing safer serializer which stops on classes that does not explicitly declare if serializable --- src/SaferSerializer.php | 49 ++++++++++++++++++++++++ tests/SaferSerializerTest.php | 70 +++++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 src/SaferSerializer.php create mode 100644 tests/SaferSerializerTest.php diff --git a/src/SaferSerializer.php b/src/SaferSerializer.php new file mode 100644 index 0000000..6f97268 --- /dev/null +++ b/src/SaferSerializer.php @@ -0,0 +1,49 @@ +<?php + + +namespace Grifart\NotSerializable; + + +final class SaferSerializer +{ + + /** + * @param mixed $dataToBeSerialized + * @return string + */ + public static function serialize($dataToBeSerialized) + { + self::check($dataToBeSerialized); + return serialize($dataToBeSerialized); + } + + + /** + * @param mixed $dataToBeChecked + */ + public static function check($dataToBeChecked): void + { + if (is_scalar($dataToBeChecked) || is_null($dataToBeChecked)) { + return; + } + + if (!is_object($dataToBeChecked)) { + throw new \LogicException(\sprintf('You have passed a non-scalar, non-object value of type %s.', gettype($dataToBeChecked))); + } + + if (! (method_exists($dataToBeChecked, '__serialize') && method_exists($dataToBeChecked, '__unserialize'))) { + throw new \LogicException(\sprintf('Object of type %s do NOT explicitly state serialization support using __serialize() && __unserialize() methods.', get_class($dataToBeChecked))); + } + + // can throw exceptions, not a problem as we will call real serialize() anyway + $valuesToBeSerialized = $dataToBeChecked->__serialize(); + foreach($valuesToBeSerialized as $value) { + self::check($value); + } + + // as there is no way how one can check referenced properties when Serializable interface or __sleep() magic method is used, + // we consider them as case where class did NOT stated that is serializable. + } + + +} \ No newline at end of file diff --git a/tests/SaferSerializerTest.php b/tests/SaferSerializerTest.php new file mode 100644 index 0000000..b7b0b86 --- /dev/null +++ b/tests/SaferSerializerTest.php @@ -0,0 +1,70 @@ +<?php + +use Grifart\NotSerializable\SaferSerializer; +use Grifart\NotSerializable\NoSerialization; +use Tester\Assert; + +require __DIR__ . '/../vendor/autoload.php'; + +class NestedSerialization { + private object $nested; + public function __construct(object $nested){$this->nested = $nested;} + + /** + * @return array{someKey: object} + */ + public function __serialize(): array + { + return [ + 'someKey' => $this->nested + ]; + } + + /** + * @param array{someKey: object} $data + */ + public function __unserialize(array $data): void + { + $this->nested = $data['someKey']; + } +} + + +// This is the case when classic serialization & SaferSerialization behaviour is different +class SerializationNotStated {} + +Assert::noError(fn() => serialize(new SerializationNotStated())); +Assert::noError(fn() => serialize(new NestedSerialization(new SerializationNotStated()))); + +Assert::exception(fn() => SaferSerializer::serialize(new SerializationNotStated()), LogicException::class, 'Object of type SerializationNotStated do NOT explicitly state serialization support using __serialize() && __unserialize() methods.'); +Assert::exception(fn() => SaferSerializer::serialize(new NestedSerialization(new SerializationNotStated())), LogicException::class, 'Object of type SerializationNotStated do NOT explicitly state serialization support using __serialize() && __unserialize() methods.'); + + + +// Now check that we did not break anything: + +class SerializationDisabled { + use NoSerialization; +} + +Assert::exception(fn() => serialize(new SerializationDisabled()), LogicException::class, "The class 'SerializationDisabled' is not meant to be serialized."); +Assert::exception(fn() => serialize(new NestedSerialization(new SerializationDisabled())), LogicException::class, "The class 'SerializationDisabled' is not meant to be serialized."); + +Assert::exception(fn() => SaferSerializer::serialize(new SerializationDisabled()), LogicException::class, "The class 'SerializationDisabled' is not meant to be serialized."); +Assert::exception(fn() => SaferSerializer::serialize(new NestedSerialization(new SerializationDisabled())), LogicException::class, "The class 'SerializationDisabled' is not meant to be serialized."); + + + +class SerializationImplemented { + /** @return array{} */ + public function __serialize(): array { return [];} + /** @param array{} $data */ + public function __unserialize(array $data): void {} +} + +Assert::noError(fn() => serialize(new SerializationImplemented())); +Assert::noError(fn() => serialize(new NestedSerialization(new SerializationImplemented()))); + +Assert::noError(fn() => SaferSerializer::serialize(new SerializationImplemented())); +Assert::noError(fn() => SaferSerializer::serialize(new NestedSerialization(new SerializationImplemented()))); + -- GitLab