diff --git a/composer.json b/composer.json index 5d4e1fe..fba0d7b 100644 --- a/composer.json +++ b/composer.json @@ -15,9 +15,11 @@ "league/flysystem-aws-s3-v3": "^3.32", "league/flysystem-bundle": "^3.6", "liip/imagine-bundle": "^2.17", + "mobiledetect/mobiledetectlib": "*", "nelmio/security-bundle": "^3.9", "phpdocumentor/reflection-docblock": "^6.0", "phpstan/phpdoc-parser": "^2.3", + "stripe/stripe-php": "*", "symfony/asset": "8.0.*", "symfony/console": "8.0.*", "symfony/doctrine-messenger": "8.0.*", diff --git a/composer.lock b/composer.lock index dd71fc5..7a21855 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "57b0fe3e59760d7d8cb08112bed744a4", + "content-hash": "06d85967d9503db645c96c90e761fa2a", "packages": [ { "name": "aws/aws-crt-php", @@ -2865,6 +2865,70 @@ }, "time": "2025-07-25T09:04:22+00:00" }, + { + "name": "mobiledetect/mobiledetectlib", + "version": "4.8.10", + "source": { + "type": "git", + "url": "https://github.com/serbanghita/Mobile-Detect.git", + "reference": "96b1e1fa9a968de7660a031106ab529f659d0192" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/serbanghita/Mobile-Detect/zipball/96b1e1fa9a968de7660a031106ab529f659d0192", + "reference": "96b1e1fa9a968de7660a031106ab529f659d0192", + "shasum": "" + }, + "require": { + "php": ">=8.0", + "psr/simple-cache": "^3" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^v3.75.0", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^2.1.11", + "phpunit/phpunit": "^9.6.22", + "squizlabs/php_codesniffer": "^3.12.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Detection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Serban Ghita", + "email": "serbanghita@gmail.com", + "homepage": "http://mobiledetect.net", + "role": "Developer" + } + ], + "description": "Mobile_Detect is a lightweight PHP class for detecting mobile devices. It uses the User-Agent string combined with specific HTTP headers to detect the mobile environment.", + "homepage": "https://github.com/serbanghita/Mobile-Detect", + "keywords": [ + "detect mobile devices", + "mobile", + "mobile detect", + "mobile detector", + "php mobile detect" + ], + "support": { + "issues": "https://github.com/serbanghita/Mobile-Detect/issues", + "source": "https://github.com/serbanghita/Mobile-Detect/tree/4.8.10" + }, + "funding": [ + { + "url": "https://github.com/serbanghita", + "type": "github" + } + ], + "time": "2026-01-09T16:21:59+00:00" + }, { "name": "monolog/monolog", "version": "3.10.0", @@ -3797,6 +3861,57 @@ }, "time": "2024-09-11T13:17:53+00:00" }, + { + "name": "psr/simple-cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" + }, + "time": "2021-10-29T13:26:27+00:00" + }, { "name": "ralouphie/getallheaders", "version": "3.0.3", @@ -3921,6 +4036,65 @@ }, "time": "2026-03-03T17:31:43+00:00" }, + { + "name": "stripe/stripe-php", + "version": "v19.4.1", + "source": { + "type": "git", + "url": "https://github.com/stripe/stripe-php.git", + "reference": "095384404587d07de2ad1154c389c4051c5ed92f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/stripe/stripe-php/zipball/095384404587d07de2ad1154c389c4051c5ed92f", + "reference": "095384404587d07de2ad1154c389c4051c5ed92f", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "php": ">=5.6.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "3.94.0", + "phpstan/phpstan": "^1.2", + "phpunit/phpunit": "^5.7 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Stripe\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Stripe and contributors", + "homepage": "https://github.com/stripe/stripe-php/contributors" + } + ], + "description": "Stripe PHP Library", + "homepage": "https://stripe.com/", + "keywords": [ + "api", + "payment processing", + "stripe" + ], + "support": { + "issues": "https://github.com/stripe/stripe-php/issues", + "source": "https://github.com/stripe/stripe-php/tree/v19.4.1" + }, + "time": "2026-03-06T22:53:13+00:00" + }, { "name": "symfony/asset", "version": "v8.0.6", diff --git a/config/services.yaml b/config/services.yaml index a79d710..0071515 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -38,10 +38,3 @@ services: arguments: $manifest: '%kernel.project_dir%/public/build/.vite/manifest.json' - App\Service\MailerService: - arguments: - $smimeCertificate: '%kernel.project_dir%/cert/certificate.pem' - $smimePrivateKey: '%kernel.project_dir%/cert/private-key.pem' - $smimePassphrase: '%env(SMIME_PASSPHRASE)%' - $fromEmail: 'contact@e-cosplay.fr' - $realMail: '%env(bool:REAL_MAIL)%' diff --git a/src/Controller/CspReportController.php b/src/Controller/CspReportController.php new file mode 100644 index 0000000..d1c8304 --- /dev/null +++ b/src/Controller/CspReportController.php @@ -0,0 +1,83 @@ +getContent(); + + if (empty($data)) { + return new Response('No data provided', 400); + } + + $report = json_decode($data, true); + + if (json_last_error() !== JSON_ERROR_NONE) { + $logger->error('CSP Report JSON decode error: ' . json_last_error_msg()); + return new Response('Invalid JSON', 400); + } + + $details = $report['csp-report'] ?? $report; + + $sourceFile = $details['source-file'] ?? ''; + $blockedUri = $details['blocked-uri'] ?? ''; + $documentUri = $details['document-uri'] ?? ''; + $violatedDirective = $details['violated-directive'] ?? ''; + + $shouldIgnore = false; + + if ( + str_contains($sourceFile, 'extension://') || + str_contains($sourceFile, 'chrome-extension') || + str_contains($sourceFile, 'moz-extension') || + str_contains($documentUri, '.local') || + str_contains($sourceFile, 'localhost') || + $blockedUri === 'wasm-eval' || + $blockedUri === 'inline' && str_contains($sourceFile, 'node_modules') || + $blockedUri === 'about:blank' + ) { + $shouldIgnore = true; + } + + if ($shouldIgnore) { + $logger->info('CSP Violation ignored (Extension or Local Dev): ' . $sourceFile); + return new Response('Report ignored', 204); + } + + $logger->warning('REAL CSP VIOLATION: ' . $data); + + $email = (new Email()) + ->from('security-notify@e-cosplay.fr') + ->to('contact@e-cosplay.fr') + ->subject('Alerte Securite : Violation CSP detectee') + ->priority(Email::PRIORITY_HIGH) + ->text( + "Un rapport de violation CSP potentiellement critique a ete intercepte.\n\n" . + "Document: " . $documentUri . "\n" . + "Directive violee: " . $violatedDirective . "\n" . + "Element bloque: " . $blockedUri . "\n" . + "Fichier source: " . $sourceFile . "\n\n" . + "Details complets :\n" . + json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) + ); + + try { + $mailer->send($email); + } catch (\Exception $e) { + $logger->error('Failed to send CSP report email: ' . $e->getMessage()); + } + + return new Response('Report processed', 204); + } +} diff --git a/src/Controller/EmailTrackingController.php b/src/Controller/EmailTrackingController.php new file mode 100644 index 0000000..1a2d2c1 --- /dev/null +++ b/src/Controller/EmailTrackingController.php @@ -0,0 +1,35 @@ +findOneBy(['messageId' => $messageId]); + + if ($tracking !== null) { + $tracking->markAsOpened(); + $em->flush(); + } + + $response = new BinaryFileResponse($projectDir . '/public/logo.jpg'); + $response->headers->set('Content-Type', 'image/jpeg'); + $response->headers->set('Cache-Control', 'no-store'); + + return $response; + } +} diff --git a/src/Controller/RedirectController.php b/src/Controller/RedirectController.php new file mode 100644 index 0000000..d65d029 --- /dev/null +++ b/src/Controller/RedirectController.php @@ -0,0 +1,23 @@ +query->get('redirUrl'); + + if (!$url) { + return $this->redirectToRoute('app_home'); + } + + return $this->render('pages/external_redirect.twig'); + } +} diff --git a/src/Entity/EmailTracking.php b/src/Entity/EmailTracking.php new file mode 100644 index 0000000..69466b9 --- /dev/null +++ b/src/Entity/EmailTracking.php @@ -0,0 +1,85 @@ +messageId = $messageId; + $this->recipient = $recipient; + $this->subject = $subject; + $this->state = 'sent'; + $this->sentAt = new \DateTimeImmutable(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getMessageId(): ?string + { + return $this->messageId; + } + + public function getRecipient(): ?string + { + return $this->recipient; + } + + public function getSubject(): ?string + { + return $this->subject; + } + + public function getState(): ?string + { + return $this->state; + } + + public function getSentAt(): ?\DateTimeImmutable + { + return $this->sentAt; + } + + public function getOpenedAt(): ?\DateTimeImmutable + { + return $this->openedAt; + } + + public function markAsOpened(): void + { + if ($this->state === 'sent') { + $this->state = 'opened'; + $this->openedAt = new \DateTimeImmutable(); + } + } +} diff --git a/src/Repository/EmailTrackingRepository.php b/src/Repository/EmailTrackingRepository.php new file mode 100644 index 0000000..89b39a1 --- /dev/null +++ b/src/Repository/EmailTrackingRepository.php @@ -0,0 +1,18 @@ + + */ +class EmailTrackingRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, EmailTracking::class); + } +} diff --git a/src/Service/UnsubscribeManager.php b/src/Service/UnsubscribeManager.php new file mode 100644 index 0000000..56dbbb8 --- /dev/null +++ b/src/Service/UnsubscribeManager.php @@ -0,0 +1,74 @@ +storagePath = $projectDir . '/var/unsubscribed.json'; + $this->secret = $appSecret; + } + + public function generateToken(string $email): string + { + return hash_hmac('sha256', strtolower(trim($email)), $this->secret); + } + + public function isValidToken(string $email, string $token): bool + { + return hash_equals($this->generateToken($email), $token); + } + + public function isUnsubscribed(string $email): bool + { + $hash = $this->generateToken($email); + + return \in_array($hash, $this->loadHashes(), true); + } + + public function unsubscribe(string $email): void + { + $hash = $this->generateToken($email); + $hashes = $this->loadHashes(); + + if (!\in_array($hash, $hashes, true)) { + $hashes[] = $hash; + $this->saveHashes($hashes); + } + } + + /** + * @return list + */ + private function loadHashes(): array + { + if (!file_exists($this->storagePath)) { + return []; + } + + $data = json_decode(file_get_contents($this->storagePath), true); + + return \is_array($data) ? $data : []; + } + + /** + * @param list $hashes + */ + private function saveHashes(array $hashes): void + { + $dir = \dirname($this->storagePath); + if (!is_dir($dir)) { + mkdir($dir, 0o755, true); + } + + file_put_contents($this->storagePath, json_encode(array_values($hashes)), \LOCK_EX); + } +} diff --git a/src/Twig/ViteAssetExtension.php b/src/Twig/ViteAssetExtension.php index 9509c30..23374d0 100644 --- a/src/Twig/ViteAssetExtension.php +++ b/src/Twig/ViteAssetExtension.php @@ -6,7 +6,7 @@ use Detection\MobileDetect; use Psr\Cache\CacheItemPoolInterface; use Twig\Extension\AbstractExtension; use Twig\TwigFunction; - +use Nelmio\SecurityBundle\EventListener\ContentSecurityPolicyListener; class ViteAssetExtension extends AbstractExtension { @@ -17,7 +17,7 @@ class ViteAssetExtension extends AbstractExtension public function __construct( private readonly string $manifest, private readonly CacheItemPoolInterface $cache, - + private readonly ?ContentSecurityPolicyListener $cspListener = null, ) { $this->isDev = $_ENV['VITE_LOAD'] === "0"; } @@ -26,18 +26,21 @@ class ViteAssetExtension extends AbstractExtension { return [ new TwigFunction('vite_asset', $this->asset(...), ['is_safe' => ['html']]), + new TwigFunction('isMobile', $this->isMobile(...), ['is_safe' => ['html']]), new TwigFunction('vite_favicons', $this->favicons(...), ['is_safe' => ['html']]) ]; } - /** - * Récupère le nonce pour les scripts via le Listener de Nelmio - */ protected function getNonce(): string { - return ''; + return $this->cspListener?->getNonce('script') ?? ''; } + public function isMobile(): bool + { + $detect = new MobileDetect(); + return $detect->isMobile() || $detect->isTablet(); + } private function loadManifest(): void { diff --git a/symfony.lock b/symfony.lock index f86e0bc..cc12b39 100644 --- a/symfony.lock +++ b/symfony.lock @@ -87,6 +87,15 @@ "bin/phpunit" ] }, + "stripe/stripe-php": { + "version": "19.4", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "19.0", + "ref": "d6829c693e3927a8972c7671d74a1a5c505712b0" + } + }, "symfony/console": { "version": "8.0", "recipe": { diff --git a/templates/base.html.twig b/templates/base.html.twig index 3cef542..40c736b 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -5,9 +5,11 @@ {% block title %}{% endblock %} {% block stylesheets %}{% endblock %} + {% block javascripts %} + {{ vite_asset('app.js') }} + {% endblock %} {% block body %}{% endblock %} - {% block javascripts %}{% endblock %} diff --git a/templates/email/base.html.twig b/templates/email/base.html.twig index 6606c1d..942225a 100644 --- a/templates/email/base.html.twig +++ b/templates/email/base.html.twig @@ -85,14 +85,14 @@
-

🎫 E-Cosplay Ticket

+

🎫 E-Ticket

{% block content %}{% endblock %}
diff --git a/templates/home/index.html.twig b/templates/home/index.html.twig new file mode 100644 index 0000000..1457891 --- /dev/null +++ b/templates/home/index.html.twig @@ -0,0 +1,7 @@ +{% extends 'base.html.twig' %} + +{% block title %}Accueil - E-Ticket{% endblock %} + +{% block body %} + +{% endblock %}