diff --git a/UPGRADE.md b/UPGRADE.md index f0931e17..a09700f2 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -3,7 +3,6 @@ * The built-in Postmark gateway has been removed. * The built-in queue gateway has been removed. * The built-in file gateway has been removed. -* Embedding images in e-mails is not supported anymore. * Attachment templates are not supported anymore. * The configurable flattening delimiter in the e-mail notification type has been removed. * The configurable template in the notification type has been removed. @@ -17,7 +16,10 @@ you will have to adjust your workflow. This module has been removed. However, the "Subscribe (Notification Center)" now has a second forward page setting. You can use this one in order to have a separate confirmation page. * Tokens will not be validated in the back end anymore. Basically because it's totally okay to write something - like `##something-not-token-related##` in your message, and you should be able to write this. + like `##something_not_token_related##` in your message, and you should be able to write this. +* Tokens will be normalized! In Contao 5, having a token with e.g. a dash (`##my-token##`) is not supported in `{if` + statements anymore. Hence, the Notification Center 2 will normalize this to `##my_token##`. Allowed token names have + the same requirement as PHP variables. All invalid characters will be replaced with a `_`. * The `filenames` token introduced in 1.7 has been removed. It's a very specific use case which can be provided very easily as a third party bundle now (not easily possible before and thus part of the core in 1.7). * The `member_*` tokens in the `lost_password` notification type used to contain the raw database values, they are now diff --git a/composer.json b/composer.json index 6ddbd2b5..8b30cef7 100644 --- a/composer.json +++ b/composer.json @@ -54,9 +54,11 @@ "require-dev": { "contao/manager-plugin": "^2.0", "contao/newsletter-bundle": "^5.0", - "phpunit/phpunit": "^10.0", - "terminal42/contao-build-tools": "dev-main", - "contao/test-case": "^4.9" + "contao/test-case": "^5.3", + "league/flysystem-memory": "^3.25", + "phpunit/phpunit": "^9.6", + "symfony/expression-language": "^5.4 || ^6.0 || ^7.0", + "terminal42/contao-build-tools": "dev-main" }, "suggest": { "terminal42/contao-notification-center-pro": "Turn your Notification Center 2 into a pro version and benefit from logs, various testing tools and your own Simple Tokens that can be completely customized with Twig." diff --git a/config/listeners.php b/config/listeners.php index 46a099b2..d09a0fdb 100644 --- a/config/listeners.php +++ b/config/listeners.php @@ -107,6 +107,7 @@ ->args([ service(NotificationCenter::class), service(FileUploadNormalizer::class), + service(ConfigLoader::class), ]) ; diff --git a/config/services.php b/config/services.php index 7c55642a..9390a619 100644 --- a/config/services.php +++ b/config/services.php @@ -8,6 +8,7 @@ use Symfony\Contracts\Translation\TranslatorInterface; use Terminal42\NotificationCenterBundle\Backend\AutoSuggester; use Terminal42\NotificationCenterBundle\BulkyItem\BulkyItemStorage; +use Terminal42\NotificationCenterBundle\BulkyItem\FileItemFactory; use Terminal42\NotificationCenterBundle\Config\ConfigLoader; use Terminal42\NotificationCenterBundle\Cron\PruneBulkyItemStorageCron; use Terminal42\NotificationCenterBundle\DependencyInjection\Terminal42NotificationCenterExtension; @@ -59,6 +60,12 @@ ]) ; + $services->set(FileItemFactory::class) + ->args([ + service('mime_types')->nullOnInvalid(), + ]) + ; + $services->set(PruneBulkyItemStorageCron::class) ->args([ service(BulkyItemStorage::class), diff --git a/contao/dca/tl_form.php b/contao/dca/tl_form.php index 21829b65..89a41e83 100644 --- a/contao/dca/tl_form.php +++ b/contao/dca/tl_form.php @@ -17,6 +17,7 @@ */ $GLOBALS['TL_DCA']['tl_form']['fields']['nc_notification'] = [ 'exclude' => true, + 'filter' => true, 'inputType' => 'select', 'eval' => ['includeBlankOption' => true, 'chosen' => true, 'tl_class' => 'clr w50'], 'sql' => ['type' => 'integer', 'default' => 0, 'unsigned' => true], diff --git a/src/BulkyItem/FileItem.php b/src/BulkyItem/FileItem.php index dac914da..850ed53d 100644 --- a/src/BulkyItem/FileItem.php +++ b/src/BulkyItem/FileItem.php @@ -5,11 +5,14 @@ namespace Terminal42\NotificationCenterBundle\BulkyItem; use Symfony\Component\Filesystem\Filesystem; +use Terminal42\NotificationCenterBundle\Exception\BulkyItem\InvalidFileItemException; class FileItem implements BulkyItemInterface { /** * @param resource $contents + * + * @throws InvalidFileItemException */ private function __construct( private $contents, @@ -17,6 +20,9 @@ private function __construct( private readonly string $mimeType, private readonly int $size, ) { + $this->assert('' !== $this->name, 'Name must not be empty'); + $this->assert('' !== $this->mimeType, 'Mime type must not be empty'); + $this->assert($this->size >= 0, 'File size must not be smaller than 0'); } public function getName(): string @@ -53,10 +59,13 @@ public static function restore($contents, array $meta): BulkyItemInterface return new self($contents, $meta['name'], $meta['type'], $meta['size']); } + /** + * @throws InvalidFileItemException + */ public static function fromPath(string $path, string $name, string $mimeType, int $size): self { if (!(new Filesystem())->exists($path)) { - throw new \InvalidArgumentException(\sprintf('The file "%s" does not exist.', $path)); + throw new InvalidFileItemException(\sprintf('The file "%s" does not exist.', $path)); } return new self(fopen($path, 'r'), $name, $mimeType, $size); @@ -64,13 +73,22 @@ public static function fromPath(string $path, string $name, string $mimeType, in /** * @param resource $resource + * + * @throws InvalidFileItemException */ public static function fromStream($resource, string $name, string $mimeType, int $size): self { if (!\is_resource($resource)) { - throw new \InvalidArgumentException('$contents must be a resource.'); + throw new InvalidFileItemException('$contents must be a resource.'); } return new self($resource, $name, $mimeType, $size); } + + private function assert(bool $condition, string $message): void + { + if (!$condition) { + throw new InvalidFileItemException($message); + } + } } diff --git a/src/BulkyItem/FileItemFactory.php b/src/BulkyItem/FileItemFactory.php new file mode 100644 index 00000000..b44484e2 --- /dev/null +++ b/src/BulkyItem/FileItemFactory.php @@ -0,0 +1,44 @@ +exists($path)) { + throw new InvalidFileItemException(\sprintf('The file "%s" does not exist.', $path)); + } + + $name = basename($path); + $mimeType = (string) $this->mimeTypeGuesser?->guessMimeType($path); + $size = (int) filesize($path); + + return FileItem::fromPath($path, $name, $mimeType, $size); + } + + /** + * @throws InvalidFileItemException + */ + public function createFromVfsFilesystemItem(FilesystemItem $file, VirtualFilesystemInterface $virtualFilesystem): FileItem + { + $stream = $virtualFilesystem->readStream($file->getPath()); + + return FileItem::fromStream($stream, $file->getName(), $file->getMimeType(), $file->getFileSize()); + } +} diff --git a/src/Config/ConfigLoader.php b/src/Config/ConfigLoader.php index 21771366..425f8eee 100644 --- a/src/Config/ConfigLoader.php +++ b/src/Config/ConfigLoader.php @@ -38,6 +38,11 @@ public function loadModule(int $id): ModuleConfig|null return $this->loadConfig($id, 'tl_module', ModuleConfig::class); } + public function loadForm(int $id): FormConfig|null + { + return $this->loadConfig($id, 'tl_form', FormConfig::class); + } + /** * @return array */ diff --git a/src/Config/FormConfig.php b/src/Config/FormConfig.php new file mode 100644 index 00000000..f8255f5d --- /dev/null +++ b/src/Config/FormConfig.php @@ -0,0 +1,9 @@ +findTaggedServiceIds(Terminal42NotificationCenterExtension::GATEWAY_TAG); $locateableServices = [ AbstractGateway::SERVICE_NAME_NOTIFICATION_CENTER => new Reference(NotificationCenter::class), + AbstractGateway::SERVICE_NAME_BULKY_ITEM_STORAGE => new Reference(BulkyItemStorage::class), AbstractGateway::SERVICE_NAME_SIMPLE_TOKEN_PARSER => new Reference('contao.string.simple_token_parser', ContainerInterface::NULL_ON_INVALID_REFERENCE), AbstractGateway::SERVICE_NAME_INSERT_TAG_PARSER => new Reference('contao.insert_tag.parser', ContainerInterface::NULL_ON_INVALID_REFERENCE), ]; diff --git a/src/EventListener/ProcessFormDataListener.php b/src/EventListener/ProcessFormDataListener.php index 3c2e74cf..2465d675 100644 --- a/src/EventListener/ProcessFormDataListener.php +++ b/src/EventListener/ProcessFormDataListener.php @@ -9,8 +9,10 @@ use Contao\Form; use Contao\StringUtil; use Terminal42\NotificationCenterBundle\BulkyItem\FileItem; +use Terminal42\NotificationCenterBundle\Config\ConfigLoader; use Terminal42\NotificationCenterBundle\NotificationCenter; use Terminal42\NotificationCenterBundle\Parcel\Stamp\BulkyItemsStamp; +use Terminal42\NotificationCenterBundle\Parcel\Stamp\FormConfigStamp; #[AsHook('processFormData')] class ProcessFormDataListener @@ -18,6 +20,7 @@ class ProcessFormDataListener public function __construct( private readonly NotificationCenter $notificationCenter, private readonly FileUploadNormalizer $fileUploadNormalizer, + private readonly ConfigLoader $configLoader, ) { } @@ -76,7 +79,7 @@ public function __invoke(array $submittedData, array $formData, array|null $file FileItem::fromStream($file['stream'], $file['name'], $file['type'], $file['size']) : FileItem::fromPath($file['tmp_name'], $file['name'], $file['type'], $file['size']); - $vouchers[] = $this->notificationCenter->getBulkyGoodsStorage()->store($fileItem); + $vouchers[] = $this->notificationCenter->getBulkyItemStorage()->store($fileItem); } $tokens['form_'.$k] = implode(',', $vouchers); @@ -95,6 +98,12 @@ public function __invoke(array $submittedData, array $formData, array|null $file $stamps = $stamps->with(new BulkyItemsStamp($bulkyItemVouchers)); } + $formConfig = $this->configLoader->loadForm((int) $form->id); + + if (null !== $formConfig) { + $stamps = $stamps->with(new FormConfigStamp($formConfig)); + } + $this->notificationCenter->sendNotificationWithStamps((int) $formData['nc_notification'], $stamps); } } diff --git a/src/Exception/BulkyItem/InvalidFileItemException.php b/src/Exception/BulkyItem/InvalidFileItemException.php new file mode 100644 index 00000000..fe84df81 --- /dev/null +++ b/src/Exception/BulkyItem/InvalidFileItemException.php @@ -0,0 +1,9 @@ +getSimpleTokenParser()?->parse( - $value, - $parcel->getStamp(TokenCollectionStamp::class)->tokenCollection->forSimpleTokenParser(), - ); + if ($simpleTokenParser = $this->getSimpleTokenParser()) { + return $simpleTokenParser->parse( + $value, + $parcel->getStamp(TokenCollectionStamp::class)->tokenCollection->forSimpleTokenParser(), + ); + } + + return $value; } protected function replaceInsertTags(string $value): string { - return $this->getInsertTagParser()?->replaceInline($value); + if ($insertTagParser = $this->getInsertTagParser()) { + return $insertTagParser->replaceInline($value); + } + + return $value; } protected function replaceTokensAndInsertTags(Parcel $parcel, string $value): string @@ -143,4 +154,15 @@ protected function getNotificationCenter(): NotificationCenter|null return !$notificationCenter instanceof NotificationCenter ? null : $notificationCenter; } + + protected function getBulkyItemStorage(): BulkyItemStorage|null + { + if (null === $this->container || !$this->container->has(self::SERVICE_NAME_BULKY_ITEM_STORAGE)) { + return null; + } + + $bulkyItemStorage = $this->container->get(self::SERVICE_NAME_BULKY_ITEM_STORAGE); + + return !$bulkyItemStorage instanceof BulkyItemStorage ? null : $bulkyItemStorage; + } } diff --git a/src/Gateway/MailerGateway.php b/src/Gateway/MailerGateway.php index 2b6c4231..0d971a3c 100644 --- a/src/Gateway/MailerGateway.php +++ b/src/Gateway/MailerGateway.php @@ -6,7 +6,7 @@ use Contao\Controller; use Contao\CoreBundle\Filesystem\Dbafs\UnableToResolveUuidException; -use Contao\CoreBundle\Filesystem\VirtualFilesystem; +use Contao\CoreBundle\Filesystem\VirtualFilesystemInterface; use Contao\CoreBundle\Framework\ContaoFramework; use Contao\CoreBundle\Util\LocaleUtil; use Contao\FrontendTemplate; @@ -33,7 +33,7 @@ class MailerGateway extends AbstractGateway public function __construct( private readonly ContaoFramework $contaoFramework, - private readonly VirtualFilesystem $filesystem, + private readonly VirtualFilesystemInterface $filesStorage, private readonly MailerInterface $mailer, ) { } @@ -143,6 +143,7 @@ private function createEmailStamp(Parcel $parcel): EmailStamp $stamp = $stamp->withText($text); if ($html) { + $html = $this->embedImages($html, $stamp); $stamp = $stamp->withHtml($html); } @@ -176,7 +177,7 @@ private function createEmail(Parcel $parcel): Email // Attachments foreach ($emailStamp->getAttachmentVouchers() as $voucher) { - $item = $this->getNotificationCenter()->getBulkyGoodsStorage()->retrieve($voucher); + $item = $this->getBulkyItemStorage()?->retrieve($voucher); if ($item instanceof FileItem) { $email->attach( @@ -187,6 +188,14 @@ private function createEmail(Parcel $parcel): Email } } + // Embedded images + foreach ($emailStamp->getEmbeddedImageVouchers() as $voucher) { + $item = $this->getBulkyItemStorage()?->retrieve($voucher); + if ($item instanceof FileItem) { + $email->attach($item->getContents(), $this->encodeVoucherForContentId($voucher)); + } + } + return $email; } @@ -267,30 +276,17 @@ private function copyBackendAttachments(Parcel $parcel, LanguageConfig $language try { $uuidObject = Uuid::isValid($uuid) ? Uuid::fromString($uuid) : Uuid::fromBinary($uuid); - - if (null === ($item = $this->filesystem->get($uuidObject))) { - continue; - } } catch (\InvalidArgumentException|UnableToResolveUuidException) { continue; } - if (!$item->isFile()) { + $voucher = $this->createBulkyItemStorageVoucher($uuidObject, $this->filesStorage); + + if (null === $voucher) { continue; } - $voucher = $this->getNotificationCenter()?->getBulkyGoodsStorage()->store( - FileItem::fromStream( - $this->filesystem->readStream($uuidObject), - $item->getName(), - $item->getMimeType(), - $item->getFileSize(), - ), - ); - - if (null !== $voucher) { - $vouchers[] = $voucher; - } + $vouchers[] = $voucher; } if (0 === \count($vouchers)) { @@ -299,4 +295,59 @@ private function copyBackendAttachments(Parcel $parcel, LanguageConfig $language return $parcel->withStamp(new BackendAttachmentsStamp($vouchers)); } + + private function createBulkyItemStorageVoucher(Uuid|string $location, VirtualFilesystemInterface $filesystem): string|null + { + try { + if (null === ($item = $filesystem->get($location))) { + return null; + } + } catch (\InvalidArgumentException|UnableToResolveUuidException) { + return null; + } + + if (!$item->isFile()) { + return null; + } + + return $this->getBulkyItemStorage()?->store( + FileItem::fromStream( + $filesystem->readStream($location), + $item->getName(), + $item->getMimeType(), + $item->getFileSize(), + ), + ); + } + + private function encodeVoucherForContentId(string $voucher): string + { + return rawurlencode($voucher); + } + + private function embedImages(string $html, EmailStamp &$stamp): string + { + $prefixToStrip = ''; + + if (method_exists($this->filesStorage, 'getPrefix')) { + $prefixToStrip = $this->filesStorage->getPrefix(); + } + + return preg_replace_callback( + '/<[a-z][a-z0-9]*\b[^>]*((src=|background=|url\()["\']??)(.+\.(jpe?g|png|gif|bmp|tiff?|swf))(["\' ]??(\)??))[^>]*>/Ui', + function ($matches) use (&$stamp, $prefixToStrip) { + $location = ltrim(ltrim(ltrim($matches[3], '/'), $prefixToStrip), '/'); + $voucher = $this->createBulkyItemStorageVoucher($location, $this->filesStorage); + + if (null === $voucher) { + return $matches[0]; + } + + $stamp = $stamp->withEmbeddedImageVoucher($voucher); + + return str_replace($matches[3], 'cid:'.$this->encodeVoucherForContentId($voucher), $matches[0]); + }, + $html, + ); + } } diff --git a/src/NotificationCenter.php b/src/NotificationCenter.php index 5345ad38..a69bbe63 100644 --- a/src/NotificationCenter.php +++ b/src/NotificationCenter.php @@ -50,7 +50,7 @@ public function __construct( private readonly ConfigLoader $configLoader, private readonly EventDispatcherInterface $eventDispatcher, private readonly RequestStack $requestStack, - private readonly BulkyItemStorage $bulkyGoodsStorage, + private readonly BulkyItemStorage $bulkyItemStorage, private readonly StringParser $stringParser, private readonly LocaleSwitcher|null $localeSwitcher, ) { @@ -58,7 +58,14 @@ public function __construct( public function getBulkyGoodsStorage(): BulkyItemStorage { - return $this->bulkyGoodsStorage; + trigger_deprecation('terminal42/notification_center', '2.1', 'Using "getBulkyGoodsStorage()" is deprecated, use "getBulkyItemStorage()" instead.'); + + return $this->bulkyItemStorage; + } + + public function getBulkyItemStorage(): BulkyItemStorage + { + return $this->bulkyItemStorage; } /** diff --git a/src/Parcel/Stamp/FormConfigStamp.php b/src/Parcel/Stamp/FormConfigStamp.php new file mode 100644 index 00000000..c0fc06dc --- /dev/null +++ b/src/Parcel/Stamp/FormConfigStamp.php @@ -0,0 +1,20 @@ +formConfig); + } + + public static function fromArray(array $data): self + { + return new self(FormConfig::fromArray($data)); + } +} diff --git a/src/Parcel/Stamp/Mailer/EmailStamp.php b/src/Parcel/Stamp/Mailer/EmailStamp.php index 4c46c214..e7a61bec 100644 --- a/src/Parcel/Stamp/Mailer/EmailStamp.php +++ b/src/Parcel/Stamp/Mailer/EmailStamp.php @@ -34,6 +34,11 @@ class EmailStamp implements StampInterface */ private array $attachmentVouchers = []; + /** + * @var array + */ + private array $embeddedImageVouchers = []; + public function withFromName(string $fromName): self { $clone = clone $this; @@ -106,6 +111,22 @@ public function withHtml(string $html): self return $clone; } + /** + * @return array + */ + public function getEmbeddedImageVouchers(): array + { + return $this->embeddedImageVouchers; + } + + public function withEmbeddedImageVoucher(string $voucher): self + { + $clone = clone $this; + $clone->embeddedImageVouchers[] = $voucher; + + return $clone; + } + public function withAttachmentVoucher(string $voucher): self { $clone = clone $this; @@ -170,6 +191,7 @@ public function toArray(): array 'text' => $this->text, 'html' => $this->html, 'attachmentVouchers' => $this->attachmentVouchers, + 'embeddedImageVouchers' => $this->embeddedImageVouchers, ]; } @@ -187,10 +209,14 @@ public static function fromArray(array $data): StampInterface ->withHtml($data['html']) ; - foreach ($data['attachmentVouchers'] as $voucher) { + foreach ($data['attachmentVouchers'] ?? [] as $voucher) { $stamp = $stamp->withAttachmentVoucher($voucher); } + foreach ($data['embeddedImageVouchers'] ?? [] as $voucher) { + $stamp = $stamp->withEmbeddedImageVoucher($voucher); + } + return $stamp; } } diff --git a/src/Token/TokenCollection.php b/src/Token/TokenCollection.php index 0bc5fd68..b432436f 100644 --- a/src/Token/TokenCollection.php +++ b/src/Token/TokenCollection.php @@ -29,6 +29,19 @@ public static function fromSerializedArray(array $data): self return new self($tokens); } + public function replaceToken(Token $token): self + { + $existing = $this->getByName($token->getName()); + + if (null !== $existing) { + $this->remove($existing); + } + + $this->addToken($token); + + return $this; + } + /** * Provides a fluent interface alternative to add() with a type hint. */ diff --git a/tests/BulkyItem/FileItemFactoryTest.php b/tests/BulkyItem/FileItemFactoryTest.php new file mode 100644 index 00000000..08eeb8ae --- /dev/null +++ b/tests/BulkyItem/FileItemFactoryTest.php @@ -0,0 +1,41 @@ +createFromLocalPath(__DIR__.'/../Fixtures/name.jpg'); + $this->assertSame('name.jpg', $item->getName()); + $this->assertSame('image/jpeg', $item->getMimeType()); + $this->assertSame(333, $item->getSize()); + $this->assertIsResource($item->getContents()); + } + + public function testCreateFromVfsFilesystemItem(): void + { + $vfsCollection = $this->createVfsCollection(); + $vfs = $vfsCollection->get('files'); + $vfs->write('media/name.jpg', file_get_contents(__DIR__.'/../Fixtures/name.jpg')); + + $item = $vfs->get('media/name.jpg'); + + $factory = new FileItemFactory(new MimeTypes()); + $item = $factory->createFromVfsFilesystemItem($item, $vfs); + $this->assertSame('name.jpg', $item->getName()); + $this->assertSame('image/jpeg', $item->getMimeType()); + $this->assertSame(333, $item->getSize()); + $this->assertIsResource($item->getContents()); + } +} diff --git a/tests/BulkyItem/FileItemTest.php b/tests/BulkyItem/FileItemTest.php new file mode 100644 index 00000000..c63c2dd4 --- /dev/null +++ b/tests/BulkyItem/FileItemTest.php @@ -0,0 +1,36 @@ +expectException(InvalidFileItemException::class); + $this->expectExceptionMessage('Name must not be empty'); + + FileItem::fromPath(__DIR__.'/../Fixtures/name.jpg', '', 'image/jpg', 0); + } + + public function testCannotCreateEmptyMimeTypeFileItem(): void + { + $this->expectException(InvalidFileItemException::class); + $this->expectExceptionMessage('Mime type must not be empty'); + + FileItem::fromPath(__DIR__.'/../Fixtures/name.jpg', 'name.jpg', '', 0); + } + + public function testCannotCreateInvalidFileSizeFileItem(): void + { + $this->expectException(InvalidFileItemException::class); + $this->expectExceptionMessage('File size must not be smaller than 0'); + + FileItem::fromPath(__DIR__.'/../Fixtures/name.jpg', 'name.jpg', 'image/jpg', -42); + } +} diff --git a/tests/BulkyItem/InMemoryDbafs.php b/tests/BulkyItem/InMemoryDbafs.php new file mode 100644 index 00000000..3c3568a4 --- /dev/null +++ b/tests/BulkyItem/InMemoryDbafs.php @@ -0,0 +1,68 @@ + + */ + private array $records = []; + + /** + * @var array> + */ + private array $meta = []; + + public function getPathFromUuid(Uuid $uuid): string|null + { + throw new \RuntimeException('Not implemented'); + } + + public function getRecord(string $path): FilesystemItem|null + { + if (isset($this->records[$path])) { + return new FilesystemItem( + true, + $path, + null, + null, + null, + $this->meta[$path] ?? [], + ); + } + + return null; + } + + public function getRecords(string $path, bool $deep = false): iterable + { + throw new \RuntimeException('Not implemented'); + } + + public function setExtraMetadata(string $path, array $metadata): void + { + $this->meta[$path] = $metadata; + } + + public function sync(string ...$paths): ChangeSet + { + foreach ($paths as $path) { + $this->records[$path] = true; + } + + return new ChangeSet([], [], []); + } + + public function getSupportedFeatures(): int + { + return DbafsInterface::FEATURES_NONE; + } +} diff --git a/tests/BulkyItem/VirtualFilesystemCollection.php b/tests/BulkyItem/VirtualFilesystemCollection.php new file mode 100644 index 00000000..594e95b6 --- /dev/null +++ b/tests/BulkyItem/VirtualFilesystemCollection.php @@ -0,0 +1,29 @@ + $vfs + */ + public function __construct(private array $vfs = []) + { + } + + public function get(string $name): VirtualFilesystem + { + return $this->vfs[$name]; + } + + public function add(VirtualFilesystem $vfs): self + { + $this->vfs[$vfs->getPrefix()] = $vfs; + + return $this; + } +} diff --git a/tests/EventListener/AdminEmailTokenListenerTest.php b/tests/EventListener/AdminEmailTokenListenerTest.php index 40d69242..aaf30e9a 100644 --- a/tests/EventListener/AdminEmailTokenListenerTest.php +++ b/tests/EventListener/AdminEmailTokenListenerTest.php @@ -8,7 +8,6 @@ use Contao\CoreBundle\Framework\ContaoFramework; use Contao\PageModel; use Contao\TestCase\ContaoTestCase; -use PHPUnit\Framework\Attributes\DataProvider; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Terminal42\NotificationCenterBundle\Config\MessageConfig; @@ -21,7 +20,9 @@ class AdminEmailTokenListenerTest extends ContaoTestCase { - #[DataProvider('adminEmailProvider')] + /** + * @dataProvider adminEmailProvider + */ public function testAddsAdminTokens(string $configFriendlyEmail, string $pageFriendlyEmail, string $expectedName, string $expectedEmail): void { $pageModel = $this->mockClassWithProperties(PageModel::class, [ @@ -46,7 +47,10 @@ public function testAddsAdminTokens(string $configFriendlyEmail, string $pageFri $this->assertSame($expectedEmail, $tokenCollection->getByName('admin_email')->getValue()); } - public static function adminEmailProvider(): \Generator + /** + * @return iterable + */ + public static function adminEmailProvider(): iterable { yield 'Basic admin email in config' => [ 'foobar-config@terminal42.ch', diff --git a/tests/Gateway/MailerGatewayTest.php b/tests/Gateway/MailerGatewayTest.php new file mode 100644 index 00000000..94d96de2 --- /dev/null +++ b/tests/Gateway/MailerGatewayTest.php @@ -0,0 +1,168 @@ + $mockFiles + * @param array $expectedAttachmentsContentsAndPath + */ + public function testEmbeddingHtmlImages(string $parsedTemplateHtml, array $mockFiles, array $expectedAttachmentsContentsAndPath): void + { + $vfsCollection = $this->createVfsCollection(); + + foreach ($mockFiles as $path => $contents) { + $vfsCollection->get('files')->write($path, $contents); + } + + $mailer = $this->createMock(MailerInterface::class); + $mailer + ->expects($this->once()) + ->method('send') + ->with($this->callback( + static function (Email $email) use ($parsedTemplateHtml, $expectedAttachmentsContentsAndPath): bool { + $attachments = []; + + foreach ($email->getAttachments() as $attachment) { + $attachments[$attachment->getBody()] = $attachment->getName(); + } + + $expectedHtml = $parsedTemplateHtml; + + foreach ($expectedAttachmentsContentsAndPath as $content => $path) { + $expectedHtml = str_replace($path, 'cid:'.$attachments[$content], $expectedHtml); + } + + return $expectedHtml === $email->getHtmlBody(); + }, + )) + ; + + $tokenCollection = new TokenCollection(); + $tokenCollection->addToken(Token::fromValue('admin_email', 'foobar@example.com')); + $tokenCollection->addToken(Token::fromValue('recipient_email', 'foobar@example.com')); + + $parcel = new Parcel(MessageConfig::fromArray([ + 'email_template' => 'mail_default', + ])); + $parcel = $parcel->withStamp(new LanguageConfigStamp(LanguageConfig::fromArray([ + 'recipients' => '##recipient_email##', + 'email_mode' => 'textAndHtml', + ]))); + $parcel = $parcel->withStamp(new TokenCollectionStamp($tokenCollection)); + + $gateway = new MailerGateway( + $this->createFrameWorkWithTemplate($parsedTemplateHtml), + $vfsCollection->get('files'), + $mailer, + ); + $container = new Container(); + $container->set(AbstractGateway::SERVICE_NAME_BULKY_ITEM_STORAGE, new BulkyItemStorage($vfsCollection->get('bulky_item'))); + $container->set(AbstractGateway::SERVICE_NAME_SIMPLE_TOKEN_PARSER, new SimpleTokenParser(new ExpressionLanguage())); + $gateway->setContainer($container); + + $parcel = $gateway->sealParcel($parcel); + $gateway->sendParcel($parcel); + } + + /** + * @return iterable, 2: array}> + */ + public static function embeddingHtmlImagesProvider(): iterable + { + yield 'Test embeds a relative upload path' => [ + '

', + [ + 'contaodemo/media/content-images/DSC_5276.jpg' => 'foobar', + ], + [ + 'foobar' => 'files/contaodemo/media/content-images/DSC_5276.jpg', + ], + ]; + + yield 'Test embeds an absolute upload path' => [ + '

', + [ + 'contaodemo/media/content-images/DSC_5276.jpg' => 'foobar', + ], + [ + 'foobar' => '/files/contaodemo/media/content-images/DSC_5276.jpg', + ], + ]; + } + + private function createVfsCollection(): VirtualFilesystemCollection + { + $mountManager = (new MountManager()) + ->mount(new InMemoryFilesystemAdapter(), 'files') + ->mount(new InMemoryFilesystemAdapter(), 'bulky_item') + ; + + $dbafsManager = new DbafsManager(); + $dbafsManager->register(new InMemoryDbafs(), 'files'); + $dbafsManager->register(new InMemoryDbafs(), 'bulky_item'); + + $vfsCollection = new VirtualFilesystemCollection(); + $vfsCollection->add(new VirtualFilesystem($mountManager, $dbafsManager, 'files')); + $vfsCollection->add(new VirtualFilesystem($mountManager, $dbafsManager, 'bulky_item')); + $vfsCollection->add(new VirtualFilesystem($mountManager, $dbafsManager, '')); // Global one + + return $vfsCollection; + } + + private function createFrameWorkWithTemplate(string $parsedTemplateHtml): ContaoFramework + { + $controllerAdapter = $this->mockAdapter(['convertRelativeUrls']); + $controllerAdapter + ->method('convertRelativeUrls') + ->willReturnCallback(static fn (string $template): string => $template) + ; + + $templateInstance = $this->createMock(FrontendTemplate::class); + $templateInstance + ->expects($this->once()) + ->method('parse') + ->willReturn($parsedTemplateHtml) + ; + + return $this->mockContaoFramework( + [ + Controller::class => $controllerAdapter, + ], + [ + FrontendTemplate::class => $templateInstance, + ], + ); + } +} diff --git a/tests/Token/TokenCollectionTest.php b/tests/Token/TokenCollectionTest.php index 9e16176b..6dd27aaf 100644 --- a/tests/Token/TokenCollectionTest.php +++ b/tests/Token/TokenCollectionTest.php @@ -61,6 +61,23 @@ public function testCollectionHandling(): void $this->assertNull($tokenCollection->getByName('form_i_do_not_exist')); } + public function testReplace(): void + { + $tokenCollection = new TokenCollection(); + $tokenCollection->addToken(Token::fromValue('test', 'foobar')); + $tokenCollection->addToken(Token::fromValue('test', 'foobar new')); + + $this->assertCount(2, $tokenCollection); + $this->assertSame('foobar', $tokenCollection->getByName('test')->getValue(), 'foobar'); + + $tokenCollection = new TokenCollection(); + $tokenCollection->replaceToken(Token::fromValue('test', 'foobar')); + $tokenCollection->replaceToken(Token::fromValue('test', 'foobar new')); + + $this->assertCount(1, $tokenCollection); + $this->assertSame('foobar new', $tokenCollection->getByName('test')->getValue(), 'foobar'); + } + public function testMerge(): void { $tokenCollectionA = new TokenCollection(); diff --git a/tests/Token/TokenTest.php b/tests/Token/TokenTest.php index de5ab067..58e68c50 100644 --- a/tests/Token/TokenTest.php +++ b/tests/Token/TokenTest.php @@ -10,7 +10,9 @@ class TokenTest extends TestCase { - #[DataProvider('anythingProvider')] + /** + * @dataProvider anythingProvider + */ public function testFromAnything(mixed $value, string $expectedParserValue): void { $token = Token::fromValue('token', $value); @@ -19,16 +21,20 @@ public function testFromAnything(mixed $value, string $expectedParserValue): voi } /** + * @dataProvider arrayProvider + * * @param array $value */ - #[DataProvider('arrayProvider')] public function testArrayParserFormat(array $value, string $expectedParserValue): void { $token = Token::fromValue('form_foobar', $value); $this->assertSame($expectedParserValue, $token->getParserValue()); } - public static function arrayProvider(): \Generator + /** + * @return iterable, 1: string}> + */ + public static function arrayProvider(): iterable { yield 'Simple list array token' => [ [ @@ -62,7 +68,10 @@ public static function arrayProvider(): \Generator ]; } - public static function anythingProvider(): \Generator + /** + * @return iterable + */ + public static function anythingProvider(): iterable { yield [ 'foobar', diff --git a/tests/VirtualFilesystemTestTrait.php b/tests/VirtualFilesystemTestTrait.php new file mode 100644 index 00000000..aeb7d07f --- /dev/null +++ b/tests/VirtualFilesystemTestTrait.php @@ -0,0 +1,34 @@ +mount(new InMemoryFilesystemAdapter(), 'files') + ->mount(new InMemoryFilesystemAdapter(), 'bulky_item') + ; + + $dbafsManager = new DbafsManager(); + $dbafsManager->register(new InMemoryDbafs(), 'files'); + $dbafsManager->register(new InMemoryDbafs(), 'bulky_item'); + + $vfsCollection = new VirtualFilesystemCollection(); + $vfsCollection->add(new VirtualFilesystem($mountManager, $dbafsManager, 'files')); + $vfsCollection->add(new VirtualFilesystem($mountManager, $dbafsManager, 'bulky_item')); + $vfsCollection->add(new VirtualFilesystem($mountManager, $dbafsManager, '')); // Global one + + return $vfsCollection; + } +}