Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
236 / 236
100.00% covered (success)
100.00%
19 / 19
CRAP
100.00% covered (success)
100.00%
1 / 1
VisitorsController
100.00% covered (success)
100.00%
236 / 236
100.00% covered (success)
100.00%
19 / 19
59
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
1
 create
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
1
 track
100.00% covered (success)
100.00%
46 / 46
100.00% covered (success)
100.00%
1 / 1
4
 getResponse
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
3
 getImageContent
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDefaultFields
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 doUrl
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 doVisitorId
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 doReferrer
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 doDeviceDetect
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 doCustom
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
6
 doEvent
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
11
 doCounter
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 doConfig
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 doPerformance
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
1
 doLocalTime
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 doLanguage
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 doLocation
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
5
 doRefererFields
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
8
1<?php
2
3namespace Drupal\visitors\Controller;
4
5use Drupal\Component\Datetime\TimeInterface;
6use Drupal\Core\Config\ConfigFactoryInterface;
7use Drupal\Core\Controller\ControllerBase;
8use Drupal\maxmind_geoip\MaxMindGeoIpInterface;
9use Drupal\visitors\Helper\VisitorsUrl;
10use Drupal\visitors\VisitorsAiAssistantsInterface;
11use Drupal\visitors\VisitorsCampaignInterface;
12use Drupal\visitors\VisitorsCookieInterface;
13use Drupal\visitors\VisitorsCounterInterface;
14use Drupal\visitors\VisitorsDeviceInterface;
15use Drupal\visitors\VisitorsLocationInterface;
16use Drupal\visitors\VisitorsSearchEngineInterface;
17use Drupal\visitors\VisitorsSocialNetworksInterface;
18use Drupal\visitors\VisitorsSpamInterface;
19use Drupal\visitors\VisitorsTrackerInterface;
20use Psr\Log\LoggerInterface;
21use Symfony\Component\DependencyInjection\ContainerInterface;
22use Symfony\Component\HttpFoundation\Request;
23use Symfony\Component\HttpFoundation\Response;
24use Symfony\Component\HttpFoundation\ServerBag;
25
26/**
27 * Visitors tracking controller.
28 */
29final class VisitorsController extends ControllerBase {
30
31  /**
32   * The time service.
33   *
34   * @var \Drupal\Component\Datetime\TimeInterface
35   */
36  protected $time;
37
38  /**
39   * The visitors settings.
40   *
41   * @var \Drupal\Core\Config\Config
42   */
43  protected $settings;
44
45  /**
46   * The logger service.
47   *
48   * @var \Psr\Log\LoggerInterface
49   */
50  protected $logger;
51
52  /**
53   * The counter service.
54   *
55   * @var \Drupal\visitors\VisitorsCounterInterface
56   */
57  protected $counter;
58
59  /**
60   * The cookie service.
61   *
62   * @var \Drupal\visitors\VisitorsCookieInterface
63   */
64  protected $cookie;
65
66  /**
67   * The device service.
68   *
69   * @var \Drupal\visitors\VisitorsDeviceInterface
70   */
71  protected $device;
72
73  /**
74   * The location service.
75   *
76   * @var \Drupal\visitors\VisitorsLocationInterface
77   */
78  protected $location;
79
80  /**
81   * The tracker service.
82   *
83   * @var \Drupal\visitors\VisitorsTrackerInterface
84   */
85  protected $tracker;
86
87  /**
88   * The search engine service.
89   *
90   * @var \Drupal\visitors\VisitorsSearchEngineInterface
91   */
92  protected $searchEngine;
93
94  /**
95   * The spam service.
96   *
97   * @var \Drupal\visitors\VisitorsSpamInterface
98   */
99  protected $spamService;
100
101  /**
102   * The social networks service.
103   *
104   * @var \Drupal\visitors\VisitorsSocialNetworksInterface
105   */
106  protected $socialNetworksService;
107
108  /**
109   * The AI assistants service.
110   *
111   * @var \Drupal\visitors\VisitorsAiAssistantsInterface
112   */
113  protected $aiAssistantsService;
114
115  /**
116   * The campaign service.
117   *
118   * @var \Drupal\visitors\VisitorsCampaignInterface
119   */
120  protected $campaignService;
121
122  /**
123   * The geoip service.
124   *
125   * @var \Drupal\maxmind_geoip\MaxMindGeoIpInterface|null
126   */
127  protected $geoip;
128
129  /**
130   * Visitor tracker.
131   *
132   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
133   *   The config factory service.
134   * @param \Drupal\Component\Datetime\TimeInterface $time
135   *   The time service.
136   * @param \Psr\Log\LoggerInterface $logger
137   *   The logger service.
138   * @param \Drupal\visitors\VisitorsCounterInterface $counter
139   *   The counter service.
140   * @param \Drupal\visitors\VisitorsCookieInterface $cookie
141   *   The cookie service.
142   * @param \Drupal\visitors\VisitorsDeviceInterface $device
143   *   The device service.
144   * @param \Drupal\visitors\VisitorsLocationInterface $location
145   *   The location service.
146   * @param \Drupal\visitors\VisitorsTrackerInterface $tracker
147   *   The date service.
148   * @param \Drupal\visitors\VisitorsSearchEngineInterface $search_engine
149   *   The search engine service.
150   * @param \Drupal\visitors\VisitorsSpamInterface $spam_service
151   *   The spam service.
152   * @param \Drupal\visitors\VisitorsSocialNetworksInterface $social_networks_service
153   *   The social networks service.
154   * @param \Drupal\visitors\VisitorsAiAssistantsInterface $ai_assistants_service
155   *   The AI assistants service.
156   * @param \Drupal\visitors\VisitorsCampaignInterface $campaign_service
157   *   The campaign service.
158   * @param \Drupal\maxmind_geoip\MaxMindGeoIpInterface|null $geoip
159   *   The geoip service.
160   */
161  public function __construct(
162    ConfigFactoryInterface $config_factory,
163    TimeInterface $time,
164    LoggerInterface $logger,
165    VisitorsCounterInterface $counter,
166    VisitorsCookieInterface $cookie,
167    VisitorsDeviceInterface $device,
168    VisitorsLocationInterface $location,
169    VisitorsTrackerInterface $tracker,
170    VisitorsSearchEngineInterface $search_engine,
171    VisitorsSpamInterface $spam_service,
172    VisitorsSocialNetworksInterface $social_networks_service,
173    VisitorsAiAssistantsInterface $ai_assistants_service,
174    VisitorsCampaignInterface $campaign_service,
175    ?MaxMindGeoIpInterface $geoip = NULL,
176  ) {
177
178    $this->settings = $config_factory->get('visitors.settings');
179
180    $this->time = $time;
181    $this->logger = $logger;
182    $this->counter = $counter;
183    $this->cookie = $cookie;
184    $this->device = $device;
185    $this->location = $location;
186    $this->tracker = $tracker;
187    $this->searchEngine = $search_engine;
188    $this->spamService = $spam_service;
189    $this->socialNetworksService = $social_networks_service;
190    $this->aiAssistantsService = $ai_assistants_service;
191    $this->campaignService = $campaign_service;
192    $this->geoip = $geoip;
193  }
194
195  /**
196   * {@inheritdoc}
197   */
198  public static function create(ContainerInterface $container): VisitorsController {
199    return new self(
200      $container->get('config.factory'),
201      $container->get('datetime.time'),
202      $container->get('logger.channel.visitors'),
203      $container->get('visitors.counter'),
204      $container->get('visitors.cookie'),
205      $container->get('visitors.device'),
206      $container->get('visitors.location'),
207      $container->get('visitors.tracker'),
208      $container->get('visitors.search_engine'),
209      $container->get('visitors.spam'),
210      $container->get('visitors.social_networks'),
211      $container->get('visitors.ai_assistants'),
212      $container->get('visitors.campaign'),
213      $container->get('maxmind_geoip.lookup', ContainerInterface::NULL_ON_INVALID_REFERENCE),
214    );
215
216  }
217
218  /**
219   * Tracks visits.
220   */
221  public function track(Request $request): Response {
222
223    $server = $request->server;
224    $query = $request->query->all();
225
226    $response = $this->getResponse($query['send_image'] ?? FALSE);
227
228    $request_time = $this->time->getRequestTime();
229
230    $visit = $this->getDefaultFields();
231    $log = [];
232
233    $ip = $request->getClientIp();
234    $visit['location_ip'] = $ip;
235    $log['uid'] = $query['uid'] ?? 0;
236    $visit['uid'] = $log['uid'] == 0 ? NULL : $log['uid'];
237    $visit['entry_time'] = $request_time;
238    $visit['exit_time'] = $request_time;
239    $visit['total_time'] = 0;
240    $visit['total_page_views'] = 0;
241    $visit['total_events'] = 0;
242    $visit['returning'] = 0;
243    $visit['total_visits'] = 1;
244
245    $log['title'] = $query['action_name'] ?? '';
246    $log['page_view'] = $query['pv_id'] ?? NULL;
247    $log['created'] = $request_time;
248
249    $bot_retention_log = $this->settings->get('bot_retention_log');
250    $discard_bot = ($bot_retention_log == -1);
251    $this->doDeviceDetect($visit, $server);
252    if ($discard_bot && $visit['bot']) {
253      return $response;
254    }
255
256    $this->doVisitorId($visit, $query);
257    $this->doUrl($log, $query);
258    $this->doReferrer($log, $query);
259
260    $custom_page_var = $query['cvar'] ?? NULL;
261    $this->doCustom($log, $custom_page_var);
262
263    $custom_event_var = $query['e_cvar'] ?? NULL;
264    $this->doEvent($log, $custom_event_var);
265
266    $this->doCounter($custom_page_var);
267
268    $this->doConfig($visit, $query);
269    $this->doPerformance($log, $query);
270
271    $this->doLocalTime($visit, $query);
272
273    $languages = $request->getLanguages() ?? [];
274    $this->doLanguage($visit, $languages);
275    $this->doLocation($visit, $ip, $languages);
276
277    $this->doRefererFields($visit, $query);
278
279    $visit_id = $this->tracker->getVisitId($visit, $request_time);
280    $log['visit_id'] = $visit_id;
281
282    // Write fields to database.
283    $event_id = $this->tracker->writeEvent($log);
284
285    $uid = $visit['uid'] ?? NULL;
286    $this->tracker->updateVisit($visit_id, $event_id, $request_time, $uid);
287
288    return $response;
289  }
290
291  /**
292   * Get the response.
293   *
294   * @param bool $send_image
295   *   Whether to send the image.
296   *
297   * @return \Symfony\Component\HttpFoundation\Response
298   *   The response.
299   */
300  protected function getResponse(bool $send_image): Response {
301    $headers = [
302      'Cache-Control' => 'no-cache, no-store, must-revalidate',
303      'Pragma' => 'no-cache',
304      'Expires' => '0',
305    ];
306    $content = '';
307    if ($send_image) {
308      $content = $this->getImageContent();
309      $headers['Content-Type'] = 'image/gif';
310      $headers['Content-Length'] = strlen($content);
311    }
312
313    $response = new Response(
314      $content,
315      ($send_image) ? Response::HTTP_OK : Response::HTTP_NO_CONTENT,
316      $headers,
317    );
318
319    return $response;
320  }
321
322  /**
323   * Get the image content.
324   *
325   * @return string
326   *   The image content.
327   */
328  protected function getImageContent(): string {
329    return hex2bin('47494638396101000100800000000000FFFFFF21F9040100000000002C00000000010001000002024401003B');
330  }
331
332  /**
333   * Get the default fields.
334   *
335   * @return array
336   *   The default fields.
337   */
338  protected function getDefaultFields(): array {
339    $fields = [
340      'bot' => 0,
341    ];
342
343    return $fields;
344  }
345
346  /**
347   * Detects the visitor url.
348   *
349   * @param string[] $fields
350   *   The fields array.
351   * @param string[] $query
352   *   The query array.
353   */
354  protected function doUrl(array &$fields, array $query) {
355    $url = $query['url'] ?? '';
356
357    $visitors_url = new VisitorsUrl($url);
358
359    $fields['url'] = $visitors_url->getUrl();
360    $fields['url_prefix'] = $visitors_url->getPrefix();
361  }
362
363  /**
364   * Detects the visitor id.
365   *
366   * @param string[] $fields
367   *   The fields array.
368   * @param string[] $query
369   *   The query array.
370   */
371  protected function doVisitorId(array &$fields, array $query) {
372    $visitor_id = $query['_id'] ?? $this->cookie->getId();
373    $fields['visitor_id'] = $visitor_id;
374  }
375
376  /**
377   * Detects the referrer.
378   *
379   * @param string[] $fields
380   *   The fields array.
381   * @param string[] $query
382   *   The query array.
383   */
384  protected function doReferrer(array &$fields, array $query) {
385    $referrer = $query['urlref'] ?? '';
386    $fields['referrer_url'] = $referrer;
387  }
388
389  /**
390   * Detects the device.
391   *
392   * @param string[] $fields
393   *   The fields array.
394   * @param \Symfony\Component\HttpFoundation\ServerBag $server
395   *   The server array.
396   */
397  protected function doDeviceDetect(array &$fields, ServerBag $server) {
398    if (!$this->device->hasLibrary()) {
399      return NULL;
400    }
401
402    $user_agent = $server->get('HTTP_USER_AGENT', '');
403    $this->device->doDeviceFields($fields, $user_agent, $server->all());
404
405  }
406
407  /**
408   * Set the fields with data in the custom variable.
409   */
410  protected function doCustom(array &$fields, $cvar = NULL) {
411    if (!is_null($cvar)) {
412      $custom = json_decode($cvar);
413      foreach ($custom as $c) {
414        switch ($c[0]) {
415          case 'path':
416            $fields['path'] = $c[1];
417            break;
418
419          case 'route':
420            $fields['route'] = $c[1];
421            break;
422
423          case 'server':
424            $fields['server'] = $c[1];
425            break;
426
427        }
428      }
429    }
430  }
431
432  /**
433   * Set the fields with data in the custom variable.
434   */
435  protected function doEvent(array &$fields, $cvar = NULL) {
436    if (!is_null($cvar)) {
437      $custom = json_decode($cvar);
438      foreach ($custom as $c) {
439        switch ($c[0]) {
440          case 'plugin':
441            $fields['plugin'] = $c[1];
442            break;
443
444          case 'event':
445            $fields['event'] = $c[1];
446            break;
447
448          case 'plugin_int_1':
449            $fields['plugin_int_1'] = $c[1];
450            break;
451
452          case 'plugin_int_2':
453            $fields['plugin_int_2'] = $c[1];
454            break;
455
456          case 'plugin_var_1':
457            $fields['plugin_var_1'] = $c[1];
458            break;
459
460          case 'plugin_var_2':
461            $fields['plugin_var_2'] = $c[1];
462            break;
463
464          case 'plugin_var_3':
465            $fields['plugin_var_3'] = $c[1];
466            break;
467
468          case 'plugin_var_4':
469            $fields['plugin_var_4'] = $c[1];
470            break;
471        }
472      }
473    }
474  }
475
476  /**
477   * Record the view of the entity.
478   */
479  protected function doCounter($cvar = NULL) {
480
481    $viewed = NULL;
482    if (!is_null($cvar)) {
483      $custom = json_decode($cvar);
484      foreach ($custom as $c) {
485        if ($c[0] == 'viewed') {
486          $viewed = $c[1];
487        }
488      }
489    }
490
491    if (!is_null($viewed)) {
492      [$type, $id] = explode(':', $viewed);
493      $this->counter->recordView($type, $id);
494    }
495  }
496
497  /**
498   * Set the configuration fields.
499   */
500  protected function doConfig(array &$fields, array $query) {
501
502    $fields['config_resolution']   = $query['res'] ?? NULL;
503    $fields['config_pdf']          = $query['pdf'] ?? NULL;
504    $fields['config_flash']        = $query['fla'] ?? NULL;
505    $fields['config_java']         = $query['java'] ?? NULL;
506    $fields['config_quicktime']    = $query['qt'] ?? NULL;
507    $fields['config_realplayer']   = $query['realp'] ?? NULL;
508    $fields['config_windowsmedia'] = $query['wma'] ?? NULL;
509    $fields['config_silverlight']  = $query['ag'] ?? NULL;
510    $fields['config_cookie']       = $query['cookie'] ?? NULL;
511  }
512
513  /**
514   * Set the performance fields.
515   */
516  protected function doPerformance(array &$fields, array $query) {
517    $fields['pf_network']        = $query['pf_net'] ?? NULL;
518    $fields['pf_server']         = $query['pf_srv'] ?? NULL;
519    $fields['pf_transfer']       = $query['pf_tfr'] ?? NULL;
520    $fields['pf_dom_processing'] = $query['pf_dm1'] ?? NULL;
521    $fields['pf_dom_complete']   = $query['pf_dm2'] ?? NULL;
522    $fields['pf_on_load']        = $query['pf_onl'] ?? NULL;
523
524    $fields['pf_total'] = ($fields['pf_network'] ?? 0)
525    + ($fields['pf_server'] ?? 0)
526    + ($fields['pf_transfer'] ?? 0)
527    + ($fields['pf_dom_processing'] ?? 0)
528    + ($fields['pf_dom_complete'] ?? 0)
529    + ($fields['pf_on_load'] ?? 0);
530  }
531
532  /**
533   * Set the visitor's local time field.
534   */
535  protected function doLocalTime(array &$fields, array $query) {
536    $hours = $query['h'] ?? NULL;
537    $minutes = $query['m'] ?? NULL;
538    $seconds = $query['s'] ?? NULL;
539
540    $has_null = is_null($hours) || is_null($minutes) || is_null($seconds);
541    if ($has_null) {
542      return NULL;
543    }
544
545    $time = $hours * 3600 + $minutes * 60 + $seconds;
546
547    $fields['localtime'] = $time;
548  }
549
550  /**
551   * Set the language fields.
552   */
553  protected function doLanguage(array &$fields, array $languages) {
554    if (empty($languages)) {
555      return NULL;
556    }
557
558    $language = $languages[0] ?? '';
559    $lang = explode('_', $language);
560    $fields['location_browser_lang'] = $lang[0];
561
562    // $fields['location_language'] = $language;
563  }
564
565  /**
566   * Set the location fields.
567   */
568  protected function doLocation(array &$fields, $ip_address, $languages) {
569    if (!empty($languages)) {
570      $language = $languages[0] ?? '';
571      $lang = explode('_', $language);
572      $country_code = strtoupper($lang[1] ?? '');
573      if ($this->location->isValidCountryCode($country_code)) {
574        $fields['location_country'] = $country_code;
575        $fields['location_continent'] = $this->location->getContinent($country_code);
576      }
577    }
578
579    if (!$this->geoip) {
580      return NULL;
581    }
582
583    /** @var \GeoIp2\Model\City|null $location */
584    $location = $this->geoip->city($ip_address);
585    if (!$location) {
586      return NULL;
587    }
588
589    $fields['location_continent'] = $location->continent->code;
590    $fields['location_country']   = $location->country->isoCode;
591    $fields['location_region']    = $location->subdivisions[0]->isoCode;
592    $fields['location_city']      = $location->city->names['en'];
593    $fields['location_latitude']  = $location->location->latitude;
594    $fields['location_longitude'] = $location->location->longitude;
595  }
596
597  /**
598   * Set the referer fields.
599   */
600  protected function doRefererFields(array &$fields, array $query) {
601
602    $referer = [];
603    $fields['referer_url'] = $query['urlref'] ?? '';
604    $url_ref_parts = parse_url($fields['referer_url']);
605    $fields['referer_name'] = $url_ref_parts['host'] ?? '';
606    $url_parts = parse_url($query['url'] ?? '');
607    $same_host = ($url_parts['host'] ?? '') == $fields['referer_name'];
608
609    if ($this->spamService->match($fields['referer_name'])) {
610      $referer['referer_type'] = 'spam';
611    }
612    elseif ($data = $this->campaignService->parse($fields['referer_url'])) {
613      $referer['referer_type'] = 'campaign';
614      $referer['referer_name'] = $data['campaign'] ?? '';
615      $referer['referer_keyword'] = $data['keyword'] ?? '';
616    }
617    elseif (empty($fields['referer_url'])) {
618      $referer['referer_type'] = 'direct';
619    }
620    elseif ($same_host) {
621      $referer['referer_type'] = 'internal';
622    }
623    elseif ($referer_name = $this->socialNetworksService->match($fields['referer_name'])) {
624      $referer['referer_type'] = 'social_network';
625      $referer['referer_name'] = $referer_name;
626    }
627    elseif ($referer_name = $this->aiAssistantsService->match($fields['referer_name'])) {
628      $referer['referer_type'] = 'ai_assistant';
629      $referer['referer_name'] = $referer_name;
630    }
631    elseif ($data = $this->searchEngine->match($fields['referer_url'])) {
632      $referer['referer_type'] = 'search_engine';
633      $referer['referer_name'] = $data['name'];
634      $referer['referer_keyword'] = $data['keyword'];
635    }
636    else {
637      $referer['referer_type'] = 'website';
638    }
639
640    $fields = array_merge($fields, $referer);
641
642  }
643
644}