Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.33% covered (success)
97.33%
1274 / 1309
92.86% covered (success)
92.86%
13 / 14
CRAP
n/a
0 / 0
visitors_help
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
2
visitors_cron
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
visitors_page_attachments
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
visitors_form_user_form_alter
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
1 / 1
5
visitors_user_profile_form_submit
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
visitors_node_links_alter
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
4
visitors_entity_delete
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
visitors_ranking
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
3
visitors_views_data
100.00% covered (success)
100.00%
1131 / 1131
100.00% covered (success)
100.00%
1 / 1
7
visitors_token_info
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
2
visitors_tokens
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
12
visitors_form_alter
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
visitors_preprocess_html
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
5
visitors_preprocess_views_view
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
240
1<?php
2
3/**
4 * @file
5 * Logs visitors for your site.
6 */
7
8use Drupal\Core\Entity\ContentEntityInterface;
9use Drupal\Core\Entity\EntityInterface;
10use Drupal\Core\Form\FormStateInterface;
11use Drupal\Core\Render\BubbleableMetadata;
12use Drupal\Core\Url;
13use Drupal\node\NodeInterface;
14use Drupal\visitors\Plugin\VisitorsEvent\Form;
15use Drupal\visitors\VisitorsVisibilityInterface;
16
17/**
18 * Implements hook_help().
19 */
20function visitors_help($route_name, $route_match) {
21  switch ($route_name) {
22    case 'help.page.visitors':
23      $help = '<p><a href="https://git.drupalcode.org/project/visitors/-/commits/3.0.x"><img alt="coverage report" src="https://git.drupalcode.org/project/visitors/badges/3.0.x/coverage.svg" /></a> &nbsp;';
24      $help .= '<a href="https://git.drupalcode.org/project/visitors/-/commits/3.0.x"><img alt="pipeline status" src="https://git.drupalcode.org/project/visitors/badges/3.0.x/pipeline.svg" /></a> &nbsp;';
25      $help .= '<a href="https://www.drupal.org/project/visitors">Homepage</a> &nbsp;';
26      $help .= '<a href="https://www.drupal.org/project/issues/visitors?version=any_3.">Issues</a></p>';
27      $help .= '<p>'
28        . t('The Visitors module logs all visitors to your site and provides various statistics about them.')
29        . '</p>';
30
31      return [
32        '#title' => t('Visitors'),
33        'description' => [
34          '#markup' => $help,
35        ],
36      ];
37  }
38}
39
40/**
41 * Implements hook_cron().
42 */
43function visitors_cron(): void {
44  \Drupal::service('visitors.cron')->execute();
45}
46
47/**
48 * Implements hook_page_attachments().
49 */
50function visitors_page_attachments(array &$page) {
51  \Drupal::service('visitors.page_attachments')->pageAttachments($page);
52}
53
54/**
55 * Implements hook_form_FORM_ID_alter().
56 *
57 * Allow users to decide if tracking code will be added to pages or not.
58 */
59function visitors_form_user_form_alter(&$form, FormStateInterface $form_state) {
60
61  $config = \Drupal::config('visitors.settings');
62  $visibility_users = $config->get('visibility.user_account_mode');
63  if ($visibility_users == VisitorsVisibilityInterface::USER_NO_PERSONALIZATION) {
64    return;
65  }
66  /** @var \Drupal\user\AccountForm $user_form */
67  $user_form = $form_state->getFormObject();
68  /** @var \Drupal\user\UserInterface $account */
69  $account = $user_form->getEntity();
70
71  if (!$account->hasPermission('opt-out of visitors tracking')) {
72    return;
73  }
74
75  $account_data_visitors = \Drupal::service('user.data')->get('visitors', $account->id());
76
77  $form['visitors'] = [
78    '#type' => 'details',
79    '#title' => t('Visitors settings'),
80    '#weight' => 3,
81    '#open' => TRUE,
82  ];
83  $description = '';
84  switch ($visibility_users) {
85    case VisitorsVisibilityInterface::USER_OPT_OUT:
86      $description = t('Users are tracked by default, but you are able to opt out.');
87      break;
88
89    case VisitorsVisibilityInterface::USER_OPT_IN:
90      $description = t('Users are <em>not</em> tracked by default, but you are able to opt in.');
91      break;
92  }
93
94  $form['visitors']['user_account_users'] = [
95    '#type' => 'checkbox',
96    '#title' => t('Enable user tracking'),
97    '#description' => $description,
98    '#default_value' => $account_data_visitors['user_account_users'] ?? $visibility_users,
99  ];
100
101  // Custom submit handler.
102  $form['actions']['submit']['#submit'][] = 'visitors_user_profile_form_submit';
103
104}
105
106/**
107 * Submit callback for user profile form to save the Visitor setting.
108 */
109function visitors_user_profile_form_submit($form, FormStateInterface $form_state) {
110  if (!$form_state->hasValue('user_account_users')) {
111    return;
112  }
113  /** @var \Drupal\user\AccountForm $user_form */
114  $user_form = $form_state->getFormObject();
115  /** @var \Drupal\user\UserInterface $account */
116  $account = $user_form->getEntity();
117
118  $value = (int) $form_state->getValue('user_account_users');
119  \Drupal::service('user.data')
120    ->set('visitors', $account->id(), 'user_account_users', $value);
121}
122
123/**
124 * Implements hook_node_links_alter().
125 */
126function visitors_node_links_alter(array &$links, NodeInterface $entity, array &$context) {
127  if ($context['view_mode'] == 'rss') {
128    return NULL;
129  }
130  $links['#cache']['contexts'][] = 'user.permissions';
131  if (!\Drupal::currentUser()->hasPermission('view visitors counter')) {
132    return NULL;
133  }
134  $settings = \Drupal::config('visitors.settings');
135
136  $statistics = \Drupal::service('visitors.counter')->fetchView('node', $entity->id());
137  if ($statistics) {
138    $statistics_links['visitors_counter']['title'] = \Drupal::translation()
139      ->formatPlural($statistics->getTotalCount(), '1 view', '@count views');
140    $links['visitors'] = [
141      '#theme' => 'links__node__visitors',
142      '#links' => $statistics_links,
143      '#attributes' => ['class' => ['links', 'inline']],
144    ];
145  }
146  $links['#cache']['max-age'] = $settings->get('counter.display_max_age');
147
148}
149
150/**
151 * Implements hook_entity_delete().
152 */
153function visitors_entity_delete(EntityInterface $entity) {
154
155  $entity_id = $entity->id();
156  if (!is_int($entity_id)) {
157    return;
158  }
159  $entity_type = $entity->getEntityTypeId();
160
161  \Drupal::service('visitors.counter')
162    ->deleteViews($entity_type, $entity_id);
163}
164
165/**
166 * Implements hook_ranking().
167 */
168function visitors_ranking() {
169  $settings = \Drupal::config('visitors.settings');
170  $is_enabled_and_has_node_entity_type = $settings->get('counter.enabled')
171    && in_array('node', $settings->get('counter.entity_types'));
172  if ($is_enabled_and_has_node_entity_type) {
173    return [
174      'views' => [
175        'title' => t('Number of views'),
176        'join' => [
177          'type' => 'LEFT',
178          'table' => 'visitors_counter',
179          'alias' => 'visitors_counter',
180          'on' => "visitors_counter.entity_id = i.sid AND visitors_counter.entity_type = 'node'",
181        ],
182        // Inverse law that maps the highest view count on the site to 1 and 0
183        // to 0. Note that the ROUND here is necessary for PostgreSQL and SQLite
184        // in order to ensure that the :statistics_scale argument is treated as
185        // a numeric type, because the PostgreSQL PDO driver sometimes puts
186        // values in as strings instead of numbers in complex expressions like
187        // this.
188        'score' => '2.0 - 2.0 / (1.0 + visitors_counter.total * (ROUND(:statistics_scale, 4)))',
189        'arguments' => [':statistics_scale' => \Drupal::state()->get('visitors.node_counter_scale', 0)],
190      ],
191    ];
192  }
193}
194
195/**
196 * Implements hook_views_data().
197 */
198function visitors_views_data() {
199  $data = [];
200  $data['visitors_counter']['table']['group'] = t('Visitors');
201  $data['visitors_counter']['table']['base'] = [
202    'title' => t('Visitors Entity Counter'),
203    'help' => t('Visitors data from visitors DB table.'),
204  ];
205  $settings = \Drupal::config('visitors.settings');
206  $supported_entity_types = $settings->get('counter.entity_types') ?? [];
207  foreach (\Drupal::entityTypeManager()->getDefinitions() as $entity_type_id => $entity_type) {
208    $base_table = $entity_type->getBaseTable();
209    if (!in_array($entity_type_id, $supported_entity_types) || !$entity_type->entityClassImplements(ContentEntityInterface::class) || !$base_table) {
210      continue;
211    }
212
213    $base_table = $entity_type->getDataTable() ?: $base_table;
214    $args = ['@entity_type' => $entity_type_id];
215
216    // Multilingual properties are stored in data table.
217    if (!($table = $entity_type->getDataTable())) {
218      $table = $entity_type->getBaseTable();
219    }
220    $data[$base_table]['visitors_counter'] = [
221      'title' => t('Visitors @entity_type counter', $args),
222      'help' => t('Relate all visitor counts on the @entity_type.', $args),
223      'relationship' => [
224        'group' => t('Visitor Counters'),
225        'label' => t('Visitor counters'),
226        'base' => 'visitors_counter',
227        'base field' => 'entity_id',
228        'relationship field' => $entity_type->getKey('id'),
229        'id' => 'standard',
230        'extra' => [
231          [
232            'field' => 'entity_type',
233            'value' => $entity_type_id,
234          ],
235        ],
236      ],
237    ];
238
239    $data['visitors_counter']['table']['join'][$table] = [
240      'type' => 'LEFT',
241      'left_field' => $entity_type->getKey('id'),
242      'field' => 'entity_id',
243      'extra' => [
244        [
245          'field' => 'entity_type',
246          'value' => $entity_type_id,
247        ],
248      ],
249    ];
250
251  }
252
253  $data['visitors_counter']['total'] = [
254    'title' => t('Total views'),
255    'help' => t('The total number of times the node has been viewed.'),
256    'field' => [
257      'id' => 'visitors_numeric',
258      'click sortable' => TRUE,
259    ],
260    'filter' => [
261      'id' => 'numeric',
262    ],
263    'argument' => [
264      'id' => 'numeric',
265    ],
266    'sort' => [
267      'id' => 'standard',
268    ],
269  ];
270  $data['visitors_counter']['today'] = [
271    'title' => t('Views today'),
272    'help' => t('The total number of times the node has been viewed today.'),
273    'field' => [
274      'id' => 'visitors_numeric',
275      'click sortable' => TRUE,
276    ],
277    'filter' => [
278      'id' => 'numeric',
279    ],
280    'argument' => [
281      'id' => 'numeric',
282    ],
283    'sort' => [
284      'id' => 'standard',
285    ],
286  ];
287  $data['visitors_counter']['timestamp'] = [
288    'title' => t('Most recent visit'),
289    'help' => t('The most recent time the node has been viewed.'),
290    'field' => [
291      'id' => 'visitors_counter_timestamp',
292      'click sortable' => TRUE,
293    ],
294    'filter' => [
295      'id' => 'date',
296    ],
297    'argument' => [
298      'id' => 'date',
299    ],
300    'sort' => [
301      'id' => 'standard',
302    ],
303  ];
304
305  $data['visitors_visit']['table']['group'] = t('Visitors');
306  $data['visitors_visit']['table']['base'] = [
307    'title' => t('Visitors'),
308    'help' => t('Visitors data from visitors DB table.'),
309  ];
310
311  $data['visitors_visit']['id'] = [
312    'title' => t('Visit ID'),
313    'help' => t('Visitors visit ID.'),
314    'field' => [
315      'id' => 'numeric',
316    ],
317    'relationship' => [
318      'title' => t('Visitors Log'),
319      'help' => t('Log of the visitors entry.'),
320      'base' => 'visitors_event',
321      'base field' => 'visit_id',
322      'id' => 'standard',
323    ],
324    'sort' => [
325      'id' => 'standard',
326    ],
327    'filter' => [
328      'id' => 'numeric',
329    ],
330    'argument' => [
331      'id' => 'numeric',
332    ],
333  ];
334  $data['visitors_visit']['visitor_id'] = [
335    'title' => t('Unique visitor'),
336    'help' => t('A unique ID for the visitor.'),
337    'field' => [
338      'id' => 'standard',
339    ],
340    'filter' => [
341      'id' => 'string',
342    ],
343    'sort' => [
344      'id' => 'standard',
345    ],
346    'argument' => [
347      'id' => 'string',
348    ],
349  ];
350  $data['visitors_visit']['uid'] = [
351    'title' => t('Visit User'),
352    'help' => t('The user Id of the Visit.'),
353    'field' => [
354      'id' => 'standard',
355    ],
356    'relationship' => [
357      'title' => t('User'),
358      'help' => t('Relate visitor data to the user entity.'),
359      'base' => 'users_field_data',
360      'base field' => 'uid',
361      'id' => 'standard',
362    ],
363    'filter' => [
364      'id' => 'numeric',
365    ],
366    'argument' => [
367      'id' => 'numeric',
368    ],
369  ];
370  $data['visitors_visit']['localtime'] = [
371    'title' => t('Visitor Hour'),
372    'help' => t('The hour (client) of the visit.'),
373    'field' => [
374      'id' => 'visitors_local_hour',
375      'field' => 'localtime',
376    ],
377  ];
378  $data['visitors_visit']['returning'] = [
379    'title' => t('Returning Visitor'),
380    'help' => t('Indicates whether the visitor is a returning visitor.'),
381    'field' => [
382      'id' => 'boolean',
383    ],
384    'filter' => [
385      'id' => 'boolean',
386    // Optional: allows filtering on NULL values.
387      'allow empty' => TRUE,
388    ],
389    'argument' => [
390      'id' => 'boolean',
391    ],
392  ];
393  $data['visitors_visit']['total_visits'] = [
394    'title' => t('Total Visits'),
395    'help' => t('Total visits.'),
396    'field' => [
397      'id' => 'visitors_number_range',
398    ],
399    'sort' => [
400      'id' => 'visitors_number_range',
401    ],
402    'filter' => [
403      'id' => 'numeric',
404    ],
405    'argument' => [
406      'id' => 'numeric',
407    ],
408  ];
409  $data['visitors_visit']['total_page_views'] = [
410    'title' => t('Total Page Views'),
411    'help' => t('The number of pages viewed in the visit.'),
412    'field' => [
413      'id' => 'visitors_number_range',
414    ],
415    'sort' => [
416      'id' => 'visitors_number_range',
417    ],
418    'filter' => [
419      'id' => 'numeric',
420    ],
421    'argument' => [
422      'id' => 'numeric',
423    ],
424  ];
425  $data['visitors_visit']['total_events'] = [
426    'title' => t('Total Events'),
427    'help' => t('The number of events in the visit.'),
428    'field' => [
429      'id' => 'visitors_number_range',
430    ],
431    'sort' => [
432      'id' => 'visitors_number_range',
433    ],
434    'filter' => [
435      'id' => 'numeric',
436    ],
437    'argument' => [
438      'id' => 'numeric',
439    ],
440  ];
441  $data['visitors_visit']['bounce'] = [
442    'title' => t('Bounce'),
443    'help' => t('The visit is a bounce.'),
444    'field' => [
445      'id' => 'visitors_bounce',
446      // 'field' => 'total_events',
447    ],
448    'sort' => [
449      'id' => 'visitors_bounce_rate',
450    ],
451    'filter' => [
452      'id' => 'boolean',
453    ],
454    'argument' => [
455      'id' => 'boolean',
456    ],
457  ];
458  $data['visitors_visit']['bounce_rate'] = [
459    'title' => t('Bounce Rate'),
460    'help' => t('The bounce rate of the visit.'),
461    'field' => [
462      'id' => 'visitors_bounce_rate',
463      'field' => 'total_events',
464    ],
465    'sort' => [
466      'id' => 'visitors_bounce_rate',
467    ],
468    'filter' => [
469      'id' => 'numeric',
470    ],
471    'argument' => [
472      'id' => 'numeric',
473    ],
474  ];
475  $data['visitors_visit']['total_time'] = [
476    'title' => t('Total Time'),
477    'help' => t('The total time of the visit.'),
478    'field' => [
479      'id' => 'visitors_number_range',
480    ],
481    'sort' => [
482      'id' => 'visitors_number_range',
483    ],
484    'filter' => [
485      'id' => 'numeric',
486    ],
487    'argument' => [
488      'id' => 'numeric',
489    ],
490  ];
491  $data['visitors_visit']['entry'] = [
492    'title' => t('Entry page'),
493    'help' => t('The first page viewed in the visit.'),
494    'field' => [
495      'id' => 'numeric',
496    ],
497    'relationship' => [
498      'title' => t('Entry page'),
499      'help' => t('Page view Log.'),
500      'base' => 'visitors_event',
501      'base field' => 'id',
502      'id' => 'standard',
503    ],
504    'sort' => [
505      'id' => 'standard',
506    ],
507    'filter' => [
508      'id' => 'numeric',
509    ],
510    'argument' => [
511      'id' => 'numeric',
512    ],
513  ];
514  $data['visitors_visit']['entry_time'] = [
515    'title' => t('Entry Time'),
516    'help' => t('The timestamp of the page view.'),
517    'field' => [
518      'id' => 'date',
519      'click sortable' => TRUE,
520    ],
521  ];
522  $data['visitors_visit']['exit'] = [
523    'title' => t('Exit page'),
524    'help' => t('The last page viewed in the visit.'),
525    'field' => [
526      'id' => 'numeric',
527    ],
528    'relationship' => [
529      'title' => t('Exit page'),
530      'help' => t('Exit Page view.'),
531      'base' => 'visitors_event',
532      'base field' => 'id',
533      'id' => 'standard',
534    ],
535    'sort' => [
536      'id' => 'standard',
537    ],
538    'filter' => [
539      'id' => 'numeric',
540    ],
541    'argument' => [
542      'id' => 'numeric',
543    ],
544  ];
545  $data['visitors_visit']['exit_time'] = [
546    'title' => t('Exit Time'),
547    'help' => t('The timestamp of the page view.'),
548    'field' => [
549      'id' => 'date',
550      'click sortable' => TRUE,
551    ],
552  ];
553  $data['visitors_visit']['time_since_first'] = [
554    'title' => t('Time Since First Visit'),
555    'help' => t("Seconds since the visitor's first visit."),
556    'field' => [
557      'id' => 'visitors_number_range',
558      'click sortable' => TRUE,
559    ],
560    'sort' => [
561      'id' => 'visitors_number_range',
562    ],
563    'filter' => [
564      'id' => 'standard',
565    ],
566    'argument' => [
567      'id' => 'numeric',
568    ],
569  ];
570  $data['visitors_visit']['time_since_last'] = [
571    'title' => t('Time Since Last Visit'),
572    'help' => t("Seconds since the visitor's last visit."),
573    'field' => [
574      'id' => 'visitors_number_range',
575      'click sortable' => TRUE,
576    ],
577    'sort' => [
578      'id' => 'visitors_number_range',
579    ],
580    'filter' => [
581      'id' => 'standard',
582    ],
583    'argument' => [
584      'id' => 'numeric',
585    ],
586  ];
587  $data['visitors_visit']['visit_time'] = [
588    'title' => t('Visit Time'),
589    'help' => t('Visit time filter.'),
590    'filter' => [
591      'id' => 'visitors_visit_date',
592      'field' => 'exit_time',
593    ],
594  ];
595  $data['visitors_visit']['config_id'] = [
596    'title' => t('Config Id'),
597    'help' => t('Visitor config hash.'),
598    'field' => [
599      'id' => 'standard',
600    ],
601    'filter' => [
602      'id' => 'string',
603    ],
604    'sort' => [
605      'id' => 'standard',
606    ],
607    'argument' => [
608      'id' => 'string',
609    ],
610  ];
611  $data['visitors_visit']['config_resolution'] = [
612    'title' => t('Resolution'),
613    'help' => t("The visitor's screen resolution."),
614    'field' => [
615      'id' => 'standard',
616    ],
617    'filter' => [
618      'id' => 'string',
619    ],
620    'argument' => [
621      'id' => 'string',
622    ],
623  ];
624  $data['visitors_visit']['config_pdf'] = [
625    'title' => t('PDF Plugin'),
626    'help' => t("The visitor's browser supports PDFs."),
627    'field' => [
628      'id' => 'visitors_pdf',
629    ],
630    'filter' => [
631      'id' => 'boolean',
632    ],
633  ];
634  $data['visitors_visit']['config_flash'] = [
635    'title' => t('Flash Plugin'),
636    'help' => t("The visitor's browser supports Flash."),
637    'field' => [
638      'id' => 'visitors_flash',
639    ],
640    'filter' => [
641      'id' => 'boolean',
642    ],
643  ];
644  $data['visitors_visit']['config_java'] = [
645    'title' => t('Java Plugin'),
646    'help' => t("The visitor's browser supports Java."),
647    'field' => [
648      'id' => 'visitors_java',
649    ],
650    'filter' => [
651      'id' => 'boolean',
652    ],
653  ];
654  $data['visitors_visit']['config_quicktime'] = [
655    'title' => t('Quicktime Plugin'),
656    'help' => t("The visitor's browser supports Quicktime."),
657    'field' => [
658      'id' => 'visitors_quicktime',
659    ],
660    'filter' => [
661      'id' => 'boolean',
662    ],
663  ];
664  $data['visitors_visit']['config_realplayer'] = [
665    'title' => t('Realplayer Plugin'),
666    'help' => t("The visitor's browser supports Realplayer."),
667    'field' => [
668      'id' => 'visitors_realplayer',
669    ],
670    'filter' => [
671      'id' => 'boolean',
672    ],
673  ];
674  $data['visitors_visit']['config_windowsmedia'] = [
675    'title' => t('Windows Media Plugin'),
676    'help' => t("The visitor's browser supports Windows Media."),
677    'field' => [
678      'id' => 'visitors_windowsmedia',
679    ],
680    'filter' => [
681      'id' => 'boolean',
682    ],
683  ];
684  $data['visitors_visit']['config_silverlight'] = [
685    'title' => t('Silverlight Plugin'),
686    'help' => t("The visitor's browser supports Silverlight."),
687    'field' => [
688      'id' => 'visitors_silverlight',
689    ],
690    'filter' => [
691      'id' => 'boolean',
692    ],
693  ];
694  $data['visitors_visit']['config_cookie'] = [
695    'title' => t('Cookie Plugin'),
696    'help' => t("The visitor's browser supports cookies."),
697    'field' => [
698      'id' => 'visitors_cookie',
699    ],
700    'filter' => [
701      'id' => 'boolean',
702    ],
703  ];
704  $data['visitors_visit']['config_browser_engine'] = [
705    'title' => t('Browser Engine'),
706    'help' => t('The engine used by the browser.'),
707    'field' => [
708      'id' => 'standard',
709    ],
710    'filter' => [
711      'id' => 'string',
712    ],
713    'argument' => [
714      'id' => 'string',
715    ],
716  ];
717  $data['visitors_visit']['config_browser_name'] = [
718    'title' => t('Browser Name'),
719    'help' => t('The name of the browser.'),
720    'field' => [
721      'id' => 'visitors_browser',
722    ],
723    'filter' => [
724      'id' => 'string',
725    ],
726    'argument' => [
727      'id' => 'string',
728    ],
729  ];
730  $data['visitors_visit']['config_browser_version'] = [
731    'title' => t('Browser Version'),
732    'help' => t('The version of the browser.'),
733    'field' => [
734      'id' => 'standard',
735    ],
736    'filter' => [
737      'id' => 'string',
738    ],
739    'argument' => [
740      'id' => 'string',
741    ],
742  ];
743  $data['visitors_visit']['config_client_type'] = [
744    'title' => t('Client type'),
745    'help' => t('The type of the client.'),
746    'field' => [
747      'id' => 'standard',
748    ],
749    'filter' => [
750      'id' => 'string',
751    ],
752    'argument' => [
753      'id' => 'string',
754    ],
755  ];
756  $data['visitors_visit']['config_device_brand'] = [
757    'title' => t('Device brand'),
758    'help' => t('The brand of the device.'),
759    'field' => [
760      'id' => 'visitors_brand',
761    ],
762    'filter' => [
763      'id' => 'string',
764    ],
765    'argument' => [
766      'id' => 'string',
767    ],
768  ];
769  $data['visitors_visit']['config_device_model'] = [
770    'title' => t('Device model'),
771    'help' => t('The model of the device.'),
772    'field' => [
773      'id' => 'standard',
774    ],
775    'filter' => [
776      'id' => 'string',
777    ],
778    'argument' => [
779      'id' => 'string',
780    ],
781  ];
782  $data['visitors_visit']['config_device_type'] = [
783    'title' => t('Device type'),
784    'help' => t('The type of device.'),
785    'field' => [
786      'id' => 'visitors_device',
787    ],
788    'filter' => [
789      'id' => 'string',
790    ],
791    'argument' => [
792      'id' => 'string',
793    ],
794  ];
795  $data['visitors_visit']['config_os'] = [
796    'title' => t('Operating System'),
797    'help' => t('The operating system.'),
798    'field' => [
799      'id' => 'visitors_operating_system',
800    ],
801    'filter' => [
802      'id' => 'string',
803    ],
804    'argument' => [
805      'id' => 'string',
806    ],
807  ];
808  $data['visitors_visit']['config_os_version'] = [
809    'title' => t('OS version'),
810    'help' => t('The version of the Operating System.'),
811    'field' => [
812      'id' => 'standard',
813    ],
814    'filter' => [
815      'id' => 'string',
816    ],
817    'argument' => [
818      'id' => 'string',
819    ],
820  ];
821  $data['visitors_visit']['bot'] = [
822    'title' => t('Bot'),
823    'help' => t("The visit is from a bot."),
824    'field' => [
825      'id' => 'boolean',
826    ],
827    'filter' => [
828      'id' => 'boolean',
829    ],
830    'argument' => [
831      'id' => 'numeric',
832    ],
833  ];
834  $data['visitors_visit']['location_browser_lang'] = [
835    'title' => t('Language'),
836    'help' => t('The browser language.'),
837    'field' => [
838      'id' => 'visitors_language',
839    ],
840    'filter' => [
841      'id' => 'string',
842    ],
843    'argument' => [
844      'id' => 'string',
845    ],
846  ];
847  $data['visitors_visit']['location_ip'] = [
848    'title' => t('Visitors IP'),
849    'help' => t('The IP of the visitors entry.'),
850    'field' => [
851      'id' => 'standard',
852    ],
853    'filter' => [
854      'id' => 'string',
855    ],
856    'argument' => [
857      'id' => 'string',
858    ],
859  ];
860  $data['visitors_visit']['location_continent'] = [
861    'title' => t('Continent'),
862    'help' => t('The location continent.'),
863    'field' => [
864      'id' => 'visitors_continent',
865    ],
866    'filter' => [
867      'id' => 'string',
868    ],
869    'argument' => [
870      'id' => 'string',
871    ],
872  ];
873  $data['visitors_visit']['location_country'] = [
874    'title' => t('Country'),
875    'help' => t('The location country.'),
876    'field' => [
877      'id' => 'visitors_country',
878    ],
879    'filter' => [
880      'id' => 'string',
881    ],
882    'argument' => [
883      'id' => 'string',
884    ],
885  ];
886  $data['visitors_visit']['location_region'] = [
887    'title' => t('Region'),
888    'help' => t('The region of the visitor.'),
889    'field' => [
890      'id' => 'standard',
891    ],
892    'filter' => [
893      'id' => 'string',
894    ],
895    'argument' => [
896      'id' => 'string',
897    ],
898  ];
899  $data['visitors_visit']['location_city'] = [
900    'title' => t('City'),
901    'help' => t('The city of the visitor.'),
902    'field' => [
903      'id' => 'standard',
904    ],
905    'filter' => [
906      'id' => 'string',
907    ],
908    'argument' => [
909      'id' => 'string',
910    ],
911  ];
912  $data['visitors_visit']['location_latitude'] = [
913    'title' => t('Location Latitude'),
914    'help' => t('The latitude derived from the IP address.'),
915    'field' => [
916      'id' => 'numeric',
917    ],
918    'filter' => [
919      'id' => 'numeric',
920    ],
921    'sort' => [
922      'id' => 'numeric',
923    ],
924  ];
925  $data['visitors_visit']['location_longitude'] = [
926    'title' => t('Location Longitude'),
927    'help' => t('The longitude derived from the IP address.'),
928    'field' => [
929      'id' => 'numeric',
930    ],
931    'filter' => [
932      'id' => 'numeric',
933    ],
934    'sort' => [
935      'id' => 'numeric',
936    ],
937  ];
938  $data['visitors_visit']['referer_type'] = [
939    'title' => t('Referer Type'),
940    'help' => t('The type of the referer.'),
941    'field' => [
942      'id' => 'standard',
943    ],
944    'filter' => [
945      'id' => 'string',
946    ],
947  ];
948  $data['visitors_visit']['referer_name'] = [
949    'title' => t('Referer Name'),
950    'help' => t('The name of the referer.'),
951    'field' => [
952      'id' => 'standard',
953    ],
954  ];
955  $data['visitors_visit']['referer_url'] = [
956    'title' => t('Referer URL'),
957    'help' => t('The URL of the referer.'),
958    'field' => [
959      'id' => 'standard',
960    ],
961  ];
962  $data['visitors_visit']['referer_keyword'] = [
963    'title' => t('Referer Keyword'),
964    'help' => t('The keyword of the referer.'),
965    'field' => [
966      'id' => 'standard',
967    ],
968  ];
969
970  $data['visitors_event']['table']['group'] = t('Visitors');
971  $data['visitors_event']['table']['base'] = [
972    'title' => t('Visitors Log'),
973    'help' => t('Visitors data from visitors DB table.'),
974  ];
975
976  $data['visitors_event']['id'] = [
977    'title' => t('Log Id'),
978    'help' => t('Visitors Log Id.'),
979    'field' => [
980      'id' => 'numeric',
981    ],
982    'sort' => [
983      'id' => 'standard',
984    ],
985    'filter' => [
986      'id' => 'numeric',
987    ],
988    'argument' => [
989      'id' => 'numeric',
990    ],
991  ];
992  $data['visitors_event']['page_view'] = [
993    'title' => t('Event Page View'),
994    'help' => t('Visitors Event Page View.'),
995    'field' => [
996      'id' => 'standard',
997    ],
998    'sort' => [
999      'id' => 'string',
1000    ],
1001    'filter' => [
1002      'id' => 'string',
1003    ],
1004    'argument' => [
1005      'id' => 'string',
1006    ],
1007  ];
1008  $data['visitors_event']['visit_id'] = [
1009    'title' => t('Event Id'),
1010    'help' => t('Visitors Event Id.'),
1011    'field' => [
1012      'id' => 'numeric',
1013    ],
1014    'relationship' => [
1015      'title' => t('Visitors Visit'),
1016      'help' => t('Visit of the Log.'),
1017      'base' => 'visitors_visit',
1018      'base field' => 'id',
1019      'id' => 'standard',
1020    ],
1021    'sort' => [
1022      'id' => 'standard',
1023    ],
1024    'filter' => [
1025      'id' => 'numeric',
1026    ],
1027    'argument' => [
1028      'id' => 'numeric',
1029    ],
1030  ];
1031  $data['visitors_event']['title'] = [
1032    'title' => t('Visitors title'),
1033    'help' => t('The title of the visitors entry.'),
1034    'field' => [
1035      'id' => 'standard',
1036    ],
1037    'filter' => [
1038      'id' => 'string',
1039    ],
1040  ];
1041  $data['visitors_event']['uid'] = [
1042    'title' => t('Visitors Event User'),
1043    'help' => t('The user ID of the event.'),
1044    'field' => [
1045      'id' => 'standard',
1046    ],
1047    'relationship' => [
1048      'title' => t('User'),
1049      'help' => t('The user entity from the visitor entry.'),
1050      'base' => 'users_field_data',
1051      'base field' => 'uid',
1052      'id' => 'standard',
1053    ],
1054    'filter' => [
1055      'id' => 'numeric',
1056    ],
1057    'argument' => [
1058      'id' => 'numeric',
1059    ],
1060  ];
1061  $data['visitors_event']['url_prefix'] = [
1062    'title' => t('URL Prefix'),
1063    'help' => t('The URL Prefix.'),
1064    'field' => [
1065      'id' => 'visitors_url_prefix',
1066    ],
1067  ];
1068  $data['visitors_event']['url'] = [
1069    'title' => t('Visitors URL'),
1070    'help' => t('The URL of the visitors entry.'),
1071    'field' => [
1072      'id' => 'standard',
1073    ],
1074    'filter' => [
1075      'id' => 'string',
1076    ],
1077  ];
1078
1079  $data['visitors_event']['path'] = [
1080    'title' => t('Visitors path'),
1081    'help' => t('The path of the visitors entry.'),
1082    'field' => [
1083      'id' => 'standard',
1084    ],
1085    'filter' => [
1086      'id' => 'string',
1087    ],
1088    'argument' => [
1089      'id' => 'string',
1090    ],
1091  ];
1092  $data['visitors_event']['route'] = [
1093    'title' => t('Route'),
1094    'help' => t('The route of the visitors entry.'),
1095    'field' => [
1096      'id' => 'standard',
1097    ],
1098    'filter' => [
1099      'id' => 'string',
1100    ],
1101    'argument' => [
1102      'id' => 'string',
1103    ],
1104  ];
1105  $data['visitors_event']['referrer_url'] = [
1106    'title' => t('Visitors referer'),
1107    'help' => t('The referer of the visitors entry.'),
1108    'field' => [
1109      'id' => 'standard',
1110    ],
1111    'filter' => [
1112      'id' => 'string',
1113    ],
1114  ];
1115  $data['visitors_event']['server'] = [
1116    'title' => t('Server'),
1117    'help' => t('The server that generated the response.'),
1118    'field' => [
1119      'id' => 'standard',
1120    ],
1121    'filter' => [
1122      'id' => 'string',
1123    ],
1124    'argument' => [
1125      'id' => 'string',
1126    ],
1127  ];
1128  $data['visitors_event']['pf_network'] = [
1129    'title' => t('Network'),
1130    'help' => t('Network performance.'),
1131    'field' => [
1132      'id' => 'numeric',
1133    ],
1134    'sort' => [
1135      'id' => 'standard',
1136    ],
1137    'filter' => [
1138      'id' => 'numeric',
1139    ],
1140    'argument' => [
1141      'id' => 'numeric',
1142    ],
1143  ];
1144  $data['visitors_event']['pf_server'] = [
1145    'title' => t('Server'),
1146    'help' => t('Server performance.'),
1147    'field' => [
1148      'id' => 'numeric',
1149    ],
1150    'sort' => [
1151      'id' => 'standard',
1152    ],
1153    'filter' => [
1154      'id' => 'numeric',
1155    ],
1156    'argument' => [
1157      'id' => 'numeric',
1158    ],
1159  ];
1160  $data['visitors_event']['pf_transfer'] = [
1161    'title' => t('Transfer'),
1162    'help' => t('Transfer performance.'),
1163    'field' => [
1164      'id' => 'numeric',
1165    ],
1166    'sort' => [
1167      'id' => 'standard',
1168    ],
1169    'filter' => [
1170      'id' => 'numeric',
1171    ],
1172    'argument' => [
1173      'id' => 'numeric',
1174    ],
1175  ];
1176  $data['visitors_event']['pf_dom_processing'] = [
1177    'title' => t('DOM Processing'),
1178    'help' => t('DOM Processing performance.'),
1179    'field' => [
1180      'id' => 'numeric',
1181    ],
1182    'sort' => [
1183      'id' => 'standard',
1184    ],
1185    'filter' => [
1186      'id' => 'numeric',
1187    ],
1188    'argument' => [
1189      'id' => 'numeric',
1190    ],
1191  ];
1192  $data['visitors_event']['pf_dom_complete'] = [
1193    'title' => t('DOM Complete'),
1194    'help' => t('DOM Complete performance.'),
1195    'field' => [
1196      'id' => 'numeric',
1197    ],
1198    'sort' => [
1199      'id' => 'standard',
1200    ],
1201    'filter' => [
1202      'id' => 'numeric',
1203    ],
1204    'argument' => [
1205      'id' => 'numeric',
1206    ],
1207  ];
1208  $data['visitors_event']['pf_on_load'] = [
1209    'title' => t('On Load'),
1210    'help' => t('On Load performance.'),
1211    'field' => [
1212      'id' => 'numeric',
1213    ],
1214    'sort' => [
1215      'id' => 'standard',
1216    ],
1217    'filter' => [
1218      'id' => 'numeric',
1219    ],
1220    'argument' => [
1221      'id' => 'numeric',
1222    ],
1223  ];
1224  $data['visitors_event']['pf_total'] = [
1225    'title' => t('Total'),
1226    'help' => t('Total performance.'),
1227    'field' => [
1228      'id' => 'numeric',
1229    ],
1230    'sort' => [
1231      'id' => 'standard',
1232    ],
1233    'filter' => [
1234      'id' => 'numeric',
1235    ],
1236    'argument' => [
1237      'id' => 'numeric',
1238    ],
1239  ];
1240  $data['visitors_event']['created'] = [
1241    'title' => t('Created'),
1242    'help' => t('The timestamp of the page view.'),
1243    'field' => [
1244      'id' => 'date',
1245      'click sortable' => TRUE,
1246    ],
1247    'sort' => [
1248      'id' => 'date',
1249    ],
1250    'filter' => [
1251      'id' => 'visitors_date',
1252    ],
1253  ];
1254  $data['visitors_event']['visitors_hour'] = [
1255    'title' => t('Hour'),
1256    'help' => t('The hour (server) of the visit.'),
1257    'field' => [
1258      'id' => 'visitors_hour',
1259      'field' => 'created',
1260    ],
1261    'sort' => [
1262      'id' => 'visitors_timestamp',
1263      'field' => 'created',
1264    ],
1265  ];
1266  $data['visitors_event']['visitors_month'] = [
1267    'title' => t('Month'),
1268    'help' => t('The month of the visit.'),
1269    'field' => [
1270      'id' => 'visitors_month',
1271      'field' => 'created',
1272    ],
1273    'sort' => [
1274      'id' => 'visitors_timestamp',
1275      'field' => 'created',
1276    ],
1277  ];
1278  $data['visitors_event']['visitors_local_hour'] = [
1279    'title' => t('Local hour'),
1280    'help' => t('Visitors local time.'),
1281    'field' => [
1282      'id' => 'visitors_local_hour',
1283      'field' => 'created',
1284    ],
1285    'sort' => [
1286      'id' => 'visitors_timestamp',
1287      'field' => 'created',
1288    ],
1289  ];
1290  $data['visitors_event']['visitors_day_of_week'] = [
1291    'title' => t('Day of Week'),
1292    'help' => t('The day of week of the visit.'),
1293    'field' => [
1294      'id' => 'visitors_day_of_week',
1295      'field' => 'created',
1296    ],
1297    'sort' => [
1298      'id' => 'visitors_timestamp',
1299      'field' => 'created',
1300    ],
1301  ];
1302  $data['visitors_event']['visitors_day_of_month'] = [
1303    'title' => t('Day of Month'),
1304    'help' => t('The day of month of the visit.'),
1305    'field' => [
1306      'id' => 'visitors_day_of_month',
1307      'field' => 'created',
1308    ],
1309    'sort' => [
1310      'id' => 'visitors_timestamp',
1311      'field' => 'created',
1312    ],
1313  ];
1314  $data['visitors_event']['visitors_day'] = [
1315    'title' => t('Day'),
1316    'help' => t('The day of the visit.'),
1317    'field' => [
1318      'id' => 'visitors_day',
1319      'field' => 'created',
1320    ],
1321    'sort' => [
1322      'id' => 'visitors_timestamp',
1323      'field' => 'created',
1324    ],
1325  ];
1326  $data['visitors_event']['visitors_week'] = [
1327    'title' => t('Week'),
1328    'help' => t('The week of the visit.'),
1329    'field' => [
1330      'id' => 'visitors_week',
1331      'field' => 'created',
1332    ],
1333    'sort' => [
1334      'id' => 'visitors_timestamp',
1335      'field' => 'created',
1336    ],
1337  ];
1338
1339  $data['visitors_visit']['visitors_display_link'] = [
1340    'title' => t('Link to Visitors display'),
1341    'help' => t('Displays a link to a non-path-based display of this view while keeping the filter criteria, sort criteria, pager settings and contextual filters.'),
1342    'area' => [
1343      'id' => 'visitors_display_link',
1344    ],
1345  ];
1346
1347  return $data;
1348}
1349
1350/**
1351 * Implements hook_token_info().
1352 */
1353function visitors_token_info() {
1354  $entity['total-count'] = [
1355    'name' => t("Number of views"),
1356    'description' => t("The number of visitors who have read the node."),
1357  ];
1358  $entity['day-count'] = [
1359    'name' => t("Views today"),
1360    'description' => t("The number of visitors who have read the node today."),
1361  ];
1362  $entity['last-view'] = [
1363    'name' => t("Last view"),
1364    'description' => t("The date on which a visitor last read the node."),
1365    'type' => 'date',
1366  ];
1367
1368  $token = [
1369    'tokens' => [],
1370  ];
1371  $entity_types = \Drupal::config('visitors.settings')
1372    ->get('counter.entity_types') ?? [];
1373  foreach ($entity_types as $entity_type) {
1374    $token['tokens'][$entity_type] = $entity;
1375  }
1376
1377  return $token;
1378}
1379
1380/**
1381 * Implements hook_tokens().
1382 */
1383function visitors_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
1384  $token_service = \Drupal::token();
1385  $entity_types = \Drupal::config('visitors.settings')
1386    ->get('counter.entity_types') ?? [];
1387  $replacements = [];
1388
1389  if (!in_array($type, $entity_types) || empty($data[$type])) {
1390    return $replacements;
1391  }
1392  $entity = $data[$type];
1393
1394  /** @var \Drupal\visitors\VisitorsCounterInterface $counter_storage */
1395  $counter_storage = \Drupal::service('visitors.counter');
1396
1397  $entity_id = $entity->id() ?? 0;
1398  $entity_view = $counter_storage->fetchView($type, $entity_id);
1399  foreach ($tokens as $name => $original) {
1400    if ($name == 'total-count') {
1401      $replacements[$original] = $entity_view ? $entity_view->getTotalCount() : 0;
1402    }
1403    elseif ($name == 'day-count') {
1404      $replacements[$original] = $entity_view ? $entity_view->getDayCount() : 0;
1405    }
1406    elseif ($name == 'last-view') {
1407      $replacements[$original] = $entity_view ? \Drupal::service('date.formatter')->format($entity_view->getTimestamp()) : t('never');
1408    }
1409  }
1410
1411  if ($created_tokens = $token_service->findWithPrefix($tokens, 'last-view')) {
1412    $replacements += $token_service->generate('date', $created_tokens, ['date' => $entity_view ? $entity_view->getTimestamp() : 0], $options, $bubbleable_metadata);
1413  }
1414
1415  return $replacements;
1416}
1417
1418/**
1419 * Implements hook_form_alter().
1420 */
1421function visitors_form_alter(&$form, FormStateInterface $form_state, $form_id) {
1422  $static = &drupal_static(__FUNCTION__);
1423
1424  $form_object = $form_state->getFormObject();
1425  if (method_exists($form_object, 'getBaseFormId')) {
1426    $base_form_id = $form_object->getBaseFormId();
1427  }
1428  else {
1429    $base_form_id = $form_id;
1430  }
1431
1432  $static[$form_id] = $base_form_id;
1433
1434  $form['#submit'][] = [Form::class, 'submit'];
1435  $form['#validate'][] = [Form::class, 'validate'];
1436}
1437
1438/**
1439 * Implements hook_preprocess_html().
1440 */
1441function visitors_preprocess_html(array &$variables) {
1442  $title = '';
1443  if (isset($variables['head_title']['title'])) {
1444    $title = strip_tags((string) $variables['head_title']['title']);
1445  }
1446  elseif (isset($variables['head_title']) && is_string($variables['head_title'])) {
1447    $title = strip_tags($variables['head_title']);
1448  }
1449
1450  if ($title) {
1451    $variables['#attached']['drupalSettings']['visitors']['title'] = $title;
1452  }
1453}
1454
1455/**
1456 * Implements hook_preprocess_views_view().
1457 */
1458function visitors_preprocess_views_view(array &$variables) {
1459  // Check if this is a Visitors view that needs sort link cleaning.
1460  $view = $variables['view'];
1461  if ($view->storage->id() !== 'visitors') {
1462    return;
1463  }
1464
1465  // Get excluded parameters from static variable set in ReportController.
1466  $excluded_params = &drupal_static('visitors_excluded_sort_params', []);
1467  $referrer_path = &drupal_static('visitors_referrer_path', '');
1468
1469  if (empty($excluded_params)) {
1470    return;
1471  }
1472
1473  $full_uri = \Drupal::request()->headers->get('referer');
1474
1475  if (isset($variables['exposed']['#action'])) {
1476    $variables['exposed']['#action'] = $full_uri;
1477  }
1478  // Clean sort links by removing AJAX parameters.
1479  if (!empty($variables['header'])) {
1480    $current_query = \Drupal::request()->query->all();
1481
1482    // Remove excluded parameters.
1483    $clean_query = [];
1484    foreach ($current_query as $key => $value) {
1485      if (!in_array($key, $excluded_params)) {
1486        $clean_query[$key] = $value;
1487      }
1488    }
1489
1490    // Rebuild sort links for each header column.
1491    foreach ($variables['header'] as $field => &$header) {
1492      if (!empty($header['url'])) {
1493        // Parse the existing URL to get the sort parameters.
1494        $url_parts = parse_url($header['url']);
1495        $url_query = [];
1496        if (!empty($url_parts['query'])) {
1497          parse_str($url_parts['query'], $url_query);
1498        }
1499
1500        // Keep only the sort-related parameters and clean query.
1501        $sort_params = [];
1502        if (isset($url_query['order'])) {
1503          $sort_params['order'] = $url_query['order'];
1504        }
1505        if (isset($url_query['sort'])) {
1506          $sort_params['sort'] = $url_query['sort'];
1507        }
1508
1509        // Merge clean query with sort parameters.
1510        $final_query = array_merge($clean_query, $sort_params);
1511
1512        // Rebuild the URL.
1513        $route_name = $view->live_preview ? '<current>' : '<none>';
1514        if (!empty($referrer_path)) {
1515          // Use the referrer path for the URL.
1516          $header['url'] = $referrer_path;
1517          if (!empty($final_query)) {
1518            $header['url'] .= '?' . http_build_query($final_query);
1519          }
1520        }
1521        else {
1522          // Fallback to Drupal URL generation.
1523          $url = new Url($route_name, [], ['query' => $final_query]);
1524          $header['url'] = $url->toString();
1525        }
1526      }
1527    }
1528  }
1529}