<?php
namespace Eckinox\SensitiveDataBundle\EventSubscriber;
use Eckinox\SensitiveDataBundle\Form\SensitiveTypeInterface;
use Eckinox\SensitiveDataBundle\Security\SensitiveDataMapper;
use Eckinox\SensitiveDataBundle\Security\SensitiveDataRetriever;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;
/**
* The `SensitiveFieldSubscriber`'s job is to set the data of
* sensitive fields to their original value if they haven't been
* changed by the user, in order to prevent empty or bogus values
* from replacing the real user data when submitting the form.
*/
class SensitiveFieldSubscriber implements EventSubscriberInterface
{
/** @var SensitiveDataMapper */
private $sensitiveDataMapper;
/** @var SensitiveDataRetriever */
private $sensitiveDataRetriever;
/** @var Request */
private $request;
/** @var array<string,array<int,object>> */
private $preChangeEntities = [];
public function __construct(SensitiveDataMapper $sensitiveDataMapper, SensitiveDataRetriever $sensitiveDataRetriever, RequestStack $requestStack)
{
$this->sensitiveDataMapper = $sensitiveDataMapper;
$this->sensitiveDataRetriever = $sensitiveDataRetriever;
$this->request = $requestStack->getCurrentRequest();
}
public static function getSubscribedEvents()
{
return [
FormEvents::POST_SET_DATA => 'onPostSetData',
FormEvents::SUBMIT => 'onSubmit',
];
}
public function onPostSetData(FormEvent $event): void
{
if (empty($this->request->request->all())) {
return;
}
$form = $event->getForm();
$this->storeSensitiveEntityData($form);
}
public function onSubmit(FormEvent $event): void
{
$form = $event->getForm();
$this->restoreUnchangedSensitiveValues($form);
}
/**
* Stores a version of each entity affected by the form before the user's
* submitted data is loaded into them.
*
* This allows us to restore the sensitive data on submit for situations
* where it would've been replaced by the "• • • • •" placeholder value.
*/
private function storeSensitiveEntityData(FormInterface $form): void
{
static $handledFormTypes = [];
$objectId = spl_object_id($form);
if (isset($handledFormTypes[$objectId])) {
return;
}
$handledFormTypes[$objectId] = true;
// Check if the form is sensitive - if so, it needs to be processed
if ($form->getConfig()->getType()->getInnerType() instanceof SensitiveTypeInterface) {
$entity = $this->sensitiveDataMapper->getEntityFromForm($form);
$entityClass = get_class($entity);
if (!isset($this->preChangeEntities[$entityClass])) {
$this->preChangeEntities[$entityClass] = [];
}
$this->preChangeEntities[$entityClass][$entity->getId()] = clone $entity;
}
// Recurse on all child forms
foreach ($form->all() as $childForm) {
$this->storeSensitiveEntityData($childForm);
}
}
/**
* Restores the sensitive data that has been replaced by the "• • • • •"
* placeholder value.
*/
private function restoreUnchangedSensitiveValues(FormInterface $form): void
{
static $handledFormTypes = [];
$objectId = spl_object_id($form);
if (isset($handledFormTypes[$objectId])) {
return;
}
$handledFormTypes[$objectId] = true;
// Check if the form is sensitive - if so, it needs to be processed
if ($form->getConfig()->getType()->getInnerType() instanceof SensitiveTypeInterface) {
$value = $form->getData();
// Check if the value is the sensitive data replacement/placeholder
if (strpos($value, '•') !== false && !strlen(str_replace([' ', '•'], '', $value))) {
// Retrieve the secret from the pre-submit entity
$entity = $this->sensitiveDataMapper->getEntityFromForm($form);
$preChangeEntity = $this->preChangeEntities[get_class($entity)][$entity->getId()] ?? null;
$mapping = $this->sensitiveDataMapper->getMapping($entity, $form->getName());
$realValue = $this->sensitiveDataRetriever->getSecret($mapping, $preChangeEntity ?: $entity);
// Restore the secret in the entity
$caseConverter = new CamelCaseToSnakeCaseNameConverter();
$setter = 'set'.ucfirst($caseConverter->denormalize($form->getName()));
$entity->$setter($realValue);
}
}
// Recurse on all child forms
foreach ($form->all() as $childForm) {
$this->restoreUnchangedSensitiveValues($childForm);
}
}
}