Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Classes/Domain/Model/TemplateBasedMailMessage.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
}

Expand Down
34 changes: 34 additions & 0 deletions Classes/FormDataProvider/MailTemplateFormatHtmlWarningProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

namespace Pluswerk\MailLogger\FormDataProvider;

use Pluswerk\MailLogger\Service\MailTemplateFormatHtmlWarningService;
use TYPO3\CMS\Backend\Form\FormDataProviderInterface;

final readonly class MailTemplateFormatHtmlWarningProvider implements FormDataProviderInterface
{
private const string TABLE_NAME = 'tx_maillogger_domain_model_mailtemplate';

public function __construct(
private MailTemplateFormatHtmlWarningService $warningService,
) {
}

/**
* @param array<string, mixed> $result
* @return array<string, mixed>
*/
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;
}
}
45 changes: 45 additions & 0 deletions Classes/Hook/MailTemplateDataHandlerHook.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

declare(strict_types=1);

namespace Pluswerk\MailLogger\Hook;

use Pluswerk\MailLogger\Service\MailTemplateFormatHtmlWarningService;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\DataHandling\DataHandler;

final readonly class MailTemplateDataHandlerHook
{
private const string TABLE_NAME = 'tx_maillogger_domain_model_mailtemplate';

public function __construct(
private MailTemplateFormatHtmlWarningService $warningService,
) {
}

/**
* @param array<string, mixed> $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);
}
}
}
4 changes: 1 addition & 3 deletions Classes/Logging/LoggingTransport.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
70 changes: 70 additions & 0 deletions Classes/Service/MailTemplateContentTransformer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

declare(strict_types=1);

namespace Pluswerk\MailLogger\Service;

use Pluswerk\MailLogger\Domain\Model\MailTemplate;
use TYPO3\CMS\Core\Configuration\Exception\ExtensionConfigurationPathDoesNotExistException;
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;

final readonly class MailTemplateContentTransformer
{
public function __construct(
private ?ExtensionConfiguration $extensionConfiguration = null,
) {
}

public function transformMailTemplate(MailTemplate $mailTemplate): MailTemplate
{
$transformedMailTemplate = clone $mailTemplate;
$transformedMailTemplate->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(
'#<f:format\.html\b[^>]*>(.*?)</f:format\.html>#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 '<f:transform.html><f:sanitize.html>' . $content . '</f:sanitize.html></f:transform.html>';
}

return '<f:transform.html>' . $content . '</f:transform.html>';
}

private function shouldSanitizeHtml(): bool
{
try {
return (bool)($this->extensionConfiguration?->get('mail_logger', 'transformFormatHtmlWithSanitize') ?? false);
} catch (ExtensionConfigurationPathDoesNotExistException) {
return false;
}
}
}
47 changes: 47 additions & 0 deletions Classes/Service/MailTemplateFormatHtmlWarningService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

namespace Pluswerk\MailLogger\Service;

use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Messaging\FlashMessageService;
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;

final readonly class MailTemplateFormatHtmlWarningService
{
private const string MESSAGE_LABEL = 'LLL:EXT:mail_logger/Resources/Private/Language/locallang_db.xlf:tx_maillogger_domain_model_mailtemplate.format_html_warning';

private const string TITLE_LABEL = 'LLL:EXT:mail_logger/Resources/Private/Language/locallang_db.xlf:tx_maillogger_domain_model_mailtemplate.format_html_warning.title';

public function __construct(
private MailTemplateContentTransformer $mailTemplateContentTransformer,
private FlashMessageService $flashMessageService,
) {
}

public function containsUnsupportedViewHelper(string $message): bool
{
return $this->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,
));
}
}
3 changes: 3 additions & 0 deletions Classes/Utility/MailUtility.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
}
}
6 changes: 6 additions & 0 deletions Resources/Private/Language/de.locallang_db.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,12 @@
<trans-unit id="tx_maillogger_domain_model_mailtemplate.message">
<target>Message</target>
</trans-unit>
<trans-unit id="tx_maillogger_domain_model_mailtemplate.format_html_warning">
<target>Warnung: Verwenden Sie nicht den ViewHelper f:format.html. Verwenden Sie stattdessen f:transform.html.</target>
</trans-unit>
<trans-unit id="tx_maillogger_domain_model_mailtemplate.format_html_warning.title">
<target>Nicht unterstützter ViewHelper</target>
</trans-unit>
<trans-unit id="tx_maillogger_domain_model_mailtemplate.mail_from_name">
<target>Mail From (name)</target>
</trans-unit>
Expand Down
6 changes: 6 additions & 0 deletions Resources/Private/Language/locallang_db.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,12 @@
<trans-unit id="tx_maillogger_domain_model_mailtemplate.message">
<source>Message</source>
</trans-unit>
<trans-unit id="tx_maillogger_domain_model_mailtemplate.format_html_warning">
<source>Warning: Do not use the f:format.html view helper. Use f:transform.html instead.</source>
</trans-unit>
<trans-unit id="tx_maillogger_domain_model_mailtemplate.format_html_warning.title">
<source>Unsupported view helper</source>
</trans-unit>
<trans-unit id="tx_maillogger_domain_model_mailtemplate.mail_from_name">
<source>Mail From (name)</source>
</trans-unit>
Expand Down
110 changes: 110 additions & 0 deletions Tests/Unit/Service/MailTemplateContentTransformerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<?php

declare(strict_types=1);

namespace Pluswerk\MailLogger\Tests\Unit\Service;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Pluswerk\MailLogger\Domain\Model\MailTemplate;
use Pluswerk\MailLogger\Service\MailTemplateContentTransformer;
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;

final class MailTemplateContentTransformerTest extends TestCase
{
/**
* @return array<string, array{0: string, 1: string, 2: string}>
*/
public static function templateContentDataProvider(): array
{
return [
'tag notation' => [
'<f:format.html>{body}</f:format.html>',
'<f:transform.html>{body}</f:transform.html>',
'<f:transform.html><f:sanitize.html>{body}</f:sanitize.html></f:transform.html>',
],
'tag notation with arguments' => [
'<f:format.html parseFuncTSPath="lib.parseFunc_RTE">{body}</f:format.html>',
'<f:transform.html>{body}</f:transform.html>',
'<f:transform.html><f:sanitize.html>{body}</f:sanitize.html></f:transform.html>',
],
'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' => [
'<f:format.html>{headline -> f:format.html()}</f:format.html>',
'<f:transform.html>{headline -> f:transform.html()}</f:transform.html>',
'<f:transform.html><f:sanitize.html>{headline -> f:sanitize.html() -> f:transform.html()}</f:sanitize.html></f:transform.html>',
],
'tag notation with arguments fuzzy' => [
'<f:format.html parseFuncTSPath = "lib.parseFunc_RTE">{body}
</f:format.html>',
'<f:transform.html>{body}
</f:transform.html>',
'<f:transform.html><f:sanitize.html>{body}
</f:sanitize.html></f:transform.html>',
],
'inline pipe notation fuzzy' => [
'{body -> f:format.html ()}',
'{body -> f:transform.html()}',
'{body -> f:sanitize.html() -> f:transform.html()}',
],
'normal' => [
'<p>{body}</p>',
'<p>{body}</p>',
'<p>{body}</p>',
],
];
}

#[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);
}
}
Loading
Loading