diff --git a/Classes/Domain/Model/TemplateBasedMailMessage.php b/Classes/Domain/Model/TemplateBasedMailMessage.php index 6709d5f..352ecf9 100644 --- a/Classes/Domain/Model/TemplateBasedMailMessage.php +++ b/Classes/Domain/Model/TemplateBasedMailMessage.php @@ -5,6 +5,7 @@ namespace Pluswerk\MailLogger\Domain\Model; use Exception; +use Pluswerk\MailLogger\Service\MailTemplateContentTransformer; use Override; use Pluswerk\MailLogger\Utility\ConfigurationUtility; use Symfony\Component\Mime\Crypto\DkimSigner; @@ -193,7 +194,8 @@ private function assignMailTemplateValues(array $values): void } if (!empty($values['message'])) { - $this->messageTemplateSource = $values['message']; + $message = GeneralUtility::makeInstance(MailTemplateContentTransformer::class)->transform((string)$values['message']); + $this->messageTemplateSource = $message; } } diff --git a/Classes/FormDataProvider/MailTemplateFormatHtmlWarningProvider.php b/Classes/FormDataProvider/MailTemplateFormatHtmlWarningProvider.php new file mode 100644 index 0000000..03d13b0 --- /dev/null +++ b/Classes/FormDataProvider/MailTemplateFormatHtmlWarningProvider.php @@ -0,0 +1,34 @@ + $result + * @return array + */ + public function addData(array $result): array + { + if ( + $result['tableName'] === self::TABLE_NAME + && $this->warningService->containsUnsupportedViewHelper((string)($result['databaseRow']['message'] ?? '')) + ) { + $this->warningService->addWarning(false); + } + + return $result; + } +} diff --git a/Classes/Hook/MailTemplateDataHandlerHook.php b/Classes/Hook/MailTemplateDataHandlerHook.php new file mode 100644 index 0000000..8d5075d --- /dev/null +++ b/Classes/Hook/MailTemplateDataHandlerHook.php @@ -0,0 +1,45 @@ + $fieldArray + */ + // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps + public function processDatamap_afterDatabaseOperations( + string $status, + string $table, + string|int $id, + array $fieldArray, + DataHandler $dataHandler, + ): void { + if ($table !== self::TABLE_NAME) { + return; + } + + $recordUid = $status === 'new' ? ($dataHandler->substNEWwithIDs[$id] ?? null) : (int)$id; + if (!is_int($recordUid) || $recordUid <= 0) { + return; + } + + $record = BackendUtility::getRecord(self::TABLE_NAME, $recordUid, 'message'); + if ($record !== null && $this->warningService->containsUnsupportedViewHelper((string)$record['message'])) { + $this->warningService->addWarning(true); + } + } +} diff --git a/Classes/Logging/LoggingTransport.php b/Classes/Logging/LoggingTransport.php index 26ea5e4..71af495 100644 --- a/Classes/Logging/LoggingTransport.php +++ b/Classes/Logging/LoggingTransport.php @@ -46,9 +46,7 @@ public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMess $this->fixTcaIfNotPresentIsUsedInInstallTool(); [$message, $correlationId] = $this->getOrCreateCorrelationId($message); - $mailLog = $correlationId !== null - ? $this->mailLogRepository->findByCorrelationId($correlationId) - : null; + $mailLog = $this->mailLogRepository->findByCorrelationId($correlationId); if (!$mailLog instanceof MailLog) { // Write a new mail log before sending or queueing the message. diff --git a/Classes/Service/MailTemplateContentTransformer.php b/Classes/Service/MailTemplateContentTransformer.php new file mode 100644 index 0000000..b3efd84 --- /dev/null +++ b/Classes/Service/MailTemplateContentTransformer.php @@ -0,0 +1,70 @@ +setMessage($this->transform($mailTemplate->getMessage())); + + return $transformedMailTemplate; + } + + public function transform(string $content): string + { + $content = $this->transformTagNotation($content); + + return $this->transformInlineNotation($content); + } + + private function transformTagNotation(string $content): string + { + return (string)preg_replace_callback( + '#]*>(.*?)#s', + fn(array $matches): string => $this->wrapTagNotationContent($matches[1]), + $content + ); + } + + private function transformInlineNotation(string $content): string + { + return (string)preg_replace( + '#->\s*f:format\.html\s*\([^)]*\)#', + $this->shouldSanitizeHtml() + ? '-> f:sanitize.html() -> f:transform.html()' + : '-> f:transform.html()', + $content + ); + } + + private function wrapTagNotationContent(string $content): string + { + if ($this->shouldSanitizeHtml()) { + return '' . $content . ''; + } + + return '' . $content . ''; + } + + private function shouldSanitizeHtml(): bool + { + try { + return (bool)($this->extensionConfiguration?->get('mail_logger', 'transformFormatHtmlWithSanitize') ?? false); + } catch (ExtensionConfigurationPathDoesNotExistException) { + return false; + } + } +} diff --git a/Classes/Service/MailTemplateFormatHtmlWarningService.php b/Classes/Service/MailTemplateFormatHtmlWarningService.php new file mode 100644 index 0000000..f7bafd9 --- /dev/null +++ b/Classes/Service/MailTemplateFormatHtmlWarningService.php @@ -0,0 +1,47 @@ +mailTemplateContentTransformer->transform($message) !== $message; + } + + public function addWarning(bool $storeInSession): void + { + $languageService = $GLOBALS['LANG']; + $message = $languageService->sL(self::MESSAGE_LABEL); + $queue = $this->flashMessageService->getMessageQueueByIdentifier(); + + foreach ($queue->getAllMessages(ContextualFeedbackSeverity::WARNING) as $flashMessage) { + if ($flashMessage->getMessage() === $message) { + return; + } + } + + $queue->enqueue(new FlashMessage( + $message, + $languageService->sL(self::TITLE_LABEL), + ContextualFeedbackSeverity::WARNING, + $storeInSession, + )); + } +} diff --git a/Classes/Utility/MailUtility.php b/Classes/Utility/MailUtility.php index 722fbef..248d810 100644 --- a/Classes/Utility/MailUtility.php +++ b/Classes/Utility/MailUtility.php @@ -7,6 +7,7 @@ use Exception; use Pluswerk\MailLogger\Domain\Model\TemplateBasedMailMessage; use Pluswerk\MailLogger\Domain\Repository\MailTemplateRepository; +use Pluswerk\MailLogger\Service\MailTemplateContentTransformer; use TYPO3\CMS\Core\Utility\GeneralUtility; class MailUtility @@ -29,6 +30,8 @@ public static function getMailByKey(string $key, ?int $languageUid = null, array throw new Exception('No "MailTemplate" was found for key "' . $key . '". Please check your database records!', 6640694639); } + $mailTemplate = GeneralUtility::makeInstance(MailTemplateContentTransformer::class)->transformMailTemplate($mailTemplate); + return $mail->setMailTemplate($mailTemplate, true, $viewParameters); } } diff --git a/Resources/Private/Language/de.locallang_db.xlf b/Resources/Private/Language/de.locallang_db.xlf index 25262b5..4371f39 100644 --- a/Resources/Private/Language/de.locallang_db.xlf +++ b/Resources/Private/Language/de.locallang_db.xlf @@ -163,6 +163,12 @@ Message + + Warnung: Verwenden Sie nicht den ViewHelper f:format.html. Verwenden Sie stattdessen f:transform.html. + + + Nicht unterstützter ViewHelper + Mail From (name) diff --git a/Resources/Private/Language/locallang_db.xlf b/Resources/Private/Language/locallang_db.xlf index 8ecd874..9a1a294 100644 --- a/Resources/Private/Language/locallang_db.xlf +++ b/Resources/Private/Language/locallang_db.xlf @@ -131,6 +131,12 @@ Message + + Warning: Do not use the f:format.html view helper. Use f:transform.html instead. + + + Unsupported view helper + Mail From (name) diff --git a/Tests/Unit/Service/MailTemplateContentTransformerTest.php b/Tests/Unit/Service/MailTemplateContentTransformerTest.php new file mode 100644 index 0000000..b38e38b --- /dev/null +++ b/Tests/Unit/Service/MailTemplateContentTransformerTest.php @@ -0,0 +1,110 @@ + + */ + public static function templateContentDataProvider(): array + { + return [ + 'tag notation' => [ + '{body}', + '{body}', + '{body}', + ], + 'tag notation with arguments' => [ + '{body}', + '{body}', + '{body}', + ], + 'inline pipe notation' => [ + '{body -> f:format.html()}', + '{body -> f:transform.html()}', + '{body -> f:sanitize.html() -> f:transform.html()}', + ], + 'inline pipe notation with arguments' => [ + "{body -> f:format.html(parseFuncTSPath: 'lib.parseFunc_RTE')}", + '{body -> f:transform.html()}', + '{body -> f:sanitize.html() -> f:transform.html()}', + ], + 'combined notation' => [ + '{headline -> f:format.html()}', + '{headline -> f:transform.html()}', + '{headline -> f:sanitize.html() -> f:transform.html()}', + ], + 'tag notation with arguments fuzzy' => [ + '{body} + ', + '{body} + ', + '{body} + ', + ], + 'inline pipe notation fuzzy' => [ + '{body -> f:format.html ()}', + '{body -> f:transform.html()}', + '{body -> f:sanitize.html() -> f:transform.html()}', + ], + 'normal' => [ + '

{body}

', + '

{body}

', + '

{body}

', + ], + ]; + } + + #[DataProvider('templateContentDataProvider')] + public function testTransformReplacesFormatHtmlWithoutSanitizeByDefault( + string $content, + string $expectedWithoutSanitize, + string $_expectedWithSanitize, + ): void { + self::assertSame($expectedWithoutSanitize, (new MailTemplateContentTransformer())->transform($content)); + } + + #[DataProvider('templateContentDataProvider')] + public function testTransformReplacesFormatHtmlWithSanitizeWhenConfigured( + string $content, + string $_expectedWithoutSanitize, + string $expectedWithSanitize, + ): void { + self::assertSame( + $expectedWithSanitize, + $this->createTransformerWithSanitizeEnabled()->transform($content) + ); + } + + public function testTransformMailTemplateReturnsTransformedClone(): void + { + $mailTemplate = (new MailTemplate())->setMessage('{body -> f:format.html()}'); + + $transformedMailTemplate = (new MailTemplateContentTransformer())->transformMailTemplate($mailTemplate); + + self::assertNotSame($mailTemplate, $transformedMailTemplate); + self::assertSame('{body -> f:format.html()}', $mailTemplate->getMessage()); + self::assertSame('{body -> f:transform.html()}', $transformedMailTemplate->getMessage()); + } + + private function createTransformerWithSanitizeEnabled(): MailTemplateContentTransformer + { + $extensionConfiguration = $this->createMock(ExtensionConfiguration::class); + $extensionConfiguration + ->expects(self::any()) + ->method('get') + ->with('mail_logger', 'transformFormatHtmlWithSanitize') + ->willReturn('1'); + + return new MailTemplateContentTransformer($extensionConfiguration); + } +} diff --git a/Tests/Unit/Service/MailTemplateFormatHtmlWarningServiceTest.php b/Tests/Unit/Service/MailTemplateFormatHtmlWarningServiceTest.php new file mode 100644 index 0000000..a036cf9 --- /dev/null +++ b/Tests/Unit/Service/MailTemplateFormatHtmlWarningServiceTest.php @@ -0,0 +1,38 @@ + + */ + public static function messageDataProvider(): array + { + return [ + 'tag notation' => ['{body}', true], + 'inline notation' => ['{body -> f:format.html()}', true], + 'supported view helper' => ['{body -> f:transform.html()}', false], + 'plain HTML' => ['

{body}

', false], + ]; + } + + #[DataProvider('messageDataProvider')] + public function testDetectsViewHelpersChangedByTheTransformer(string $message, bool $expected): void + { + $warningService = new MailTemplateFormatHtmlWarningService( + new MailTemplateContentTransformer(), + new FlashMessageService(), + ); + + self::assertSame($expected, $warningService->containsUnsupportedViewHelper($message)); + } +} diff --git a/composer.json b/composer.json index 38e94c2..04902e1 100644 --- a/composer.json +++ b/composer.json @@ -19,10 +19,10 @@ "ext-json": "*", "andersundsehr/no-ci": "^1.1", "helhum/typo3-console": "^8.3.1", - "pluswerk/grumphp-config": "^10.2.8", - "saschaegerer/phpstan-typo3": "^2.1.1", + "pluswerk/grumphp-config": "^10.2.9", + "saschaegerer/phpstan-typo3": "^2.2.0", "spatie/phpunit-snapshot-assertions": "^5.4.0", - "ssch/typo3-rector": "^3.14.3", + "ssch/typo3-rector": "^3.14.4", "typo3/testing-framework": "^9.6.1" }, "replace": { diff --git a/ext_conf_template.txt b/ext_conf_template.txt index 6a96cfc..e25cc7e 100644 --- a/ext_conf_template.txt +++ b/ext_conf_template.txt @@ -1,2 +1,5 @@ # cat=Cleanup; type=int+; label=Minimum interval between cleanup runs (in seconds) cleanupMinInterval = 3600 + +# cat=Mail templates; type=boolean; label=Transform f:format.html with f:sanitize.html:Enable to wrap transformed f:format.html output in f:sanitize.html before f:transform.html. +transformFormatHtmlWithSanitize = 0 diff --git a/ext_localconf.php b/ext_localconf.php index eee266f..8ee1179 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -3,6 +3,9 @@ declare(strict_types=1); use Pluswerk\MailLogger\Logging\MailerExtender; +use Pluswerk\MailLogger\FormDataProvider\MailTemplateFormatHtmlWarningProvider; +use Pluswerk\MailLogger\Hook\MailTemplateDataHandlerHook; +use TYPO3\CMS\Backend\Form\FormDataProvider\TcaColumnsProcessFieldLabels; use TYPO3\CMS\Core\Mail\Mailer; use TYPO3\CMS\Core\Utility\ExtensionManagementUtility; @@ -19,3 +22,12 @@ $GLOBALS['TYPO3_CONF_VARS']['SYS']['Objects'][Mailer::class] = [ 'className' => MailerExtender::class, ]; + +$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'][] = MailTemplateDataHandlerHook::class; + +$GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['formDataGroup']['tcaDatabaseRecord'] +[MailTemplateFormatHtmlWarningProvider::class] = [ + 'depends' => [ + TcaColumnsProcessFieldLabels::class, + ], +];