From 2d80f4b32e7ae5a4fc29ffd2796691876828892a Mon Sep 17 00:00:00 2001 From: Ben Challis Date: Fri, 10 Nov 2017 10:57:57 +0000 Subject: [PATCH 1/5] Initial V2 clean up / development --- .gitignore | 2 + DependencyInjection/Configuration.php | 28 -- EventListener/VisitorTrackingSubscriber.php | 200 -------------- Manager/DeviceFingerprintManager.php | 26 -- README.md | 2 +- composer.json | 17 +- .../AlphaVisitorTrackingBundle.php | 0 .../Controller}/DeviceController.php | 20 +- .../AlphaVisitorTrackingExtension.php | 26 +- src/DependencyInjection/Configuration.php | 35 +++ {Entity => src/Entity}/Device.php | 128 ++++----- {Entity => src/Entity}/Lifetime.php | 113 ++++---- {Entity => src/Entity}/PageView.php | 63 ++--- {Entity => src/Entity}/Seed.php | 74 +++-- {Entity => src/Entity}/Session.php | 255 +++++++++--------- .../VisitorTrackingSubscriber.php | 235 ++++++++++++++++ .../Features}/Context/DeviceContext.php | 24 +- .../Features}/Context/FeatureContext.php | 3 + {Features => src/Features}/device.feature | 0 src/Manager/DeviceFingerprintManager.php | 35 +++ .../Resources}/config/routing.yml | 0 .../Resources}/config/services.yml | 10 +- {Resources => src/Resources}/public/device.js | 0 src/Storage/SessionStore.php | 42 +++ .../Manager/DeviceFingerprintManagerTest.php | 2 +- 25 files changed, 726 insertions(+), 614 deletions(-) create mode 100644 .gitignore delete mode 100644 DependencyInjection/Configuration.php delete mode 100644 EventListener/VisitorTrackingSubscriber.php delete mode 100644 Manager/DeviceFingerprintManager.php rename AlphaVisitorTrackingBundle.php => src/AlphaVisitorTrackingBundle.php (100%) rename {Controller => src/Controller}/DeviceController.php (63%) rename {DependencyInjection => src/DependencyInjection}/AlphaVisitorTrackingExtension.php (63%) create mode 100644 src/DependencyInjection/Configuration.php rename {Entity => src/Entity}/Device.php (83%) rename {Entity => src/Entity}/Lifetime.php (65%) rename {Entity => src/Entity}/PageView.php (69%) rename {Entity => src/Entity}/Seed.php (62%) rename {Entity => src/Entity}/Session.php (77%) create mode 100644 src/EventListener/VisitorTrackingSubscriber.php rename {Features => src/Features}/Context/DeviceContext.php (77%) rename {Features => src/Features}/Context/FeatureContext.php (91%) rename {Features => src/Features}/device.feature (100%) create mode 100644 src/Manager/DeviceFingerprintManager.php rename {Resources => src/Resources}/config/routing.yml (100%) rename {Resources => src/Resources}/config/services.yml (50%) rename {Resources => src/Resources}/public/device.js (100%) create mode 100644 src/Storage/SessionStore.php rename {Tests => tests}/Manager/DeviceFingerprintManagerTest.php (97%) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d1502b0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +vendor/ +composer.lock diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php deleted file mode 100644 index 48c5c9f..0000000 --- a/DependencyInjection/Configuration.php +++ /dev/null @@ -1,28 +0,0 @@ -root('alpha_visitor_tracking'); - - // Here you should define the parameters that are allowed to - // configure your bundle. See the documentation linked above for - // more information on that topic. - return $treeBuilder; - } -} diff --git a/EventListener/VisitorTrackingSubscriber.php b/EventListener/VisitorTrackingSubscriber.php deleted file mode 100644 index 3e182f8..0000000 --- a/EventListener/VisitorTrackingSubscriber.php +++ /dev/null @@ -1,200 +0,0 @@ -em = $em; - } - - public function onKernelRequest(GetResponseEvent $event) - { - $request = $event->getRequest(); - if (substr($request->get("_route"), 0, 1) == "_") { - //these are requests for assets/symfony toolbar etc. Not relevant for our tracking - return; - } - - if ($request->cookies->has(self::COOKIE_SESSION)) { - $this->session = $this->em->getRepository("AlphaVisitorTrackingBundle:Session")->find($request->cookies->get(self::COOKIE_SESSION)); - - if ($this->session instanceof Session && (!$this->requestHasUTMParameters($request) || $this->sessionMatchesRequestParameters($request))) { - $this->lifetime = $this->session->getLifetime(); - } else { - $this->generateSessionAndLifetime($request); - } - } else { - $this->generateSessionAndLifetime($request); - } - } - - public function onKernelResponse(FilterResponseEvent $event) - { - if (false === $this->session instanceof Session) { - return; - } - - $request = $event->getRequest(); - $response = $event->getResponse(); - - if (!$request->cookies->has(self::COOKIE_LIFETIME)) { - $response->headers->setCookie(new Cookie(self::COOKIE_LIFETIME, $this->lifetime->getId(), new \DateTime("+2 years"), "/", null, false, false)); - } - //no session cookie set OR session cookie value != current session ID - if (!$request->cookies->has(self::COOKIE_SESSION) or ($request->cookies->get(self::COOKIE_SESSION) != $this->session->getId())) { - $response->headers->setCookie(new Cookie(self::COOKIE_SESSION, $this->session->getId(), 0, "/", null, false, false)); - } - - if (substr($request->get("_route"), 0, 1) == "_" or ($response instanceof RedirectResponse)) { - //these are requests for assets/symfony toolbar etc. Not relevant for our tracking - return; - } - - $pageView = new PageView(); - $pageView->setUrl($request->getUri()); - - $this->session->addPageView($pageView); - - $this->em->flush($this->session); - } - - protected function requestHasUTMParameters(Request $request) - { - foreach ($this->utmCodes as $code) { - if ($request->query->has($code)) { - return true; - } - } - - return false; - } - - protected function setUTMSessionCookies(Request $request, Response $response) - { - foreach ($this->utmCodes as $code) { - $response->headers->clearCookie($code); - if ($request->query->has($code)) { - $response->headers->setCookie(new Cookie($code, $request->query->get($code), 0, "/", null, false, false)); - } - } - } - - private function sessionMatchesRequestParameters(Request $request) - { - foreach ($this->utmCodes as $code) { - $method = 'get'.Inflector::classify($code); - if ($request->query->get($code, '') !== $this->session->$method()) { - return false; - } - } - - return true; - } - - /** - * @return Session - */ - public function getSession() - { - return $this->session; - } - - /** - * @return Lifetime - */ - public function getLifetime() - { - return $this->lifetime; - } - - public static function getSubscribedEvents() - { - return array( - KernelEvents::RESPONSE => ['onKernelResponse', 1024], - KernelEvents::REQUEST => ["onKernelRequest", 16] - ); - } - - private function generateSessionAndLifetime(Request $request) - { - $lifetime = false; - if ($request->cookies->has(self::COOKIE_LIFETIME)) { - $lifetime = $this->em->getRepository("AlphaVisitorTrackingBundle:Lifetime")->find($request->cookies->get(self::COOKIE_LIFETIME)); - } - if (!$lifetime) { - $lifetime = new Lifetime(); - $this->em->persist($lifetime); - } - $session = new Session(); - $session->setIp($request->getClientIp() ?: ""); - $session->setReferrer($request->headers->get("Referer") ?: ""); - $session->setUserAgent($request->headers->get("User-Agent") ?: ""); - $session->setQueryString($request->getQueryString() ?: ""); - $session->setLoanTerm($request->query->get("y") ?: ""); - $session->setRepApr($request->query->has("r") ? hexdec($request->query->get("r")) / 100 : ""); - foreach ($this->utmCodes as $code) { - $method = "set" . \Doctrine\Common\Inflector\Inflector::classify($code); - $session->$method($request->query->get($code) ?: ""); - } - $lifetime->addSession($session); - - $this->em->flush(); - - $this->session = $session; - $this->lifetime = $lifetime; - } -} diff --git a/Manager/DeviceFingerprintManager.php b/Manager/DeviceFingerprintManager.php deleted file mode 100644 index 5ee5dcf..0000000 --- a/Manager/DeviceFingerprintManager.php +++ /dev/null @@ -1,26 +0,0 @@ -getFingerprint(), true); - - if (JSON_ERROR_NONE === json_last_error() && false === empty($data)) { - foreach ($this->hashes as $field) { - $method = 'set' . ucfirst($field); - $value = array_key_exists($field, $data) ? md5(serialize($data[$field])) : null; - $device->$method($value); - } - } - - return $device; - } -} diff --git a/README.md b/README.md index 12af0d7..5920357 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ Visitor Tracking Bundle ======================= -A Symfony2 bundle to track the requests. +A Symfony2 bundle to track requests. ## Upgrading from 0.x to 1.x diff --git a/composer.json b/composer.json index 2b1367f..c8558f5 100644 --- a/composer.json +++ b/composer.json @@ -15,19 +15,24 @@ } ], "require": { - "php": ">=5.6", - "symfony/framework-bundle": "~2.8|~3.0", + "php": ">=7.1", + "symfony/framework-bundle": "^3.3", "doctrine/doctrine-bundle": "~1.6", "doctrine/orm": "~2.1", - "stof/doctrine-extensions-bundle": "*" + "stof/doctrine-extensions-bundle": "*", + "symfony/security-bundle": "^3.3" }, "require-dev": { "phpunit/phpunit": "^5.6" }, "autoload": { - "psr-0": { - "Alpha\\VisitorTrackingBundle": "" + "psr-4": { + "Alpha\\VisitorTrackingBundle\\": "src/" } }, - "target-dir": "Alpha/VisitorTrackingBundle" + "autoload-dev": { + "psr-4": { + "Alpha\\VisitorTrackingBundle\\Tests\\": "tests/" + } + } } diff --git a/AlphaVisitorTrackingBundle.php b/src/AlphaVisitorTrackingBundle.php similarity index 100% rename from AlphaVisitorTrackingBundle.php rename to src/AlphaVisitorTrackingBundle.php diff --git a/Controller/DeviceController.php b/src/Controller/DeviceController.php similarity index 63% rename from Controller/DeviceController.php rename to src/Controller/DeviceController.php index f556688..5ba331a 100644 --- a/Controller/DeviceController.php +++ b/src/Controller/DeviceController.php @@ -1,29 +1,31 @@ getDoctrine()->getManager(); - $cookie = $request->cookies->get(VisitorTrackingSubscriber::COOKIE_SESSION, false); + $session = $this->get('alpha.visitor_tracking.storage.session')->getSession(); $device = null; - $session = null; - if ($cookie) { - $device = $em->getRepository('AlphaVisitorTrackingBundle:Device')->findOneBySession($cookie); - $session = $em->getRepository('AlphaVisitorTrackingBundle:Session')->find($cookie); + if ($session instanceof Session) { + if ($session->getDevices()->count() > 0) { + $device = $session->getDevices()->first(); + } } - if (false === $device instanceof Device) { + if (!$device instanceof Device) { $device = new Device(); $device->setFingerprint($request->getContent()); $device->setSession($session); @@ -40,4 +42,4 @@ public function fingerprintAction(Request $request) return new Response('', 204); } -} \ No newline at end of file +} diff --git a/DependencyInjection/AlphaVisitorTrackingExtension.php b/src/DependencyInjection/AlphaVisitorTrackingExtension.php similarity index 63% rename from DependencyInjection/AlphaVisitorTrackingExtension.php rename to src/DependencyInjection/AlphaVisitorTrackingExtension.php index 2e6d615..83b281a 100644 --- a/DependencyInjection/AlphaVisitorTrackingExtension.php +++ b/src/DependencyInjection/AlphaVisitorTrackingExtension.php @@ -1,28 +1,32 @@ processConfiguration($configuration, $configs); + $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); $loader->load('services.yml'); + + $this->handleFirewallBlacklist($container, $config); + } + + private function handleFirewallBlacklist(ContainerBuilder $container, array $config): void + { + $subscriber = $container->getDefinition('alpha.visitor_tracking_subscriber'); + + $subscriber->replaceArgument(2, $config['session_subscriber']['firewall_blacklist']); } } diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php new file mode 100644 index 0000000..31cb9b8 --- /dev/null +++ b/src/DependencyInjection/Configuration.php @@ -0,0 +1,35 @@ +root('alpha_visitor_tracking'); + + $rootNode->append($this->createSubscriberNode()); + + return $treeBuilder; + } + + private function createSubscriberNode(): ArrayNodeDefinition + { + $root = (new TreeBuilder())->root('session_subscriber'); + $root->addDefaultsIfNotSet(); + + $root->children()->arrayNode('firewall_blacklist') + ->defaultValue([]) + ->prototype('scalar') + ->cannotBeEmpty(); + + return $root; + } +} diff --git a/Entity/Device.php b/src/Entity/Device.php similarity index 83% rename from Entity/Device.php rename to src/Entity/Device.php index 2204bfd..9d0d634 100644 --- a/Entity/Device.php +++ b/src/Entity/Device.php @@ -1,5 +1,7 @@ id; } - public function setSession(Session $session = null) + public function getSession() + { + return $this->session; + } + + public function setSession(?Session $session = null) { $this->session = $session; return $this; } - public function getSession() + public function getFingerprint() { - return $this->session; + return $this->fingerprint; } public function setFingerprint($fingerprint) @@ -98,16 +103,18 @@ public function setFingerprint($fingerprint) return $this; } - public function getFingerprint() + /** + * @return \DateTime + */ + public function getCreated() { - return $this->fingerprint; + return $this->created; } /** - * Set created - * * @param \DateTime $created - * @return Session + * + * @return $this */ public function setCreated($created) { @@ -117,20 +124,17 @@ public function setCreated($created) } /** - * Get created - * - * @return \DateTime + * @return string */ - public function getCreated() + public function getCanvas() { - return $this->created; + return $this->canvas; } /** - * Set canvas - * * @param string $canvas - * @return Device + * + * @return $this */ public function setCanvas($canvas) { @@ -140,20 +144,17 @@ public function setCanvas($canvas) } /** - * Get canvas - * - * @return string + * @return string */ - public function getCanvas() + public function getFonts() { - return $this->canvas; + return $this->fonts; } /** - * Set fonts - * * @param string $fonts - * @return Device + * + * @return $this */ public function setFonts($fonts) { @@ -163,20 +164,17 @@ public function setFonts($fonts) } /** - * Get fonts - * - * @return string + * @return string */ - public function getFonts() + public function getNavigator() { - return $this->fonts; + return $this->navigator; } /** - * Set navigator - * * @param string $navigator - * @return Device + * + * @return $this */ public function setNavigator($navigator) { @@ -186,20 +184,17 @@ public function setNavigator($navigator) } /** - * Get navigator - * - * @return string + * @return string */ - public function getNavigator() + public function getPlugins() { - return $this->navigator; + return $this->plugins; } /** - * Set plugins - * * @param string $plugins - * @return Device + * + * @return $this */ public function setPlugins($plugins) { @@ -209,20 +204,17 @@ public function setPlugins($plugins) } /** - * Get plugins - * - * @return string + * @return string */ - public function getPlugins() + public function getScreen() { - return $this->plugins; + return $this->screen; } /** - * Set screen - * * @param string $screen - * @return Device + * + * @return $this */ public function setScreen($screen) { @@ -232,20 +224,17 @@ public function setScreen($screen) } /** - * Get screen - * - * @return string + * @return string */ - public function getScreen() + public function getSystemColors() { - return $this->screen; + return $this->systemColors; } /** - * Set systemColors - * * @param string $systemColors - * @return Device + * + * @return $this */ public function setSystemColors($systemColors) { @@ -255,20 +244,17 @@ public function setSystemColors($systemColors) } /** - * Get systemColors - * - * @return string + * @return string */ - public function getSystemColors() + public function getStoredIds() { - return $this->systemColors; + return $this->storedIds; } /** - * Set storedIds - * * @param string $storedIds - * @return Device + * + * @return $this */ public function setStoredIds($storedIds) { @@ -276,14 +262,4 @@ public function setStoredIds($storedIds) return $this; } - - /** - * Get storedIds - * - * @return string - */ - public function getStoredIds() - { - return $this->storedIds; - } } diff --git a/Entity/Lifetime.php b/src/Entity/Lifetime.php similarity index 65% rename from Entity/Lifetime.php rename to src/Entity/Lifetime.php index a94e562..0564ef2 100644 --- a/Entity/Lifetime.php +++ b/src/Entity/Lifetime.php @@ -1,20 +1,22 @@ sessions = new ArrayCollection(); @@ -48,8 +52,6 @@ public function __construct() } /** - * Get id - * * @return string */ public function getId() @@ -58,93 +60,102 @@ public function getId() } /** - * Set created - * - * @param \DateTime $created - * @return Lifetime + * @return \DateTime */ - public function setCreated($created) + public function getCreated() { - $this->created = $created; - - return $this; + return $this->created; } /** - * Get created + * @param \DateTime $created * - * @return \DateTime + * @return $this */ - public function getCreated() + public function setCreated(\DateTime $created) { - return $this->created; + $this->created = $created; + + return $this; } /** - * Add sessions + * @param Session $session * - * @param \Alpha\VisitorTrackingBundle\Entity\Session $sessions - * @return Lifetime + * @return $this */ - public function addSession(\Alpha\VisitorTrackingBundle\Entity\Session $sessions) + public function addSession(Session $session) { - $this->sessions[] = $sessions; - $sessions->setLifetime($this); + if (!$this->sessions->contains($session)) { + $this->sessions->add($session); + } + + if ($session->getLifetime() !== $this) { + $session->setLifetime($this); + } return $this; } /** - * Remove sessions + * @param Session $session * - * @param \Alpha\VisitorTrackingBundle\Entity\Session $sessions + * @return $this */ - public function removeSession(\Alpha\VisitorTrackingBundle\Entity\Session $sessions) + public function removeSession(Session $session) { - $this->sessions->removeElement($sessions); + $this->sessions->removeElement($session); + + return $this; } /** - * Get sessions - * - * @return \Doctrine\Common\Collections\Collection + * @return Collection|Session[] */ - public function getSessions() + public function getSessions(): Collection { return $this->sessions; } /** * @param Seed $seed + * * @return $this */ public function addSeed(Seed $seed) { if (!$this->seeds->contains($seed)) { - $seed->setLifetime($this); $this->seeds->add($seed); } + $seed->setLifetime($this); + return $this; } /** * @param Seed $seed + * + * @return $this */ public function removeSeed(Seed $seed) { $this->seeds->removeElement($seed); + + return $this; } /** - * @return \Doctrine\Common\Collections\ArrayCollection + * @return Collection|Seed[] */ - public function getSeeds() + public function getSeeds(): Collection { return $this->seeds; } /** + * Gets a pre-existing seed, or creates a fresh seed if one does not exist for the given name. + * * $weights is an optional array, ideally associative to name your variations and set their likelihood * eg you want 75% of people to see a green button and 25% to see red use: * $name = "button-colour-test"; @@ -153,16 +164,16 @@ public function getSeeds() * This method will then return the string "green" 75% of the time and "red" 25% of the time. Since this cookie lasts 2 years, its very sticky * Querying your test results then becomes very easy and descriptive * - * @param $name - * @param $numberOfValues - * @param null $weights + * @param string $name + * @param int $numberOfValues + * @param array|null $weights + * * @return string */ - public function getSeed($name, $numberOfValues, $weights = null) + public function getSeed(string $name, int $numberOfValues, array $weights = null): string { - foreach($this->seeds as $seed) - { - if($seed->getName() === $name) { + foreach ($this->seeds as $seed) { + if ($seed->getName() === $name) { return $seed->getValue(); } } diff --git a/Entity/PageView.php b/src/Entity/PageView.php similarity index 69% rename from Entity/PageView.php rename to src/Entity/PageView.php index e1b2214..4206c7e 100644 --- a/Entity/PageView.php +++ b/src/Entity/PageView.php @@ -1,10 +1,6 @@ url = $url; - - return $this; - } - - /** - * Get url - * * @return string */ public function getUrl() @@ -75,21 +52,18 @@ public function getUrl() } /** - * Set created + * @param string $url * - * @param \DateTime $created - * @return PageView + * @return $this */ - public function setCreated($created) + public function setUrl($url) { - $this->created = $created; + $this->url = $url; return $this; } /** - * Get created - * * @return \DateTime */ public function getCreated() @@ -98,25 +72,34 @@ public function getCreated() } /** - * Set session + * @param \DateTime $created * - * @param \Alpha\VisitorTrackingBundle\Entity\Session $session - * @return PageView + * @return $this */ - public function setSession(\Alpha\VisitorTrackingBundle\Entity\Session $session = null) + public function setCreated($created) { - $this->session = $session; + $this->created = $created; return $this; } /** - * Get session - * - * @return \Alpha\VisitorTrackingBundle\Entity\Session + * @return Session */ public function getSession() { return $this->session; } + + /** + * @param Session $session + * + * @return $this + */ + public function setSession(?Session $session = null) + { + $this->session = $session; + + return $this; + } } diff --git a/Entity/Seed.php b/src/Entity/Seed.php similarity index 62% rename from Entity/Seed.php rename to src/Entity/Seed.php index c34dd26..e02982e 100644 --- a/Entity/Seed.php +++ b/src/Entity/Seed.php @@ -1,5 +1,7 @@ name = $name; $this->setValue($numberOfValues, $weights); } - private function setValue($numberOfValues, $weights) - { - if($weights === null){ - $weights = array_fill(1, $numberOfValues, 1); - } - - if(count($weights) !== $numberOfValues){ - throw new \RuntimeException("Number of seed values must equal the count of the weights array"); - } - - $random = mt_rand(1, array_sum($weights)); - - $total = 0; - - foreach($weights as $seed => $weight) - { - $total += $weight; - if($random <= $total){ - $this->value = $seed; - return; - } - } - } - /** * @return int */ @@ -89,10 +69,18 @@ public function getLifetime() /** * @param Lifetime $lifetime + * + * @return $this */ public function setLifetime(Lifetime $lifetime) { $this->lifetime = $lifetime; + + if (!$this->lifetime->getSeeds()->contains($this)) { + $this->lifetime->addSeed($this); + } + + return $this; } /** @@ -104,10 +92,40 @@ public function getName() } /** - * @return int + * @return string */ public function getValue() { return $this->value; } -} \ No newline at end of file + + private function setValue(int $numberOfValues, ?array $weights = null): void + { + if ($weights === null) { + $weights = array_fill(1, $numberOfValues, 1); + } + + if (\count($weights) !== $numberOfValues) { + throw new \RuntimeException('Number of seed values must equal the count of the weights array'); + } + + // This does not need to be cryptographically secure, mt_rand() is roughly 2x faster than random_int(). + /** @noinspection RandomApiMigrationInspection */ + $random = mt_rand(1, array_sum($weights)); + $total = 0; + + foreach ($weights as $seed => $weight) { + $total += $weight; + if ($random <= $total) { + + if (!\is_string($seed)) { + $seed = (string) $seed; + } + + $this->value = $seed; + + return; + } + } + } +} diff --git a/Entity/Session.php b/src/Entity/Session.php similarity index 77% rename from Entity/Session.php rename to src/Entity/Session.php index 1dac0e4..96f7aaf 100644 --- a/Entity/Session.php +++ b/src/Entity/Session.php @@ -1,26 +1,23 @@ pageViews = new \Doctrine\Common\Collections\ArrayCollection(); - $this->devices = new \Doctrine\Common\Collections\ArrayCollection(); + $this->pageViews = new ArrayCollection(); + $this->devices = new ArrayCollection(); } - public function __toString() + public function __toString(): string + { + $id = $this->getId(); + + if (!\is_string($id)) { + return 'N/A'; + } + + return $id; + } + + public function getId() { - return $this->getId(); + return $this->id; } /** - * Get id - * * @return string */ - public function getId() + public function getIp() { - return $this->id; + return $this->ip; } /** - * Set ip - * * @param string $ip + * * @return Session */ public function setIp($ip) @@ -141,19 +173,16 @@ public function setIp($ip) } /** - * Get ip - * * @return string */ - public function getIp() + public function getReferrer() { - return $this->ip; + return $this->referrer; } /** - * Set referrer - * * @param string $referrer + * * @return Session */ public function setReferrer($referrer) @@ -164,19 +193,16 @@ public function setReferrer($referrer) } /** - * Get referrer - * * @return string */ - public function getReferrer() + public function getUserAgent() { - return $this->referrer; + return $this->userAgent; } /** - * Set userAgent - * * @param string $userAgent + * * @return Session */ public function setUserAgent($userAgent) @@ -187,19 +213,16 @@ public function setUserAgent($userAgent) } /** - * Get userAgent - * * @return string */ - public function getUserAgent() + public function getQueryString() { - return $this->userAgent; + return $this->queryString; } /** - * Set queryString - * * @param string $queryString + * * @return Session */ public function setQueryString($queryString) @@ -210,19 +233,16 @@ public function setQueryString($queryString) } /** - * Get queryString - * * @return string */ - public function getQueryString() + public function getUtmSource() { - return $this->queryString; + return $this->utmSource; } /** - * Set utmSource - * * @param string $utmSource + * * @return Session */ public function setUtmSource($utmSource) @@ -233,19 +253,16 @@ public function setUtmSource($utmSource) } /** - * Get utmSource - * * @return string */ - public function getUtmSource() + public function getUtmMedium() { - return $this->utmSource; + return $this->utmMedium; } /** - * Set utmMedium - * * @param string $utmMedium + * * @return Session */ public function setUtmMedium($utmMedium) @@ -256,19 +273,16 @@ public function setUtmMedium($utmMedium) } /** - * Get utmMedium - * * @return string */ - public function getUtmMedium() + public function getUtmCampaign() { - return $this->utmMedium; + return $this->utmCampaign; } /** - * Set utmCampaign - * * @param string $utmCampaign + * * @return Session */ public function setUtmCampaign($utmCampaign) @@ -279,19 +293,16 @@ public function setUtmCampaign($utmCampaign) } /** - * Get utmCampaign - * * @return string */ - public function getUtmCampaign() + public function getUtmTerm() { - return $this->utmCampaign; + return $this->utmTerm; } /** - * Set utmTerm - * * @param string $utmTerm + * * @return Session */ public function setUtmTerm($utmTerm) @@ -302,19 +313,16 @@ public function setUtmTerm($utmTerm) } /** - * Get utmTerm - * * @return string */ - public function getUtmTerm() + public function getUtmContent() { - return $this->utmTerm; + return $this->utmContent; } /** - * Set utmContent - * * @param string $utmContent + * * @return Session */ public function setUtmContent($utmContent) @@ -325,19 +333,16 @@ public function setUtmContent($utmContent) } /** - * Get utmContent - * * @return string */ - public function getUtmContent() + public function getLoanTerm() { - return $this->utmContent; + return $this->loanTerm; } /** - * Set loanTerm - * * @param string $loanTerm + * * @return Session */ public function setLoanTerm($loanTerm) @@ -348,19 +353,16 @@ public function setLoanTerm($loanTerm) } /** - * Get loanTerm - * * @return string */ - public function getLoanTerm() + public function getRepApr() { - return $this->loanTerm; + return $this->repApr; } /** - * Set repApr - * * @param string $repApr + * * @return Session */ public function setRepApr($repApr) @@ -371,22 +373,19 @@ public function setRepApr($repApr) } /** - * Get repApr - * - * @return string + * @return \DateTime */ - public function getRepApr() + public function getCreated() { - return $this->repApr; + return $this->created; } /** - * Set created - * * @param \DateTime $created + * * @return Session */ - public function setCreated($created) + public function setCreated(\DateTime $created) { $this->created = $created; @@ -394,86 +393,96 @@ public function setCreated($created) } /** - * Get created - * - * @return \DateTime + * @return Lifetime */ - public function getCreated() + public function getLifetime() { - return $this->created; + return $this->lifetime; } /** - * Set lifetime + * @param Lifetime $lifetime * - * @param \Alpha\VisitorTrackingBundle\Entity\Lifetime $lifetime * @return Session */ - public function setLifetime(\Alpha\VisitorTrackingBundle\Entity\Lifetime $lifetime = null) + public function setLifetime(?Lifetime $lifetime = null) { $this->lifetime = $lifetime; - return $this; - } + if (!$this->lifetime->getSessions()->contains($this)) { + $this->lifetime->addSession($this); + } - /** - * Get lifetime - * - * @return \Alpha\VisitorTrackingBundle\Entity\Lifetime - */ - public function getLifetime() - { - return $this->lifetime; + return $this; } /** - * Add pageViews + * @param PageView $pageView * - * @param \Alpha\VisitorTrackingBundle\Entity\PageView $pageViews * @return Session */ - public function addPageView(\Alpha\VisitorTrackingBundle\Entity\PageView $pageViews) + public function addPageView(PageView $pageView) { - $this->pageViews[] = $pageViews; - $pageViews->setSession($this); + if ($pageView->getSession() !== $this) { + $pageView->setSession($this); + } + + if (!$this->pageViews->contains($pageView)) { + $this->pageViews->add($pageView); + } return $this; } /** - * Remove pageViews + * @param PageView $pageViews * - * @param \Alpha\VisitorTrackingBundle\Entity\PageView $pageViews + * @return $this */ - public function removePageView(\Alpha\VisitorTrackingBundle\Entity\PageView $pageViews) + public function removePageView(PageView $pageViews) { $this->pageViews->removeElement($pageViews); + + return $this; } public function addDevice(Device $device) { - $this->devices[] = $device; - $device->setSession($this); + if ($device->getSession() !== $this) { + $device->setSession($this); + } + + if (!$this->devices->contains($device)) { + $this->devices->add($device); + } return $this; } + /** + * @param Device $device + * + * @return $this + */ public function removeDevice(Device $device) { $this->devices->removeElement($device); + + return $this; } - public function getDevices() + /** + * @return Collection|Device[] + */ + public function getDevices(): Collection { return $this->devices; } /** - * Get pageViews - * - * @return \Doctrine\Common\Collections\Collection + * @return Collection|PageView[] */ - public function getPageViews() + public function getPageViews(): Collection { return $this->pageViews; } diff --git a/src/EventListener/VisitorTrackingSubscriber.php b/src/EventListener/VisitorTrackingSubscriber.php new file mode 100644 index 0000000..370ccad --- /dev/null +++ b/src/EventListener/VisitorTrackingSubscriber.php @@ -0,0 +1,235 @@ + 'utm_source', + 'utm_medium' => 'utm_medium', + 'utm_campaign' => 'utm_campaign', + 'utm_term' => 'utm_term', + 'utm_content' => 'utm_content', + ]; + + protected const COOKIE_SESSION_TTL = '+2 years'; + + /** + * @var EntityManager + */ + protected $em; + + /** + * @var SessionStore + */ + private $sessionStore; + + /** + * @var array + */ + private $firewallBlacklist; + + /** + * @var FirewallMap + */ + private $firewallMap; + + public function __construct(EntityManager $em, SessionStore $sessionStore, array $firewallBlacklist, FirewallMap $firewallMap) + { + $this->em = $em; + $this->sessionStore = $sessionStore; + $this->firewallBlacklist = $firewallBlacklist; + $this->firewallMap = $firewallMap; + } + + public static function getSubscribedEvents(): iterable + { + return [ + KernelEvents::RESPONSE => ['onKernelResponse', 1024], + KernelEvents::REQUEST => ['onKernelRequest', 16], + ]; + } + + public function onKernelRequest(GetResponseEvent $event): void + { + $request = $event->getRequest(); + + if ($this->isBlacklistedFirewall($request) || !$this->shouldActOnRequest($request)) { + return; + } + + if ($request->cookies->has(self::COOKIE_SESSION)) { + $session = $this->em->getRepository(Session::class)->find($request->cookies->get(self::COOKIE_SESSION)); + + if ($session instanceof Session && (!$this->requestHasUTMParameters($request) || $this->sessionMatchesRequestParameters($request))) { + $this->sessionStore->setSession($session); + } else { + $this->generateSessionAndLifetime($request); + } + } else { + $this->generateSessionAndLifetime($request); + } + } + + public function onKernelResponse(FilterResponseEvent $event): void + { + if ($this->isBlacklistedFirewall($event->getRequest())) { + return; + } + + $session = $this->sessionStore->getSession(); + + if (!$session instanceof Session) { + return; + } + + $request = $event->getRequest(); + $response = $event->getResponse(); + + if (!$request->cookies->has(self::COOKIE_LIFETIME)) { + $response->headers->setCookie(new Cookie(self::COOKIE_LIFETIME, $session->getLifetime()->getId(), new \DateTime('+2 years'), '/', null, false, false)); + } + + if (!$request->cookies->has(self::COOKIE_SESSION) || ($request->cookies->get(self::COOKIE_SESSION) !== $session->getId())) { + $response->headers->setCookie(new Cookie(self::COOKIE_SESSION, $session->getId(), 0, '/', null, false, false)); + } + + if (!$this->shouldActOnRequest($request)) { + return; + } + + $pageView = new PageView(); + $pageView->setUrl($request->getUri()); + $session->addPageView($pageView); + + $this->em->flush($session); + } + + /** + * @deprecated Use the SessionStore directly. + * + * @return Session|null + */ + public function getSession(): ?Session + { + return $this->sessionStore->getSession(); + } + + protected function requestHasUTMParameters(Request $request): bool + { + foreach (static::UTM_CODES as $code) { + if ($request->query->has($code)) { + return true; + } + } + + return false; + } + + protected function setUTMSessionCookies(Request $request, Response $response): void + { + foreach (self::UTM_CODES as $code) { + $response->headers->clearCookie($code); + if ($request->query->has($code)) { + $response->headers->setCookie(new Cookie($code, $request->query->get($code), 0, '/', null, false, false)); + } + } + } + + private function generateSessionAndLifetime(Request $request): void + { + $lifetime = false; + + if ($request->cookies->has(self::COOKIE_LIFETIME)) { + $lifetime = $this->em->getRepository(Lifetime::class)->find($request->cookies->get(self::COOKIE_LIFETIME)); + } + + if (!$lifetime) { + $lifetime = new Lifetime(); + $this->em->persist($lifetime); + } + + $session = new Session(); + $this->em->persist($session); + $session->setIp($request->getClientIp() ?: ''); + $session->setReferrer($request->headers->get('Referer') ?: ''); + $session->setUserAgent($request->headers->get('User-Agent') ?: ''); + $session->setQueryString($request->getQueryString() ?: ''); + $session->setLoanTerm($request->query->get('y') ?: ''); + $session->setRepApr($request->query->has('r') ? hexdec($request->query->get('r')) / 100 : ''); + + foreach (self::UTM_CODES as $code) { + $method = 'set'.Inflector::classify($code); + $session->$method($request->query->get($code) ?: ''); + } + + $lifetime->addSession($session); + + $this->em->flush(); + + $this->sessionStore->setSession($session); + } + + private function shouldActOnRequest(Request $request, ?Response $response = null): bool + { + $route = $request->attributes->get('_route'); + + if ($response instanceof RedirectResponse || (!\is_string($route) || 0 === \strpos($route, '_'))) { + //these are requests for assets/symfony toolbar etc. Not relevant for our tracking + return false; + } + + return true; + } + + private function isBlacklistedFirewall(Request $request): bool + { + $firewallConfig = $this->firewallMap->getFirewallConfig($request); + + return $firewallConfig !== null && \in_array($firewallConfig->getName(), $this->firewallBlacklist, true); + } + + private function sessionMatchesRequestParameters(Request $request): bool + { + $session = $this->sessionStore->getSession(); + + if (!$session instanceof Session) { + return false; + } + + foreach (self::UTM_CODES as $code) { + $method = 'get'.Inflector::classify($code); + + if ($request->query->get($code, '') !== $session->$method()) { + return false; + } + } + + return true; + } +} diff --git a/Features/Context/DeviceContext.php b/src/Features/Context/DeviceContext.php similarity index 77% rename from Features/Context/DeviceContext.php rename to src/Features/Context/DeviceContext.php index e36237d..a912af9 100644 --- a/Features/Context/DeviceContext.php +++ b/src/Features/Context/DeviceContext.php @@ -16,11 +16,11 @@ class DeviceContext extends RawMinkContext implements Context, SnippetAcceptingC private $entityManager; private $utmCodes = [ - "utm_source", - "utm_medium", - "utm_campaign", - "utm_term", - "utm_content" + 'utm_source', + 'utm_medium', + 'utm_campaign', + 'utm_term', + 'utm_content' ]; public function __construct(EntityManagerInterface $entityManager) @@ -35,14 +35,14 @@ public function theCookieHasTheValue() { $session = new Session(); $session->setIp('127.0.0.1'); - $session->setReferrer(""); - $session->setUserAgent(""); - $session->setQueryString(""); - $session->setLoanTerm(""); - $session->setRepApr(""); + $session->setReferrer(''); + $session->setUserAgent(''); + $session->setQueryString(''); + $session->setLoanTerm(''); + $session->setRepApr(''); foreach ($this->utmCodes as $code) { - $method = "set" . Inflector::classify($code); - $session->$method(""); + $method = 'set'. Inflector::classify($code); + $session->$method(''); } $lifetime = new Lifetime(); diff --git a/Features/Context/FeatureContext.php b/src/Features/Context/FeatureContext.php similarity index 91% rename from Features/Context/FeatureContext.php rename to src/Features/Context/FeatureContext.php index 30a86ff..67c2844 100644 --- a/Features/Context/FeatureContext.php +++ b/src/Features/Context/FeatureContext.php @@ -16,6 +16,9 @@ class FeatureContext extends MinkContext implements Context, SnippetAcceptingCon /** * @Given /^the cookie "([^"]*)" has the value "([^"]*)"$/ + * + * @param string $name + * @param string $value */ public function theCookieHasTheValue($name, $value) { diff --git a/Features/device.feature b/src/Features/device.feature similarity index 100% rename from Features/device.feature rename to src/Features/device.feature diff --git a/src/Manager/DeviceFingerprintManager.php b/src/Manager/DeviceFingerprintManager.php new file mode 100644 index 0000000..1860e20 --- /dev/null +++ b/src/Manager/DeviceFingerprintManager.php @@ -0,0 +1,35 @@ +getFingerprint(), true); + + if (!empty($data) && JSON_ERROR_NONE === json_last_error()) { + foreach (self::HASHES as $field) { + $method = 'set'.ucfirst($field); + $value = array_key_exists($field, $data) ? md5(serialize($data[$field])) : null; + $device->$method($value); + } + } + + return $device; + } +} diff --git a/Resources/config/routing.yml b/src/Resources/config/routing.yml similarity index 100% rename from Resources/config/routing.yml rename to src/Resources/config/routing.yml diff --git a/Resources/config/services.yml b/src/Resources/config/services.yml similarity index 50% rename from Resources/config/services.yml rename to src/Resources/config/services.yml index 882e455..bab332f 100644 --- a/Resources/config/services.yml +++ b/src/Resources/config/services.yml @@ -1,12 +1,18 @@ parameters: -# alpha_visitor_tracking.example.class: Alpha\VisitorTrackingBundle\Example services: alpha.visitor_tracking_subscriber: class: Alpha\VisitorTrackingBundle\EventListener\VisitorTrackingSubscriber - arguments: ["@doctrine.orm.entity_manager"] + arguments: + - "@doctrine.orm.entity_manager" + - "@alpha.visitor_tracking.storage.session" + - [] # Firewall blacklist, populated from config. + - "@security.firewall.map" tags: - { name: kernel.event_subscriber } alpha.visitor_tracking.manager.device_fingerprint: class: Alpha\VisitorTrackingBundle\Manager\DeviceFingerprintManager + + alpha.visitor_tracking.storage.session: + class: Alpha\VisitorTrackingBundle\Storage\SessionStore diff --git a/Resources/public/device.js b/src/Resources/public/device.js similarity index 100% rename from Resources/public/device.js rename to src/Resources/public/device.js diff --git a/src/Storage/SessionStore.php b/src/Storage/SessionStore.php new file mode 100644 index 0000000..a87ad0b --- /dev/null +++ b/src/Storage/SessionStore.php @@ -0,0 +1,42 @@ +session = null; + } + + public function setSession(Session $session): void + { + $this->session = $session; + } + + public function getSession(): ?Session + { + return $this->session; + } + + public function getLifetime(): ?Lifetime + { + $session = $this->getSession(); + + if ($session === null) { + return null; + } + + return $session->getLifetime(); + } +} diff --git a/Tests/Manager/DeviceFingerprintManagerTest.php b/tests/Manager/DeviceFingerprintManagerTest.php similarity index 97% rename from Tests/Manager/DeviceFingerprintManagerTest.php rename to tests/Manager/DeviceFingerprintManagerTest.php index e437116..2237420 100644 --- a/Tests/Manager/DeviceFingerprintManagerTest.php +++ b/tests/Manager/DeviceFingerprintManagerTest.php @@ -71,7 +71,7 @@ public function hashesAreGeneratedForExistingKeys() $manager = new DeviceFingerprintManager(); $manager->generateHashes($device); - $this->assertSame(md5(serialize("valid")), $device->getCanvas()); + $this->assertSame(md5(serialize('valid')), $device->getCanvas()); $this->assertSame(md5(serialize([3, 4])), $device->getScreen()); $this->assertNull($device->getFonts()); $this->assertNull($device->getNavigator()); From 956174695a36aaf5edbacbdbc1bd3065f93a99fe Mon Sep 17 00:00:00 2001 From: Alex Longshaw Date: Tue, 23 Jan 2018 10:57:06 +0000 Subject: [PATCH 2/5] Add hasSeed to the Lifetime entity (#1) * Add hasSeed to the Lifetime entity * Update Lifetime hadSeed to PHP7 --- src/Entity/Lifetime.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Entity/Lifetime.php b/src/Entity/Lifetime.php index 0564ef2..2f62798 100644 --- a/src/Entity/Lifetime.php +++ b/src/Entity/Lifetime.php @@ -183,4 +183,15 @@ public function getSeed(string $name, int $numberOfValues, array $weights = null return $seed->getValue(); } + + public function hasSeed(string $name): bool + { + foreach ($this->seeds as $seed) { + if ($seed->getName() === $name) { + return true; + } + } + + return false; + } } From 152f22936f48e7c493aadc1acdcb17d2b71d1243 Mon Sep 17 00:00:00 2001 From: Ben Challis Date: Tue, 3 Apr 2018 17:31:23 +0100 Subject: [PATCH 3/5] Made Session::pageViews extra lazy --- src/Entity/Session.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Entity/Session.php b/src/Entity/Session.php index 96f7aaf..4e64906 100644 --- a/src/Entity/Session.php +++ b/src/Entity/Session.php @@ -34,7 +34,7 @@ class Session /** * @var Collection|PageView[] * - * @ORM\OneToMany(targetEntity="PageView", mappedBy="session", cascade={"persist"}) + * @ORM\OneToMany(targetEntity="PageView", mappedBy="session", cascade={"persist"}, fetch="EXTRA_LAZY") */ protected $pageViews; From 36c75253de2e4d140f05bbf1cf92db700d4fe234 Mon Sep 17 00:00:00 2001 From: Martin Georgiev Date: Thu, 12 Apr 2018 14:39:27 +0100 Subject: [PATCH 4/5] Fix bug when checking for existing session on empty lifetime --- src/Entity/Session.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Entity/Session.php b/src/Entity/Session.php index 4e64906..5b5bb7f 100644 --- a/src/Entity/Session.php +++ b/src/Entity/Session.php @@ -409,7 +409,7 @@ public function setLifetime(?Lifetime $lifetime = null) { $this->lifetime = $lifetime; - if (!$this->lifetime->getSessions()->contains($this)) { + if ($lifetime instanceof Lifetime && !$this->lifetime->getSessions()->contains($this)) { $this->lifetime->addSession($this); } From 254d1ad8eae3b76b1b80599d405606b0926997fe Mon Sep 17 00:00:00 2001 From: Ben Challis Date: Thu, 12 Apr 2018 15:19:30 +0100 Subject: [PATCH 5/5] Removed 7.0 from travis as not supported anymore --- .travis.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 76b0bc7..5a6c2da 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,12 +8,6 @@ cache: matrix: include: - - php: 7.0 - env: SYMFONY_VERSION=2.7.* - - php: 7.0 - env: SYMFONY_VERSION=3.3.* - - php: 7.0 - env: DEPENDENCIES=beta - php: 7.1 env: SYMFONY_VERSION=2.7.* - php: 7.1