Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
135 / 135
100.00% covered (success)
100.00%
16 / 16
CRAP
100.00% covered (success)
100.00%
1 / 1
Visitors
100.00% covered (success)
100.00%
135 / 135
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%
9 / 9
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    $custom_page_var = $query['cvar'] ?? NULL;
183    $this->doCustom($fields, $custom_page_var);
184
185    $this->doCounter($fields, $custom_page_var);
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
279    if (!is_null($cvar)) {
280      $custom = json_decode($cvar);
281      foreach ($custom as $c) {
282        if ($c[0] == 'path') {
283          $fields['visitors_path'] = $c[1];
284        }
285        if ($c[0] == 'route') {
286          $fields['route'] = $c[1];
287        }
288        if ($c[0] == 'server') {
289          $fields['server'] = $c[1];
290        }
291      }
292    }
293
294  }
295
296  /**
297   * Record the view of the entity.
298   */
299  protected function doCounter(array &$fields, $cvar = NULL) {
300
301    $viewed = NULL;
302    if (!is_null($cvar)) {
303      $custom = json_decode($cvar);
304      foreach ($custom as $c) {
305        if ($c[0] == 'viewed') {
306          $viewed = $c[1];
307        }
308      }
309    }
310
311    if (!is_null($viewed)) {
312      [$type, $id] = explode(':', $viewed);
313      $this->counter->recordView($type, $id);
314    }
315  }
316
317  /**
318   * Set the configuration fields.
319   */
320  protected function doConfig(array &$fields, array $query) {
321
322    $fields['config_resolution']   = $query['res'] ?? NULL;
323    $fields['config_pdf']          = $query['pdf'] ?? NULL;
324    $fields['config_flash']        = $query['fla'] ?? NULL;
325    $fields['config_java']         = $query['java'] ?? NULL;
326    $fields['config_quicktime']    = $query['qt'] ?? NULL;
327    $fields['config_realplayer']   = $query['realp'] ?? NULL;
328    $fields['config_windowsmedia'] = $query['wma'] ?? NULL;
329    $fields['config_silverlight']  = $query['ag'] ?? NULL;
330    $fields['config_cookie']       = $query['cookie'] ?? NULL;
331  }
332
333  /**
334   * Set the performance fields.
335   */
336  protected function doPerformance(array &$fields, array $query) {
337    $fields['pf_network']        = $query['pf_net'] ?? NULL;
338    $fields['pf_server']         = $query['pf_srv'] ?? NULL;
339    $fields['pf_transfer']       = $query['pf_tfr'] ?? NULL;
340    $fields['pf_dom_processing'] = $query['pf_dm1'] ?? NULL;
341    $fields['pf_dom_complete']   = $query['pf_dm2'] ?? NULL;
342    $fields['pf_on_load']        = $query['pf_onl'] ?? NULL;
343
344    $fields['pf_total'] = ($fields['pf_network'] ?? 0)
345    + ($fields['pf_server'] ?? 0)
346    + ($fields['pf_transfer'] ?? 0)
347    + ($fields['pf_dom_processing'] ?? 0)
348    + ($fields['pf_dom_complete'] ?? 0)
349    + ($fields['pf_on_load'] ?? 0);
350  }
351
352  /**
353   * Set the visitor's local time field.
354   */
355  protected function doLocalTime(array &$fields, array $query) {
356    $hours = $query['h'] ?? NULL;
357    $minutes = $query['m'] ?? NULL;
358    $seconds = $query['s'] ?? NULL;
359
360    $has_null = is_null($hours) || is_null($minutes) || is_null($seconds);
361    if ($has_null) {
362      return NULL;
363    }
364
365    $time = $hours * 3600 + $minutes * 60 + $seconds;
366
367    $fields['visitor_localtime'] = $time;
368  }
369
370  /**
371   * Set the server time field.
372   */
373  protected function doTime(array &$fields) {
374    $fields['visitors_date_time'] = $this->time->getRequestTime();
375  }
376
377  /**
378   * Set the language fields.
379   */
380  protected function doLanguage(array &$fields, array $languages) {
381    if (empty($languages)) {
382      return NULL;
383    }
384
385    $language = $languages[0] ?? '';
386    $lang = explode('_', $language);
387    $fields['language'] = $lang[0];
388  }
389
390  /**
391   * Set the location fields.
392   */
393  protected function doLocation(array &$fields, $ip_address, $languages) {
394    if (!empty($languages)) {
395      $language = $languages[0] ?? '';
396      $lang = explode('_', $language);
397      $country_code = strtoupper($lang[1] ?? '');
398      if ($this->location->isValidCountryCode($country_code)) {
399        $fields['location_country'] = $country_code;
400        $fields['location_continent'] = $this->location->getContinent($country_code);
401      }
402    }
403
404    if (!$this->geoip) {
405      return NULL;
406    }
407
408    /** @var \GeoIp2\Model\City|null $location */
409    $location = $this->geoip->city($ip_address);
410    if (!$location) {
411      return NULL;
412    }
413
414    $fields['location_continent'] = $location->continent->code;
415    $fields['location_country']   = $location->country->isoCode;
416    $fields['location_region']    = $location->subdivisions[0]->isoCode;
417    $fields['location_city']      = $location->city->names['en'];
418    $fields['location_latitude']  = $location->location->latitude;
419    $fields['location_longitude'] = $location->location->longitude;
420
421  }
422
423}