diff --git a/src/DataMapper/RequestDataMapper.php b/src/DataMapper/RequestDataMapper.php index 8bb3c4f..9024b9f 100644 --- a/src/DataMapper/RequestDataMapper.php +++ b/src/DataMapper/RequestDataMapper.php @@ -4,12 +4,24 @@ namespace Setono\SyliusFacebookPlugin\DataMapper; +use Setono\SyliusFacebookPlugin\Manager\FbcManagerInterface; +use Setono\SyliusFacebookPlugin\Manager\FbpManagerInterface; use Setono\SyliusFacebookPlugin\ServerSide\ServerSideEventInterface; use Symfony\Component\HttpFoundation\Request; use Webmozart\Assert\Assert; /* not final */ class RequestDataMapper implements DataMapperInterface { + protected FbcManagerInterface $fbcManager; + + protected FbpManagerInterface $fbpManager; + + public function __construct(FbcManagerInterface $fbcManager, FbpManagerInterface $fbpManager) + { + $this->fbcManager = $fbcManager; + $this->fbpManager = $fbpManager; + } + /** * @psalm-assert-if-true Request $context['request'] */ @@ -37,5 +49,15 @@ public function map(object $source, ServerSideEventInterface $target, array $con /** @psalm-suppress PossiblyNullArgument */ $userData->setClientUserAgent($request->headers->get('User-Agent')); + + $fbc = $this->fbcManager->getFbcValue(); + if (is_string($fbc)) { + $userData->setFbc($fbc); + } + + $fbp = $this->fbpManager->getFbpValue(); + if (is_string($fbp)) { + $userData->setFbp($fbp); + } } } diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index f8ffe4b..5cd9fed 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -56,6 +56,14 @@ public function getConfigTreeBuilder(): TreeBuilder ->defaultValue(30 * 24 * 60 * 60) // 30 days ->info('The number of seconds to wait until remove sent event') ->end() + ->integerNode('fbc_ttl') + ->defaultValue(28 * 24 * 60 * 60) // 28 days + ->info('Time to live for fbc cookie') + ->end() + ->integerNode('fbp_ttl') + ->defaultValue(365 * 24 * 60 * 60) // 365 days + ->info('Time to live for fbp cookie') + ->end() ->end() ; diff --git a/src/DependencyInjection/SetonoSyliusFacebookExtension.php b/src/DependencyInjection/SetonoSyliusFacebookExtension.php index a60e11e..b1a3944 100644 --- a/src/DependencyInjection/SetonoSyliusFacebookExtension.php +++ b/src/DependencyInjection/SetonoSyliusFacebookExtension.php @@ -18,7 +18,7 @@ public function load(array $configs, ContainerBuilder $container): void /** * @psalm-suppress PossiblyNullArgument * - * @var array{api_version: string, access_token: string, test_event_code: string|null, send_delay: int, cleanup_delay:int, driver: string, resources: array} $config + * @var array{api_version: string, access_token: string, test_event_code: string|null, send_delay: int, cleanup_delay:int, fbc_ttl: int, fbp_ttl: int, driver: string, resources: array} $config */ $config = $this->processConfiguration($this->getConfiguration([], $container), $configs); $loader = new XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); @@ -28,6 +28,8 @@ public function load(array $configs, ContainerBuilder $container): void $container->setParameter('setono_sylius_facebook.test_event_code', $config['test_event_code']); $container->setParameter('setono_sylius_facebook.send_delay', $config['send_delay']); $container->setParameter('setono_sylius_facebook.cleanup_delay', $config['cleanup_delay']); + $container->setParameter('setono_sylius_facebook.fbc_ttl', $config['fbc_ttl']); + $container->setParameter('setono_sylius_facebook.fbp_ttl', $config['fbp_ttl']); $loader->load('services.xml'); diff --git a/src/EventListener/SetCookiesSubscriber.php b/src/EventListener/SetCookiesSubscriber.php new file mode 100644 index 0000000..b4f4793 --- /dev/null +++ b/src/EventListener/SetCookiesSubscriber.php @@ -0,0 +1,73 @@ +fbcManager = $fbcManager; + $this->fbpManager = $fbpManager; + } + + public static function getSubscribedEvents(): array + { + return [ + KernelEvents::RESPONSE => 'setCookies', + ]; + } + + public function setCookies(ResponseEvent $event): void + { + if (!$this->isRequestEligible()) { + return; + } + + $request = $this->requestStack->getCurrentRequest(); + if (null === $request || $request->isXmlHttpRequest()) { + return; + } + + $response = $event->getResponse(); + $fbcCookie = $this->fbcManager->getFbcCookie(); + if (null !== $fbcCookie) { + $response->headers->setCookie($fbcCookie); + } + + $fbpCookie = $this->fbpManager->getFbpCookie(); + if (null !== $fbpCookie) { + $response->headers->setCookie($fbpCookie); + } + } +} diff --git a/src/Manager/FbcManager.php b/src/Manager/FbcManager.php new file mode 100644 index 0000000..630cd5c --- /dev/null +++ b/src/Manager/FbcManager.php @@ -0,0 +1,135 @@ +requestStack = $requestStack; + $this->fbcTtl = $fbcTtl; + $this->fbcCookieName = $fbcCookieName; + } + + public function getFbcCookie(): ?Cookie + { + $fbc = $this->getFbcValue(); + if (null === $fbc) { + return null; + } + + return Cookie::create( + $this->fbcCookieName, + $fbc, + time() + $this->fbcTtl + ); + } + + /** + * We call it twice per request: + * 1. When populate fbc to UserData (on request) + * 2. When setting a fbc cookie (on response) + */ + public function getFbcValue(): ?string + { + // We already have fbc generated at previous call + if (null !== $this->generatedFbc) { + return $this->generatedFbc; + } + + $request = $this->requestStack->getCurrentRequest(); + if (null === $request) { + return null; + } + + /** @var string|null $fbc */ + $fbc = $request->cookies->get($this->fbcCookieName); + + /** @var string|null $fbclid */ + $fbclid = $request->query->get('fbclid'); + + // We have both fbc and fbclid + if (is_string($fbclid) && is_string($fbc)) { + // So should decide if we should regenerate it. + // Extracting fbclid from fbc to compare + $existingFbclid = $this->extractFbclid($fbc); + + // If fbclid is the same - we shouldn't regenerate fbc + // and use old one from cookie with old timestamp + if ($existingFbclid !== $fbclid) { + return $this->generateFbc($fbclid); + } + } + + // We have fbc cookie and shouldn't try to + // regenerate it from fbclid (as it is empty) + if (is_string($fbc)) { + return $fbc; + } + + // Have no fbc cookie, but have fbclid + // to generate fbc from it + if (is_string($fbclid)) { + return $this->generateFbc($fbclid); + } + + // We have no fbc cookie and no fbclid, so can't generate + return null; + } + + private function generateFbc(string $fbclid): string + { + $creationTime = ceil(microtime(true) * 1000); + + $fbc = sprintf( + 'fb.1.%s.%s', + $creationTime, + $fbclid + ); + + $this->generatedFbc = $fbc; + + return $fbc; + } + + private function extractFbclid(string $fbc): ?string + { + if (false === preg_match('/fb\.1\.(\d+)\.(.+)/', $fbc, $m)) { + return null; + } + + if (!isset($m[2])) { + return null; + } + + /** @var string $fbclid */ + $fbclid = $m[2]; + + return $fbclid; + } +} diff --git a/src/Manager/FbcManagerInterface.php b/src/Manager/FbcManagerInterface.php new file mode 100644 index 0000000..a566340 --- /dev/null +++ b/src/Manager/FbcManagerInterface.php @@ -0,0 +1,14 @@ +requestStack = $requestStack; + $this->clientIdProvider = $clientIdProvider; + $this->fbpTtl = $fbpTtl; + $this->fbpCookieName = $fbpCookieName; + } + + public function getfbpCookie(): ?Cookie + { + $fbp = $this->getfbpValue(); + if (null === $fbp) { + return null; + } + + return Cookie::create( + $this->fbpCookieName, + $fbp, + time() + $this->fbpTtl + ); + } + + /** + * We call it twice per request: + * 1. When populate fbp to UserData (on request) + * 2. When setting a fbp cookie (on response) + */ + public function getFbpValue(): ?string + { + $request = $this->requestStack->getCurrentRequest(); + if (null === $request) { + return null; + } + + /** @var string|null $fbp */ + $fbp = $request->cookies->get($this->fbpCookieName); + + // We have fbp cookie and shouldn't try to + // regenerate it from fbplid (as it is empty) + if (is_string($fbp)) { + return $fbp; + } + + $clientId = (string) $this->clientIdProvider->getClientId(); + + return $this->generateFbp($clientId); + } + + private function generateFbp(string $clientId): string + { + $creationTime = ceil(microtime(true) * 1000); + + return sprintf( + 'fb.1.%s.%s', + $creationTime, + $clientId + ); + } +} diff --git a/src/Manager/FbpManagerInterface.php b/src/Manager/FbpManagerInterface.php new file mode 100644 index 0000000..4022dc8 --- /dev/null +++ b/src/Manager/FbpManagerInterface.php @@ -0,0 +1,14 @@ + + diff --git a/src/Resources/config/services/data_mapper.xml b/src/Resources/config/services/data_mapper.xml index 58924db..0f494b1 100644 --- a/src/Resources/config/services/data_mapper.xml +++ b/src/Resources/config/services/data_mapper.xml @@ -33,6 +33,8 @@ + + diff --git a/src/Resources/config/services/event_listener.xml b/src/Resources/config/services/event_listener.xml index 0c7292b..fc9c85b 100644 --- a/src/Resources/config/services/event_listener.xml +++ b/src/Resources/config/services/event_listener.xml @@ -13,6 +13,15 @@ + + + + + + + diff --git a/src/Resources/config/services/manager.xml b/src/Resources/config/services/manager.xml new file mode 100644 index 0000000..78ac34e --- /dev/null +++ b/src/Resources/config/services/manager.xml @@ -0,0 +1,26 @@ + + + + + + + %setono_sylius_facebook.fbc_ttl% + + + + + + + + %setono_sylius_facebook.fbp_ttl% + + + + + + diff --git a/tests/DependencyInjection/ConfigurationTest.php b/tests/DependencyInjection/ConfigurationTest.php index 2e3e127..cba639d 100644 --- a/tests/DependencyInjection/ConfigurationTest.php +++ b/tests/DependencyInjection/ConfigurationTest.php @@ -58,6 +58,8 @@ public function processed_value_contains_required_and_default_values(): void 'api_version' => 'v12.0', 'send_delay' => 300, 'cleanup_delay' => 2592000, + 'fbc_ttl' => 2419200, + 'fbp_ttl' => 31536000, ]); } }