From b16c18fe0bea557ab4a6df8e17124e63be1370f5 Mon Sep 17 00:00:00 2001 From: Alexander Kellner Date: Fri, 2 Feb 2024 16:41:26 +0100 Subject: [PATCH] [FEATURE] Add two basic events to LUXletter 1) to stop testmails and implement own testmail service 2) to set own bodytext for newsletter mails Related: https://github.com/in2code-de/luxletter/pull/205/files --- Classes/Controller/NewsletterController.php | 78 +++++++++++++-- Classes/Domain/Repository/LogRepository.php | 9 +- .../AfterTestMailButtonClickedEvent.php | 96 +++++++++++++++++++ .../Events/BeforeBodytextIsParsedEvent.php | 35 +++++++ Classes/Mail/ProgressQueue.php | 6 +- Documentation/Index.md | 1 + Documentation/Tech/Events.md | 70 ++++++++++++++ Resources/Private/Build/JavaScript/Module.js | 9 +- .../Public/JavaScript/Luxletter/Module.min.js | 2 +- 9 files changed, 290 insertions(+), 16 deletions(-) create mode 100644 Classes/Events/AfterTestMailButtonClickedEvent.php create mode 100644 Classes/Events/BeforeBodytextIsParsedEvent.php create mode 100644 Documentation/Tech/Events.md diff --git a/Classes/Controller/NewsletterController.php b/Classes/Controller/NewsletterController.php index f06eabaa..6e24fe8a 100644 --- a/Classes/Controller/NewsletterController.php +++ b/Classes/Controller/NewsletterController.php @@ -3,6 +3,7 @@ declare(strict_types=1); namespace In2code\Luxletter\Controller; +use Doctrine\DBAL\Driver\Exception as ExceptionDbalDriver; use In2code\Lux\Domain\Repository\VisitorRepository; use In2code\Luxletter\Domain\Model\Dto\Filter; use In2code\Luxletter\Domain\Model\Newsletter; @@ -11,15 +12,23 @@ use In2code\Luxletter\Domain\Service\PreviewUrlService; use In2code\Luxletter\Domain\Service\QueueService; use In2code\Luxletter\Domain\Service\ReceiverAnalysisService; +use In2code\Luxletter\Events\AfterTestMailButtonClickedEvent; +use In2code\Luxletter\Exception\ApiConnectionException; use In2code\Luxletter\Exception\AuthenticationFailedException; +use In2code\Luxletter\Exception\InvalidUrlException; +use In2code\Luxletter\Exception\MisconfigurationException; use In2code\Luxletter\Mail\TestMail; use In2code\Luxletter\Utility\BackendUserUtility; use In2code\Luxletter\Utility\ConfigurationUtility; use In2code\Luxletter\Utility\LocalizationUtility; use In2code\Luxletter\Utility\ObjectUtility; +use JsonException; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; +use TYPO3\CMS\Core\Configuration\Exception\ExtensionConfigurationExtensionNotConfiguredException; +use TYPO3\CMS\Core\Configuration\Exception\ExtensionConfigurationPathDoesNotExistException; use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Extbase\Configuration\Exception\InvalidConfigurationTypeException; use TYPO3\CMS\Fluid\View\StandaloneView; class NewsletterController extends AbstractNewsletterController @@ -220,36 +229,80 @@ public function wizardUserPreviewAjax(ServerRequestInterface $request): Response return $response; } + /** + * @param ServerRequestInterface $request + * @return ResponseInterface + * @throws AuthenticationFailedException + * @throws ExceptionDbalDriver + * @throws ApiConnectionException + * @throws InvalidUrlException + * @throws MisconfigurationException + * @throws JsonException + * @throws ExtensionConfigurationExtensionNotConfiguredException + * @throws ExtensionConfigurationPathDoesNotExistException + * @throws InvalidConfigurationTypeException + */ public function testMailAjax(ServerRequestInterface $request): ResponseInterface { if (BackendUserUtility::isBackendUserAuthenticated() === false) { throw new AuthenticationFailedException('You are not authenticated to send mails', 1560872725); } - $testMail = GeneralUtility::makeInstance(TestMail::class); - $status = $testMail->preflight( - $request->getQueryParams()['origin'], - $request->getQueryParams()['layout'], - (int)$request->getQueryParams()['configuration'], - $request->getQueryParams()['subject'], - $request->getQueryParams()['email'] - ); + $status = null; + + /** + * This event can be used for sending the Test-email with external logic + * @see Documentation/Tech/Events.md + */ + $event = GeneralUtility::makeInstance(AfterTestMailButtonClickedEvent::class, $request); + $this->eventDispatcher->dispatch($event); + + if ($event->isTestMailIsSendExternal() === false) { + $testMail = GeneralUtility::makeInstance(TestMail::class); + $status = $testMail->preflight( + $request->getQueryParams()['origin'], + $request->getQueryParams()['layout'], + (int)$request->getQueryParams()['configuration'], + $request->getQueryParams()['subject'], + $request->getQueryParams()['email'] + ); + } + $responseData = [ + 'status' => $status ?? $event->getStatus(), + ]; + if ($event->isTestMailIsSendExternal()) { + $responseData += $event->getStatusResponse(); + } $response = ObjectUtility::getJsonResponse(); - $response->getBody()->write(json_encode(['status' => $status])); + $response->getBody()->write(json_encode($responseData, JSON_THROW_ON_ERROR)); return $response; } + /** + * @param ServerRequestInterface $request + * @return ResponseInterface + * @throws AuthenticationFailedException + * @throws ExceptionDbalDriver + * @throws ExtensionConfigurationExtensionNotConfiguredException + * @throws ExtensionConfigurationPathDoesNotExistException + * @throws InvalidConfigurationTypeException + * @throws MisconfigurationException + */ public function previewSourcesAjax(ServerRequestInterface $request): ResponseInterface { if (BackendUserUtility::isBackendUserAuthenticated() === false) { throw new AuthenticationFailedException('You are not authenticated to send mails', 1645707268); } $previewUrlService = GeneralUtility::makeInstance(PreviewUrlService::class); - $response = ObjectUtility::getJsonResponse(); $content = $previewUrlService->get($request->getQueryParams()['origin'], $request->getQueryParams()['layout']); + $response = ObjectUtility::getJsonResponse(); $response->getBody()->write(json_encode($content)); return $response; } + /** + * @param ServerRequestInterface $request + * @return ResponseInterface + */ public function receiverDetailAjax(ServerRequestInterface $request): ResponseInterface { $userRepository = GeneralUtility::makeInstance(UserRepository::class); @@ -270,6 +323,11 @@ public function receiverDetailAjax(ServerRequestInterface $request): ResponseInt return $response; } + /** + * @return void + * @throws ExtensionConfigurationExtensionNotConfiguredException + * @throws ExtensionConfigurationPathDoesNotExistException + */ protected function addDocumentHeaderForNewsletterController(): void { $menuConfiguration = [ diff --git a/Classes/Domain/Repository/LogRepository.php b/Classes/Domain/Repository/LogRepository.php index c651af20..3dc522c9 100644 --- a/Classes/Domain/Repository/LogRepository.php +++ b/Classes/Domain/Repository/LogRepository.php @@ -245,11 +245,14 @@ public function findRawByUser(User $user, array $statusWhitelist = [], array $st } /** - * @param User $user - * @return QueryResultInterface + * @param ?User $user + * @return ?QueryResultInterface */ - public function findByUser(User $user): QueryResultInterface + public function findByUser(?User $user): ?QueryResultInterface { + if ($user === null) { + return null; + } $query = $this->createQuery(); $query->matching($query->equals('user', $user)); $query->setLimit(100); diff --git a/Classes/Events/AfterTestMailButtonClickedEvent.php b/Classes/Events/AfterTestMailButtonClickedEvent.php new file mode 100644 index 00000000..40ba3b40 --- /dev/null +++ b/Classes/Events/AfterTestMailButtonClickedEvent.php @@ -0,0 +1,96 @@ +request = $request; + } + + public function getRequest(): ServerRequestInterface + { + return $this->request; + } + + public function isTestMailIsSendExternal(): bool + { + return $this->testMailIsSendExternal; + } + + public function setTestMailIsSendExternal(): self + { + $this->testMailIsSendExternal = true; + return $this; + } + + public function getStatus(): bool + { + return $this->status; + } + + public function setStatus(bool $status): self + { + $this->status = $status; + return $this; + } + + public function getStatusTitle(): string + { + return $this->statusTitle; + } + + public function setStatusTitle(string $statusTitle): self + { + $this->statusTitle = $statusTitle; + return $this; + } + + public function getStatusMessage(): string + { + return $this->statusMessage; + } + + public function setStatusMessage(string $statusMessage): self + { + $this->statusMessage = $statusMessage; + return $this; + } + + public function getStatusSeverity(): string + { + return $this->statusSeverity; + } + + public function setStatusSeverity(string $statusSeverity): self + { + $this->statusSeverity = $statusSeverity; + return $this; + } + + public function getStatusResponse(): array + { + return [ + 'statusTitle' => $this->getStatusTitle(), + 'statusMessage' => $this->getStatusMessage(), + 'statusSeverity' => $this->getStatusSeverity(), + ]; + } +} diff --git a/Classes/Events/BeforeBodytextIsParsedEvent.php b/Classes/Events/BeforeBodytextIsParsedEvent.php new file mode 100644 index 00000000..be18e133 --- /dev/null +++ b/Classes/Events/BeforeBodytextIsParsedEvent.php @@ -0,0 +1,35 @@ +queue = $queue; + $this->bodytext = $queue->getNewsletter()->getBodytext(); + } + + public function getQueue(): Queue + { + return $this->queue; + } + + public function getBodytext(): string + { + return $this->bodytext; + } + + public function setBodytext(string $bodytext): self + { + $this->bodytext = $bodytext; + return $this; + } +} diff --git a/Classes/Mail/ProgressQueue.php b/Classes/Mail/ProgressQueue.php index 4735cb30..8334a4a8 100644 --- a/Classes/Mail/ProgressQueue.php +++ b/Classes/Mail/ProgressQueue.php @@ -9,6 +9,7 @@ use In2code\Luxletter\Domain\Service\BodytextManipulation\LinkHashing; use In2code\Luxletter\Domain\Service\LogService; use In2code\Luxletter\Domain\Service\Parsing\Newsletter; +use In2code\Luxletter\Events\BeforeBodytextIsParsedEvent; use In2code\Luxletter\Events\ProgressQueueEvent; use In2code\Luxletter\Exception\ArgumentMissingException; use In2code\Luxletter\Exception\MisconfigurationException; @@ -181,8 +182,11 @@ protected function getSubject(Queue $queue): string */ protected function getBodyText(Queue $queue): string { + $event = GeneralUtility::makeInstance(BeforeBodytextIsParsedEvent::class, $queue); + $this->eventDispatcher->dispatch($event); + $bodytext = $this->parseService->parseBodytext( - $queue->getNewsletter()->getBodytext(), + $event->getBodytext(), [ 'user' => $queue->getUser(), 'newsletter' => $queue->getNewsletter(), diff --git a/Documentation/Index.md b/Documentation/Index.md index 96bfc051..f44a7c7d 100644 --- a/Documentation/Index.md +++ b/Documentation/Index.md @@ -21,6 +21,7 @@ Send newsletters the easy way ### Tech corner and FAQ [Tech corner and FAQ](Tech/Index.md) +[Events](Tech/Events.md) ### Changelog and breaking changes diff --git a/Documentation/Tech/Events.md b/Documentation/Tech/Events.md new file mode 100644 index 00000000..12bffc3f --- /dev/null +++ b/Documentation/Tech/Events.md @@ -0,0 +1,70 @@ + + +# Luxletter - Email marketing in TYPO3. Send newsletters the easy way. + +## Events + +There are some events that can be used to extend Luxletter. +This documentation is under construction and not all events are documented yet. + +### AfterTestMailButtonClickedEvent + +This event can be used to deactivate the internal sending of test emails and implement your own logic, e.g. sending test newsletters via a separate queue. + +To deactivate the internal logic, the `$testMailIsSendExternal` property of the event must be set to true. +If the general status `$status` is set to `false`, no message is shown. +If the general status `$status` is set to `true`, a message with the properties `$statusTitle`, `$statusMessage` and `$statusSeverity` is shown. +The values for `$statusSeverity` should be `AfterTestMailButtonClickedEvent::STATUS_SEVERITY_SUCCESS`, `AfterTestMailButtonClickedEvent::STATUS_SEVERITY_WARNING` and `AfterTestMailButtonClickedEvent::STATUS_SEVERITY_ERROR`, the default value is `AfterTestMailButtonClickedEvent::STATUS_SEVERITY_ERROR`. + +The `$request` property is available in the event, from which all necessary data can be obtained to send the test e-mail. + +Sample Eventlistener: + +``` +setTestMailIsSendExternal(true); + // ... handle email sending + $event->setStatus(true); + $event->setStatusSeverity(AfterTestMailButtonClickedEvent::STATUS_SEVERITY_SUCCESS); + $event->setStatusTitle('Success'); + $event->setStatusMessage('The test email is successfully added to the queue'); + } +} +``` + +### BeforeBodytextIsParsedEvent + +This event is executed before the body text for the individual newsletter is processed. The event can be used to change or reset the body text that is used for parsing. + +The `$queue` property is available in the event, which can be used to access all relevant data for the newsletter. +If the `$bodytext` property is set via an event listener, the content of this property is used for further processing. If there is no content in the property, the standard text from the newsletter is used. + +Sample Eventlistener: + +``` +setBodytext('

I am the new body

'); + } +} +``` diff --git a/Resources/Private/Build/JavaScript/Module.js b/Resources/Private/Build/JavaScript/Module.js index 2fd81316..4b95be97 100644 --- a/Resources/Private/Build/JavaScript/Module.js +++ b/Resources/Private/Build/JavaScript/Module.js @@ -265,13 +265,20 @@ define(['jquery'], function($) { */ this.testMailListenerCallback = function(response) { const messageElement = document.querySelector('[data-luxletter-testmail="message"]'); - if (messageElement !== null && response.status === true) { + if (messageElement !== null && response.statusSeverity === undefined && response.status === true) { showElement(messageElement); const counterElement = messageElement.querySelector('p>span'); let counter = parseInt(counterElement.innerHTML); counter++; counterElement.innerHTML = counter.toString(); + } else if (messageElement !== null && response.statusSeverity !== '' && response.status === true) { + messageElement.classList.remove(...messageElement.classList); + messageElement.classList.add('alert'); + messageElement.classList.add(response.statusSeverity); + messageElement.querySelector('.alert-heading').innerHTML = response.statusTitle; + messageElement.querySelector('p').innerHTML = response.statusMessage; + showElement(messageElement); } }; diff --git a/Resources/Public/JavaScript/Luxletter/Module.min.js b/Resources/Public/JavaScript/Luxletter/Module.min.js index 4138281a..25a019e5 100644 --- a/Resources/Public/JavaScript/Luxletter/Module.min.js +++ b/Resources/Public/JavaScript/Luxletter/Module.min.js @@ -1 +1 @@ -define(["jquery"],function(e){"use strict";function t(e){var l=this,i=!1,r=!1,t=(this.initialize=function(){t(),n(),o(),d(),s(),v(),f()},function(){for(var r=document.querySelectorAll(".wizardform > fieldset"),e=document.querySelectorAll("[data-wizardform-gotostep]"),n=document.querySelectorAll(".wizard > a"),t=1;tspan"),t=parseInt(e.innerHTML),e.innerHTML=(++t).toString())},this.userDetailListenerCallback=function(e){var t=document.getElementById("luxletter-newsletter-receiver-container");null!==t&&(t.innerHTML=e.html)},function(e,t,r){var n;void 0!==e&&""!==e?((n=new XMLHttpRequest).onreadystatechange=function(){4===this.readyState&&200===this.status&&null!==r&&l[r](JSON.parse(this.responseText))},n.open("POST",g(e,t),!0),n.send()):console.log("No ajax URI given!")}),w=function(){if(x()&&y())for(var e=document.querySelectorAll("[data-luxletter-wizardstatus]"),t=0;t fieldset"),e=document.querySelectorAll("[data-wizardform-gotostep]"),l=document.querySelectorAll(".wizard > a"),t=1;tspan"),r=parseInt(t.innerHTML),t.innerHTML=(++r).toString()):null!==l&&""!==e.statusSeverity&&!0===e.status&&(l.classList.remove(...l.classList),l.classList.add("alert"),l.classList.add(e.statusSeverity),l.querySelector(".alert-heading").innerHTML=e.statusTitle,l.querySelector("p").innerHTML=e.statusMessage,h(l))},this.userDetailListenerCallback=function(e){var t=document.getElementById("luxletter-newsletter-receiver-container");null!==t&&(t.innerHTML=e.html)},function(e,t,r){var l;void 0!==e&&""!==e?((l=new XMLHttpRequest).onreadystatechange=function(){4===this.readyState&&200===this.status&&null!==r&&n[r](JSON.parse(this.responseText))},l.open("POST",x(e,t),!0),l.send()):console.log("No ajax URI given!")}),w=function(){if(y()&&g())for(var e=document.querySelectorAll("[data-luxletter-wizardstatus]"),t=0;t