Skip to content

Commit

Permalink
Add support for images embedded in HTML (#361)
Browse files Browse the repository at this point in the history
  • Loading branch information
Toflar authored Sep 19, 2024
1 parent 0e8563b commit d768b3a
Show file tree
Hide file tree
Showing 13 changed files with 427 additions and 40 deletions.
1 change: 0 additions & 1 deletion UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
8 changes: 5 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down
2 changes: 2 additions & 0 deletions src/DependencyInjection/CompilerPass/AbstractGatewayPass.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Reference;
use Terminal42\NotificationCenterBundle\BulkyItem\BulkyItemStorage;
use Terminal42\NotificationCenterBundle\DependencyInjection\Terminal42NotificationCenterExtension;
use Terminal42\NotificationCenterBundle\Gateway\AbstractGateway;
use Terminal42\NotificationCenterBundle\NotificationCenter;
Expand All @@ -20,6 +21,7 @@ public function process(ContainerBuilder $container): void
$taggedServices = $container->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),
];
Expand Down
2 changes: 1 addition & 1 deletion src/EventListener/ProcessFormDataListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,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);
Expand Down
32 changes: 27 additions & 5 deletions src/Gateway/AbstractGateway.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Contao\CoreBundle\InsertTag\InsertTagParser;
use Contao\CoreBundle\String\SimpleTokenParser;
use Psr\Container\ContainerInterface;
use Terminal42\NotificationCenterBundle\BulkyItem\BulkyItemStorage;
use Terminal42\NotificationCenterBundle\Exception\Parcel\CouldNotDeliverParcelException;
use Terminal42\NotificationCenterBundle\Exception\Parcel\CouldNotSealParcelException;
use Terminal42\NotificationCenterBundle\NotificationCenter;
Expand All @@ -20,6 +21,8 @@ abstract class AbstractGateway implements GatewayInterface
{
public const SERVICE_NAME_NOTIFICATION_CENTER = 'notification_center';

public const SERVICE_NAME_BULKY_ITEM_STORAGE = 'bulky_item_storage';

public const SERVICE_NAME_SIMPLE_TOKEN_PARSER = 'simple_token_parser';

public const SERVICE_NAME_INSERT_TAG_PARSER = 'insert_tag_parser';
Expand Down Expand Up @@ -83,15 +86,23 @@ protected function replaceTokens(Parcel $parcel, string $value): string
return $value;
}

return $this->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
Expand Down Expand Up @@ -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;
}
}
91 changes: 71 additions & 20 deletions src/Gateway/MailerGateway.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
) {
}
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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(
Expand All @@ -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;
}

Expand Down Expand Up @@ -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)) {
Expand All @@ -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,
);
}
}
11 changes: 9 additions & 2 deletions src/NotificationCenter.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,22 @@ 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,
) {
}

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;
}

/**
Expand Down
28 changes: 27 additions & 1 deletion src/Parcel/Stamp/Mailer/EmailStamp.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ class EmailStamp implements StampInterface
*/
private array $attachmentVouchers = [];

/**
* @var array<string>
*/
private array $embeddedImageVouchers = [];

public function withFromName(string $fromName): self
{
$clone = clone $this;
Expand Down Expand Up @@ -106,6 +111,22 @@ public function withHtml(string $html): self
return $clone;
}

/**
* @return array<string>
*/
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;
Expand Down Expand Up @@ -170,6 +191,7 @@ public function toArray(): array
'text' => $this->text,
'html' => $this->html,
'attachmentVouchers' => $this->attachmentVouchers,
'embeddedImageVouchers' => $this->embeddedImageVouchers,
];
}

Expand All @@ -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;
}
}
Loading

0 comments on commit d768b3a

Please sign in to comment.