From 96f2bfaa2075fcdbcbf0a7cfae1bac00df3da728 Mon Sep 17 00:00:00 2001 From: Rostislav Wolny Date: Tue, 6 Oct 2020 11:47:23 +0200 Subject: [PATCH] Add entity lifecycle listener for emoji sanitisation [MAILPOET-3196] --- lib/DI/ContainerConfigurator.php | 1 + lib/Doctrine/EntityManagerFactory.php | 13 ++++- .../EventListeners/EmojiEncodingListener.php | 34 +++++++++++++ lib/WP/Emoji.php | 6 +++ .../EmojiEncodingListenerTest.php | 50 +++++++++++++++++++ .../EventListeners/TimestampListenerTest.php | 5 +- .../EventListeners/ValidationTest.php | 5 +- .../Doctrine/Types/JsonTypesTest.php | 5 +- 8 files changed, 115 insertions(+), 4 deletions(-) create mode 100644 lib/Doctrine/EventListeners/EmojiEncodingListener.php create mode 100644 tests/integration/Doctrine/EventListeners/EmojiEncodingListenerTest.php diff --git a/lib/DI/ContainerConfigurator.php b/lib/DI/ContainerConfigurator.php index 8d2256db74..2ebc0cab76 100644 --- a/lib/DI/ContainerConfigurator.php +++ b/lib/DI/ContainerConfigurator.php @@ -124,6 +124,7 @@ class ContainerConfigurator implements IContainerConfigurator { $container->autowire(\MailPoetVendor\Doctrine\ORM\EntityManager::class) ->setFactory([new Reference(\MailPoet\Doctrine\EntityManagerFactory::class), 'createEntityManager']) ->setPublic(true); + $container->autowire(\MailPoet\Doctrine\EventListeners\EmojiEncodingListener::class)->setPublic(true); $container->autowire(\MailPoet\Doctrine\EventListeners\TimestampListener::class); $container->autowire(\MailPoet\Doctrine\EventListeners\ValidationListener::class); $container->autowire(\MailPoet\Doctrine\Validator\ValidatorFactory::class); diff --git a/lib/Doctrine/EntityManagerFactory.php b/lib/Doctrine/EntityManagerFactory.php index 926774fb16..225e6b62dc 100644 --- a/lib/Doctrine/EntityManagerFactory.php +++ b/lib/Doctrine/EntityManagerFactory.php @@ -2,6 +2,7 @@ namespace MailPoet\Doctrine; +use MailPoet\Doctrine\EventListeners\EmojiEncodingListener; use MailPoet\Doctrine\EventListeners\TimestampListener; use MailPoet\Doctrine\EventListeners\ValidationListener; use MailPoet\Tracy\DoctrinePanel\DoctrinePanel; @@ -25,16 +26,21 @@ class EntityManagerFactory { /** @var ValidationListener */ private $validationListener; + /** @var EmojiEncodingListener */ + private $emojiEncodingListener; + public function __construct( Connection $connection, Configuration $configuration, TimestampListener $timestampListener, - ValidationListener $validationListener + ValidationListener $validationListener, + EmojiEncodingListener $emojiEncodingListener ) { $this->connection = $connection; $this->configuration = $configuration; $this->timestampListener = $timestampListener; $this->validationListener = $validationListener; + $this->emojiEncodingListener = $emojiEncodingListener; } public function createEntityManager() { @@ -56,5 +62,10 @@ class EntityManagerFactory { [Events::onFlush], $this->validationListener ); + + $entityManager->getEventManager()->addEventListener( + [Events::prePersist, Events::preUpdate], + $this->emojiEncodingListener + ); } } diff --git a/lib/Doctrine/EventListeners/EmojiEncodingListener.php b/lib/Doctrine/EventListeners/EmojiEncodingListener.php new file mode 100644 index 0000000000..eba88eb505 --- /dev/null +++ b/lib/Doctrine/EventListeners/EmojiEncodingListener.php @@ -0,0 +1,34 @@ +emoji = $emoji; + } + + public function prePersist(LifecycleEventArgs $eventArgs) { + $this->sanitizeEmojiBeforeSaving($eventArgs); + } + + public function preUpdate(LifecycleEventArgs $eventArgs) { + $this->sanitizeEmojiBeforeSaving($eventArgs); + } + + private function sanitizeEmojiBeforeSaving(LifecycleEventArgs $eventArgs) { + $entity = $eventArgs->getEntity(); + if ($entity instanceof FormEntity) { + $body = $entity->getBody(); + if ($body !== null) { + $entity->setBody($this->emoji->sanitizeEmojisInFormBody($body)); + } + } + } +} diff --git a/lib/WP/Emoji.php b/lib/WP/Emoji.php index 75ba1fc861..948a4a2b1e 100644 --- a/lib/WP/Emoji.php +++ b/lib/WP/Emoji.php @@ -29,6 +29,12 @@ class Emoji { return $this->decodeEntities($newsletterRenderedBody); } + public function sanitizeEmojisInFormBody(array $body): array { + $bodyJson = json_encode($body, JSON_UNESCAPED_UNICODE); + $fixedJson = $this->encodeForUTF8Column(MP_FORMS_TABLE, 'body', $bodyJson); + return json_decode($fixedJson, true); + } + private function encodeRenderedBodyForUTF8Column($value) { return $this->encodeForUTF8Column( MP_SENDING_QUEUES_TABLE, diff --git a/tests/integration/Doctrine/EventListeners/EmojiEncodingListenerTest.php b/tests/integration/Doctrine/EventListeners/EmojiEncodingListenerTest.php new file mode 100644 index 0000000000..2dc70acab7 --- /dev/null +++ b/tests/integration/Doctrine/EventListeners/EmojiEncodingListenerTest.php @@ -0,0 +1,50 @@ +setBody(['body']); + $emojiMock = $this->createMock(Emoji::class); + $emojiMock->expects($this->exactly(2)) + ->method('sanitizeEmojisInFormBody') + ->willReturn(['sanitizedBody']); + $emojiEncodingListenerWithMockedEmoji = new EmojiEncodingListener($emojiMock); + $originalListener = $this->diContainer->get(EmojiEncodingListener::class); + $this->replaceListeners($originalListener, $emojiEncodingListenerWithMockedEmoji); + $this->entityManager->persist($form); + $this->entityManager->flush($form); + expect($form->getBody())->equals(['sanitizedBody']); + $form->setBody(['updatedBody']); + $this->entityManager->flush($form); + expect($form->getBody())->equals(['sanitizedBody']); + $this->replaceListeners($emojiEncodingListenerWithMockedEmoji, $originalListener); + } + + /** + * We have to replace event listeners since EventManager + * is shared for all entity managers using same DB connection + */ + private function replaceListeners($original, $replacement) { + $this->entityManager->getEventManager()->removeEventListener( + [Events::prePersist, Events::preUpdate], + $original + ); + + $this->entityManager->getEventManager()->addEventListener( + [Events::prePersist, Events::preUpdate], + $replacement + ); + } + + public function _after() { + parent::_after(); + $this->truncateEntity(FormEntity::class); + } +} diff --git a/tests/integration/Doctrine/EventListeners/TimestampListenerTest.php b/tests/integration/Doctrine/EventListeners/TimestampListenerTest.php index 7b51f64a28..a5823f30a9 100644 --- a/tests/integration/Doctrine/EventListeners/TimestampListenerTest.php +++ b/tests/integration/Doctrine/EventListeners/TimestampListenerTest.php @@ -5,9 +5,11 @@ namespace MailPoet\Test\Doctrine\EventListeners; use MailPoet\Doctrine\Annotations\AnnotationReaderProvider; use MailPoet\Doctrine\ConfigurationFactory; use MailPoet\Doctrine\EntityManagerFactory; +use MailPoet\Doctrine\EventListeners\EmojiEncodingListener; use MailPoet\Doctrine\EventListeners\TimestampListener; use MailPoet\Doctrine\EventListeners\ValidationListener; use MailPoet\Doctrine\Validator\ValidatorFactory; +use MailPoet\WP\Emoji; use MailPoet\WP\Functions as WPFunctions; use MailPoetVendor\Carbon\Carbon; use MailPoetVendor\Doctrine\Common\Cache\ArrayCache; @@ -91,7 +93,8 @@ class TimestampListenerTest extends \MailPoetTest { $validatorFactory = new ValidatorFactory($annotationReaderProvider); $timestampListener = new TimestampListener($this->wp); $validationListener = new ValidationListener($validatorFactory->createValidator()); - $entityManagerFactory = new EntityManagerFactory($this->connection, $configuration, $timestampListener, $validationListener); + $emojiEncodingListener = new EmojiEncodingListener(new Emoji($this->wp)); + $entityManagerFactory = new EntityManagerFactory($this->connection, $configuration, $timestampListener, $validationListener, $emojiEncodingListener); return $entityManagerFactory->createEntityManager(); } } diff --git a/tests/integration/Doctrine/EventListeners/ValidationTest.php b/tests/integration/Doctrine/EventListeners/ValidationTest.php index a9f9d7aee9..a4ec80ba46 100644 --- a/tests/integration/Doctrine/EventListeners/ValidationTest.php +++ b/tests/integration/Doctrine/EventListeners/ValidationTest.php @@ -5,10 +5,12 @@ namespace MailPoet\Test\Doctrine\EventListeners; use MailPoet\Doctrine\Annotations\AnnotationReaderProvider; use MailPoet\Doctrine\ConfigurationFactory; use MailPoet\Doctrine\EntityManagerFactory; +use MailPoet\Doctrine\EventListeners\EmojiEncodingListener; use MailPoet\Doctrine\EventListeners\TimestampListener; use MailPoet\Doctrine\EventListeners\ValidationListener; use MailPoet\Doctrine\Validator\ValidationException; use MailPoet\Doctrine\Validator\ValidatorFactory; +use MailPoet\WP\Emoji; use MailPoet\WP\Functions as WPFunctions; use MailPoetVendor\Doctrine\Common\Cache\ArrayCache; @@ -75,7 +77,8 @@ class ValidationTest extends \MailPoetTest { $validatorFactory = new ValidatorFactory($annotationReaderProvider); $timestampListener = new TimestampListener($this->wp); $validationListener = new ValidationListener($validatorFactory->createValidator()); - $entityManagerFactory = new EntityManagerFactory($this->connection, $configuration, $timestampListener, $validationListener); + $emojiEncodingListener = new EmojiEncodingListener(new Emoji($this->wp)); + $entityManagerFactory = new EntityManagerFactory($this->connection, $configuration, $timestampListener, $validationListener, $emojiEncodingListener); return $entityManagerFactory->createEntityManager(); } } diff --git a/tests/integration/Doctrine/Types/JsonTypesTest.php b/tests/integration/Doctrine/Types/JsonTypesTest.php index 8264970248..aaaff76ab7 100644 --- a/tests/integration/Doctrine/Types/JsonTypesTest.php +++ b/tests/integration/Doctrine/Types/JsonTypesTest.php @@ -6,10 +6,12 @@ use Exception; use MailPoet\Doctrine\Annotations\AnnotationReaderProvider; use MailPoet\Doctrine\ConfigurationFactory; use MailPoet\Doctrine\EntityManagerFactory; +use MailPoet\Doctrine\EventListeners\EmojiEncodingListener; use MailPoet\Doctrine\EventListeners\TimestampListener; use MailPoet\Doctrine\EventListeners\ValidationListener; use MailPoet\Doctrine\Validator\ValidatorFactory; use MailPoet\Test\Doctrine\Types\JsonEntity; +use MailPoet\WP\Emoji; use MailPoet\WP\Functions as WPFunctions; use MailPoetVendor\Doctrine\Common\Cache\ArrayCache; use RuntimeException; @@ -183,7 +185,8 @@ class JsonTypesTest extends \MailPoetTest { $validatorFactory = new ValidatorFactory($annotationReaderProvider); $timestampListener = new TimestampListener($this->wp); $validationListener = new ValidationListener($validatorFactory->createValidator()); - $entityManagerFactory = new EntityManagerFactory($this->connection, $configuration, $timestampListener, $validationListener); + $emojiEncodingListener = new EmojiEncodingListener(new Emoji($this->wp)); + $entityManagerFactory = new EntityManagerFactory($this->connection, $configuration, $timestampListener, $validationListener, $emojiEncodingListener); return $entityManagerFactory->createEntityManager(); } }