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()}" |
|
|
|
|
402
|
|
|
); |
403
|
|
|
|
404
|
|
|
if ($previouslyTrackedId !== null) { |
405
|
|
|
if ($this->dispatcher->hasListeners(LeadEvents::CURRENT_LEAD_CHANGED)) { |
406
|
|
|
$event = new LeadChangeEvent($previouslyTrackedContact, $previouslyTrackedId, $this->trackedContact, $newTrackingId); |
|
|
|
|
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) { |
|
|
|
|
415
|
|
|
$this->deviceTracker->createDeviceFromUserAgent($this->trackedContact, $this->request->server->get('HTTP_USER_AGENT')); |
|
|
|
|
416
|
|
|
} |
417
|
|
|
} |
418
|
|
|
} |
419
|
|
|
|
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.