Completed
Push — staging ( c0ad47...753b21 )
by Woeler
44s queued 35s
created

ContactTracker   C

Complexity

Total Complexity 54

Size/Duplication

Total Lines 385
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 122
c 1
b 0
f 0
dl 0
loc 385
rs 6.4799
wmc 54

15 Methods

Rating   Name   Duplication   Size   Complexity  
A getContact() 0 24 6
A __construct() 0 22 1
B setTrackedContact() 0 40 7
A getSystemContact() 0 13 4
A setSystemContact() 0 12 3
A getTrackingId() 0 9 2
A getCurrentContact() 0 7 2
A getContactByTrackedDevice() 0 28 6
A isUserSession() 0 3 1
A useSystemContact() 0 3 4
A dispatchContactChangeEvent() 0 11 3
A createNewContact() 0 24 4
A generateTrackingCookies() 0 4 3
A getContactByIpAddress() 0 21 6
A hydrateCustomFieldData() 0 9 2

How to fix   Complexity   

Complex Class

Complex classes like ContactTracker often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ContactTracker, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/*
4
 * @copyright   2017 Mautic Contributors. All rights reserved
5
 * @author      Mautic, Inc.
6
 *
7
 * @link        https://mautic.org
8
 *
9
 * @license     GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html
10
 */
11
12
namespace Mautic\LeadBundle\Tracker;
13
14
use Mautic\CoreBundle\Entity\IpAddress;
15
use Mautic\CoreBundle\Helper\CoreParametersHelper;
16
use Mautic\CoreBundle\Helper\IpLookupHelper;
17
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
18
use Mautic\LeadBundle\Entity\Lead;
19
use Mautic\LeadBundle\Entity\LeadRepository;
20
use Mautic\LeadBundle\Event\LeadChangeEvent;
21
use Mautic\LeadBundle\Event\LeadEvent;
22
use Mautic\LeadBundle\LeadEvents;
23
use Mautic\LeadBundle\Model\DefaultValueTrait;
24
use Mautic\LeadBundle\Model\FieldModel;
25
use Mautic\LeadBundle\Tracker\Service\ContactTrackingService\ContactTrackingServiceInterface;
26
use Monolog\Logger;
27
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
28
use Symfony\Component\HttpFoundation\Request;
29
use Symfony\Component\HttpFoundation\RequestStack;
30
31
class ContactTracker
32
{
33
    use DefaultValueTrait;
34
    /**
35
     * @var LeadRepository
36
     */
37
    private $leadRepository;
38
39
    /**
40
     * @var ContactTrackingServiceInterface
41
     */
42
    private $contactTrackingService;
43
44
    /**
45
     * @var DeviceTracker
46
     */
47
    private $deviceTracker;
48
49
    /**
50
     * @var CorePermissions
51
     */
52
    private $security;
53
54
    /**
55
     * @var null|Lead
56
     */
57
    private $systemContact;
58
59
    /**
60
     * @var null|Lead
61
     */
62
    private $trackedContact;
63
64
    /**
65
     * @var Logger
66
     */
67
    private $logger;
68
69
    /**
70
     * @var IpLookupHelper
71
     */
72
    private $ipLookupHelper;
73
74
    /**
75
     * @var Request
76
     */
77
    private $request;
78
79
    /**
80
     * @var CoreParametersHelper
81
     */
82
    private $coreParametersHelper;
83
84
    /**
85
     * @var EventDispatcherInterface
86
     */
87
    private $dispatcher;
88
89
    /**
90
     * @var FieldModel
91
     */
92
    private $leadFieldModel;
93
94
    /**
95
     * ContactTracker constructor.
96
     *
97
     * @param LeadRepository                  $leadRepository
98
     * @param ContactTrackingServiceInterface $contactTrackingService
99
     * @param DeviceTracker                   $deviceTracker
100
     * @param CorePermissions                 $security
101
     * @param Logger                          $logger
102
     * @param IpLookupHelper                  $ipLookupHelper
103
     * @param RequestStack                    $requestStack
104
     * @param CoreParametersHelper            $coreParametersHelper
105
     * @param EventDispatcherInterface        $dispatcher
106
     * @param FieldModel                      $leadFieldModel
107
     */
108
    public function __construct(
109
        LeadRepository $leadRepository,
110
        ContactTrackingServiceInterface $contactTrackingService,
111
        DeviceTracker $deviceTracker,
112
        CorePermissions $security,
113
        Logger $logger,
114
        IpLookupHelper $ipLookupHelper,
115
        RequestStack $requestStack,
116
        CoreParametersHelper $coreParametersHelper,
117
        EventDispatcherInterface $dispatcher,
118
        FieldModel $leadFieldModel
119
    ) {
120
        $this->leadRepository         = $leadRepository;
121
        $this->contactTrackingService = $contactTrackingService;
122
        $this->deviceTracker          = $deviceTracker;
123
        $this->security               = $security;
124
        $this->logger                 = $logger;
125
        $this->ipLookupHelper         = $ipLookupHelper;
126
        $this->request                = $requestStack->getCurrentRequest();
127
        $this->coreParametersHelper   = $coreParametersHelper;
128
        $this->dispatcher             = $dispatcher;
129
        $this->leadFieldModel         = $leadFieldModel;
130
    }
131
132
    /**
133
     * @return Lead|null
134
     */
135
    public function getContact()
136
    {
137
        if ($systemContact = $this->getSystemContact()) {
138
            return $systemContact;
139
        } elseif ($this->isUserSession()) {
140
            return null;
141
        }
142
143
        if (empty($this->trackedContact)) {
144
            $this->trackedContact = $this->getCurrentContact();
145
            $this->generateTrackingCookies();
146
        }
147
148
        if ($this->request) {
149
            $this->logger->addDebug('CONTACT: Tracking session for contact ID# '.$this->trackedContact->getId().' through '.$this->request->getMethod().' '.$this->request->getRequestUri());
150
        }
151
152
        // Log last active for the tracked contact
153
        if (!defined('MAUTIC_LEAD_LASTACTIVE_LOGGED')) {
154
            $this->leadRepository->updateLastActive($this->trackedContact->getId());
155
            define('MAUTIC_LEAD_LASTACTIVE_LOGGED', 1);
156
        }
157
158
        return $this->trackedContact;
159
    }
160
161
    /**
162
     * Set the contact and generate cookies for future tracking.
163
     *
164
     * @param Lead $lead
165
     */
166
    public function setTrackedContact(Lead $trackedContact)
167
    {
168
        $this->logger->addDebug("CONTACT: {$trackedContact->getId()} set as current lead.");
169
170
        if ($this->useSystemContact()) {
171
            // Overwrite system current lead
172
            $this->setSystemContact($trackedContact);
173
174
            return;
175
        }
176
177
        // Take note of previously tracked in order to dispatched change event
178
        $previouslyTrackedContact = (is_null($this->trackedContact)) ? null : $this->trackedContact;
179
        $previouslyTrackedId      = $this->getTrackingId();
180
181
        // Set the newly tracked contact
182
        $this->trackedContact = $trackedContact;
183
184
        // Hydrate custom field data
185
        $fields = $trackedContact->getFields();
186
        if (empty($fields)) {
187
            $this->hydrateCustomFieldData($trackedContact);
188
        }
189
190
        // Set last active
191
        $this->trackedContact->setLastActive(new \DateTime());
192
193
        // If for whatever reason this contact has not been saved yet, don't generate tracking cookies
194
        if (!$trackedContact->getId()) {
195
            // Delete existing cookies to prevent tracking as someone else
196
            $this->deviceTracker->clearTrackingCookies();
197
198
            return;
199
        }
200
201
        // Generate cookies for the newly tracked contact
202
        $this->generateTrackingCookies();
203
204
        if ($previouslyTrackedContact && $previouslyTrackedContact->getId() != $this->trackedContact->getId()) {
205
            $this->dispatchContactChangeEvent($previouslyTrackedContact, $previouslyTrackedId);
206
        }
207
    }
208
209
    /**
210
     * System contact bypasses cookie tracking.
211
     *
212
     * @param Lead|null $lead
213
     */
214
    public function setSystemContact(Lead $lead = null)
215
    {
216
        if (null !== $lead) {
217
            $this->logger->addDebug("LEAD: {$lead->getId()} set as system lead.");
218
219
            $fields = $lead->getFields();
220
            if (empty($fields)) {
221
                $this->hydrateCustomFieldData($lead);
222
            }
223
        }
224
225
        $this->systemContact = $lead;
226
    }
227
228
    /**
229
     * @return null|string
230
     */
231
    public function getTrackingId()
232
    {
233
        // Use the new method first
234
        if ($trackedDevice = $this->deviceTracker->getTrackedDevice()) {
235
            return $trackedDevice->getTrackingId();
236
        }
237
238
        // That failed, so look for the old cookies
239
        return $this->contactTrackingService->getTrackedIdentifier();
240
    }
241
242
    /**
243
     * @return Lead|null
244
     */
245
    private function getSystemContact()
246
    {
247
        if ($this->useSystemContact() && $this->systemContact) {
248
            $this->logger->addDebug('CONTACT: System lead is being used');
249
250
            return $this->systemContact;
251
        }
252
253
        if ($this->isUserSession()) {
254
            $this->logger->addDebug('CONTACT: In a Mautic user session');
255
        }
256
257
        return null;
258
    }
259
260
    /**
261
     * @return Lead|null
262
     */
263
    private function getCurrentContact()
264
    {
265
        if ($lead = $this->getContactByTrackedDevice()) {
266
            return $lead;
267
        }
268
269
        return $this->getContactByIpAddress();
270
    }
271
272
    /**
273
     * @return Lead|null
274
     */
275
    private function getContactByTrackedDevice()
276
    {
277
        $lead = null;
278
279
        // Return null for leads that are from a non-trackable IP, prevent anonymous lead with a non-trackable IP to be tracked
280
        $ip = $this->ipLookupHelper->getIpAddress();
281
        if ($ip && !$ip->isTrackable()) {
282
            return $lead;
283
        }
284
285
        // Is there a device being tracked?
286
        if ($trackedDevice = $this->deviceTracker->getTrackedDevice()) {
287
            $lead = $trackedDevice->getLead();
288
289
            // Lead associations are not hydrated with custom field values by default
290
            $this->hydrateCustomFieldData($lead);
291
        }
292
293
        if (null === $lead) {
294
            // Check to see if a contact is being tracked via the old cookie method in order to migrate them to the new
295
            $lead = $this->contactTrackingService->getTrackedLead();
296
        }
297
298
        if ($lead) {
299
            $this->logger->addDebug("CONTACT: Existing lead found with ID# {$lead->getId()}.");
300
        }
301
302
        return $lead;
303
    }
304
305
    /**
306
     * @return Lead
307
     */
308
    private function getContactByIpAddress()
309
    {
310
        $ip = $this->ipLookupHelper->getIpAddress();
311
        // if no trackingId cookie set the lead is not tracked yet so create a new one
312
        if ($ip && !$ip->isTrackable()) {
313
            // Don't save leads that are from a non-trackable IP by default
314
            return $this->createNewContact($ip, false);
315
        }
316
317
        if ($this->coreParametersHelper->getParameter('track_contact_by_ip') && $this->coreParametersHelper->getParameter('anonymize_ip')) {
318
            /** @var Lead[] $leads */
319
            $leads = $this->leadRepository->getLeadsByIp($ip->getIpAddress());
320
            if (count($leads)) {
321
                $lead = $leads[0];
322
                $this->logger->addDebug("CONTACT: Existing lead found with ID# {$lead->getId()}.");
323
324
                return $lead;
325
            }
326
        }
327
328
        return $this->createNewContact($ip);
329
    }
330
331
    /**
332
     * @param IpAddress|null $ip
333
     * @param bool           $persist
334
     *
335
     * @return Lead
336
     */
337
    private function createNewContact(IpAddress $ip = null, $persist = true)
338
    {
339
        //let's create a lead
340
        $lead = new Lead();
341
        $lead->setNewlyCreated(true);
342
343
        if ($ip) {
344
            $lead->addIpAddress($ip);
345
        }
346
347
        if ($persist && !defined('MAUTIC_NON_TRACKABLE_REQUEST')) {
348
            // Dispatch events for new lead to write create log, ip address change, etc
349
            $event = new LeadEvent($lead, true);
350
            $this->dispatcher->dispatch(LeadEvents::LEAD_PRE_SAVE, $event);
351
            $this->setEntityDefaultValues($lead);
352
            $this->leadRepository->saveEntity($lead);
353
            $this->hydrateCustomFieldData($lead);
354
355
            $this->dispatcher->dispatch(LeadEvents::LEAD_POST_SAVE, $event);
356
357
            $this->logger->addDebug("CONTACT: New lead created with ID# {$lead->getId()}.");
358
        }
359
360
        return $lead;
361
    }
362
363
    /**
364
     * @param Lead $lead
365
     */
366
    private function hydrateCustomFieldData(Lead $lead = null)
367
    {
368
        if (null === $lead) {
369
            return;
370
        }
371
372
        // Hydrate fields with custom field data
373
        $fields = $this->leadRepository->getFieldValues($lead->getId());
374
        $lead->setFields($fields);
375
    }
376
377
    /**
378
     * @return bool
379
     */
380
    private function useSystemContact()
381
    {
382
        return $this->isUserSession() || $this->systemContact || defined('IN_MAUTIC_CONSOLE') || $this->request === null;
383
    }
384
385
    /**
386
     * @return bool
387
     */
388
    private function isUserSession()
389
    {
390
        return !$this->security->isAnonymous();
391
    }
392
393
    /**
394
     * @param Lead $previouslyTrackedContact
395
     * @param      $previouslyTrackedId
396
     */
397
    private function dispatchContactChangeEvent(Lead $previouslyTrackedContact, $previouslyTrackedId)
398
    {
399
        $newTrackingId = $this->getTrackingId();
400
        $this->logger->addDebug(
401
            "CONTACT: Tracking code changed from $previouslyTrackedId for contact ID# {$previouslyTrackedContact->getId()} to $newTrackingId for contact ID# {$this->trackedContact->getId()}"
0 ignored issues
show
Bug introduced by
The method getId() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

401
            "CONTACT: Tracking code changed from $previouslyTrackedId for contact ID# {$previouslyTrackedContact->getId()} to $newTrackingId for contact ID# {$this->trackedContact->/** @scrutinizer ignore-call */ getId()}"

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
402
        );
403
404
        if ($previouslyTrackedId !== null) {
405
            if ($this->dispatcher->hasListeners(LeadEvents::CURRENT_LEAD_CHANGED)) {
406
                $event = new LeadChangeEvent($previouslyTrackedContact, $previouslyTrackedId, $this->trackedContact, $newTrackingId);
0 ignored issues
show
Bug introduced by
It seems like $this->trackedContact can also be of type null; however, parameter $newLead of Mautic\LeadBundle\Event\...ngeEvent::__construct() does only seem to accept Mautic\LeadBundle\Entity\Lead, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

406
                $event = new LeadChangeEvent($previouslyTrackedContact, $previouslyTrackedId, /** @scrutinizer ignore-type */ $this->trackedContact, $newTrackingId);
Loading history...
407
                $this->dispatcher->dispatch(LeadEvents::CURRENT_LEAD_CHANGED, $event);
408
            }
409
        }
410
    }
411
412
    private function generateTrackingCookies()
413
    {
414
        if ($leadId = $this->trackedContact->getId() && $this->request !== null) {
0 ignored issues
show
Comprehensibility introduced by
Consider adding parentheses for clarity. Current Interpretation: $leadId = ($this->tracke...this->request !== null), Probably Intended Meaning: ($leadId = $this->tracke...$this->request !== null
Loading history...
Unused Code introduced by
The assignment to $leadId is dead and can be removed.
Loading history...
415
            $this->deviceTracker->createDeviceFromUserAgent($this->trackedContact, $this->request->server->get('HTTP_USER_AGENT'));
0 ignored issues
show
Bug introduced by
It seems like $this->trackedContact can also be of type null; however, parameter $trackedContact of Mautic\LeadBundle\Tracke...teDeviceFromUserAgent() does only seem to accept Mautic\LeadBundle\Entity\Lead, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

415
            $this->deviceTracker->createDeviceFromUserAgent(/** @scrutinizer ignore-type */ $this->trackedContact, $this->request->server->get('HTTP_USER_AGENT'));
Loading history...
416
        }
417
    }
418
}
419