Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
235 / 235
100.00% covered (success)
100.00%
19 / 19
CRAP
100.00% covered (success)
100.00%
1 / 1
VisitorsController
100.00% covered (success)
100.00%
235 / 235
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%
45 / 45
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['page_count'] = 0;
241    $visit['returning'] = 0;
242    $visit['visit_count'] = 0;
243
244    $log['title'] = $query['action_name'] ?? '';
245    $log['page_view'] = $query['pv_id'] ?? NULL;
246    $log['created'] = $request_time;
247
248    $bot_retention_log = $this->settings->get('bot_retention_log');
249    $discard_bot = ($bot_retention_log == -1);
250    $this->doDeviceDetect($visit, $server);
251    if ($discard_bot && $visit['bot']) {
252      return $response;
253    }
254
255    $this->doVisitorId($visit, $query);
256    $this->doUrl($log, $query);
257    $this->doReferrer($log, $query);
258
259    $custom_page_var = $query['cvar'] ?? NULL;
260    $this->doCustom($log, $custom_page_var);
261
262    $custom_event_var = $query['e_cvar'] ?? NULL;
263    $this->doEvent($log, $custom_event_var);
264
265    $this->doCounter($custom_page_var);
266
267    $this->doConfig($visit, $query);
268    $this->doPerformance($log, $query);
269
270    $this->doLocalTime($visit, $query);
271
272    $languages = $request->getLanguages() ?? [];
273    $this->doLanguage($visit, $languages);
274    $this->doLocation($visit, $ip, $languages);
275
276    $this->doRefererFields($visit, $query);
277
278    $visit_id = $this->tracker->getVisitId($visit, $request_time);
279    $log['visit_id'] = $visit_id;
280
281    // Write fields to database.
282    $log_id = $this->tracker->writeLog($log);
283
284    $uid = $visit['uid'] ?? NULL;
285    $this->tracker->updateVisit($visit_id, $log_id, $request_time, $uid);
286
287    return $response;
288  }
289
290  /**
291   * Get the response.
292   *
293   * @param bool $send_image
294   *   Whether to send the image.
295   *
296   * @return \Symfony\Component\HttpFoundation\Response
297   *   The response.
298   */
299  protected function getResponse(bool $send_image): Response {
300    $headers = [
301      'Cache-Control' => 'no-cache, no-store, must-revalidate',
302      'Pragma' => 'no-cache',
303      'Expires' => '0',
304    ];
305    $content = '';
306    if ($send_image) {
307      $content = $this->getImageContent();
308      $headers['Content-Type'] = 'image/gif';
309      $headers['Content-Length'] = strlen($content);
310    }
311
312    $response = new Response(
313      $content,
314      ($send_image) ? Response::HTTP_OK : Response::HTTP_NO_CONTENT,
315      $headers,
316    );
317
318    return $response;
319  }
320
321  /**
322   * Get the image content.
323   *
324   * @return string
325   *   The image content.
326   */
327  protected function getImageContent(): string {
328    return hex2bin('47494638396101000100800000000000FFFFFF21F9040100000000002C00000000010001000002024401003B');
329  }
330
331  /**
332   * Get the default fields.
333   *
334   * @return array
335   *   The default fields.
336   */
337  protected function getDefaultFields(): array {
338    $fields = [
339      'bot' => 0,
340    ];
341
342    return $fields;
343  }
344
345  /**
346   * Detects the visitor url.
347   *
348   * @param string[] $fields
349   *   The fields array.
350   * @param string[] $query
351   *   The query array.
352   */
353  protected function doUrl(array &$fields, array $query) {
354    $url = $query['url'] ?? '';
355
356    $visitors_url = new VisitorsUrl($url);
357
358    $fields['url'] = $visitors_url->getUrl();
359    $fields['url_prefix'] = $visitors_url->getPrefix();
360  }
361
362  /**
363   * Detects the visitor id.
364   *
365   * @param string[] $fields
366   *   The fields array.
367   * @param string[] $query
368   *   The query array.
369   */
370  protected function doVisitorId(array &$fields, array $query) {
371    $visitor_id = $query['_id'] ?? $this->cookie->getId();
372    $fields['visitor_id'] = $visitor_id;
373  }
374
375  /**
376   * Detects the referrer.
377   *
378   * @param string[] $fields
379   *   The fields array.
380   * @param string[] $query
381   *   The query array.
382   */
383  protected function doReferrer(array &$fields, array $query) {
384    $referrer = $query['urlref'] ?? '';
385    $fields['referrer_url'] = $referrer;
386  }
387
388  /**
389   * Detects the device.
390   *
391   * @param string[] $fields
392   *   The fields array.
393   * @param \Symfony\Component\HttpFoundation\ServerBag $server
394   *   The server array.
395   */
396  protected function doDeviceDetect(array &$fields, ServerBag $server) {
397    if (!$this->device->hasLibrary()) {
398      return NULL;
399    }
400
401    $user_agent = $server->get('HTTP_USER_AGENT', '');
402    $this->device->doDeviceFields($fields, $user_agent, $server->all());
403
404  }
405
406  /**
407   * Set the fields with data in the custom variable.
408   */
409  protected function doCustom(array &$fields, $cvar = NULL) {
410    if (!is_null($cvar)) {
411      $custom = json_decode($cvar);
412      foreach ($custom as $c) {
413        switch ($c[0]) {
414          case 'path':
415            $fields['path'] = $c[1];
416            break;
417
418          case 'route':
419            $fields['route'] = $c[1];
420            break;
421
422          case 'server':
423            $fields['server'] = $c[1];
424            break;
425
426        }
427      }
428    }
429  }
430
431  /**
432   * Set the fields with data in the custom variable.
433   */
434  protected function doEvent(array &$fields, $cvar = NULL) {
435    if (!is_null($cvar)) {
436      $custom = json_decode($cvar);
437      foreach ($custom as $c) {
438        switch ($c[0]) {
439          case 'plugin':
440            $fields['plugin'] = $c[1];
441            break;
442
443          case 'event':
444            $fields['event'] = $c[1];
445            break;
446
447          case 'plugin_int_1':
448            $fields['plugin_int_1'] = $c[1];
449            break;
450
451          case 'plugin_int_2':
452            $fields['plugin_int_2'] = $c[1];
453            break;
454
455          case 'plugin_var_1':
456            $fields['plugin_var_1'] = $c[1];
457            break;
458
459          case 'plugin_var_2':
460            $fields['plugin_var_2'] = $c[1];
461            break;
462
463          case 'plugin_var_3':
464            $fields['plugin_var_3'] = $c[1];
465            break;
466
467          case 'plugin_var_4':
468            $fields['plugin_var_4'] = $c[1];
469            break;
470        }
471      }
472    }
473  }
474
475  /**
476   * Record the view of the entity.
477   */
478  protected function doCounter($cvar = NULL) {
479
480    $viewed = NULL;
481    if (!is_null($cvar)) {
482      $custom = json_decode($cvar);
483      foreach ($custom as $c) {
484        if ($c[0] == 'viewed') {
485          $viewed = $c[1];
486        }
487      }
488    }
489
490    if (!is_null($viewed)) {
491      [$type, $id] = explode(':', $viewed);
492      $this->counter->recordView($type, $id);
493    }
494  }
495
496  /**
497   * Set the configuration fields.
498   */
499  protected function doConfig(array &$fields, array $query) {
500
501    $fields['config_resolution']   = $query['res'] ?? NULL;
502    $fields['config_pdf']          = $query['pdf'] ?? NULL;
503    $fields['config_flash']        = $query['fla'] ?? NULL;
504    $fields['config_java']         = $query['java'] ?? NULL;
505    $fields['config_quicktime']    = $query['qt'] ?? NULL;
506    $fields['config_realplayer']   = $query['realp'] ?? NULL;
507    $fields['config_windowsmedia'] = $query['wma'] ?? NULL;
508    $fields['config_silverlight']  = $query['ag'] ?? NULL;
509    $fields['config_cookie']       = $query['cookie'] ?? NULL;
510  }
511
512  /**
513   * Set the performance fields.
514   */
515  protected function doPerformance(array &$fields, array $query) {
516    $fields['pf_network']        = $query['pf_net'] ?? NULL;
517    $fields['pf_server']         = $query['pf_srv'] ?? NULL;
518    $fields['pf_transfer']       = $query['pf_tfr'] ?? NULL;
519    $fields['pf_dom_processing'] = $query['pf_dm1'] ?? NULL;
520    $fields['pf_dom_complete']   = $query['pf_dm2'] ?? NULL;
521    $fields['pf_on_load']        = $query['pf_onl'] ?? NULL;
522
523    $fields['pf_total'] = ($fields['pf_network'] ?? 0)
524    + ($fields['pf_server'] ?? 0)
525    + ($fields['pf_transfer'] ?? 0)
526    + ($fields['pf_dom_processing'] ?? 0)
527    + ($fields['pf_dom_complete'] ?? 0)
528    + ($fields['pf_on_load'] ?? 0);
529  }
530
531  /**
532   * Set the visitor's local time field.
533   */
534  protected function doLocalTime(array &$fields, array $query) {
535    $hours = $query['h'] ?? NULL;
536    $minutes = $query['m'] ?? NULL;
537    $seconds = $query['s'] ?? NULL;
538
539    $has_null = is_null($hours) || is_null($minutes) || is_null($seconds);
540    if ($has_null) {
541      return NULL;
542    }
543
544    $time = $hours * 3600 + $minutes * 60 + $seconds;
545
546    $fields['localtime'] = $time;
547  }
548
549  /**
550   * Set the language fields.
551   */
552  protected function doLanguage(array &$fields, array $languages) {
553    if (empty($languages)) {
554      return NULL;
555    }
556
557    $language = $languages[0] ?? '';
558    $lang = explode('_', $language);
559    $fields['location_browser_lang'] = $lang[0];
560
561    // $fields['location_language'] = $language;
562  }
563
564  /**
565   * Set the location fields.
566   */
567  protected function doLocation(array &$fields, $ip_address, $languages) {
568    if (!empty($languages)) {
569      $language = $languages[0] ?? '';
570      $lang = explode('_', $language);
571      $country_code = strtoupper($lang[1] ?? '');
572      if ($this->location->isValidCountryCode($country_code)) {
573        $fields['location_country'] = $country_code;
574        $fields['location_continent'] = $this->location->getContinent($country_code);
575      }
576    }
577
578    if (!$this->geoip) {
579      return NULL;
580    }
581
582    /** @var \GeoIp2\Model\City|null $location */
583    $location = $this->geoip->city($ip_address);
584    if (!$location) {
585      return NULL;
586    }
587
588    $fields['location_continent'] = $location->continent->code;
589    $fields['location_country']   = $location->country->isoCode;
590    $fields['location_region']    = $location->subdivisions[0]->isoCode;
591    $fields['location_city']      = $location->city->names['en'];
592    $fields['location_latitude']  = $location->location->latitude;
593    $fields['location_longitude'] = $location->location->longitude;
594  }
595
596  /**
597   * Set the referer fields.
598   */
599  protected function doRefererFields(array &$fields, array $query) {
600
601    $referer = [];
602    $fields['referer_url'] = $query['urlref'] ?? '';
603    $url_ref_parts = parse_url($fields['referer_url']);
604    $fields['referer_name'] = $url_ref_parts['host'] ?? '';
605    $url_parts = parse_url($query['url'] ?? '');
606    $same_host = ($url_parts['host'] ?? '') == $fields['referer_name'];
607
608    if ($this->spamService->match($fields['referer_name'])) {
609      $referer['referer_type'] = 'spam';
610    }
611    elseif ($data = $this->campaignService->parse($fields['referer_url'])) {
612      $referer['referer_type'] = 'campaign';
613      $referer['referer_name'] = $data['campaign'] ?? '';
614      $referer['referer_keyword'] = $data['keyword'] ?? '';
615    }
616    elseif (empty($fields['referer_url'])) {
617      $referer['referer_type'] = 'direct';
618    }
619    elseif ($same_host) {
620      $referer['referer_type'] = 'internal';
621    }
622    elseif ($referer_name = $this->socialNetworksService->match($fields['referer_name'])) {
623      $referer['referer_type'] = 'social_network';
624      $referer['referer_name'] = $referer_name;
625    }
626    elseif ($referer_name = $this->aiAssistantsService->match($fields['referer_name'])) {
627      $referer['referer_type'] = 'ai_assistant';
628      $referer['referer_name'] = $referer_name;
629    }
630    elseif ($data = $this->searchEngine->match($fields['referer_url'])) {
631      $referer['referer_type'] = 'search_engine';
632      $referer['referer_name'] = $data['name'];
633      $referer['referer_keyword'] = $data['keyword'];
634    }
635    else {
636      $referer['referer_type'] = 'website';
637    }
638
639    $fields = array_merge($fields, $referer);
640
641  }
642
643}