Skip to content

Implemented file tokens for bulky items #375

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Nov 25, 2024
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
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"symfony/http-kernel": "^5.4 || ^6.0 || ^7.0",
"symfony/mailer": "^5.4 || ^6.0 || ^7.0",
"symfony/mime": "^5.4 || ^6.0 || ^7.0",
"symfony/routing": "^5.4 || ^6.0 || ^7.0",
"symfony/security-core": "^5.4 || ^6.0 || ^7.0",
"symfony/service-contracts": "^1.1 || ^2.0 || ^3.0",
"symfony/translation-contracts": "^2.0 || ^3.0",
Expand Down
10 changes: 10 additions & 0 deletions config/listeners.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Codefog\HasteBundle\Formatter;
use Symfony\Contracts\Translation\TranslatorInterface;
use Terminal42\NotificationCenterBundle\Backend\AutoSuggester;
use Terminal42\NotificationCenterBundle\BulkyItem\BulkyItemStorage;
use Terminal42\NotificationCenterBundle\Config\ConfigLoader;
use Terminal42\NotificationCenterBundle\EventListener\AdminEmailTokenListener;
use Terminal42\NotificationCenterBundle\EventListener\Backend\BackendMenuListener;
Expand All @@ -17,6 +18,7 @@
use Terminal42\NotificationCenterBundle\EventListener\Backend\DataContainer\MessageListener;
use Terminal42\NotificationCenterBundle\EventListener\Backend\DataContainer\ModuleListener;
use Terminal42\NotificationCenterBundle\EventListener\Backend\DataContainer\NotificationListener;
use Terminal42\NotificationCenterBundle\EventListener\BulkyItemsTokenListener;
use Terminal42\NotificationCenterBundle\EventListener\DbafsMetadataListener;
use Terminal42\NotificationCenterBundle\EventListener\DisableDeliveryListener;
use Terminal42\NotificationCenterBundle\EventListener\DoctrineSchemaListener;
Expand Down Expand Up @@ -100,6 +102,14 @@
])
;

$services->set(BulkyItemsTokenListener::class)
->args([
service(BulkyItemStorage::class),
service(TokenDefinitionFactoryInterface::class),
service('twig'),
])
;

$services->set(DisableDeliveryListener::class);

$services->set(NotificationTypeForModuleListener::class);
Expand Down
20 changes: 20 additions & 0 deletions config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Terminal42\NotificationCenterBundle\BulkyItem\BulkyItemStorage;
use Terminal42\NotificationCenterBundle\BulkyItem\FileItemFactory;
use Terminal42\NotificationCenterBundle\Config\ConfigLoader;
use Terminal42\NotificationCenterBundle\Controller\DownloadBulkyItemController;
use Terminal42\NotificationCenterBundle\Cron\PruneBulkyItemStorageCron;
use Terminal42\NotificationCenterBundle\DependencyInjection\Terminal42NotificationCenterExtension;
use Terminal42\NotificationCenterBundle\Gateway\GatewayRegistry;
Expand All @@ -18,6 +19,8 @@
use Terminal42\NotificationCenterBundle\Token\Definition\Factory\ChainTokenDefinitionFactory;
use Terminal42\NotificationCenterBundle\Token\Definition\Factory\CoreTokenDefinitionFactory;
use Terminal42\NotificationCenterBundle\Token\Definition\Factory\TokenDefinitionFactoryInterface;
use Terminal42\NotificationCenterBundle\Twig\NotificationCenterExtension;
use Terminal42\NotificationCenterBundle\Twig\NotificationCenterRuntime;

return static function (ContainerConfigurator $container): void {
$services = $container->services();
Expand All @@ -31,6 +34,14 @@
])
;

$services->set(DownloadBulkyItemController::class)
->args([
service('uri_signer'),
service(BulkyItemStorage::class),
])
->public()
;

$services->set(GatewayRegistry::class)
->args([
tagged_iterator(Terminal42NotificationCenterExtension::GATEWAY_TAG),
Expand All @@ -57,6 +68,8 @@
$services->set(BulkyItemStorage::class)
->args([
service('contao.filesystem.virtual.'.Terminal42NotificationCenterExtension::BULKY_ITEMS_VFS_NAME),
service('router'),
service('uri_signer'),
])
;

Expand All @@ -72,6 +85,13 @@
])
;

$services->set(NotificationCenterExtension::class);
$services->set(NotificationCenterRuntime::class)
->args([
service(BulkyItemStorage::class),
])
;

$services->set(NotificationCenter::class)
->args([
service('database_connection'),
Expand Down
Empty file added contao/templates/.twig-root
Empty file.
13 changes: 13 additions & 0 deletions contao/templates/notification_center/file_token.html.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{% if format is same as 'html' %}
<ul>
{% for voucher, file in files %}
<li><a href="{{ notification_center_file_url(voucher) }}">{{ file.name }} ({{ file.size|format_bytes }})</a></li>
{% endfor %}
</ul>
{% endif %}

{% if format is same as 'text' %}
{% for voucher, file in files %}
- [{{ file.name }} ({{ file.size|format_bytes }})]({{ notification_center_file_url(voucher) }})
{% endfor %}
{% endif %}
21 changes: 16 additions & 5 deletions src/BulkyItem/BulkyItemStorage.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,19 @@
use Contao\CoreBundle\Filesystem\ExtraMetadata;
use Contao\CoreBundle\Filesystem\VirtualFilesystemException;
use Contao\CoreBundle\Filesystem\VirtualFilesystemInterface;
use Symfony\Component\HttpFoundation\UriSigner;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Uid\Uuid;

class BulkyItemStorage
{
public const VOUCHER_REGEX = '^\d{8}/[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$';

public function __construct(
private readonly VirtualFilesystemInterface $filesystem,
private readonly RouterInterface $router,
private readonly UriSigner $uriSigner,
private readonly int $retentionPeriodInDays = 7,
) {
}
Expand Down Expand Up @@ -95,12 +102,16 @@ public function prune(): void
}
}

public static function validateVoucherFormat(string $voucher): bool
public function generatePublicUri(string $voucher, int|null $ttl = null): string
{
if (!preg_match('@^\d{8}/@', $voucher)) {
return false;
}
return $this->uriSigner->sign(
$this->router->generate('nc_bulky_item_download', ['voucher' => $voucher], UrlGeneratorInterface::ABSOLUTE_URL),
time() + $ttl,
);
}

return Uuid::isValid(substr($voucher, 9));
public static function validateVoucherFormat(string $voucher): bool
{
return 1 === preg_match('@'.self::VOUCHER_REGEX.'@', $voucher);
}
}
13 changes: 12 additions & 1 deletion src/ContaoManager/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@
use Contao\ManagerPlugin\Bundle\BundlePluginInterface;
use Contao\ManagerPlugin\Bundle\Config\BundleConfig;
use Contao\ManagerPlugin\Bundle\Parser\ParserInterface;
use Contao\ManagerPlugin\Routing\RoutingPluginInterface;
use Symfony\Component\Config\Loader\LoaderResolverInterface;
use Symfony\Component\HttpKernel\KernelInterface;
use Terminal42\NotificationCenterBundle\Terminal42NotificationCenterBundle;

class Plugin implements BundlePluginInterface
class Plugin implements BundlePluginInterface, RoutingPluginInterface
{
public function getBundles(ParserInterface $parser): array
{
Expand All @@ -20,4 +23,12 @@ public function getBundles(ParserInterface $parser): array
->setLoadAfter([ContaoCoreBundle::class]),
];
}

public function getRouteCollection(LoaderResolverInterface $resolver, KernelInterface $kernel)
{
return $resolver
->resolve(__DIR__.'/../Controller/DownloadBulkyItemController.php', 'attribute')
->load(__DIR__.'/../Controller/DownloadBulkyItemController.php')
;
}
}
51 changes: 51 additions & 0 deletions src/Controller/DownloadBulkyItemController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

declare(strict_types=1);

namespace Terminal42\NotificationCenterBundle\Controller;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpFoundation\UriSigner;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Attribute\Route;
use Terminal42\NotificationCenterBundle\BulkyItem\BulkyItemStorage;

#[Route('/notifications/download/{voucher}', 'nc_bulky_item_download', requirements: ['voucher' => BulkyItemStorage::VOUCHER_REGEX])]
class DownloadBulkyItemController
{
public function __construct(
private readonly UriSigner $uriSigner,
private readonly BulkyItemStorage $bulkyItemStorage,
) {
}

public function __invoke(Request $request, string $voucher): Response
{
if (!$this->uriSigner->checkRequest($request)) {
throw new NotFoundHttpException();
}

if (!$bulkyItem = $this->bulkyItemStorage->retrieve($voucher)) {
throw new NotFoundHttpException();
}

$stream = $bulkyItem->getContents();

$response = new StreamedResponse(
static function () use ($stream): void {
while (!feof($stream)) {
echo fread($stream, 8192); // Read in chunks of 8 KB
flush();
}
fclose($stream);
},
);

$response->headers->set('Content-Type', 'application/octet-stream');
$response->headers->set('Cache-Control', 'no-cache, no-store, must-revalidate');

return $response;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public function load(array $configs, ContainerBuilder $container): void
}

$container->findDefinition(BulkyItemStorage::class)
->setArgument(1, $config['bulky_items_storage']['retention_period'])
->setArgument(3, $config['bulky_items_storage']['retention_period'])
;
}

Expand Down
105 changes: 105 additions & 0 deletions src/EventListener/BulkyItemsTokenListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<?php

declare(strict_types=1);

namespace Terminal42\NotificationCenterBundle\EventListener;

use Contao\StringUtil;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Terminal42\NotificationCenterBundle\BulkyItem\BulkyItemInterface;
use Terminal42\NotificationCenterBundle\BulkyItem\BulkyItemStorage;
use Terminal42\NotificationCenterBundle\Event\CreateParcelEvent;
use Terminal42\NotificationCenterBundle\Event\GetTokenDefinitionsForNotificationTypeEvent;
use Terminal42\NotificationCenterBundle\Parcel\Parcel;
use Terminal42\NotificationCenterBundle\Parcel\Stamp\BulkyItemsStamp;
use Terminal42\NotificationCenterBundle\Parcel\Stamp\TokenCollectionStamp;
use Terminal42\NotificationCenterBundle\Token\Definition\AnythingTokenDefinition;
use Terminal42\NotificationCenterBundle\Token\Definition\Factory\TokenDefinitionFactoryInterface;
use Terminal42\NotificationCenterBundle\Token\Definition\HtmlTokenDefinition;
use Terminal42\NotificationCenterBundle\Token\Definition\TextTokenDefinition;
use Terminal42\NotificationCenterBundle\Token\Token;
use Twig\Environment;

class BulkyItemsTokenListener
{
public function __construct(
private readonly BulkyItemStorage $bulkyItemStorage,
private readonly TokenDefinitionFactoryInterface $tokenDefinitionFactory,
private readonly Environment $twig,
) {
}

#[AsEventListener]
public function onGetTokenDefinitions(GetTokenDefinitionsForNotificationTypeEvent $event): void
{
$event
->addTokenDefinition($this->tokenDefinitionFactory->create(AnythingTokenDefinition::class, 'file_item_html_*', 'file_item_html_*'))
->addTokenDefinition($this->tokenDefinitionFactory->create(AnythingTokenDefinition::class, 'file_item_text_*', 'file_item_text_*'))
;
}

#[AsEventListener]
public function onCreateParcel(CreateParcelEvent $event): void
{
if (!$event->getParcel()->hasStamp(TokenCollectionStamp::class) || !$event->getParcel()->getStamp(BulkyItemsStamp::class)) {
return;
}

$tokenCollection = $event->getParcel()->getStamp(TokenCollectionStamp::class)->tokenCollection;

foreach ($tokenCollection as $token) {
$items = $this->extractFileItems($token, $event->getParcel()->getStamp(BulkyItemsStamp::class));

if ([] === $items) {
continue;
}

$tokenCollection->addToken($this->createFileToken($event->getParcel(), $token, $items, 'html', HtmlTokenDefinition::class));
$tokenCollection->addToken($this->createFileToken($event->getParcel(), $token, $items, 'text', TextTokenDefinition::class));
}
}

/**
* @param array<string, BulkyItemInterface> $items
*/
private function createFileToken(Parcel $parcel, Token $token, array $items, string $format, string $tokenDefinitionClass): Token
{
$content = $this->twig->render('@Contao/notification_center/file_token.html.twig', [
'files' => $items,
'parcel' => $parcel,
'format' => $format,
]);

$tokenName = 'file_item_'.$format.'_'.$token->getName();

return $this->tokenDefinitionFactory->create($tokenDefinitionClass, $tokenName, $tokenName)
->createToken($tokenName, $content)
;
}

/**
* @return array<string, BulkyItemInterface>
*/
private function extractFileItems(Token $token, BulkyItemsStamp $bulkyItemsStamp): array
{
$possibleVouchers = StringUtil::trimsplit(',', $token->getParserValue());
$items = [];

foreach ($possibleVouchers as $possibleVoucher) {
// Shortcut: Not a possibly bulky item voucher anyway - continue
if (!BulkyItemStorage::validateVoucherFormat($possibleVoucher)) {
continue;
}

if (!$bulkyItemsStamp->has($possibleVoucher)) {
continue;
}

if ($item = $this->bulkyItemStorage->retrieve($possibleVoucher)) {
$items[$possibleVoucher] = $item;
}
}

return $items;
}
}
Loading
Loading