Skip to content

Commit

Permalink
Merge pull request #42 from Setono/fbc
Browse files Browse the repository at this point in the history
FBC/FBP support
  • Loading branch information
igormukhingmailcom authored Nov 26, 2021
2 parents eddc140 + 7bc2a8b commit 42d9863
Show file tree
Hide file tree
Showing 13 changed files with 398 additions and 1 deletion.
22 changes: 22 additions & 0 deletions src/DataMapper/RequestDataMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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']
*/
Expand Down Expand Up @@ -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);
}
}
}
8 changes: 8 additions & 0 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()
;

Expand Down
4 changes: 3 additions & 1 deletion src/DependencyInjection/SetonoSyliusFacebookExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
Expand All @@ -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');

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

declare(strict_types=1);

namespace Setono\SyliusFacebookPlugin\EventListener;

use Setono\BotDetectionBundle\BotDetector\BotDetectorInterface;
use Setono\SyliusFacebookPlugin\Context\PixelContextInterface;
use Setono\SyliusFacebookPlugin\Generator\PixelEventsGeneratorInterface;
use Setono\SyliusFacebookPlugin\Manager\FbcManagerInterface;
use Setono\SyliusFacebookPlugin\Manager\FbpManagerInterface;
use Symfony\Bundle\SecurityBundle\Security\FirewallMap;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;

final class SetCookiesSubscriber extends AbstractSubscriber
{
private FbcManagerInterface $fbcManager;

private FbpManagerInterface $fbpManager;

public function __construct(
RequestStack $requestStack,
FirewallMap $firewallMap,
PixelContextInterface $pixelContext,
PixelEventsGeneratorInterface $pixelEventsGenerator,
BotDetectorInterface $botDetector,
FbcManagerInterface $fbcManager,
FbpManagerInterface $fbpManager
) {
parent::__construct(
$requestStack,
$firewallMap,
$pixelContext,
$pixelEventsGenerator,
$botDetector
);

$this->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);
}
}
}
135 changes: 135 additions & 0 deletions src/Manager/FbcManager.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
<?php

declare(strict_types=1);

namespace Setono\SyliusFacebookPlugin\Manager;

use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;

/**
* We need this manager to be able to access fbc
* at different stages of Request and generate it only once
*/
final class FbcManager implements FbcManagerInterface
{
private RequestStack $requestStack;

private int $fbcTtl;

private string $fbcCookieName;

/**
* Cached value of fbc generated from fbclid at current request
*/
private ?string $generatedFbc = null;

public function __construct(
RequestStack $requestStack,
int $fbcTtl,
string $fbcCookieName = 'ssf_fbc'
) {
$this->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;
}
}
14 changes: 14 additions & 0 deletions src/Manager/FbcManagerInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace Setono\SyliusFacebookPlugin\Manager;

use Symfony\Component\HttpFoundation\Cookie;

interface FbcManagerInterface
{
public function getFbcCookie(): ?Cookie;

public function getFbcValue(): ?string;
}
89 changes: 89 additions & 0 deletions src/Manager/FbpManager.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php

declare(strict_types=1);

namespace Setono\SyliusFacebookPlugin\Manager;

use Setono\ClientId\Provider\ClientIdProviderInterface;
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;

/**
* We need this manager to be able to access fbp
* at different stages of Request and generate it only once
*/
final class FbpManager implements FbpManagerInterface
{
private RequestStack $requestStack;

private ClientIdProviderInterface $clientIdProvider;

private int $fbpTtl;

private string $fbpCookieName;

public function __construct(
RequestStack $requestStack,
ClientIdProviderInterface $clientIdProvider,
int $fbpTtl,
string $fbpCookieName = 'ssf_fbp'
) {
$this->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
);
}
}
Loading

0 comments on commit 42d9863

Please sign in to comment.