Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
141 / 141
100.00% covered (success)
100.00%
16 / 16
CRAP
100.00% covered (success)
100.00%
1 / 1
Visitors
100.00% covered (success)
100.00%
141 / 141
100.00% covered (success)
100.00%
16 / 16
36
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 create
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 track
100.00% covered (success)
100.00%
30 / 30
100.00% covered (success)
100.00%
1 / 1
3
 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%
2 / 2
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%
15 / 15
100.00% covered (success)
100.00%
1 / 1
6
 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
 doTime
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 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
1<?php
2
3namespace Drupal\visitors\Controller;
4
5use Drupal\Core\Controller\ControllerBase;
6use Drupal\visitors\VisitorsTrackerInterface;
7use Symfony\Component\DependencyInjection\ContainerInterface;
8use Symfony\Component\HttpFoundation\Response;
9use Drupal\visitors_geoip\VisitorsGeoIpInterface;
10use Drupal\visitors\VisitorsCounterInterface;
11use Drupal\visitors\VisitorsCookieInterface;
12use Drupal\visitors\VisitorsDeviceInterface;
13use Drupal\visitors\VisitorsLocationInterface;
14use Drupal\Component\Datetime\TimeInterface;
15use Drupal\Core\Config\ConfigFactoryInterface;
16use Psr\Log\LoggerInterface;
17use Symfony\Component\HttpFoundation\Request;
18use Symfony\Component\HttpFoundation\ServerBag;
19
20/**
21 * Visitors tracking controller.
22 */
23final class Visitors extends ControllerBase {
24
25  /**
26   * The time service.
27   *
28   * @var \Drupal\Component\Datetime\TimeInterface
29   */
30  protected $time;
31
32  /**
33   * The visitors settings.
34   *
35   * @var \Drupal\Core\Config\Config
36   */
37  protected $settings;
38
39  /**
40   * The logger service.
41   *
42   * @var \Psr\Log\LoggerInterface
43   */
44  protected $logger;
45
46  /**
47   * The counter service.
48   *
49   * @var \Drupal\visitors\VisitorsCounterInterface
50   */
51  protected $counter;
52
53  /**
54   * The cookie service.
55   *
56   * @var \Drupal\visitors\VisitorsCookieInterface
57   */
58  protected $cookie;
59
60  /**
61   * The device service.
62   *
63   * @var \Drupal\visitors\VisitorsDeviceInterface
64   */
65  protected $device;
66
67  /**
68   * The location service.
69   *
70   * @var \Drupal\visitors\VisitorsLocationInterface
71   */
72  protected $location;
73
74  /**
75   * The tracker service.
76   *
77   * @var \Drupal\visitors\VisitorsTrackerInterface
78   */
79  protected $tracker;
80
81  /**
82   * The geoip service.
83   *
84   * @var \Drupal\visitors_geoip\VisitorsGeoIpInterface|null
85   */
86  protected $geoip;
87
88  /**
89   * Visitor tracker.
90   *
91   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
92   *   The config factory service.
93   * @param \Drupal\Component\Datetime\TimeInterface $time
94   *   The time service.
95   * @param \Psr\Log\LoggerInterface $logger
96   *   The logger service.
97   * @param \Drupal\visitors\VisitorsCounterInterface $counter
98   *   The counter service.
99   * @param \Drupal\visitors\VisitorsCookieInterface $cookie
100   *   The cookie service.
101   * @param \Drupal\visitors\VisitorsDeviceInterface $device
102   *   The device service.
103   * @param \Drupal\visitors\VisitorsLocationInterface $location
104   *   The location service.
105   * @param \Drupal\visitors\VisitorsTrackerInterface $tracker
106   *   The date service.
107   * @param \Drupal\visitors_geoip\VisitorsGeoIpInterface|null $geoip
108   *   The geoip service.
109   */
110  public function __construct(
111    ConfigFactoryInterface $config_factory,
112    TimeInterface $time,
113    LoggerInterface $logger,
114    VisitorsCounterInterface $counter,
115    VisitorsCookieInterface $cookie,
116    VisitorsDeviceInterface $device,
117    VisitorsLocationInterface $location,
118    VisitorsTrackerInterface $tracker,
119    ?VisitorsGeoIpInterface $geoip = NULL,
120  ) {
121
122    $this->settings = $config_factory->get('visitors.config');
123
124    $this->time = $time;
125    $this->logger = $logger;
126    $this->counter = $counter;
127    $this->cookie = $cookie;
128    $this->device = $device;
129    $this->location = $location;
130    $this->tracker = $tracker;
131    $this->geoip = $geoip;
132  }
133
134  /**
135   * {@inheritdoc}
136   */
137  public static function create(ContainerInterface $container): Visitors {
138    return new self(
139      $container->get('config.factory'),
140      $container->get('datetime.time'),
141      $container->get('logger.channel.visitors'),
142      $container->get('visitors.counter'),
143      $container->get('visitors.cookie'),
144      $container->get('visitors.device'),
145      $container->get('visitors.location'),
146      $container->get('visitors.tracker'),
147      $container->get('visitors_geoip.lookup', ContainerInterface::NULL_ON_INVALID_REFERENCE),
148    );
149
150  }
151
152  /**
153   * Tracks visits.
154   */
155  public function track(Request $request): Response {
156    $response = new Response();
157    $response->setStatusCode(Response::HTTP_NO_CONTENT);
158
159    $server = $request->server;
160    $query = $request->query->all();
161
162    $fields = $this->getDefaultFields();
163
164    $ip = $request->getClientIp();
165    $fields['visitors_ip'] = $ip;
166    $fields['visitors_uid'] = $query['uid'] ?? 0;
167    $fields['visitors_title'] = $query['action_name'] ?? '';
168    $fields['visitors_user_agent'] = $server->get('HTTP_USER_AGENT', '');
169
170    $bot_retention_log = $this->settings->get('bot_retention_log');
171    $discard_bot = ($bot_retention_log == -1);
172
173    $this->doDeviceDetect($fields, $server);
174    if ($discard_bot && $fields['bot']) {
175      return $response;
176    }
177
178    $this->doVisitorId($fields, $query);
179    $this->doUrl($fields, $query);
180    $this->doReferrer($fields, $query);
181
182    $cvar = $query['_cvar'] ?? NULL;
183    $this->doCustom($fields, $cvar);
184
185    $this->doCounter($fields, $cvar);
186
187    $this->doConfig($fields, $query);
188    $this->doPerformance($fields, $query);
189
190    $this->doLocalTime($fields, $query);
191    $this->doTime($fields);
192
193    $languages = $request->getLanguages() ?? [];
194    $this->doLanguage($fields, $languages);
195    $this->doLocation($fields, $ip, $languages);
196
197    // Write fields to database.
198    $this->tracker->writeLog($fields);
199
200    return $response;
201  }
202
203  /**
204   * Get the default fields.
205   *
206   * @return array
207   *   The default fields.
208   */
209  protected function getDefaultFields(): array {
210    $fields = [
211      'bot' => 0,
212    ];
213
214    return $fields;
215  }
216
217  /**
218   * Detects the visitor url.
219   *
220   * @param string[] $fields
221   *   The fields array.
222   * @param string[] $query
223   *   The query array.
224   */
225  protected function doUrl(array &$fields, array $query) {
226    $url = $query['url'] ?? '';
227    $fields['visitors_url'] = $url;
228  }
229
230  /**
231   * Detects the visitor id.
232   *
233   * @param string[] $fields
234   *   The fields array.
235   * @param string[] $query
236   *   The query array.
237   */
238  protected function doVisitorId(array &$fields, array $query) {
239    $visitor_id = $query['_id'] ?? $this->cookie->getId();
240    $fields['visitor_id'] = $visitor_id;
241  }
242
243  /**
244   * Detects the referrer.
245   *
246   * @param string[] $fields
247   *   The fields array.
248   * @param string[] $query
249   *   The query array.
250   */
251  protected function doReferrer(array &$fields, array $query) {
252    $referrer = $query['urlref'] ?? '';
253    $fields['visitors_referer'] = $referrer;
254  }
255
256  /**
257   * Detects the device.
258   *
259   * @param string[] $fields
260   *   The fields array.
261   * @param \Symfony\Component\HttpFoundation\ServerBag $server
262   *   The server array.
263   */
264  protected function doDeviceDetect(array &$fields, ServerBag $server) {
265    if (!$this->device->hasLibrary()) {
266      return NULL;
267    }
268
269    $user_agent = $server->get('HTTP_USER_AGENT', '');
270    $this->device->doDeviceFields($fields, $user_agent, $server->all());
271
272  }
273
274  /**
275   * Set the fields with data in the custom variable.
276   */
277  protected function doCustom(array &$fields, $cvar = NULL) {
278    $path = '';
279    $route = '';
280    $server = NULL;
281
282    if (!is_null($cvar)) {
283      $custom = json_decode($cvar);
284      foreach ($custom as $c) {
285        if ($c[0] == 'path') {
286          $path = $c[1];
287        }
288        if ($c[0] == 'route') {
289          $route = $c[1];
290        }
291        if ($c[0] == 'server') {
292          $server = $c[1];
293        }
294      }
295    }
296
297    $fields['visitors_path'] = $path;
298    $fields['route'] = $route;
299    $fields['server'] = $server;
300
301  }
302
303  /**
304   * Record the view of the entity.
305   */
306  protected function doCounter(array &$fields, $cvar = NULL) {
307
308    $viewed = NULL;
309    if (!is_null($cvar)) {
310      $custom = json_decode($cvar);
311      foreach ($custom as $c) {
312        if ($c[0] == 'viewed') {
313          $viewed = $c[1];
314        }
315      }
316    }
317
318    if (!is_null($viewed)) {
319      [$type, $id] = explode(':', $viewed);
320      $this->counter->recordView($type, $id);
321    }
322  }
323
324  /**
325   * Set the configuration fields.
326   */
327  protected function doConfig(array &$fields, array $query) {
328
329    $fields['config_resolution']   = $query['res'] ?? NULL;
330    $fields['config_pdf']          = $query['pdf'] ?? NULL;
331    $fields['config_flash']        = $query['fla'] ?? NULL;
332    $fields['config_java']         = $query['java'] ?? NULL;
333    $fields['config_quicktime']    = $query['qt'] ?? NULL;
334    $fields['config_realplayer']   = $query['realp'] ?? NULL;
335    $fields['config_windowsmedia'] = $query['wma'] ?? NULL;
336    $fields['config_silverlight']  = $query['ag'] ?? NULL;
337    $fields['config_cookie']       = $query['cookie'] ?? NULL;
338  }
339
340  /**
341   * Set the performance fields.
342   */
343  protected function doPerformance(array &$fields, array $query) {
344    $fields['pf_network']        = $query['pf_net'] ?? NULL;
345    $fields['pf_server']         = $query['pf_srv'] ?? NULL;
346    $fields['pf_transfer']       = $query['pf_tfr'] ?? NULL;
347    $fields['pf_dom_processing'] = $query['pf_dm1'] ?? NULL;
348    $fields['pf_dom_complete']   = $query['pf_dm2'] ?? NULL;
349    $fields['pf_on_load']        = $query['pf_onl'] ?? NULL;
350
351    $fields['pf_total'] = ($fields['pf_network'] ?? 0)
352    + ($fields['pf_server'] ?? 0)
353    + ($fields['pf_transfer'] ?? 0)
354    + ($fields['pf_dom_processing'] ?? 0)
355    + ($fields['pf_dom_complete'] ?? 0)
356    + ($fields['pf_on_load'] ?? 0);
357  }
358
359  /**
360   * Set the visitor's local time field.
361   */
362  protected function doLocalTime(array &$fields, array $query) {
363    $hours = $query['h'] ?? NULL;
364    $minutes = $query['m'] ?? NULL;
365    $seconds = $query['s'] ?? NULL;
366
367    $has_null = is_null($hours) || is_null($minutes) || is_null($seconds);
368    if ($has_null) {
369      return NULL;
370    }
371
372    $time = $hours * 3600 + $minutes * 60 + $seconds;
373
374    $fields['visitor_localtime'] = $time;
375  }
376
377  /**
378   * Set the server time field.
379   */
380  protected function doTime(array &$fields) {
381    $fields['visitors_date_time'] = $this->time->getRequestTime();
382  }
383
384  /**
385   * Set the language fields.
386   */
387  protected function doLanguage(array &$fields, array $languages) {
388    if (empty($languages)) {
389      return NULL;
390    }
391
392    $language = $languages[0] ?? '';
393    $lang = explode('_', $language);
394    $fields['language'] = $lang[0];
395  }
396
397  /**
398   * Set the location fields.
399   */
400  protected function doLocation(array &$fields, $ip_address, $languages) {
401    if (!empty($languages)) {
402      $language = $languages[0] ?? '';
403      $lang = explode('_', $language);
404      $country_code = strtoupper($lang[1] ?? '');
405      if ($this->location->isValidCountryCode($country_code)) {
406        $fields['location_country'] = $country_code;
407        $fields['location_continent'] = $this->location->getContinent($country_code);
408      }
409    }
410
411    if (!$this->geoip) {
412      return NULL;
413    }
414
415    /** @var \GeoIp2\Model\City|null $location */
416    $location = $this->geoip->city($ip_address);
417    if (!$location) {
418      return NULL;
419    }
420
421    $fields['location_continent'] = $location->continent->code;
422    $fields['location_country']   = $location->country->isoCode;
423    $fields['location_region']    = $location->subdivisions[0]->isoCode;
424    $fields['location_city']      = $location->city->names['en'];
425    $fields['location_latitude']  = $location->location->latitude;
426    $fields['location_longitude'] = $location->location->longitude;
427
428  }
429
430}