Issues (85)

src/EventListener/VisitorTrackingSubscriber.php (2 issues)

Severity
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Alpha\VisitorTrackingBundle\EventListener;
6
7
use Alpha\VisitorTrackingBundle\Entity\Lifetime;
8
use Alpha\VisitorTrackingBundle\Entity\PageView;
9
use Alpha\VisitorTrackingBundle\Entity\Session;
10
use Alpha\VisitorTrackingBundle\Storage\SessionStore;
11
use Doctrine\Inflector\InflectorFactory;
12
use Doctrine\ORM\EntityManager;
13
use Symfony\Bundle\SecurityBundle\Security\FirewallMap;
14
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
15
use Symfony\Component\HttpFoundation\Cookie;
16
use Symfony\Component\HttpFoundation\RedirectResponse;
17
use Symfony\Component\HttpFoundation\Request;
18
use Symfony\Component\HttpFoundation\Response;
19
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
20
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
21
use Symfony\Component\HttpKernel\KernelEvents;
22
23
/**
24
 * Tracks the source of a session and each page view in that session.
25
 */
26
class VisitorTrackingSubscriber implements EventSubscriberInterface
27
{
28
    public const COOKIE_LIFETIME = 'lifetime';
29
30
    public const COOKIE_SESSION = 'session';
31
32
    protected const UTM_CODES = [
33
        'utm_source' => 'utm_source',
34
        'utm_medium' => 'utm_medium',
35
        'utm_campaign' => 'utm_campaign',
36
        'utm_term' => 'utm_term',
37
        'utm_content' => 'utm_content',
38
    ];
39
40
    protected const COOKIE_SESSION_TTL = '+2 years';
41
42
    private $entityManager;
43
44
    private $sessionStore;
45
46
    private $firewallBlacklist;
47
48
    private $firewallMap;
49
50
    public function __construct(
51
        EntityManager $entityManager,
52
        SessionStore $sessionStore,
53
        array $firewallBlacklist,
54
        FirewallMap $firewallMap
55
    ) {
56
        $this->entityManager = $entityManager;
57
        $this->sessionStore = $sessionStore;
58
        $this->firewallBlacklist = $firewallBlacklist;
59
        $this->firewallMap = $firewallMap;
60
    }
61
62
    public static function getSubscribedEvents(): iterable
63
    {
64
        return [
65
            KernelEvents::RESPONSE => ['onKernelResponse', 1024],
66
            KernelEvents::REQUEST => ['onKernelRequest', 16],
67
        ];
68
    }
69
70
    public function onKernelRequest(GetResponseEvent $event): void
71
    {
72
        $request = $event->getRequest();
73
74
        if ($this->isBlacklistedFirewall($request) || !$this->shouldActOnRequest($request)) {
75
            return;
76
        }
77
78
        if ($request->cookies->has(self::COOKIE_SESSION)) {
79
            $session = $this->entityManager->getRepository(Session::class)->find($request->cookies->get(self::COOKIE_SESSION));
80
81
            if ($session instanceof Session && (!$this->requestHasUTMParameters($request) || $this->sessionMatchesRequestParameters($request))) {
82
                $this->sessionStore->setSession($session);
83
            } else {
84
                $this->generateSessionAndLifetime($request);
85
            }
86
        } else {
87
            $this->generateSessionAndLifetime($request);
88
        }
89
    }
90
91
    public function onKernelResponse(FilterResponseEvent $event): void
92
    {
93
        if ($this->isBlacklistedFirewall($event->getRequest())) {
94
            return;
95
        }
96
97
        $session = $this->sessionStore->getSession();
98
99
        if (!$session instanceof Session) {
100
            return;
101
        }
102
103
        $request = $event->getRequest();
104
        $response = $event->getResponse();
105
106
        if (!$request->cookies->has(self::COOKIE_LIFETIME)) {
107
            \assert($session->getLifetime() instanceof Lifetime);
108
            $response->headers->setCookie(new Cookie(self::COOKIE_LIFETIME, $session->getLifetime()->getId(), new \DateTime('+2 years'), '/', null, false, false));
109
        }
110
111
        if (!$request->cookies->has(self::COOKIE_SESSION) || ($request->cookies->get(self::COOKIE_SESSION) !== $session->getId())) {
112
            $response->headers->setCookie(new Cookie(self::COOKIE_SESSION, $session->getId(), 0, '/', null, false, false));
113
        }
114
115
        if (!$this->shouldActOnRequest($request)) {
116
            return;
117
        }
118
119
        $pageView = new PageView();
120
        $pageView->setUrl($request->getUri());
121
        $session->addPageView($pageView);
122
123
        $this->entityManager->flush($session);
124
    }
125
126
    protected function requestHasUTMParameters(Request $request): bool
127
    {
128
        foreach (static::UTM_CODES as $code) {
129
            if ($request->query->has($code)) {
130
                return true;
131
            }
132
        }
133
134
        return false;
135
    }
136
137
    protected function setUTMSessionCookies(Request $request, Response $response): void
138
    {
139
        foreach (self::UTM_CODES as $code) {
140
            $response->headers->clearCookie($code);
141
            if ($request->query->has($code)) {
142
                $response->headers->setCookie(new Cookie($code, $request->query->get($code), 0, '/', null, false, false));
143
            }
144
        }
145
    }
146
147
    private function generateSessionAndLifetime(Request $request): void
148
    {
149
        $lifetime = null;
150
151
        if ($request->cookies->has(self::COOKIE_LIFETIME)) {
152
            $lifetime = $this->entityManager->getRepository(Lifetime::class)->find($request->cookies->get(self::COOKIE_LIFETIME));
153
        }
154
155
        if (!$lifetime instanceof Lifetime) {
156
            $lifetime = new Lifetime();
157
            $this->entityManager->persist($lifetime);
158
            $this->entityManager->flush($lifetime);
159
        }
160
161
        $session = new Session();
162
        $this->entityManager->persist($session);
163
        $session->setIp($request->getClientIp() ?: '');
164
        $referer = $request->headers->get('Referer');
165
        $session->setReferrer(\is_string($referer) ? $referer : '');
0 ignored issues
show
The condition is_string($referer) is always true.
Loading history...
166
        $userAgent = $request->headers->get('User-Agent');
167
        $session->setUserAgent(\is_string($userAgent) ? $userAgent : '');
0 ignored issues
show
The condition is_string($userAgent) is always true.
Loading history...
168
        $session->setQueryString($request->getQueryString() ?: '');
169
        $session->setLoanTerm($request->query->get('y') ?: '');
170
        $session->setRepApr($request->query->has('r') ? (string) (\hexdec((string) $request->query->get('r')) / 100) : '');
171
172
        foreach (self::UTM_CODES as $code) {
173
            $method = 'set'.InflectorFactory::create()->build()->classify($code);
174
            $session->$method($request->query->get($code) ?: '');
175
        }
176
177
        $lifetime->addSession($session);
178
179
        $this->entityManager->flush($session);
180
181
        $this->sessionStore->setSession($session);
182
    }
183
184
    private function shouldActOnRequest(Request $request, ?Response $response = null): bool
185
    {
186
        $route = $request->attributes->get('_route');
187
188
        if ($response instanceof RedirectResponse || (!\is_string($route) || 0 === \strpos($route, '_'))) {
189
            //these are requests for assets/symfony toolbar etc. Not relevant for our tracking
190
            return false;
191
        }
192
193
        return true;
194
    }
195
196
    private function isBlacklistedFirewall(Request $request): bool
197
    {
198
        $firewallConfig = $this->firewallMap->getFirewallConfig($request);
199
200
        return $firewallConfig !== null && \in_array($firewallConfig->getName(), $this->firewallBlacklist, true);
201
    }
202
203
    private function sessionMatchesRequestParameters(Request $request): bool
204
    {
205
        $session = $this->sessionStore->getSession();
206
207
        if (!$session instanceof Session) {
208
            return false;
209
        }
210
211
        foreach (self::UTM_CODES as $code) {
212
            $method = 'get'.InflectorFactory::create()->build()->classify($code);
213
214
            if ($request->query->get($code, '') !== $session->$method()) {
215
                return false;
216
            }
217
        }
218
219
        return true;
220
    }
221
}
222