diff --git a/Subscribers/DoctrineEncryptSubscriber.php b/Subscribers/DoctrineEncryptSubscriber.php index afb4ffc..2bd59d1 100644 --- a/Subscribers/DoctrineEncryptSubscriber.php +++ b/Subscribers/DoctrineEncryptSubscriber.php @@ -5,17 +5,15 @@ use Doctrine\Common\Annotations\Reader; use Doctrine\Common\EventSubscriber; use Doctrine\ORM\EntityManagerInterface; -use Doctrine\ORM\Event\LifecycleEventArgs; use Doctrine\ORM\Event\OnFlushEventArgs; -use Doctrine\ORM\Event\PostFlushEventArgs; -use Doctrine\ORM\Event\PreUpdateEventArgs; use Doctrine\ORM\Events; -use Psr\Log\LoggerInterface; +use Doctrine\Persistence\Event\LifecycleEventArgs; use ReflectionProperty; use SpecShaper\EncryptBundle\Annotations\Encrypted; use SpecShaper\EncryptBundle\Encryptors\EncryptorInterface; use SpecShaper\EncryptBundle\Exception\EncryptException; -use Symfony\Component\PropertyAccess\PropertyAccess; +use Psr\Log\LoggerInterface; + /** * Doctrine event subscriber which encrypt/decrypt entities. @@ -33,29 +31,13 @@ class DoctrineEncryptSubscriber implements EventSubscriber, DoctrineEncryptSubsc */ protected array $annotationArray; - /** - * Register to avoid multi decode operations for one entity. - */ - private array $decodedRegistry = []; - - /** - * An array of decoded values populated during the onLoad event. - * Used to compare any resubmitted values during onFlush event. - * If the flushed unencoded value is the same as in the array then there is no change - * to the value and the entity field update is removed from the Unit of Work change set. - */ - private array $decodedValues = []; - /** * Caches information on an entity's encrypted fields in an array keyed on * the entity's class name. The value will be a list of Reflected fields that are encrypted. */ protected array $encryptedFieldCache = []; - /** - * Remember which entities have to be decrypted back in postFlush after onFlush. - */ - private array $postFlushDecryptQueue = []; + private array $rawValues = []; private bool $isDisabled; @@ -65,8 +47,7 @@ public function __construct( private EncryptorInterface $encryptor, array $annotationArray, bool $isDisabled - ) - { + ) { $this->annotationArray = $annotationArray; $this->isDisabled = $isDisabled; } @@ -98,8 +79,8 @@ public function getSubscribedEvents(): array { return [ Events::postLoad, + Events::postUpdate, Events::onFlush, - Events::postFlush, ]; } @@ -115,59 +96,13 @@ public function onFlush(OnFlushEventArgs $args): void $em = $args->getObjectManager(); $unitOfWork = $em->getUnitOfWork(); - $this->postFlushDecryptQueue = []; - foreach ($unitOfWork->getScheduledEntityInsertions() as $entity) { - $this->entityOnFlush($entity, $em); - $unitOfWork->recomputeSingleEntityChangeSet($em->getClassMetadata(get_class($entity)), $entity); + $this->processFields($entity, $em, true, true); } foreach ($unitOfWork->getScheduledEntityUpdates() as $entity) { - $this->entityOnFlush($entity, $em); - $unitOfWork->recomputeSingleEntityChangeSet($em->getClassMetadata(get_class($entity)), $entity); - } - } - - /** - * Processes the entity for an onFlush event. - * - * @param $entity - * @param EntityManagerInterface $em - * @throws EncryptException - */ - protected function entityOnFlush($entity, EntityManagerInterface $em): void - { - if ($this->isDisabled) { - return; - } - - // Add the entity to a decrypt Queue for postFlush decryption. - $this->postFlushDecryptQueue[] = $entity; - $this->processFields($entity, $em); - } - - /** - * After we have persisted the entities, we want to have the - * decrypted information available once more. - */ - public function postFlush(PostFlushEventArgs $args): void - { - if ($this->isDisabled) { - return; - } - - foreach ($this->postFlushDecryptQueue as $entity) { - $hasFieldsEncrypted = $this->processFields($entity, $args->getEntityManager(), false); - - // If no fields were marked encrypted then skip. - if (false === $hasFieldsEncrypted) { - continue; - } - - $this->addToDecodedRegistry($entity); + $this->processFields($entity, $em, true, false); } - - $this->postFlushDecryptQueue = []; } /** @@ -179,20 +114,8 @@ public function postFlush(PostFlushEventArgs $args): void public function postLoad(LifecycleEventArgs $args): void { $entity = $args->getObject(); - $em = $args->getObjectManager(); - - // If this entity has already been decoded in an earlier postFlush event then do nothing. - if ($this->hasInDecodedRegistry($entity)) { - return; - } - // Decrypt the entity fields. - $hasFieldsEncrypted = $this->processFields($entity, $em, false); - - // If the entity contained encrypted fields that were decrypted then add to a registry. - if ($hasFieldsEncrypted) { - $this->addToDecodedRegistry($entity); - } + $this->processFields($entity, $args->getObjectManager(), false, false); } /** @@ -223,7 +146,7 @@ public function getEncryptionableProperties(array $allProperties): array /** * Process (encrypt/decrypt) entities fields. */ - protected function processFields(object $entity, EntityManagerInterface $em, ?bool $isEncryptOperation = true): bool + protected function processFields(object $entity, EntityManagerInterface $em, bool $isEncryptOperation, bool $isInsert): bool { // Get the encrypted properties in the entity. $properties = $this->getEncryptedFields($entity, $em); @@ -236,12 +159,7 @@ protected function processFields(object $entity, EntityManagerInterface $em, ?bo $unitOfWork = $em->getUnitOfWork(); $oid = spl_object_id($entity); - if(!array_key_exists($oid, $this->decodedValues)){ - $this->decodedValues[$oid] = []; - } - foreach ($properties as $key => $refProperty) { - // Get the value in the entity. $value = $refProperty->getValue($entity); @@ -256,60 +174,51 @@ protected function processFields(object $entity, EntityManagerInterface $em, ?bo // Encryption is fired by onFlush event, else it is an onLoad event. if ($isEncryptOperation) { + $changeSet = $unitOfWork->getEntityChangeSet($entity); - // If the field has already been decrypted by the onLoad event, and the flushed value is the same - if(isset($this->decodedValues[$oid][$refProperty->getName()]) && $this->decodedValues[$oid][$refProperty->getName()][1] === $value){ - - // Remove the field from the UoW change set. - unset($unitOfWork->getEntityChangeSet($entity)[$refProperty->getName()]); - - // Get the originally created encrypted value. - $encryptedValue = $this->decodedValues[$oid][$refProperty->getName()][0]; - - // Reset that to the original in the UoW. - $unitOfWork->setOriginalEntityProperty($oid, $refProperty->getName(), $encryptedValue); - } else { - // The field is part of an insert or the value of the field has changed, then create a new encrypted value. + // Encrypt value only if change has been detected by Doctrine (comparing unencrypted values, see postLoad flow) + if (isset($changeSet[$refProperty->getName()])) { $encryptedValue = $this->encryptor->encrypt($value); + $refProperty->setValue($entity, $encryptedValue); + $unitOfWork->recomputeSingleEntityChangeSet($em->getClassMetadata(get_class($entity)), $entity); + + if ($isInsert) { + // Restore the decrypted value after the change set update + $refProperty->setValue($entity, $value); + } else { + // Will be restored during postUpdate cycle + $this->rawValues[$oid][$refProperty->getName()] = $value; + } } - - // Replace the unencrypted value with the encrypted value on the entity. - $refProperty->setValue($entity, $encryptedValue); - } else { // Decryption is fired by onLoad and postFlush events. $decryptedValue = $this->decryptValue($value); $refProperty->setValue($entity, $decryptedValue); - // Store the decrypted value for comparison during a flush event. - $this->decodedValues[$oid][$refProperty->getName()] = [$value, $decryptedValue]; - - // We don't want the object to be dirty immediately after reading - $unitOfWork->setOriginalEntityProperty($oid, $refProperty->getName(), $value); + // Tell Doctrine the original value was the decrypted one. + $unitOfWork->setOriginalEntityProperty($oid, $refProperty->getName(), $decryptedValue); } } return !empty($properties); } - /** - * Check if we have entity in decoded registry. - * - * @param object $entity Some doctrine entity - */ - protected function hasInDecodedRegistry(object $entity): bool + public function postUpdate(LifecycleEventArgs $args): void { - return isset($this->decodedRegistry[spl_object_id($entity)]); - } + $entity = $args->getObject(); + $em = $args->getObjectManager(); - /** - * Adds entity to decoded registry. - * - * @param object $entity Some doctrine entity - */ - protected function addToDecodedRegistry(object $entity): void - { - $this->decodedRegistry[spl_object_id($entity)] = true; + $oid = spl_object_id($entity); + if (isset($this->rawValues[$oid])) { + $className = get_class($entity); + $meta = $em->getClassMetadata($className); + foreach ($this->rawValues[$oid] as $prop => $rawValue) { + $refProperty = $meta->getReflectionProperty($prop); + $refProperty->setValue($entity, $rawValue); + } + + unset($this->rawValues[$oid]); + } } /** @@ -354,6 +263,7 @@ private function isEncryptedProperty(ReflectionProperty $refProperty) Please use #[Encrypted] attribute instead.', $refProperty )); + return true; } }